refactor tab bar behaviour; respect scroll preferences of websites;
This commit is contained in:
+12
@@ -26,6 +26,7 @@ import eu.weblibre.flutter_mozilla_components.addons.WebExtensionActionPopupActi
|
||||
import eu.weblibre.flutter_mozilla_components.addons.WebExtensionPromptFeature
|
||||
import eu.weblibre.flutter_mozilla_components.databinding.FragmentBrowserBinding
|
||||
import eu.weblibre.flutter_mozilla_components.ext.getPreferenceKey
|
||||
import eu.weblibre.flutter_mozilla_components.feature.BrowserHandlingScrollFeature
|
||||
import eu.weblibre.flutter_mozilla_components.feature.KeyboardVisibilityFeature
|
||||
import eu.weblibre.flutter_mozilla_components.feature.ReadabilityExtractFeature
|
||||
import eu.weblibre.flutter_mozilla_components.feature.WebExtensionToolbarFeature
|
||||
@@ -97,6 +98,9 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
|
||||
// Keyboard visibility detection feature
|
||||
private var keyboardVisibilityFeature: KeyboardVisibilityFeature? = null
|
||||
|
||||
// Browser scroll-handling detection feature
|
||||
private var browserHandlingScrollFeature: BrowserHandlingScrollFeature? = null
|
||||
|
||||
// Registers a photo picker activity launcher in single-select mode.
|
||||
private val singleMediaPicker =
|
||||
AndroidPhotoPicker.singleMediaPicker(
|
||||
@@ -500,6 +504,10 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
|
||||
keyboardVisibilityFeature = KeyboardVisibilityFeature(viewportEvents).also {
|
||||
it.start(binding.root)
|
||||
}
|
||||
|
||||
browserHandlingScrollFeature = BrowserHandlingScrollFeature(viewportEvents).also {
|
||||
it.start()
|
||||
}
|
||||
}
|
||||
|
||||
onEngineSetupComplete()
|
||||
@@ -614,6 +622,10 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
|
||||
keyboardVisibilityFeature?.stop()
|
||||
keyboardVisibilityFeature = null
|
||||
|
||||
// Stop browser scroll-handling detection
|
||||
browserHandlingScrollFeature?.stop()
|
||||
browserHandlingScrollFeature = null
|
||||
|
||||
GlobalComponents.onPullToRefreshEnabledChanged = null
|
||||
val engineView = fragmentEngineView
|
||||
engineView?.setActivityContext(null)
|
||||
|
||||
+2
-1
@@ -216,7 +216,8 @@ class GeckoTabsApiImpl : GeckoTabsApi {
|
||||
progress = tab.content.progress.toLong(),
|
||||
isPrivate = tab.content.private,
|
||||
isFullScreen = tab.content.fullScreen,
|
||||
isLoading = tab.content.loading
|
||||
isLoading = tab.content.loading,
|
||||
showToolbarAsExpanded = tab.content.showToolbarAsExpanded,
|
||||
)
|
||||
) { }
|
||||
}
|
||||
|
||||
+14
-2
@@ -30,6 +30,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import mozilla.components.browser.state.action.BrowserAction
|
||||
import mozilla.components.browser.state.action.ContentAction
|
||||
import mozilla.components.browser.state.selector.selectedTab
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.feature.addons.logger
|
||||
@@ -188,7 +189,8 @@ class Events(
|
||||
it.content.private,
|
||||
it.content.fullScreen,
|
||||
it.content.progress,
|
||||
it.content.loading
|
||||
it.content.loading,
|
||||
it.content.showToolbarAsExpanded,
|
||||
)
|
||||
}
|
||||
.debounce(15)
|
||||
@@ -204,9 +206,19 @@ class Events(
|
||||
progress = tab.content.progress.toLong(),
|
||||
isPrivate = tab.content.private,
|
||||
isFullScreen = tab.content.fullScreen,
|
||||
isLoading = tab.content.loading
|
||||
isLoading = tab.content.loading,
|
||||
showToolbarAsExpanded = tab.content.showToolbarAsExpanded,
|
||||
)
|
||||
) { _ -> }
|
||||
|
||||
// Reset showToolbarAsExpanded after forwarding to Flutter,
|
||||
// mirroring Fenix ToolbarBehaviorController behavior.
|
||||
// This ensures subsequent expand events trigger a new state change.
|
||||
if (tab.content.showToolbarAsExpanded) {
|
||||
stateFlow.dispatch(
|
||||
ContentAction.UpdateExpandedToolbarStateAction(tab.id, false)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package eu.weblibre.flutter_mozilla_components.feature
|
||||
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import eu.weblibre.flutter_mozilla_components.GlobalComponents
|
||||
import eu.weblibre.flutter_mozilla_components.ext.EventSequence
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoViewportEvents
|
||||
import org.mozilla.gecko.util.ThreadUtils.runOnUiThread
|
||||
|
||||
/**
|
||||
* Feature that reports whether GeckoView is currently handling scroll input.
|
||||
*
|
||||
* Checks InputResultDetail while active and only emits on state changes.
|
||||
*/
|
||||
class BrowserHandlingScrollFeature(
|
||||
private val flutterEvents: GeckoViewportEvents,
|
||||
) {
|
||||
private var running = false
|
||||
private var touchSessionActive = false
|
||||
private var lastValue: Boolean? = null
|
||||
private var touchListener: View.OnTouchListener? = null
|
||||
private var touchTargetView: View? = null
|
||||
|
||||
fun start() {
|
||||
if (running) return
|
||||
running = true
|
||||
touchSessionActive = false
|
||||
lastValue = null
|
||||
attachTouchListenerIfPossible()
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
if (!running) return
|
||||
running = false
|
||||
touchSessionActive = false
|
||||
detachTouchListenerIfPossible()
|
||||
lastValue = null
|
||||
}
|
||||
|
||||
private fun attachTouchListenerIfPossible() {
|
||||
val engineViewRoot = GlobalComponents.components?.mainBrowserEngineView?.asView()
|
||||
if (engineViewRoot == null) return
|
||||
|
||||
if (touchListener != null) return
|
||||
|
||||
val targetView = resolveTouchTargetView(engineViewRoot)
|
||||
|
||||
touchListener = View.OnTouchListener { _, event ->
|
||||
if (!running) return@OnTouchListener false
|
||||
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
touchSessionActive = true
|
||||
emitIfChanged()
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (touchSessionActive) {
|
||||
emitIfChanged()
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP,
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
touchSessionActive = false
|
||||
}
|
||||
}
|
||||
|
||||
// Never consume touch; GeckoView must keep handling it.
|
||||
false
|
||||
}
|
||||
|
||||
targetView.setOnTouchListener(touchListener)
|
||||
touchTargetView = targetView
|
||||
}
|
||||
|
||||
private fun detachTouchListenerIfPossible() {
|
||||
if (touchTargetView != null && touchListener != null) {
|
||||
touchTargetView?.setOnTouchListener(null)
|
||||
}
|
||||
touchListener = null
|
||||
touchTargetView = null
|
||||
}
|
||||
|
||||
private fun resolveTouchTargetView(root: View): View {
|
||||
if (root !is ViewGroup) return root
|
||||
|
||||
// GeckoEngineView wraps the actual NestedGeckoView as its first child.
|
||||
if (root.childCount > 0) {
|
||||
return root.getChildAt(0)
|
||||
}
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
private fun emitIfChanged() {
|
||||
val isHandling = try {
|
||||
val engineView = GlobalComponents.components?.mainBrowserEngineView
|
||||
if (engineView == null) {
|
||||
false
|
||||
} else {
|
||||
val detail = engineView.getInputResultDetail()
|
||||
detail.canScrollToTop() || detail.canScrollToBottom()
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
false
|
||||
}
|
||||
|
||||
if (lastValue == isHandling) return
|
||||
lastValue = isHandling
|
||||
|
||||
val sequence = EventSequence.next()
|
||||
runOnUiThread {
|
||||
flutterEvents.onBrowserHandlingScrollChanged(sequence, isHandling) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
-2
@@ -1597,7 +1597,8 @@ data class TabContentState (
|
||||
val progress: Long,
|
||||
val isPrivate: Boolean,
|
||||
val isFullScreen: Boolean,
|
||||
val isLoading: Boolean
|
||||
val isLoading: Boolean,
|
||||
val showToolbarAsExpanded: Boolean
|
||||
)
|
||||
{
|
||||
companion object {
|
||||
@@ -1611,7 +1612,8 @@ data class TabContentState (
|
||||
val isPrivate = pigeonVar_list[6] as Boolean
|
||||
val isFullScreen = pigeonVar_list[7] as Boolean
|
||||
val isLoading = pigeonVar_list[8] as Boolean
|
||||
return TabContentState(id, parentId, contextId, url, title, progress, isPrivate, isFullScreen, isLoading)
|
||||
val showToolbarAsExpanded = pigeonVar_list[9] as Boolean
|
||||
return TabContentState(id, parentId, contextId, url, title, progress, isPrivate, isFullScreen, isLoading, showToolbarAsExpanded)
|
||||
}
|
||||
}
|
||||
fun toList(): List<Any?> {
|
||||
@@ -1625,6 +1627,7 @@ data class TabContentState (
|
||||
isPrivate,
|
||||
isFullScreen,
|
||||
isLoading,
|
||||
showToolbarAsExpanded,
|
||||
)
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
@@ -6919,6 +6922,31 @@ class GeckoViewportEvents(private val binaryMessenger: BinaryMessenger, private
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Called when GeckoView scroll-handling eligibility changes.
|
||||
*
|
||||
* [sequence] Event sequence number for ordering.
|
||||
* [isHandling] True when browser content can consume scrolling for
|
||||
* dynamic toolbar behavior. False when content is not scrollable or
|
||||
* the page consumed touch input.
|
||||
*/
|
||||
fun onBrowserHandlingScrollChanged(sequenceArg: Long, isHandlingArg: Boolean, callback: (Result<Unit>) -> Unit)
|
||||
{
|
||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportEvents.onBrowserHandlingScrollChanged$separatedMessageChannelSuffix"
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
|
||||
channel.send(listOf(sequenceArg, isHandlingArg)) {
|
||||
if (it is List<*>) {
|
||||
if (it.size > 1) {
|
||||
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
|
||||
} else {
|
||||
callback(Result.success(Unit))
|
||||
}
|
||||
} else {
|
||||
callback(Result.failure(GeckoPigeonUtils.createConnectionError(channelName)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||
interface GeckoBookmarksApi {
|
||||
|
||||
Reference in New Issue
Block a user