Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) :
private val onScrollDispatchHelper = OnScrollDispatchHelper()
private val scroller: OverScroller? = getOverScrollerFromParent()
private val velocityHelper = VelocityHelper()
private val stableTouchCoordinatesHelper = StableTouchCoordinatesHelper(this)
private val tempRect = Rect()
private val defaultFlingAnimator: ValueAnimator = ObjectAnimator.ofInt(this, "scrollX", 0, 0)

Expand Down Expand Up @@ -659,7 +660,9 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) :
if (!PointerEvents.canChildrenBeTouchTarget(pointerEvents)) return true

try {
if (super.onInterceptTouchEvent(ev)) {
if (
stableTouchCoordinatesHelper.withStableCoordinates(ev) { super.onInterceptTouchEvent(it) }
) {
handleInterceptedTouchEvent(ev)
return true
}
Expand Down Expand Up @@ -738,7 +741,7 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) :
// We do not accept the touch event if this view is not supposed to receive it.
if (!PointerEvents.canBeTouchTarget(pointerEvents)) return false

velocityHelper.calculateVelocity(ev)
stableTouchCoordinatesHelper.withStableCoordinates(ev) { velocityHelper.calculateVelocity(it) }
val action = ev.actionMasked
if ((action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) && dragging) {
ReactScrollViewHelper.updateFabricScrollState(this)
Expand All @@ -760,7 +763,7 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) :
}

return try {
super.onTouchEvent(ev)
stableTouchCoordinatesHelper.withStableCoordinates(ev) { super.onTouchEvent(it) }
} catch (e: IllegalArgumentException) {
// Log and ignore the error. This seems to be a bug in the android SDK and
// this is the commonly accepted workaround.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) :
private val onScrollDispatchHelper = OnScrollDispatchHelper()
private val scroller: OverScroller? = getOverScrollerFromParent()
private val velocityHelper = VelocityHelper()
private val stableTouchCoordinatesHelper = StableTouchCoordinatesHelper(this)
private val tempRect = Rect()
private val defaultFlingAnimator: ValueAnimator = ObjectAnimator.ofInt(this, "scrollY", 0, 0)

Expand Down Expand Up @@ -562,7 +563,9 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) :
if (!PointerEvents.canChildrenBeTouchTarget(pointerEvents)) return true

try {
if (super.onInterceptTouchEvent(ev)) {
if (
stableTouchCoordinatesHelper.withStableCoordinates(ev) { super.onInterceptTouchEvent(it) }
) {
handleInterceptedTouchEvent(ev)
return true
}
Expand All @@ -588,7 +591,7 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) :
if (!scrollEnabled) return false
if (!PointerEvents.canBeTouchTarget(pointerEvents)) return false

velocityHelper.calculateVelocity(ev)
stableTouchCoordinatesHelper.withStableCoordinates(ev) { velocityHelper.calculateVelocity(it) }
val action = ev.actionMasked
if ((action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) && dragging) {
ReactScrollViewHelper.updateFabricScrollState(this)
Expand All @@ -607,7 +610,7 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) :
}

return try {
super.onTouchEvent(ev)
stableTouchCoordinatesHelper.withStableCoordinates(ev) { super.onTouchEvent(it) }
} catch (e: IllegalArgumentException) {
FLog.w(ReactConstants.TAG, "Error handling touch event.", e)
false
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

package com.facebook.react.views.scroll

import android.view.MotionEvent
import android.view.View

internal class StableTouchCoordinatesHelper(private val view: View) {
private val location = IntArray(2)
private var downLocationX = 0
private var downLocationY = 0

fun <T> withStableCoordinates(ev: MotionEvent, handler: (MotionEvent) -> T): T {
getLayoutLocationOnScreen(location)
if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
downLocationX = location[0]
downLocationY = location[1]
}
val screenDx = location[0] - downLocationX
val screenDy = location[1] - downLocationY
if (screenDx == 0 && screenDy == 0) {
return handler(ev)
}
val scaleX = getCumulativeScaleX()
val scaleY = getCumulativeScaleY()
if (scaleX == 0f || scaleY == 0f) {
return handler(ev)
}
val dx = screenDx / scaleX
val dy = screenDy / scaleY
ev.offsetLocation(dx, dy)
try {
return handler(ev)
} finally {
ev.offsetLocation(-dx, -dy)
}
}

private fun getCumulativeScaleX(): Float {
var scale = view.scaleX
var ancestor = view.parent
while (ancestor is View) {
scale *= ancestor.scaleX
ancestor = ancestor.parent
}
return scale
}

private fun getCumulativeScaleY(): Float {
var scale = view.scaleY
var ancestor = view.parent
while (ancestor is View) {
scale *= ancestor.scaleY
ancestor = ancestor.parent
}
return scale
}

private fun getLayoutLocationOnScreen(outLocation: IntArray) {
view.getLocationOnScreen(outLocation)
var ancestor = view.parent
while (ancestor is View) {
outLocation[0] += ancestor.scrollX
outLocation[1] += ancestor.scrollY
ancestor = ancestor.parent
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

package com.facebook.react.views.scroll

import android.app.Activity
import android.view.MotionEvent
import android.view.View
import android.widget.FrameLayout
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
class StableTouchCoordinatesHelperTest {
private lateinit var parent: FrameLayout
private lateinit var view: View
private lateinit var helper: StableTouchCoordinatesHelper

@Before
fun setUp() {
val activity = Robolectric.buildActivity(Activity::class.java).setup().get()
parent = FrameLayout(activity)
view = View(activity)
parent.addView(view)
activity.setContentView(parent)
parent.layout(0, 0, 1000, 2000)
view.layout(0, 500, 1000, 1500)
helper = StableTouchCoordinatesHelper(view)
}

@Test
fun passesEventUnchangedWhenViewDidNotMove() {
dispatch(MotionEvent.ACTION_DOWN, 100f)

assertThat(dispatch(MotionEvent.ACTION_MOVE, 80f)).isEqualTo(80f)
}

@Test
fun compensatesLayoutMovementSinceDown() {
dispatch(MotionEvent.ACTION_DOWN, 100f)
view.offsetTopAndBottom(-50)

assertThat(dispatch(MotionEvent.ACTION_MOVE, 130f)).isEqualTo(80f)
}

@Test
fun compensatesTranslationSinceDown() {
dispatch(MotionEvent.ACTION_DOWN, 100f)
view.translationY = 40f

assertThat(dispatch(MotionEvent.ACTION_MOVE, 40f)).isEqualTo(80f)
}

@Test
fun compensatesMovementOfFlippedView() {
view.scaleY = -1f
dispatch(MotionEvent.ACTION_DOWN, 100f)
view.offsetTopAndBottom(-50)

assertThat(dispatch(MotionEvent.ACTION_MOVE, 70f)).isEqualTo(120f)
}

@Test
fun compensatesMovementOfScaledAncestor() {
parent.scaleY = 2f
dispatch(MotionEvent.ACTION_DOWN, 100f)
view.offsetTopAndBottom(-50)

assertThat(dispatch(MotionEvent.ACTION_MOVE, 150f)).isEqualTo(100f)
}

@Test
fun ignoresAncestorScroll() {
dispatch(MotionEvent.ACTION_DOWN, 100f)
parent.scrollTo(0, 50)

assertThat(dispatch(MotionEvent.ACTION_MOVE, 130f)).isEqualTo(130f)
}

@Test
fun resetsReferenceOnNextDown() {
dispatch(MotionEvent.ACTION_DOWN, 100f)
view.offsetTopAndBottom(-50)
dispatch(MotionEvent.ACTION_UP, 100f)

dispatch(MotionEvent.ACTION_DOWN, 100f)

assertThat(dispatch(MotionEvent.ACTION_MOVE, 80f)).isEqualTo(80f)
}

@Test
fun restoresEventCoordinatesAfterHandler() {
dispatch(MotionEvent.ACTION_DOWN, 100f)
view.offsetTopAndBottom(-50)
val event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 10f, 130f, 0)

helper.withStableCoordinates(event) {}

assertThat(event.x).isEqualTo(10f)
assertThat(event.y).isEqualTo(130f)
event.recycle()
}

private fun dispatch(action: Int, y: Float): Float {
val event = MotionEvent.obtain(0, 0, action, 10f, y, 0)
val seenY = helper.withStableCoordinates(event) { it.y }
event.recycle()
return seenY
}
}
Loading
Loading