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 {
|
||||
|
||||
@@ -30,6 +30,7 @@ class GeckoViewportService extends GeckoViewportEvents {
|
||||
isVisible: false,
|
||||
isAnimating: false,
|
||||
));
|
||||
final _browserHandlingScrollSubject = BehaviorSubject<bool>.seeded(false);
|
||||
|
||||
/// Stream of keyboard visibility changes.
|
||||
///
|
||||
@@ -46,6 +47,17 @@ class GeckoViewportService extends GeckoViewportEvents {
|
||||
/// Whether the keyboard is currently visible.
|
||||
bool get isKeyboardVisible => _keyboardSubject.value.isVisible;
|
||||
|
||||
/// Stream of browser scroll-handling eligibility.
|
||||
///
|
||||
/// Emits true when GeckoView reports that browser content can handle
|
||||
/// scrolling for dynamic toolbar behavior.
|
||||
ValueStream<bool> get browserHandlingScrollEvents =>
|
||||
_browserHandlingScrollSubject.stream;
|
||||
|
||||
/// Current browser scroll-handling eligibility.
|
||||
bool get isBrowserHandlingScrollEnabled =>
|
||||
_browserHandlingScrollSubject.value;
|
||||
|
||||
/// Creates a new viewport service.
|
||||
///
|
||||
/// Call [setUp] to register the event handlers after construction.
|
||||
@@ -113,8 +125,14 @@ class GeckoViewportService extends GeckoViewportEvents {
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
void onBrowserHandlingScrollChanged(int sequence, bool isHandling) {
|
||||
_browserHandlingScrollSubject.addWhenMoreRecent(sequence, null, isHandling);
|
||||
}
|
||||
|
||||
/// Disposes the service and closes all streams.
|
||||
Future<void> dispose() async {
|
||||
await _keyboardSubject.close();
|
||||
await _browserHandlingScrollSubject.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1735,6 +1735,7 @@ class TabContentState {
|
||||
required this.isPrivate,
|
||||
required this.isFullScreen,
|
||||
required this.isLoading,
|
||||
required this.showToolbarAsExpanded,
|
||||
});
|
||||
|
||||
String id;
|
||||
@@ -1755,6 +1756,8 @@ class TabContentState {
|
||||
|
||||
bool isLoading;
|
||||
|
||||
bool showToolbarAsExpanded;
|
||||
|
||||
List<Object?> _toList() {
|
||||
return <Object?>[
|
||||
id,
|
||||
@@ -1766,6 +1769,7 @@ class TabContentState {
|
||||
isPrivate,
|
||||
isFullScreen,
|
||||
isLoading,
|
||||
showToolbarAsExpanded,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1784,6 +1788,7 @@ class TabContentState {
|
||||
isPrivate: result[6]! as bool,
|
||||
isFullScreen: result[7]! as bool,
|
||||
isLoading: result[8]! as bool,
|
||||
showToolbarAsExpanded: result[9]! as bool,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8133,6 +8138,14 @@ abstract class GeckoViewportEvents {
|
||||
/// [isAnimating] Whether the keyboard is currently animating.
|
||||
void onKeyboardVisibilityChanged(int sequence, int heightPx, bool isVisible, bool isAnimating);
|
||||
|
||||
/// 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.
|
||||
void onBrowserHandlingScrollChanged(int sequence, bool isHandling);
|
||||
|
||||
static void setUp(GeckoViewportEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) {
|
||||
messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||
{
|
||||
@@ -8169,6 +8182,34 @@ abstract class GeckoViewportEvents {
|
||||
});
|
||||
}
|
||||
}
|
||||
{
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
'dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportEvents.onBrowserHandlingScrollChanged$messageChannelSuffix', pigeonChannelCodec,
|
||||
binaryMessenger: binaryMessenger);
|
||||
if (api == null) {
|
||||
pigeonVar_channel.setMessageHandler(null);
|
||||
} else {
|
||||
pigeonVar_channel.setMessageHandler((Object? message) async {
|
||||
assert(message != null,
|
||||
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportEvents.onBrowserHandlingScrollChanged was null.');
|
||||
final List<Object?> args = (message as List<Object?>?)!;
|
||||
final int? arg_sequence = (args[0] as int?);
|
||||
assert(arg_sequence != null,
|
||||
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportEvents.onBrowserHandlingScrollChanged was null, expected non-null int.');
|
||||
final bool? arg_isHandling = (args[1] as bool?);
|
||||
assert(arg_isHandling != null,
|
||||
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportEvents.onBrowserHandlingScrollChanged was null, expected non-null bool.');
|
||||
try {
|
||||
api.onBrowserHandlingScrollChanged(arg_sequence!, arg_isHandling!);
|
||||
return wrapResponse(empty: true);
|
||||
} on PlatformException catch (e) {
|
||||
return wrapResponse(error: e);
|
||||
} catch (e) {
|
||||
return wrapResponse(error: PlatformException(code: 'error', message: e.toString()));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -517,6 +517,7 @@ class TabContentState {
|
||||
final bool isPrivate;
|
||||
final bool isFullScreen;
|
||||
final bool isLoading;
|
||||
final bool showToolbarAsExpanded;
|
||||
|
||||
TabContentState(
|
||||
this.id,
|
||||
@@ -528,6 +529,7 @@ class TabContentState {
|
||||
this.isPrivate,
|
||||
this.isFullScreen,
|
||||
this.isLoading,
|
||||
this.showToolbarAsExpanded,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1746,6 +1748,14 @@ abstract class GeckoViewportEvents {
|
||||
bool isVisible,
|
||||
bool isAnimating,
|
||||
);
|
||||
|
||||
/// 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.
|
||||
void onBrowserHandlingScrollChanged(int sequence, bool isHandling);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user