@onekeyfe/react-native-bottom-tabs 6.7.0-19 → 6.7.0-21

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.
Files changed (30) hide show
  1. package/android/src/main/java/com/onekey/bottomtabs/BottomBarBlurRenderer.kt +50 -30
  2. package/android/src/main/java/com/onekey/bottomtabs/BottomTabsDrawerManager.kt +55 -0
  3. package/android/src/main/java/com/onekey/bottomtabs/BottomTabsDrawerView.kt +496 -0
  4. package/android/src/main/java/com/onekey/bottomtabs/BottomTabsPackage.kt +1 -1
  5. package/android/src/main/java/com/onekey/bottomtabs/BottomTabsView.kt +24 -4
  6. package/android/src/main/java/com/onekey/bottomtabs/BottomTabsViewManager.kt +3 -0
  7. package/docs/SPEC.md +184 -20
  8. package/ios/RCTBottomTabsComponentView.mm +37 -0
  9. package/ios/RCTBottomTabsContainerView.swift +192 -7
  10. package/ios/RCTBottomTabsDrawerComponentView.h +8 -0
  11. package/ios/RCTBottomTabsDrawerComponentView.mm +79 -0
  12. package/ios/RCTBottomTabsDrawerContainerView.swift +327 -0
  13. package/lib/module/BottomTabs.js +144 -87
  14. package/lib/module/BottomTabsDrawerNativeComponent.ts +19 -0
  15. package/lib/module/BottomTabsDrawerNativeComponent.web.js +13 -0
  16. package/lib/module/BottomTabsNativeComponent.ts +13 -0
  17. package/lib/typescript/src/BottomTabs.d.ts +36 -10
  18. package/lib/typescript/src/BottomTabsDrawerNativeComponent.d.ts +15 -0
  19. package/lib/typescript/src/BottomTabsDrawerNativeComponent.web.d.ts +4 -0
  20. package/lib/typescript/src/BottomTabsNativeComponent.d.ts +8 -0
  21. package/lib/typescript/src/index.d.ts +1 -1
  22. package/lib/typescript/src/types.d.ts +1 -0
  23. package/package.json +3 -2
  24. package/react-native.config.js +4 -1
  25. package/src/BottomTabs.tsx +235 -115
  26. package/src/BottomTabsDrawerNativeComponent.ts +19 -0
  27. package/src/BottomTabsDrawerNativeComponent.web.tsx +9 -0
  28. package/src/BottomTabsNativeComponent.ts +13 -0
  29. package/src/index.tsx +1 -1
  30. package/src/types.ts +3 -0
@@ -0,0 +1,79 @@
1
+ #import "RCTBottomTabsDrawerComponentView.h"
2
+ #import <react/renderer/components/RNBottomTabs/ComponentDescriptors.h>
3
+ #import <react/renderer/components/RNBottomTabs/EventEmitters.h>
4
+ #import <react/renderer/components/RNBottomTabs/Props.h>
5
+ #import <React/RCTFabricComponentsPlugins.h>
6
+
7
+ // Swift keeps the host internal to this module, so its framework compatibility
8
+ // header does not export the class. Its explicit @objc name and selectors form
9
+ // this private boundary, matching the existing tab container's runtime lookup.
10
+ @protocol RNBottomTabsDrawerContainer <NSObject>
11
+ @property(nonatomic) BOOL enabled;
12
+ @property(nonatomic) BOOL open;
13
+ @property(nonatomic) BOOL swipeEnabled;
14
+ @property(nonatomic) BOOL hapticFeedbackEnabled;
15
+ @property(nonatomic) double bottomBarHeight;
16
+ @property(nonatomic, copy) void (^onDrawerOpenChange)(BOOL open);
17
+ - (void)insertChild:(UIView *)child atIndex:(NSInteger)index;
18
+ - (void)removeChildAtIndex:(NSInteger)index;
19
+ @end
20
+
21
+ using namespace facebook::react;
22
+
23
+ @implementation RCTBottomTabsDrawerComponentView {
24
+ UIView<RNBottomTabsDrawerContainer> *_drawer;
25
+ }
26
+
27
+ + (ComponentDescriptorProvider)componentDescriptorProvider
28
+ {
29
+ return concreteComponentDescriptorProvider<RNBottomTabsDrawerComponentDescriptor>();
30
+ }
31
+
32
+ + (BOOL)shouldBeRecycled { return NO; }
33
+
34
+ - (instancetype)initWithFrame:(CGRect)frame
35
+ {
36
+ if (self = [super initWithFrame:frame]) {
37
+ _props = std::make_shared<const RNBottomTabsDrawerProps>();
38
+ Class containerClass = NSClassFromString(@"RCTBottomTabsDrawerContainerView");
39
+ _drawer = (UIView<RNBottomTabsDrawerContainer> *)[[containerClass alloc] initWithFrame:frame];
40
+ self.contentView = _drawer;
41
+ __weak RCTBottomTabsDrawerComponentView *weakSelf = self;
42
+ _drawer.onDrawerOpenChange = ^(BOOL open) {
43
+ auto strongSelf = weakSelf;
44
+ if (!strongSelf) return;
45
+ auto emitter = std::static_pointer_cast<const RNBottomTabsDrawerEventEmitter>(strongSelf->_eventEmitter);
46
+ if (emitter) emitter->onDrawerOpenChange({.open = (bool)open});
47
+ };
48
+ }
49
+ return self;
50
+ }
51
+
52
+ - (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)child index:(NSInteger)index
53
+ {
54
+ [_drawer insertChild:child atIndex:index];
55
+ }
56
+
57
+ - (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)child index:(NSInteger)index
58
+ {
59
+ [_drawer removeChildAtIndex:index];
60
+ }
61
+
62
+ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
63
+ {
64
+ const auto &next = *std::static_pointer_cast<const RNBottomTabsDrawerProps>(props);
65
+ const auto &previous = *std::static_pointer_cast<const RNBottomTabsDrawerProps>(_props);
66
+ if (next.enabled != previous.enabled) _drawer.enabled = next.enabled;
67
+ if (next.open != previous.open) _drawer.open = next.open;
68
+ if (next.swipeEnabled != previous.swipeEnabled) _drawer.swipeEnabled = next.swipeEnabled;
69
+ if (next.hapticFeedbackEnabled != previous.hapticFeedbackEnabled)
70
+ _drawer.hapticFeedbackEnabled = next.hapticFeedbackEnabled;
71
+ if (next.bottomBarHeight != previous.bottomBarHeight) _drawer.bottomBarHeight = next.bottomBarHeight;
72
+ [super updateProps:props oldProps:oldProps];
73
+ }
74
+ @end
75
+
76
+ Class<RCTComponentViewProtocol> RNBottomTabsDrawerCls(void)
77
+ {
78
+ return RCTBottomTabsDrawerComponentView.class;
79
+ }
@@ -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
+ }