@onekeyfe/react-native-bottom-tabs 6.7.0-19 → 6.7.0-20

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.
Files changed (28) hide show
  1. package/android/src/main/java/com/onekey/bottomtabs/BottomTabsDrawerManager.kt +55 -0
  2. package/android/src/main/java/com/onekey/bottomtabs/BottomTabsDrawerView.kt +496 -0
  3. package/android/src/main/java/com/onekey/bottomtabs/BottomTabsPackage.kt +1 -1
  4. package/android/src/main/java/com/onekey/bottomtabs/BottomTabsViewManager.kt +3 -0
  5. package/docs/SPEC.md +163 -2
  6. package/ios/RCTBottomTabsComponentView.mm +37 -0
  7. package/ios/RCTBottomTabsContainerView.swift +192 -7
  8. package/ios/RCTBottomTabsDrawerComponentView.h +8 -0
  9. package/ios/RCTBottomTabsDrawerComponentView.mm +79 -0
  10. package/ios/RCTBottomTabsDrawerContainerView.swift +327 -0
  11. package/lib/module/BottomTabs.js +144 -87
  12. package/lib/module/BottomTabsDrawerNativeComponent.ts +19 -0
  13. package/lib/module/BottomTabsDrawerNativeComponent.web.js +13 -0
  14. package/lib/module/BottomTabsNativeComponent.ts +13 -0
  15. package/lib/typescript/src/BottomTabs.d.ts +36 -10
  16. package/lib/typescript/src/BottomTabsDrawerNativeComponent.d.ts +15 -0
  17. package/lib/typescript/src/BottomTabsDrawerNativeComponent.web.d.ts +4 -0
  18. package/lib/typescript/src/BottomTabsNativeComponent.d.ts +8 -0
  19. package/lib/typescript/src/index.d.ts +1 -1
  20. package/lib/typescript/src/types.d.ts +1 -0
  21. package/package.json +3 -2
  22. package/react-native.config.js +4 -1
  23. package/src/BottomTabs.tsx +235 -115
  24. package/src/BottomTabsDrawerNativeComponent.ts +19 -0
  25. package/src/BottomTabsDrawerNativeComponent.web.tsx +9 -0
  26. package/src/BottomTabsNativeComponent.ts +13 -0
  27. package/src/index.tsx +1 -1
  28. package/src/types.ts +3 -0
@@ -0,0 +1,55 @@
1
+ package com.onekey.bottomtabs
2
+
3
+ import android.view.View
4
+ import com.facebook.react.bridge.Arguments
5
+ import com.facebook.react.uimanager.ThemedReactContext
6
+ import com.facebook.react.uimanager.UIManagerHelper
7
+ import com.facebook.react.uimanager.ViewGroupManager
8
+ import com.facebook.react.uimanager.ViewManagerDelegate
9
+ import com.facebook.react.uimanager.events.Event
10
+ import com.facebook.react.viewmanagers.RNBottomTabsDrawerManagerDelegate
11
+ import com.facebook.react.viewmanagers.RNBottomTabsDrawerManagerInterface
12
+
13
+ class BottomTabsDrawerManager : ViewGroupManager<BottomTabsDrawerView>(),
14
+ RNBottomTabsDrawerManagerInterface<BottomTabsDrawerView> {
15
+ private val delegate = RNBottomTabsDrawerManagerDelegate(this)
16
+ override fun getName() = "RNBottomTabsDrawer"
17
+ override fun getDelegate(): ViewManagerDelegate<BottomTabsDrawerView> = delegate
18
+
19
+ override fun createViewInstance(context: ThemedReactContext): BottomTabsDrawerView {
20
+ val view = BottomTabsDrawerView(context)
21
+ view.onDrawerOpenChange = { open ->
22
+ UIManagerHelper.getEventDispatcherForReactTag(context, view.id)?.dispatchEvent(
23
+ DrawerOpenChangeEvent(UIManagerHelper.getSurfaceId(context), view.id, open),
24
+ )
25
+ }
26
+ return view
27
+ }
28
+
29
+ override fun addView(parent: BottomTabsDrawerView, child: View, index: Int) = parent.addReactChild(child, index)
30
+ override fun getChildCount(parent: BottomTabsDrawerView) = parent.getReactChildCount()
31
+ override fun getChildAt(parent: BottomTabsDrawerView, index: Int) = parent.getReactChild(index)
32
+ override fun removeViewAt(parent: BottomTabsDrawerView, index: Int) = parent.removeReactChild(index)
33
+ override fun removeAllViews(parent: BottomTabsDrawerView) {
34
+ while (parent.getReactChildCount() > 0) parent.removeReactChild(parent.getReactChildCount() - 1)
35
+ }
36
+ override fun needsCustomLayoutForChildren() = true
37
+ override fun onDropViewInstance(view: BottomTabsDrawerView) {
38
+ view.onDrawerOpenChange = null
39
+ view.dispose()
40
+ super.onDropViewInstance(view)
41
+ }
42
+
43
+ override fun setEnabled(view: BottomTabsDrawerView, value: Boolean) = view.setDrawerEnabled(value)
44
+ override fun setOpen(view: BottomTabsDrawerView, value: Boolean) = view.setDrawerOpen(value)
45
+ override fun setSwipeEnabled(view: BottomTabsDrawerView, value: Boolean) { view.swipeEnabled = value }
46
+ override fun setHapticFeedbackEnabled(view: BottomTabsDrawerView, value: Boolean) { view.drawerHapticFeedbackEnabled = value }
47
+ override fun setBottomBarHeight(view: BottomTabsDrawerView, value: Double) { view.bottomBarHeight = value.toFloat() }
48
+ }
49
+
50
+ private class DrawerOpenChangeEvent(surfaceId: Int, tag: Int, private val open: Boolean) :
51
+ Event<DrawerOpenChangeEvent>(surfaceId, tag) {
52
+ override fun getEventName() = "onDrawerOpenChange"
53
+ override fun canCoalesce() = false
54
+ override fun getEventData() = Arguments.createMap().apply { putBoolean("open", open) }
55
+ }
@@ -0,0 +1,496 @@
1
+ package com.onekey.bottomtabs
2
+
3
+ import android.animation.Animator
4
+ import android.animation.AnimatorListenerAdapter
5
+ import android.animation.ValueAnimator
6
+ import android.app.Activity
7
+ import android.content.Context
8
+ import android.content.ContextWrapper
9
+ import android.graphics.Canvas
10
+ import android.graphics.Color
11
+ import android.graphics.Matrix
12
+ import android.graphics.Path
13
+ import android.graphics.RectF
14
+ import android.view.HapticFeedbackConstants
15
+ import android.view.MotionEvent
16
+ import android.view.VelocityTracker
17
+ import android.view.View
18
+ import android.view.ViewConfiguration
19
+ import android.view.ViewGroup
20
+ import android.view.SurfaceView
21
+ import android.view.TextureView
22
+ import android.view.animation.LinearInterpolator
23
+ import android.widget.FrameLayout
24
+ import android.widget.AbsSeekBar
25
+ import android.widget.EditText
26
+ import android.widget.CompoundButton
27
+ import android.webkit.WebView
28
+ import androidx.activity.OnBackPressedCallback
29
+ import androidx.activity.OnBackPressedDispatcherOwner
30
+ import androidx.core.view.ViewCompat
31
+ import androidx.core.view.WindowInsetsCompat
32
+ import androidx.viewpager2.widget.ViewPager2
33
+ import com.facebook.react.bridge.ReactContext
34
+ import com.facebook.react.uimanager.PointerEvents
35
+ import com.facebook.react.uimanager.ReactPointerEventsView
36
+ import com.facebook.react.uimanager.events.NativeGestureUtil
37
+ import kotlin.math.abs
38
+ import kotlin.math.exp
39
+ import kotlin.math.roundToInt
40
+ import java.lang.ref.WeakReference
41
+
42
+ private class DrawerContent(context: Context) : FrameLayout(context), ReactPointerEventsView {
43
+ override var pointerEvents = PointerEvents.AUTO
44
+ }
45
+
46
+ private class DrawerForeground(context: Context) : FrameLayout(context) {
47
+ var cornerRadius = 0f
48
+ private val path = Path()
49
+ private val rect = RectF()
50
+ override fun dispatchDraw(canvas: Canvas) {
51
+ val save = canvas.save()
52
+ rect.set(0f, 0f, width.toFloat(), height.toFloat())
53
+ val r = cornerRadius
54
+ path.reset()
55
+ path.addRoundRect(rect, floatArrayOf(r, r, 0f, 0f, 0f, 0f, r, r), Path.Direction.CW)
56
+ canvas.clipPath(path)
57
+ super.dispatchDraw(canvas)
58
+ canvas.restoreToCount(save)
59
+ }
60
+ }
61
+
62
+ /** React owns two stable slots; translation, input and settling stay on the UI thread. */
63
+ class BottomTabsDrawerView(context: Context) : FrameLayout(context) {
64
+ private val menu = DrawerContent(context)
65
+ private val menuShade = View(context)
66
+ private val foreground = DrawerForeground(context)
67
+ private val content = DrawerContent(context)
68
+ private val dim = View(context)
69
+ private val slots = mutableListOf<View>()
70
+ var onDrawerOpenChange: ((Boolean) -> Unit)? = null
71
+ var swipeEnabled = false
72
+ var drawerHapticFeedbackEnabled = false
73
+ var bottomBarHeight = 0f
74
+ private var drawerEnabled = false
75
+ private var requestedOpen = false
76
+ private var stableOpen = false
77
+ private var progress = 0f
78
+ private var animator: ValueAnimator? = null
79
+ private var animationTarget = false
80
+ private var animationUser = false
81
+ private var downX = 0f
82
+ private var downY = 0f
83
+ private var downProgress = 0f
84
+ private var gestureReturnOpen = false
85
+ private var eligible = false
86
+ private var touchedView: WeakReference<View>? = null
87
+ private val touchMatrix = Matrix()
88
+ private val touchPoint = FloatArray(2)
89
+ private var dragging = false
90
+ private var tracker: VelocityTracker? = null
91
+ private var lastNativeEvent: MotionEvent? = null
92
+ private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
93
+ private val density = resources.displayMetrics.density
94
+ private var backRegistered = false
95
+ private var detached = true
96
+ private var initialized = false
97
+ private val backCallback = object : OnBackPressedCallback(false) {
98
+ override fun handleOnBackPressed() { closeFromUser() }
99
+ }
100
+
101
+ init {
102
+ clipChildren = true
103
+ menuShade.setBackgroundColor(Color.BLACK)
104
+ menuShade.isEnabled = false
105
+ menuShade.importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO
106
+ dim.setBackgroundColor(Color.rgb(28, 28, 28))
107
+ dim.contentDescription = "Close menu"
108
+ dim.importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_YES
109
+ dim.setOnClickListener { closeFromUser() }
110
+ menu.addView(menuShade, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
111
+ foreground.addView(content, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
112
+ foreground.addView(dim, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
113
+ super.addView(menu, LayoutParams(0, LayoutParams.MATCH_PARENT))
114
+ super.addView(foreground, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
115
+ initialized = true
116
+ setProgress(0f)
117
+ }
118
+
119
+ fun setDrawerEnabled(value: Boolean) {
120
+ if (drawerEnabled == value) return
121
+ cancelTouch()
122
+ stopAnimation()
123
+ drawerEnabled = value
124
+ stableOpen = false
125
+ setProgress(0f)
126
+ if (value && requestedOpen) settle(true, 0f, false)
127
+ }
128
+
129
+ fun setDrawerOpen(value: Boolean) {
130
+ if (requestedOpen == value) return
131
+ requestedOpen = value
132
+ cancelTouch()
133
+ settle(drawerEnabled && value, 0f, false)
134
+ }
135
+
136
+ fun addReactChild(child: View, index: Int) {
137
+ require(index in 0..slots.size && slots.size < 2)
138
+ slots.add(index, child)
139
+ updateSlots()
140
+ }
141
+
142
+ fun getReactChildCount() = slots.size
143
+ fun getReactChild(index: Int): View = slots[index]
144
+ fun removeReactChild(index: Int) {
145
+ val child = slots.removeAt(index)
146
+ (child.parent as? ViewGroup)?.removeView(child)
147
+ updateSlots()
148
+ }
149
+
150
+ private fun updateSlots() {
151
+ slots.forEachIndexed { index, child ->
152
+ (child.parent as? ViewGroup)?.removeView(child)
153
+ (if (index == 0) menu else content).addView(
154
+ child, 0, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT),
155
+ )
156
+ }
157
+ requestLayout()
158
+ if (width > 0 && height > 0) layoutSlots()
159
+ }
160
+
161
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
162
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
163
+ menu.measure(MeasureSpec.makeMeasureSpec((measuredWidth * .78f).roundToInt(), MeasureSpec.EXACTLY),
164
+ MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY))
165
+ }
166
+
167
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
168
+ layoutSlots()
169
+ setProgress(progress)
170
+ }
171
+
172
+ private fun layoutSlots() {
173
+ val exactHeight = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
174
+ val menuWidth = (width * .78f).roundToInt()
175
+ menu.measure(MeasureSpec.makeMeasureSpec(menuWidth, MeasureSpec.EXACTLY), exactHeight)
176
+ foreground.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), exactHeight)
177
+ menu.layout(0, 0, menuWidth, height)
178
+ foreground.layout(0, 0, width, height)
179
+ // ReactViewGroup can absorb descendant requestLayout calls. A newly mounted
180
+ // slot must receive native bounds during this mounting transaction itself.
181
+ slots.forEachIndexed { index, child ->
182
+ val childWidth = if (index == 0) menuWidth else width
183
+ child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY), exactHeight)
184
+ child.layout(0, 0, childWidth, height)
185
+ }
186
+ }
187
+
188
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
189
+ super.onSizeChanged(w, h, oldw, oldh)
190
+ val target = if (animator != null) animationTarget else if (dragging) gestureReturnOpen else stableOpen
191
+ val user = animator != null && animationUser && oldw != 0
192
+ cancelTouch()
193
+ stopAnimation()
194
+ finish(drawerEnabled && if (oldw == 0) requestedOpen else target, user)
195
+ }
196
+
197
+ override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
198
+ when (event.actionMasked) {
199
+ MotionEvent.ACTION_DOWN -> {
200
+ cancelTouch()
201
+ downX = event.x
202
+ downY = event.y
203
+ downProgress = progress
204
+ touchedView = touchedViewAt(this, event.x, event.y)?.let(::WeakReference)
205
+ val systemLeft = ViewCompat.getRootWindowInsets(this)
206
+ ?.getInsets(WindowInsetsCompat.Type.systemGestures())?.left ?: 0
207
+ eligible = drawerEnabled && event.y < height - bottomBarHeight * density &&
208
+ (if (progress > 0f) event.x >= width * .78f * progress
209
+ else swipeEnabled && event.x >= systemLeft && !touchPathHasExclusiveControl())
210
+ if (eligible) tracker = VelocityTracker.obtain().also { it.addMovement(event) }
211
+ }
212
+ MotionEvent.ACTION_POINTER_DOWN -> {
213
+ val wasDragging = dragging
214
+ cancelTouch()
215
+ if (wasDragging) settle(gestureReturnOpen, 0f, false)
216
+ }
217
+ MotionEvent.ACTION_MOVE -> {
218
+ if (dragging) return true
219
+ if (!eligible) return false
220
+ tracker?.addMovement(event)
221
+ val dx = event.x - downX
222
+ val dy = event.y - downY
223
+ if (abs(dy) > touchSlop && abs(dy) >= abs(dx)) {
224
+ eligible = false
225
+ return false
226
+ }
227
+ if (abs(dx) > touchSlop && abs(dx) > abs(dy) && progress == 0f &&
228
+ (dx < 0f || touchPathCanScrollRight())) {
229
+ // This decision remains locked even if the child reaches its boundary.
230
+ eligible = false
231
+ return false
232
+ }
233
+ if (abs(dx) > touchSlop && abs(dx) > abs(dy) && (progress > 0f || dx > 0f)) {
234
+ gestureReturnOpen = if (animator != null && !animationUser) animationTarget else stableOpen
235
+ stopAnimation()
236
+ downProgress = progress
237
+ dragging = true
238
+ parent?.requestDisallowInterceptTouchEvent(true)
239
+ NativeGestureUtil.notifyNativeGestureStarted(this, event)
240
+ rememberNativeEvent(event)
241
+ return true
242
+ }
243
+ }
244
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> if (!dragging) cancelTouch()
245
+ }
246
+ return dragging
247
+ }
248
+
249
+ override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
250
+ // Nested pagers may disallow on DOWN, before direction/boundary is known.
251
+ // Once the child wins (eligible=false), preserve its normal ownership.
252
+ if (initialized && eligible && !dragging && disallowIntercept) return
253
+ super.requestDisallowInterceptTouchEvent(disallowIntercept)
254
+ }
255
+
256
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean {
257
+ val handled = super.dispatchTouchEvent(event)
258
+ if (event.actionMasked == MotionEvent.ACTION_UP || event.actionMasked == MotionEvent.ACTION_CANCEL) cancelTouch()
259
+ return handled
260
+ }
261
+
262
+ private fun touchedViewAt(view: View, x: Float, y: Float): View? {
263
+ var pointerEvents = (view as? ReactPointerEventsView)?.pointerEvents ?: PointerEvents.AUTO
264
+ if (!view.isEnabled) pointerEvents = when (pointerEvents) {
265
+ PointerEvents.AUTO -> PointerEvents.BOX_NONE
266
+ PointerEvents.BOX_ONLY -> PointerEvents.NONE
267
+ else -> pointerEvents
268
+ }
269
+ if (pointerEvents == PointerEvents.NONE) return null
270
+ if (view is ViewGroup && pointerEvents != PointerEvents.BOX_ONLY) {
271
+ // Fabric already orders native children for React zIndex. Native elevation
272
+ // can additionally reorder siblings; examine frontmost candidates first.
273
+ val order = (0 until view.childCount).sortedWith(
274
+ compareByDescending<Int> { view.getChildAt(it).z }.thenByDescending { it },
275
+ )
276
+ for (index in order) {
277
+ val child = view.getChildAt(index)
278
+ if (child.visibility != VISIBLE || child.alpha <= 0f) continue
279
+ touchPoint[0] = x + view.scrollX - child.left
280
+ touchPoint[1] = y + view.scrollY - child.top
281
+ if (!child.matrix.isIdentity) {
282
+ if (!child.matrix.invert(touchMatrix)) continue
283
+ touchMatrix.mapPoints(touchPoint)
284
+ }
285
+ val localX = touchPoint[0]
286
+ val localY = touchPoint[1]
287
+ if (localX >= 0 && localY >= 0 && localX < child.width && localY < child.height) {
288
+ touchedViewAt(child, localX, localY)?.let { return it }
289
+ }
290
+ }
291
+ }
292
+ return if (pointerEvents == PointerEvents.BOX_NONE) null else view
293
+ }
294
+
295
+ private fun touchPathHasExclusiveControl(): Boolean {
296
+ var view = touchedView?.get()
297
+ while (view != null && view !== this) {
298
+ if (view is AbsSeekBar || view is EditText || view is CompoundButton || view is WebView ||
299
+ view is SurfaceView || view is TextureView) return true
300
+ view = view.parent as? View
301
+ }
302
+ return false
303
+ }
304
+
305
+ private fun touchPathCanScrollRight(): Boolean {
306
+ var view = touchedView?.get()
307
+ while (view != null && view !== this) {
308
+ // ViewPager2 reports its scroll range even when user input is disabled.
309
+ // Skip only that pager and its direct RecyclerView, leaving any enabled
310
+ // horizontal list inside the page eligible to own its own gesture.
311
+ val pager = (view as? ViewPager2) ?: (view.parent as? ViewPager2)
312
+ if (pager?.isUserInputEnabled != false && view.canScrollHorizontally(-1)) return true
313
+ view = view.parent as? View
314
+ }
315
+ return false
316
+ }
317
+
318
+ override fun onTouchEvent(event: MotionEvent): Boolean {
319
+ if (!dragging) {
320
+ // A disabled pager or an empty native page may decline DOWN entirely.
321
+ // Retain that eligible stream and apply the same first-direction decision
322
+ // when Android routes later events straight to this host's onTouchEvent.
323
+ if (event.actionMasked == MotionEvent.ACTION_DOWN) return eligible || super.onTouchEvent(event)
324
+ val pending = eligible
325
+ if (pending) onInterceptTouchEvent(event)
326
+ if (!dragging) return pending || super.onTouchEvent(event)
327
+ }
328
+ rememberNativeEvent(event)
329
+ tracker?.addMovement(event)
330
+ when (event.actionMasked) {
331
+ MotionEvent.ACTION_MOVE -> setProgress(downProgress + (event.x - downX) / maxOf(1f, width * .78f))
332
+ MotionEvent.ACTION_UP -> {
333
+ setProgress(downProgress + (event.x - downX) / maxOf(1f, width * .78f))
334
+ tracker?.computeCurrentVelocity(1000)
335
+ val velocity = ((tracker?.xVelocity ?: 0f) / maxOf(1f, width * .78f)).coerceIn(-8f, 8f)
336
+ val target = progress + velocity * .12f >= .5f
337
+ cancelTouch()
338
+ settle(target, velocity, true)
339
+ }
340
+ MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_POINTER_DOWN -> {
341
+ cancelTouch()
342
+ settle(gestureReturnOpen, 0f, false)
343
+ }
344
+ }
345
+ return true
346
+ }
347
+
348
+ private fun rememberNativeEvent(event: MotionEvent) {
349
+ lastNativeEvent?.recycle()
350
+ lastNativeEvent = MotionEvent.obtain(event)
351
+ }
352
+
353
+ private fun cancelTouch() {
354
+ if (dragging) {
355
+ lastNativeEvent?.let { NativeGestureUtil.notifyNativeGestureEnded(this, it) }
356
+ parent?.requestDisallowInterceptTouchEvent(false)
357
+ }
358
+ dragging = false
359
+ eligible = false
360
+ touchedView = null
361
+ tracker?.recycle()
362
+ tracker = null
363
+ lastNativeEvent?.recycle()
364
+ lastNativeEvent = null
365
+ }
366
+
367
+ private fun closeFromUser() {
368
+ cancelTouch()
369
+ settle(false, 0f, true)
370
+ }
371
+
372
+ private fun setProgress(value: Float) {
373
+ progress = (if (drawerEnabled) value else 0f).coerceIn(0f, 1f)
374
+ foreground.translationX = width * .78f * progress
375
+ foreground.cornerRadius = 64f * density * progress
376
+ foreground.invalidate()
377
+ dim.alpha = .7f * progress
378
+ dim.visibility = if (progress > 0f) VISIBLE else INVISIBLE
379
+ dim.isEnabled = progress > 0f
380
+ content.pointerEvents = if (progress > 0f) PointerEvents.NONE else PointerEvents.AUTO
381
+ content.importantForAccessibility = if (progress > 0f) IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
382
+ else IMPORTANT_FOR_ACCESSIBILITY_AUTO
383
+ menu.translationX = -.12f * width * .78f * (1f - progress)
384
+ menuShade.alpha = .12f * (1f - progress)
385
+ menu.visibility = if (progress > 0f) VISIBLE else INVISIBLE
386
+ menu.pointerEvents = if (progress > 0f) PointerEvents.AUTO else PointerEvents.NONE
387
+ menu.importantForAccessibility = if (progress > 0f) IMPORTANT_FOR_ACCESSIBILITY_AUTO
388
+ else IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
389
+ updateBackCallback()
390
+ }
391
+
392
+ private fun settle(open: Boolean, velocity: Float, user: Boolean) {
393
+ stopAnimation()
394
+ val target = drawerEnabled && open
395
+ if (width == 0 || !isAttachedToWindow) {
396
+ stableOpen = target
397
+ setProgress(if (target) 1f else 0f)
398
+ return
399
+ }
400
+ if (progress == if (target) 1f else 0f) {
401
+ finish(target, user)
402
+ return
403
+ }
404
+ val end = if (target) 1f else 0f
405
+ val a = progress - end
406
+ val b = velocity.coerceIn(-8f, 8f) + 22f * a
407
+ animationTarget = target
408
+ animationUser = user
409
+ val next = ValueAnimator.ofFloat(0f, .5f)
410
+ next.duration = 500
411
+ next.interpolator = LinearInterpolator()
412
+ next.addUpdateListener {
413
+ val time = it.animatedValue as Float
414
+ setProgress(end + (a + b * time) * exp(-22f * time))
415
+ }
416
+ next.addListener(object : AnimatorListenerAdapter() {
417
+ override fun onAnimationEnd(animation: Animator) {
418
+ if (animator !== animation) return
419
+ animator = null
420
+ finish(target, user)
421
+ }
422
+ })
423
+ animator = next
424
+ next.start()
425
+ }
426
+
427
+ private fun finish(target: Boolean, user: Boolean) {
428
+ val changed = stableOpen != target
429
+ stableOpen = target
430
+ setProgress(if (target) 1f else 0f)
431
+ if (user && (changed || target != requestedOpen)) {
432
+ if (drawerHapticFeedbackEnabled && changed) performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
433
+ onDrawerOpenChange?.invoke(target)
434
+ }
435
+ }
436
+
437
+ private fun stopAnimation() {
438
+ animator?.removeAllListeners()
439
+ animator?.removeAllUpdateListeners()
440
+ animator?.cancel()
441
+ animator = null
442
+ }
443
+
444
+ private fun updateBackCallback() {
445
+ val active = !detached && isAttachedToWindow && isShown && windowVisibility == VISIBLE && drawerEnabled && progress > 0f
446
+ if (active && !backRegistered) {
447
+ val owner = findActivity(context) as? OnBackPressedDispatcherOwner
448
+ if (owner != null) {
449
+ owner.onBackPressedDispatcher.addCallback(backCallback)
450
+ backRegistered = true
451
+ }
452
+ } else if (!active && backRegistered) {
453
+ backCallback.remove()
454
+ backRegistered = false
455
+ }
456
+ backCallback.isEnabled = active
457
+ }
458
+
459
+ private fun findActivity(context: Context): Activity? = when (context) {
460
+ is Activity -> context
461
+ is ReactContext -> context.currentActivity
462
+ is ContextWrapper -> findActivity(context.baseContext)
463
+ else -> null
464
+ }
465
+
466
+ override fun onAttachedToWindow() {
467
+ super.onAttachedToWindow()
468
+ detached = false
469
+ updateBackCallback()
470
+ if (drawerEnabled && stableOpen != requestedOpen) settle(requestedOpen, 0f, false)
471
+ }
472
+
473
+ override fun onVisibilityChanged(changedView: View, visibility: Int) {
474
+ super.onVisibilityChanged(changedView, visibility)
475
+ if (initialized) updateBackCallback()
476
+ }
477
+
478
+ override fun onWindowVisibilityChanged(visibility: Int) {
479
+ super.onWindowVisibilityChanged(visibility)
480
+ if (initialized) updateBackCallback()
481
+ }
482
+
483
+ override fun onDetachedFromWindow() {
484
+ dispose()
485
+ super.onDetachedFromWindow()
486
+ }
487
+
488
+ fun dispose() {
489
+ detached = true
490
+ cancelTouch()
491
+ stopAnimation()
492
+ backCallback.remove()
493
+ backRegistered = false
494
+ setProgress(if (stableOpen && drawerEnabled) 1f else 0f)
495
+ }
496
+ }
@@ -7,7 +7,7 @@ import com.facebook.react.uimanager.ViewManager
7
7
 
8
8
  class BottomTabsPackage : ReactPackage {
9
9
  override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
10
- return listOf(BottomTabsViewManager(reactContext))
10
+ return listOf(BottomTabsViewManager(reactContext), BottomTabsDrawerManager())
11
11
  }
12
12
 
13
13
  override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
@@ -5,6 +5,7 @@ import android.view.View
5
5
  import android.view.ViewGroup
6
6
  import com.facebook.react.bridge.ReactApplicationContext
7
7
  import com.facebook.react.bridge.ReadableArray
8
+ import com.facebook.react.bridge.ReadableMap
8
9
  import com.facebook.react.module.annotations.ReactModule
9
10
  import com.facebook.react.uimanager.ThemedReactContext
10
11
  import com.facebook.react.uimanager.UIManagerHelper
@@ -213,6 +214,8 @@ class BottomTabsViewManager(context: ReactApplicationContext) :
213
214
  override fun setSidebarAdaptable(view: BottomTabsView?, value: Boolean) {}
214
215
  override fun setScrollEdgeAppearance(view: BottomTabsView?, value: String?) {}
215
216
  override fun setMinimizeBehavior(view: BottomTabsView?, value: String?) {}
217
+ override fun setSideAccessoryIcon(view: BottomTabsView?, value: ReadableMap?) {}
218
+ override fun setSideAccessoryLabel(view: BottomTabsView?, value: String?) {}
216
219
 
217
220
  companion object {
218
221
  const val NAME = "RNBottomTabsView"