@onekeyfe/react-native-bottom-tabs 6.7.0-alpha.383 → 6.7.0-alpha.385
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/android/src/main/java/com/onekey/bottomtabs/BottomTabsDrawerManager.kt +55 -0
- package/android/src/main/java/com/onekey/bottomtabs/BottomTabsDrawerView.kt +496 -0
- package/android/src/main/java/com/onekey/bottomtabs/BottomTabsPackage.kt +1 -1
- package/docs/SPEC.md +121 -2
- package/ios/RCTBottomTabsContainerView.swift +2 -6
- package/ios/RCTBottomTabsDrawerComponentView.h +8 -0
- package/ios/RCTBottomTabsDrawerComponentView.mm +79 -0
- package/ios/RCTBottomTabsDrawerContainerView.swift +327 -0
- package/lib/module/BottomTabs.js +95 -65
- package/lib/module/BottomTabsDrawerNativeComponent.ts +19 -0
- package/lib/module/BottomTabsDrawerNativeComponent.web.js +13 -0
- package/lib/typescript/src/BottomTabs.d.ts +9 -1
- package/lib/typescript/src/BottomTabsDrawerNativeComponent.d.ts +15 -0
- package/lib/typescript/src/BottomTabsDrawerNativeComponent.web.d.ts +4 -0
- package/package.json +3 -2
- package/react-native.config.js +4 -1
- package/src/BottomTabs.tsx +124 -80
- package/src/BottomTabsDrawerNativeComponent.ts +19 -0
- package/src/BottomTabsDrawerNativeComponent.web.tsx +9 -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> {
|