@onekeyfe/react-native-bottom-tabs 6.7.0-7 → 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
@@ -902,6 +908,120 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
902
908
  layout(left, top, right, bottom)
903
909
  }
904
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
+
905
1025
  override fun requestLayout() {
906
1026
  super.requestLayout()
907
1027
  @Suppress("SENSELESS_COMPARISON") // layoutCallback can be null here since this method can be called in init
@@ -1058,11 +1178,15 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
1058
1178
  return
1059
1179
  }
1060
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()
1061
1184
  if (nextStyle == AndroidTabBarStyle.FLOATING) {
1062
1185
  attachFloatingLayout()
1063
1186
  } else {
1064
1187
  attachDefaultLayout()
1065
1188
  }
1189
+ updateBlurAppearance()
1066
1190
  updateTextAppearance()
1067
1191
  requestLayout()
1068
1192
  }
@@ -1087,19 +1211,12 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
1087
1211
  bottomNavigation.visibility = if (isHidden) GONE else VISIBLE
1088
1212
  }
1089
1213
 
1090
- // Force re-measure and notify JS about the new layout dimensions
1091
- post {
1092
- val dpWidth = Utils.convertPixelsToDp(context, layoutHolder.width)
1093
- val dpHeight = Utils.convertPixelsToDp(context, layoutHolder.height)
1094
- onNativeLayoutListener?.invoke(dpWidth, dpHeight)
1095
-
1096
- // Update tab bar height measurement (0 when hidden)
1097
- val tabBarHeight = Utils.convertPixelsToDp(context, getTabBarMeasuredHeight()).toInt()
1098
- onTabBarMeasuredListener?.invoke(tabBarHeight)
1099
- if (!isHidden) {
1100
- updateFloatingIndicator(animate = false)
1101
- }
1102
- }
1214
+ updateBlurAppearance()
1215
+ requestLayout()
1216
+ floatingRoot.invalidate()
1217
+ invalidate()
1218
+ if (!isHidden) post { updateFloatingIndicator(animate = false) }
1219
+
1103
1220
  }
1104
1221
 
1105
1222
  fun updateItems(items: MutableList<TabInfo>) {
@@ -1213,6 +1330,7 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
1213
1330
  // Floating mode replaces the ripple with the pressed lens.
1214
1331
  if (androidTabBarStyle != AndroidTabBarStyle.FLOATING) {
1215
1332
  bottomNavigation.itemRippleColor = color
1333
+ if (blurAppearanceApplied) savedItemBackground = bottomNavigation.itemBackground
1216
1334
  }
1217
1335
  }
1218
1336
 
@@ -1250,14 +1368,20 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
1250
1368
  val backgroundColor =
1251
1369
  color ?: Utils.getDefaultColorFor(context, android.R.attr.colorPrimary) ?: return
1252
1370
  barTintColor = backgroundColor
1371
+ blurSurfaceColor = backgroundColor
1253
1372
 
1254
1373
  if (androidTabBarStyle == AndroidTabBarStyle.FLOATING) {
1255
1374
  updateFloatingAppearance()
1375
+ } else if (blurAppearanceApplied) {
1376
+ savedItemBackground = ColorDrawable(backgroundColor)
1377
+ savedBarTint = ColorStateList.valueOf(backgroundColor)
1256
1378
  } else {
1257
1379
  bottomNavigation.itemBackground = ColorDrawable(backgroundColor)
1258
1380
  bottomNavigation.backgroundTintList = ColorStateList.valueOf(backgroundColor)
1259
1381
  }
1260
1382
  hasCustomAppearance = true
1383
+ floatingRoot.invalidate()
1384
+ invalidate()
1261
1385
  }
1262
1386
 
1263
1387
  fun setActiveTintColor(color: Int?) {
@@ -1296,6 +1420,9 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
1296
1420
  }
1297
1421
 
1298
1422
  fun onDropViewInstance() {
1423
+ androidBottomBlurEnabled = false
1424
+ updateBlurAppearance()
1425
+ releaseBottomBlur()
1299
1426
  resetFloatingGesture()
1300
1427
  imageLoader.shutdown()
1301
1428
  }
@@ -1382,6 +1509,12 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
1382
1509
 
1383
1510
  override fun onConfigurationChanged(newConfig: Configuration?) {
1384
1511
  super.onConfigurationChanged(newConfig)
1512
+ releaseBottomBlur()
1513
+ if (androidBottomBlurEnabled) {
1514
+ requestLayout()
1515
+ floatingRoot.invalidate()
1516
+ invalidate()
1517
+ }
1385
1518
  if (uiModeConfiguration == newConfig?.uiMode || hasCustomAppearance) {
1386
1519
  return
1387
1520
  }
@@ -1394,6 +1527,7 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
1394
1527
  // If appearance wasn't changed re-create the bottom navigation view when configuration changes.
1395
1528
  // React Native opts out ouf Activity re-creation when configuration changes, this workarounds that.
1396
1529
  // We also opt-out of this recreation when custom styles are used.
1530
+ blurAppearanceApplied = false
1397
1531
  removeFromParent(bottomNavigation)
1398
1532
  bottomNavigation = ExtendedBottomNavigationView(context)
1399
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
 
@@ -78,6 +81,67 @@ JS-driven selection changes slide the pill with a spring and velocity stretch.
78
81
  Switching style at runtime rebuilds the bar and restores Material's label sizes,
79
82
  icon size, ripple, padding, and gravity when returning to `default`.
80
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
+
81
145
  ## Threading and ownership
82
146
 
83
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-7",
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;