Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

[webview_flutter] Implement zoom enabled for ios and android #4417

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -65,4 +65,5 @@ Anton Borries <[email protected]>
Alex Li <[email protected]>
Rahul Raj <[email protected]>
Maurits van Beusekom <[email protected]>
Nick Bradshaw <[email protected]>

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 @@
## 2.1.0

* Add `zoomEnabled` functionality.

## 2.0.15

* Added Overrides in FlutterWebView.java
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,10 @@ static WebView createWebView(
.setJavaScriptCanOpenWindowsAutomatically(
true) // Always allow automatically opening of windows.
.setSupportMultipleWindows(true) // Always support multiple windows.
.setWebChromeClient(webChromeClient)
.setDownloadListener(
downloadListener); // Always use {@link FlutterWebChromeClient} as web Chrome client.
.setWebChromeClient(
webChromeClient) // Always use {@link FlutterWebChromeClient} as web Chrome client.
.setDownloadListener(downloadListener)
.setZoomControlsEnabled(true); // Always use built-in zoom mechanisms.

return webViewBuilder.build();
}
Expand Down Expand Up @@ -428,6 +429,9 @@ private void applySettings(Map<String, Object> settings) {
case "allowsInlineMediaPlayback":
// no-op inline media playback is always allowed on Android.
break;
case "zoomEnabled":
setZoomEnabled((boolean) settings.get(key));
break;
default:
throw new IllegalArgumentException("Unknown WebView setting: " + key);
}
Expand Down Expand Up @@ -467,6 +471,10 @@ private void updateUserAgent(String userAgent) {
webView.getSettings().setUserAgentString(userAgent);
}

private void setZoomEnabled(boolean shouldEnable) {
webView.getSettings().setSupportZoom(shouldEnable);
}

@Override
public void dispose() {
methodChannel.setMethodCallHandler(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ static WebView create(Context context, boolean usesHybridComposition, View conta
private boolean usesHybridComposition;
private WebChromeClient webChromeClient;
private DownloadListener downloadListener;
private boolean enableBuiltInZoomControls;

/**
* Constructs a new {@link WebViewBuilder} object with a custom implementation of the {@link
Expand Down Expand Up @@ -136,6 +137,18 @@ public WebViewBuilder setDownloadListener(@Nullable DownloadListener downloadLis
return this;
}

/**
* Sets whether the {@link WebView} should use its built-in zoom mechanisms. The default value is
* {@code true}.
*
* @param flag {@code true} if built in zoom controls are allowed.
* @return This builder. This value cannot be {@code null}.
*/
public WebViewBuilder setZoomControlsEnabled(boolean flag) {
this.enableBuiltInZoomControls = flag;
return this;
}

/**
* Build the {@link android.webkit.WebView} using the current settings.
*
Expand All @@ -148,6 +161,10 @@ public WebView build() {
webSettings.setDomStorageEnabled(enableDomStorage);
webSettings.setJavaScriptCanOpenWindowsAutomatically(javaScriptCanOpenWindowsAutomatically);
webSettings.setSupportMultipleWindows(supportMultipleWindows);
webSettings.setLoadWithOverviewMode(true);
webSettings.setUseWideViewPort(true);
webSettings.setDisplayZoomControls(false);
webSettings.setBuiltInZoomControls(enableBuiltInZoomControls);
webView.setWebChromeClient(webChromeClient);
webView.setDownloadListener(downloadListener);
return webView;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public void before() {
.thenReturn(mockWebViewBuilder);
when(mockWebViewBuilder.setSupportMultipleWindows(anyBoolean())).thenReturn(mockWebViewBuilder);
when(mockWebViewBuilder.setUsesHybridComposition(anyBoolean())).thenReturn(mockWebViewBuilder);
when(mockWebViewBuilder.setZoomControlsEnabled(anyBoolean())).thenReturn(mockWebViewBuilder);
when(mockWebViewBuilder.setWebChromeClient(any(WebChromeClient.class)))
.thenReturn(mockWebViewBuilder);
when(mockWebViewBuilder.setDownloadListener(any(DownloadListener.class)))
Expand All @@ -55,6 +56,7 @@ public void createWebView_should_create_webview_with_default_configuration() {
verify(mockWebViewBuilder, times(1)).setSupportMultipleWindows(true);
verify(mockWebViewBuilder, times(1)).setUsesHybridComposition(false);
verify(mockWebViewBuilder, times(1)).setWebChromeClient(mockWebChromeClient);
verify(mockWebViewBuilder, times(1)).setZoomControlsEnabled(true);
}

private Map<String, Object> createParameterMap(boolean usesHybridComposition) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ class WebView extends StatefulWidget {
this.debuggingEnabled = false,
this.gestureNavigationEnabled = false,
this.userAgent,
this.zoomEnabled = true,
this.initialMediaPlaybackPolicy =
AutoMediaPlaybackPolicy.require_user_action_for_all_media_types,
this.allowsInlineMediaPlayback = false,
Expand Down Expand Up @@ -221,6 +222,11 @@ class WebView extends StatefulWidget {
/// By default `gestureNavigationEnabled` is false.
final bool gestureNavigationEnabled;

/// A Boolean value indicating whether the WebView should support zooming using its on-screen zoom controls and gestures.
///
/// By default 'zoomEnabled' is true
final bool zoomEnabled;

/// The value used for the HTTP User-Agent: request header.
///
/// When null the platform's webview default is used for the User-Agent header.
Expand Down Expand Up @@ -553,12 +559,14 @@ class WebViewController {
assert(newValue.hasNavigationDelegate != null);
assert(newValue.debuggingEnabled != null);
assert(newValue.userAgent != null);
assert(newValue.zoomEnabled != null);

JavascriptMode? javascriptMode;
bool? hasNavigationDelegate;
bool? hasProgressTracking;
bool? debuggingEnabled;
WebSetting<String?> userAgent = WebSetting.absent();
bool? zoomEnabled;
if (currentValue.javascriptMode != newValue.javascriptMode) {
javascriptMode = newValue.javascriptMode;
}
Expand All @@ -574,13 +582,17 @@ class WebViewController {
if (currentValue.userAgent != newValue.userAgent) {
userAgent = newValue.userAgent;
}
if (currentValue.zoomEnabled != newValue.zoomEnabled) {
zoomEnabled = newValue.zoomEnabled;
}

return WebSettings(
javascriptMode: javascriptMode,
hasNavigationDelegate: hasNavigationDelegate,
hasProgressTracking: hasProgressTracking,
debuggingEnabled: debuggingEnabled,
userAgent: userAgent,
zoomEnabled: zoomEnabled,
);
}

Expand Down Expand Up @@ -613,5 +625,6 @@ WebSettings _webSettingsFromWidget(WebView widget) {
gestureNavigationEnabled: widget.gestureNavigationEnabled,
allowsInlineMediaPlayback: widget.allowsInlineMediaPlayback,
userAgent: WebSetting<String?>.of(widget.userAgent),
zoomEnabled: widget.zoomEnabled,
);
}
5 changes: 2 additions & 3 deletions packages/webview_flutter/webview_flutter_android/pubspec.yaml
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/plugins/tree/master/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: 2.0.15
version: 2.1.0

environment:
sdk: ">=2.14.0 <3.0.0"
Expand All @@ -19,8 +19,7 @@ flutter:
dependencies:
flutter:
sdk: flutter

webview_flutter_platform_interface: ^1.0.0
webview_flutter_platform_interface: ^1.2.0

dev_dependencies:
flutter_driver:
Expand Down
2 changes: 2 additions & 0 deletions packages/webview_flutter/webview_flutter_wkwebview/AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,5 @@ Anton Borries <[email protected]>
Alex Li <[email protected]>
Rahul Raj <[email protected]>
Maurits van Beusekom <[email protected]>
Antonino Di Natale <[email protected]>
Nick Bradshaw <[email protected]>
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.1.0

* Add `zoomEnabled` functionality.

## 2.0.14

* Update example App so navigation menu loads immediatly but only becomes available when `WebViewController` is available (same behavior as example App in webview_flutter package).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,19 @@ @interface FLTWKNavigationDelegateTests : XCTestCase

@property(strong, nonatomic) FlutterMethodChannel *mockMethodChannel;
@property(strong, nonatomic) FLTWKNavigationDelegate *navigationDelegate;
@property(strong, nonatomic) WKNavigation *navigation;

@end

@implementation FLTWKNavigationDelegateTests

NSString *const zoomDisablingJavascript =
@"var meta = document.createElement('meta');"
@"meta.name = 'viewport';"
@"meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0,"
@"user-scalable=no';"
@"var head = document.getElementsByTagName('head')[0];head.appendChild(meta);";

- (void)setUp {
self.mockMethodChannel = OCMClassMock(FlutterMethodChannel.class);
self.navigationDelegate =
Expand All @@ -38,4 +46,27 @@ - (void)testWebViewWebContentProcessDidTerminateCallsRecourseErrorChannel {
}
}

- (void)testWebViewWebEvaluateJavaScriptSourceIsCorrectWhenShouldEnableZoomIsFalse {
WKWebView *webview = OCMClassMock(WKWebView.class);
WKNavigation *navigation = OCMClassMock(WKNavigation.class);
NSURL *testUrl = [[NSURL alloc] initWithString:@"www.example.com"];
OCMStub([webview URL]).andReturn(testUrl);

self.navigationDelegate.shouldEnableZoom = false;
[self.navigationDelegate webView:webview didFinishNavigation:navigation];
OCMVerify([webview evaluateJavaScript:zoomDisablingJavascript completionHandler:nil]);
}

- (void)testWebViewWebEvaluateJavaScriptShouldNotBeCalledWhenShouldEnableZoomIsTrue {
WKWebView *webview = OCMClassMock(WKWebView.class);
WKNavigation *navigation = OCMClassMock(WKNavigation.class);
NSURL *testUrl = [[NSURL alloc] initWithString:@"www.example.com"];
OCMStub([webview URL]).andReturn(testUrl);

self.navigationDelegate.shouldEnableZoom = true;

OCMReject([webview evaluateJavaScript:zoomDisablingJavascript completionHandler:nil]);
[self.navigationDelegate webView:webview didFinishNavigation:navigation];
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

import 'dart:async';

import 'package:flutter/widgets.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';
import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';
import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';

Expand Down Expand Up @@ -65,6 +65,7 @@ class WebView extends StatefulWidget {
this.debuggingEnabled = false,
this.gestureNavigationEnabled = false,
this.userAgent,
this.zoomEnabled = true,
this.initialMediaPlaybackPolicy =
AutoMediaPlaybackPolicy.require_user_action_for_all_media_types,
this.allowsInlineMediaPlayback = false,
Expand Down Expand Up @@ -213,6 +214,11 @@ class WebView extends StatefulWidget {
/// By default `userAgent` is null.
final String? userAgent;

/// A Boolean value indicating whether the WebView should support zooming using its on-screen zoom controls and gestures.
///
/// By default 'zoomEnabled' is true
final bool zoomEnabled;

/// Which restrictions apply on automatic media playback.
///
/// This initial value is applied to the platform's webview upon creation. Any following
Expand Down Expand Up @@ -542,6 +548,7 @@ WebSettings _webSettingsFromWidget(WebView widget) {
gestureNavigationEnabled: widget.gestureNavigationEnabled,
allowsInlineMediaPlayback: widget.allowsInlineMediaPlayback,
userAgent: WebSetting<String?>.of(widget.userAgent),
zoomEnabled: widget.zoomEnabled,
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@ NS_ASSUME_NONNULL_BEGIN

@interface FLTWKNavigationDelegate : NSObject <WKNavigationDelegate>

- (instancetype)initWithChannel:(FlutterMethodChannel*)channel;
- (instancetype)initWithChannel:(FlutterMethodChannel *)channel;

/**
* Whether to delegate navigation decisions over the method channel.
*/
@property(nonatomic, assign) BOOL hasDartNavigationDelegate;

/**
* Whether to allow zoom functionality on the WebView.
*/
@property(nonatomic, assign) BOOL shouldEnableZoom;

@end

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,17 @@ - (void)webView:(WKWebView *)webView
}

- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
if (!self.shouldEnableZoom) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would happen if shouldEnableZoom was changed without the page reloading? Shouldn't this also be called for the current webpage along with every call to didFinishNavigation.

Copy link
Contributor Author

@NickalasB NickalasB Oct 7, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense but I'm not totally sure where you are suggesting I should also make this check? Inside decidePolicyForNavigationAction?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This workaround actually makes me question if this feature should even be supported by iOS directly. I'm leaning towards that a platform shouldn't support a feature from the platform interface unless it is directly supported by the API or there is a reasonable workaround. And I wouldn't consider running java script as a reasonable workaround.

@stuartmorgan Do we have a policy for this already?

cc @mvanbeusekom

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the WKWebView API doesn't provide a built-in way of toggling whether or not zoom is enabled. This workaround was intended to at least allow Flutter devs to toggle that on both Android and iOS. The original intention of this work was strictly to ENABLE zoom for Android as that functionality is currently missing. My background is in Android but in my research, I couldn't find a cleaner way of DISABLING zoom for iOS.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One other proposal, although it would require reverting #4404, is to just enable zooming for Android, and not make it togglable in the Flutter WebView. That would basically only require code-changes to webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java. Could just be me but it doesn't seem like there are many valid use-cases for disabling zoom... which is probably why the WKWebView API doesn't provide a way to do it. Again, appreciate the team helping move this forward so we can get off our fork.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And I wouldn't consider running java script as a reasonable workaround.

I can see this either way. I spent years working on a project that attempted to make UIWebView usable in a browser context though, and basically everything had to be done with JS in that case, so my judgement on this may well be skewed.

In this case, my take was:

  • The code being run here seems pretty benign. I.e., I would not expect it to have much likelihood of causing problems for pages.
  • It's only run if the plugin client explicitly turns off zoom. I would not expect that to be common, so problems would be relegated to edge cases of edge cases.

Given that my feeling is that it's reasonable to provide a best-effort implementation rather than nothing, as if someone is explicitly trying to turn on this behavior they presumably would want it to work on iOS too.

@stuartmorgan Do we have a policy for this already?

We don't have an explicit policy. There are obvious cases where we would say no, like use of private API on iOS, but this isn't doing anything like that, and using JS as a polyfill for OS web view limitations is something that definitely has a history of being relatively common on iOS, so I don't think this is a clear-cut case of "unreasonable".

Copy link
Contributor

@bparrishMines bparrishMines Oct 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense. Then I'm fine with the current implementation of the feature. I would just make it clear how the feature works on iOS in the followup PR for webview_flutter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you both!

NSString *source =
@"var meta = document.createElement('meta');"
@"meta.name = 'viewport';"
@"meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0,"
@"user-scalable=no';"
@"var head = document.getElementsByTagName('head')[0];head.appendChild(meta);";

[webView evaluateJavaScript:source completionHandler:nil];
}

[_methodChannel invokeMethod:@"onPageFinished" arguments:@{@"url" : webView.URL.absoluteString}];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,9 @@ - (NSString*)applySettings:(NSDictionary<NSString*, id>*)settings {
} else if ([key isEqualToString:@"userAgent"]) {
NSString* userAgent = settings[key];
[self updateUserAgent:[userAgent isEqual:[NSNull null]] ? nil : userAgent];
} else if ([key isEqualToString:@"zoomEnabled"]) {
NSNumber* zoomEnabled = settings[key];
_navigationDelegate.shouldEnableZoom = [zoomEnabled boolValue];
} else {
[unknownKeys addObject:key];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: webview_flutter_wkwebview
description: A Flutter plugin that provides a WebView widget based on Apple's WKWebView control.
repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter_wkwebview
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22
version: 2.0.14
version: 2.1.0

environment:
sdk: ">=2.14.0 <3.0.0"
Expand All @@ -18,8 +18,7 @@ flutter:
dependencies:
flutter:
sdk: flutter

webview_flutter_platform_interface: ^1.0.0
webview_flutter_platform_interface: ^1.2.0

dev_dependencies:
flutter_driver:
Expand Down