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/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/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/example/integration_test/webview_flutter_test.dart index 50af77fe6c6e..beabe579de23 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,111 @@ 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 _evaluateJavascript( + controller, 'JSON.stringify(viewport.getBoundingClientRect())'); + final Map viewportRectRelativeToViewport = + jsonDecode(viewportRectJSON); + + // Check that the input is originally outside of the viewport. + + 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 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: !Platform.isAndroid); group('NavigationDelegate', () { @@ -966,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); } }); @@ -1236,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)); } 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(), ) 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"