@onekeyfe/react-native-bottom-tabs 0.0.0-stage → 6.7.0-8

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 (78) hide show
  1. package/LICENSE +46 -0
  2. package/README.md +94 -2
  3. package/android/build.gradle +119 -0
  4. package/android/gradle.properties +4 -0
  5. package/android/src/main/AndroidManifest.xml +3 -0
  6. package/android/src/main/AndroidManifestNew.xml +2 -0
  7. package/android/src/main/java/com/onekey/bottomtabs/BottomBarBlurRenderer.kt +92 -0
  8. package/android/src/main/java/com/onekey/bottomtabs/BottomTabsPackage.kt +16 -0
  9. package/android/src/main/java/com/onekey/bottomtabs/BottomTabsView.kt +1558 -0
  10. package/android/src/main/java/com/onekey/bottomtabs/BottomTabsViewManager.kt +220 -0
  11. package/android/src/main/java/com/onekey/bottomtabs/FloatingTabBarStyle.kt +50 -0
  12. package/android/src/main/java/com/onekey/bottomtabs/ImageSource.kt +86 -0
  13. package/android/src/main/java/com/onekey/bottomtabs/TabInfo.kt +13 -0
  14. package/android/src/main/java/com/onekey/bottomtabs/Utils.kt +31 -0
  15. package/android/src/main/java/com/onekey/bottomtabs/events/OnNativeLayoutEvent.kt +20 -0
  16. package/android/src/main/java/com/onekey/bottomtabs/events/OnTabBarMeasuredEvent.kt +19 -0
  17. package/android/src/main/java/com/onekey/bottomtabs/events/PageSelectedEvent.kt +21 -0
  18. package/android/src/main/java/com/onekey/bottomtabs/events/TabDoublePressEvent.kt +19 -0
  19. package/common/cpp/react/renderer/components/RNBottomTabs/RNBottomTabsViewComponentDescriptor.h +32 -0
  20. package/common/cpp/react/renderer/components/RNBottomTabs/RNBottomTabsViewShadowNode.cpp +18 -0
  21. package/common/cpp/react/renderer/components/RNBottomTabs/RNBottomTabsViewShadowNode.h +35 -0
  22. package/common/cpp/react/renderer/components/RNBottomTabs/RNBottomTabsViewState.cpp +15 -0
  23. package/common/cpp/react/renderer/components/RNBottomTabs/RNBottomTabsViewState.h +25 -0
  24. package/docs/SPEC.md +168 -0
  25. package/ios/Extensions.swift +46 -0
  26. package/ios/RCTBottomAccessoryComponentView.h +12 -0
  27. package/ios/RCTBottomAccessoryComponentView.mm +67 -0
  28. package/ios/RCTBottomAccessoryContainerView.swift +51 -0
  29. package/ios/RCTBottomTabsComponentView.h +12 -0
  30. package/ios/RCTBottomTabsComponentView.mm +338 -0
  31. package/ios/RCTBottomTabsContainerView.swift +777 -0
  32. package/ios/RCTBottomTabsLog.h +7 -0
  33. package/ios/RCTBottomTabsLog.m +32 -0
  34. package/ios/SVG/CoreSVG.h +13 -0
  35. package/ios/SVG/CoreSVG.mm +177 -0
  36. package/ios/SVG/SvgDecoder.h +10 -0
  37. package/ios/SVG/SvgDecoder.mm +32 -0
  38. package/ios/TabBarFontSize.swift +55 -0
  39. package/lib/module/BottomAccessoryView.js +45 -0
  40. package/lib/module/BottomAccessoryViewNativeComponent.ts +27 -0
  41. package/lib/module/BottomTabs.js +325 -0
  42. package/lib/module/BottomTabsNativeComponent.ts +70 -0
  43. package/lib/module/DelayedFreeze.js +31 -0
  44. package/lib/module/NativeSVGDecoder.js +5 -0
  45. package/lib/module/SceneMap.js +28 -0
  46. package/lib/module/codegen-types.d.js +2 -0
  47. package/lib/module/index.js +20 -0
  48. package/lib/module/package.json +1 -0
  49. package/lib/module/types.js +4 -0
  50. package/lib/module/utils/BottomTabBarHeightContext.js +5 -0
  51. package/lib/module/utils/useBottomTabBarHeight.js +12 -0
  52. package/lib/typescript/package.json +1 -0
  53. package/lib/typescript/src/BottomAccessoryView.d.ts +8 -0
  54. package/lib/typescript/src/BottomAccessoryViewNativeComponent.d.ts +16 -0
  55. package/lib/typescript/src/BottomTabs.d.ts +209 -0
  56. package/lib/typescript/src/BottomTabsNativeComponent.d.ts +59 -0
  57. package/lib/typescript/src/DelayedFreeze.d.ts +9 -0
  58. package/lib/typescript/src/NativeSVGDecoder.d.ts +6 -0
  59. package/lib/typescript/src/SceneMap.d.ts +10 -0
  60. package/lib/typescript/src/index.d.ts +16 -0
  61. package/lib/typescript/src/types.d.ts +29 -0
  62. package/lib/typescript/src/utils/BottomTabBarHeightContext.d.ts +3 -0
  63. package/lib/typescript/src/utils/useBottomTabBarHeight.d.ts +2 -0
  64. package/package.json +117 -4
  65. package/react-native-bottom-tabs.podspec +36 -0
  66. package/react-native.config.js +13 -0
  67. package/src/BottomAccessoryView.tsx +58 -0
  68. package/src/BottomAccessoryViewNativeComponent.ts +27 -0
  69. package/src/BottomTabs.tsx +559 -0
  70. package/src/BottomTabsNativeComponent.ts +70 -0
  71. package/src/DelayedFreeze.tsx +33 -0
  72. package/src/NativeSVGDecoder.ts +5 -0
  73. package/src/SceneMap.tsx +34 -0
  74. package/src/codegen-types.d.ts +28 -0
  75. package/src/index.tsx +18 -0
  76. package/src/types.ts +31 -0
  77. package/src/utils/BottomTabBarHeightContext.ts +5 -0
  78. package/src/utils/useBottomTabBarHeight.ts +15 -0
package/docs/SPEC.md ADDED
@@ -0,0 +1,168 @@
1
+ # Native Tab View behavioral contract
2
+
3
+ ## Tab presses and double press
4
+
5
+ Status: implemented in module source. Runtime verification status is recorded
6
+ under Conformance and acceptance.
7
+
8
+ Every tab press that the native tab bar accepts emits `onPageSelected` with the
9
+ tab key, including a press on the already selected tab (reselection). A
10
+ reselection never changes the selected page. A tab with `preventsDefault` emits
11
+ `onPageSelected` but keeps the current page selected.
12
+
13
+ `onTabDoublePress` fires when two consecutive accepted presses target the same
14
+ tab within the platform double-tap timeout (iOS 0.3 s; Android
15
+ `ViewConfiguration.getDoubleTapTimeout()`, 300 ms by default). Both presses still
16
+ emit `onPageSelected` first; the double press is an additional event, emitted
17
+ after the second press. After it fires, tracking resets, so a third quick press
18
+ starts a new pair. Double press is derived from accepted selections only: no
19
+ extra gesture recognizer is attached and single-press latency is unchanged.
20
+ JS-driven `selectedPage` changes do not count as presses.
21
+
22
+ Tab long press is not supported. The former `onTabLongPress` prop/event and the
23
+ native long-press recognizers were removed.
24
+
25
+ ## iOS native tab bar gestures
26
+
27
+ The iOS tab bar keeps UIKit's own gesture handling. On iOS 26 this includes the
28
+ Liquid Glass press-and-drag selection: pressing a tab lifts the lens and dragging
29
+ scrubs across tabs, selecting on release through the normal
30
+ `tabBarController(_:shouldSelect:)` path. The module MUST NOT install recognizers
31
+ that delay, cancel, or require failure of the system tab bar gestures.
32
+
33
+ The previous drag-cancellation guard (which refused selection for any touch that
34
+ moved more than 10 points) was removed because it disabled drag-to-switch.
35
+
36
+ ## Android tab bar style
37
+
38
+ `androidTabBarStyle` selects the Android presentation; iOS ignores it.
39
+
40
+ - `default` (default): Material `BottomNavigationView` docked at the bottom,
41
+ with the blur-controlled scene underlap described below.
42
+ - `floating`: a capsule bar modeled on the iOS 26 tab bar. Scene content extends
43
+ underneath it.
44
+
45
+ Floating geometry (dp): bar height 62, side margin 20 plus the navigation-bar
46
+ side insets, bottom gap `max(bottomInset, 20) + 4`, fully rounded corners. The
47
+ bottom inset is the navigation-bar inset (0 when `ignoreBottomInsets`) or the
48
+ keyboard inset when `ignoreKeyboardInsets` is false, whichever is larger. Icons
49
+ are 26, labels 10 sp in both states unless `fontSize` is set, and item content is
50
+ vertically centered. The reported tab bar height is the bar height plus its
51
+ bottom gap, or 0 while hidden.
52
+
53
+ Floating appearance with background blur disabled: the bar surface is `barTintColor` (or the theme background)
54
+ at 95% opacity (lightened slightly for dark colors), with
55
+ a hairline rim and an outline shadow. When background blur is enabled (see below), the bottom strip extends from
56
+ 24 dp above the capsule to the bottom of the scene, including the gap beneath
57
+ the capsule. The existing capsule keeps its rim/shadow and uses a 12 percent
58
+ surface fill over the blurred strip; its lens and navigation controls remain
59
+ sharp. The selected tab sits on a capsule pill inset 4 dp
60
+ (`activeIndicatorColor` when provided). The Material ripple is disabled; the
61
+ lens provides press feedback.
62
+
63
+ Floating interaction (single finger; motion splitting is disabled):
64
+
65
+ 1. Touch down on any tab moves the pill to that tab and lifts it into a lens
66
+ (grows past the bar edges, gains a rim) and magnifies that tab's content.
67
+ 2. Movement beyond touch slop starts a drag: the lens follows the finger with
68
+ edge resistance and stretches with velocity; the tab nearest the lens center
69
+ becomes the candidate, is magnified, and ticks haptically when it changes.
70
+ No page change or event happens during the drag.
71
+ 3. Release without drag performs the tab's click (selection or reselection).
72
+ Release displacement is also checked against touch slop, including when no
73
+ MOVE was delivered or the last MOVE remained within slop. If release crosses
74
+ slop, it starts a drag and updates the candidate from the release position.
75
+ Release after a drag selects the candidate once, only if it differs from the
76
+ current tab. Cancel selects nothing.
77
+ 4. The lens then settles back into the pill on the selected tab, which stays on
78
+ the old tab for cancelled or `preventsDefault` presses.
79
+
80
+ JS-driven selection changes slide the pill with a spring and velocity stretch.
81
+ Switching style at runtime rebuilds the bar and restores Material's label sizes,
82
+ icon size, ripple, padding, and gravity when returning to `default`.
83
+
84
+ ## Android bottom background blur
85
+
86
+ Status: implemented in module source. The renamed package passed TypeScript
87
+ checking, Android compilation with regenerated Fabric bindings, eight React
88
+ wrapper tests, and ten direct-native API 36 tests (five each for docked and
89
+ floating styles). Style switching with blur active received focused regression
90
+ coverage. Complete consumer-app and physical-device acceptance remain pending;
91
+ API 31-35 and visual dark-mode acceptance have not been reverified for this port.
92
+
93
+ `androidBottomBlurEnabled?: boolean` controls the native Android tab bar's live
94
+ background blur, translucent surface tint, and 24 dp fade into the page above it.
95
+ Omitting it (including `undefined`) MUST enable the effect only on Android 16
96
+ (API 36) and newer. `false` MUST restore the existing appearance and layout for the current
97
+ `androidTabBarStyle`: docked scenes reserve bar height; floating scenes retain
98
+ their existing underlap and lens gestures. `true` MAY enable it on Android 12 (API 31) and newer; older
99
+ Android versions and software-rendered windows MUST retain the existing layout
100
+ and appearance. iOS and custom JS tab bars MUST ignore this option.
101
+
102
+ When active, the selected scene MUST fill the native container behind the tab
103
+ bar; icons, labels, badges, and the style's press feedback MUST stay sharp and interactive.
104
+ The reported scene dimensions MUST include this underlap, while tab-bar height
105
+ MUST retain its existing meaning (including system/IME padding unless ignored).
106
+ Callers MUST use `useBottomTabBarHeight` to pad scrollable content when its final
107
+ row must scroll clear of the bar. Hiding the bar MUST remove the effect and
108
+ report zero bar height. Toggling, resizing, theme changes, and system/keyboard
109
+ insets MUST refresh geometry and appearance without remounting scenes.
110
+
111
+ The module owns the GPU resources on the UI thread. It MUST sample only the
112
+ scene layer, excluding the tab bar and its own output. It MUST use hardware
113
+ RenderNode/RenderEffect rendering of a bottom strip at one-quarter resolution
114
+ in each dimension, with a 20 dp blur radius and 40 dp sampling margin above the
115
+ fade. It MUST NOT capture bitmaps, read pixels back to the CPU, emit scroll
116
+ events to JS, or run a recurring frame callback for this effect. Rendering MUST
117
+ follow the normal view invalidation cycle, using the scene's existing display
118
+ lists. The translucent tint MUST follow `tabBarStyle.backgroundColor` when set,
119
+ otherwise the docked Material surface color or floating theme background,
120
+ with opacity capped at 78 percent.
121
+
122
+ Only one blur RenderNode and cached paints/shaders MAY be retained per mounted
123
+ tab view. Its offscreen dimensions MUST be bounded by one-quarter of the view
124
+ width and the smaller of the view height or reported bar footprint plus 64 dp (rounded up).
125
+ Disabling, hiding, detaching, and dropping the native view MUST discard the
126
+ display list; no scene snapshots, disk cache, timers, or background workers are
127
+ owned. SurfaceView content is outside the normal View display list and is not
128
+ supported as a blur source. Liquid Glass refraction and shape morphing are out
129
+ of scope.
130
+
131
+ Acceptance MUST cover API 35/36 default behavior and explicit overrides, live
132
+ scrolling under the bar with sharp controls, on/off restoration, hidden bars,
133
+ tab changes, resize, dark/light themes, ignored bottom insets, and both keyboard
134
+ inset modes. A representative scroll comparison MUST distinguish UI/RenderThread
135
+ timings and missed frames from visual correctness. Source/type checks and
136
+ emulator measurements MUST NOT be reported as physical-device performance.
137
+
138
+ Implementation: `src/BottomTabs.tsx` owns the public default and custom-bar guard;
139
+ `src/BottomTabsNativeComponent.ts` and `BottomTabsViewManager.kt` define the Fabric
140
+ boundary; `BottomTabsView.kt` owns scene geometry, appearance, and disposal;
141
+ `BottomBarBlurRenderer.kt` owns the bounded GPU pipeline. Focused native tests
142
+ and runner instructions are in `native-tests/android`. The example's settings
143
+ page exposes the switch and its scrolling scene reserves the reported bar height.
144
+
145
+ ## Threading and ownership
146
+
147
+ All gesture handling, animations, and timers run on the platform UI thread in
148
+ the main-runtime native view. The view owns no background-runtime or shared JS
149
+ objects. Retained state is bounded to the current touch sequence, the last
150
+ pressed tab and timestamp, and at most two running animators.
151
+
152
+ ## Conformance and acceptance
153
+
154
+ Implementation:
155
+ - iOS: `ios/RCTBottomTabsContainerView.swift` (`shouldSelectTab`,
156
+ `emitDoublePressIfNeeded`), `ios/RCTBottomTabsComponentView.mm` event forwarding.
157
+ - Android: `android/src/main/java/com/onekey/bottomtabs/BottomTabsView.kt` (floating
158
+ layout, `handleFloatingTabTouch`, `emitDoublePressIfNeeded`),
159
+ `FloatingTabBarStyle.kt`, `events/TabDoublePressEvent.kt`.
160
+
161
+ Required runtime cases per platform: stationary tap selects; reselect emits
162
+ `onPageSelected` without page change; two quick taps on one tab emit
163
+ `onTabDoublePress` exactly once; slow taps do not. iOS 26: press-and-drag across
164
+ tabs selects the tab under the finger on release. Android floating: press on a
165
+ non-selected tab lifts the lens there; drag crossing tab centers does not change
166
+ pages; release selects the nearest tab once; cancel restores; hidden bar, dark
167
+ mode, and 3-button navigation insets lay out correctly. Source compilation is
168
+ recorded separately from runtime acceptance.
@@ -0,0 +1,46 @@
1
+ import UIKit
2
+
3
+ extension Collection {
4
+ subscript(safe index: Index) -> Element? {
5
+ indices.contains(index) ? self[index] : nil
6
+ }
7
+ }
8
+
9
+ extension Collection where Element == TabItem {
10
+ func findByKey(_ key: String?) -> Element? {
11
+ guard let key else { return nil }
12
+ guard !isEmpty else { return nil }
13
+ return first { $0.key == key }
14
+ }
15
+ }
16
+
17
+ extension UIView {
18
+ func pinEdges(to other: UIView) {
19
+ NSLayoutConstraint.activate([
20
+ leadingAnchor.constraint(equalTo: other.leadingAnchor),
21
+ trailingAnchor.constraint(equalTo: other.trailingAnchor),
22
+ topAnchor.constraint(equalTo: other.topAnchor),
23
+ bottomAnchor.constraint(equalTo: other.bottomAnchor)
24
+ ])
25
+ }
26
+ }
27
+
28
+ extension UIImage {
29
+ func resizeImageTo(size: CGSize) -> UIImage? {
30
+ let renderer = UIGraphicsImageRenderer(size: size)
31
+ return renderer.image { _ in
32
+ self.draw(in: CGRect(origin: .zero, size: size))
33
+ }
34
+ }
35
+ }
36
+
37
+ extension UIColor {
38
+ convenience init(rgb: Int) {
39
+ self.init(
40
+ red: CGFloat((rgb >> 16) & 0xFF) / 255.0,
41
+ green: CGFloat((rgb >> 8) & 0xFF) / 255.0,
42
+ blue: CGFloat(rgb & 0xFF) / 255.0,
43
+ alpha: CGFloat((rgb >> 24) & 0xFF) / 255.0
44
+ )
45
+ }
46
+ }
@@ -0,0 +1,12 @@
1
+ #ifdef RCT_NEW_ARCH_ENABLED
2
+ #import <React/RCTViewComponentView.h>
3
+ #import <UIKit/UIKit.h>
4
+
5
+ NS_ASSUME_NONNULL_BEGIN
6
+
7
+ @interface RCTBottomAccessoryComponentView: RCTViewComponentView
8
+ @end
9
+
10
+ NS_ASSUME_NONNULL_END
11
+
12
+ #endif /* RCT_NEW_ARCH_ENABLED */
@@ -0,0 +1,67 @@
1
+ #import "RCTBottomAccessoryComponentView.h"
2
+
3
+ #import <react/renderer/components/RNBottomTabs/ComponentDescriptors.h>
4
+ #import <react/renderer/components/RNBottomTabs/EventEmitters.h>
5
+ #import <react/renderer/components/RNBottomTabs/Props.h>
6
+ #import <react/renderer/components/RNBottomTabs/RCTComponentViewHelpers.h>
7
+
8
+ #import <React/RCTFabricComponentsPlugins.h>
9
+
10
+ #if __has_include(<BottomTabsModule/BottomTabsModule-Swift.h>)
11
+ #import <BottomTabsModule/BottomTabsModule-Swift.h>
12
+ #elif __has_include("BottomTabsModule/BottomTabsModule-Swift.h")
13
+ #import "BottomTabsModule/BottomTabsModule-Swift.h"
14
+ #elif __has_include("BottomTabsModule-Swift.h")
15
+ #import "BottomTabsModule-Swift.h"
16
+ #else
17
+ #import "react_native_bottom_tabs-Swift.h"
18
+ #endif
19
+
20
+ using namespace facebook::react;
21
+
22
+ @implementation RCTBottomAccessoryComponentView {
23
+ UIView *_accessoryView;
24
+ }
25
+
26
+ + (ComponentDescriptorProvider)componentDescriptorProvider
27
+ {
28
+ return concreteComponentDescriptorProvider<RNBottomTabsAccessoryViewComponentDescriptor>();
29
+ }
30
+
31
+ - (instancetype)initWithFrame:(CGRect)frame
32
+ {
33
+ if (self = [super initWithFrame:frame]) {
34
+ static const auto defaultProps = std::make_shared<const RNBottomTabsAccessoryViewProps>();
35
+ _props = defaultProps;
36
+
37
+ Class containerClass = NSClassFromString(@"BottomTabsModule.RCTBottomAccessoryContainerView");
38
+ if (!containerClass) {
39
+ containerClass = NSClassFromString(@"RCTBottomAccessoryContainerView");
40
+ }
41
+ _accessoryView = [[containerClass alloc] init];
42
+ self.contentView = _accessoryView;
43
+ }
44
+
45
+ return self;
46
+ }
47
+
48
+ - (void)setFrame:(CGRect)frame
49
+ {
50
+ [super setFrame:frame];
51
+ if (frame.size.width > 0 && frame.size.height > 0) {
52
+ auto eventEmitter = std::static_pointer_cast<const RNBottomTabsAccessoryViewEventEmitter>(_eventEmitter);
53
+ if (eventEmitter) {
54
+ eventEmitter->onNativeLayout(RNBottomTabsAccessoryViewEventEmitter::OnNativeLayout {
55
+ .width = frame.size.width,
56
+ .height = frame.size.height
57
+ });
58
+ }
59
+ }
60
+ }
61
+
62
+ @end
63
+
64
+ Class<RCTComponentViewProtocol> RNBottomTabsAccessoryViewCls(void)
65
+ {
66
+ return RCTBottomAccessoryComponentView.class;
67
+ }
@@ -0,0 +1,51 @@
1
+ import Foundation
2
+ import UIKit
3
+ import React
4
+
5
+ extension Notification.Name {
6
+ static let bottomAccessoryPlacementChanged = Notification.Name("bottomAccessoryPlacementChanged")
7
+ }
8
+
9
+ class RCTBottomAccessoryContainerView: UIView {
10
+
11
+ // MARK: - Events
12
+ @objc var onNativeLayout: RCTDirectEventBlock?
13
+ @objc var onPlacementChanged: RCTDirectEventBlock?
14
+
15
+ // MARK: - Init
16
+
17
+ override init(frame: CGRect) {
18
+ super.init(frame: frame)
19
+
20
+ NotificationCenter.default.addObserver(
21
+ self,
22
+ selector: #selector(handlePlacementNotification(_:)),
23
+ name: .bottomAccessoryPlacementChanged,
24
+ object: nil
25
+ )
26
+ }
27
+
28
+ required init?(coder: NSCoder) {
29
+ fatalError("init(coder:) has not been implemented")
30
+ }
31
+
32
+ deinit {
33
+ NotificationCenter.default.removeObserver(self)
34
+ }
35
+
36
+ // MARK: - Layout
37
+
38
+ override func layoutSubviews() {
39
+ super.layoutSubviews()
40
+ let bounds = self.bounds
41
+ guard bounds.width > 0 && bounds.height > 0 else { return }
42
+ onNativeLayout?(["width": Double(bounds.width), "height": Double(bounds.height)])
43
+ }
44
+
45
+ // MARK: - Placement
46
+
47
+ @objc private func handlePlacementNotification(_ notification: Notification) {
48
+ guard let placement = notification.userInfo?["placement"] as? String else { return }
49
+ onPlacementChanged?(["placement": placement])
50
+ }
51
+ }
@@ -0,0 +1,12 @@
1
+ #ifdef RCT_NEW_ARCH_ENABLED
2
+ #import <React/RCTViewComponentView.h>
3
+ #import <UIKit/UIKit.h>
4
+
5
+ NS_ASSUME_NONNULL_BEGIN
6
+
7
+ @interface RCTBottomTabsComponentView: RCTViewComponentView
8
+ @end
9
+
10
+ NS_ASSUME_NONNULL_END
11
+
12
+ #endif /* RCT_NEW_ARCH_ENABLED */