From a3ccf62e54545923276a3a43814a77fe7fc65a05 Mon Sep 17 00:00:00 2001 From: pedia Date: Sat, 16 Dec 2017 18:05:41 +0800 Subject: [PATCH] ios: remove event channel android: add rect, fullScreen, userAgent, eval --- .../FlutterWebviewPlugin.java | 169 ++++++++++++++++-- example/lib/main.dart | 9 +- ios/Classes/FlutterWebviewPlugin.m | 81 ++++----- lib/flutter_webview_plugin.dart | 94 ++++------ 4 files changed, 230 insertions(+), 123 deletions(-) diff --git a/android/src/main/java/com/flutter_webview_plugin/FlutterWebviewPlugin.java b/android/src/main/java/com/flutter_webview_plugin/FlutterWebviewPlugin.java index 5a19504a..0fba5d43 100644 --- a/android/src/main/java/com/flutter_webview_plugin/FlutterWebviewPlugin.java +++ b/android/src/main/java/com/flutter_webview_plugin/FlutterWebviewPlugin.java @@ -1,10 +1,21 @@ package com.flutter_webview_plugin; -import android.content.Intent; + import android.app.Activity; import android.content.Context; +import android.graphics.Bitmap; +import android.os.Build; +import android.view.ViewGroup; +import android.view.View; +import android.webkit.CookieManager; +import android.webkit.ValueCallback; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.FrameLayout; + +import java.util.HashMap; +import java.util.Map; -import io.flutter.app.FlutterActivity; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; @@ -15,13 +26,13 @@ */ public class FlutterWebviewPlugin implements MethodCallHandler { private Activity activity; + private WebView webView; public static MethodChannel channel; - private final int WEBVIEW_ACTIVITY_CODE = 1; private static final String CHANNEL_NAME = "flutter_webview_plugin"; public static void registerWith(PluginRegistry.Registrar registrar) { channel = new MethodChannel(registrar.messenger(), CHANNEL_NAME); - FlutterWebviewPlugin instance = new FlutterWebviewPlugin((Activity) registrar.activity()); + FlutterWebviewPlugin instance = new FlutterWebviewPlugin((Activity)registrar.activity()); channel.setMethodCallHandler(instance); } @@ -38,27 +49,163 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { case "close": close(call, result); break; + case "eval": + eval(call, result); + break; default: result.notImplemented(); break; } } + private void clearCookies() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + CookieManager.getInstance().removeAllCookies(new ValueCallback() { + @Override + public void onReceiveValue(Boolean aBoolean) { + + } + }); + } else { + CookieManager.getInstance().removeAllCookie(); + } + } + + private void clearCache() { + webView.clearCache(true); + webView.clearFormData(); + } + + private WebViewClient setWebViewClient() { + WebViewClient webViewClient = new BrowserClient(); + webView.setWebViewClient(webViewClient); + return webViewClient; + } + + private void eval(String code, final MethodChannel.Result result) { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { + webView.evaluateJavascript(code, new ValueCallback() { + @Override + public void onReceiveValue(String value) { + result.success(value); + } + }); + } else { + webView.loadUrl(code); + } + } + + // @Override + protected void onDestroy() { + FlutterWebviewPlugin.channel.invokeMethod("onDestroy", null); + } + + // @Override + public void onBackPressed() { + if(webView.canGoBack()){ + webView.goBack(); + return; + } + FlutterWebviewPlugin.channel.invokeMethod("onBackPressed", null); + } + + private static int dp2px(Context context, float dp) { + final float scale = context.getResources().getDisplayMetrics().density; + return (int) (dp * scale +0.5f); + } + private void openUrl(MethodCall call, MethodChannel.Result result) { - Intent intent = new Intent(activity, WebviewActivity.class); + if (webView == null) { + webView = new WebView(activity); - intent.putExtra(WebviewActivity.URL_KEY, (String) call.argument("url")); - intent.putExtra(WebviewActivity.WITH_JAVASCRIPT_KEY, (boolean) call.argument("withJavascript")); - intent.putExtra(WebviewActivity.CLEAR_CACHE_KEY, (boolean) call.argument("clearCache")); - intent.putExtra(WebviewActivity.CLEAR_COOKIES_KEY, (boolean) call.argument("clearCookies")); + Map rc = call.argument("rect"); + if (rc != null) { + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( + dp2px(activity, rc.get("width").intValue()), dp2px(activity, rc.get("height").intValue())); + params.setMargins(dp2px(activity, rc.get("left").intValue()), dp2px(activity, rc.get("top").intValue()), + 0, 0); + activity.addContentView(webView, params); + } + else if (!(boolean) call.argument("hidden")) { + activity.setContentView(webView); + } + + setWebViewClient(); + } - activity.startActivityForResult(intent, WEBVIEW_ACTIVITY_CODE); + webView.getSettings().setJavaScriptEnabled((boolean) call.argument("withJavascript")); + if ((boolean) call.argument("clearCache")) { + clearCache(); + } + + if ((boolean) call.argument("hidden")) { + webView.setVisibility(View.INVISIBLE); + } + + if ((boolean) call.argument("clearCookies")) { + clearCookies(); + } + + String userAgent = call.argument("userAgent"); + if (userAgent != null) { + webView.getSettings().setUserAgentString(userAgent); + } + + String url = (String) call.argument("url"); + webView.loadUrl(url); result.success(null); } private void close(MethodCall call, MethodChannel.Result result) { - activity.finishActivity(WEBVIEW_ACTIVITY_CODE); + ViewGroup vg = (ViewGroup)(webView.getParent()); + vg.removeView(webView); + webView = null; result.success(null); + + FlutterWebviewPlugin.channel.invokeMethod("onDestroy", null); + } + + private void eval(MethodCall call, final MethodChannel.Result result) { + String code = call.argument("code"); + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { + webView.evaluateJavascript(code, new ValueCallback() { + @Override + public void onReceiveValue(String value) { + result.success(value); + } + }); + } else { + // TODO: + webView.loadUrl(code); + } + } + + + private class BrowserClient extends WebViewClient { + private BrowserClient() { + super(); + } + + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + super.onPageStarted(view, url, favicon); + Map data = new HashMap<>(); + data.put("url", url); + FlutterWebviewPlugin.channel.invokeMethod("onUrlChanged", data); + + data.put("type", "startLoad"); + FlutterWebviewPlugin.channel.invokeMethod("onState", data); + } + + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + Map data = new HashMap<>(); + data.put("url", url); + data.put("type", "finishLoad"); + FlutterWebviewPlugin.channel.invokeMethod("onState", data); + } } } diff --git a/example/lib/main.dart b/example/lib/main.dart index 926031b6..23c18ef9 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -59,10 +59,11 @@ class _MyHomePageState extends State { initState() { super.initState(); - _onStateChanged = flutterWebviewPlugin.stateChanged.listen((dynamic state) { + _onStateChanged = + flutterWebviewPlugin.onStateChanged.listen((dynamic state) { if (mounted) { setState(() { - _history.add("stateChanged: $state"); + _history.add("state: $state"); }); } }); @@ -149,7 +150,9 @@ class _MyHomePageState extends State { ), new RaisedButton( onPressed: () { - _history.clear(); + setState(() { + _history.clear(); + }); flutterWebviewPlugin.close(); }, child: new Text("Close"), diff --git a/ios/Classes/FlutterWebviewPlugin.m b/ios/Classes/FlutterWebviewPlugin.m index d9989de2..51d2faf9 100644 --- a/ios/Classes/FlutterWebviewPlugin.m +++ b/ios/Classes/FlutterWebviewPlugin.m @@ -1,11 +1,9 @@ #import "FlutterWebviewPlugin.h" static NSString *const CHANNEL_NAME = @"flutter_webview_plugin"; -static NSString *const EVENT_CHANNEL_NAME = @"flutter_webview_plugin_event"; // UIWebViewDelegate -@interface FlutterWebviewPlugin() { - FlutterEventSink _eventSink; +@interface FlutterWebviewPlugin() { BOOL _enableAppScheme; } @end @@ -20,11 +18,6 @@ + (void)registerWithRegistrar:(NSObject*)registrar { FlutterWebviewPlugin* instance = [[FlutterWebviewPlugin alloc] initWithViewController:viewController]; [registrar addMethodCallDelegate:instance channel:channel]; - - FlutterEventChannel* event = - [FlutterEventChannel eventChannelWithName:EVENT_CHANNEL_NAME - binaryMessenger:[registrar messenger]]; - [event setStreamHandler:instance]; } - (instancetype)initWithViewController:(UIViewController *)viewController { @@ -62,34 +55,36 @@ - (void)initWebView:(FlutterMethodCall*)call { NSString *userAgent = call.arguments[@"userAgent"]; // - if ([clearCache boolValue]) { + if (clearCache != (id)[NSNull null] && [clearCache boolValue]) { [[NSURLCache sharedURLCache] removeAllCachedResponses]; } - if ([clearCookies boolValue]) { + if (clearCookies != (id)[NSNull null] && [clearCookies boolValue]) { [[NSURLSession sharedSession] resetWithCompletionHandler:^{ }]; } + if (userAgent != (id)[NSNull null]) { + [[NSUserDefaults standardUserDefaults] registerDefaults:@{@"UserAgent": userAgent}]; + } + CGRect rc; - if (rect) { + if (rect != nil) { rc = CGRectMake([[rect valueForKey:@"left"] doubleValue], - [[rect valueForKey:@"top"] doubleValue], - [[rect valueForKey:@"width"] doubleValue], - [[rect valueForKey:@"height"] doubleValue]); + [[rect valueForKey:@"top"] doubleValue], + [[rect valueForKey:@"width"] doubleValue], + [[rect valueForKey:@"height"] doubleValue]); } else { + // TODO: create top NavigatorController and push rc = self.viewController.view.bounds; } - if (userAgent) { - [[NSUserDefaults standardUserDefaults] registerDefaults:@{@"UserAgent": userAgent}]; - } - self.webview = [[UIWebView alloc] initWithFrame:rc]; self.webview.delegate = self; - if (!hidden || ![hidden boolValue]) - [self.viewController.view addSubview:self.webview]; + if (hidden != (id)[NSNull null] && [hidden boolValue]) + self.webview.hidden = YES; + [self.viewController.view addSubview:self.webview]; [self launch:call]; } @@ -113,16 +108,25 @@ - (void)closeWebView { [self.webview removeFromSuperview]; self.webview.delegate = nil; self.webview = nil; - [self sendEvent:@"destroy"]; + + // manually trigger onDestroy + [channel invokeMethod:@"onDestroy" arguments:nil]; } #pragma mark -- WebView Delegate - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { - NSArray *data = [NSArray arrayWithObjects:@"shouldStart", - request.URL.absoluteString, [NSNumber numberWithInt:navigationType], - nil]; - [self sendEvent:data]; + id data = @{@"url": request.URL.absoluteString, + @"type": @"shouldStart", + @"navigationType": [NSNumber numberWithInt:navigationType]}; + [channel invokeMethod:@"onState" arguments:data]; + + if (navigationType == UIWebViewNavigationTypeBackForward) + [channel invokeMethod:@"onBackPressed" arguments:nil]; + else { + id data = @{@"url": request.URL.absoluteString}; + [channel invokeMethod:@"onUrlChanged" arguments:data]; + } if (_enableAppScheme) return YES; @@ -134,40 +138,19 @@ - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *) } -(void)webViewDidStartLoad:(UIWebView *)webView { - [self sendEvent:@"startLoad"]; + [channel invokeMethod:@"onState" arguments:@{@"type": @"startLoad"}]; } - (void)webViewDidFinishLoad:(UIWebView *)webView { - [self sendEvent:@"finishLoad"]; + [channel invokeMethod:@"onState" arguments:@{@"type": @"finishLoad"}]; } - (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error { id data = [FlutterError errorWithCode:[NSString stringWithFormat:@"%ld", error.code] message:error.localizedDescription details:error.localizedFailureReason]; - [self sendEvent:data]; + [channel invokeMethod:@"onError" arguments:data]; } #pragma mark -- WkWebView Delegate - -#pragma mark -- FlutterStreamHandler impl - -- (FlutterError*)onListenWithArguments:(id)arguments eventSink:(FlutterEventSink)eventSink { - _eventSink = eventSink; - return nil; -} - -- (FlutterError*)onCancelWithArguments:(id)arguments { - [[NSNotificationCenter defaultCenter] removeObserver:self]; - _eventSink = nil; - return nil; -} - -- (void)sendEvent:(id)data { - // data should be @"" or [FlutterError] - if (!_eventSink) - return; - - _eventSink(data); -} @end diff --git a/lib/flutter_webview_plugin.dart b/lib/flutter_webview_plugin.dart index 890c9cac..be61dbea 100644 --- a/lib/flutter_webview_plugin.dart +++ b/lib/flutter_webview_plugin.dart @@ -4,57 +4,15 @@ import 'package:flutter/services.dart'; import 'package:flutter/material.dart'; const _kChannel = 'flutter_webview_plugin'; -const _kEvent = 'flutter_webview_plugin_event'; // TODO: more genral state for iOS/android -enum WebViewState { startLoad, finishLoad } - -// copy from UIWebView.h -enum _WebViewNavigateType { - TypeLinkClicked, - TypeFormSubmitted, - TypeBackForward, - TypeReload, - TypeFormResubmitted, - TypeOther -} +enum WebViewState { shouldStart, startLoad, finishLoad } /// Singleton Class that communicate with a fullscreen Webview Instance /// Have to be instanciate after `runApp` called. class FlutterWebViewPlugin { final MethodChannel _channel; - /// iOS WebView: Implemented - /// Android WebView: not implemented - final EventChannel _event; - Stream _stateChanged; - - Stream get stateChanged { - assert(_WebViewNavigateType.TypeLinkClicked.index == 0); - assert(_WebViewNavigateType.TypeOther.index == 5); - if (_stateChanged == null) { - _stateChanged = _event.receiveBroadcastStream(); - _stateChanged.listen((var result) { - // the list like: [state, url, navtype] - if (result is List && result.length == 3) { - if (_WebViewNavigateType.TypeBackForward.index == result[2]) { - _onBackPressed.add(Null); - } else if (_WebViewNavigateType.TypeOther.index == result[2] || - _WebViewNavigateType.TypeLinkClicked.index == result[2] || - _WebViewNavigateType.TypeFormSubmitted.index == result[2]) { - // TODO: find out better way - _onUrlChanged.add(result[1]); - } - } else if (result is String) { - if (result == "destroy") { - _onDestroy.add(Null); - } - } - }); - } - return _stateChanged; - } - final StreamController _onDestroy = new StreamController.broadcast(); final StreamController _onBackPressed = new StreamController.broadcast(); @@ -62,9 +20,12 @@ class FlutterWebViewPlugin { final StreamController _onUrlChanged = new StreamController.broadcast(); - FlutterWebViewPlugin() - : _channel = const MethodChannel(_kChannel), - _event = const EventChannel(_kEvent) { + final StreamController _onStateChanged = + new StreamController.broadcast(); + + final StreamController _onError = new StreamController.broadcast(); + + FlutterWebViewPlugin() : _channel = const MethodChannel(_kChannel) { _channel.setMethodCallHandler(_handleMessages); } @@ -79,19 +40,37 @@ class FlutterWebViewPlugin { case "onUrlChanged": _onUrlChanged.add(call.arguments["url"]); break; + case "onState": + _onStateChanged.add(call.arguments); + break; + case "onError": + _onError.add(call.arguments); + break; } } - ////////////////////// - /// Listening the OnDestroy LifeCycle Event for Android - /// + /// content is Map for url Stream get onDestroy => _onDestroy.stream; + /// Listening url changed + /// iOS WebView: worked + /// android: worked + Stream get onUrlChanged => _onUrlChanged.stream; + /// Listening the onBackPressed Event for Android - /// + /// content null + /// iOS WebView: worked + /// android: worked Stream get onBackPressed => _onBackPressed.stream; + /// Listening the onState Event for iOS WebView and Android + /// content is Map for type: {shouldStart|startLoad|finishLoad} + /// more detail than other events + /// iOS WebView: worked + /// android: Not for now. + Stream get onStateChanged => _onStateChanged.stream; + /// Start the Webview with [url] /// - [withJavascript] enable Javascript or not for the Webview /// iOS WebView: Not implemented yet @@ -106,19 +85,19 @@ class FlutterWebViewPlugin { /// android: Implemented /// - [hidden] not show /// iOS WebView: not shown(addSubView) in ViewController - /// android: Not implemented yet. + /// android: Implemented /// [fullScreen]: show in full screen mode, default true /// iOS WebView: without rect, show in full screen mode /// android: Implemented /// [rect]: show in rect(not full screen) /// iOS WebView: worked - /// android: Not implemented yet + /// android: Implemented /// [enableAppScheme]: false will enable all schemes, true only for httt/https/about /// iOS WebView: worked /// android: Not implemented yet /// [userAgent]: set the User-Agent of WebView /// iOS WebView: worked - /// android: Not implemented yet + /// android: Implemented Future launch(String url, {bool withJavascript: true, bool clearCache: false, @@ -142,7 +121,7 @@ class FlutterWebViewPlugin { if (rect != null) { args["rect"] = { "left": rect.left, - "right": rect.right, + "top": rect.top, "width": rect.width, "height": rect.height }; @@ -151,7 +130,7 @@ class FlutterWebViewPlugin { } /// iOS WebView: worked - /// android: Not implemented yet + /// android: implemented Future evalJavascript(String code) { return _channel.invokeMethod('eval', {"code": code}); } @@ -159,9 +138,4 @@ class FlutterWebViewPlugin { /// Close the Webview /// Will trigger the [onDestroy] event Future close() => _channel.invokeMethod("close"); - - /// Listening url changed - /// iOS WebView: worked - /// android: worked - Stream get onUrlChanged => _onUrlChanged.stream; }