@onekeyfe/react-native-bottom-tabs 0.0.0-stage → 6.7.0-10
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/LICENSE +46 -0
- package/README.md +94 -2
- package/android/build.gradle +119 -0
- package/android/gradle.properties +4 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/AndroidManifestNew.xml +2 -0
- package/android/src/main/java/com/onekey/bottomtabs/BottomBarBlurRenderer.kt +92 -0
- package/android/src/main/java/com/onekey/bottomtabs/BottomTabsPackage.kt +16 -0
- package/android/src/main/java/com/onekey/bottomtabs/BottomTabsView.kt +1558 -0
- package/android/src/main/java/com/onekey/bottomtabs/BottomTabsViewManager.kt +220 -0
- package/android/src/main/java/com/onekey/bottomtabs/FloatingTabBarStyle.kt +50 -0
- package/android/src/main/java/com/onekey/bottomtabs/ImageSource.kt +86 -0
- package/android/src/main/java/com/onekey/bottomtabs/TabInfo.kt +13 -0
- package/android/src/main/java/com/onekey/bottomtabs/Utils.kt +31 -0
- package/android/src/main/java/com/onekey/bottomtabs/events/OnNativeLayoutEvent.kt +20 -0
- package/android/src/main/java/com/onekey/bottomtabs/events/OnTabBarMeasuredEvent.kt +19 -0
- package/android/src/main/java/com/onekey/bottomtabs/events/PageSelectedEvent.kt +21 -0
- package/android/src/main/java/com/onekey/bottomtabs/events/TabDoublePressEvent.kt +19 -0
- package/common/cpp/react/renderer/components/RNBottomTabs/RNBottomTabsViewComponentDescriptor.h +32 -0
- package/common/cpp/react/renderer/components/RNBottomTabs/RNBottomTabsViewShadowNode.cpp +18 -0
- package/common/cpp/react/renderer/components/RNBottomTabs/RNBottomTabsViewShadowNode.h +35 -0
- package/common/cpp/react/renderer/components/RNBottomTabs/RNBottomTabsViewState.cpp +15 -0
- package/common/cpp/react/renderer/components/RNBottomTabs/RNBottomTabsViewState.h +25 -0
- package/docs/SPEC.md +168 -0
- package/ios/Extensions.swift +46 -0
- package/ios/RCTBottomAccessoryComponentView.h +12 -0
- package/ios/RCTBottomAccessoryComponentView.mm +67 -0
- package/ios/RCTBottomAccessoryContainerView.swift +51 -0
- package/ios/RCTBottomTabsComponentView.h +12 -0
- package/ios/RCTBottomTabsComponentView.mm +338 -0
- package/ios/RCTBottomTabsContainerView.swift +777 -0
- package/ios/RCTBottomTabsLog.h +7 -0
- package/ios/RCTBottomTabsLog.m +32 -0
- package/ios/SVG/CoreSVG.h +13 -0
- package/ios/SVG/CoreSVG.mm +177 -0
- package/ios/SVG/SvgDecoder.h +10 -0
- package/ios/SVG/SvgDecoder.mm +32 -0
- package/ios/TabBarFontSize.swift +55 -0
- package/lib/module/BottomAccessoryView.js +45 -0
- package/lib/module/BottomAccessoryViewNativeComponent.ts +27 -0
- package/lib/module/BottomTabs.js +325 -0
- package/lib/module/BottomTabsNativeComponent.ts +70 -0
- package/lib/module/DelayedFreeze.js +31 -0
- package/lib/module/NativeSVGDecoder.js +5 -0
- package/lib/module/SceneMap.js +28 -0
- package/lib/module/codegen-types.d.js +2 -0
- package/lib/module/index.js +20 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/utils/BottomTabBarHeightContext.js +5 -0
- package/lib/module/utils/useBottomTabBarHeight.js +12 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/BottomAccessoryView.d.ts +8 -0
- package/lib/typescript/src/BottomAccessoryViewNativeComponent.d.ts +16 -0
- package/lib/typescript/src/BottomTabs.d.ts +209 -0
- package/lib/typescript/src/BottomTabsNativeComponent.d.ts +59 -0
- package/lib/typescript/src/DelayedFreeze.d.ts +9 -0
- package/lib/typescript/src/NativeSVGDecoder.d.ts +6 -0
- package/lib/typescript/src/SceneMap.d.ts +10 -0
- package/lib/typescript/src/index.d.ts +16 -0
- package/lib/typescript/src/types.d.ts +29 -0
- package/lib/typescript/src/utils/BottomTabBarHeightContext.d.ts +3 -0
- package/lib/typescript/src/utils/useBottomTabBarHeight.d.ts +2 -0
- package/package.json +117 -4
- package/react-native-bottom-tabs.podspec +36 -0
- package/react-native.config.js +13 -0
- package/src/BottomAccessoryView.tsx +58 -0
- package/src/BottomAccessoryViewNativeComponent.ts +27 -0
- package/src/BottomTabs.tsx +559 -0
- package/src/BottomTabsNativeComponent.ts +70 -0
- package/src/DelayedFreeze.tsx +33 -0
- package/src/NativeSVGDecoder.ts +5 -0
- package/src/SceneMap.tsx +34 -0
- package/src/codegen-types.d.ts +28 -0
- package/src/index.tsx +18 -0
- package/src/types.ts +31 -0
- package/src/utils/BottomTabBarHeightContext.ts +5 -0
- package/src/utils/useBottomTabBarHeight.ts +15 -0
|
@@ -0,0 +1,1558 @@
|
|
|
1
|
+
package com.onekey.bottomtabs
|
|
2
|
+
|
|
3
|
+
import android.animation.ValueAnimator
|
|
4
|
+
import android.annotation.SuppressLint
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.content.res.ColorStateList
|
|
7
|
+
import android.content.res.Configuration
|
|
8
|
+
import androidx.appcompat.view.ContextThemeWrapper
|
|
9
|
+
import android.graphics.Color
|
|
10
|
+
import android.graphics.Canvas
|
|
11
|
+
import android.graphics.Outline
|
|
12
|
+
import android.graphics.drawable.ColorDrawable
|
|
13
|
+
import android.graphics.drawable.Drawable
|
|
14
|
+
import android.graphics.drawable.GradientDrawable
|
|
15
|
+
import android.os.Build
|
|
16
|
+
import android.os.SystemClock
|
|
17
|
+
import android.transition.TransitionManager
|
|
18
|
+
import android.util.Log
|
|
19
|
+
import android.util.Size
|
|
20
|
+
import android.util.TypedValue
|
|
21
|
+
import android.view.Choreographer
|
|
22
|
+
import android.view.Gravity
|
|
23
|
+
import android.view.HapticFeedbackConstants
|
|
24
|
+
import android.view.MenuItem
|
|
25
|
+
import android.view.MotionEvent
|
|
26
|
+
import android.view.View
|
|
27
|
+
import android.view.ViewConfiguration
|
|
28
|
+
import android.view.ViewGroup
|
|
29
|
+
import android.view.ViewOutlineProvider
|
|
30
|
+
import android.view.animation.DecelerateInterpolator
|
|
31
|
+
import android.widget.FrameLayout
|
|
32
|
+
import android.widget.LinearLayout
|
|
33
|
+
import android.widget.TextView
|
|
34
|
+
import androidx.core.animation.doOnEnd
|
|
35
|
+
import androidx.core.graphics.ColorUtils
|
|
36
|
+
import androidx.core.view.ViewCompat
|
|
37
|
+
import androidx.core.view.WindowInsetsCompat
|
|
38
|
+
import androidx.core.view.forEachIndexed
|
|
39
|
+
import coil3.ImageLoader
|
|
40
|
+
import coil3.asDrawable
|
|
41
|
+
import coil3.request.ImageRequest
|
|
42
|
+
import coil3.svg.SvgDecoder
|
|
43
|
+
import coil3.size.Precision
|
|
44
|
+
import coil3.size.Size as CoilSize
|
|
45
|
+
import coil3.size.Scale
|
|
46
|
+
import com.facebook.react.bridge.ReadableArray
|
|
47
|
+
import com.facebook.react.common.assets.ReactFontManager
|
|
48
|
+
import com.facebook.react.modules.core.ReactChoreographer
|
|
49
|
+
import com.facebook.react.uimanager.PointerEvents
|
|
50
|
+
import com.facebook.react.uimanager.ReactPointerEventsView
|
|
51
|
+
import com.facebook.react.views.text.ReactTypefaceUtils
|
|
52
|
+
import com.google.android.material.bottomnavigation.BottomNavigationView
|
|
53
|
+
import com.google.android.material.navigation.NavigationBarView
|
|
54
|
+
import com.google.android.material.navigation.NavigationBarView.LABEL_VISIBILITY_AUTO
|
|
55
|
+
import com.google.android.material.navigation.NavigationBarView.LABEL_VISIBILITY_LABELED
|
|
56
|
+
import com.google.android.material.navigation.NavigationBarView.LABEL_VISIBILITY_UNLABELED
|
|
57
|
+
import com.google.android.material.transition.platform.MaterialFadeThrough
|
|
58
|
+
import kotlin.math.abs
|
|
59
|
+
import kotlin.math.roundToInt
|
|
60
|
+
|
|
61
|
+
private fun getMaterialContext(context: Context): Context {
|
|
62
|
+
return ContextThemeWrapper(context, com.google.android.material.R.style.Theme_MaterialComponents_DayNight)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
class ExtendedBottomNavigationView(context: Context) : BottomNavigationView(getMaterialContext(context)) {
|
|
66
|
+
private val basePaddingStart = paddingStart
|
|
67
|
+
private val basePaddingTop = paddingTop
|
|
68
|
+
private val basePaddingEnd = paddingEnd
|
|
69
|
+
private val basePaddingBottom = paddingBottom
|
|
70
|
+
private var ignoreBottomInsets = false
|
|
71
|
+
private var ignoreKeyboardInsets = true
|
|
72
|
+
// Floating mode lays the bar out inside an inset container that already
|
|
73
|
+
// accounts for system bars and the keyboard, so the menu keeps fixed padding.
|
|
74
|
+
private var floatingHorizontalPadding: Int? = null
|
|
75
|
+
|
|
76
|
+
init {
|
|
77
|
+
// OneKey patch: Material pads the bar with getSystemWindowInsetBottom(), which
|
|
78
|
+
// includes the IME height only while the window uses adjustResize (switched on by
|
|
79
|
+
// react-native-keyboard-controller hooks), so whether the bar rose above the
|
|
80
|
+
// keyboard depended on the screen. Pad for system bars, and add the IME only when
|
|
81
|
+
// ignoreKeyboardInsets is turned off.
|
|
82
|
+
ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets ->
|
|
83
|
+
floatingHorizontalPadding?.let { padding ->
|
|
84
|
+
view.setPaddingRelative(padding, 0, padding, 0)
|
|
85
|
+
return@setOnApplyWindowInsetsListener insets
|
|
86
|
+
}
|
|
87
|
+
val barInsets = insets.getInsets(
|
|
88
|
+
WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()
|
|
89
|
+
)
|
|
90
|
+
val navigationBarBottom = if (ignoreBottomInsets) 0 else barInsets.bottom
|
|
91
|
+
val keyboardBottom = if (ignoreKeyboardInsets) {
|
|
92
|
+
0
|
|
93
|
+
} else {
|
|
94
|
+
insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
|
|
95
|
+
}
|
|
96
|
+
val isRtl = view.layoutDirection == View.LAYOUT_DIRECTION_RTL
|
|
97
|
+
view.setPaddingRelative(
|
|
98
|
+
basePaddingStart + if (isRtl) barInsets.right else barInsets.left,
|
|
99
|
+
basePaddingTop,
|
|
100
|
+
basePaddingEnd + if (isRtl) barInsets.left else barInsets.right,
|
|
101
|
+
basePaddingBottom + maxOf(navigationBarBottom, keyboardBottom),
|
|
102
|
+
)
|
|
103
|
+
insets
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
fun setIgnoreBottomInsets(ignore: Boolean) {
|
|
108
|
+
if (ignoreBottomInsets == ignore) {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
ignoreBottomInsets = ignore
|
|
112
|
+
requestApplyInsets()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fun setIgnoreKeyboardInsets(ignore: Boolean) {
|
|
116
|
+
if (ignoreKeyboardInsets == ignore) {
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
ignoreKeyboardInsets = ignore
|
|
120
|
+
requestApplyInsets()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
fun setFloatingHorizontalPadding(padding: Int?) {
|
|
124
|
+
if (floatingHorizontalPadding == padding) {
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
floatingHorizontalPadding = padding
|
|
128
|
+
if (padding != null) {
|
|
129
|
+
setPaddingRelative(padding, 0, padding, 0)
|
|
130
|
+
} else {
|
|
131
|
+
setPaddingRelative(basePaddingStart, basePaddingTop, basePaddingEnd, basePaddingBottom)
|
|
132
|
+
}
|
|
133
|
+
requestApplyInsets()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
override fun getMaxItemCount(): Int {
|
|
137
|
+
return 100
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// OneKey patch: RN hit-testing traverses INVISIBLE native wrappers unless pointer events opt out.
|
|
142
|
+
private class TabSceneContainer(context: Context) : FrameLayout(context), ReactPointerEventsView {
|
|
143
|
+
override var pointerEvents: PointerEvents = PointerEvents.NONE
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private enum class AndroidTabBarStyle {
|
|
147
|
+
DEFAULT,
|
|
148
|
+
FLOATING,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
class BottomTabsView(context: Context) : LinearLayout(context) {
|
|
152
|
+
private var bottomNavigation = ExtendedBottomNavigationView(context)
|
|
153
|
+
val layoutHolder = 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
|
+
}
|
|
164
|
+
private val floatingBar = FrameLayout(context)
|
|
165
|
+
private val floatingBarBackground = View(context)
|
|
166
|
+
private val floatingIndicator = View(context)
|
|
167
|
+
private val floatingIndicatorDrawable = GradientDrawable()
|
|
168
|
+
|
|
169
|
+
var onTabSelectedListener: ((key: String) -> Unit)? = null
|
|
170
|
+
var onTabDoublePressedListener: ((key: String) -> Unit)? = null
|
|
171
|
+
var onNativeLayoutListener: ((width: Double, height: Double) -> Unit)? = null
|
|
172
|
+
var onTabBarMeasuredListener: ((height: Int) -> Unit)? = null
|
|
173
|
+
var disablePageAnimations = false
|
|
174
|
+
set(value) {
|
|
175
|
+
if (field == value) {
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
field = value
|
|
179
|
+
layoutHolder.forEachIndexed { _, view ->
|
|
180
|
+
if (view.visibility != VISIBLE) {
|
|
181
|
+
view.visibility = if (value) INVISIBLE else GONE
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
layoutHolder.requestLayout()
|
|
185
|
+
}
|
|
186
|
+
var items: MutableList<TabInfo> = mutableListOf()
|
|
187
|
+
private val iconSources: MutableMap<Int, ImageSource> = mutableMapOf()
|
|
188
|
+
private val drawableCache: MutableMap<ImageSource, Drawable> = mutableMapOf()
|
|
189
|
+
|
|
190
|
+
private var isLayoutEnqueued = false
|
|
191
|
+
private var selectedItem: String? = null
|
|
192
|
+
private var activeTintColor: Int? = null
|
|
193
|
+
private var inactiveTintColor: Int? = null
|
|
194
|
+
private val checkedStateSet = intArrayOf(android.R.attr.state_checked)
|
|
195
|
+
private val uncheckedStateSet = intArrayOf(-android.R.attr.state_checked)
|
|
196
|
+
// OneKey patch: rely on View.isHapticFeedbackEnabled as the single source of truth.
|
|
197
|
+
// private var hapticFeedbackEnabled = false
|
|
198
|
+
private var fontSize: Int? = null
|
|
199
|
+
private var fontFamily: String? = null
|
|
200
|
+
private var fontWeight: Int? = null
|
|
201
|
+
private var labeled: Boolean? = null
|
|
202
|
+
private var lastReportedSize: Size? = null
|
|
203
|
+
private var lastReportedTabBarHeight = -1
|
|
204
|
+
private var hasCustomAppearance = false
|
|
205
|
+
private var uiModeConfiguration: Int = Configuration.UI_MODE_NIGHT_UNDEFINED
|
|
206
|
+
private var androidTabBarStyle = AndroidTabBarStyle.DEFAULT
|
|
207
|
+
private var ignoreBottomInsets = false
|
|
208
|
+
private var ignoreKeyboardInsets = true
|
|
209
|
+
private var isTabBarHidden = false
|
|
210
|
+
private var floatingBottomInset = 0
|
|
211
|
+
private var floatingLeftInset = 0
|
|
212
|
+
private var floatingRightInset = 0
|
|
213
|
+
private var floatingIndicatorPositioned = false
|
|
214
|
+
// Lens state: 0 is the resting selection pill, 1 is the lifted glass lens.
|
|
215
|
+
private var floatingLift = 0f
|
|
216
|
+
private var floatingStretch = 0f
|
|
217
|
+
private var floatingLiftAnimator: ValueAnimator? = null
|
|
218
|
+
private var floatingSlideAnimator: ValueAnimator? = null
|
|
219
|
+
private var floatingTouchView: View? = null
|
|
220
|
+
private var floatingTouchItemId = View.NO_ID
|
|
221
|
+
private var floatingCandidateItemId = View.NO_ID
|
|
222
|
+
private var floatingDragging = false
|
|
223
|
+
private var floatingTouchStartRawX = 0f
|
|
224
|
+
private var floatingTouchStartRawY = 0f
|
|
225
|
+
private var floatingLastMoveRawX = 0f
|
|
226
|
+
private var floatingLastMoveTime = 0L
|
|
227
|
+
private var floatingRestingFillColor = Color.TRANSPARENT
|
|
228
|
+
private var floatingLensFillColor = Color.TRANSPARENT
|
|
229
|
+
private var floatingLensStrokeColor = Color.TRANSPARENT
|
|
230
|
+
private val floatingTouchSlop = ViewConfiguration.get(context).scaledTouchSlop
|
|
231
|
+
private val floatingBarLocationOnScreen = IntArray(2)
|
|
232
|
+
private var lastPressedItemId = View.NO_ID
|
|
233
|
+
private var lastPressedTime = 0L
|
|
234
|
+
private var defaultLabelTextSizes: Pair<Float, Float>? = null
|
|
235
|
+
private var barTintColor: Int? = null
|
|
236
|
+
private var activeIndicatorColor: ColorStateList? = null
|
|
237
|
+
private var rippleColor: ColorStateList? = null
|
|
238
|
+
private var defaultItemBackground = bottomNavigation.itemBackground
|
|
239
|
+
private var defaultBackgroundTintList = bottomNavigation.backgroundTintList
|
|
240
|
+
private var defaultElevation = bottomNavigation.elevation
|
|
241
|
+
private var defaultActiveIndicatorEnabled = bottomNavigation.isItemActiveIndicatorEnabled
|
|
242
|
+
private var defaultActiveIndicatorColor = bottomNavigation.itemActiveIndicatorColor
|
|
243
|
+
private var defaultMinimumHeight = bottomNavigation.minimumHeight
|
|
244
|
+
private var defaultPaddingLeft = bottomNavigation.paddingLeft
|
|
245
|
+
private var defaultPaddingTop = bottomNavigation.paddingTop
|
|
246
|
+
private var defaultPaddingRight = bottomNavigation.paddingRight
|
|
247
|
+
private var defaultPaddingBottom = bottomNavigation.paddingBottom
|
|
248
|
+
private var defaultItemGravity = bottomNavigation.itemGravity
|
|
249
|
+
private var defaultItemPaddingTop = bottomNavigation.itemPaddingTop
|
|
250
|
+
private var defaultItemPaddingBottom = bottomNavigation.itemPaddingBottom
|
|
251
|
+
private var defaultItemIconSize = bottomNavigation.itemIconSize
|
|
252
|
+
private var defaultItemRippleColor = bottomNavigation.itemRippleColor
|
|
253
|
+
|
|
254
|
+
private val imageLoader = ImageLoader.Builder(context)
|
|
255
|
+
.components {
|
|
256
|
+
add(SvgDecoder.Factory())
|
|
257
|
+
}
|
|
258
|
+
.build()
|
|
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
|
+
|
|
273
|
+
init {
|
|
274
|
+
orientation = VERTICAL
|
|
275
|
+
layoutHolder.isSaveEnabled = false
|
|
276
|
+
floatingRoot.clipChildren = false
|
|
277
|
+
floatingRoot.clipToPadding = false
|
|
278
|
+
// The pressed lens grows past the bar edges, so the bar does not clip; its
|
|
279
|
+
// shadow comes from a capsule outline instead.
|
|
280
|
+
floatingBar.clipChildren = false
|
|
281
|
+
floatingBar.clipToPadding = false
|
|
282
|
+
floatingBar.outlineProvider = object : ViewOutlineProvider() {
|
|
283
|
+
override fun getOutline(view: View, outline: Outline) {
|
|
284
|
+
outline.setRoundRect(0, 0, view.width, view.height, view.height / 2f)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
floatingIndicatorDrawable.shape = GradientDrawable.RECTANGLE
|
|
288
|
+
floatingIndicator.background = floatingIndicatorDrawable
|
|
289
|
+
floatingBar.addOnLayoutChangeListener { _, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
|
|
290
|
+
if (right - left != oldRight - oldLeft || bottom - top != oldBottom - oldTop) {
|
|
291
|
+
floatingBar.invalidateOutline()
|
|
292
|
+
updateFloatingIndicator(animate = false)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
attachDefaultLayout()
|
|
297
|
+
uiModeConfiguration = resources.configuration.uiMode
|
|
298
|
+
|
|
299
|
+
ViewCompat.setOnApplyWindowInsetsListener(this) { _, insets ->
|
|
300
|
+
val navigationBars = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
|
|
301
|
+
val keyboardBottom = if (ignoreKeyboardInsets) {
|
|
302
|
+
0
|
|
303
|
+
} else {
|
|
304
|
+
insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
|
|
305
|
+
}
|
|
306
|
+
floatingBottomInset = maxOf(if (ignoreBottomInsets) 0 else navigationBars.bottom, keyboardBottom)
|
|
307
|
+
floatingLeftInset = navigationBars.left
|
|
308
|
+
floatingRightInset = navigationBars.right
|
|
309
|
+
if (androidTabBarStyle == AndroidTabBarStyle.FLOATING) {
|
|
310
|
+
updateFloatingBarLayout()
|
|
311
|
+
}
|
|
312
|
+
insets
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private fun dp(value: Float): Int =
|
|
319
|
+
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, resources.displayMetrics).roundToInt()
|
|
320
|
+
|
|
321
|
+
private fun removeFromParent(view: View) {
|
|
322
|
+
(view.parent as? ViewGroup)?.removeView(view)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private fun detachTabBarViews() {
|
|
326
|
+
releaseBottomBlur()
|
|
327
|
+
removeFromParent(layoutHolder)
|
|
328
|
+
removeFromParent(bottomNavigation)
|
|
329
|
+
removeFromParent(floatingRoot)
|
|
330
|
+
removeFromParent(floatingBar)
|
|
331
|
+
removeFromParent(floatingBarBackground)
|
|
332
|
+
removeFromParent(floatingIndicator)
|
|
333
|
+
super.removeAllViews()
|
|
334
|
+
floatingRoot.removeAllViews()
|
|
335
|
+
floatingBar.removeAllViews()
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private fun attachDefaultLayout() {
|
|
339
|
+
resetFloatingGesture()
|
|
340
|
+
detachTabBarViews()
|
|
341
|
+
|
|
342
|
+
super.addView(
|
|
343
|
+
layoutHolder,
|
|
344
|
+
LayoutParams(LayoutParams.MATCH_PARENT, 0).apply { weight = 1f },
|
|
345
|
+
)
|
|
346
|
+
super.addView(
|
|
347
|
+
bottomNavigation,
|
|
348
|
+
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT),
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
bottomNavigation.setFloatingHorizontalPadding(null)
|
|
352
|
+
bottomNavigation.minimumHeight = defaultMinimumHeight
|
|
353
|
+
bottomNavigation.setPadding(
|
|
354
|
+
defaultPaddingLeft,
|
|
355
|
+
defaultPaddingTop,
|
|
356
|
+
defaultPaddingRight,
|
|
357
|
+
defaultPaddingBottom,
|
|
358
|
+
)
|
|
359
|
+
bottomNavigation.setIgnoreBottomInsets(ignoreBottomInsets)
|
|
360
|
+
bottomNavigation.setIgnoreKeyboardInsets(ignoreKeyboardInsets)
|
|
361
|
+
bottomNavigation.elevation = defaultElevation
|
|
362
|
+
bottomNavigation.isItemActiveIndicatorEnabled = defaultActiveIndicatorEnabled
|
|
363
|
+
bottomNavigation.itemActiveIndicatorColor =
|
|
364
|
+
activeIndicatorColor ?: defaultActiveIndicatorColor
|
|
365
|
+
bottomNavigation.itemGravity = defaultItemGravity
|
|
366
|
+
bottomNavigation.itemPaddingTop = defaultItemPaddingTop
|
|
367
|
+
bottomNavigation.itemPaddingBottom = defaultItemPaddingBottom
|
|
368
|
+
(bottomNavigation.getChildAt(0) as? ViewGroup)?.isMotionEventSplittingEnabled = true
|
|
369
|
+
setNavigationIconSize(defaultItemIconSize)
|
|
370
|
+
if (barTintColor != null) {
|
|
371
|
+
applyDefaultBarBackground()
|
|
372
|
+
} else {
|
|
373
|
+
bottomNavigation.itemBackground = defaultItemBackground
|
|
374
|
+
bottomNavigation.backgroundTintList = defaultBackgroundTintList
|
|
375
|
+
}
|
|
376
|
+
bottomNavigation.itemRippleColor = rippleColor ?: defaultItemRippleColor
|
|
377
|
+
bottomNavigation.visibility = if (isTabBarHidden) GONE else VISIBLE
|
|
378
|
+
ViewCompat.requestApplyInsets(this)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private fun attachFloatingLayout() {
|
|
382
|
+
resetFloatingGesture()
|
|
383
|
+
detachTabBarViews()
|
|
384
|
+
|
|
385
|
+
val barHeight = dp(FloatingTabBarMetrics.BAR_HEIGHT)
|
|
386
|
+
val indicatorInset = dp(FloatingTabBarMetrics.INDICATOR_INSET)
|
|
387
|
+
floatingRoot.addView(
|
|
388
|
+
layoutHolder,
|
|
389
|
+
FrameLayout.LayoutParams(
|
|
390
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
391
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
392
|
+
),
|
|
393
|
+
)
|
|
394
|
+
floatingBar.addView(
|
|
395
|
+
floatingBarBackground,
|
|
396
|
+
FrameLayout.LayoutParams(
|
|
397
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
398
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
399
|
+
),
|
|
400
|
+
)
|
|
401
|
+
floatingBar.addView(
|
|
402
|
+
floatingIndicator,
|
|
403
|
+
FrameLayout.LayoutParams(0, barHeight - indicatorInset * 2, Gravity.TOP or Gravity.START).apply {
|
|
404
|
+
topMargin = indicatorInset
|
|
405
|
+
},
|
|
406
|
+
)
|
|
407
|
+
floatingBar.addView(
|
|
408
|
+
bottomNavigation,
|
|
409
|
+
FrameLayout.LayoutParams(
|
|
410
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
411
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
412
|
+
),
|
|
413
|
+
)
|
|
414
|
+
floatingRoot.addView(
|
|
415
|
+
floatingBar,
|
|
416
|
+
FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, barHeight, Gravity.BOTTOM),
|
|
417
|
+
)
|
|
418
|
+
super.addView(
|
|
419
|
+
floatingRoot,
|
|
420
|
+
LayoutParams(LayoutParams.MATCH_PARENT, 0).apply { weight = 1f },
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
bottomNavigation.setFloatingHorizontalPadding(dp(FloatingTabBarMetrics.BAR_CONTENT_PADDING))
|
|
424
|
+
bottomNavigation.minimumHeight = 0
|
|
425
|
+
bottomNavigation.setIgnoreBottomInsets(true)
|
|
426
|
+
bottomNavigation.setIgnoreKeyboardInsets(true)
|
|
427
|
+
bottomNavigation.isItemActiveIndicatorEnabled = false
|
|
428
|
+
bottomNavigation.itemActiveIndicatorColor = ColorStateList.valueOf(Color.TRANSPARENT)
|
|
429
|
+
bottomNavigation.itemBackground = ColorDrawable(Color.TRANSPARENT)
|
|
430
|
+
bottomNavigation.backgroundTintList = ColorStateList.valueOf(Color.TRANSPARENT)
|
|
431
|
+
bottomNavigation.itemRippleColor = ColorStateList.valueOf(Color.TRANSPARENT)
|
|
432
|
+
bottomNavigation.itemGravity = NavigationBarView.ITEM_GRAVITY_CENTER
|
|
433
|
+
bottomNavigation.itemPaddingTop = 0
|
|
434
|
+
bottomNavigation.itemPaddingBottom = 0
|
|
435
|
+
bottomNavigation.elevation = 0f
|
|
436
|
+
bottomNavigation.visibility = VISIBLE
|
|
437
|
+
// One finger owns the lens; a second finger must not retarget it mid-drag.
|
|
438
|
+
(bottomNavigation.getChildAt(0) as? ViewGroup)?.isMotionEventSplittingEnabled = false
|
|
439
|
+
setNavigationIconSize(dp(FloatingTabBarMetrics.ICON_SIZE))
|
|
440
|
+
floatingBar.elevation = dp(FloatingTabBarMetrics.SHADOW_ELEVATION).toFloat()
|
|
441
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
442
|
+
floatingBar.outlineAmbientShadowColor = Color.argb(38, 0, 0, 0)
|
|
443
|
+
floatingBar.outlineSpotShadowColor = Color.argb(64, 0, 0, 0)
|
|
444
|
+
}
|
|
445
|
+
floatingBar.visibility = if (isTabBarHidden) GONE else VISIBLE
|
|
446
|
+
floatingIndicatorPositioned = false
|
|
447
|
+
updateFloatingAppearance()
|
|
448
|
+
updateFloatingBarLayout()
|
|
449
|
+
post { updateFloatingIndicator(animate = false) }
|
|
450
|
+
ViewCompat.requestApplyInsets(this)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
private fun setNavigationIconSize(size: Int) {
|
|
454
|
+
if (bottomNavigation.itemIconSize == size) {
|
|
455
|
+
return
|
|
456
|
+
}
|
|
457
|
+
bottomNavigation.itemIconSize = size
|
|
458
|
+
// Icons are rasterized at the icon size, so reload them for the new size.
|
|
459
|
+
drawableCache.clear()
|
|
460
|
+
iconSources.forEach { (index, source) ->
|
|
461
|
+
bottomNavigation.menu.findItem(index)?.let { menuItem ->
|
|
462
|
+
getDrawable(source) { menuItem.icon = it }
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
private fun updateFloatingBarLayout() {
|
|
468
|
+
if (androidTabBarStyle != AndroidTabBarStyle.FLOATING) {
|
|
469
|
+
return
|
|
470
|
+
}
|
|
471
|
+
val sideMargin = dp(FloatingTabBarMetrics.BAR_SIDE_MARGIN)
|
|
472
|
+
val params = floatingBar.layoutParams as? FrameLayout.LayoutParams ?: return
|
|
473
|
+
params.leftMargin = sideMargin + floatingLeftInset
|
|
474
|
+
params.rightMargin = sideMargin + floatingRightInset
|
|
475
|
+
params.bottomMargin = maxOf(floatingBottomInset, dp(FloatingTabBarMetrics.BAR_MIN_BOTTOM_GAP)) +
|
|
476
|
+
dp(FloatingTabBarMetrics.BAR_BOTTOM_EXTRA)
|
|
477
|
+
floatingBar.layoutParams = params
|
|
478
|
+
floatingRoot.requestLayout()
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
private fun getTabBarMeasuredHeight(): Int {
|
|
482
|
+
if (isTabBarHidden) {
|
|
483
|
+
return 0
|
|
484
|
+
}
|
|
485
|
+
if (androidTabBarStyle != AndroidTabBarStyle.FLOATING) {
|
|
486
|
+
return bottomNavigation.height
|
|
487
|
+
}
|
|
488
|
+
val params = floatingBar.layoutParams as? FrameLayout.LayoutParams
|
|
489
|
+
return floatingBar.height + (params?.bottomMargin ?: 0)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
private fun blendColor(base: Int, overlay: Int, amount: Float): Int {
|
|
493
|
+
val inverse = 1f - amount
|
|
494
|
+
return Color.argb(
|
|
495
|
+
Color.alpha(base),
|
|
496
|
+
(Color.red(base) * inverse + Color.red(overlay) * amount).roundToInt(),
|
|
497
|
+
(Color.green(base) * inverse + Color.green(overlay) * amount).roundToInt(),
|
|
498
|
+
(Color.blue(base) * inverse + Color.blue(overlay) * amount).roundToInt(),
|
|
499
|
+
)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
private fun withAlpha(color: Int, alpha: Float): Int =
|
|
503
|
+
Color.argb((alpha * 255f).roundToInt(), Color.red(color), Color.green(color), Color.blue(color))
|
|
504
|
+
|
|
505
|
+
private fun isDarkColor(color: Int): Boolean {
|
|
506
|
+
val luminance =
|
|
507
|
+
(Color.red(color) * 0.299f) +
|
|
508
|
+
(Color.green(color) * 0.587f) +
|
|
509
|
+
(Color.blue(color) * 0.114f)
|
|
510
|
+
return luminance < 128f
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private fun resolvedBarTintColor(): Int =
|
|
514
|
+
barTintColor
|
|
515
|
+
?: Utils.getDefaultColorFor(context, android.R.attr.colorBackground)
|
|
516
|
+
?: Color.BLACK
|
|
517
|
+
|
|
518
|
+
private fun applyDefaultBarBackground() {
|
|
519
|
+
val color = resolvedBarTintColor()
|
|
520
|
+
bottomNavigation.itemBackground = ColorDrawable(color)
|
|
521
|
+
bottomNavigation.backgroundTintList = ColorStateList.valueOf(color)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
private fun updateFloatingAppearance() {
|
|
525
|
+
if (androidTabBarStyle != AndroidTabBarStyle.FLOATING) {
|
|
526
|
+
return
|
|
527
|
+
}
|
|
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.
|
|
530
|
+
val baseColor = resolvedBarTintColor()
|
|
531
|
+
val isDark = isDarkColor(baseColor)
|
|
532
|
+
val contrastColor = if (isDark) Color.WHITE else Color.BLACK
|
|
533
|
+
val surfaceColor = if (isDark) blendColor(baseColor, Color.WHITE, 0.08f) else baseColor
|
|
534
|
+
floatingBarBackground.background = GradientDrawable().apply {
|
|
535
|
+
shape = GradientDrawable.RECTANGLE
|
|
536
|
+
cornerRadius = dp(FloatingTabBarMetrics.BAR_HEIGHT / 2f).toFloat()
|
|
537
|
+
setColor(withAlpha(surfaceColor, if (usesBottomBlur) 0.12f else 0.95f))
|
|
538
|
+
setStroke(maxOf(1, dp(0.5f)), withAlpha(contrastColor, if (isDark) 0.14f else 0.07f))
|
|
539
|
+
}
|
|
540
|
+
floatingRestingFillColor = activeIndicatorColor?.defaultColor
|
|
541
|
+
?: withAlpha(contrastColor, if (isDark) 0.14f else 0.07f)
|
|
542
|
+
// The lens reads as clear glass: a light fill that brightens what it covers,
|
|
543
|
+
// outlined by a rim where it extends past the bar.
|
|
544
|
+
floatingLensFillColor = withAlpha(Color.WHITE, if (isDark) 0.18f else 0.55f)
|
|
545
|
+
floatingLensStrokeColor = withAlpha(contrastColor, if (isDark) 0.3f else 0.1f)
|
|
546
|
+
applyFloatingIndicatorState()
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
private fun applyFloatingIndicatorState() {
|
|
550
|
+
val colorFraction = floatingLift.coerceIn(0f, 1f)
|
|
551
|
+
floatingIndicatorDrawable.cornerRadius = dp(
|
|
552
|
+
(FloatingTabBarMetrics.BAR_HEIGHT - FloatingTabBarMetrics.INDICATOR_INSET * 2) / 2f
|
|
553
|
+
).toFloat()
|
|
554
|
+
floatingIndicatorDrawable.setColor(
|
|
555
|
+
ColorUtils.blendARGB(floatingRestingFillColor, floatingLensFillColor, colorFraction)
|
|
556
|
+
)
|
|
557
|
+
floatingIndicatorDrawable.setStroke(
|
|
558
|
+
dp(1f),
|
|
559
|
+
ColorUtils.blendARGB(
|
|
560
|
+
withAlpha(floatingLensStrokeColor, 0f),
|
|
561
|
+
floatingLensStrokeColor,
|
|
562
|
+
colorFraction,
|
|
563
|
+
),
|
|
564
|
+
)
|
|
565
|
+
floatingIndicator.scaleX =
|
|
566
|
+
1f + (FloatingTabBarMetrics.LENS_SCALE_X - 1f) * floatingLift + floatingStretch
|
|
567
|
+
floatingIndicator.scaleY =
|
|
568
|
+
1f + (FloatingTabBarMetrics.LENS_SCALE_Y - 1f) * floatingLift - floatingStretch * 0.4f
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
private fun animateFloatingLift(target: Float) {
|
|
572
|
+
floatingLiftAnimator?.cancel()
|
|
573
|
+
val start = floatingLift
|
|
574
|
+
if (start == target) {
|
|
575
|
+
applyFloatingIndicatorState()
|
|
576
|
+
return
|
|
577
|
+
}
|
|
578
|
+
val lifting = target > start
|
|
579
|
+
floatingLiftAnimator = ValueAnimator.ofFloat(start, target).apply {
|
|
580
|
+
duration = if (lifting) 320L else 460L
|
|
581
|
+
interpolator = SpringInterpolator(dampingRatio = if (lifting) 0.6f else 0.72f)
|
|
582
|
+
addUpdateListener {
|
|
583
|
+
floatingLift = it.animatedValue as Float
|
|
584
|
+
applyFloatingIndicatorState()
|
|
585
|
+
}
|
|
586
|
+
start()
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
private fun animateFloatingIndicatorTo(targetX: Float, durationMs: Long = 440L) {
|
|
591
|
+
floatingSlideAnimator?.cancel()
|
|
592
|
+
val startX = floatingIndicator.x
|
|
593
|
+
if (abs(targetX - startX) < 0.5f) {
|
|
594
|
+
floatingIndicator.x = targetX
|
|
595
|
+
floatingStretch = 0f
|
|
596
|
+
applyFloatingIndicatorState()
|
|
597
|
+
return
|
|
598
|
+
}
|
|
599
|
+
var previousX = startX
|
|
600
|
+
floatingSlideAnimator = ValueAnimator.ofFloat(startX, targetX).apply {
|
|
601
|
+
duration = durationMs
|
|
602
|
+
interpolator = SpringInterpolator(dampingRatio = 0.74f)
|
|
603
|
+
addUpdateListener {
|
|
604
|
+
val x = it.animatedValue as Float
|
|
605
|
+
val frameDelta = abs(x - previousX)
|
|
606
|
+
previousX = x
|
|
607
|
+
floatingIndicator.x = x
|
|
608
|
+
// The capsule stretches along the travel direction, like a liquid drop.
|
|
609
|
+
val rawStretch = (frameDelta / floatingIndicator.width.coerceAtLeast(1) * 1.6f)
|
|
610
|
+
.coerceAtMost(FloatingTabBarMetrics.LENS_MAX_STRETCH)
|
|
611
|
+
floatingStretch = floatingStretch * 0.5f + rawStretch * 0.5f
|
|
612
|
+
applyFloatingIndicatorState()
|
|
613
|
+
}
|
|
614
|
+
doOnEnd {
|
|
615
|
+
floatingStretch = 0f
|
|
616
|
+
applyFloatingIndicatorState()
|
|
617
|
+
}
|
|
618
|
+
start()
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
private fun updateFloatingIndicator(animate: Boolean) {
|
|
623
|
+
if (
|
|
624
|
+
androidTabBarStyle != AndroidTabBarStyle.FLOATING ||
|
|
625
|
+
isTabBarHidden ||
|
|
626
|
+
floatingTouchItemId != View.NO_ID
|
|
627
|
+
) {
|
|
628
|
+
return
|
|
629
|
+
}
|
|
630
|
+
val selectedKey = selectedItem ?: return
|
|
631
|
+
val selectedIndex = items.indexOfFirst { it.key == selectedKey }
|
|
632
|
+
if (selectedIndex == -1) {
|
|
633
|
+
return
|
|
634
|
+
}
|
|
635
|
+
val itemView = bottomNavigation.findViewById<View>(selectedIndex) ?: return
|
|
636
|
+
if (itemView.width == 0 || floatingBar.width == 0) {
|
|
637
|
+
floatingIndicator.visibility = INVISIBLE
|
|
638
|
+
return
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
updateFloatingIndicatorWidth(itemView.width)
|
|
642
|
+
val targetX = getFloatingItemX(selectedIndex) ?: return
|
|
643
|
+
floatingIndicator.visibility = VISIBLE
|
|
644
|
+
|
|
645
|
+
if (!animate || !floatingIndicatorPositioned) {
|
|
646
|
+
floatingSlideAnimator?.cancel()
|
|
647
|
+
floatingIndicator.x = targetX
|
|
648
|
+
floatingStretch = 0f
|
|
649
|
+
floatingIndicatorPositioned = true
|
|
650
|
+
applyFloatingIndicatorState()
|
|
651
|
+
return
|
|
652
|
+
}
|
|
653
|
+
animateFloatingIndicatorTo(targetX)
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
private fun updateFloatingIndicatorWidth(width: Int) {
|
|
657
|
+
val params = floatingIndicator.layoutParams as? FrameLayout.LayoutParams ?: return
|
|
658
|
+
if (params.width != width) {
|
|
659
|
+
params.width = width
|
|
660
|
+
floatingIndicator.layoutParams = params
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
private fun getFloatingItemX(itemId: Int): Float? {
|
|
665
|
+
val menuView = bottomNavigation.getChildAt(0) as? ViewGroup ?: return null
|
|
666
|
+
val itemView = bottomNavigation.findViewById<View>(itemId) ?: return null
|
|
667
|
+
if (itemView.width == 0 || itemView.visibility != VISIBLE) {
|
|
668
|
+
return null
|
|
669
|
+
}
|
|
670
|
+
return bottomNavigation.x + menuView.x + itemView.x
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
private fun getNearestVisibleItemId(centerX: Float): Int? {
|
|
674
|
+
val menuView = bottomNavigation.getChildAt(0) as? ViewGroup ?: return null
|
|
675
|
+
var nearestItemId: Int? = null
|
|
676
|
+
var nearestDistance = Float.MAX_VALUE
|
|
677
|
+
items.indices.forEach { itemId ->
|
|
678
|
+
val itemView = bottomNavigation.findViewById<View>(itemId) ?: return@forEach
|
|
679
|
+
if (itemView.visibility != VISIBLE || itemView.width == 0) {
|
|
680
|
+
return@forEach
|
|
681
|
+
}
|
|
682
|
+
val itemCenterX = bottomNavigation.x + menuView.x + itemView.x + itemView.width / 2f
|
|
683
|
+
val distance = abs(itemCenterX - centerX)
|
|
684
|
+
if (distance < nearestDistance) {
|
|
685
|
+
nearestDistance = distance
|
|
686
|
+
nearestItemId = itemId
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return nearestItemId
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
private fun setFloatingItemMagnified(magnifiedItemId: Int) {
|
|
693
|
+
items.indices.forEach { itemId ->
|
|
694
|
+
val itemView = bottomNavigation.findViewById<View>(itemId) ?: return@forEach
|
|
695
|
+
val targetScale = if (itemId == magnifiedItemId) {
|
|
696
|
+
FloatingTabBarMetrics.ITEM_PRESSED_SCALE
|
|
697
|
+
} else {
|
|
698
|
+
1f
|
|
699
|
+
}
|
|
700
|
+
if (itemView.scaleX == targetScale && itemView.scaleY == targetScale) {
|
|
701
|
+
return@forEach
|
|
702
|
+
}
|
|
703
|
+
itemView.animate()
|
|
704
|
+
.scaleX(targetScale)
|
|
705
|
+
.scaleY(targetScale)
|
|
706
|
+
.setDuration(260L)
|
|
707
|
+
.setInterpolator(SpringInterpolator(dampingRatio = 0.64f))
|
|
708
|
+
.start()
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Mirrors the iOS 26 tab bar: touching any tab lifts a glass lens onto it,
|
|
713
|
+
// dragging scrubs the lens across tabs, and release selects the tab under it.
|
|
714
|
+
@SuppressLint("ClickableViewAccessibility")
|
|
715
|
+
private fun handleFloatingTabTouch(view: View, item: MenuItem, event: MotionEvent): Boolean {
|
|
716
|
+
if (androidTabBarStyle != AndroidTabBarStyle.FLOATING) {
|
|
717
|
+
return false
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
when (event.actionMasked) {
|
|
721
|
+
MotionEvent.ACTION_DOWN -> {
|
|
722
|
+
if (isTabBarHidden || floatingBar.width == 0) {
|
|
723
|
+
return false
|
|
724
|
+
}
|
|
725
|
+
clearFloatingTouchTracking()
|
|
726
|
+
floatingTouchView = view
|
|
727
|
+
floatingTouchItemId = item.itemId
|
|
728
|
+
floatingCandidateItemId = item.itemId
|
|
729
|
+
floatingTouchStartRawX = event.rawX
|
|
730
|
+
floatingTouchStartRawY = event.rawY
|
|
731
|
+
floatingLastMoveRawX = event.rawX
|
|
732
|
+
floatingLastMoveTime = event.eventTime
|
|
733
|
+
view.parent?.requestDisallowInterceptTouchEvent(true)
|
|
734
|
+
beginFloatingPress(item.itemId)
|
|
735
|
+
return true
|
|
736
|
+
}
|
|
737
|
+
MotionEvent.ACTION_MOVE -> {
|
|
738
|
+
if (floatingTouchView !== view) {
|
|
739
|
+
return true
|
|
740
|
+
}
|
|
741
|
+
if (!floatingDragging) {
|
|
742
|
+
val deltaX = event.rawX - floatingTouchStartRawX
|
|
743
|
+
val deltaY = event.rawY - floatingTouchStartRawY
|
|
744
|
+
if (deltaX * deltaX + deltaY * deltaY <= floatingTouchSlop * floatingTouchSlop) {
|
|
745
|
+
return true
|
|
746
|
+
}
|
|
747
|
+
floatingDragging = true
|
|
748
|
+
}
|
|
749
|
+
updateFloatingDrag(event.rawX, event.eventTime)
|
|
750
|
+
return true
|
|
751
|
+
}
|
|
752
|
+
MotionEvent.ACTION_UP -> {
|
|
753
|
+
if (floatingTouchView !== view) {
|
|
754
|
+
return true
|
|
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
|
+
}
|
|
761
|
+
if (floatingDragging) {
|
|
762
|
+
updateFloatingDrag(event.rawX, event.eventTime)
|
|
763
|
+
}
|
|
764
|
+
finishFloatingPress(view, cancelled = false)
|
|
765
|
+
return true
|
|
766
|
+
}
|
|
767
|
+
MotionEvent.ACTION_CANCEL -> {
|
|
768
|
+
if (floatingTouchView === view) {
|
|
769
|
+
finishFloatingPress(view, cancelled = true)
|
|
770
|
+
}
|
|
771
|
+
return true
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
return true
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
private fun beginFloatingPress(itemId: Int) {
|
|
778
|
+
val itemView = bottomNavigation.findViewById<View>(itemId) ?: return
|
|
779
|
+
val targetX = getFloatingItemX(itemId) ?: return
|
|
780
|
+
updateFloatingIndicatorWidth(itemView.width)
|
|
781
|
+
floatingIndicator.visibility = VISIBLE
|
|
782
|
+
if (floatingIndicatorPositioned) {
|
|
783
|
+
animateFloatingIndicatorTo(targetX, durationMs = 320L)
|
|
784
|
+
} else {
|
|
785
|
+
floatingIndicator.x = targetX
|
|
786
|
+
floatingIndicatorPositioned = true
|
|
787
|
+
}
|
|
788
|
+
animateFloatingLift(1f)
|
|
789
|
+
setFloatingItemMagnified(itemId)
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
private fun updateFloatingDrag(rawX: Float, eventTime: Long) {
|
|
793
|
+
if (floatingBar.width == 0 || floatingIndicator.width == 0) {
|
|
794
|
+
return
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
floatingBar.getLocationOnScreen(floatingBarLocationOnScreen)
|
|
798
|
+
val halfWidth = floatingIndicator.width / 2f
|
|
799
|
+
val contentPadding = dp(FloatingTabBarMetrics.BAR_CONTENT_PADDING).toFloat()
|
|
800
|
+
val minCenter = contentPadding + halfWidth
|
|
801
|
+
val maxCenter = floatingBar.width - contentPadding - halfWidth
|
|
802
|
+
val fingerCenter = rawX - floatingBarLocationOnScreen[0]
|
|
803
|
+
// Past either end the lens follows with resistance instead of stopping hard.
|
|
804
|
+
val centerX = when {
|
|
805
|
+
fingerCenter < minCenter ->
|
|
806
|
+
minCenter - (minCenter - fingerCenter) * FloatingTabBarMetrics.DRAG_EDGE_RESISTANCE
|
|
807
|
+
fingerCenter > maxCenter ->
|
|
808
|
+
maxCenter + (fingerCenter - maxCenter) * FloatingTabBarMetrics.DRAG_EDGE_RESISTANCE
|
|
809
|
+
else -> fingerCenter
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
val elapsed = (eventTime - floatingLastMoveTime).coerceAtLeast(1L)
|
|
813
|
+
val velocityDpPerMs = abs(rawX - floatingLastMoveRawX) / elapsed / resources.displayMetrics.density
|
|
814
|
+
floatingLastMoveRawX = rawX
|
|
815
|
+
floatingLastMoveTime = eventTime
|
|
816
|
+
val targetStretch = (velocityDpPerMs * 0.06f).coerceAtMost(FloatingTabBarMetrics.LENS_MAX_STRETCH)
|
|
817
|
+
floatingStretch = floatingStretch * 0.6f + targetStretch * 0.4f
|
|
818
|
+
|
|
819
|
+
floatingSlideAnimator?.cancel()
|
|
820
|
+
floatingIndicator.x = centerX - halfWidth
|
|
821
|
+
applyFloatingIndicatorState()
|
|
822
|
+
floatingBar.removeCallbacks(floatingStretchDecay)
|
|
823
|
+
floatingBar.postDelayed(floatingStretchDecay, 64L)
|
|
824
|
+
|
|
825
|
+
getNearestVisibleItemId(centerX)?.let { itemId ->
|
|
826
|
+
if (itemId != floatingCandidateItemId) {
|
|
827
|
+
floatingCandidateItemId = itemId
|
|
828
|
+
setFloatingItemMagnified(itemId)
|
|
829
|
+
emitHapticFeedback(HapticFeedbackConstants.CLOCK_TICK)
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
private val floatingStretchDecay = Runnable {
|
|
835
|
+
if (floatingTouchItemId == View.NO_ID || floatingStretch == 0f) {
|
|
836
|
+
return@Runnable
|
|
837
|
+
}
|
|
838
|
+
ValueAnimator.ofFloat(floatingStretch, 0f).apply {
|
|
839
|
+
duration = 180L
|
|
840
|
+
interpolator = DecelerateInterpolator()
|
|
841
|
+
addUpdateListener {
|
|
842
|
+
if (floatingTouchItemId != View.NO_ID) {
|
|
843
|
+
floatingStretch = it.animatedValue as Float
|
|
844
|
+
applyFloatingIndicatorState()
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
start()
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
private fun finishFloatingPress(view: View, cancelled: Boolean) {
|
|
852
|
+
val requestedItemId = if (floatingDragging) floatingCandidateItemId else floatingTouchItemId
|
|
853
|
+
val wasDragging = floatingDragging
|
|
854
|
+
clearFloatingTouchTracking()
|
|
855
|
+
setFloatingItemMagnified(View.NO_ID)
|
|
856
|
+
animateFloatingLift(0f)
|
|
857
|
+
|
|
858
|
+
if (!cancelled && requestedItemId != View.NO_ID) {
|
|
859
|
+
if (!wasDragging) {
|
|
860
|
+
view.performClick()
|
|
861
|
+
} else if (requestedItemId != bottomNavigation.selectedItemId) {
|
|
862
|
+
bottomNavigation.menu.findItem(requestedItemId)?.let { menuItem ->
|
|
863
|
+
onTabSelected(menuItem)
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
// Settles on the selected tab, which is the old one for cancelled or
|
|
868
|
+
// preventsDefault presses.
|
|
869
|
+
updateFloatingIndicator(animate = true)
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
private fun clearFloatingTouchTracking() {
|
|
873
|
+
floatingBar.removeCallbacks(floatingStretchDecay)
|
|
874
|
+
floatingTouchView = null
|
|
875
|
+
floatingTouchItemId = View.NO_ID
|
|
876
|
+
floatingCandidateItemId = View.NO_ID
|
|
877
|
+
floatingDragging = false
|
|
878
|
+
floatingTouchStartRawX = 0f
|
|
879
|
+
floatingTouchStartRawY = 0f
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
private fun resetFloatingGesture() {
|
|
883
|
+
clearFloatingTouchTracking()
|
|
884
|
+
floatingLiftAnimator?.cancel()
|
|
885
|
+
floatingSlideAnimator?.cancel()
|
|
886
|
+
floatingLift = 0f
|
|
887
|
+
floatingStretch = 0f
|
|
888
|
+
items.indices.forEach { itemId ->
|
|
889
|
+
bottomNavigation.findViewById<View>(itemId)?.apply {
|
|
890
|
+
animate().cancel()
|
|
891
|
+
scaleX = 1f
|
|
892
|
+
scaleY = 1f
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
applyFloatingIndicatorState()
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
private val layoutCallback = Choreographer.FrameCallback {
|
|
899
|
+
isLayoutEnqueued = false
|
|
900
|
+
refreshLayout()
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
private fun refreshLayout() {
|
|
904
|
+
measure(
|
|
905
|
+
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
906
|
+
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY),
|
|
907
|
+
)
|
|
908
|
+
layout(left, top, right, bottom)
|
|
909
|
+
}
|
|
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
|
+
|
|
1025
|
+
override fun requestLayout() {
|
|
1026
|
+
super.requestLayout()
|
|
1027
|
+
@Suppress("SENSELESS_COMPARISON") // layoutCallback can be null here since this method can be called in init
|
|
1028
|
+
|
|
1029
|
+
if (!isLayoutEnqueued && layoutCallback != null) {
|
|
1030
|
+
isLayoutEnqueued = true
|
|
1031
|
+
// we use NATIVE_ANIMATED_MODULE choreographer queue because it allows us to catch the current
|
|
1032
|
+
// looper loop instead of enqueueing the update in the next loop causing a one frame delay.
|
|
1033
|
+
ReactChoreographer
|
|
1034
|
+
.getInstance()
|
|
1035
|
+
.postFrameCallback(
|
|
1036
|
+
ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE,
|
|
1037
|
+
layoutCallback,
|
|
1038
|
+
)
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
fun setSelectedItem(value: String) {
|
|
1043
|
+
selectedItem = value
|
|
1044
|
+
syncSelectedItem()
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
private fun syncSelectedItem() {
|
|
1048
|
+
val selectedKey = selectedItem ?: return
|
|
1049
|
+
val selectedIndex = items.indexOfFirst { it.key == selectedKey }
|
|
1050
|
+
if (selectedIndex == -1 || selectedIndex >= layoutHolder.childCount) {
|
|
1051
|
+
return
|
|
1052
|
+
}
|
|
1053
|
+
setSelectedIndex(selectedIndex)
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams?) {
|
|
1057
|
+
if (child === layoutHolder || child === bottomNavigation || child === floatingRoot) {
|
|
1058
|
+
super.addView(child, index, params)
|
|
1059
|
+
return
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
val container = createContainer()
|
|
1063
|
+
container.addView(child, params)
|
|
1064
|
+
layoutHolder.addView(container, index)
|
|
1065
|
+
|
|
1066
|
+
if (index in items.indices && selectedItem == items[index].key) {
|
|
1067
|
+
syncSelectedItem()
|
|
1068
|
+
refreshLayout()
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
private fun createContainer(): FrameLayout {
|
|
1073
|
+
val container = TabSceneContainer(context).apply {
|
|
1074
|
+
layoutParams = FrameLayout.LayoutParams(
|
|
1075
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
1076
|
+
FrameLayout.LayoutParams.MATCH_PARENT
|
|
1077
|
+
)
|
|
1078
|
+
isSaveEnabled = false
|
|
1079
|
+
visibility = if (disablePageAnimations) INVISIBLE else GONE
|
|
1080
|
+
isEnabled = false
|
|
1081
|
+
}
|
|
1082
|
+
return container
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
private fun setSelectedIndex(itemId: Int) {
|
|
1086
|
+
val selectionChanged = bottomNavigation.selectedItemId != itemId
|
|
1087
|
+
bottomNavigation.selectedItemId = itemId
|
|
1088
|
+
if (androidTabBarStyle == AndroidTabBarStyle.FLOATING) {
|
|
1089
|
+
post { updateFloatingIndicator(animate = selectionChanged) }
|
|
1090
|
+
}
|
|
1091
|
+
if (disablePageAnimations) {
|
|
1092
|
+
// INVISIBLE scenes stay laid out, so this visibility swap is atomic at
|
|
1093
|
+
// the next draw boundary and does not expose an empty container frame.
|
|
1094
|
+
toggleViewVisibility(layoutHolder.getChildAt(itemId), true)
|
|
1095
|
+
layoutHolder.forEachIndexed { index, view ->
|
|
1096
|
+
if (itemId != index) {
|
|
1097
|
+
toggleViewVisibility(view, false)
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
layoutHolder.invalidate()
|
|
1101
|
+
return
|
|
1102
|
+
}
|
|
1103
|
+
if (layoutHolder.getChildAt(itemId)?.visibility == VISIBLE) {
|
|
1104
|
+
return
|
|
1105
|
+
}
|
|
1106
|
+
val fadeThrough = MaterialFadeThrough()
|
|
1107
|
+
TransitionManager.beginDelayedTransition(layoutHolder, fadeThrough)
|
|
1108
|
+
// Apply every visibility change to the transition's captured frame.
|
|
1109
|
+
layoutHolder.forEachIndexed { index, view ->
|
|
1110
|
+
if (itemId == index) {
|
|
1111
|
+
toggleViewVisibility(view, true)
|
|
1112
|
+
} else {
|
|
1113
|
+
toggleViewVisibility(view, false)
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
layoutHolder.requestLayout()
|
|
1118
|
+
layoutHolder.invalidate()
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
private fun toggleViewVisibility(view: View, isVisible: Boolean) {
|
|
1122
|
+
check(view is TabSceneContainer) { "Native component tree is corrupted." }
|
|
1123
|
+
|
|
1124
|
+
view.pointerEvents = if (isVisible) PointerEvents.AUTO else PointerEvents.NONE
|
|
1125
|
+
view.visibility = if (isVisible) {
|
|
1126
|
+
VISIBLE
|
|
1127
|
+
} else if (disablePageAnimations) {
|
|
1128
|
+
INVISIBLE
|
|
1129
|
+
} else {
|
|
1130
|
+
GONE
|
|
1131
|
+
}
|
|
1132
|
+
view.isEnabled = isVisible
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
private fun onTabSelected(item: MenuItem, emitHaptic: Boolean = true) {
|
|
1136
|
+
val selectedItem = items[item.itemId]
|
|
1137
|
+
selectedItem.let {
|
|
1138
|
+
if (!selectedItem.preventsDefault) {
|
|
1139
|
+
setSelectedItem(selectedItem.key)
|
|
1140
|
+
}
|
|
1141
|
+
onTabSelectedListener?.invoke(selectedItem.key)
|
|
1142
|
+
if (emitHaptic) {
|
|
1143
|
+
emitHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
|
|
1144
|
+
}
|
|
1145
|
+
emitDoublePressIfNeeded(item.itemId, selectedItem.key)
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
private fun emitDoublePressIfNeeded(itemId: Int, key: String) {
|
|
1150
|
+
val now = SystemClock.uptimeMillis()
|
|
1151
|
+
if (
|
|
1152
|
+
lastPressedItemId == itemId &&
|
|
1153
|
+
now - lastPressedTime <= ViewConfiguration.getDoubleTapTimeout()
|
|
1154
|
+
) {
|
|
1155
|
+
lastPressedItemId = View.NO_ID
|
|
1156
|
+
onTabDoublePressedListener?.invoke(key)
|
|
1157
|
+
return
|
|
1158
|
+
}
|
|
1159
|
+
lastPressedItemId = itemId
|
|
1160
|
+
lastPressedTime = now
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
fun setIgnoreBottomInsets(ignore: Boolean) {
|
|
1164
|
+
ignoreBottomInsets = ignore
|
|
1165
|
+
bottomNavigation.setIgnoreBottomInsets(
|
|
1166
|
+
if (androidTabBarStyle == AndroidTabBarStyle.FLOATING) true else ignore
|
|
1167
|
+
)
|
|
1168
|
+
ViewCompat.requestApplyInsets(this)
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
fun setAndroidTabBarStyle(value: String?) {
|
|
1172
|
+
val nextStyle = if (value == "floating") {
|
|
1173
|
+
AndroidTabBarStyle.FLOATING
|
|
1174
|
+
} else {
|
|
1175
|
+
AndroidTabBarStyle.DEFAULT
|
|
1176
|
+
}
|
|
1177
|
+
if (nextStyle == androidTabBarStyle) {
|
|
1178
|
+
return
|
|
1179
|
+
}
|
|
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()
|
|
1184
|
+
if (nextStyle == AndroidTabBarStyle.FLOATING) {
|
|
1185
|
+
attachFloatingLayout()
|
|
1186
|
+
} else {
|
|
1187
|
+
attachDefaultLayout()
|
|
1188
|
+
}
|
|
1189
|
+
updateBlurAppearance()
|
|
1190
|
+
updateTextAppearance()
|
|
1191
|
+
requestLayout()
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
fun setIgnoreKeyboardInsets(ignore: Boolean) {
|
|
1195
|
+
ignoreKeyboardInsets = ignore
|
|
1196
|
+
bottomNavigation.setIgnoreKeyboardInsets(
|
|
1197
|
+
if (androidTabBarStyle == AndroidTabBarStyle.FLOATING) true else ignore
|
|
1198
|
+
)
|
|
1199
|
+
ViewCompat.requestApplyInsets(this)
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
fun setTabBarHidden(isHidden: Boolean) {
|
|
1203
|
+
isTabBarHidden = isHidden
|
|
1204
|
+
if (isHidden) {
|
|
1205
|
+
resetFloatingGesture()
|
|
1206
|
+
}
|
|
1207
|
+
if (androidTabBarStyle == AndroidTabBarStyle.FLOATING) {
|
|
1208
|
+
floatingBar.visibility = if (isHidden) GONE else VISIBLE
|
|
1209
|
+
bottomNavigation.visibility = VISIBLE
|
|
1210
|
+
} else {
|
|
1211
|
+
bottomNavigation.visibility = if (isHidden) GONE else VISIBLE
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
updateBlurAppearance()
|
|
1215
|
+
requestLayout()
|
|
1216
|
+
floatingRoot.invalidate()
|
|
1217
|
+
invalidate()
|
|
1218
|
+
if (!isHidden) post { updateFloatingIndicator(animate = false) }
|
|
1219
|
+
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
fun updateItems(items: MutableList<TabInfo>) {
|
|
1223
|
+
// If an item got removed, let's re-add all items
|
|
1224
|
+
if (items.size < this.items.size) {
|
|
1225
|
+
bottomNavigation.menu.clear()
|
|
1226
|
+
}
|
|
1227
|
+
this.items = items
|
|
1228
|
+
items.forEachIndexed { index, item ->
|
|
1229
|
+
val menuItem = getOrCreateItem(index, item.title)
|
|
1230
|
+
if (item.title != menuItem.title) {
|
|
1231
|
+
menuItem.title = item.title
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
menuItem.isVisible = !item.hidden
|
|
1235
|
+
if (iconSources.containsKey(index)) {
|
|
1236
|
+
getDrawable(iconSources[index]!!) {
|
|
1237
|
+
menuItem.icon = it
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
if (item.badge?.isNotEmpty() == true) {
|
|
1242
|
+
val badge = bottomNavigation.getOrCreateBadge(index)
|
|
1243
|
+
badge.isVisible = true
|
|
1244
|
+
// Set the badge text only if it's different than an empty space to show a small badge.
|
|
1245
|
+
// More context: https://github.com/callstackincubator/react-native-bottom-tabs/issues/422
|
|
1246
|
+
if (item.badge != " ") {
|
|
1247
|
+
badge.text = item.badge
|
|
1248
|
+
}
|
|
1249
|
+
// Apply badge colors if provided (Material will use its default theme colors otherwise)
|
|
1250
|
+
item.badgeBackgroundColor?.let { badge.backgroundColor = it }
|
|
1251
|
+
item.badgeTextColor?.let { badge.badgeTextColor = it }
|
|
1252
|
+
} else {
|
|
1253
|
+
bottomNavigation.removeBadge(index)
|
|
1254
|
+
}
|
|
1255
|
+
post {
|
|
1256
|
+
val itemView = bottomNavigation.findViewById<View>(menuItem.itemId)
|
|
1257
|
+
itemView?.let { view ->
|
|
1258
|
+
view.setOnClickListener {
|
|
1259
|
+
onTabSelected(menuItem)
|
|
1260
|
+
}
|
|
1261
|
+
view.setOnTouchListener { touchedView, event ->
|
|
1262
|
+
handleFloatingTabTouch(touchedView, menuItem, event)
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
item.testID?.let { testId ->
|
|
1266
|
+
view.findViewById<View>(com.google.android.material.R.id.navigation_bar_item_content_container)
|
|
1267
|
+
?.apply {
|
|
1268
|
+
tag = testId
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
syncSelectedItem()
|
|
1276
|
+
|
|
1277
|
+
// Update tint colors and text appearance after updating all items.
|
|
1278
|
+
post {
|
|
1279
|
+
updateTextAppearance()
|
|
1280
|
+
updateTintColors()
|
|
1281
|
+
updateFloatingIndicator(animate = false)
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
private fun getOrCreateItem(index: Int, title: String): MenuItem {
|
|
1286
|
+
return bottomNavigation.menu.findItem(index) ?: bottomNavigation.menu.add(0, index, 0, title)
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
fun setIcons(icons: ReadableArray?) {
|
|
1290
|
+
if (icons == null || icons.size() == 0) {
|
|
1291
|
+
return
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
for (idx in 0 until icons.size()) {
|
|
1295
|
+
val source = icons.getMap(idx)
|
|
1296
|
+
val uri = source?.getString("uri")
|
|
1297
|
+
if (uri.isNullOrEmpty()) {
|
|
1298
|
+
continue
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
val imageSource = ImageSource(context, uri)
|
|
1302
|
+
this.iconSources[idx] = imageSource
|
|
1303
|
+
|
|
1304
|
+
// Update existing item if exists.
|
|
1305
|
+
bottomNavigation.menu.findItem(idx)?.let { menuItem ->
|
|
1306
|
+
getDrawable(imageSource) {
|
|
1307
|
+
menuItem.icon = it
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
fun setLabeled(labeled: Boolean?) {
|
|
1314
|
+
this.labeled = labeled
|
|
1315
|
+
bottomNavigation.labelVisibilityMode = when (labeled) {
|
|
1316
|
+
false -> {
|
|
1317
|
+
LABEL_VISIBILITY_UNLABELED
|
|
1318
|
+
}
|
|
1319
|
+
true -> {
|
|
1320
|
+
LABEL_VISIBILITY_LABELED
|
|
1321
|
+
}
|
|
1322
|
+
else -> {
|
|
1323
|
+
LABEL_VISIBILITY_AUTO
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
fun setRippleColor(color: ColorStateList) {
|
|
1329
|
+
rippleColor = color
|
|
1330
|
+
// Floating mode replaces the ripple with the pressed lens.
|
|
1331
|
+
if (androidTabBarStyle != AndroidTabBarStyle.FLOATING) {
|
|
1332
|
+
bottomNavigation.itemRippleColor = color
|
|
1333
|
+
if (blurAppearanceApplied) savedItemBackground = bottomNavigation.itemBackground
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
@SuppressLint("CheckResult")
|
|
1338
|
+
private fun getDrawable(imageSource: ImageSource, onDrawableReady: (Drawable?) -> Unit) {
|
|
1339
|
+
drawableCache[imageSource]?.let {
|
|
1340
|
+
onDrawableReady(it)
|
|
1341
|
+
return
|
|
1342
|
+
}
|
|
1343
|
+
val iconSizePx = bottomNavigation.itemIconSize
|
|
1344
|
+
val request = ImageRequest.Builder(context)
|
|
1345
|
+
.data(imageSource.getUri(context))
|
|
1346
|
+
.size(CoilSize(iconSizePx, iconSizePx))
|
|
1347
|
+
.scale(Scale.FILL)
|
|
1348
|
+
.precision(Precision.EXACT)
|
|
1349
|
+
.target { drawable ->
|
|
1350
|
+
post {
|
|
1351
|
+
val stateDrawable = drawable.asDrawable(context.resources)
|
|
1352
|
+
drawableCache[imageSource] = stateDrawable
|
|
1353
|
+
onDrawableReady(stateDrawable)
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
.listener(
|
|
1357
|
+
onError = { _, result ->
|
|
1358
|
+
Log.e("BottomTabs", "Error loading image: ${imageSource.uri}", result.throwable)
|
|
1359
|
+
}
|
|
1360
|
+
)
|
|
1361
|
+
.build()
|
|
1362
|
+
|
|
1363
|
+
imageLoader.enqueue(request)
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
fun setBarTintColor(color: Int?) {
|
|
1367
|
+
// Set the color, either using the active background color or a default color.
|
|
1368
|
+
val backgroundColor =
|
|
1369
|
+
color ?: Utils.getDefaultColorFor(context, android.R.attr.colorPrimary) ?: return
|
|
1370
|
+
barTintColor = backgroundColor
|
|
1371
|
+
blurSurfaceColor = backgroundColor
|
|
1372
|
+
|
|
1373
|
+
if (androidTabBarStyle == AndroidTabBarStyle.FLOATING) {
|
|
1374
|
+
updateFloatingAppearance()
|
|
1375
|
+
} else if (blurAppearanceApplied) {
|
|
1376
|
+
savedItemBackground = ColorDrawable(backgroundColor)
|
|
1377
|
+
savedBarTint = ColorStateList.valueOf(backgroundColor)
|
|
1378
|
+
} else {
|
|
1379
|
+
bottomNavigation.itemBackground = ColorDrawable(backgroundColor)
|
|
1380
|
+
bottomNavigation.backgroundTintList = ColorStateList.valueOf(backgroundColor)
|
|
1381
|
+
}
|
|
1382
|
+
hasCustomAppearance = true
|
|
1383
|
+
floatingRoot.invalidate()
|
|
1384
|
+
invalidate()
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
fun setActiveTintColor(color: Int?) {
|
|
1388
|
+
activeTintColor = color
|
|
1389
|
+
updateTintColors()
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
fun setInactiveTintColor(color: Int?) {
|
|
1393
|
+
inactiveTintColor = color
|
|
1394
|
+
updateTintColors()
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
fun setActiveIndicatorColor(color: ColorStateList) {
|
|
1398
|
+
activeIndicatorColor = color
|
|
1399
|
+
if (androidTabBarStyle == AndroidTabBarStyle.FLOATING) {
|
|
1400
|
+
updateFloatingAppearance()
|
|
1401
|
+
} else {
|
|
1402
|
+
bottomNavigation.itemActiveIndicatorColor = color
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
fun setFontSize(size: Int) {
|
|
1407
|
+
fontSize = size
|
|
1408
|
+
updateTextAppearance()
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
fun setFontFamily(family: String?) {
|
|
1412
|
+
fontFamily = family
|
|
1413
|
+
updateTextAppearance()
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
fun setFontWeight(weight: String?) {
|
|
1417
|
+
val fontWeight = ReactTypefaceUtils.parseFontWeight(weight)
|
|
1418
|
+
this.fontWeight = fontWeight
|
|
1419
|
+
updateTextAppearance()
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
fun onDropViewInstance() {
|
|
1423
|
+
androidBottomBlurEnabled = false
|
|
1424
|
+
updateBlurAppearance()
|
|
1425
|
+
releaseBottomBlur()
|
|
1426
|
+
resetFloatingGesture()
|
|
1427
|
+
imageLoader.shutdown()
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
private fun updateTextAppearance() {
|
|
1431
|
+
val isFloating = androidTabBarStyle == AndroidTabBarStyle.FLOATING
|
|
1432
|
+
val customSize = fontSize?.toFloat()?.takeIf { it > 0 }
|
|
1433
|
+
// Floating labels use one compact size for both states, as on iOS. Material's
|
|
1434
|
+
// own sizes are remembered so leaving floating mode can restore them.
|
|
1435
|
+
val restoreLabelSizes = if (!isFloating && customSize == null) defaultLabelTextSizes else null
|
|
1436
|
+
if (
|
|
1437
|
+
!isFloating &&
|
|
1438
|
+
restoreLabelSizes == null &&
|
|
1439
|
+
fontSize == null &&
|
|
1440
|
+
fontFamily == null &&
|
|
1441
|
+
fontWeight == null
|
|
1442
|
+
) {
|
|
1443
|
+
return
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
val typeface = if (fontFamily != null || fontWeight != null) {
|
|
1447
|
+
ReactFontManager.getInstance().getTypeface(
|
|
1448
|
+
fontFamily ?: "",
|
|
1449
|
+
Utils.getTypefaceStyle(fontWeight),
|
|
1450
|
+
context.assets
|
|
1451
|
+
)
|
|
1452
|
+
} else null
|
|
1453
|
+
val size = customSize ?: if (isFloating) FloatingTabBarMetrics.LABEL_SIZE_SP else null
|
|
1454
|
+
|
|
1455
|
+
val menuView = bottomNavigation.getChildAt(0) as? ViewGroup ?: return
|
|
1456
|
+
for (i in 0 until menuView.childCount) {
|
|
1457
|
+
val item = menuView.getChildAt(i)
|
|
1458
|
+
val largeLabel =
|
|
1459
|
+
item.findViewById<TextView>(com.google.android.material.R.id.navigation_bar_item_large_label_view)
|
|
1460
|
+
val smallLabel =
|
|
1461
|
+
item.findViewById<TextView>(com.google.android.material.R.id.navigation_bar_item_small_label_view)
|
|
1462
|
+
if (isFloating && defaultLabelTextSizes == null && largeLabel != null && smallLabel != null) {
|
|
1463
|
+
defaultLabelTextSizes = largeLabel.textSize to smallLabel.textSize
|
|
1464
|
+
}
|
|
1465
|
+
if (restoreLabelSizes != null) {
|
|
1466
|
+
largeLabel?.setTextSize(TypedValue.COMPLEX_UNIT_PX, restoreLabelSizes.first)
|
|
1467
|
+
smallLabel?.setTextSize(TypedValue.COMPLEX_UNIT_PX, restoreLabelSizes.second)
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
listOf(largeLabel, smallLabel).forEach { label ->
|
|
1471
|
+
label?.apply {
|
|
1472
|
+
size?.let { size ->
|
|
1473
|
+
setTextSize(TypedValue.COMPLEX_UNIT_SP, size)
|
|
1474
|
+
}
|
|
1475
|
+
typeface?.let { setTypeface(it) }
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
private fun emitHapticFeedback(feedbackConstants: Int) {
|
|
1482
|
+
// OneKey patch: use the Fabric-populated View flag on every supported API level.
|
|
1483
|
+
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && hapticFeedbackEnabled) {
|
|
1484
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && isHapticFeedbackEnabled) {
|
|
1485
|
+
this.performHapticFeedback(feedbackConstants)
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
private fun updateTintColors() {
|
|
1490
|
+
// First let's check current item color.
|
|
1491
|
+
val currentItemTintColor = items.firstOrNull { it.key == selectedItem }?.activeTintColor
|
|
1492
|
+
|
|
1493
|
+
// getDefaultColor will always return a valid color but to satisfy the compiler we need to check for null
|
|
1494
|
+
val colorPrimary = currentItemTintColor ?: activeTintColor ?: Utils.getDefaultColorFor(
|
|
1495
|
+
context,
|
|
1496
|
+
android.R.attr.colorPrimary
|
|
1497
|
+
) ?: return
|
|
1498
|
+
val colorSecondary =
|
|
1499
|
+
inactiveTintColor ?: Utils.getDefaultColorFor(context, android.R.attr.textColorSecondary)
|
|
1500
|
+
?: return
|
|
1501
|
+
val states = arrayOf(uncheckedStateSet, checkedStateSet)
|
|
1502
|
+
val colors = intArrayOf(colorSecondary, colorPrimary)
|
|
1503
|
+
|
|
1504
|
+
ColorStateList(states, colors).apply {
|
|
1505
|
+
this@BottomTabsView.bottomNavigation.itemTextColor = this
|
|
1506
|
+
this@BottomTabsView.bottomNavigation.itemIconTintList = this
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
override fun onConfigurationChanged(newConfig: Configuration?) {
|
|
1511
|
+
super.onConfigurationChanged(newConfig)
|
|
1512
|
+
releaseBottomBlur()
|
|
1513
|
+
if (androidBottomBlurEnabled) {
|
|
1514
|
+
requestLayout()
|
|
1515
|
+
floatingRoot.invalidate()
|
|
1516
|
+
invalidate()
|
|
1517
|
+
}
|
|
1518
|
+
if (uiModeConfiguration == newConfig?.uiMode || hasCustomAppearance) {
|
|
1519
|
+
return
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
// User has hidden the bottom navigation bar, don't re-attach it.
|
|
1523
|
+
if (isTabBarHidden) {
|
|
1524
|
+
return
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// If appearance wasn't changed re-create the bottom navigation view when configuration changes.
|
|
1528
|
+
// React Native opts out ouf Activity re-creation when configuration changes, this workarounds that.
|
|
1529
|
+
// We also opt-out of this recreation when custom styles are used.
|
|
1530
|
+
blurAppearanceApplied = false
|
|
1531
|
+
removeFromParent(bottomNavigation)
|
|
1532
|
+
bottomNavigation = ExtendedBottomNavigationView(context)
|
|
1533
|
+
defaultItemBackground = bottomNavigation.itemBackground
|
|
1534
|
+
defaultBackgroundTintList = bottomNavigation.backgroundTintList
|
|
1535
|
+
defaultElevation = bottomNavigation.elevation
|
|
1536
|
+
defaultActiveIndicatorEnabled = bottomNavigation.isItemActiveIndicatorEnabled
|
|
1537
|
+
defaultActiveIndicatorColor = bottomNavigation.itemActiveIndicatorColor
|
|
1538
|
+
defaultMinimumHeight = bottomNavigation.minimumHeight
|
|
1539
|
+
defaultPaddingLeft = bottomNavigation.paddingLeft
|
|
1540
|
+
defaultPaddingTop = bottomNavigation.paddingTop
|
|
1541
|
+
defaultPaddingRight = bottomNavigation.paddingRight
|
|
1542
|
+
defaultPaddingBottom = bottomNavigation.paddingBottom
|
|
1543
|
+
defaultItemGravity = bottomNavigation.itemGravity
|
|
1544
|
+
defaultItemPaddingTop = bottomNavigation.itemPaddingTop
|
|
1545
|
+
defaultItemPaddingBottom = bottomNavigation.itemPaddingBottom
|
|
1546
|
+
defaultItemIconSize = bottomNavigation.itemIconSize
|
|
1547
|
+
defaultItemRippleColor = bottomNavigation.itemRippleColor
|
|
1548
|
+
if (androidTabBarStyle == AndroidTabBarStyle.FLOATING) {
|
|
1549
|
+
attachFloatingLayout()
|
|
1550
|
+
} else {
|
|
1551
|
+
attachDefaultLayout()
|
|
1552
|
+
}
|
|
1553
|
+
updateItems(items)
|
|
1554
|
+
setLabeled(this.labeled)
|
|
1555
|
+
this.selectedItem?.let { setSelectedItem(it) }
|
|
1556
|
+
uiModeConfiguration = newConfig?.uiMode ?: uiModeConfiguration
|
|
1557
|
+
}
|
|
1558
|
+
}
|