@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 +8 -0
- package/android/src/main/java/com/onekey/bottomtabs/BottomBarBlurRenderer.kt +92 -0
- package/android/src/main/java/com/onekey/bottomtabs/BottomTabsView.kt +175 -36
- package/android/src/main/java/com/onekey/bottomtabs/BottomTabsViewManager.kt +4 -0
- package/docs/SPEC.md +71 -4
- package/lib/module/BottomTabs.js +2 -0
- package/lib/module/BottomTabsNativeComponent.ts +1 -0
- package/lib/typescript/src/BottomTabs.d.ts +8 -1
- package/lib/typescript/src/BottomTabsNativeComponent.d.ts +1 -0
- package/package.json +1 -1
- package/src/BottomTabs.tsx +14 -0
- package/src/BottomTabsNativeComponent.ts +1 -0
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
|
-
//
|
|
523
|
-
//
|
|
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
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
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
|
-
|
|
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.
|
|
56
|
-
|
|
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
|
package/lib/module/BottomTabs.js
CHANGED
|
@@ -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.
|
|
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",
|
package/src/BottomTabs.tsx
CHANGED
|
@@ -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;
|