@onekeyfe/react-native-bottom-tabs 6.7.0-alpha.339 → 6.7.0-alpha.341

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/README.md CHANGED
@@ -48,6 +48,14 @@ Some appearance and interaction options are platform-specific, including Apple
48
48
  symbols and newer iOS tab-bar presentation. Check those options against the
49
49
  consumer's supported OS versions rather than assuming identical native layouts.
50
50
 
51
+ On Android, `androidBottomBlurEnabled` controls live background blur, translucent
52
+ tint, and a gradient above the native bar. It defaults to enabled on Android 16+
53
+ (API 36); pass `false` to disable it or `true` to enable it on Android 12–15.
54
+ Earlier Android versions retain the standard bar. It is ignored on iOS and with
55
+ a custom `tabBar`. When enabled, scenes extend behind the bar: use
56
+ `useBottomTabBarHeight()` as scroll-content bottom padding so the last row can
57
+ scroll above it. `tabBarStyle.backgroundColor` sets the surface tint.
58
+
51
59
  See [src/BottomTabs.tsx](src/BottomTabs.tsx) for component props and
52
60
  [src/types.ts](src/types.ts) for route definitions.
53
61
 
@@ -0,0 +1,92 @@
1
+ package com.onekey.bottomtabs
2
+
3
+ import android.graphics.BlendMode
4
+ import android.graphics.Canvas
5
+ import android.graphics.Color
6
+ import android.graphics.LinearGradient
7
+ import android.graphics.Paint
8
+ import android.graphics.RenderEffect
9
+ import android.graphics.RenderNode
10
+ import android.graphics.Shader
11
+ import androidx.annotation.RequiresApi
12
+ import kotlin.math.ceil
13
+ import kotlin.math.max
14
+
15
+ /** A GPU-only backdrop. The source callback must draw scenes, never this overlay. */
16
+ @RequiresApi(31)
17
+ internal class BottomBarBlurRenderer(private val density: Float) {
18
+ private val node = RenderNode("TabBarBackdrop")
19
+ private val tintPaint = Paint()
20
+ private val maskPaint = Paint().apply { blendMode = BlendMode.DST_IN }
21
+ private var lastWidth = 0
22
+ private var lastBarTop = -1
23
+ private var lastBottom = 0
24
+ private var lastTint = Color.TRANSPARENT
25
+ private var fadeTop = 0f
26
+ private var sampleTop = 0f
27
+
28
+ init {
29
+ node.setRenderEffect(RenderEffect.createBlurEffect(
30
+ 20f * density / SCALE,
31
+ 20f * density / SCALE,
32
+ Shader.TileMode.CLAMP,
33
+ ))
34
+ }
35
+
36
+ fun draw(
37
+ canvas: Canvas,
38
+ width: Int,
39
+ barTop: Int,
40
+ bottom: Int,
41
+ tint: Int,
42
+ drawScenes: (Canvas) -> Unit,
43
+ ) {
44
+ if (!canvas.isHardwareAccelerated || width <= 0 || bottom <= barTop) return
45
+
46
+ if (width != lastWidth || barTop != lastBarTop || bottom != lastBottom || tint != lastTint) {
47
+ fadeTop = max(0f, barTop - 24f * density)
48
+ sampleTop = max(0f, fadeTop - 40f * density)
49
+ node.setPosition(0, 0, ceil(width / SCALE).toInt(), ceil((bottom - sampleTop) / SCALE).toInt())
50
+ maskPaint.shader = LinearGradient(
51
+ 0f, fadeTop, 0f, barTop.toFloat(),
52
+ Color.TRANSPARENT, Color.WHITE, Shader.TileMode.CLAMP,
53
+ )
54
+ val surface = (tint and 0x00ffffff) or (minOf(Color.alpha(tint), 199) shl 24)
55
+ tintPaint.color = surface
56
+ lastWidth = width
57
+ lastBarTop = barTop
58
+ lastBottom = bottom
59
+ lastTint = tint
60
+ }
61
+
62
+ val recording = node.beginRecording()
63
+ try {
64
+ // Reuse child View display lists. No bitmap capture or full-screen blur.
65
+ recording.scale(1f / SCALE, 1f / SCALE)
66
+ recording.translate(0f, -sampleTop)
67
+ recording.drawColor(tint or (0xff shl 24))
68
+ drawScenes(recording)
69
+ } finally {
70
+ node.endRecording()
71
+ }
72
+
73
+ val layer = canvas.saveLayer(0f, fadeTop, width.toFloat(), bottom.toFloat(), null)
74
+ val transform = canvas.save()
75
+ canvas.translate(0f, sampleTop)
76
+ canvas.scale(SCALE, SCALE)
77
+ canvas.drawRenderNode(node)
78
+ canvas.restoreToCount(transform)
79
+ canvas.drawRect(0f, fadeTop, width.toFloat(), bottom.toFloat(), tintPaint)
80
+ // Fade the combined blur and tint, keeping the transition free of a hard seam.
81
+ canvas.drawRect(0f, fadeTop, width.toFloat(), bottom.toFloat(), maskPaint)
82
+ canvas.restoreToCount(layer)
83
+ }
84
+
85
+ fun release() {
86
+ node.discardDisplayList()
87
+ }
88
+
89
+ private companion object {
90
+ const val SCALE = 4f
91
+ }
92
+ }
@@ -7,6 +7,7 @@ import android.content.res.ColorStateList
7
7
  import android.content.res.Configuration
8
8
  import androidx.appcompat.view.ContextThemeWrapper
9
9
  import android.graphics.Color
10
+ import android.graphics.Canvas
10
11
  import android.graphics.Outline
11
12
  import android.graphics.drawable.ColorDrawable
12
13
  import android.graphics.drawable.Drawable
@@ -150,7 +151,16 @@ private enum class AndroidTabBarStyle {
150
151
  class BottomTabsView(context: Context) : LinearLayout(context) {
151
152
  private var bottomNavigation = ExtendedBottomNavigationView(context)
152
153
  val layoutHolder = FrameLayout(context)
153
- private val floatingRoot = FrameLayout(context)
154
+ private val floatingRoot = object : FrameLayout(context) {
155
+ override fun drawChild(canvas: Canvas, child: View, drawingTime: Long): Boolean {
156
+ if (child === floatingBar && usesBottomBlur) {
157
+ drawBlurBackdrop(canvas, width, floatingBar.top, height) { recording ->
158
+ super.drawChild(recording, layoutHolder, drawingTime)
159
+ }
160
+ }
161
+ return super.drawChild(canvas, child, drawingTime)
162
+ }
163
+ }
154
164
  private val floatingBar = FrameLayout(context)
155
165
  private val floatingBarBackground = View(context)
156
166
  private val floatingIndicator = View(context)
@@ -190,6 +200,7 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
190
200
  private var fontWeight: Int? = null
191
201
  private var labeled: Boolean? = null
192
202
  private var lastReportedSize: Size? = null
203
+ private var lastReportedTabBarHeight = -1
193
204
  private var hasCustomAppearance = false
194
205
  private var uiModeConfiguration: Int = Configuration.UI_MODE_NIGHT_UNDEFINED
195
206
  private var androidTabBarStyle = AndroidTabBarStyle.DEFAULT
@@ -246,6 +257,19 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
246
257
  }
247
258
  .build()
248
259
 
260
+ private var androidBottomBlurEnabled = false
261
+ private var bottomBarBlur: BottomBarBlurRenderer? = null
262
+ private var blurAppearanceApplied = false
263
+ private var floatingBlurAppearanceApplied = false
264
+ private var blurSurfaceColor = Color.WHITE
265
+ private var savedBarTint: ColorStateList? = null
266
+ private var savedItemBackground: Drawable? = null
267
+ private var savedBarElevation = 0f
268
+
269
+ private val usesBottomBlur: Boolean
270
+ get() = androidBottomBlurEnabled && Build.VERSION.SDK_INT >= 31 &&
271
+ isHardwareAccelerated && !isTabBarHidden
272
+
249
273
  init {
250
274
  orientation = VERTICAL
251
275
  layoutHolder.isSaveEnabled = false
@@ -288,26 +312,7 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
288
312
  insets
289
313
  }
290
314
 
291
- post {
292
- addOnLayoutChangeListener { _, left, top, right, bottom,
293
- _, _, _, _ ->
294
- val newWidth = right - left
295
- val newHeight = bottom - top
296
-
297
- // Notify about tab bar height.
298
- onTabBarMeasuredListener?.invoke(
299
- Utils.convertPixelsToDp(context, getTabBarMeasuredHeight()).toInt()
300
- )
301
-
302
- if (newWidth != lastReportedSize?.width || newHeight != lastReportedSize?.height) {
303
- val dpWidth = Utils.convertPixelsToDp(context, layoutHolder.width)
304
- val dpHeight = Utils.convertPixelsToDp(context, layoutHolder.height)
305
315
 
306
- onNativeLayoutListener?.invoke(dpWidth, dpHeight)
307
- lastReportedSize = Size(newWidth, newHeight)
308
- }
309
- }
310
- }
311
316
  }
312
317
 
313
318
  private fun dp(value: Float): Int =
@@ -318,6 +323,7 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
318
323
  }
319
324
 
320
325
  private fun detachTabBarViews() {
326
+ releaseBottomBlur()
321
327
  removeFromParent(layoutHolder)
322
328
  removeFromParent(bottomNavigation)
323
329
  removeFromParent(floatingRoot)
@@ -519,8 +525,8 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
519
525
  if (androidTabBarStyle != AndroidTabBarStyle.FLOATING) {
520
526
  return
521
527
  }
522
- // Android has no system backdrop material, so the glass is approximated with
523
- // a near-opaque tinted surface, a hairline rim, and a soft outline shadow.
528
+ // With a live backdrop, keep only a light surface tint over the blur.
529
+ // The existing rim, shadow, and pressed lens keep their own sharp layers.
524
530
  val baseColor = resolvedBarTintColor()
525
531
  val isDark = isDarkColor(baseColor)
526
532
  val contrastColor = if (isDark) Color.WHITE else Color.BLACK
@@ -528,7 +534,7 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
528
534
  floatingBarBackground.background = GradientDrawable().apply {
529
535
  shape = GradientDrawable.RECTANGLE
530
536
  cornerRadius = dp(FloatingTabBarMetrics.BAR_HEIGHT / 2f).toFloat()
531
- setColor(withAlpha(surfaceColor, 0.95f))
537
+ setColor(withAlpha(surfaceColor, if (usesBottomBlur) 0.12f else 0.95f))
532
538
  setStroke(maxOf(1, dp(0.5f)), withAlpha(contrastColor, if (isDark) 0.14f else 0.07f))
533
539
  }
534
540
  floatingRestingFillColor = activeIndicatorColor?.defaultColor
@@ -747,6 +753,11 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
747
753
  if (floatingTouchView !== view) {
748
754
  return true
749
755
  }
756
+ val deltaX = event.rawX - floatingTouchStartRawX
757
+ val deltaY = event.rawY - floatingTouchStartRawY
758
+ if (deltaX * deltaX + deltaY * deltaY > floatingTouchSlop * floatingTouchSlop) {
759
+ floatingDragging = true
760
+ }
750
761
  if (floatingDragging) {
751
762
  updateFloatingDrag(event.rawX, event.eventTime)
752
763
  }
@@ -897,6 +908,120 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
897
908
  layout(left, top, right, bottom)
898
909
  }
899
910
 
911
+ fun setAndroidBottomBlurEnabled(enabled: Boolean) {
912
+ if (androidBottomBlurEnabled == enabled) return
913
+ androidBottomBlurEnabled = enabled
914
+ updateBlurAppearance()
915
+ requestLayout()
916
+ floatingRoot.invalidate()
917
+ invalidate()
918
+ }
919
+
920
+ private fun updateBlurAppearance() {
921
+ if (!usesBottomBlur) releaseBottomBlur()
922
+ val enabled = usesBottomBlur && androidTabBarStyle == AndroidTabBarStyle.DEFAULT
923
+ if (androidTabBarStyle == AndroidTabBarStyle.FLOATING) {
924
+ blurSurfaceColor = resolvedBarTintColor()
925
+ if (floatingBlurAppearanceApplied != usesBottomBlur) {
926
+ floatingBlurAppearanceApplied = usesBottomBlur
927
+ updateFloatingAppearance()
928
+ }
929
+ } else if (enabled) {
930
+ blurSurfaceColor = barTintColor ?: Utils.getDefaultColorFor(
931
+ bottomNavigation.context, com.google.android.material.R.attr.colorSurface,
932
+ ) ?: if (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES) {
933
+ Color.BLACK
934
+ } else Color.WHITE
935
+ }
936
+ if (enabled == blurAppearanceApplied) return
937
+ if (enabled) {
938
+ savedBarTint = bottomNavigation.backgroundTintList
939
+ savedItemBackground = bottomNavigation.itemBackground
940
+ savedBarElevation = bottomNavigation.elevation
941
+ val ripple = bottomNavigation.itemRippleColor
942
+ bottomNavigation.backgroundTintList = ColorStateList.valueOf(Color.TRANSPARENT)
943
+ bottomNavigation.itemBackground = null
944
+ bottomNavigation.itemRippleColor = ripple
945
+ bottomNavigation.elevation = 0f
946
+ } else {
947
+ bottomNavigation.backgroundTintList = savedBarTint
948
+ bottomNavigation.itemBackground = savedItemBackground
949
+ bottomNavigation.elevation = savedBarElevation
950
+ releaseBottomBlur()
951
+ }
952
+ blurAppearanceApplied = enabled
953
+ }
954
+
955
+ private fun releaseBottomBlur() {
956
+ if (Build.VERSION.SDK_INT >= 31) bottomBarBlur?.release()
957
+ bottomBarBlur = null
958
+ }
959
+
960
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
961
+ updateBlurAppearance()
962
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
963
+ if (usesBottomBlur && androidTabBarStyle == AndroidTabBarStyle.DEFAULT) {
964
+ layoutHolder.measure(
965
+ MeasureSpec.makeMeasureSpec(maxOf(0, measuredWidth - paddingLeft - paddingRight), MeasureSpec.EXACTLY),
966
+ MeasureSpec.makeMeasureSpec(maxOf(0, measuredHeight - paddingTop - paddingBottom), MeasureSpec.EXACTLY),
967
+ )
968
+ }
969
+ }
970
+
971
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
972
+ super.onLayout(changed, left, top, right, bottom)
973
+ if (usesBottomBlur && androidTabBarStyle == AndroidTabBarStyle.DEFAULT) {
974
+ layoutHolder.layout(paddingLeft, paddingTop, width - paddingRight, height - paddingBottom)
975
+ bottomNavigation.layout(
976
+ paddingLeft, height - paddingBottom - bottomNavigation.measuredHeight,
977
+ width - paddingRight, height - paddingBottom,
978
+ )
979
+ }
980
+ val barHeight = Utils.convertPixelsToDp(context, getTabBarMeasuredHeight()).toInt()
981
+ if (barHeight != lastReportedTabBarHeight) {
982
+ onTabBarMeasuredListener?.invoke(barHeight)
983
+ lastReportedTabBarHeight = barHeight
984
+ }
985
+ // Scene bounds change when blur toggles even if the host bounds stay fixed.
986
+ val sceneSize = Size(layoutHolder.width, layoutHolder.height)
987
+ if (sceneSize != lastReportedSize) {
988
+ onNativeLayoutListener?.invoke(
989
+ Utils.convertPixelsToDp(context, sceneSize.width),
990
+ Utils.convertPixelsToDp(context, sceneSize.height),
991
+ )
992
+ lastReportedSize = sceneSize
993
+ }
994
+ }
995
+
996
+ private fun drawBlurBackdrop(
997
+ canvas: Canvas, width: Int, barTop: Int, bottom: Int, drawScenes: (Canvas) -> Unit,
998
+ ) {
999
+ if (!usesBottomBlur || !canvas.isHardwareAccelerated || Build.VERSION.SDK_INT < 31) return
1000
+ val blur = bottomBarBlur ?: BottomBarBlurRenderer(resources.displayMetrics.density).also {
1001
+ bottomBarBlur = it
1002
+ }
1003
+ blur.draw(canvas, width, barTop, bottom, blurSurfaceColor, drawScenes)
1004
+ }
1005
+
1006
+ override fun drawChild(canvas: Canvas, child: View, drawingTime: Long): Boolean {
1007
+ if (child === bottomNavigation && androidTabBarStyle == AndroidTabBarStyle.DEFAULT) {
1008
+ drawBlurBackdrop(canvas, width, bottomNavigation.top, bottomNavigation.bottom) { recording ->
1009
+ super.drawChild(recording, layoutHolder, drawingTime)
1010
+ }
1011
+ }
1012
+ return super.drawChild(canvas, child, drawingTime)
1013
+ }
1014
+
1015
+ override fun onAttachedToWindow() {
1016
+ super.onAttachedToWindow()
1017
+ requestLayout()
1018
+ }
1019
+
1020
+ override fun onDetachedFromWindow() {
1021
+ releaseBottomBlur()
1022
+ super.onDetachedFromWindow()
1023
+ }
1024
+
900
1025
  override fun requestLayout() {
901
1026
  super.requestLayout()
902
1027
  @Suppress("SENSELESS_COMPARISON") // layoutCallback can be null here since this method can be called in init
@@ -1053,11 +1178,15 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
1053
1178
  return
1054
1179
  }
1055
1180
  androidTabBarStyle = nextStyle
1181
+ // Restore the docked appearance before reparenting, then capture the new
1182
+ // style only after its normal appearance has been installed.
1183
+ if (blurAppearanceApplied) updateBlurAppearance()
1056
1184
  if (nextStyle == AndroidTabBarStyle.FLOATING) {
1057
1185
  attachFloatingLayout()
1058
1186
  } else {
1059
1187
  attachDefaultLayout()
1060
1188
  }
1189
+ updateBlurAppearance()
1061
1190
  updateTextAppearance()
1062
1191
  requestLayout()
1063
1192
  }
@@ -1082,19 +1211,12 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
1082
1211
  bottomNavigation.visibility = if (isHidden) GONE else VISIBLE
1083
1212
  }
1084
1213
 
1085
- // Force re-measure and notify JS about the new layout dimensions
1086
- post {
1087
- val dpWidth = Utils.convertPixelsToDp(context, layoutHolder.width)
1088
- val dpHeight = Utils.convertPixelsToDp(context, layoutHolder.height)
1089
- onNativeLayoutListener?.invoke(dpWidth, dpHeight)
1090
-
1091
- // Update tab bar height measurement (0 when hidden)
1092
- val tabBarHeight = Utils.convertPixelsToDp(context, getTabBarMeasuredHeight()).toInt()
1093
- onTabBarMeasuredListener?.invoke(tabBarHeight)
1094
- if (!isHidden) {
1095
- updateFloatingIndicator(animate = false)
1096
- }
1097
- }
1214
+ updateBlurAppearance()
1215
+ requestLayout()
1216
+ floatingRoot.invalidate()
1217
+ invalidate()
1218
+ if (!isHidden) post { updateFloatingIndicator(animate = false) }
1219
+
1098
1220
  }
1099
1221
 
1100
1222
  fun updateItems(items: MutableList<TabInfo>) {
@@ -1208,6 +1330,7 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
1208
1330
  // Floating mode replaces the ripple with the pressed lens.
1209
1331
  if (androidTabBarStyle != AndroidTabBarStyle.FLOATING) {
1210
1332
  bottomNavigation.itemRippleColor = color
1333
+ if (blurAppearanceApplied) savedItemBackground = bottomNavigation.itemBackground
1211
1334
  }
1212
1335
  }
1213
1336
 
@@ -1245,14 +1368,20 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
1245
1368
  val backgroundColor =
1246
1369
  color ?: Utils.getDefaultColorFor(context, android.R.attr.colorPrimary) ?: return
1247
1370
  barTintColor = backgroundColor
1371
+ blurSurfaceColor = backgroundColor
1248
1372
 
1249
1373
  if (androidTabBarStyle == AndroidTabBarStyle.FLOATING) {
1250
1374
  updateFloatingAppearance()
1375
+ } else if (blurAppearanceApplied) {
1376
+ savedItemBackground = ColorDrawable(backgroundColor)
1377
+ savedBarTint = ColorStateList.valueOf(backgroundColor)
1251
1378
  } else {
1252
1379
  bottomNavigation.itemBackground = ColorDrawable(backgroundColor)
1253
1380
  bottomNavigation.backgroundTintList = ColorStateList.valueOf(backgroundColor)
1254
1381
  }
1255
1382
  hasCustomAppearance = true
1383
+ floatingRoot.invalidate()
1384
+ invalidate()
1256
1385
  }
1257
1386
 
1258
1387
  fun setActiveTintColor(color: Int?) {
@@ -1291,6 +1420,9 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
1291
1420
  }
1292
1421
 
1293
1422
  fun onDropViewInstance() {
1423
+ androidBottomBlurEnabled = false
1424
+ updateBlurAppearance()
1425
+ releaseBottomBlur()
1294
1426
  resetFloatingGesture()
1295
1427
  imageLoader.shutdown()
1296
1428
  }
@@ -1377,6 +1509,12 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
1377
1509
 
1378
1510
  override fun onConfigurationChanged(newConfig: Configuration?) {
1379
1511
  super.onConfigurationChanged(newConfig)
1512
+ releaseBottomBlur()
1513
+ if (androidBottomBlurEnabled) {
1514
+ requestLayout()
1515
+ floatingRoot.invalidate()
1516
+ invalidate()
1517
+ }
1380
1518
  if (uiModeConfiguration == newConfig?.uiMode || hasCustomAppearance) {
1381
1519
  return
1382
1520
  }
@@ -1389,6 +1527,7 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
1389
1527
  // If appearance wasn't changed re-create the bottom navigation view when configuration changes.
1390
1528
  // React Native opts out ouf Activity re-creation when configuration changes, this workarounds that.
1391
1529
  // We also opt-out of this recreation when custom styles are used.
1530
+ blurAppearanceApplied = false
1392
1531
  removeFromParent(bottomNavigation)
1393
1532
  bottomNavigation = ExtendedBottomNavigationView(context)
1394
1533
  defaultItemBackground = bottomNavigation.itemBackground
@@ -201,6 +201,10 @@ class BottomTabsViewManager(context: ReactApplicationContext) :
201
201
  view?.setIgnoreKeyboardInsets(value)
202
202
  }
203
203
 
204
+ override fun setAndroidBottomBlurEnabled(view: BottomTabsView?, value: Boolean) {
205
+ view?.setAndroidBottomBlurEnabled(value)
206
+ }
207
+
204
208
  // iOS-only props (no-ops on Android)
205
209
  // selectedIcons feeds UITabBarItem.selectedImage on iOS; Android keeps
206
210
  // swapping the icon in JS via the focusedKey-driven `icons` array.
package/docs/SPEC.md CHANGED
@@ -38,7 +38,7 @@ moved more than 10 points) was removed because it disabled drag-to-switch.
38
38
  `androidTabBarStyle` selects the Android presentation; iOS ignores it.
39
39
 
40
40
  - `default` (default): Material `BottomNavigationView` docked at the bottom,
41
- unchanged behavior.
41
+ with the blur-controlled scene underlap described below.
42
42
  - `floating`: a capsule bar modeled on the iOS 26 tab bar. Scene content extends
43
43
  underneath it.
44
44
 
@@ -50,10 +50,13 @@ are 26, labels 10 sp in both states unless `fontSize` is set, and item content i
50
50
  vertically centered. The reported tab bar height is the bar height plus its
51
51
  bottom gap, or 0 while hidden.
52
52
 
53
- Floating appearance: the bar surface is `barTintColor` (or the theme background)
53
+ Floating appearance with background blur disabled: the bar surface is `barTintColor` (or the theme background)
54
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
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
57
60
  (`activeIndicatorColor` when provided). The Material ripple is disabled; the
58
61
  lens provides press feedback.
59
62
 
@@ -66,6 +69,9 @@ Floating interaction (single finger; motion splitting is disabled):
66
69
  becomes the candidate, is magnified, and ticks haptically when it changes.
67
70
  No page change or event happens during the drag.
68
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.
69
75
  Release after a drag selects the candidate once, only if it differs from the
70
76
  current tab. Cancel selects nothing.
71
77
  4. The lens then settles back into the pill on the selected tab, which stays on
@@ -75,6 +81,67 @@ JS-driven selection changes slide the pill with a spring and velocity stretch.
75
81
  Switching style at runtime rebuilds the bar and restores Material's label sizes,
76
82
  icon size, ripple, padding, and gravity when returning to `default`.
77
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
+
78
145
  ## Threading and ownership
79
146
 
80
147
  All gesture handling, animations, and timers run on the platform UI thread in
@@ -69,6 +69,7 @@ const BottomTabs = ({
69
69
  tabLabelStyle,
70
70
  renderBottomAccessoryView,
71
71
  activeIndicatorColor,
72
+ androidBottomBlurEnabled = Platform.OS === 'android' && Number(Platform.Version) >= 36,
72
73
  delayedFreeze = false,
73
74
  ...props
74
75
  }) => {
@@ -265,6 +266,7 @@ const BottomTabs = ({
265
266
  barTintColor: tabBarStyle?.backgroundColor,
266
267
  rippleColor: rippleColor,
267
268
  activeIndicatorColor: activeIndicatorColor,
269
+ androidBottomBlurEnabled: Platform.OS === 'android' && !renderCustomTabBar && androidBottomBlurEnabled,
268
270
  labeled: labeled,
269
271
  children: [trimmedRoutes.map(route => {
270
272
  if (getLazy({
@@ -51,6 +51,7 @@ export interface BottomTabsNativeProps extends ViewProps {
51
51
  scrollEdgeAppearance?: string;
52
52
  barTintColor?: ColorValue;
53
53
  translucent?: WithDefault<boolean, true>;
54
+ androidBottomBlurEnabled?: WithDefault<boolean, false>;
54
55
  rippleColor?: ColorValue;
55
56
  activeTintColor?: ColorValue;
56
57
  inactiveTintColor?: ColorValue;
@@ -157,6 +157,13 @@ interface Props<Route extends BaseRoute> {
157
157
  * A Boolean value that indicates whether the tab bar is translucent. (iOS only)
158
158
  */
159
159
  translucent?: boolean;
160
+ /**
161
+ * Live background blur, translucent tint, and a fade above the native Android
162
+ * tab bar. Defaults to true on Android 16+ (API 36). Explicit true supports
163
+ * Android 12+; older versions retain the standard bar. Ignored on iOS.
164
+ * Pad scroll content with useBottomTabBarHeight when enabled.
165
+ */
166
+ androidBottomBlurEnabled?: boolean;
160
167
  rippleColor?: ColorValue;
161
168
  /**
162
169
  * Color of tab indicator. (Android only)
@@ -197,6 +204,6 @@ interface Props<Route extends BaseRoute> {
197
204
  */
198
205
  delayedFreeze?: boolean;
199
206
  }
200
- declare const BottomTabs: <Route extends BaseRoute>({ navigationState, renderScene, renderLazyPlaceholder, onIndexChange, onTabDoublePress, 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;
207
+ declare const BottomTabs: <Route extends BaseRoute>({ navigationState, renderScene, renderLazyPlaceholder, onIndexChange, onTabDoublePress, 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, androidBottomBlurEnabled, delayedFreeze, ...props }: Props<Route>) => import("react/jsx-runtime").JSX.Element;
201
208
  export default BottomTabs;
202
209
  //# sourceMappingURL=BottomTabs.d.ts.map
@@ -39,6 +39,7 @@ export interface BottomTabsNativeProps extends ViewProps {
39
39
  scrollEdgeAppearance?: string;
40
40
  barTintColor?: ColorValue;
41
41
  translucent?: WithDefault<boolean, true>;
42
+ androidBottomBlurEnabled?: WithDefault<boolean, false>;
42
43
  rippleColor?: ColorValue;
43
44
  activeTintColor?: ColorValue;
44
45
  inactiveTintColor?: ColorValue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-bottom-tabs",
3
- "version": "6.7.0-alpha.339",
3
+ "version": "6.7.0-alpha.341",
4
4
  "description": "Native bottom tabs (tab bar + scene container) for React Native on iOS and Android",
5
5
  "source": "./src/index.tsx",
6
6
  "main": "./lib/module/index.js",
@@ -169,6 +169,13 @@ interface Props<Route extends BaseRoute> {
169
169
  * A Boolean value that indicates whether the tab bar is translucent. (iOS only)
170
170
  */
171
171
  translucent?: boolean;
172
+ /**
173
+ * Live background blur, translucent tint, and a fade above the native Android
174
+ * tab bar. Defaults to true on Android 16+ (API 36). Explicit true supports
175
+ * Android 12+; older versions retain the standard bar. Ignored on iOS.
176
+ * Pad scroll content with useBottomTabBarHeight when enabled.
177
+ */
178
+ androidBottomBlurEnabled?: boolean;
172
179
  rippleColor?: ColorValue;
173
180
  /**
174
181
  * Color of tab indicator. (Android only)
@@ -247,6 +254,8 @@ const BottomTabs = <Route extends BaseRoute>({
247
254
  tabLabelStyle,
248
255
  renderBottomAccessoryView,
249
256
  activeIndicatorColor,
257
+ androidBottomBlurEnabled = Platform.OS === 'android' &&
258
+ Number(Platform.Version) >= 36,
250
259
  delayedFreeze = false,
251
260
  ...props
252
261
  }: Props<Route>) => {
@@ -471,6 +480,11 @@ const BottomTabs = <Route extends BaseRoute>({
471
480
  barTintColor={tabBarStyle?.backgroundColor}
472
481
  rippleColor={rippleColor}
473
482
  activeIndicatorColor={activeIndicatorColor}
483
+ androidBottomBlurEnabled={
484
+ Platform.OS === 'android' &&
485
+ !renderCustomTabBar &&
486
+ androidBottomBlurEnabled
487
+ }
474
488
  labeled={labeled}
475
489
  >
476
490
  {trimmedRoutes.map((route) => {
@@ -51,6 +51,7 @@ export interface BottomTabsNativeProps extends ViewProps {
51
51
  scrollEdgeAppearance?: string;
52
52
  barTintColor?: ColorValue;
53
53
  translucent?: WithDefault<boolean, true>;
54
+ androidBottomBlurEnabled?: WithDefault<boolean, false>;
54
55
  rippleColor?: ColorValue;
55
56
  activeTintColor?: ColorValue;
56
57
  inactiveTintColor?: ColorValue;