@onekeyfe/react-native-pager-view 3.0.157-alpha.257 → 3.0.157-alpha.258
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -4
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +237 -43
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerScrollAwayInsets.kt +15 -11
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerView.kt +573 -0
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerViewManager.kt +130 -0
- package/android/src/main/java/com/reactnativepagerview/PagerViewViewPackage.kt +1 -1
- package/android/src/main/jni/CMakeLists.txt +19 -0
- package/android/src/main/jni/pagerview.h +11 -0
- package/android/src/main/res/values/native_scroll_coordinator_ids.xml +1 -0
- package/android/src/test/java/com/reactnativepagerview/NativeScrollerScrollAwayInsetsTest.kt +32 -52
- package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerComponentDescriptor.h +8 -0
- package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerShadowNode.cpp +15 -0
- package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerShadowNode.h +23 -0
- package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerState.h +33 -0
- package/docs/SPEC.md +267 -34
- package/ios/RNCCollapsiblePagerViewComponentView.mm +187 -1
- package/ios/RNCNativeScrollerComponentView.h +17 -0
- package/ios/RNCNativeScrollerComponentView.mm +479 -0
- package/ios/tests/RNCCollapsiblePagerPressCancellationTests.mm +346 -0
- package/ios/tests/RNCCollapsiblePagerPressCancellationWiringTests.mm +109 -0
- package/ios/tests/RNCNativeScrollerTests.mm +252 -0
- package/lib/module/CollapsiblePagerView.js +1 -1
- package/lib/module/NativeScroller.js +171 -11
- package/lib/module/NativeScrollerNativeComponent.ts +101 -0
- package/lib/typescript/src/CollapsiblePagerView.d.ts +2 -3
- package/lib/typescript/src/NativeScroller.d.ts +15 -7
- package/lib/typescript/src/NativeScrollerNativeComponent.d.ts +79 -0
- package/package.json +6 -3
- package/react-native-pager-view.podspec +2 -1
- package/react-native.config.js +15 -0
- package/src/CollapsiblePagerView.tsx +4 -4
- package/src/NativeScroller.tsx +291 -14
- package/src/NativeScrollerNativeComponent.ts +101 -0
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
package com.reactnativepagerview
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Color
|
|
5
|
+
import android.os.SystemClock
|
|
6
|
+
import android.view.MotionEvent
|
|
7
|
+
import android.view.View
|
|
8
|
+
import android.view.VelocityTracker
|
|
9
|
+
import android.view.ViewConfiguration
|
|
10
|
+
import android.view.ViewGroup
|
|
11
|
+
import android.view.inputmethod.InputMethodManager
|
|
12
|
+
import android.widget.FrameLayout
|
|
13
|
+
import androidx.core.view.ViewCompat
|
|
14
|
+
import androidx.core.widget.NestedScrollView
|
|
15
|
+
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|
16
|
+
import com.facebook.react.bridge.Arguments
|
|
17
|
+
import com.facebook.react.bridge.ReactContext
|
|
18
|
+
import com.facebook.react.bridge.WritableMap
|
|
19
|
+
import com.facebook.react.uimanager.StateWrapper
|
|
20
|
+
import com.facebook.react.uimanager.PixelUtil
|
|
21
|
+
import com.facebook.react.uimanager.UIManagerHelper
|
|
22
|
+
import com.facebook.react.uimanager.events.Event
|
|
23
|
+
import com.facebook.react.uimanager.events.NativeGestureUtil
|
|
24
|
+
import java.util.WeakHashMap
|
|
25
|
+
import kotlin.math.abs
|
|
26
|
+
import kotlin.math.max
|
|
27
|
+
import kotlin.math.roundToInt
|
|
28
|
+
|
|
29
|
+
/** Fabric owns the React child; this host owns only its native viewport and extent. */
|
|
30
|
+
class NativeScrollerView(context: Context) : FrameLayout(context) {
|
|
31
|
+
internal val viewport = NativeScrollerViewport(context, this)
|
|
32
|
+
private val refresh = NativeScrollerRefreshLayout(context)
|
|
33
|
+
private val contentOriginFrame = object : FrameLayout(context) {
|
|
34
|
+
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
|
|
35
|
+
setMeasuredDimension(MeasureSpec.getSize(widthSpec), MeasureSpec.getSize(heightSpec))
|
|
36
|
+
}
|
|
37
|
+
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) = Unit
|
|
38
|
+
}
|
|
39
|
+
private val contentFrame = object : FrameLayout(context) {
|
|
40
|
+
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
|
|
41
|
+
setMeasuredDimension(
|
|
42
|
+
max(MeasureSpec.getSize(widthSpec), contentWidthPx + insetLeft + insetRight),
|
|
43
|
+
contentHeightPx + contentTop + insetBottom,
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
48
|
+
// Only the native origin layer moves. Fabric owns its React child's layout/transform.
|
|
49
|
+
contentOriginFrame.measure(
|
|
50
|
+
MeasureSpec.makeMeasureSpec(contentWidthPx, MeasureSpec.EXACTLY),
|
|
51
|
+
MeasureSpec.makeMeasureSpec(contentHeightPx, MeasureSpec.EXACTLY),
|
|
52
|
+
)
|
|
53
|
+
contentOriginFrame.layout(insetLeft, contentTop, insetLeft + contentWidthPx, contentTop + contentHeightPx)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
private val pagerInsets = WeakHashMap<View, Int>()
|
|
57
|
+
private var reactChild: View? = null
|
|
58
|
+
private var fabricState: StateWrapper? = null
|
|
59
|
+
private var lastOriginX: Int? = null
|
|
60
|
+
private var lastOriginY: Int? = null
|
|
61
|
+
private var lastAncestorY: Int? = null
|
|
62
|
+
private var lastContentViewportHeight: Int? = null
|
|
63
|
+
private val consumedOffsetListener = Runnable { publishContentOrigin() }
|
|
64
|
+
private var lastScrollEvent = 0L
|
|
65
|
+
private var laidOut = false
|
|
66
|
+
private var requestedRefresh = false
|
|
67
|
+
private var refreshOffset = 0
|
|
68
|
+
private var appliedRefreshOffset: Int? = null
|
|
69
|
+
private var appliedRefreshSize = SwipeRefreshLayout.DEFAULT
|
|
70
|
+
private var layoutPosted = false
|
|
71
|
+
internal var contentWidthPx = 0
|
|
72
|
+
internal var contentHeightPx = 0
|
|
73
|
+
internal var insetTop = 0
|
|
74
|
+
internal var insetRight = 0
|
|
75
|
+
internal var insetBottom = 0
|
|
76
|
+
internal var insetLeft = 0
|
|
77
|
+
internal var scrollEventThrottle = 0.0
|
|
78
|
+
internal var keyboardDismissMode = "none"
|
|
79
|
+
internal var keyboardShouldPersistTaps = "never"
|
|
80
|
+
internal var refreshEnabled = false
|
|
81
|
+
set(value) {
|
|
82
|
+
field = value
|
|
83
|
+
refresh.isEnabled = value && viewport.scrollEnabled
|
|
84
|
+
}
|
|
85
|
+
internal val contentTop: Int
|
|
86
|
+
get() = insetTop + pagerInsets.values.sum()
|
|
87
|
+
|
|
88
|
+
init {
|
|
89
|
+
clipChildren = true
|
|
90
|
+
contentFrame.clipChildren = false
|
|
91
|
+
contentOriginFrame.clipChildren = false
|
|
92
|
+
contentFrame.addView(contentOriginFrame)
|
|
93
|
+
viewport.isFillViewport = false
|
|
94
|
+
viewport.isSmoothScrollingEnabled = false
|
|
95
|
+
viewport.clipToPadding = false
|
|
96
|
+
viewport.addView(contentFrame, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
|
|
97
|
+
refresh.addView(viewport, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
|
|
98
|
+
super.addView(refresh, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
|
|
99
|
+
refresh.isEnabled = false
|
|
100
|
+
refresh.setColorSchemeColors(Color.GRAY)
|
|
101
|
+
refresh.setOnChildScrollUpCallback { _, _ -> viewport.canScrollVertically(-1) }
|
|
102
|
+
refresh.setOnRefreshListener { emit("topRefresh", Arguments.createMap()) }
|
|
103
|
+
refresh.onTouchStream = { event -> viewport.recordTouchEvent(event) }
|
|
104
|
+
refresh.onTouchStreamFinished = { viewport.releaseTouchTracking() }
|
|
105
|
+
refresh.onGestureStart = { event -> viewport.beginDrag(event) }
|
|
106
|
+
refresh.onGestureEnd = { event -> viewport.endDrag(event) }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
internal fun setFabricState(wrapper: StateWrapper?) {
|
|
110
|
+
fabricState = wrapper
|
|
111
|
+
if (wrapper == null) {
|
|
112
|
+
lastOriginX = null
|
|
113
|
+
lastOriginY = null
|
|
114
|
+
lastAncestorY = null
|
|
115
|
+
lastContentViewportHeight = null
|
|
116
|
+
} else {
|
|
117
|
+
val state = wrapper.stateData
|
|
118
|
+
if (state == null || !state.hasKey("contentOriginX") || !state.hasKey("contentOriginY") ||
|
|
119
|
+
!state.hasKey("ancestorOffsetX") || !state.hasKey("ancestorOffsetY") ||
|
|
120
|
+
lastOriginX == null || lastOriginY == null || lastAncestorY == null ||
|
|
121
|
+
abs(state.getDouble("contentOriginX") - px(lastOriginX!!)) > 0.001 ||
|
|
122
|
+
abs(state.getDouble("contentOriginY") - px(lastOriginY!!)) > 0.001 ||
|
|
123
|
+
abs(state.getDouble("ancestorOffsetX")) > 0.001 ||
|
|
124
|
+
abs(state.getDouble("ancestorOffsetY") - px(lastAncestorY!!)) > 0.001) {
|
|
125
|
+
lastOriginX = null
|
|
126
|
+
lastOriginY = null
|
|
127
|
+
lastAncestorY = null
|
|
128
|
+
}
|
|
129
|
+
publishContentOrigin()
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
internal fun publishContentOrigin() {
|
|
134
|
+
val wrapper = fabricState ?: return
|
|
135
|
+
val ancestors = viewport.getTag(R.id.onekey_native_scroll_coordinator_consumed_offsets)
|
|
136
|
+
as? NativeScrollCoordinatorContributions
|
|
137
|
+
val x = insetLeft - viewport.scrollX
|
|
138
|
+
val y = contentTop - viewport.scrollY
|
|
139
|
+
val ancestorY = -(ancestors?.values?.sum() ?: 0)
|
|
140
|
+
if (lastOriginX == x && lastOriginY == y && lastAncestorY == ancestorY) return
|
|
141
|
+
lastOriginX = x
|
|
142
|
+
lastOriginY = y
|
|
143
|
+
lastAncestorY = ancestorY
|
|
144
|
+
wrapper.updateState(Arguments.createMap().apply {
|
|
145
|
+
putDouble("contentOriginX", px(x))
|
|
146
|
+
putDouble("contentOriginY", px(y))
|
|
147
|
+
putDouble("ancestorOffsetX", 0.0)
|
|
148
|
+
putDouble("ancestorOffsetY", px(ancestorY))
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
internal fun setContentSize(width: Int? = null, height: Int? = null) {
|
|
153
|
+
val nextWidth = width?.coerceAtLeast(0) ?: contentWidthPx
|
|
154
|
+
val nextHeight = height?.coerceAtLeast(0) ?: contentHeightPx
|
|
155
|
+
if (nextWidth == contentWidthPx && nextHeight == contentHeightPx) return
|
|
156
|
+
contentWidthPx = nextWidth
|
|
157
|
+
contentHeightPx = nextHeight
|
|
158
|
+
scheduleLayout()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
internal fun setPagerInset(owner: View, inset: Int?) {
|
|
162
|
+
val next = inset?.coerceAtLeast(0)
|
|
163
|
+
if (pagerInsets[owner] == next) return
|
|
164
|
+
if (next == null) pagerInsets.remove(owner) else pagerInsets[owner] = next
|
|
165
|
+
scheduleLayout()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
internal fun addReactChild(child: View, index: Int) {
|
|
169
|
+
require(index == 0 && reactChild == null) { "NativeScroller requires one React content view" }
|
|
170
|
+
viewport.stopMotion()
|
|
171
|
+
reactChild = child
|
|
172
|
+
contentOriginFrame.addView(child)
|
|
173
|
+
scheduleLayout()
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
internal fun removeReactChild() {
|
|
177
|
+
viewport.stopMotion()
|
|
178
|
+
reactChild?.let(contentOriginFrame::removeView)
|
|
179
|
+
reactChild = null
|
|
180
|
+
contentWidthPx = 0
|
|
181
|
+
contentHeightPx = 0
|
|
182
|
+
scheduleLayout()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
internal fun reactChildCount() = if (reactChild == null) 0 else 1
|
|
186
|
+
internal fun reactChildAt(index: Int): View {
|
|
187
|
+
require(index == 0)
|
|
188
|
+
return requireNotNull(reactChild)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
internal fun scheduleLayout() {
|
|
192
|
+
if (layoutPosted) return
|
|
193
|
+
layoutPosted = true
|
|
194
|
+
post {
|
|
195
|
+
layoutPosted = false
|
|
196
|
+
if (isAttachedToWindow && width > 0 && height > 0) layoutContents()
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
201
|
+
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec))
|
|
202
|
+
refresh.measure(
|
|
203
|
+
MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY),
|
|
204
|
+
MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY),
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
209
|
+
layoutContents()
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private fun layoutContents() {
|
|
213
|
+
val x = viewport.scrollX
|
|
214
|
+
val y = viewport.scrollY
|
|
215
|
+
contentFrame.forceLayout()
|
|
216
|
+
viewport.forceLayout()
|
|
217
|
+
refresh.forceLayout()
|
|
218
|
+
refresh.measure(
|
|
219
|
+
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
220
|
+
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY),
|
|
221
|
+
)
|
|
222
|
+
refresh.layout(0, 0, width, height)
|
|
223
|
+
// Reapply the current anchor after content shrink; NestedScrollView clamps it.
|
|
224
|
+
viewport.scrollTo(x, y)
|
|
225
|
+
laidOut = true
|
|
226
|
+
publishContentViewport()
|
|
227
|
+
publishContentOrigin()
|
|
228
|
+
applyRefreshOffset()
|
|
229
|
+
if (refresh.isRefreshing != requestedRefresh) refresh.isRefreshing = requestedRefresh
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
internal fun setRefreshing(value: Boolean) {
|
|
233
|
+
requestedRefresh = value
|
|
234
|
+
if (laidOut) refresh.isRefreshing = value
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
internal fun setRefreshOffset(value: Int) {
|
|
238
|
+
refreshOffset = value
|
|
239
|
+
applyRefreshOffset()
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
internal fun setRefreshSize(value: String?) {
|
|
243
|
+
val size = if (value == "large") SwipeRefreshLayout.LARGE else SwipeRefreshLayout.DEFAULT
|
|
244
|
+
if (appliedRefreshSize == size) return
|
|
245
|
+
appliedRefreshSize = size
|
|
246
|
+
refresh.setSize(size)
|
|
247
|
+
appliedRefreshOffset = null
|
|
248
|
+
applyRefreshOffset()
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private fun applyRefreshOffset() {
|
|
252
|
+
if (!laidOut || appliedRefreshOffset == refreshOffset) return
|
|
253
|
+
val diameter = refresh.progressCircleDiameter
|
|
254
|
+
val wasRefreshing = refresh.isRefreshing
|
|
255
|
+
refresh.setProgressViewOffset(
|
|
256
|
+
false, refreshOffset - diameter, refreshOffset + dip(64.0) - diameter,
|
|
257
|
+
)
|
|
258
|
+
appliedRefreshOffset = refreshOffset
|
|
259
|
+
// This component reserves content insets, not refresh-control insets.
|
|
260
|
+
refresh.setTag(R.id.onekey_native_scroll_coordinator_refresh_inset, 0)
|
|
261
|
+
if (wasRefreshing) refresh.isRefreshing = true
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
internal fun setRefreshColors(colors: IntArray) {
|
|
265
|
+
refresh.setColorSchemeColors(*(if (colors.isEmpty()) intArrayOf(Color.GRAY) else colors))
|
|
266
|
+
}
|
|
267
|
+
internal fun setRefreshBackground(color: Int) = refresh.setProgressBackgroundColorSchemeColor(color)
|
|
268
|
+
internal fun setScrollEnabled(value: Boolean) {
|
|
269
|
+
viewport.scrollEnabled = value
|
|
270
|
+
refresh.isEnabled = refreshEnabled && value
|
|
271
|
+
if (!value) viewport.stopMotion()
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
internal fun scrollToPosition(x: Int, y: Int, animated: Boolean) {
|
|
275
|
+
viewport.stopMotion()
|
|
276
|
+
if (animated) viewport.scrollToAnimated(y)
|
|
277
|
+
else viewport.scrollTo(0, y.coerceAtLeast(0))
|
|
278
|
+
emitScroll(force = true)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
internal fun scrollToEnd(animated: Boolean) = scrollToPosition(
|
|
282
|
+
0, (contentHeightPx + contentTop + insetBottom - viewport.height).coerceAtLeast(0), animated,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
internal fun emitScroll(force: Boolean = false) {
|
|
286
|
+
if (!isAttachedToWindow) return
|
|
287
|
+
val now = SystemClock.uptimeMillis()
|
|
288
|
+
if (!force && scrollEventThrottle > 16 && now - lastScrollEvent < scrollEventThrottle) return
|
|
289
|
+
lastScrollEvent = now
|
|
290
|
+
emit("topScroll", scrollPayload())
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
internal fun emitLifecycle(name: String, velocity: Double = 0.0) {
|
|
294
|
+
if (!isAttachedToWindow) return
|
|
295
|
+
emit(name, scrollPayload(velocity))
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private fun scrollPayload(velocity: Double = 0.0): WritableMap = Arguments.createMap().apply {
|
|
299
|
+
fun pair(first: String, firstValue: Double, second: String, secondValue: Double) =
|
|
300
|
+
Arguments.createMap().apply { putDouble(first, firstValue); putDouble(second, secondValue) }
|
|
301
|
+
putMap("contentOffset", pair("x", px(viewport.scrollX), "y", px(viewport.scrollY)))
|
|
302
|
+
putMap("contentSize", pair("width", px(contentWidthPx), "height", px(contentHeightPx)))
|
|
303
|
+
putMap("layoutMeasurement", pair("width", px(width), "height", px(height)))
|
|
304
|
+
putMap("velocity", pair("x", 0.0, "y", velocity))
|
|
305
|
+
putMap("targetContentOffset", pair("x", px(viewport.scrollX), "y", px(viewport.scrollY)))
|
|
306
|
+
putMap("contentInset", Arguments.createMap().apply {
|
|
307
|
+
putDouble("top", px(insetTop)); putDouble("right", px(insetRight))
|
|
308
|
+
putDouble("bottom", px(insetBottom)); putDouble("left", px(insetLeft))
|
|
309
|
+
})
|
|
310
|
+
putDouble("zoomScale", 1.0)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private fun publishContentViewport() {
|
|
314
|
+
val availableHeight = (viewport.height - contentTop - insetBottom).coerceAtLeast(0)
|
|
315
|
+
if (lastContentViewportHeight == availableHeight) return
|
|
316
|
+
if (emit("topContentViewportChange", Arguments.createMap().apply {
|
|
317
|
+
putDouble("height", px(availableHeight))
|
|
318
|
+
})) {
|
|
319
|
+
lastContentViewportHeight = availableHeight
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private fun emit(name: String, payload: WritableMap): Boolean {
|
|
324
|
+
if (!isAttachedToWindow || id == View.NO_ID) return false
|
|
325
|
+
val reactContext = context as? ReactContext ?: return false
|
|
326
|
+
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, id) ?: return false
|
|
327
|
+
dispatcher.dispatchEvent(
|
|
328
|
+
NativeScrollerEvent(UIManagerHelper.getSurfaceId(this), id, name, payload),
|
|
329
|
+
)
|
|
330
|
+
return true
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
internal fun dismissKeyboard() {
|
|
334
|
+
(context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager)
|
|
335
|
+
?.hideSoftInputFromWindow(windowToken, 0)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
override fun onAttachedToWindow() {
|
|
339
|
+
super.onAttachedToWindow()
|
|
340
|
+
viewport.setTag(R.id.onekey_native_scroll_coordinator_offset_listener, consumedOffsetListener)
|
|
341
|
+
publishContentOrigin()
|
|
342
|
+
scheduleLayout()
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
override fun onDetachedFromWindow() {
|
|
346
|
+
lastContentViewportHeight = null
|
|
347
|
+
viewport.stopMotion()
|
|
348
|
+
viewport.setTag(R.id.onekey_native_scroll_coordinator_offset_listener, null)
|
|
349
|
+
super.onDetachedFromWindow()
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
internal fun dip(value: Double) = PixelUtil.toPixelFromDIP(value).roundToInt()
|
|
353
|
+
private fun px(value: Int) = PixelUtil.toDIPFromPixel(value.toFloat()).toDouble()
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
internal class NativeScrollerViewport(context: Context, internal val host: NativeScrollerView) : NestedScrollView(context) {
|
|
357
|
+
var scrollEnabled = true
|
|
358
|
+
private var dragging = false
|
|
359
|
+
private var momentum = false
|
|
360
|
+
private var stopping = false
|
|
361
|
+
private var startingProgrammaticScroll = false
|
|
362
|
+
private var downY = 0f
|
|
363
|
+
private var velocityTracker: VelocityTracker? = null
|
|
364
|
+
private var activePointerId = MotionEvent.INVALID_POINTER_ID
|
|
365
|
+
private val maximumVelocity = ViewConfiguration.get(context).scaledMaximumFlingVelocity
|
|
366
|
+
private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
|
|
367
|
+
|
|
368
|
+
init {
|
|
369
|
+
isNestedScrollingEnabled = true
|
|
370
|
+
isVerticalScrollBarEnabled = true
|
|
371
|
+
overScrollMode = OVER_SCROLL_NEVER
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
|
|
375
|
+
if (!scrollEnabled) return false
|
|
376
|
+
if (event.actionMasked == MotionEvent.ACTION_DOWN) downY = event.y
|
|
377
|
+
val intercepted = super.onInterceptTouchEvent(event)
|
|
378
|
+
if (intercepted && event.actionMasked == MotionEvent.ACTION_MOVE) beginDrag(event)
|
|
379
|
+
return intercepted
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
383
|
+
if (!scrollEnabled) return false
|
|
384
|
+
when (event.actionMasked) {
|
|
385
|
+
MotionEvent.ACTION_DOWN -> downY = event.y
|
|
386
|
+
MotionEvent.ACTION_MOVE -> {
|
|
387
|
+
val pointerIndex = event.findPointerIndex(activePointerId)
|
|
388
|
+
if (pointerIndex >= 0 && abs(event.getY(pointerIndex) - downY) > touchSlop) beginDrag(event)
|
|
389
|
+
}
|
|
390
|
+
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> endDrag(event)
|
|
391
|
+
}
|
|
392
|
+
return super.onTouchEvent(event)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Record at the refresh boundary so interception does not truncate the pointer history.
|
|
396
|
+
internal fun recordTouchEvent(event: MotionEvent) {
|
|
397
|
+
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
|
398
|
+
stopMotion()
|
|
399
|
+
velocityTracker = VelocityTracker.obtain()
|
|
400
|
+
activePointerId = event.getPointerId(0)
|
|
401
|
+
downY = event.y
|
|
402
|
+
} else if (event.actionMasked == MotionEvent.ACTION_POINTER_DOWN) {
|
|
403
|
+
activePointerId = event.getPointerId(event.actionIndex)
|
|
404
|
+
downY = event.getY(event.actionIndex)
|
|
405
|
+
} else if (event.actionMasked == MotionEvent.ACTION_POINTER_UP &&
|
|
406
|
+
event.getPointerId(event.actionIndex) == activePointerId) {
|
|
407
|
+
val replacement = if (event.actionIndex == 0) 1 else 0
|
|
408
|
+
activePointerId = if (replacement < event.pointerCount) event.getPointerId(replacement)
|
|
409
|
+
else MotionEvent.INVALID_POINTER_ID
|
|
410
|
+
if (replacement < event.pointerCount) downY = event.getY(replacement)
|
|
411
|
+
velocityTracker?.clear()
|
|
412
|
+
}
|
|
413
|
+
velocityTracker?.addMovement(event)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
internal fun releaseTouchTracking() {
|
|
417
|
+
velocityTracker?.recycle()
|
|
418
|
+
velocityTracker = null
|
|
419
|
+
activePointerId = MotionEvent.INVALID_POINTER_ID
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
internal fun beginDrag(event: MotionEvent) {
|
|
423
|
+
if (dragging) return
|
|
424
|
+
dragging = true
|
|
425
|
+
if (host.keyboardDismissMode != "none") host.dismissKeyboard()
|
|
426
|
+
NativeGestureUtil.notifyNativeGestureStarted(host, event)
|
|
427
|
+
host.emitLifecycle("topScrollBeginDrag")
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
internal fun endDrag(event: MotionEvent) {
|
|
431
|
+
if (!dragging) return
|
|
432
|
+
dragging = false
|
|
433
|
+
host.emitScroll(force = true)
|
|
434
|
+
val tracker = velocityTracker
|
|
435
|
+
val velocity = if (event.actionMasked == MotionEvent.ACTION_UP && tracker != null &&
|
|
436
|
+
activePointerId != MotionEvent.INVALID_POINTER_ID) {
|
|
437
|
+
tracker.computeCurrentVelocity(1000, maximumVelocity.toFloat())
|
|
438
|
+
tracker.getYVelocity(activePointerId) / 1000.0 / resources.displayMetrics.density
|
|
439
|
+
} else 0.0
|
|
440
|
+
host.emitLifecycle("topScrollEndDrag", velocity)
|
|
441
|
+
NativeGestureUtil.notifyNativeGestureEnded(host, event)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
override fun fling(velocityY: Int) {
|
|
445
|
+
finishMomentum()
|
|
446
|
+
momentum = true
|
|
447
|
+
host.emitLifecycle("topMomentumScrollBegin", velocityY / 1000.0 / resources.displayMetrics.density)
|
|
448
|
+
// AndroidX uses one unbounded trajectory with pre/content/post nested consumption.
|
|
449
|
+
super.fling(velocityY)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
internal fun scrollToAnimated(y: Int) {
|
|
453
|
+
val child = getChildAt(0) ?: return
|
|
454
|
+
val margins = child.layoutParams as FrameLayout.LayoutParams
|
|
455
|
+
val range = (child.height + margins.topMargin + margins.bottomMargin -
|
|
456
|
+
height + paddingTop + paddingBottom).coerceAtLeast(0)
|
|
457
|
+
val target = y.coerceIn(0, range)
|
|
458
|
+
if (target == scrollY) return
|
|
459
|
+
momentum = true
|
|
460
|
+
host.emitLifecycle("topMomentumScrollBegin")
|
|
461
|
+
startingProgrammaticScroll = true
|
|
462
|
+
try {
|
|
463
|
+
// AndroidX stops nested scrolling while starting its programmatic animation.
|
|
464
|
+
super.smoothScrollTo(0, target)
|
|
465
|
+
} finally {
|
|
466
|
+
startingProgrammaticScroll = false
|
|
467
|
+
}
|
|
468
|
+
// Commands within AndroidX's animation gap execute immediately instead of animating.
|
|
469
|
+
if (scrollY == target) finishMomentum()
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
override fun stopNestedScroll(type: Int) {
|
|
473
|
+
super.stopNestedScroll(type)
|
|
474
|
+
if (type == ViewCompat.TYPE_NON_TOUCH && !stopping && !startingProgrammaticScroll) finishMomentum()
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
internal fun stopMotion() {
|
|
478
|
+
if (dragging) {
|
|
479
|
+
val now = SystemClock.uptimeMillis()
|
|
480
|
+
val cancel = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0f, 0f, 0)
|
|
481
|
+
try { endDrag(cancel) } finally { cancel.recycle() }
|
|
482
|
+
}
|
|
483
|
+
releaseTouchTracking()
|
|
484
|
+
stopping = true
|
|
485
|
+
try {
|
|
486
|
+
// Replace the same AndroidX trajectory with zero velocity without touching its smooth-scroll clock.
|
|
487
|
+
super.fling(0)
|
|
488
|
+
super.stopNestedScroll(ViewCompat.TYPE_NON_TOUCH)
|
|
489
|
+
} finally {
|
|
490
|
+
stopping = false
|
|
491
|
+
}
|
|
492
|
+
finishMomentum()
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
private fun finishMomentum() {
|
|
496
|
+
if (!momentum) return
|
|
497
|
+
momentum = false
|
|
498
|
+
host.emitScroll(force = true)
|
|
499
|
+
host.emitLifecycle("topMomentumScrollEnd")
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
override fun onScrollChanged(x: Int, y: Int, oldX: Int, oldY: Int) {
|
|
503
|
+
super.onScrollChanged(x, y, oldX, oldY)
|
|
504
|
+
host.publishContentOrigin()
|
|
505
|
+
host.emitScroll()
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
internal fun flashIndicators() = awakenScrollBars()
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private class NativeScrollerRefreshLayout(context: Context) : SwipeRefreshLayout(context) {
|
|
512
|
+
var onTouchStream: ((MotionEvent) -> Unit)? = null
|
|
513
|
+
var onTouchStreamFinished: (() -> Unit)? = null
|
|
514
|
+
var onGestureStart: ((MotionEvent) -> Unit)? = null
|
|
515
|
+
var onGestureEnd: ((MotionEvent) -> Unit)? = null
|
|
516
|
+
private var downX = 0f
|
|
517
|
+
private var downY = 0f
|
|
518
|
+
private var horizontal = false
|
|
519
|
+
private var intercepted = false
|
|
520
|
+
private val slop = ViewConfiguration.get(context).scaledTouchSlop
|
|
521
|
+
|
|
522
|
+
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
|
523
|
+
onTouchStream?.invoke(event)
|
|
524
|
+
try {
|
|
525
|
+
return super.dispatchTouchEvent(event)
|
|
526
|
+
} finally {
|
|
527
|
+
if (event.actionMasked == MotionEvent.ACTION_UP || event.actionMasked == MotionEvent.ACTION_CANCEL) {
|
|
528
|
+
onTouchStreamFinished?.invoke()
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
|
|
534
|
+
super.requestDisallowInterceptTouchEvent(disallowIntercept)
|
|
535
|
+
parent?.requestDisallowInterceptTouchEvent(disallowIntercept)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
|
|
539
|
+
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
|
540
|
+
downX = event.x
|
|
541
|
+
downY = event.y
|
|
542
|
+
horizontal = false
|
|
543
|
+
intercepted = false
|
|
544
|
+
} else if (event.actionMasked == MotionEvent.ACTION_MOVE && abs(event.x - downX) > slop &&
|
|
545
|
+
abs(event.x - downX) > abs(event.y - downY)) {
|
|
546
|
+
horizontal = true
|
|
547
|
+
}
|
|
548
|
+
if (horizontal) return false
|
|
549
|
+
val result = super.onInterceptTouchEvent(event)
|
|
550
|
+
if (result && !intercepted) {
|
|
551
|
+
intercepted = true
|
|
552
|
+
onGestureStart?.invoke(event)
|
|
553
|
+
}
|
|
554
|
+
return result
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
558
|
+
val result = super.onTouchEvent(event)
|
|
559
|
+
if (event.actionMasked == MotionEvent.ACTION_UP || event.actionMasked == MotionEvent.ACTION_CANCEL) {
|
|
560
|
+
if (intercepted) onGestureEnd?.invoke(event)
|
|
561
|
+
intercepted = false
|
|
562
|
+
}
|
|
563
|
+
return result
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private class NativeScrollerEvent(
|
|
568
|
+
surface: Int, tag: Int, private val name: String, private val payload: WritableMap,
|
|
569
|
+
) : Event<NativeScrollerEvent>(surface, tag) {
|
|
570
|
+
override fun getEventName() = name
|
|
571
|
+
override fun canCoalesce() = name == "topScroll"
|
|
572
|
+
override fun getEventData() = payload
|
|
573
|
+
}
|