From adebeedfa148e932072e53a3cd57872d05ecee11 Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Mon, 1 Mar 2021 17:54:41 -0800 Subject: [PATCH 1/7] Bring HTML inputs into view automatically --- .../webviewflutter/FlutterWebView.java | 30 ++++++++++++++----- .../lib/src/webview_method_channel.dart | 5 +++- .../webview_flutter/lib/webview_flutter.dart | 1 + 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java index 4578c7e0d1fe..022f1c3597e7 100644 --- a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java +++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java @@ -29,7 +29,7 @@ public class FlutterWebView implements PlatformView, MethodCallHandler { private static final String JS_CHANNEL_NAMES_FIELD = "javascriptChannelNames"; - private final InputAwareWebView webView; + private final WebView webView; private final MethodChannel methodChannel; private final FlutterWebViewClient flutterWebViewClient; private final Handler platformThreadHandler; @@ -92,7 +92,13 @@ public void onProgressChanged(WebView view, int progress) { DisplayManager displayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); displayListenerProxy.onPreWebViewInitialization(displayManager); - webView = new InputAwareWebView(context, containerView); + + Boolean usesHybridComposition = (Boolean) params.get("usesHybridComposition"); + webView = + (usesHybridComposition) + ? new WebView(context) + : new InputAwareWebView(context, containerView); + displayListenerProxy.onPostWebViewInitialization(displayManager); platformThreadHandler = new Handler(context.getMainLooper()); @@ -140,7 +146,9 @@ public View getView() { // of Flutter but used as an override anyway wherever it's actually defined. // TODO(mklim): Add the @Override annotation once flutter/engine#9727 rolls to stable. public void onInputConnectionUnlocked() { - webView.unlockInputConnection(); + if (webView instanceof InputAwareWebView) { + ((InputAwareWebView) webView).unlockInputConnection(); + } } // @Override @@ -150,7 +158,9 @@ public void onInputConnectionUnlocked() { // of Flutter but used as an override anyway wherever it's actually defined. // TODO(mklim): Add the @Override annotation once flutter/engine#9727 rolls to stable. public void onInputConnectionLocked() { - webView.lockInputConnection(); + if (webView instanceof InputAwareWebView) { + ((InputAwareWebView) webView).lockInputConnection(); + } } // @Override @@ -160,7 +170,9 @@ public void onInputConnectionLocked() { // of Flutter but used as an override anyway wherever it's actually defined. // TODO(mklim): Add the @Override annotation once stable passes v1.10.9. public void onFlutterViewAttached(View flutterView) { - webView.setContainerView(flutterView); + if (webView instanceof InputAwareWebView) { + ((InputAwareWebView) webView).setContainerView(flutterView); + } } // @Override @@ -170,7 +182,9 @@ public void onFlutterViewAttached(View flutterView) { // of Flutter but used as an override anyway wherever it's actually defined. // TODO(mklim): Add the @Override annotation once stable passes v1.10.9. public void onFlutterViewDetached() { - webView.setContainerView(null); + if (webView instanceof InputAwareWebView) { + ((InputAwareWebView) webView).setContainerView(null); + } } @Override @@ -425,7 +439,9 @@ private void updateUserAgent(String userAgent) { @Override public void dispose() { methodChannel.setMethodCallHandler(null); - webView.dispose(); + if (webView instanceof InputAwareWebView) { + ((InputAwareWebView) webView).dispose(); + } webView.destroy(); } } diff --git a/packages/webview_flutter/lib/src/webview_method_channel.dart b/packages/webview_flutter/lib/src/webview_method_channel.dart index ef1ed51835b8..e26604f74628 100644 --- a/packages/webview_flutter/lib/src/webview_method_channel.dart +++ b/packages/webview_flutter/lib/src/webview_method_channel.dart @@ -201,13 +201,16 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { /// This is used for the `creationParams` argument of the platform views created by /// [AndroidWebViewBuilder] and [CupertinoWebViewBuilder]. static Map creationParamsToMap( - CreationParams creationParams) { + CreationParams creationParams, { + bool usesHybridComposition = false, + }) { return { 'initialUrl': creationParams.initialUrl, 'settings': _webSettingsToMap(creationParams.webSettings), 'javascriptChannelNames': creationParams.javascriptChannelNames.toList(), 'userAgent': creationParams.userAgent, 'autoMediaPlaybackPolicy': creationParams.autoMediaPlaybackPolicy.index, + 'usesHybridComposition': usesHybridComposition, }; } } diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/lib/webview_flutter.dart index 7e4f3d6ac079..22dd00e485c4 100644 --- a/packages/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/lib/webview_flutter.dart @@ -109,6 +109,7 @@ class SurfaceAndroidWebView extends AndroidWebView { layoutDirection: TextDirection.rtl, creationParams: MethodChannelWebViewPlatform.creationParamsToMap( creationParams, + usesHybridComposition: true, ), creationParamsCodec: const StandardMessageCodec(), ) From 600fd2c505ea5e5433f0f63b37b9ede10ee05288 Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Wed, 3 Mar 2021 17:01:06 -0800 Subject: [PATCH 2/7] Test --- .../webview_flutter_test.dart | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/packages/webview_flutter/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/example/integration_test/webview_flutter_test.dart index 50af77fe6c6e..40245917caa8 100644 --- a/packages/webview_flutter/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/example/integration_test/webview_flutter_test.dart @@ -899,6 +899,114 @@ void main() { expect(X_SCROLL * 2, scrollPosX); expect(Y_SCROLL * 2, scrollPosY); }); + + testWidgets('inputs are scrolled into view when focused', + (WidgetTester tester) async { + final String scrollTestPage = ''' + + + + + + +
+ + + + '''; + + final String scrollTestPageBase64 = + base64Encode(const Utf8Encoder().convert(scrollTestPage)); + + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + + await tester.runAsync(() async { + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: SizedBox( + width: 200, + height: 200, + child: WebView( + initialUrl: + 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + javascriptMode: JavascriptMode.unrestricted, + ), + ), + ), + ); + await Future.delayed(Duration(milliseconds: 20)); + await tester.pump(); + }); + + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + final String viewportRectJSON = await controller.evaluateJavascript( + 'JSON.stringify(viewport.getBoundingClientRect())'); + final Map viewportRectRelativeToViewport = + jsonDecode(jsonDecode(viewportRectJSON)); + + // Check that the input is originally outside of the viewport. + { + final String inputClientRectJSON = await controller.evaluateJavascript( + 'JSON.stringify(inputEl.getBoundingClientRect())'); + final Map inputClientRectRelativeToViewport = + jsonDecode(jsonDecode(inputClientRectJSON)); + + expect( + inputClientRectRelativeToViewport['bottom'] <= + viewportRectRelativeToViewport['bottom'], + isFalse); + } + + await controller.evaluateJavascript('inputEl.focus()'); + + // Check that focusing the input brought it into view. + { + final String inputClientRectJSON = await controller.evaluateJavascript( + 'JSON.stringify(inputEl.getBoundingClientRect())'); + final Map inputClientRectRelativeToViewport = + jsonDecode(jsonDecode(inputClientRectJSON)); + + expect( + inputClientRectRelativeToViewport['top'] >= + viewportRectRelativeToViewport['top'], + isTrue); + expect( + inputClientRectRelativeToViewport['bottom'] <= + viewportRectRelativeToViewport['bottom'], + isTrue); + + expect( + inputClientRectRelativeToViewport['left'] >= + viewportRectRelativeToViewport['left'], + isTrue); + expect( + inputClientRectRelativeToViewport['right'] <= + viewportRectRelativeToViewport['right'], + isTrue); + } + }); }, skip: !Platform.isAndroid); group('NavigationDelegate', () { From e0292cf64b3f4798e12d83ca9b36a6e104671bfd Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Thu, 4 Mar 2021 11:28:07 -0800 Subject: [PATCH 3/7] Fix skip check --- .../example/integration_test/webview_flutter_test.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/webview_flutter/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/example/integration_test/webview_flutter_test.dart index 40245917caa8..b4e1a44aa448 100644 --- a/packages/webview_flutter/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/example/integration_test/webview_flutter_test.dart @@ -1007,7 +1007,7 @@ void main() { isTrue); } }); - }, skip: !Platform.isAndroid); + }, skip: defaultTargetPlatform != TargetPlatform.android); group('NavigationDelegate', () { final String blankPage = ""; @@ -1261,7 +1261,7 @@ void main() { await controller.goBack(); expect(controller.currentUrl(), completion('https://www.flutter.dev')); }, - skip: !Platform.isAndroid, + skip: defaultTargetPlatform != TargetPlatform.android, ); testWidgets( @@ -1329,7 +1329,7 @@ void main() { completion('null'), ); }, - skip: !Platform.isAndroid, + skip: defaultTargetPlatform != TargetPlatform.android, ); } From 034bcb4aeed8ab555b8db87c246cc5ba0105727a Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Thu, 4 Mar 2021 11:30:05 -0800 Subject: [PATCH 4/7] Change log --- packages/webview_flutter/CHANGELOG.md | 5 +++++ packages/webview_flutter/pubspec.yaml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/webview_flutter/CHANGELOG.md b/packages/webview_flutter/CHANGELOG.md index 139f120f12b2..6d2b4bb26815 100644 --- a/packages/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.0.2 + +* Fixes bug where text fields are hidden behind the keyboard +when hybrid composition is used [flutter/issues/75667](https://github.com/flutter/flutter/issues/75667). + ## 2.0.1 * Run CocoaPods iOS tests in RunnerUITests target diff --git a/packages/webview_flutter/pubspec.yaml b/packages/webview_flutter/pubspec.yaml index 0640386c517d..6ee9e119bd3a 100644 --- a/packages/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/pubspec.yaml @@ -1,7 +1,7 @@ name: webview_flutter description: A Flutter plugin that provides a WebView widget on Android and iOS. homepage: https://github.com/flutter/plugins/tree/master/packages/webview_flutter -version: 2.0.1 +version: 2.0.2 environment: sdk: ">=2.12.0-259.9.beta <3.0.0" From 911cd975035237e21d8ede0b8aebe799ece9bac9 Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Thu, 4 Mar 2021 12:16:00 -0800 Subject: [PATCH 5/7] Fix test --- .../webview_flutter_test.dart | 92 ++++++++++--------- 1 file changed, 47 insertions(+), 45 deletions(-) diff --git a/packages/webview_flutter/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/example/integration_test/webview_flutter_test.dart index b4e1a44aa448..beabe579de23 100644 --- a/packages/webview_flutter/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/example/integration_test/webview_flutter_test.dart @@ -960,54 +960,51 @@ void main() { final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - - final String viewportRectJSON = await controller.evaluateJavascript( - 'JSON.stringify(viewport.getBoundingClientRect())'); + final String viewportRectJSON = await _evaluateJavascript( + controller, 'JSON.stringify(viewport.getBoundingClientRect())'); final Map viewportRectRelativeToViewport = - jsonDecode(jsonDecode(viewportRectJSON)); + jsonDecode(viewportRectJSON); // Check that the input is originally outside of the viewport. - { - final String inputClientRectJSON = await controller.evaluateJavascript( - 'JSON.stringify(inputEl.getBoundingClientRect())'); - final Map inputClientRectRelativeToViewport = - jsonDecode(jsonDecode(inputClientRectJSON)); - - expect( - inputClientRectRelativeToViewport['bottom'] <= - viewportRectRelativeToViewport['bottom'], - isFalse); - } + + final String initialInputClientRectJSON = await _evaluateJavascript( + controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); + final Map initialInputClientRectRelativeToViewport = + jsonDecode(initialInputClientRectJSON); + + expect( + initialInputClientRectRelativeToViewport['bottom'] <= + viewportRectRelativeToViewport['bottom'], + isFalse); await controller.evaluateJavascript('inputEl.focus()'); // Check that focusing the input brought it into view. - { - final String inputClientRectJSON = await controller.evaluateJavascript( - 'JSON.stringify(inputEl.getBoundingClientRect())'); - final Map inputClientRectRelativeToViewport = - jsonDecode(jsonDecode(inputClientRectJSON)); - - expect( - inputClientRectRelativeToViewport['top'] >= - viewportRectRelativeToViewport['top'], - isTrue); - expect( - inputClientRectRelativeToViewport['bottom'] <= - viewportRectRelativeToViewport['bottom'], - isTrue); - expect( - inputClientRectRelativeToViewport['left'] >= - viewportRectRelativeToViewport['left'], - isTrue); - expect( - inputClientRectRelativeToViewport['right'] <= - viewportRectRelativeToViewport['right'], - isTrue); - } + final String lastInputClientRectJSON = await _evaluateJavascript( + controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); + final Map lastInputClientRectRelativeToViewport = + jsonDecode(lastInputClientRectJSON); + + expect( + lastInputClientRectRelativeToViewport['top'] >= + viewportRectRelativeToViewport['top'], + isTrue); + expect( + lastInputClientRectRelativeToViewport['bottom'] <= + viewportRectRelativeToViewport['bottom'], + isTrue); + + expect( + lastInputClientRectRelativeToViewport['left'] >= + viewportRectRelativeToViewport['left'], + isTrue); + expect( + lastInputClientRectRelativeToViewport['right'] <= + viewportRectRelativeToViewport['right'], + isTrue); }); - }, skip: defaultTargetPlatform != TargetPlatform.android); + }, skip: !Platform.isAndroid); group('NavigationDelegate', () { final String blankPage = ""; @@ -1074,7 +1071,8 @@ void main() { expect(error.failingUrl, isNull); } else if (Platform.isAndroid) { expect(error.errorType, isNotNull); - expect(error.failingUrl, 'https://www.notawebsite..com'); + expect(error.failingUrl.startsWith('https://www.notawebsite..com'), + isTrue); } }); @@ -1261,7 +1259,7 @@ void main() { await controller.goBack(); expect(controller.currentUrl(), completion('https://www.flutter.dev')); }, - skip: defaultTargetPlatform != TargetPlatform.android, + skip: !Platform.isAndroid, ); testWidgets( @@ -1329,7 +1327,7 @@ void main() { completion('null'), ); }, - skip: defaultTargetPlatform != TargetPlatform.android, + skip: !Platform.isAndroid, ); } @@ -1344,9 +1342,13 @@ String _webviewBool(bool value) { /// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests. Future _getUserAgent(WebViewController controller) async { + return _evaluateJavascript(controller, 'navigator.userAgent;'); +} + +Future _evaluateJavascript( + WebViewController controller, String js) async { if (defaultTargetPlatform == TargetPlatform.iOS) { - return await controller.evaluateJavascript('navigator.userAgent;'); + return await controller.evaluateJavascript(js); } - return jsonDecode( - await controller.evaluateJavascript('navigator.userAgent;')); + return jsonDecode(await controller.evaluateJavascript(js)); } From c22306093256bbfc420b62e9ec8f49c4c81f24ab Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Thu, 4 Mar 2021 17:47:43 -0800 Subject: [PATCH 6/7] More checks --- .../example/integration_test/webview_flutter_test.dart | 6 ++++-- packages/webview_flutter/lib/webview_flutter.dart | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/webview_flutter/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/example/integration_test/webview_flutter_test.dart index beabe579de23..1602eef2415a 100644 --- a/packages/webview_flutter/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/example/integration_test/webview_flutter_test.dart @@ -823,7 +823,7 @@ void main() { }); }); - group('$SurfaceAndroidWebView', () { + group('SurfaceAndroidWebView', () { setUpAll(() { WebView.platform = SurfaceAndroidWebView(); }); @@ -902,6 +902,8 @@ void main() { testWidgets('inputs are scrolled into view when focused', (WidgetTester tester) async { + expect(Platform.isAndroid, isTrue); + final String scrollTestPage = ''' @@ -1004,7 +1006,7 @@ void main() { viewportRectRelativeToViewport['right'], isTrue); }); - }, skip: !Platform.isAndroid); + }, skip: Platform.isAndroid ? false : 'Platform isn\'t Android'); group('NavigationDelegate', () { final String blankPage = ""; diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/lib/webview_flutter.dart index 22dd00e485c4..56315b6692a5 100644 --- a/packages/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/lib/webview_flutter.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; @@ -85,6 +86,7 @@ class SurfaceAndroidWebView extends AndroidWebView { Set>? gestureRecognizers, required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, }) { + assert(Platform.isAndroid); assert(webViewPlatformCallbacksHandler != null); return PlatformViewLink( viewType: 'plugins.flutter.io/webview', From 813329942e23061e18892872273500cce10ba415 Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Fri, 5 Mar 2021 11:01:01 -0800 Subject: [PATCH 7/7] testWidgets are not skipped on stable if skip:true in the group --- .../example/integration_test/webview_flutter_test.dart | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/webview_flutter/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/example/integration_test/webview_flutter_test.dart index 1602eef2415a..d91ccc53b231 100644 --- a/packages/webview_flutter/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/example/integration_test/webview_flutter_test.dart @@ -898,12 +898,10 @@ void main() { scrollPosY = await controller.getScrollY(); expect(X_SCROLL * 2, scrollPosX); expect(Y_SCROLL * 2, scrollPosY); - }); + }, skip: !Platform.isAndroid); testWidgets('inputs are scrolled into view when focused', (WidgetTester tester) async { - expect(Platform.isAndroid, isTrue); - final String scrollTestPage = ''' @@ -1005,8 +1003,8 @@ void main() { lastInputClientRectRelativeToViewport['right'] <= viewportRectRelativeToViewport['right'], isTrue); - }); - }, skip: Platform.isAndroid ? false : 'Platform isn\'t Android'); + }, skip: !Platform.isAndroid); + }); group('NavigationDelegate', () { final String blankPage = "";