Skip to content

Commit

Permalink
[webview_flutter_android][webview_flutter_wkwebview] Adds support for…
Browse files Browse the repository at this point in the history
… `setOnScrollPositionChange` for webview_flutter platform implementations (#5664)

Adds iOS and Android implementation for content offset listener

This PR is part of a series of PRs that aim to close flutter/flutter#31027.

The PR that contains all changes can be found at #3444.
  • Loading branch information
TheVinhLuong committed Feb 7, 2024
1 parent e4ea6bf commit 0c2473f
Show file tree
Hide file tree
Showing 49 changed files with 2,038 additions and 784 deletions.
1 change: 1 addition & 0 deletions packages/webview_flutter/webview_flutter_android/AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,5 @@ Rahul Raj <64.rahulraj@gmail.com>
Maurits van Beusekom <maurits@baseflow.com>
Nick Bradshaw <nickalasb@gmail.com>
Kai Yu <yk3372@gmail.com>
The Vinh Luong <ltv.luongthevinh@gmail.com>

4 changes: 4 additions & 0 deletions packages/webview_flutter/webview_flutter_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 3.15.0

* Adds support for `setOnScrollPositionChange` method to the `AndroidWebViewController`.

## 3.14.0

* Adds support to show JavaScript dialog. See `AndroidWebViewController.setOnJavaScriptAlertDialog`, `AndroidWebViewController.setOnJavaScriptConfirmDialog` and `AndroidWebViewController.setOnJavaScriptTextInputDialog`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1758,6 +1758,24 @@ public void create(@NonNull Long identifierArg, @NonNull Reply<Void> callback) {
new ArrayList<Object>(Collections.singletonList(identifierArg)),
channelReply -> callback.reply(null));
}

public void onScrollChanged(
@NonNull Long webViewInstanceIdArg,
@NonNull Long leftArg,
@NonNull Long topArg,
@NonNull Long oldLeftArg,
@NonNull Long oldTopArg,
@NonNull Reply<Void> callback) {
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger,
"dev.flutter.pigeon.webview_flutter_android.WebViewFlutterApi.onScrollChanged",
getCodec());
channel.send(
new ArrayList<Object>(
Arrays.asList(webViewInstanceIdArg, leftArg, topArg, oldLeftArg, oldTopArg)),
channelReply -> callback.reply(null));
}
}
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
public interface WebSettingsHostApi {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import androidx.annotation.VisibleForTesting;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebViewFlutterApi;
import java.util.Objects;

/**
* Flutter API implementation for `WebView`.
Expand Down Expand Up @@ -56,4 +57,20 @@ public void create(@NonNull WebView instance, @NonNull WebViewFlutterApi.Reply<V
void setApi(@NonNull WebViewFlutterApi api) {
this.api = api;
}

public void onScrollChanged(
@NonNull WebView instance,
@NonNull Long left,
@NonNull Long top,
@NonNull Long oldLeft,
@NonNull Long oldTop,
@NonNull WebViewFlutterApi.Reply<Void> callback) {
api.onScrollChanged(
Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(instance)),
left,
top,
oldLeft,
oldTop,
callback);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,13 @@ public WebChromeClient getWebChromeClient() {
return currentWebChromeClient;
}

@Override
protected void onScrollChanged(int left, int top, int oldLeft, int oldTop) {
super.onScrollChanged(left, top, oldLeft, oldTop);
api.onScrollChanged(
this, (long) left, (long) top, (long) oldLeft, (long) oldTop, reply -> {});
}

/**
* Flutter API used to send messages back to Dart.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -358,4 +358,25 @@ public void setImportantForAutofillForParentFlutterView() {

verify(mockFlutterView).setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_YES);
}

@Test
public void onScrollChanged() {
final InstanceManager instanceManager = InstanceManager.create(identifier -> {});

final WebViewFlutterApiImpl flutterApiImpl =
new WebViewFlutterApiImpl(mockBinaryMessenger, instanceManager);

final WebViewFlutterApi mockFlutterApi = mock(WebViewFlutterApi.class);
flutterApiImpl.setApi(mockFlutterApi);
flutterApiImpl.create(mockWebView, reply -> {});

flutterApiImpl.onScrollChanged(mockWebView, 0L, 1L, 2L, 3L, reply -> {});

final long instanceIdentifier =
Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(mockWebView));
verify(mockFlutterApi)
.onScrollChanged(eq(instanceIdentifier), eq(0L), eq(1L), eq(2L), eq(3L), any());

instanceManager.stopFinalizationListener();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -758,7 +758,8 @@ Future<void> main() async {
});

group('Programmatic Scroll', () {
testWidgets('setAndGetScrollPosition', (WidgetTester tester) async {
testWidgets('setAndGetAndListenScrollPosition',
(WidgetTester tester) async {
const String scrollTestPage = '''
<!DOCTYPE html>
<html>
Expand All @@ -784,6 +785,7 @@ Future<void> main() async {
base64Encode(const Utf8Encoder().convert(scrollTestPage));

final Completer<void> pageLoaded = Completer<void>();
ScrollPositionChange? recordedPosition;
final PlatformWebViewController controller = PlatformWebViewController(
const PlatformWebViewControllerCreationParams(),
);
Expand All @@ -793,6 +795,10 @@ Future<void> main() async {
);
unawaited(delegate.setOnPageFinished((_) => pageLoaded.complete()));
unawaited(controller.setPlatformNavigationDelegate(delegate));
unawaited(controller.setOnScrollPositionChange(
(ScrollPositionChange contentOffsetChange) {
recordedPosition = contentOffsetChange;
}));

await controller.loadRequest(
LoadRequestParams(
Expand Down Expand Up @@ -824,17 +830,22 @@ Future<void> main() async {
// time to settle.
expect(scrollPos.dx, isNot(X_SCROLL));
expect(scrollPos.dy, isNot(Y_SCROLL));
expect(recordedPosition, null);

await controller.scrollTo(X_SCROLL, Y_SCROLL);
scrollPos = await controller.getScrollPosition();
expect(scrollPos.dx, X_SCROLL);
expect(scrollPos.dy, Y_SCROLL);
expect(recordedPosition?.x, X_SCROLL);
expect(recordedPosition?.y, Y_SCROLL);

// Check scrollBy() (on top of scrollTo())
await controller.scrollBy(X_SCROLL, Y_SCROLL);
scrollPos = await controller.getScrollPosition();
expect(scrollPos.dx, X_SCROLL * 2);
expect(scrollPos.dy, Y_SCROLL * 2);
expect(recordedPosition?.x, X_SCROLL * 2);
expect(recordedPosition?.y, Y_SCROLL * 2);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ dependencies:
# The example app is bundled with the plugin so we use a path dependency on
# the parent directory to use the current plugin's version.
path: ../
webview_flutter_platform_interface: ^2.9.0
webview_flutter_platform_interface: ^2.10.0

dev_dependencies:
espresso: ^0.2.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ class AndroidWebViewProxy {
});

/// Constructs a [android_webview.WebView].
final android_webview.WebView Function() createAndroidWebView;
final android_webview.WebView Function({
void Function(int left, int top, int oldLeft, int oldTop)? onScrollChanged,
}) createAndroidWebView;

/// Constructs a [android_webview.WebChromeClient].
final android_webview.WebChromeClient Function({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,8 @@ class GeolocationPermissionsCallback extends JavaObject {
/// When a [WebView] is no longer needed [release] must be called.
class WebView extends View {
/// Constructs a new WebView.
///
/// Due to changes in Flutter 3.0 the [useHybridComposition] doesn't have
/// any effect and should not be exposed publicly. More info here:
/// https://github.com/flutter/flutter/issues/108106
WebView({
this.onScrollChanged,
@visibleForTesting super.binaryMessenger,
@visibleForTesting super.instanceManager,
}) : super.detached() {
Expand All @@ -149,6 +146,7 @@ class WebView extends View {
/// create copies.
@protected
WebView.detached({
this.onScrollChanged,
super.binaryMessenger,
super.instanceManager,
}) : super.detached();
Expand All @@ -160,6 +158,18 @@ class WebView extends View {
/// The [WebSettings] object used to control the settings for this WebView.
late final WebSettings settings = WebSettings(this);

/// Called in response to an internal scroll in this view
/// (i.e., the view scrolled its own contents).
///
/// This is typically as a result of [scrollBy] or [scrollTo]
/// having been called.
final void Function(
int left,
int top,
int oldLeft,
int oldTop,
)? onScrollChanged;

/// Enables debugging of web contents (HTML / CSS / JavaScript) loaded into any WebViews of this application.
///
/// This flag can be enabled in order to facilitate debugging of web layouts
Expand Down Expand Up @@ -448,6 +458,7 @@ class WebView extends View {
@override
WebView copy() {
return WebView.detached(
onScrollChanged: onScrollChanged,
binaryMessenger: _api.binaryMessenger,
instanceManager: _api.instanceManager,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1135,6 +1135,9 @@ abstract class WebViewFlutterApi {
/// Create a new Dart instance and add it to the `InstanceManager`.
void create(int identifier);

void onScrollChanged(
int webViewInstanceId, int left, int top, int oldLeft, int oldTop);

static void setup(WebViewFlutterApi? api,
{BinaryMessenger? binaryMessenger}) {
{
Expand All @@ -1157,6 +1160,39 @@ abstract class WebViewFlutterApi {
});
}
}
{
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.webview_flutter_android.WebViewFlutterApi.onScrollChanged',
codec,
binaryMessenger: binaryMessenger);
if (api == null) {
channel.setMessageHandler(null);
} else {
channel.setMessageHandler((Object? message) async {
assert(message != null,
'Argument for dev.flutter.pigeon.webview_flutter_android.WebViewFlutterApi.onScrollChanged was null.');
final List<Object?> args = (message as List<Object?>?)!;
final int? arg_webViewInstanceId = (args[0] as int?);
assert(arg_webViewInstanceId != null,
'Argument for dev.flutter.pigeon.webview_flutter_android.WebViewFlutterApi.onScrollChanged was null, expected non-null int.');
final int? arg_left = (args[1] as int?);
assert(arg_left != null,
'Argument for dev.flutter.pigeon.webview_flutter_android.WebViewFlutterApi.onScrollChanged was null, expected non-null int.');
final int? arg_top = (args[2] as int?);
assert(arg_top != null,
'Argument for dev.flutter.pigeon.webview_flutter_android.WebViewFlutterApi.onScrollChanged was null, expected non-null int.');
final int? arg_oldLeft = (args[3] as int?);
assert(arg_oldLeft != null,
'Argument for dev.flutter.pigeon.webview_flutter_android.WebViewFlutterApi.onScrollChanged was null, expected non-null int.');
final int? arg_oldTop = (args[4] as int?);
assert(arg_oldTop != null,
'Argument for dev.flutter.pigeon.webview_flutter_android.WebViewFlutterApi.onScrollChanged was null, expected non-null int.');
api.onScrollChanged(arg_webViewInstanceId!, arg_left!, arg_top!,
arg_oldLeft!, arg_oldTop!);
return;
});
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,18 @@ class WebViewFlutterApiImpl implements WebViewFlutterApi {
void create(int identifier) {
instanceManager.addHostCreatedInstance(WebView.detached(), identifier);
}

@override
void onScrollChanged(
int webViewInstanceId, int left, int top, int oldLeft, int oldTop) {
final WebView? webViewInstance = instanceManager
.getInstanceWithWeakReference(webViewInstanceId) as WebView?;
assert(
webViewInstance != null,
'InstanceManager does not contain a WebView with instanceId: $webViewInstanceId',
);
webViewInstance!.onScrollChanged?.call(left, top, oldLeft, oldTop);
}
}

/// Host api implementation for [WebSettings].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,15 @@ class AndroidWebViewController extends PlatformWebViewController {

/// The native [android_webview.WebView] being controlled.
late final android_webview.WebView _webView =
_androidWebViewParams.androidWebViewProxy.createAndroidWebView();
_androidWebViewParams.androidWebViewProxy.createAndroidWebView(
onScrollChanged: withWeakReferenceTo(this,
(WeakReference<AndroidWebViewController> weakReference) {
return (int left, int top, int oldLeft, int oldTop) async {
final void Function(ScrollPositionChange)? callback =
weakReference.target?._onScrollPositionChangedCallback;
callback?.call(ScrollPositionChange(left.toDouble(), top.toDouble()));
};
}));

late final android_webview.WebChromeClient _webChromeClient =
_androidWebViewParams.androidWebViewProxy.createAndroidWebChromeClient(
Expand Down Expand Up @@ -340,6 +348,9 @@ class AndroidWebViewController extends PlatformWebViewController {
Future<String> Function(JavaScriptTextInputDialogRequest request)?
_onJavaScriptPrompt;

void Function(ScrollPositionChange scrollPositionChange)?
_onScrollPositionChangedCallback;

/// Whether to enable the platform's webview content debugging tools.
///
/// Defaults to false.
Expand Down Expand Up @@ -559,6 +570,13 @@ class AndroidWebViewController extends PlatformWebViewController {
Future<void> setUserAgent(String? userAgent) =>
_webView.settings.setUserAgentString(userAgent);

@override
Future<void> setOnScrollPositionChange(
void Function(ScrollPositionChange scrollPositionChange)?
onScrollPositionChange) async {
_onScrollPositionChangedCallback = onScrollPositionChange;
}

/// Sets the restrictions that apply on automatic media playback.
Future<void> setMediaPlaybackRequiresUserGesture(bool require) {
return _webView.settings.setMediaPlaybackRequiresUserGesture(require);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,14 @@ abstract class WebViewHostApi {
abstract class WebViewFlutterApi {
/// Create a new Dart instance and add it to the `InstanceManager`.
void create(int identifier);

void onScrollChanged(
int webViewInstanceId,
int left,
int top,
int oldLeft,
int oldTop,
);
}

@HostApi(dartHostTestHandler: 'TestWebSettingsHostApi')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: webview_flutter_android
description: A Flutter plugin that provides a WebView widget on Android.
repository: https://github.com/flutter/packages/tree/main/packages/webview_flutter/webview_flutter_android
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22
version: 3.14.0
version: 3.15.0

environment:
sdk: ">=3.0.0 <4.0.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,11 @@ void main() {
onJsPrompt,
}) =>
MockWebChromeClient(),
createAndroidWebView: () => nonNullMockWebView,
createAndroidWebView: (
{dynamic Function(
int left, int top, int oldLeft, int oldTop)?
onScrollChanged}) =>
nonNullMockWebView,
createAndroidWebViewClient: ({
void Function(android_webview.WebView webView, String url)?
onPageFinished,
Expand Down
Loading

0 comments on commit 0c2473f

Please sign in to comment.