attempt to fix scroll / pinch / gesture issues

This commit is contained in:
Fabian Freund
2026-05-07 04:09:00 +02:00
parent 2520e34abf
commit fa204f7d1a
2 changed files with 138 additions and 34 deletions
@@ -569,19 +569,11 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
} }
browserHandlingScrollFeature = BrowserHandlingScrollFeature(viewportEvents).also { browserHandlingScrollFeature = BrowserHandlingScrollFeature(viewportEvents).also {
if (GlobalComponents.browserHandlingScrollEnabled) { it.start(GlobalComponents.browserHandlingScrollEnabled)
it.start()
}
} }
GlobalComponents.onBrowserHandlingScrollEnabledChanged = { enabled -> GlobalComponents.onBrowserHandlingScrollEnabledChanged = { enabled ->
browserHandlingScrollFeature?.let { browserHandlingScrollFeature?.setScrollDetectionEnabled(enabled)
if (enabled) {
it.start()
} else {
it.stop()
}
}
} }
} }
@@ -15,31 +15,54 @@ import eu.weblibre.flutter_mozilla_components.pigeons.GeckoViewportEvents
import org.mozilla.gecko.util.ThreadUtils.runOnUiThread import org.mozilla.gecko.util.ThreadUtils.runOnUiThread
/** /**
* Feature that reports whether GeckoView is currently handling scroll input. * Feature that filters browser touch input and reports whether GeckoView is handling scroll input.
* *
* Checks InputResultDetail while active and only emits on state changes. * Checks InputResultDetail while scroll detection is enabled and only emits on state changes.
*/ */
class BrowserHandlingScrollFeature( class BrowserHandlingScrollFeature(
private val flutterEvents: GeckoViewportEvents, private val flutterEvents: GeckoViewportEvents,
) { ) {
private var running = false private var running = false
private var scrollDetectionEnabled = false
private var touchSessionActive = false private var touchSessionActive = false
private var lastValue: Boolean? = null private var lastValue: Boolean? = null
private var touchListener: View.OnTouchListener? = null private var touchListener: View.OnTouchListener? = null
private var touchTargetView: View? = null private var touchTargetView: View? = null
private var multiTouchSequenceActive = false
private var suppressUntilAllPointersUp = false
private var syntheticCancelDispatched = false
fun start(scrollDetectionEnabled: Boolean) {
if (running) {
setScrollDetectionEnabled(scrollDetectionEnabled)
return
}
fun start() {
if (running) return
running = true running = true
this.scrollDetectionEnabled = scrollDetectionEnabled
touchSessionActive = false touchSessionActive = false
lastValue = null lastValue = null
resetPinchGuard()
attachTouchListenerIfPossible() attachTouchListenerIfPossible()
} }
fun setScrollDetectionEnabled(enabled: Boolean) {
if (scrollDetectionEnabled == enabled) return
scrollDetectionEnabled = enabled
if (!enabled) {
touchSessionActive = false
emitIfChanged(false)
}
}
fun stop() { fun stop() {
if (!running) return if (!running) return
running = false running = false
scrollDetectionEnabled = false
touchSessionActive = false touchSessionActive = false
resetPinchGuard()
emitIfChanged(false)
detachTouchListenerIfPossible() detachTouchListenerIfPossible()
lastValue = null lastValue = null
} }
@@ -55,32 +78,117 @@ class BrowserHandlingScrollFeature(
touchListener = View.OnTouchListener { _, event -> touchListener = View.OnTouchListener { _, event ->
if (!running) return@OnTouchListener false if (!running) return@OnTouchListener false
when (event.actionMasked) { val consumeEvent = handlePinchGesture(targetView, event)
MotionEvent.ACTION_DOWN -> { updateScrollDetection(event)
touchSessionActive = true
emitIfChanged()
}
MotionEvent.ACTION_MOVE -> { consumeEvent
if (touchSessionActive) {
emitIfChanged()
}
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL -> {
touchSessionActive = false
}
}
// Never consume touch; GeckoView must keep handling it.
false
} }
targetView.setOnTouchListener(touchListener) targetView.setOnTouchListener(touchListener)
touchTargetView = targetView touchTargetView = targetView
} }
private fun updateScrollDetection(event: MotionEvent) {
if (!scrollDetectionEnabled) return
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
touchSessionActive = true
emitCurrentStateIfChanged()
}
MotionEvent.ACTION_POINTER_DOWN -> {
touchSessionActive = false
emitIfChanged(false)
}
MotionEvent.ACTION_MOVE -> {
if (touchSessionActive) {
emitCurrentStateIfChanged()
}
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL -> {
touchSessionActive = false
emitIfChanged(false)
}
}
}
private fun handlePinchGesture(targetView: View, event: MotionEvent): Boolean {
if (suppressUntilAllPointersUp) {
when (event.actionMasked) {
MotionEvent.ACTION_MOVE -> {
dispatchSyntheticCancelIfNeeded(targetView, event)
return true
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL -> {
val consumeEvent = syntheticCancelDispatched
if (!syntheticCancelDispatched && event.actionMasked == MotionEvent.ACTION_UP) {
dispatchSyntheticCancelIfNeeded(targetView, event)
resetPinchGuard()
return true
}
resetPinchGuard()
return consumeEvent
}
}
}
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> resetPinchGuard()
MotionEvent.ACTION_POINTER_DOWN -> multiTouchSequenceActive = true
MotionEvent.ACTION_POINTER_UP -> {
if (multiTouchSequenceActive) {
suppressUntilAllPointersUp = true
// Prevent APZ from treating the remaining finger as a pan/fling after pinch end.
postSyntheticCancelIfNeeded(targetView, event)
}
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL -> resetPinchGuard()
}
// Do not consume normal touch; GeckoView must keep handling it.
return false
}
private fun postSyntheticCancelIfNeeded(targetView: View, event: MotionEvent) {
val cancelEvent = MotionEvent.obtain(event)
targetView.post {
if (running && suppressUntilAllPointersUp && !syntheticCancelDispatched) {
dispatchSyntheticCancel(targetView, cancelEvent)
} else {
cancelEvent.recycle()
}
}
}
private fun dispatchSyntheticCancelIfNeeded(targetView: View, event: MotionEvent) {
if (syntheticCancelDispatched) return
val cancelEvent = MotionEvent.obtain(event)
dispatchSyntheticCancel(targetView, cancelEvent)
}
private fun dispatchSyntheticCancel(targetView: View, cancelEvent: MotionEvent) {
cancelEvent.action = MotionEvent.ACTION_CANCEL
targetView.onTouchEvent(cancelEvent)
cancelEvent.recycle()
syntheticCancelDispatched = true
}
private fun resetPinchGuard() {
multiTouchSequenceActive = false
suppressUntilAllPointersUp = false
syntheticCancelDispatched = false
}
private fun detachTouchListenerIfPossible() { private fun detachTouchListenerIfPossible() {
if (touchTargetView != null && touchListener != null) { if (touchTargetView != null && touchListener != null) {
touchTargetView?.setOnTouchListener(null) touchTargetView?.setOnTouchListener(null)
@@ -100,7 +208,7 @@ class BrowserHandlingScrollFeature(
return root return root
} }
private fun emitIfChanged() { private fun emitCurrentStateIfChanged() {
val isHandling = try { val isHandling = try {
val engineView = GlobalComponents.components?.mainBrowserEngineView val engineView = GlobalComponents.components?.mainBrowserEngineView
if (engineView == null) { if (engineView == null) {
@@ -113,6 +221,10 @@ class BrowserHandlingScrollFeature(
false false
} }
emitIfChanged(isHandling)
}
private fun emitIfChanged(isHandling: Boolean) {
if (lastValue == isHandling) return if (lastValue == isHandling) return
lastValue = isHandling lastValue = isHandling