@onekeyfe/react-native-bottom-tabs 6.7.0-19 → 6.7.0-20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/onekey/bottomtabs/BottomTabsDrawerManager.kt +55 -0
- package/android/src/main/java/com/onekey/bottomtabs/BottomTabsDrawerView.kt +496 -0
- package/android/src/main/java/com/onekey/bottomtabs/BottomTabsPackage.kt +1 -1
- package/android/src/main/java/com/onekey/bottomtabs/BottomTabsViewManager.kt +3 -0
- package/docs/SPEC.md +163 -2
- package/ios/RCTBottomTabsComponentView.mm +37 -0
- package/ios/RCTBottomTabsContainerView.swift +192 -7
- package/ios/RCTBottomTabsDrawerComponentView.h +8 -0
- package/ios/RCTBottomTabsDrawerComponentView.mm +79 -0
- package/ios/RCTBottomTabsDrawerContainerView.swift +327 -0
- package/lib/module/BottomTabs.js +144 -87
- package/lib/module/BottomTabsDrawerNativeComponent.ts +19 -0
- package/lib/module/BottomTabsDrawerNativeComponent.web.js +13 -0
- package/lib/module/BottomTabsNativeComponent.ts +13 -0
- package/lib/typescript/src/BottomTabs.d.ts +36 -10
- package/lib/typescript/src/BottomTabsDrawerNativeComponent.d.ts +15 -0
- package/lib/typescript/src/BottomTabsDrawerNativeComponent.web.d.ts +4 -0
- package/lib/typescript/src/BottomTabsNativeComponent.d.ts +8 -0
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/types.d.ts +1 -0
- package/package.json +3 -2
- package/react-native.config.js +4 -1
- package/src/BottomTabs.tsx +235 -115
- package/src/BottomTabsDrawerNativeComponent.ts +19 -0
- package/src/BottomTabsDrawerNativeComponent.web.tsx +9 -0
- package/src/BottomTabsNativeComponent.ts +13 -0
- package/src/index.tsx +1 -1
- package/src/types.ts +3 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
/// Internal native host. React owns two fixed slots: menu, then foreground.
|
|
4
|
+
@objc(RCTBottomTabsDrawerContainerView)
|
|
5
|
+
final class RCTBottomTabsDrawerContainerView: UIView, UIGestureRecognizerDelegate {
|
|
6
|
+
@objc var enabled = false {
|
|
7
|
+
didSet {
|
|
8
|
+
guard enabled != oldValue else { return }
|
|
9
|
+
interruptGesture()
|
|
10
|
+
stopAnimation()
|
|
11
|
+
stableOpen = false
|
|
12
|
+
setProgress(0)
|
|
13
|
+
if enabled && open { settle(to: true, velocity: 0, user: false) }
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
@objc var open = false {
|
|
17
|
+
didSet {
|
|
18
|
+
guard open != oldValue else { return }
|
|
19
|
+
interruptGesture()
|
|
20
|
+
settle(to: enabled && open, velocity: 0, user: false)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
@objc var swipeEnabled = false
|
|
24
|
+
@objc var hapticFeedbackEnabled = false
|
|
25
|
+
@objc var bottomBarHeight: Double = 0
|
|
26
|
+
@objc var onDrawerOpenChange: ((Bool) -> Void)?
|
|
27
|
+
|
|
28
|
+
private let menu = UIView()
|
|
29
|
+
private let menuShade = UIView()
|
|
30
|
+
private let foreground = UIView()
|
|
31
|
+
private let foregroundContent = UIView()
|
|
32
|
+
private let dim = UIButton(type: .custom)
|
|
33
|
+
private var slots: [UIView] = []
|
|
34
|
+
private var pan: UIPanGestureRecognizer!
|
|
35
|
+
private var progress: CGFloat = 0
|
|
36
|
+
private var stableOpen = false
|
|
37
|
+
private var gestureStartProgress: CGFloat = 0
|
|
38
|
+
private var gestureReturnOpen = false
|
|
39
|
+
private weak var touchTarget: UIView?
|
|
40
|
+
private var suppressCancellation = false
|
|
41
|
+
private var displayLink: CADisplayLink?
|
|
42
|
+
private var animationStart: CFTimeInterval = 0
|
|
43
|
+
private var animationFrom: CGFloat = 0
|
|
44
|
+
private var animationVelocity: CGFloat = 0
|
|
45
|
+
private var animationTarget = false
|
|
46
|
+
private var animationUser = false
|
|
47
|
+
private var previousSize = CGSize.zero
|
|
48
|
+
|
|
49
|
+
override init(frame: CGRect) {
|
|
50
|
+
super.init(frame: frame)
|
|
51
|
+
clipsToBounds = true
|
|
52
|
+
addSubview(menu)
|
|
53
|
+
menuShade.backgroundColor = .black
|
|
54
|
+
menuShade.isUserInteractionEnabled = false
|
|
55
|
+
menu.addSubview(menuShade)
|
|
56
|
+
addSubview(foreground)
|
|
57
|
+
foreground.addSubview(foregroundContent)
|
|
58
|
+
dim.backgroundColor = UIColor(white: 28.0 / 255.0, alpha: 1)
|
|
59
|
+
dim.accessibilityLabel = NSLocalizedString("Close menu", comment: "Close the tab drawer")
|
|
60
|
+
dim.addTarget(self, action: #selector(closeFromUser), for: .touchUpInside)
|
|
61
|
+
foreground.addSubview(dim)
|
|
62
|
+
foreground.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
|
|
63
|
+
foreground.layer.cornerCurve = .circular
|
|
64
|
+
foreground.clipsToBounds = true
|
|
65
|
+
pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
|
|
66
|
+
pan.maximumNumberOfTouches = 1
|
|
67
|
+
pan.delegate = self
|
|
68
|
+
addGestureRecognizer(pan)
|
|
69
|
+
setProgress(0)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
73
|
+
|
|
74
|
+
@objc func insertChild(_ child: UIView, atIndex index: Int) {
|
|
75
|
+
guard index >= 0 && index <= slots.count && slots.count < 2 else { return }
|
|
76
|
+
slots.insert(child, at: index)
|
|
77
|
+
updateSlots()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@objc func removeChild(atIndex index: Int) {
|
|
81
|
+
guard slots.indices.contains(index) else { return }
|
|
82
|
+
slots.remove(at: index).removeFromSuperview()
|
|
83
|
+
updateSlots()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private func updateSlots() {
|
|
87
|
+
for (index, child) in slots.enumerated() {
|
|
88
|
+
(index == 0 ? menu : foregroundContent).insertSubview(child, at: 0)
|
|
89
|
+
}
|
|
90
|
+
menu.bringSubviewToFront(menuShade)
|
|
91
|
+
setNeedsLayout()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
override func layoutSubviews() {
|
|
95
|
+
super.layoutSubviews()
|
|
96
|
+
if previousSize != bounds.size {
|
|
97
|
+
let dragging = pan.state == .began || pan.state == .changed
|
|
98
|
+
let target = displayLink != nil ? animationTarget : (dragging ? gestureReturnOpen : stableOpen)
|
|
99
|
+
let user = displayLink != nil && animationUser && previousSize != .zero
|
|
100
|
+
interruptGesture()
|
|
101
|
+
stopAnimation()
|
|
102
|
+
finish(target: enabled && (previousSize == .zero ? open : target), user: user)
|
|
103
|
+
previousSize = bounds.size
|
|
104
|
+
}
|
|
105
|
+
menu.transform = .identity
|
|
106
|
+
foreground.transform = .identity
|
|
107
|
+
menu.frame = CGRect(x: 0, y: 0, width: bounds.width * 0.78, height: bounds.height)
|
|
108
|
+
foreground.frame = bounds
|
|
109
|
+
foregroundContent.frame = foreground.bounds
|
|
110
|
+
dim.frame = foreground.bounds
|
|
111
|
+
menuShade.frame = menu.bounds
|
|
112
|
+
if slots.count > 0 { slots[0].frame = menu.bounds }
|
|
113
|
+
if slots.count > 1 { slots[1].frame = foreground.bounds }
|
|
114
|
+
setProgress(progress)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
override func didMoveToWindow() {
|
|
118
|
+
super.didMoveToWindow()
|
|
119
|
+
if window == nil {
|
|
120
|
+
interruptGesture()
|
|
121
|
+
stopAnimation()
|
|
122
|
+
setProgress(stableOpen && enabled ? 1 : 0)
|
|
123
|
+
} else if enabled && stableOpen != open {
|
|
124
|
+
settle(to: open, velocity: 0, user: false)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
override func accessibilityPerformEscape() -> Bool {
|
|
129
|
+
guard enabled && progress > 0 else { return false }
|
|
130
|
+
closeFromUser()
|
|
131
|
+
return true
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
|
135
|
+
guard enabled else { return false }
|
|
136
|
+
let start = touch.location(in: self)
|
|
137
|
+
guard start.y < bounds.height - CGFloat(max(0, bottomBarHeight)) else { return false }
|
|
138
|
+
touchTarget = touch.view
|
|
139
|
+
if progress > 0 { return start.x >= bounds.width * 0.78 * progress }
|
|
140
|
+
return swipeEnabled && !touchPathContainsExclusiveControl()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
144
|
+
guard enabled, bounds.width > 0 else { return false }
|
|
145
|
+
let velocity = pan.velocity(in: self)
|
|
146
|
+
guard abs(velocity.x) > abs(velocity.y) else { return false }
|
|
147
|
+
let location = pan.location(in: self)
|
|
148
|
+
let translation = pan.translation(in: self)
|
|
149
|
+
let start = CGPoint(x: location.x - translation.x, y: location.y - translation.y)
|
|
150
|
+
guard start.y < bounds.height - CGFloat(max(0, bottomBarHeight)) else { return false }
|
|
151
|
+
if progress > 0 {
|
|
152
|
+
return start.x >= bounds.width * 0.78 * progress
|
|
153
|
+
}
|
|
154
|
+
return swipeEnabled && velocity.x > 0 && !touchPathCanScrollRight()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Let this recognizer make its one-time direction/boundary decision before
|
|
158
|
+
// descendant scroll pans. Returning false from shouldBegin releases them.
|
|
159
|
+
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
|
|
160
|
+
shouldBeRequiredToFailBy other: UIGestureRecognizer) -> Bool {
|
|
161
|
+
guard gestureRecognizer === pan,
|
|
162
|
+
let scroll = other.view as? UIScrollView,
|
|
163
|
+
other === scroll.panGestureRecognizer else { return false }
|
|
164
|
+
return scroll.isDescendant(of: foregroundContent)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
|
|
168
|
+
shouldRequireFailureOf other: UIGestureRecognizer) -> Bool {
|
|
169
|
+
guard gestureRecognizer === pan else { return false }
|
|
170
|
+
// UIKit's navigation edge gesture retains priority over the drawer.
|
|
171
|
+
return other is UIScreenEdgePanGestureRecognizer
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private func touchPathContainsExclusiveControl() -> Bool {
|
|
175
|
+
var view = touchTarget
|
|
176
|
+
while let current = view, current !== self {
|
|
177
|
+
if current is UISlider || current is UISwitch || current is UISegmentedControl ||
|
|
178
|
+
current is UITextField || current is UITextView { return true }
|
|
179
|
+
if let webClass = NSClassFromString("WKWebView"), current.isKind(of: webClass) { return true }
|
|
180
|
+
if current.layer is CAMetalLayer { return true }
|
|
181
|
+
view = current.superview
|
|
182
|
+
}
|
|
183
|
+
return false
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private func pageController(for scroll: UIScrollView) -> UIPageViewController? {
|
|
187
|
+
var responder: UIResponder? = scroll.next
|
|
188
|
+
while let current = responder, current !== self {
|
|
189
|
+
if let page = current as? UIPageViewController, scroll.superview === page.view { return page }
|
|
190
|
+
responder = current.next
|
|
191
|
+
}
|
|
192
|
+
return nil
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private func touchPathCanScrollRight() -> Bool {
|
|
196
|
+
var view = touchTarget
|
|
197
|
+
while let current = view, current !== self {
|
|
198
|
+
if let scroll = current as? UIScrollView, scroll.isScrollEnabled {
|
|
199
|
+
// The native smooth pager exposes this existing header exclusion. Read
|
|
200
|
+
// its getter instead of invoking UIKit's recognition lifecycle hook.
|
|
201
|
+
let selector = NSSelectorFromString("excludedHeaderView")
|
|
202
|
+
if scroll.responds(to: selector),
|
|
203
|
+
let header = scroll.perform(selector)?.takeUnretainedValue() as? UIView,
|
|
204
|
+
let target = touchTarget, target.isDescendant(of: header) {
|
|
205
|
+
view = current.superview
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
if let page = pageController(for: scroll) {
|
|
209
|
+
// UIPageViewController keeps its private scroll view centered even at
|
|
210
|
+
// the first page. Its public before query describes physical right.
|
|
211
|
+
if page.navigationOrientation == .horizontal,
|
|
212
|
+
let displayed = page.viewControllers?.first,
|
|
213
|
+
page.dataSource?.pageViewController(page, viewControllerBefore: displayed) != nil { return true }
|
|
214
|
+
} else {
|
|
215
|
+
let inset = scroll.adjustedContentInset
|
|
216
|
+
let horizontal = scroll.contentSize.width + inset.left + inset.right > scroll.bounds.width + 0.5
|
|
217
|
+
if horizontal && scroll.contentOffset.x > -inset.left + 0.5 { return true }
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
view = current.superview
|
|
221
|
+
}
|
|
222
|
+
return false
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
@objc private func handlePan(_ recognizer: UIPanGestureRecognizer) {
|
|
226
|
+
switch recognizer.state {
|
|
227
|
+
case .began:
|
|
228
|
+
gestureReturnOpen = displayLink != nil && !animationUser ? animationTarget : stableOpen
|
|
229
|
+
stopAnimation()
|
|
230
|
+
gestureStartProgress = progress
|
|
231
|
+
case .changed:
|
|
232
|
+
guard recognizer.numberOfTouches <= 1 else {
|
|
233
|
+
interruptGesture()
|
|
234
|
+
settle(to: gestureReturnOpen, velocity: 0, user: false)
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
setProgress(gestureStartProgress + recognizer.translation(in: self).x / max(1, bounds.width * 0.78))
|
|
238
|
+
case .ended:
|
|
239
|
+
let velocity = min(8, max(-8, recognizer.velocity(in: self).x / max(1, bounds.width * 0.78)))
|
|
240
|
+
settle(to: progress + velocity * 0.12 >= 0.5, velocity: velocity, user: true)
|
|
241
|
+
case .cancelled, .failed:
|
|
242
|
+
if !suppressCancellation { settle(to: gestureReturnOpen, velocity: 0, user: false) }
|
|
243
|
+
default: break
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private func interruptGesture() {
|
|
248
|
+
guard pan != nil else { return }
|
|
249
|
+
suppressCancellation = true
|
|
250
|
+
pan.isEnabled = false
|
|
251
|
+
pan.isEnabled = true
|
|
252
|
+
touchTarget = nil
|
|
253
|
+
suppressCancellation = false
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
@objc private func closeFromUser() {
|
|
257
|
+
interruptGesture()
|
|
258
|
+
settle(to: false, velocity: 0, user: true)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private func setProgress(_ value: CGFloat) {
|
|
262
|
+
progress = min(1, max(0, enabled ? value : 0))
|
|
263
|
+
foreground.transform = CGAffineTransform(translationX: bounds.width * 0.78 * progress, y: 0)
|
|
264
|
+
foreground.layer.cornerRadius = 64 * progress
|
|
265
|
+
dim.alpha = 0.7 * progress
|
|
266
|
+
dim.isHidden = progress == 0
|
|
267
|
+
foregroundContent.isUserInteractionEnabled = progress == 0
|
|
268
|
+
foregroundContent.accessibilityElementsHidden = progress > 0
|
|
269
|
+
menu.transform = CGAffineTransform(translationX: -0.12 * bounds.width * 0.78 * (1 - progress), y: 0)
|
|
270
|
+
menuShade.alpha = 0.12 * (1 - progress)
|
|
271
|
+
menu.isHidden = progress == 0
|
|
272
|
+
menu.accessibilityElementsHidden = progress == 0
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private func settle(to target: Bool, velocity: CGFloat, user: Bool) {
|
|
276
|
+
stopAnimation()
|
|
277
|
+
let target = enabled && target
|
|
278
|
+
guard bounds.width > 0, window != nil else {
|
|
279
|
+
stableOpen = target
|
|
280
|
+
setProgress(target ? 1 : 0)
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
if progress == (target ? 1 : 0) {
|
|
284
|
+
finish(target: target, user: user)
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
animationFrom = progress
|
|
288
|
+
animationVelocity = min(8, max(-8, velocity))
|
|
289
|
+
animationTarget = target
|
|
290
|
+
animationUser = user
|
|
291
|
+
animationStart = CACurrentMediaTime()
|
|
292
|
+
let link = CADisplayLink(target: self, selector: #selector(tick))
|
|
293
|
+
link.add(to: .main, forMode: .common)
|
|
294
|
+
displayLink = link
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
@objc private func tick() {
|
|
298
|
+
let elapsed = CGFloat(CACurrentMediaTime() - animationStart)
|
|
299
|
+
let target: CGFloat = animationTarget ? 1 : 0
|
|
300
|
+
let a = animationFrom - target
|
|
301
|
+
let b = animationVelocity + 22 * a
|
|
302
|
+
setProgress(target + (a + b * elapsed) * exp(-22 * elapsed))
|
|
303
|
+
if elapsed >= 0.5 {
|
|
304
|
+
let target = animationTarget
|
|
305
|
+
let user = animationUser
|
|
306
|
+
stopAnimation()
|
|
307
|
+
finish(target: target, user: user)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private func finish(target: Bool, user: Bool) {
|
|
312
|
+
let changed = stableOpen != target
|
|
313
|
+
stableOpen = target
|
|
314
|
+
setProgress(target ? 1 : 0)
|
|
315
|
+
if user && (changed || target != open) {
|
|
316
|
+
if hapticFeedbackEnabled && changed { UIImpactFeedbackGenerator(style: .light).impactOccurred() }
|
|
317
|
+
onDrawerOpenChange?(target)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private func stopAnimation() {
|
|
322
|
+
displayLink?.invalidate()
|
|
323
|
+
displayLink = nil
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
deinit { displayLink?.invalidate() }
|
|
327
|
+
}
|
package/lib/module/BottomTabs.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import React, { useLayoutEffect, useRef } from
|
|
4
|
-
import { Image, Platform, StyleSheet, View, processColor } from
|
|
3
|
+
import React, { useLayoutEffect, useRef } from "react";
|
|
4
|
+
import { Image, Platform, StyleSheet, View, processColor } from "react-native";
|
|
5
5
|
import { BottomTabBarHeightContext } from "./utils/BottomTabBarHeightContext.js";
|
|
6
6
|
|
|
7
7
|
// eslint-disable-next-line @react-native/no-deep-imports
|
|
8
8
|
|
|
9
|
-
import NativeBottomTabs from
|
|
10
|
-
import useLatestCallback from
|
|
9
|
+
import NativeBottomTabs from "./BottomTabsNativeComponent";
|
|
10
|
+
import useLatestCallback from "use-latest-callback";
|
|
11
11
|
import DelayedFreeze from "./DelayedFreeze.js";
|
|
12
|
+
import NativeBottomTabsDrawer from "./BottomTabsDrawerNativeComponent";
|
|
12
13
|
import { BottomAccessoryView } from "./BottomAccessoryView.js";
|
|
13
14
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
14
15
|
const isAppleSymbol = icon => icon?.sfSymbol;
|
|
@@ -19,6 +20,10 @@ const BottomTabs = ({
|
|
|
19
20
|
renderLazyPlaceholder,
|
|
20
21
|
onIndexChange,
|
|
21
22
|
onTabDoublePress,
|
|
23
|
+
renderDrawerContent,
|
|
24
|
+
drawerOpen = false,
|
|
25
|
+
drawerSwipeEnabled = false,
|
|
26
|
+
onDrawerOpenChange,
|
|
22
27
|
rippleColor,
|
|
23
28
|
tabBarActiveTintColor: activeTintColor,
|
|
24
29
|
tabBarInactiveTintColor: inactiveTintColor,
|
|
@@ -60,7 +65,7 @@ const BottomTabs = ({
|
|
|
60
65
|
route
|
|
61
66
|
}) => route.preventsDefault,
|
|
62
67
|
hapticFeedbackEnabled = false,
|
|
63
|
-
labeled = Platform.OS !==
|
|
68
|
+
labeled = Platform.OS !== "android" ? true : undefined,
|
|
64
69
|
getFreezeOnBlur = ({
|
|
65
70
|
route
|
|
66
71
|
}) => route.freezeOnBlur,
|
|
@@ -69,21 +74,27 @@ const BottomTabs = ({
|
|
|
69
74
|
tabLabelStyle,
|
|
70
75
|
renderBottomAccessoryView,
|
|
71
76
|
activeIndicatorColor,
|
|
72
|
-
androidBottomBlurEnabled = Platform.OS ===
|
|
77
|
+
androidBottomBlurEnabled = Platform.OS === "android" && Number(Platform.Version) >= 36,
|
|
73
78
|
delayedFreeze = false,
|
|
79
|
+
onTabBarPlacementChange,
|
|
80
|
+
sideAccessoryIcon,
|
|
81
|
+
sideAccessoryLabel,
|
|
82
|
+
onSideAccessoryPress,
|
|
74
83
|
...props
|
|
75
84
|
}) => {
|
|
76
85
|
// @ts-ignore
|
|
77
86
|
const focusedKey = navigationState.routes[navigationState.index].key;
|
|
78
87
|
const customTabBarWrapperRef = useRef(null);
|
|
79
88
|
const nativeBottomTabsRef = useRef(null);
|
|
80
|
-
const [
|
|
89
|
+
const [nativeTabBarHeight, setNativeTabBarHeight] = React.useState(0);
|
|
90
|
+
const [customTabBarHeight, setCustomTabBarHeight] = React.useState(0);
|
|
91
|
+
const tabBarHeight = renderCustomTabBar ? customTabBarHeight : props.tabBarHidden ? 0 : nativeTabBarHeight;
|
|
81
92
|
const [measuredDimensions, setMeasuredDimensions] = React.useState({
|
|
82
|
-
width:
|
|
83
|
-
height:
|
|
93
|
+
width: "100%",
|
|
94
|
+
height: "100%"
|
|
84
95
|
});
|
|
85
96
|
const trimmedRoutes = React.useMemo(() => {
|
|
86
|
-
if (Platform.OS ===
|
|
97
|
+
if (Platform.OS === "android" && navigationState.routes.length > ANDROID_MAX_TABS) {
|
|
87
98
|
console.warn(`BottomTabs only supports up to ${ANDROID_MAX_TABS} tabs on Android`);
|
|
88
99
|
return navigationState.routes.slice(0, ANDROID_MAX_TABS);
|
|
89
100
|
}
|
|
@@ -93,7 +104,7 @@ const BottomTabs = ({
|
|
|
93
104
|
if (!loaded.includes(focusedKey)) {
|
|
94
105
|
setLoaded(loaded => [...loaded, focusedKey]);
|
|
95
106
|
}
|
|
96
|
-
const isIOS = Platform.OS ===
|
|
107
|
+
const isIOS = Platform.OS === "ios";
|
|
97
108
|
|
|
98
109
|
// Unfocused (outline) icons — independent of focusedKey so the array stays
|
|
99
110
|
// stable across tab switches and the native side never reloads on switch.
|
|
@@ -120,8 +131,8 @@ const BottomTabs = ({
|
|
|
120
131
|
const items = React.useMemo(() => trimmedRoutes.map((route, index) => {
|
|
121
132
|
const icon = icons[index];
|
|
122
133
|
const isSfSymbol = isAppleSymbol(icon);
|
|
123
|
-
if (Platform.OS ===
|
|
124
|
-
console.warn(
|
|
134
|
+
if (Platform.OS === "android" && isSfSymbol) {
|
|
135
|
+
console.warn("SF Symbols are not supported on Android. Use require() or pass uri to load an image instead.");
|
|
125
136
|
}
|
|
126
137
|
const color = processColor(getActiveTintColor({
|
|
127
138
|
route
|
|
@@ -141,9 +152,9 @@ const BottomTabs = ({
|
|
|
141
152
|
badge: getBadge?.({
|
|
142
153
|
route
|
|
143
154
|
}),
|
|
144
|
-
badgeBackgroundColor: typeof badgeBgColor ===
|
|
145
|
-
badgeTextColor: typeof badgeTxtColor ===
|
|
146
|
-
activeTintColor: typeof color ===
|
|
155
|
+
badgeBackgroundColor: typeof badgeBgColor === "number" ? badgeBgColor : undefined,
|
|
156
|
+
badgeTextColor: typeof badgeTxtColor === "number" ? badgeTxtColor : undefined,
|
|
157
|
+
activeTintColor: typeof color === "number" ? color : undefined,
|
|
147
158
|
hidden: getHidden?.({
|
|
148
159
|
route
|
|
149
160
|
}),
|
|
@@ -163,14 +174,14 @@ const BottomTabs = ({
|
|
|
163
174
|
// @ts-ignore - resolveAssetSource accepts ImageSourcePropType
|
|
164
175
|
const resolved = Image.resolveAssetSource(icon);
|
|
165
176
|
return {
|
|
166
|
-
uri: resolved?.uri ??
|
|
177
|
+
uri: resolved?.uri ?? "",
|
|
167
178
|
width: resolved?.width ?? 0,
|
|
168
179
|
height: resolved?.height ?? 0,
|
|
169
180
|
scale: resolved?.scale ?? 1
|
|
170
181
|
};
|
|
171
182
|
}
|
|
172
183
|
return {
|
|
173
|
-
uri:
|
|
184
|
+
uri: "",
|
|
174
185
|
width: 0,
|
|
175
186
|
height: 0,
|
|
176
187
|
scale: 1
|
|
@@ -184,14 +195,14 @@ const BottomTabs = ({
|
|
|
184
195
|
// @ts-ignore - resolveAssetSource accepts ImageSourcePropType
|
|
185
196
|
const resolved = Image.resolveAssetSource(icon);
|
|
186
197
|
return {
|
|
187
|
-
uri: resolved?.uri ??
|
|
198
|
+
uri: resolved?.uri ?? "",
|
|
188
199
|
width: resolved?.width ?? 0,
|
|
189
200
|
height: resolved?.height ?? 0,
|
|
190
201
|
scale: resolved?.scale ?? 1
|
|
191
202
|
};
|
|
192
203
|
}
|
|
193
204
|
return {
|
|
194
|
-
uri:
|
|
205
|
+
uri: "",
|
|
195
206
|
width: 0,
|
|
196
207
|
height: 0,
|
|
197
208
|
scale: 1
|
|
@@ -225,8 +236,8 @@ const BottomTabs = ({
|
|
|
225
236
|
}
|
|
226
237
|
}, [focusedKey, jumpTo]);
|
|
227
238
|
const handleTabBarMeasured = React.useCallback(event => {
|
|
228
|
-
|
|
229
|
-
}, [
|
|
239
|
+
setNativeTabBarHeight(event.nativeEvent.height);
|
|
240
|
+
}, [setNativeTabBarHeight]);
|
|
230
241
|
const handleNativeLayout = React.useCallback(event => {
|
|
231
242
|
const {
|
|
232
243
|
width,
|
|
@@ -237,88 +248,134 @@ const BottomTabs = ({
|
|
|
237
248
|
height
|
|
238
249
|
});
|
|
239
250
|
}, [setMeasuredDimensions]);
|
|
251
|
+
const handleTabBarPlacementChange = React.useCallback(event => {
|
|
252
|
+
onTabBarPlacementChange?.(event.nativeEvent.placement);
|
|
253
|
+
}, [onTabBarPlacementChange]);
|
|
254
|
+
const handleSideAccessoryPress = React.useCallback(() => {
|
|
255
|
+
onSideAccessoryPress?.();
|
|
256
|
+
}, [onSideAccessoryPress]);
|
|
257
|
+
const resolvedSideAccessoryIcon = React.useMemo(() => {
|
|
258
|
+
if (!isIOS || !sideAccessoryIcon) {
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
// @ts-ignore - resolveAssetSource accepts ImageSourcePropType
|
|
262
|
+
const resolved = Image.resolveAssetSource(sideAccessoryIcon);
|
|
263
|
+
return resolved?.uri ? {
|
|
264
|
+
uri: resolved.uri,
|
|
265
|
+
width: resolved.width ?? 0,
|
|
266
|
+
height: resolved.height ?? 0,
|
|
267
|
+
scale: resolved.scale ?? 1
|
|
268
|
+
} : undefined;
|
|
269
|
+
}, [sideAccessoryIcon]);
|
|
240
270
|
useLayoutEffect(() => {
|
|
241
271
|
if (renderCustomTabBar && customTabBarWrapperRef.current) {
|
|
242
272
|
customTabBarWrapperRef.current.measure((_x, _y, _width, height) => {
|
|
243
|
-
|
|
273
|
+
setCustomTabBarHeight(height);
|
|
244
274
|
});
|
|
245
275
|
}
|
|
246
276
|
}, [renderCustomTabBar]);
|
|
247
|
-
return /*#__PURE__*/
|
|
277
|
+
return /*#__PURE__*/_jsx(BottomTabBarHeightContext.Provider, {
|
|
248
278
|
value: tabBarHeight,
|
|
249
|
-
children:
|
|
250
|
-
ref: nativeBottomTabsRef,
|
|
251
|
-
...props,
|
|
252
|
-
...tabLabelStyle,
|
|
279
|
+
children: /*#__PURE__*/_jsxs(NativeBottomTabsDrawer, {
|
|
253
280
|
style: styles.fullWidth,
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
selectedPage: focusedKey,
|
|
258
|
-
tabBarHidden: props.tabBarHidden ?? !!renderCustomTabBar,
|
|
259
|
-
onTabDoublePress: handleTabDoublePress,
|
|
260
|
-
onPageSelected: handlePageSelected,
|
|
261
|
-
onTabBarMeasured: handleTabBarMeasured,
|
|
262
|
-
onNativeLayout: handleNativeLayout,
|
|
281
|
+
enabled: !!renderDrawerContent,
|
|
282
|
+
open: drawerOpen,
|
|
283
|
+
swipeEnabled: drawerSwipeEnabled,
|
|
263
284
|
hapticFeedbackEnabled: hapticFeedbackEnabled,
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
285
|
+
bottomBarHeight: tabBarHeight ?? 0,
|
|
286
|
+
onDrawerOpenChange: event => onDrawerOpenChange?.(event.nativeEvent.open),
|
|
287
|
+
children: [/*#__PURE__*/_jsx(View, {
|
|
288
|
+
collapsable: false,
|
|
289
|
+
style: styles.drawerMenu,
|
|
290
|
+
children: Platform.OS !== "web" ? renderDrawerContent?.() : null
|
|
291
|
+
}), /*#__PURE__*/_jsxs(View, {
|
|
292
|
+
collapsable: false,
|
|
293
|
+
style: styles.fullWidth,
|
|
294
|
+
children: [/*#__PURE__*/_jsxs(NativeBottomTabs, {
|
|
295
|
+
ref: nativeBottomTabsRef,
|
|
296
|
+
...props,
|
|
297
|
+
...tabLabelStyle,
|
|
298
|
+
style: styles.fullWidth,
|
|
299
|
+
items: items,
|
|
300
|
+
icons: renderCustomTabBar ? undefined : resolvedIconAssets,
|
|
301
|
+
selectedIcons: isIOS && !renderCustomTabBar ? resolvedSelectedIconAssets : undefined,
|
|
302
|
+
selectedPage: focusedKey,
|
|
303
|
+
tabBarHidden: props.tabBarHidden ?? !!renderCustomTabBar,
|
|
304
|
+
onTabDoublePress: handleTabDoublePress,
|
|
305
|
+
onPageSelected: handlePageSelected,
|
|
306
|
+
onTabBarMeasured: handleTabBarMeasured,
|
|
307
|
+
onNativeLayout: handleNativeLayout,
|
|
308
|
+
onTabBarPlacementChange: handleTabBarPlacementChange,
|
|
309
|
+
sideAccessoryIcon: renderCustomTabBar ? undefined : resolvedSideAccessoryIcon,
|
|
310
|
+
sideAccessoryLabel: sideAccessoryLabel,
|
|
311
|
+
onSideAccessoryPress: handleSideAccessoryPress,
|
|
312
|
+
hapticFeedbackEnabled: hapticFeedbackEnabled,
|
|
313
|
+
activeTintColor: activeTintColor,
|
|
314
|
+
inactiveTintColor: inactiveTintColor,
|
|
315
|
+
barTintColor: tabBarStyle?.backgroundColor,
|
|
316
|
+
rippleColor: rippleColor,
|
|
317
|
+
activeIndicatorColor: activeIndicatorColor,
|
|
318
|
+
androidBottomBlurEnabled: Platform.OS === "android" && !renderCustomTabBar && androidBottomBlurEnabled,
|
|
319
|
+
labeled: labeled,
|
|
320
|
+
children: [trimmedRoutes.map(route => {
|
|
321
|
+
if (getLazy({
|
|
322
|
+
route
|
|
323
|
+
}) !== false && !loaded.includes(route.key)) {
|
|
324
|
+
return /*#__PURE__*/_jsx(View, {
|
|
325
|
+
collapsable: false,
|
|
326
|
+
style: styles.fullWidth,
|
|
327
|
+
children: renderLazyPlaceholder?.({
|
|
328
|
+
route
|
|
329
|
+
})
|
|
330
|
+
}, route.key);
|
|
331
|
+
}
|
|
332
|
+
const focused = route.key === focusedKey;
|
|
333
|
+
const freeze = !focused ? getFreezeOnBlur({
|
|
279
334
|
route
|
|
280
|
-
})
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
})
|
|
308
|
-
})
|
|
309
|
-
ref: customTabBarWrapperRef,
|
|
310
|
-
children: renderCustomTabBar()
|
|
311
|
-
}) : null]
|
|
335
|
+
}) : false;
|
|
336
|
+
const customStyle = getSceneStyle({
|
|
337
|
+
route
|
|
338
|
+
});
|
|
339
|
+
return /*#__PURE__*/_jsx(View, {
|
|
340
|
+
style: [styles.screen, renderCustomTabBar ? styles.fullWidth : measuredDimensions, customStyle],
|
|
341
|
+
collapsable: false,
|
|
342
|
+
pointerEvents: focused ? "auto" : "none",
|
|
343
|
+
accessibilityElementsHidden: !focused,
|
|
344
|
+
importantForAccessibility: focused ? "auto" : "no-hide-descendants",
|
|
345
|
+
children: /*#__PURE__*/_jsx(DelayedFreeze, {
|
|
346
|
+
freeze: !!freeze,
|
|
347
|
+
delayedFreeze: delayedFreeze,
|
|
348
|
+
children: renderScene({
|
|
349
|
+
route,
|
|
350
|
+
jumpTo
|
|
351
|
+
})
|
|
352
|
+
})
|
|
353
|
+
}, route.key);
|
|
354
|
+
}), Platform.OS === "ios" && parseFloat(Platform.Version) >= 26 && renderBottomAccessoryView && !renderCustomTabBar ? /*#__PURE__*/_jsx(BottomAccessoryView, {
|
|
355
|
+
renderBottomAccessoryView: renderBottomAccessoryView
|
|
356
|
+
}) : null]
|
|
357
|
+
}), renderCustomTabBar ? /*#__PURE__*/_jsx(View, {
|
|
358
|
+
ref: customTabBarWrapperRef,
|
|
359
|
+
onLayout: event => setCustomTabBarHeight(event.nativeEvent.layout.height),
|
|
360
|
+
children: renderCustomTabBar()
|
|
361
|
+
}) : null]
|
|
362
|
+
})]
|
|
363
|
+
})
|
|
312
364
|
});
|
|
313
365
|
};
|
|
314
366
|
const styles = StyleSheet.create({
|
|
315
367
|
fullWidth: {
|
|
316
|
-
width:
|
|
317
|
-
height:
|
|
368
|
+
width: "100%",
|
|
369
|
+
height: "100%",
|
|
318
370
|
flex: 1
|
|
319
371
|
},
|
|
372
|
+
drawerMenu: {
|
|
373
|
+
position: "absolute",
|
|
374
|
+
width: "78%",
|
|
375
|
+
height: "100%"
|
|
376
|
+
},
|
|
320
377
|
screen: {
|
|
321
|
-
position:
|
|
378
|
+
position: "absolute"
|
|
322
379
|
}
|
|
323
380
|
});
|
|
324
381
|
export default BottomTabs;
|