@onekeyfe/react-native-tab-view 3.0.96 → 3.0.99
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/rcttabview/RCTTabView.kt +55 -10
- package/android/src/main/java/com/rcttabview/RCTTabViewManager.kt +2 -1
- package/android/src/main/java/com/rcttabview/TabInfo.kt +2 -1
- package/ios/RCTTabViewContainerView.swift +34 -0
- package/lib/module/TabView.js +15 -4
- package/lib/typescript/src/TabView.d.ts +8 -2
- package/package.json +1 -1
- package/src/TabView.tsx +17 -5
|
@@ -31,6 +31,8 @@ import coil3.size.Scale
|
|
|
31
31
|
import com.facebook.react.bridge.ReadableArray
|
|
32
32
|
import com.facebook.react.common.assets.ReactFontManager
|
|
33
33
|
import com.facebook.react.modules.core.ReactChoreographer
|
|
34
|
+
import com.facebook.react.uimanager.PointerEvents
|
|
35
|
+
import com.facebook.react.uimanager.ReactPointerEventsView
|
|
34
36
|
import com.facebook.react.views.text.ReactTypefaceUtils
|
|
35
37
|
import com.google.android.material.bottomnavigation.BottomNavigationView
|
|
36
38
|
import com.google.android.material.navigation.NavigationBarView.LABEL_VISIBILITY_AUTO
|
|
@@ -59,6 +61,11 @@ class ExtendedBottomNavigationView(context: Context) : BottomNavigationView(getM
|
|
|
59
61
|
}
|
|
60
62
|
}
|
|
61
63
|
|
|
64
|
+
// OneKey patch: RN hit-testing traverses INVISIBLE native wrappers unless pointer events opt out.
|
|
65
|
+
private class TabSceneContainer(context: Context) : FrameLayout(context), ReactPointerEventsView {
|
|
66
|
+
override var pointerEvents: PointerEvents = PointerEvents.NONE
|
|
67
|
+
}
|
|
68
|
+
|
|
62
69
|
class ReactBottomNavigationView(context: Context) : LinearLayout(context) {
|
|
63
70
|
private var bottomNavigation = ExtendedBottomNavigationView(context)
|
|
64
71
|
val layoutHolder = FrameLayout(context)
|
|
@@ -68,6 +75,18 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) {
|
|
|
68
75
|
var onNativeLayoutListener: ((width: Double, height: Double) -> Unit)? = null
|
|
69
76
|
var onTabBarMeasuredListener: ((height: Int) -> Unit)? = null
|
|
70
77
|
var disablePageAnimations = false
|
|
78
|
+
set(value) {
|
|
79
|
+
if (field == value) {
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
field = value
|
|
83
|
+
layoutHolder.forEachIndexed { _, view ->
|
|
84
|
+
if (view.visibility != VISIBLE) {
|
|
85
|
+
view.visibility = if (value) INVISIBLE else GONE
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
layoutHolder.requestLayout()
|
|
89
|
+
}
|
|
71
90
|
var items: MutableList<TabInfo> = mutableListOf()
|
|
72
91
|
private val iconSources: MutableMap<Int, ImageSource> = mutableMapOf()
|
|
73
92
|
private val drawableCache: MutableMap<ImageSource, Drawable> = mutableMapOf()
|
|
@@ -78,7 +97,8 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) {
|
|
|
78
97
|
private var inactiveTintColor: Int? = null
|
|
79
98
|
private val checkedStateSet = intArrayOf(android.R.attr.state_checked)
|
|
80
99
|
private val uncheckedStateSet = intArrayOf(-android.R.attr.state_checked)
|
|
81
|
-
|
|
100
|
+
// OneKey patch: rely on View.isHapticFeedbackEnabled as the single source of truth.
|
|
101
|
+
// private var hapticFeedbackEnabled = false
|
|
82
102
|
private var fontSize: Int? = null
|
|
83
103
|
private var fontFamily: String? = null
|
|
84
104
|
private var fontWeight: Int? = null
|
|
@@ -191,13 +211,13 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) {
|
|
|
191
211
|
}
|
|
192
212
|
|
|
193
213
|
private fun createContainer(): FrameLayout {
|
|
194
|
-
val container =
|
|
214
|
+
val container = TabSceneContainer(context).apply {
|
|
195
215
|
layoutParams = FrameLayout.LayoutParams(
|
|
196
216
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
197
217
|
FrameLayout.LayoutParams.MATCH_PARENT
|
|
198
218
|
)
|
|
199
219
|
isSaveEnabled = false
|
|
200
|
-
visibility = GONE
|
|
220
|
+
visibility = if (disablePageAnimations) INVISIBLE else GONE
|
|
201
221
|
isEnabled = false
|
|
202
222
|
}
|
|
203
223
|
return container
|
|
@@ -205,11 +225,24 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) {
|
|
|
205
225
|
|
|
206
226
|
private fun setSelectedIndex(itemId: Int) {
|
|
207
227
|
bottomNavigation.selectedItemId = itemId
|
|
208
|
-
if (
|
|
209
|
-
|
|
210
|
-
|
|
228
|
+
if (disablePageAnimations) {
|
|
229
|
+
// INVISIBLE scenes stay laid out, so this visibility swap is atomic at
|
|
230
|
+
// the next draw boundary and does not expose an empty container frame.
|
|
231
|
+
toggleViewVisibility(layoutHolder.getChildAt(itemId), true)
|
|
232
|
+
layoutHolder.forEachIndexed { index, view ->
|
|
233
|
+
if (itemId != index) {
|
|
234
|
+
toggleViewVisibility(view, false)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
layoutHolder.invalidate()
|
|
238
|
+
return
|
|
211
239
|
}
|
|
212
|
-
|
|
240
|
+
if (layoutHolder.getChildAt(itemId)?.visibility == VISIBLE) {
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
val fadeThrough = MaterialFadeThrough()
|
|
244
|
+
TransitionManager.beginDelayedTransition(layoutHolder, fadeThrough)
|
|
245
|
+
// Apply every visibility change to the transition's captured frame.
|
|
213
246
|
layoutHolder.forEachIndexed { index, view ->
|
|
214
247
|
if (itemId == index) {
|
|
215
248
|
toggleViewVisibility(view, true)
|
|
@@ -223,15 +256,25 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) {
|
|
|
223
256
|
}
|
|
224
257
|
|
|
225
258
|
private fun toggleViewVisibility(view: View, isVisible: Boolean) {
|
|
226
|
-
check(view is
|
|
259
|
+
check(view is TabSceneContainer) { "Native component tree is corrupted." }
|
|
227
260
|
|
|
228
|
-
view.
|
|
261
|
+
view.pointerEvents = if (isVisible) PointerEvents.AUTO else PointerEvents.NONE
|
|
262
|
+
view.visibility = if (isVisible) {
|
|
263
|
+
VISIBLE
|
|
264
|
+
} else if (disablePageAnimations) {
|
|
265
|
+
INVISIBLE
|
|
266
|
+
} else {
|
|
267
|
+
GONE
|
|
268
|
+
}
|
|
229
269
|
view.isEnabled = isVisible
|
|
230
270
|
}
|
|
231
271
|
|
|
232
272
|
private fun onTabSelected(item: MenuItem) {
|
|
233
273
|
val selectedItem = items[item.itemId]
|
|
234
274
|
selectedItem.let {
|
|
275
|
+
if (!selectedItem.preventsDefault) {
|
|
276
|
+
setSelectedItem(selectedItem.key)
|
|
277
|
+
}
|
|
235
278
|
onTabSelectedListener?.invoke(selectedItem.key)
|
|
236
279
|
emitHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
|
|
237
280
|
}
|
|
@@ -489,7 +532,9 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) {
|
|
|
489
532
|
}
|
|
490
533
|
|
|
491
534
|
private fun emitHapticFeedback(feedbackConstants: Int) {
|
|
492
|
-
|
|
535
|
+
// OneKey patch: use the Fabric-populated View flag on every supported API level.
|
|
536
|
+
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && hapticFeedbackEnabled) {
|
|
537
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && isHapticFeedbackEnabled) {
|
|
493
538
|
this.performHapticFeedback(feedbackConstants)
|
|
494
539
|
}
|
|
495
540
|
}
|
|
@@ -110,7 +110,8 @@ class RCTTabViewManager(context: ReactApplicationContext) :
|
|
|
110
110
|
badgeTextColor = if (item.hasKey("badgeTextColor")) item.getInt("badgeTextColor") else null,
|
|
111
111
|
activeTintColor = if (item.hasKey("activeTintColor")) item.getInt("activeTintColor") else null,
|
|
112
112
|
hidden = if (item.hasKey("hidden")) item.getBoolean("hidden") else false,
|
|
113
|
-
testID = item.getString("testID")
|
|
113
|
+
testID = item.getString("testID"),
|
|
114
|
+
preventsDefault = if (item.hasKey("preventsDefault")) item.getBoolean("preventsDefault") else false
|
|
114
115
|
)
|
|
115
116
|
)
|
|
116
117
|
}
|
|
@@ -731,6 +731,27 @@ class RCTTabViewContainerView: UIView {
|
|
|
731
731
|
}
|
|
732
732
|
}
|
|
733
733
|
|
|
734
|
+
// OneKey patch: keep native tab selection immediate while suppressing UIKit's content crossfade.
|
|
735
|
+
private final class ImmediateTabTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
|
|
736
|
+
static let shared = ImmediateTabTransitionAnimator()
|
|
737
|
+
|
|
738
|
+
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
|
739
|
+
0
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
|
743
|
+
guard let toViewController = transitionContext.viewController(forKey: .to),
|
|
744
|
+
let toView = transitionContext.view(forKey: .to) ?? toViewController.view else {
|
|
745
|
+
transitionContext.completeTransition(false)
|
|
746
|
+
return
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
toView.frame = transitionContext.finalFrame(for: toViewController)
|
|
750
|
+
transitionContext.containerView.addSubview(toView)
|
|
751
|
+
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
734
755
|
// MARK: - UITabBarControllerDelegate proxy
|
|
735
756
|
|
|
736
757
|
class TabViewDelegateProxy: NSObject, UITabBarControllerDelegate {
|
|
@@ -757,6 +778,19 @@ class TabViewDelegateProxy: NSObject, UITabBarControllerDelegate {
|
|
|
757
778
|
|
|
758
779
|
return false
|
|
759
780
|
}
|
|
781
|
+
|
|
782
|
+
func tabBarController(
|
|
783
|
+
_ tabBarController: UITabBarController,
|
|
784
|
+
animationControllerForTransitionFrom fromViewController: UIViewController,
|
|
785
|
+
to toViewController: UIViewController
|
|
786
|
+
) -> UIViewControllerAnimatedTransitioning? {
|
|
787
|
+
guard let containerView = mapping[ObjectIdentifier(tabBarController)],
|
|
788
|
+
containerView.disablePageAnimations else {
|
|
789
|
+
return nil
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
return ImmediateTabTransitionAnimator.shared
|
|
793
|
+
}
|
|
760
794
|
}
|
|
761
795
|
|
|
762
796
|
// MARK: - Long press gesture handler
|
package/lib/module/TabView.js
CHANGED
|
@@ -16,6 +16,7 @@ const ANDROID_MAX_TABS = 100;
|
|
|
16
16
|
const TabView = ({
|
|
17
17
|
navigationState,
|
|
18
18
|
renderScene,
|
|
19
|
+
renderLazyPlaceholder,
|
|
19
20
|
onIndexChange,
|
|
20
21
|
onTabLongPress,
|
|
21
22
|
rippleColor,
|
|
@@ -74,6 +75,7 @@ const TabView = ({
|
|
|
74
75
|
// @ts-ignore
|
|
75
76
|
const focusedKey = navigationState.routes[navigationState.index].key;
|
|
76
77
|
const customTabBarWrapperRef = useRef(null);
|
|
78
|
+
const nativeTabViewRef = useRef(null);
|
|
77
79
|
const [tabBarHeight, setTabBarHeight] = React.useState(0);
|
|
78
80
|
const [measuredDimensions, setMeasuredDimensions] = React.useState({
|
|
79
81
|
width: '100%',
|
|
@@ -199,7 +201,7 @@ const TabView = ({
|
|
|
199
201
|
if (index === -1) {
|
|
200
202
|
return;
|
|
201
203
|
}
|
|
202
|
-
onIndexChange(index);
|
|
204
|
+
return onIndexChange(index);
|
|
203
205
|
});
|
|
204
206
|
const handleTabLongPress = React.useCallback(event => {
|
|
205
207
|
const {
|
|
@@ -214,8 +216,13 @@ const TabView = ({
|
|
|
214
216
|
const {
|
|
215
217
|
key
|
|
216
218
|
} = event.nativeEvent;
|
|
217
|
-
jumpTo(key);
|
|
218
|
-
|
|
219
|
+
const accepted = jumpTo(key);
|
|
220
|
+
if (accepted === false) {
|
|
221
|
+
nativeTabViewRef.current?.setNativeProps({
|
|
222
|
+
selectedPage: focusedKey
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}, [focusedKey, jumpTo]);
|
|
219
226
|
const handleTabBarMeasured = React.useCallback(event => {
|
|
220
227
|
setTabBarHeight(event.nativeEvent.height);
|
|
221
228
|
}, [setTabBarHeight]);
|
|
@@ -239,6 +246,7 @@ const TabView = ({
|
|
|
239
246
|
return /*#__PURE__*/_jsxs(BottomTabBarHeightContext.Provider, {
|
|
240
247
|
value: tabBarHeight,
|
|
241
248
|
children: [/*#__PURE__*/_jsxs(NativeTabView, {
|
|
249
|
+
ref: nativeTabViewRef,
|
|
242
250
|
...props,
|
|
243
251
|
...tabLabelStyle,
|
|
244
252
|
style: styles.fullWidth,
|
|
@@ -264,7 +272,10 @@ const TabView = ({
|
|
|
264
272
|
}) !== false && !loaded.includes(route.key)) {
|
|
265
273
|
return /*#__PURE__*/_jsx(View, {
|
|
266
274
|
collapsable: false,
|
|
267
|
-
style: styles.fullWidth
|
|
275
|
+
style: styles.fullWidth,
|
|
276
|
+
children: renderLazyPlaceholder?.({
|
|
277
|
+
route
|
|
278
|
+
})
|
|
268
279
|
}, route.key);
|
|
269
280
|
}
|
|
270
281
|
const focused = route.key === focusedKey;
|
|
@@ -51,10 +51,16 @@ interface Props<Route extends BaseRoute> {
|
|
|
51
51
|
route: Route;
|
|
52
52
|
jumpTo: (key: string) => void;
|
|
53
53
|
}) => React.ReactNode | null;
|
|
54
|
+
/**
|
|
55
|
+
* Function which takes a route and returns a placeholder for an unloaded lazy scene.
|
|
56
|
+
*/
|
|
57
|
+
renderLazyPlaceholder?: (props: {
|
|
58
|
+
route: Route;
|
|
59
|
+
}) => React.ReactNode | null;
|
|
54
60
|
/**
|
|
55
61
|
* Callback which is called on tab change, receives the index of the new tab as argument.
|
|
56
62
|
*/
|
|
57
|
-
onIndexChange: (index: number) => void;
|
|
63
|
+
onIndexChange: (index: number) => boolean | void;
|
|
58
64
|
/**
|
|
59
65
|
* Callback which is called on long press on tab, receives the index of the tab as argument.
|
|
60
66
|
*/
|
|
@@ -179,6 +185,6 @@ interface Props<Route extends BaseRoute> {
|
|
|
179
185
|
*/
|
|
180
186
|
delayedFreeze?: boolean;
|
|
181
187
|
}
|
|
182
|
-
declare const TabView: <Route extends BaseRoute>({ navigationState, renderScene, onIndexChange, onTabLongPress, rippleColor, tabBarActiveTintColor: activeTintColor, tabBarInactiveTintColor: inactiveTintColor, getBadge, getBadgeBackgroundColor, getBadgeTextColor, getLazy, getLabelText, getIcon, getHidden, getActiveTintColor, getTestID, getRole, getSceneStyle, getPreventsDefault, hapticFeedbackEnabled, labeled, getFreezeOnBlur, tabBar: renderCustomTabBar, tabBarStyle, tabLabelStyle, renderBottomAccessoryView, activeIndicatorColor, delayedFreeze, ...props }: Props<Route>) => import("react/jsx-runtime").JSX.Element;
|
|
188
|
+
declare const TabView: <Route extends BaseRoute>({ navigationState, renderScene, renderLazyPlaceholder, onIndexChange, onTabLongPress, rippleColor, tabBarActiveTintColor: activeTintColor, tabBarInactiveTintColor: inactiveTintColor, getBadge, getBadgeBackgroundColor, getBadgeTextColor, getLazy, getLabelText, getIcon, getHidden, getActiveTintColor, getTestID, getRole, getSceneStyle, getPreventsDefault, hapticFeedbackEnabled, labeled, getFreezeOnBlur, tabBar: renderCustomTabBar, tabBarStyle, tabLabelStyle, renderBottomAccessoryView, activeIndicatorColor, delayedFreeze, ...props }: Props<Route>) => import("react/jsx-runtime").JSX.Element;
|
|
183
189
|
export default TabView;
|
|
184
190
|
//# sourceMappingURL=TabView.d.ts.map
|
package/package.json
CHANGED
package/src/TabView.tsx
CHANGED
|
@@ -81,10 +81,14 @@ interface Props<Route extends BaseRoute> {
|
|
|
81
81
|
route: Route;
|
|
82
82
|
jumpTo: (key: string) => void;
|
|
83
83
|
}) => React.ReactNode | null;
|
|
84
|
+
/**
|
|
85
|
+
* Function which takes a route and returns a placeholder for an unloaded lazy scene.
|
|
86
|
+
*/
|
|
87
|
+
renderLazyPlaceholder?: (props: { route: Route }) => React.ReactNode | null;
|
|
84
88
|
/**
|
|
85
89
|
* Callback which is called on tab change, receives the index of the new tab as argument.
|
|
86
90
|
*/
|
|
87
|
-
onIndexChange: (index: number) => void;
|
|
91
|
+
onIndexChange: (index: number) => boolean | void;
|
|
88
92
|
/**
|
|
89
93
|
* Callback which is called on long press on tab, receives the index of the tab as argument.
|
|
90
94
|
*/
|
|
@@ -199,6 +203,7 @@ const ANDROID_MAX_TABS = 100;
|
|
|
199
203
|
const TabView = <Route extends BaseRoute>({
|
|
200
204
|
navigationState,
|
|
201
205
|
renderScene,
|
|
206
|
+
renderLazyPlaceholder,
|
|
202
207
|
onIndexChange,
|
|
203
208
|
onTabLongPress,
|
|
204
209
|
rippleColor,
|
|
@@ -236,6 +241,7 @@ const TabView = <Route extends BaseRoute>({
|
|
|
236
241
|
// @ts-ignore
|
|
237
242
|
const focusedKey = navigationState.routes[navigationState.index].key;
|
|
238
243
|
const customTabBarWrapperRef = useRef<View>(null) as React.RefObject<any>;
|
|
244
|
+
const nativeTabViewRef = useRef<React.ElementRef<typeof NativeTabView>>(null);
|
|
239
245
|
const [tabBarHeight, setTabBarHeight] = React.useState<number | undefined>(0);
|
|
240
246
|
const [measuredDimensions, setMeasuredDimensions] = React.useState<
|
|
241
247
|
{ width: DimensionValue; height: DimensionValue } | undefined
|
|
@@ -381,7 +387,7 @@ const TabView = <Route extends BaseRoute>({
|
|
|
381
387
|
if (index === -1) {
|
|
382
388
|
return;
|
|
383
389
|
}
|
|
384
|
-
onIndexChange(index);
|
|
390
|
+
return onIndexChange(index);
|
|
385
391
|
});
|
|
386
392
|
|
|
387
393
|
const handleTabLongPress = React.useCallback(
|
|
@@ -398,9 +404,12 @@ const TabView = <Route extends BaseRoute>({
|
|
|
398
404
|
const handlePageSelected = React.useCallback(
|
|
399
405
|
(event: NativeSyntheticEvent<{ key: string }>) => {
|
|
400
406
|
const { key } = event.nativeEvent;
|
|
401
|
-
jumpTo(key);
|
|
407
|
+
const accepted = jumpTo(key);
|
|
408
|
+
if (accepted === false) {
|
|
409
|
+
nativeTabViewRef.current?.setNativeProps({ selectedPage: focusedKey });
|
|
410
|
+
}
|
|
402
411
|
},
|
|
403
|
-
[jumpTo]
|
|
412
|
+
[focusedKey, jumpTo],
|
|
404
413
|
);
|
|
405
414
|
|
|
406
415
|
const handleTabBarMeasured = React.useCallback(
|
|
@@ -429,6 +438,7 @@ const TabView = <Route extends BaseRoute>({
|
|
|
429
438
|
return (
|
|
430
439
|
<BottomTabBarHeightContext.Provider value={tabBarHeight}>
|
|
431
440
|
<NativeTabView
|
|
441
|
+
ref={nativeTabViewRef}
|
|
432
442
|
{...props}
|
|
433
443
|
{...tabLabelStyle}
|
|
434
444
|
style={styles.fullWidth}
|
|
@@ -458,7 +468,9 @@ const TabView = <Route extends BaseRoute>({
|
|
|
458
468
|
key={route.key}
|
|
459
469
|
collapsable={false}
|
|
460
470
|
style={styles.fullWidth}
|
|
461
|
-
|
|
471
|
+
>
|
|
472
|
+
{renderLazyPlaceholder?.({ route })}
|
|
473
|
+
</View>
|
|
462
474
|
);
|
|
463
475
|
}
|
|
464
476
|
|