@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.
Files changed (33) hide show
  1. package/README.md +51 -4
  2. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +237 -43
  3. package/android/src/main/java/com/reactnativepagerview/NativeScrollerScrollAwayInsets.kt +15 -11
  4. package/android/src/main/java/com/reactnativepagerview/NativeScrollerView.kt +573 -0
  5. package/android/src/main/java/com/reactnativepagerview/NativeScrollerViewManager.kt +130 -0
  6. package/android/src/main/java/com/reactnativepagerview/PagerViewViewPackage.kt +1 -1
  7. package/android/src/main/jni/CMakeLists.txt +19 -0
  8. package/android/src/main/jni/pagerview.h +11 -0
  9. package/android/src/main/res/values/native_scroll_coordinator_ids.xml +1 -0
  10. package/android/src/test/java/com/reactnativepagerview/NativeScrollerScrollAwayInsetsTest.kt +32 -52
  11. package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerComponentDescriptor.h +8 -0
  12. package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerShadowNode.cpp +15 -0
  13. package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerShadowNode.h +23 -0
  14. package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerState.h +33 -0
  15. package/docs/SPEC.md +267 -34
  16. package/ios/RNCCollapsiblePagerViewComponentView.mm +187 -1
  17. package/ios/RNCNativeScrollerComponentView.h +17 -0
  18. package/ios/RNCNativeScrollerComponentView.mm +479 -0
  19. package/ios/tests/RNCCollapsiblePagerPressCancellationTests.mm +346 -0
  20. package/ios/tests/RNCCollapsiblePagerPressCancellationWiringTests.mm +109 -0
  21. package/ios/tests/RNCNativeScrollerTests.mm +252 -0
  22. package/lib/module/CollapsiblePagerView.js +1 -1
  23. package/lib/module/NativeScroller.js +171 -11
  24. package/lib/module/NativeScrollerNativeComponent.ts +101 -0
  25. package/lib/typescript/src/CollapsiblePagerView.d.ts +2 -3
  26. package/lib/typescript/src/NativeScroller.d.ts +15 -7
  27. package/lib/typescript/src/NativeScrollerNativeComponent.d.ts +79 -0
  28. package/package.json +6 -3
  29. package/react-native-pager-view.podspec +2 -1
  30. package/react-native.config.js +15 -0
  31. package/src/CollapsiblePagerView.tsx +4 -4
  32. package/src/NativeScroller.tsx +291 -14
  33. 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
+ }