@onekeyfe/react-native-bottom-tabs 0.0.0-stage → 6.7.0-7
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/LICENSE +46 -0
- package/README.md +86 -2
- package/android/build.gradle +119 -0
- package/android/gradle.properties +4 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/AndroidManifestNew.xml +2 -0
- package/android/src/main/java/com/onekey/bottomtabs/BottomTabsPackage.kt +16 -0
- package/android/src/main/java/com/onekey/bottomtabs/BottomTabsView.kt +1424 -0
- package/android/src/main/java/com/onekey/bottomtabs/BottomTabsViewManager.kt +216 -0
- package/android/src/main/java/com/onekey/bottomtabs/FloatingTabBarStyle.kt +50 -0
- package/android/src/main/java/com/onekey/bottomtabs/ImageSource.kt +86 -0
- package/android/src/main/java/com/onekey/bottomtabs/TabInfo.kt +13 -0
- package/android/src/main/java/com/onekey/bottomtabs/Utils.kt +31 -0
- package/android/src/main/java/com/onekey/bottomtabs/events/OnNativeLayoutEvent.kt +20 -0
- package/android/src/main/java/com/onekey/bottomtabs/events/OnTabBarMeasuredEvent.kt +19 -0
- package/android/src/main/java/com/onekey/bottomtabs/events/PageSelectedEvent.kt +21 -0
- package/android/src/main/java/com/onekey/bottomtabs/events/TabDoublePressEvent.kt +19 -0
- package/common/cpp/react/renderer/components/RNBottomTabs/RNBottomTabsViewComponentDescriptor.h +32 -0
- package/common/cpp/react/renderer/components/RNBottomTabs/RNBottomTabsViewShadowNode.cpp +18 -0
- package/common/cpp/react/renderer/components/RNBottomTabs/RNBottomTabsViewShadowNode.h +35 -0
- package/common/cpp/react/renderer/components/RNBottomTabs/RNBottomTabsViewState.cpp +15 -0
- package/common/cpp/react/renderer/components/RNBottomTabs/RNBottomTabsViewState.h +25 -0
- package/docs/SPEC.md +104 -0
- package/ios/Extensions.swift +46 -0
- package/ios/RCTBottomAccessoryComponentView.h +12 -0
- package/ios/RCTBottomAccessoryComponentView.mm +67 -0
- package/ios/RCTBottomAccessoryContainerView.swift +51 -0
- package/ios/RCTBottomTabsComponentView.h +12 -0
- package/ios/RCTBottomTabsComponentView.mm +338 -0
- package/ios/RCTBottomTabsContainerView.swift +777 -0
- package/ios/RCTBottomTabsLog.h +7 -0
- package/ios/RCTBottomTabsLog.m +32 -0
- package/ios/SVG/CoreSVG.h +13 -0
- package/ios/SVG/CoreSVG.mm +177 -0
- package/ios/SVG/SvgDecoder.h +10 -0
- package/ios/SVG/SvgDecoder.mm +32 -0
- package/ios/TabBarFontSize.swift +55 -0
- package/lib/module/BottomAccessoryView.js +45 -0
- package/lib/module/BottomAccessoryViewNativeComponent.ts +27 -0
- package/lib/module/BottomTabs.js +323 -0
- package/lib/module/BottomTabsNativeComponent.ts +69 -0
- package/lib/module/DelayedFreeze.js +31 -0
- package/lib/module/NativeSVGDecoder.js +5 -0
- package/lib/module/SceneMap.js +28 -0
- package/lib/module/codegen-types.d.js +2 -0
- package/lib/module/index.js +20 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/utils/BottomTabBarHeightContext.js +5 -0
- package/lib/module/utils/useBottomTabBarHeight.js +12 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/BottomAccessoryView.d.ts +8 -0
- package/lib/typescript/src/BottomAccessoryViewNativeComponent.d.ts +16 -0
- package/lib/typescript/src/BottomTabs.d.ts +202 -0
- package/lib/typescript/src/BottomTabsNativeComponent.d.ts +58 -0
- package/lib/typescript/src/DelayedFreeze.d.ts +9 -0
- package/lib/typescript/src/NativeSVGDecoder.d.ts +6 -0
- package/lib/typescript/src/SceneMap.d.ts +10 -0
- package/lib/typescript/src/index.d.ts +16 -0
- package/lib/typescript/src/types.d.ts +29 -0
- package/lib/typescript/src/utils/BottomTabBarHeightContext.d.ts +3 -0
- package/lib/typescript/src/utils/useBottomTabBarHeight.d.ts +2 -0
- package/package.json +117 -4
- package/react-native-bottom-tabs.podspec +36 -0
- package/react-native.config.js +13 -0
- package/src/BottomAccessoryView.tsx +58 -0
- package/src/BottomAccessoryViewNativeComponent.ts +27 -0
- package/src/BottomTabs.tsx +545 -0
- package/src/BottomTabsNativeComponent.ts +69 -0
- package/src/DelayedFreeze.tsx +33 -0
- package/src/NativeSVGDecoder.ts +5 -0
- package/src/SceneMap.tsx +34 -0
- package/src/codegen-types.d.ts +28 -0
- package/src/index.tsx +18 -0
- package/src/types.ts +31 -0
- package/src/utils/BottomTabBarHeightContext.ts +5 -0
- package/src/utils/useBottomTabBarHeight.ts +15 -0
package/docs/SPEC.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
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
|
+
unchanged behavior.
|
|
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: 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. Android has no system backdrop material,
|
|
56
|
+
so no blur is applied. The selected tab sits on a capsule pill inset 4 dp
|
|
57
|
+
(`activeIndicatorColor` when provided). The Material ripple is disabled; the
|
|
58
|
+
lens provides press feedback.
|
|
59
|
+
|
|
60
|
+
Floating interaction (single finger; motion splitting is disabled):
|
|
61
|
+
|
|
62
|
+
1. Touch down on any tab moves the pill to that tab and lifts it into a lens
|
|
63
|
+
(grows past the bar edges, gains a rim) and magnifies that tab's content.
|
|
64
|
+
2. Movement beyond touch slop starts a drag: the lens follows the finger with
|
|
65
|
+
edge resistance and stretches with velocity; the tab nearest the lens center
|
|
66
|
+
becomes the candidate, is magnified, and ticks haptically when it changes.
|
|
67
|
+
No page change or event happens during the drag.
|
|
68
|
+
3. Release without drag performs the tab's click (selection or reselection).
|
|
69
|
+
Release displacement is also checked against touch slop, including when no
|
|
70
|
+
MOVE was delivered or the last MOVE remained within slop. If release crosses
|
|
71
|
+
slop, it starts a drag and updates the candidate from the release position.
|
|
72
|
+
Release after a drag selects the candidate once, only if it differs from the
|
|
73
|
+
current tab. Cancel selects nothing.
|
|
74
|
+
4. The lens then settles back into the pill on the selected tab, which stays on
|
|
75
|
+
the old tab for cancelled or `preventsDefault` presses.
|
|
76
|
+
|
|
77
|
+
JS-driven selection changes slide the pill with a spring and velocity stretch.
|
|
78
|
+
Switching style at runtime rebuilds the bar and restores Material's label sizes,
|
|
79
|
+
icon size, ripple, padding, and gravity when returning to `default`.
|
|
80
|
+
|
|
81
|
+
## Threading and ownership
|
|
82
|
+
|
|
83
|
+
All gesture handling, animations, and timers run on the platform UI thread in
|
|
84
|
+
the main-runtime native view. The view owns no background-runtime or shared JS
|
|
85
|
+
objects. Retained state is bounded to the current touch sequence, the last
|
|
86
|
+
pressed tab and timestamp, and at most two running animators.
|
|
87
|
+
|
|
88
|
+
## Conformance and acceptance
|
|
89
|
+
|
|
90
|
+
Implementation:
|
|
91
|
+
- iOS: `ios/RCTBottomTabsContainerView.swift` (`shouldSelectTab`,
|
|
92
|
+
`emitDoublePressIfNeeded`), `ios/RCTBottomTabsComponentView.mm` event forwarding.
|
|
93
|
+
- Android: `android/src/main/java/com/onekey/bottomtabs/BottomTabsView.kt` (floating
|
|
94
|
+
layout, `handleFloatingTabTouch`, `emitDoublePressIfNeeded`),
|
|
95
|
+
`FloatingTabBarStyle.kt`, `events/TabDoublePressEvent.kt`.
|
|
96
|
+
|
|
97
|
+
Required runtime cases per platform: stationary tap selects; reselect emits
|
|
98
|
+
`onPageSelected` without page change; two quick taps on one tab emit
|
|
99
|
+
`onTabDoublePress` exactly once; slow taps do not. iOS 26: press-and-drag across
|
|
100
|
+
tabs selects the tab under the finger on release. Android floating: press on a
|
|
101
|
+
non-selected tab lifts the lens there; drag crossing tab centers does not change
|
|
102
|
+
pages; release selects the nearest tab once; cancel restores; hidden bar, dark
|
|
103
|
+
mode, and 3-button navigation insets lay out correctly. Source compilation is
|
|
104
|
+
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 */
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
#import "RCTBottomTabsComponentView.h"
|
|
2
|
+
|
|
3
|
+
#import <react/renderer/components/RNBottomTabs/RNBottomTabsViewComponentDescriptor.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
|
+
#import <React/RCTConversions.h>
|
|
10
|
+
#import <React/RCTImageLoader.h>
|
|
11
|
+
#import <React/RCTImageSource.h>
|
|
12
|
+
#import <React/RCTBridge+Private.h>
|
|
13
|
+
#import <react/utils/ManagedObjectWrapper.h>
|
|
14
|
+
|
|
15
|
+
#if __has_include(<BottomTabsModule/BottomTabsModule-Swift.h>)
|
|
16
|
+
#import <BottomTabsModule/BottomTabsModule-Swift.h>
|
|
17
|
+
#elif __has_include("BottomTabsModule/BottomTabsModule-Swift.h")
|
|
18
|
+
#import "BottomTabsModule/BottomTabsModule-Swift.h"
|
|
19
|
+
#elif __has_include("BottomTabsModule-Swift.h")
|
|
20
|
+
#import "BottomTabsModule-Swift.h"
|
|
21
|
+
#else
|
|
22
|
+
#import "react_native_bottom_tabs-Swift.h"
|
|
23
|
+
#endif
|
|
24
|
+
|
|
25
|
+
using namespace facebook::react;
|
|
26
|
+
|
|
27
|
+
static inline NSString* _Nullable NSStringFromStdStringNilIfEmpty(const std::string &string) {
|
|
28
|
+
return string.empty() ? nil : [NSString stringWithUTF8String:string.c_str()];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Overload `==` and `!=` operators for `RNBottomTabsViewItemsStruct`
|
|
32
|
+
namespace facebook::react {
|
|
33
|
+
|
|
34
|
+
bool operator==(const RNBottomTabsViewItemsStruct& lhs, const RNBottomTabsViewItemsStruct& rhs) {
|
|
35
|
+
return lhs.key == rhs.key &&
|
|
36
|
+
lhs.title == rhs.title &&
|
|
37
|
+
lhs.sfSymbol == rhs.sfSymbol &&
|
|
38
|
+
lhs.badge == rhs.badge &&
|
|
39
|
+
lhs.activeTintColor == rhs.activeTintColor &&
|
|
40
|
+
lhs.hidden == rhs.hidden &&
|
|
41
|
+
lhs.testID == rhs.testID &&
|
|
42
|
+
lhs.role == rhs.role &&
|
|
43
|
+
lhs.preventsDefault == rhs.preventsDefault;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
bool operator!=(const RNBottomTabsViewItemsStruct& lhs, const RNBottomTabsViewItemsStruct& rhs) {
|
|
47
|
+
return !(lhs == rhs);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static NSNumber* _Nullable colorToProcessedInt(const SharedColor &color) {
|
|
53
|
+
UIColor *uiColor = RCTUIColorFromSharedColor(color);
|
|
54
|
+
if (!uiColor) return nil;
|
|
55
|
+
CGFloat r, g, b, a;
|
|
56
|
+
[uiColor getRed:&r green:&g blue:&b alpha:&a];
|
|
57
|
+
int32_t colorInt = ((int)(a * 255) << 24) | ((int)(r * 255) << 16) | ((int)(g * 255) << 8) | (int)(b * 255);
|
|
58
|
+
return @(colorInt);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
static NSArray<NSDictionary *>* convertItemsToArray(const std::vector<RNBottomTabsViewItemsStruct>& items) {
|
|
62
|
+
NSMutableArray *result = [NSMutableArray arrayWithCapacity:items.size()];
|
|
63
|
+
|
|
64
|
+
for (const auto& item : items) {
|
|
65
|
+
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
|
|
66
|
+
dict[@"key"] = [NSString stringWithUTF8String:item.key.c_str()];
|
|
67
|
+
dict[@"title"] = [NSString stringWithUTF8String:item.title.c_str()];
|
|
68
|
+
|
|
69
|
+
if (!item.sfSymbol.empty()) {
|
|
70
|
+
dict[@"sfSymbol"] = [NSString stringWithUTF8String:item.sfSymbol.c_str()];
|
|
71
|
+
}
|
|
72
|
+
if (!item.badge.empty()) {
|
|
73
|
+
dict[@"badge"] = [NSString stringWithUTF8String:item.badge.c_str()];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
NSNumber *activeTint = colorToProcessedInt(item.activeTintColor);
|
|
77
|
+
if (activeTint) dict[@"activeTintColor"] = activeTint;
|
|
78
|
+
|
|
79
|
+
NSNumber *badgeBg = colorToProcessedInt(item.badgeBackgroundColor);
|
|
80
|
+
if (badgeBg) dict[@"badgeBackgroundColor"] = badgeBg;
|
|
81
|
+
|
|
82
|
+
NSNumber *badgeTxt = colorToProcessedInt(item.badgeTextColor);
|
|
83
|
+
if (badgeTxt) dict[@"badgeTextColor"] = badgeTxt;
|
|
84
|
+
|
|
85
|
+
dict[@"hidden"] = @(item.hidden);
|
|
86
|
+
dict[@"preventsDefault"] = @(item.preventsDefault);
|
|
87
|
+
|
|
88
|
+
if (!item.testID.empty()) {
|
|
89
|
+
dict[@"testID"] = [NSString stringWithUTF8String:item.testID.c_str()];
|
|
90
|
+
}
|
|
91
|
+
if (!item.role.empty()) {
|
|
92
|
+
dict[@"role"] = [NSString stringWithUTF8String:item.role.c_str()];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
[result addObject:dict];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@interface RCTBottomTabsComponentView () <RCTRNBottomTabsViewViewProtocol>
|
|
102
|
+
@end
|
|
103
|
+
|
|
104
|
+
@implementation RCTBottomTabsComponentView {
|
|
105
|
+
UIView *_containerView;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider
|
|
109
|
+
{
|
|
110
|
+
return concreteComponentDescriptorProvider<RNBottomTabsViewComponentDescriptor>();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
- (instancetype)initWithFrame:(CGRect)frame
|
|
114
|
+
{
|
|
115
|
+
if (self = [super initWithFrame:frame]) {
|
|
116
|
+
static const auto defaultProps = std::make_shared<const RNBottomTabsViewProps>();
|
|
117
|
+
_props = defaultProps;
|
|
118
|
+
|
|
119
|
+
Class containerClass = NSClassFromString(@"BottomTabsModule.RCTBottomTabsContainerView");
|
|
120
|
+
if (!containerClass) {
|
|
121
|
+
containerClass = NSClassFromString(@"RCTBottomTabsContainerView");
|
|
122
|
+
}
|
|
123
|
+
_containerView = [[containerClass alloc] init];
|
|
124
|
+
self.contentView = _containerView;
|
|
125
|
+
|
|
126
|
+
// Set up event forwarding from Swift container to Fabric EventEmitter.
|
|
127
|
+
// Swift container uses RCTDirectEventBlock (void (^)(NSDictionary *)) for events,
|
|
128
|
+
// but in Fabric mode we need to forward them through the C++ EventEmitter.
|
|
129
|
+
typedef void (^EventBlock)(NSDictionary *);
|
|
130
|
+
__weak auto weakSelf = self;
|
|
131
|
+
|
|
132
|
+
EventBlock onPageSelected = ^(NSDictionary *body) {
|
|
133
|
+
auto strongSelf = weakSelf;
|
|
134
|
+
if (!strongSelf) return;
|
|
135
|
+
auto emitter = std::static_pointer_cast<const RNBottomTabsViewEventEmitter>(strongSelf->_eventEmitter);
|
|
136
|
+
if (emitter) {
|
|
137
|
+
emitter->onPageSelected({.key = std::string([[body objectForKey:@"key"] UTF8String] ?: "")});
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
[_containerView setValue:onPageSelected forKey:@"onPageSelected"];
|
|
141
|
+
|
|
142
|
+
EventBlock onTabDoublePress = ^(NSDictionary *body) {
|
|
143
|
+
auto strongSelf = weakSelf;
|
|
144
|
+
if (!strongSelf) return;
|
|
145
|
+
auto emitter = std::static_pointer_cast<const RNBottomTabsViewEventEmitter>(strongSelf->_eventEmitter);
|
|
146
|
+
if (emitter) {
|
|
147
|
+
emitter->onTabDoublePress({.key = std::string([[body objectForKey:@"key"] UTF8String] ?: "")});
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
[_containerView setValue:onTabDoublePress forKey:@"onTabDoublePress"];
|
|
151
|
+
|
|
152
|
+
EventBlock onTabBarMeasured = ^(NSDictionary *body) {
|
|
153
|
+
auto strongSelf = weakSelf;
|
|
154
|
+
if (!strongSelf) return;
|
|
155
|
+
auto emitter = std::static_pointer_cast<const RNBottomTabsViewEventEmitter>(strongSelf->_eventEmitter);
|
|
156
|
+
if (emitter) {
|
|
157
|
+
emitter->onTabBarMeasured({.height = [[body objectForKey:@"height"] intValue]});
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
[_containerView setValue:onTabBarMeasured forKey:@"onTabBarMeasured"];
|
|
161
|
+
|
|
162
|
+
EventBlock onNativeLayout = ^(NSDictionary *body) {
|
|
163
|
+
auto strongSelf = weakSelf;
|
|
164
|
+
if (!strongSelf) return;
|
|
165
|
+
auto emitter = std::static_pointer_cast<const RNBottomTabsViewEventEmitter>(strongSelf->_eventEmitter);
|
|
166
|
+
if (emitter) {
|
|
167
|
+
emitter->onNativeLayout({
|
|
168
|
+
.width = [[body objectForKey:@"width"] doubleValue],
|
|
169
|
+
.height = [[body objectForKey:@"height"] doubleValue]
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
[_containerView setValue:onNativeLayout forKey:@"onNativeLayout"];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return self;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
+ (BOOL)shouldBeRecycled
|
|
180
|
+
{
|
|
181
|
+
return NO;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index {
|
|
185
|
+
SEL sel = @selector(insertChild:atIndex:);
|
|
186
|
+
if ([_containerView respondsToSelector:sel]) {
|
|
187
|
+
NSMethodSignature *sig = [_containerView methodSignatureForSelector:sel];
|
|
188
|
+
NSInvocation *inv = [NSInvocation invocationWithMethodSignature:sig];
|
|
189
|
+
[inv setSelector:sel];
|
|
190
|
+
[inv setTarget:_containerView];
|
|
191
|
+
UIView *child = childComponentView;
|
|
192
|
+
[inv setArgument:&child atIndex:2];
|
|
193
|
+
[inv setArgument:&index atIndex:3];
|
|
194
|
+
[inv invoke];
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index {
|
|
199
|
+
SEL sel = @selector(removeChildAtIndex:);
|
|
200
|
+
if ([_containerView respondsToSelector:sel]) {
|
|
201
|
+
NSMethodSignature *sig = [_containerView methodSignatureForSelector:sel];
|
|
202
|
+
NSInvocation *inv = [NSInvocation invocationWithMethodSignature:sig];
|
|
203
|
+
[inv setSelector:sel];
|
|
204
|
+
[inv setTarget:_containerView];
|
|
205
|
+
[inv setArgument:&index atIndex:2];
|
|
206
|
+
[inv invoke];
|
|
207
|
+
}
|
|
208
|
+
[childComponentView removeFromSuperview];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
|
|
212
|
+
{
|
|
213
|
+
const auto &oldViewProps = *std::static_pointer_cast<RNBottomTabsViewProps const>(_props);
|
|
214
|
+
const auto &newViewProps = *std::static_pointer_cast<RNBottomTabsViewProps const>(props);
|
|
215
|
+
|
|
216
|
+
#define SET_PROP(prop, value) \
|
|
217
|
+
if ([_containerView respondsToSelector:@selector(setProp:)]) { \
|
|
218
|
+
[_containerView setValue:value forKey:@#prop]; \
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (oldViewProps.items != newViewProps.items) {
|
|
222
|
+
[_containerView setValue:convertItemsToArray(newViewProps.items) forKey:@"items"];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (oldViewProps.translucent != newViewProps.translucent) {
|
|
226
|
+
[_containerView setValue:@(newViewProps.translucent) forKey:@"translucent"];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (oldViewProps.icons != newViewProps.icons) {
|
|
230
|
+
auto iconsArray = [[NSMutableArray alloc] init];
|
|
231
|
+
for (auto &source: newViewProps.icons) {
|
|
232
|
+
NSMutableDictionary *iconDict = [NSMutableDictionary dictionary];
|
|
233
|
+
iconDict[@"uri"] = [NSString stringWithUTF8String:source.uri.c_str()];
|
|
234
|
+
iconDict[@"width"] = @(source.size.width);
|
|
235
|
+
iconDict[@"height"] = @(source.size.height);
|
|
236
|
+
iconDict[@"scale"] = @(source.scale);
|
|
237
|
+
[iconsArray addObject:iconDict];
|
|
238
|
+
}
|
|
239
|
+
[_containerView setValue:iconsArray forKey:@"icons"];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (oldViewProps.selectedIcons != newViewProps.selectedIcons) {
|
|
243
|
+
auto selectedIconsArray = [[NSMutableArray alloc] init];
|
|
244
|
+
for (auto &source: newViewProps.selectedIcons) {
|
|
245
|
+
NSMutableDictionary *iconDict = [NSMutableDictionary dictionary];
|
|
246
|
+
iconDict[@"uri"] = [NSString stringWithUTF8String:source.uri.c_str()];
|
|
247
|
+
iconDict[@"width"] = @(source.size.width);
|
|
248
|
+
iconDict[@"height"] = @(source.size.height);
|
|
249
|
+
iconDict[@"scale"] = @(source.scale);
|
|
250
|
+
[selectedIconsArray addObject:iconDict];
|
|
251
|
+
}
|
|
252
|
+
[_containerView setValue:selectedIconsArray forKey:@"selectedIcons"];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (oldViewProps.sidebarAdaptable != newViewProps.sidebarAdaptable) {
|
|
256
|
+
[_containerView setValue:@(newViewProps.sidebarAdaptable) forKey:@"sidebarAdaptable"];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (oldViewProps.minimizeBehavior != newViewProps.minimizeBehavior) {
|
|
260
|
+
[_containerView setValue:NSStringFromStdStringNilIfEmpty(newViewProps.minimizeBehavior) forKey:@"minimizeBehavior"];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (oldViewProps.disablePageAnimations != newViewProps.disablePageAnimations) {
|
|
264
|
+
[_containerView setValue:@(newViewProps.disablePageAnimations) forKey:@"disablePageAnimations"];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (oldViewProps.labeled != newViewProps.labeled) {
|
|
268
|
+
[_containerView setValue:@(newViewProps.labeled) forKey:@"labeled"];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (oldViewProps.selectedPage != newViewProps.selectedPage) {
|
|
272
|
+
[_containerView setValue:[NSString stringWithUTF8String:newViewProps.selectedPage.c_str()] forKey:@"selectedPage"];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (oldViewProps.scrollEdgeAppearance != newViewProps.scrollEdgeAppearance) {
|
|
276
|
+
[_containerView setValue:NSStringFromStdStringNilIfEmpty(newViewProps.scrollEdgeAppearance) forKey:@"scrollEdgeAppearance"];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (oldViewProps.barTintColor != newViewProps.barTintColor) {
|
|
280
|
+
[_containerView setValue:RCTUIColorFromSharedColor(newViewProps.barTintColor) forKey:@"barTintColor"];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (oldViewProps.activeTintColor != newViewProps.activeTintColor) {
|
|
284
|
+
[_containerView setValue:RCTUIColorFromSharedColor(newViewProps.activeTintColor) forKey:@"activeTintColor"];
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (oldViewProps.inactiveTintColor != newViewProps.inactiveTintColor) {
|
|
288
|
+
[_containerView setValue:RCTUIColorFromSharedColor(newViewProps.inactiveTintColor) forKey:@"inactiveTintColor"];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (oldViewProps.hapticFeedbackEnabled != newViewProps.hapticFeedbackEnabled) {
|
|
292
|
+
[_containerView setValue:@(newViewProps.hapticFeedbackEnabled) forKey:@"hapticFeedbackEnabled"];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (oldViewProps.fontSize != newViewProps.fontSize) {
|
|
296
|
+
[_containerView setValue:@(newViewProps.fontSize) forKey:@"fontSize"];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (oldViewProps.fontWeight != newViewProps.fontWeight) {
|
|
300
|
+
[_containerView setValue:NSStringFromStdStringNilIfEmpty(newViewProps.fontWeight) forKey:@"fontWeight"];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (oldViewProps.fontFamily != newViewProps.fontFamily) {
|
|
304
|
+
[_containerView setValue:NSStringFromStdStringNilIfEmpty(newViewProps.fontFamily) forKey:@"fontFamily"];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (oldViewProps.tabBarHidden != newViewProps.tabBarHidden) {
|
|
308
|
+
[_containerView setValue:@(newViewProps.tabBarHidden) forKey:@"tabBarHidden"];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
#undef SET_PROP
|
|
312
|
+
|
|
313
|
+
[super updateProps:props oldProps:oldProps];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
- (void)updateState:(const facebook::react::State::Shared &)state oldState:(const facebook::react::State::Shared &)oldState
|
|
317
|
+
{
|
|
318
|
+
auto _state = std::static_pointer_cast<RNBottomTabsViewShadowNode::ConcreteState const>(state);
|
|
319
|
+
auto data = _state->getData();
|
|
320
|
+
if (auto imgLoaderPtr = _state.get()->getData().getImageLoader().lock()) {
|
|
321
|
+
id imageLoader = unwrapManagedObject(imgLoaderPtr);
|
|
322
|
+
if ([_containerView respondsToSelector:@selector(setImageLoader:)]) {
|
|
323
|
+
[_containerView performSelector:@selector(setImageLoader:) withObject:imageLoader];
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
- (void)prepareForRecycle
|
|
329
|
+
{
|
|
330
|
+
[super prepareForRecycle];
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
@end
|
|
334
|
+
|
|
335
|
+
Class<RCTComponentViewProtocol> RNBottomTabsViewCls(void)
|
|
336
|
+
{
|
|
337
|
+
return RCTBottomTabsComponentView.class;
|
|
338
|
+
}
|