@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.
@@ -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
- private var hapticFeedbackEnabled = false
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 = FrameLayout(context).apply {
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 (!disablePageAnimations) {
209
- val fadeThrough = MaterialFadeThrough()
210
- TransitionManager.beginDelayedTransition(layoutHolder, fadeThrough)
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 ViewGroup) { "Native component tree is corrupted." }
259
+ check(view is TabSceneContainer) { "Native component tree is corrupted." }
227
260
 
228
- view.visibility = if (isVisible) VISIBLE else GONE
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
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && hapticFeedbackEnabled) {
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
  }
@@ -8,5 +8,6 @@ data class TabInfo(
8
8
  val badgeTextColor: Int? = null,
9
9
  val activeTintColor: Int? = null,
10
10
  val hidden: Boolean = false,
11
- val testID: String? = null
11
+ val testID: String? = null,
12
+ val preventsDefault: Boolean = false
12
13
  )
@@ -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
@@ -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
- }, [jumpTo]);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-tab-view",
3
- "version": "3.0.96",
3
+ "version": "3.0.99",
4
4
  "description": "Native Bottom Tabs for React Native (UIKit implementation)",
5
5
  "source": "./src/index.tsx",
6
6
  "main": "./lib/module/index.js",
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