Skip to content

Commit fc1b17e

Browse files
author
Emmanuel Garcia
authored
Bring HTML inputs into view automatically (#3655)
1 parent b709f7e commit fc1b17e

File tree

6 files changed

+153
-16
lines changed

6 files changed

+153
-16
lines changed

packages/webview_flutter/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 2.0.2
2+
3+
* Fixes bug where text fields are hidden behind the keyboard
4+
when hybrid composition is used [flutter/issues/75667](https://github.com/flutter/flutter/issues/75667).
5+
16
## 2.0.1
27

38
* Run CocoaPods iOS tests in RunnerUITests target

packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030
public class FlutterWebView implements PlatformView, MethodCallHandler {
3131
private static final String JS_CHANNEL_NAMES_FIELD = "javascriptChannelNames";
32-
private final InputAwareWebView webView;
32+
private final WebView webView;
3333
private final MethodChannel methodChannel;
3434
private final FlutterWebViewClient flutterWebViewClient;
3535
private final Handler platformThreadHandler;
@@ -92,7 +92,13 @@ public void onProgressChanged(WebView view, int progress) {
9292
DisplayManager displayManager =
9393
(DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
9494
displayListenerProxy.onPreWebViewInitialization(displayManager);
95-
webView = new InputAwareWebView(context, containerView);
95+
96+
Boolean usesHybridComposition = (Boolean) params.get("usesHybridComposition");
97+
webView =
98+
(usesHybridComposition)
99+
? new WebView(context)
100+
: new InputAwareWebView(context, containerView);
101+
96102
displayListenerProxy.onPostWebViewInitialization(displayManager);
97103

98104
platformThreadHandler = new Handler(context.getMainLooper());
@@ -140,7 +146,9 @@ public View getView() {
140146
// of Flutter but used as an override anyway wherever it's actually defined.
141147
// TODO(mklim): Add the @Override annotation once flutter/engine#9727 rolls to stable.
142148
public void onInputConnectionUnlocked() {
143-
webView.unlockInputConnection();
149+
if (webView instanceof InputAwareWebView) {
150+
((InputAwareWebView) webView).unlockInputConnection();
151+
}
144152
}
145153

146154
// @Override
@@ -150,7 +158,9 @@ public void onInputConnectionUnlocked() {
150158
// of Flutter but used as an override anyway wherever it's actually defined.
151159
// TODO(mklim): Add the @Override annotation once flutter/engine#9727 rolls to stable.
152160
public void onInputConnectionLocked() {
153-
webView.lockInputConnection();
161+
if (webView instanceof InputAwareWebView) {
162+
((InputAwareWebView) webView).lockInputConnection();
163+
}
154164
}
155165

156166
// @Override
@@ -160,7 +170,9 @@ public void onInputConnectionLocked() {
160170
// of Flutter but used as an override anyway wherever it's actually defined.
161171
// TODO(mklim): Add the @Override annotation once stable passes v1.10.9.
162172
public void onFlutterViewAttached(View flutterView) {
163-
webView.setContainerView(flutterView);
173+
if (webView instanceof InputAwareWebView) {
174+
((InputAwareWebView) webView).setContainerView(flutterView);
175+
}
164176
}
165177

166178
// @Override
@@ -170,7 +182,9 @@ public void onFlutterViewAttached(View flutterView) {
170182
// of Flutter but used as an override anyway wherever it's actually defined.
171183
// TODO(mklim): Add the @Override annotation once stable passes v1.10.9.
172184
public void onFlutterViewDetached() {
173-
webView.setContainerView(null);
185+
if (webView instanceof InputAwareWebView) {
186+
((InputAwareWebView) webView).setContainerView(null);
187+
}
174188
}
175189

176190
@Override
@@ -425,7 +439,9 @@ private void updateUserAgent(String userAgent) {
425439
@Override
426440
public void dispose() {
427441
methodChannel.setMethodCallHandler(null);
428-
webView.dispose();
442+
if (webView instanceof InputAwareWebView) {
443+
((InputAwareWebView) webView).dispose();
444+
}
429445
webView.destroy();
430446
}
431447
}

packages/webview_flutter/example/integration_test/webview_flutter_test.dart

Lines changed: 117 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -823,7 +823,7 @@ void main() {
823823
});
824824
});
825825

826-
group('$SurfaceAndroidWebView', () {
826+
group('SurfaceAndroidWebView', () {
827827
setUpAll(() {
828828
WebView.platform = SurfaceAndroidWebView();
829829
});
@@ -898,8 +898,113 @@ void main() {
898898
scrollPosY = await controller.getScrollY();
899899
expect(X_SCROLL * 2, scrollPosX);
900900
expect(Y_SCROLL * 2, scrollPosY);
901-
});
902-
}, skip: !Platform.isAndroid);
901+
}, skip: !Platform.isAndroid);
902+
903+
testWidgets('inputs are scrolled into view when focused',
904+
(WidgetTester tester) async {
905+
final String scrollTestPage = '''
906+
<!DOCTYPE html>
907+
<html>
908+
<head>
909+
<style>
910+
input {
911+
margin: 10000px 0;
912+
}
913+
#viewport {
914+
position: fixed;
915+
top:0;
916+
bottom:0;
917+
left:0;
918+
right:0;
919+
}
920+
</style>
921+
</head>
922+
<body>
923+
<div id="viewport"></div>
924+
<input type="text" id="inputEl">
925+
</body>
926+
</html>
927+
''';
928+
929+
final String scrollTestPageBase64 =
930+
base64Encode(const Utf8Encoder().convert(scrollTestPage));
931+
932+
final Completer<void> pageLoaded = Completer<void>();
933+
final Completer<WebViewController> controllerCompleter =
934+
Completer<WebViewController>();
935+
936+
await tester.runAsync(() async {
937+
await tester.pumpWidget(
938+
Directionality(
939+
textDirection: TextDirection.ltr,
940+
child: SizedBox(
941+
width: 200,
942+
height: 200,
943+
child: WebView(
944+
initialUrl:
945+
'data:text/html;charset=utf-8;base64,$scrollTestPageBase64',
946+
onWebViewCreated: (WebViewController controller) {
947+
controllerCompleter.complete(controller);
948+
},
949+
onPageFinished: (String url) {
950+
pageLoaded.complete(null);
951+
},
952+
javascriptMode: JavascriptMode.unrestricted,
953+
),
954+
),
955+
),
956+
);
957+
await Future.delayed(Duration(milliseconds: 20));
958+
await tester.pump();
959+
});
960+
961+
final WebViewController controller = await controllerCompleter.future;
962+
await pageLoaded.future;
963+
final String viewportRectJSON = await _evaluateJavascript(
964+
controller, 'JSON.stringify(viewport.getBoundingClientRect())');
965+
final Map<String, dynamic> viewportRectRelativeToViewport =
966+
jsonDecode(viewportRectJSON);
967+
968+
// Check that the input is originally outside of the viewport.
969+
970+
final String initialInputClientRectJSON = await _evaluateJavascript(
971+
controller, 'JSON.stringify(inputEl.getBoundingClientRect())');
972+
final Map<String, dynamic> initialInputClientRectRelativeToViewport =
973+
jsonDecode(initialInputClientRectJSON);
974+
975+
expect(
976+
initialInputClientRectRelativeToViewport['bottom'] <=
977+
viewportRectRelativeToViewport['bottom'],
978+
isFalse);
979+
980+
await controller.evaluateJavascript('inputEl.focus()');
981+
982+
// Check that focusing the input brought it into view.
983+
984+
final String lastInputClientRectJSON = await _evaluateJavascript(
985+
controller, 'JSON.stringify(inputEl.getBoundingClientRect())');
986+
final Map<String, dynamic> lastInputClientRectRelativeToViewport =
987+
jsonDecode(lastInputClientRectJSON);
988+
989+
expect(
990+
lastInputClientRectRelativeToViewport['top'] >=
991+
viewportRectRelativeToViewport['top'],
992+
isTrue);
993+
expect(
994+
lastInputClientRectRelativeToViewport['bottom'] <=
995+
viewportRectRelativeToViewport['bottom'],
996+
isTrue);
997+
998+
expect(
999+
lastInputClientRectRelativeToViewport['left'] >=
1000+
viewportRectRelativeToViewport['left'],
1001+
isTrue);
1002+
expect(
1003+
lastInputClientRectRelativeToViewport['right'] <=
1004+
viewportRectRelativeToViewport['right'],
1005+
isTrue);
1006+
}, skip: !Platform.isAndroid);
1007+
});
9031008

9041009
group('NavigationDelegate', () {
9051010
final String blankPage = "<!DOCTYPE html><head></head><body></body></html>";
@@ -966,7 +1071,8 @@ void main() {
9661071
expect(error.failingUrl, isNull);
9671072
} else if (Platform.isAndroid) {
9681073
expect(error.errorType, isNotNull);
969-
expect(error.failingUrl, 'https://www.notawebsite..com');
1074+
expect(error.failingUrl.startsWith('https://www.notawebsite..com'),
1075+
isTrue);
9701076
}
9711077
});
9721078

@@ -1236,9 +1342,13 @@ String _webviewBool(bool value) {
12361342

12371343
/// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests.
12381344
Future<String> _getUserAgent(WebViewController controller) async {
1345+
return _evaluateJavascript(controller, 'navigator.userAgent;');
1346+
}
1347+
1348+
Future<String> _evaluateJavascript(
1349+
WebViewController controller, String js) async {
12391350
if (defaultTargetPlatform == TargetPlatform.iOS) {
1240-
return await controller.evaluateJavascript('navigator.userAgent;');
1351+
return await controller.evaluateJavascript(js);
12411352
}
1242-
return jsonDecode(
1243-
await controller.evaluateJavascript('navigator.userAgent;'));
1353+
return jsonDecode(await controller.evaluateJavascript(js));
12441354
}

packages/webview_flutter/lib/src/webview_method_channel.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,13 +201,16 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController {
201201
/// This is used for the `creationParams` argument of the platform views created by
202202
/// [AndroidWebViewBuilder] and [CupertinoWebViewBuilder].
203203
static Map<String, dynamic> creationParamsToMap(
204-
CreationParams creationParams) {
204+
CreationParams creationParams, {
205+
bool usesHybridComposition = false,
206+
}) {
205207
return <String, dynamic>{
206208
'initialUrl': creationParams.initialUrl,
207209
'settings': _webSettingsToMap(creationParams.webSettings),
208210
'javascriptChannelNames': creationParams.javascriptChannelNames.toList(),
209211
'userAgent': creationParams.userAgent,
210212
'autoMediaPlaybackPolicy': creationParams.autoMediaPlaybackPolicy.index,
213+
'usesHybridComposition': usesHybridComposition,
211214
};
212215
}
213216
}

packages/webview_flutter/lib/webview_flutter.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// found in the LICENSE file.
44

55
import 'dart:async';
6+
import 'dart:io';
67

78
import 'package:flutter/foundation.dart';
89
import 'package:flutter/gestures.dart';
@@ -85,6 +86,7 @@ class SurfaceAndroidWebView extends AndroidWebView {
8586
Set<Factory<OneSequenceGestureRecognizer>>? gestureRecognizers,
8687
required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler,
8788
}) {
89+
assert(Platform.isAndroid);
8890
assert(webViewPlatformCallbacksHandler != null);
8991
return PlatformViewLink(
9092
viewType: 'plugins.flutter.io/webview',
@@ -109,6 +111,7 @@ class SurfaceAndroidWebView extends AndroidWebView {
109111
layoutDirection: TextDirection.rtl,
110112
creationParams: MethodChannelWebViewPlatform.creationParamsToMap(
111113
creationParams,
114+
usesHybridComposition: true,
112115
),
113116
creationParamsCodec: const StandardMessageCodec(),
114117
)

packages/webview_flutter/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: webview_flutter
22
description: A Flutter plugin that provides a WebView widget on Android and iOS.
33
homepage: https://github.com/flutter/plugins/tree/master/packages/webview_flutter
4-
version: 2.0.1
4+
version: 2.0.2
55

66
environment:
77
sdk: ">=2.12.0-259.9.beta <3.0.0"

0 commit comments

Comments
 (0)