diff --git a/.idea/libraries/Dart_Packages.xml b/.idea/libraries/Dart_Packages.xml index fbbebaa1..08cb7b47 100644 --- a/.idea/libraries/Dart_Packages.xml +++ b/.idea/libraries/Dart_Packages.xml @@ -133,7 +133,7 @@ - @@ -377,7 +377,7 @@ - @@ -441,7 +441,7 @@ - @@ -647,7 +647,7 @@ - + @@ -682,7 +682,7 @@ - + @@ -691,7 +691,7 @@ - + diff --git a/CHANGELOG.md b/CHANGELOG.md index 8310ff8a..5fb94eba 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 5.1.0 + +- Added support for pull-to-refresh feature [#395](https://github.com/pichillilorenzo/flutter_inappwebview/issues/395) +- Fixed issue not rendering WebView content when scrolling on iOS [#703](https://github.com/pichillilorenzo/flutter_inappwebview/issues/703) +- Fixed `InAppBrowser.openData` method +- `InAppBrowser.initialUserScripts`, `InAppBrowser.id`, `HeadlessInAppWebView.id` properties are `final` now + ## 5.0.5+3 - Fixed Android `evaluateJavascript` method when using `contentWorld: ContentWorld.PAGE` diff --git a/README.md b/README.md index ea5a507f..aa0334ef 100755 --- a/README.md +++ b/README.md @@ -302,15 +302,21 @@ Use `InAppWebViewController` to control the WebView instance. Example: ```dart import 'dart:async'; +import 'dart:collection'; +import 'dart:convert'; import 'dart:io'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:url_launcher/url_launcher.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); + if (Platform.isAndroid) { await AndroidInAppWebViewController.setWebContentsDebuggingEnabled(true); } + runApp(new MyApp()); } @@ -321,13 +327,42 @@ class MyApp extends StatefulWidget { class _MyAppState extends State { - InAppWebViewController? webView; + final GlobalKey webViewKey = GlobalKey(); + + InAppWebViewController? webViewController; + InAppWebViewGroupOptions options = InAppWebViewGroupOptions( + crossPlatform: InAppWebViewOptions( + mediaPlaybackRequiresUserGesture: false, + ), + android: AndroidInAppWebViewOptions( + useHybridComposition: true, + ), + ios: IOSInAppWebViewOptions( + allowsInlineMediaPlayback: true, + )); + + late PullToRefreshController pullToRefreshController; String url = ""; double progress = 0; + final urlController = TextEditingController(); @override void initState() { super.initState(); + + pullToRefreshController = PullToRefreshController( + options: PullToRefreshOptions( + color: Colors.blue, + ), + onRefresh: () async { + if (Platform.isAndroid) { + webViewController?.reload(); + } else if (Platform.isIOS) { + webViewController?.loadUrl( + urlRequest: URLRequest(url: await webViewController?.getUrl())); + } + }, + ); } @override @@ -339,87 +374,131 @@ class _MyAppState extends State { Widget build(BuildContext context) { return MaterialApp( home: Scaffold( - appBar: AppBar( - title: const Text('InAppWebView Example'), - ), - body: Container( - child: Column(children: [ - Container( - padding: EdgeInsets.all(20.0), - child: Text( - "CURRENT URL\n${(url.length > 50) ? url.substring(0, 50) + "..." : url}"), - ), - Container( - padding: EdgeInsets.all(10.0), - child: progress < 1.0 - ? LinearProgressIndicator(value: progress) - : Container()), - Expanded( - child: Container( - margin: const EdgeInsets.all(10.0), - decoration: - BoxDecoration(border: Border.all(color: Colors.blueAccent)), - child: InAppWebView( - initialUrlRequest: URLRequest( - url: Uri.parse("https://flutter.dev/") - ), - initialOptions: InAppWebViewGroupOptions( - crossPlatform: InAppWebViewOptions( + appBar: AppBar(title: Text("Official InAppWebView website")), + body: SafeArea( + child: Column(children: [ + TextField( + decoration: InputDecoration( + prefixIcon: Icon(Icons.search) + ), + controller: urlController, + keyboardType: TextInputType.url, + onSubmitted: (value) { + var url = Uri.parse(value); + if (url.scheme.isEmpty) { + url = Uri.parse("https://www.google.com/search?q=" + value); + } + webViewController?.loadUrl( + urlRequest: URLRequest(url: url)); + }, + ), + Expanded( + child: Stack( + children: [ + InAppWebView( + key: webViewKey, + initialUrlRequest: + URLRequest(url: Uri.parse("https://inappwebview.dev/")), + initialOptions: options, + pullToRefreshController: pullToRefreshController, + onWebViewCreated: (controller) { + webViewController = controller; + }, + onLoadStart: (controller, url) { + setState(() { + this.url = url.toString(); + urlController.text = this.url; + }); + }, + androidOnPermissionRequest: (InAppWebViewController controller, + String origin, List resources) async { + return PermissionRequestResponse( + resources: resources, + action: PermissionRequestResponseAction.GRANT); + }, + shouldOverrideUrlLoading: (controller, navigationAction) async { + var uri = navigationAction.request.url!; - ), - ios: IOSInAppWebViewOptions( + if (![ + "http", + "https", + "file", + "chrome", + "data", + "javascript", + "about" + ].contains(uri.scheme)) { + if (await canLaunch(url)) { + // Launch the App + await launch( + url, + ); + // and cancel the request + return NavigationActionPolicy.CANCEL; + } + } + return NavigationActionPolicy.ALLOW; + }, + onLoadStop: (controller, url) async { + pullToRefreshController.endRefreshing(); + setState(() { + this.url = url.toString(); + urlController.text = this.url; + }); + }, + onLoadError: (controller, url, code, message) { + pullToRefreshController.endRefreshing(); + }, + onProgressChanged: (controller, progress) { + if (progress == 100) { + pullToRefreshController.endRefreshing(); + } + setState(() { + this.progress = progress / 100; + urlController.text = this.url; + }); + }, + onUpdateVisitedHistory: (controller, url, androidIsReload) { + setState(() { + this.url = url.toString(); + urlController.text = this.url; + }); + }, + onConsoleMessage: (controller, consoleMessage) { + print(consoleMessage); + }, ), - android: AndroidInAppWebViewOptions( - useHybridComposition: true - ) - ), - onWebViewCreated: (InAppWebViewController controller) { - webView = controller; - }, - onLoadStart: (controller, url) { - setState(() { - this.url = url?.toString() ?? ''; - }); - }, - onLoadStop: (controller, url) async { - setState(() { - this.url = url?.toString() ?? ''; - }); - }, - onProgressChanged: (controller, progress) { - setState(() { - this.progress = progress / 100; - }); - }, + progress < 1.0 + ? LinearProgressIndicator(value: progress) + : Container(), + ], ), ), - ), - ButtonBar( - alignment: MainAxisAlignment.center, - children: [ - ElevatedButton( - child: Icon(Icons.arrow_back), - onPressed: () { - webView?.goBack(); - }, - ), - ElevatedButton( - child: Icon(Icons.arrow_forward), - onPressed: () { - webView?.goForward(); - }, - ), - ElevatedButton( - child: Icon(Icons.refresh), - onPressed: () { - webView?.reload(); - }, - ), - ], - ), - ])), - ), + ButtonBar( + alignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + child: Icon(Icons.arrow_back), + onPressed: () { + webViewController?.goBack(); + }, + ), + ElevatedButton( + child: Icon(Icons.arrow_forward), + onPressed: () { + webViewController?.goForward(); + }, + ), + ElevatedButton( + child: Icon(Icons.refresh), + onPressed: () { + webViewController?.reload(); + }, + ), + ], + ), + ]))), ); } } @@ -428,11 +507,11 @@ class _MyAppState extends State { Screenshots: - Android: -![android](https://user-images.githubusercontent.com/5956938/47271038-7aebda80-d574-11e8-98fd-41e6bbc9fe2d.gif) +![android](https://user-images.githubusercontent.com/5956938/110179602-a18ad300-7e08-11eb-849b-2c7f1af28155.gif) - iOS: -![ios](https://user-images.githubusercontent.com/5956938/54096363-e1e72000-43ab-11e9-85c2-983a830ab7a0.gif) +![ios](https://user-images.githubusercontent.com/5956938/110179614-a8194a80-7e08-11eb-85f9-3da10acbbcb2.gif) #### `InAppWebViewController` Methods diff --git a/android/build.gradle b/android/build.gradle index 43194fe3..bb894086 100755 --- a/android/build.gradle +++ b/android/build.gradle @@ -49,5 +49,6 @@ android { implementation 'androidx.browser:browser:1.2.0' implementation 'androidx.appcompat:appcompat:1.2.0-rc02' implementation 'com.squareup.okhttp3:mockwebserver:3.14.7' + implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' } } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserActivity.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserActivity.java index 69b36f3a..786826ef 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserActivity.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserActivity.java @@ -29,6 +29,8 @@ import com.pichillilorenzo.flutter_inappwebview.in_app_webview.InAppWebViewOptio import com.pichillilorenzo.flutter_inappwebview.InAppWebViewMethodHandler; import com.pichillilorenzo.flutter_inappwebview.R; import com.pichillilorenzo.flutter_inappwebview.Shared; +import com.pichillilorenzo.flutter_inappwebview.pull_to_refresh.PullToRefreshLayout; +import com.pichillilorenzo.flutter_inappwebview.pull_to_refresh.PullToRefreshOptions; import com.pichillilorenzo.flutter_inappwebview.types.URLRequest; import com.pichillilorenzo.flutter_inappwebview.types.UserScript; import com.pichillilorenzo.flutter_inappwebview.Util; @@ -48,6 +50,7 @@ public class InAppBrowserActivity extends AppCompatActivity implements InAppBrow public Integer windowId; public String id; public InAppWebView webView; + public PullToRefreshLayout pullToRefreshLayout; public ActionBar actionBar; public Menu menu; public SearchView searchView; @@ -74,6 +77,15 @@ public class InAppBrowserActivity extends AppCompatActivity implements InAppBrow setContentView(R.layout.activity_web_view); + Map pullToRefreshInitialOptions = (Map) b.getSerializable("pullToRefreshInitialOptions"); + MethodChannel pullToRefreshLayoutChannel = new MethodChannel(Shared.messenger, "com.pichillilorenzo/flutter_inappwebview_pull_to_refresh_" + id); + PullToRefreshOptions pullToRefreshOptions = new PullToRefreshOptions(); + pullToRefreshOptions.parse(pullToRefreshInitialOptions); + pullToRefreshLayout = findViewById(R.id.pullToRefresh); + pullToRefreshLayout.channel = pullToRefreshLayoutChannel; + pullToRefreshLayout.options = pullToRefreshOptions; + pullToRefreshLayout.prepare(); + webView = findViewById(R.id.webView); webView.windowId = windowId; webView.inAppBrowserDelegate = this; @@ -128,10 +140,10 @@ public class InAppBrowserActivity extends AppCompatActivity implements InAppBrow } } else if (initialData != null) { - String mimeType = b.getString("mimeType"); - String encoding = b.getString("encoding"); - String baseUrl = b.getString("baseUrl"); - String historyUrl = b.getString("historyUrl"); + String mimeType = b.getString("initialMimeType"); + String encoding = b.getString("initialEncoding"); + String baseUrl = b.getString("initialBaseUrl"); + String historyUrl = b.getString("initialHistoryUrl"); webView.loadDataWithBaseURL(baseUrl, initialData, mimeType, encoding, historyUrl); } else if (initialUrlRequest != null) { diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserManager.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserManager.java index b5c3a7f2..4e54c8ed 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserManager.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserManager.java @@ -62,44 +62,8 @@ public class InAppBrowserManager implements MethodChannel.MethodCallHandler { final Activity activity = Shared.activity; switch (call.method) { - case "openUrlRequest": - { - String id = (String) call.argument("id"); - Map urlRequest = (Map) call.argument("urlRequest"); - Map options = (Map) call.argument("options"); - Map contextMenu = (Map) call.argument("contextMenu"); - Integer windowId = (Integer) call.argument("windowId"); - List> initialUserScripts = (List>) call.argument("initialUserScripts"); - openUrlRequest(activity, id, urlRequest, options, contextMenu, windowId, initialUserScripts); - } - result.success(true); - break; - case "openFile": - { - String id = (String) call.argument("id"); - String assetFilePath = (String) call.argument("assetFilePath"); - Map options = (Map) call.argument("options"); - Map contextMenu = (Map) call.argument("contextMenu"); - Integer windowId = (Integer) call.argument("windowId"); - List> initialUserScripts = (List>) call.argument("initialUserScripts"); - openFile(activity, id, assetFilePath, options, contextMenu, windowId, initialUserScripts); - } - result.success(true); - break; - case "openData": - { - String id = (String) call.argument("id"); - Map options = (Map) call.argument("options"); - String data = (String) call.argument("data"); - String mimeType = (String) call.argument("mimeType"); - String encoding = (String) call.argument("encoding"); - String baseUrl = (String) call.argument("baseUrl"); - String historyUrl = (String) call.argument("historyUrl"); - Map contextMenu = (Map) call.argument("contextMenu"); - Integer windowId = (Integer) call.argument("windowId"); - List> initialUserScripts = (List>) call.argument("initialUserScripts"); - openData(activity, id, options, data, mimeType, encoding, baseUrl, historyUrl, contextMenu, windowId, initialUserScripts); - } + case "open": + open(activity, (Map) call.arguments()); result.success(true); break; case "openWithSystemBrowser": @@ -189,45 +153,36 @@ public class InAppBrowserManager implements MethodChannel.MethodCallHandler { } } - public void openUrlRequest(Activity activity, String id, Map urlRequest, Map options, - Map contextMenu, Integer windowId, List> initialUserScripts) { + public void open(Activity activity, Map arguments) { + String id = (String) arguments.get("id"); + Map urlRequest = (Map) arguments.get("urlRequest"); + String assetFilePath = (String) arguments.get("assetFilePath"); + String data = (String) arguments.get("data"); + String mimeType = (String) arguments.get("mimeType"); + String encoding = (String) arguments.get("encoding"); + String baseUrl = (String) arguments.get("baseUrl"); + String historyUrl = (String) arguments.get("historyUrl"); + Map options = (Map) arguments.get("options"); + Map contextMenu = (Map) arguments.get("contextMenu"); + Integer windowId = (Integer) arguments.get("windowId"); + List> initialUserScripts = (List>) arguments.get("initialUserScripts"); + Map pullToRefreshInitialOptions = (Map) arguments.get("pullToRefreshOptions"); + Bundle extras = new Bundle(); extras.putString("fromActivity", activity.getClass().getName()); extras.putSerializable("initialUrlRequest", (Serializable) urlRequest); - extras.putString("id", id); - extras.putSerializable("options", (Serializable) options); - extras.putSerializable("contextMenu", (Serializable) contextMenu); - extras.putInt("windowId", windowId != null ? windowId : -1); - extras.putSerializable("initialUserScripts", (Serializable) initialUserScripts); - startInAppBrowserActivity(activity, extras); - } - - public void openFile(Activity activity, String id, String assetFilePath, Map options, - Map contextMenu, Integer windowId, List> initialUserScripts) { - Bundle extras = new Bundle(); - extras.putString("fromActivity", activity.getClass().getName()); extras.putString("initialFile", assetFilePath); - extras.putString("id", id); - extras.putSerializable("options", (Serializable) options); - extras.putSerializable("contextMenu", (Serializable) contextMenu); - extras.putInt("windowId", windowId != null ? windowId : -1); - extras.putSerializable("initialUserScripts", (Serializable) initialUserScripts); - startInAppBrowserActivity(activity, extras); - } - - public void openData(Activity activity, String id, Map options, String data, String mimeType, String encoding, - String baseUrl, String historyUrl, Map contextMenu, Integer windowId, List> initialUserScripts) { - Bundle extras = new Bundle(); - extras.putString("id", id); - extras.putSerializable("options", (Serializable) options); extras.putString("initialData", data); extras.putString("initialMimeType", mimeType); extras.putString("initialEncoding", encoding); extras.putString("initialBaseUrl", baseUrl); extras.putString("initialHistoryUrl", historyUrl); + extras.putString("id", id); + extras.putSerializable("options", (Serializable) options); extras.putSerializable("contextMenu", (Serializable) contextMenu); extras.putInt("windowId", windowId != null ? windowId : -1); extras.putSerializable("initialUserScripts", (Serializable) initialUserScripts); + extras.putSerializable("pullToRefreshInitialOptions", (Serializable) pullToRefreshInitialOptions); startInAppBrowserActivity(activity, extras); } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserOptions.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserOptions.java index 4f113b34..b50d15b0 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserOptions.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserOptions.java @@ -15,7 +15,7 @@ public class InAppBrowserOptions implements Options { public Boolean hidden = false; public Boolean hideToolbarTop = false; @Nullable - public String toolbarTopBackgroundColor = null; + public String toolbarTopBackgroundColor; @Nullable public String toolbarTopFixedTitle; public Boolean hideUrlBar = false; diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/ContextMenuOptions.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/ContextMenuOptions.java index 9736643f..31946245 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/ContextMenuOptions.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/ContextMenuOptions.java @@ -35,7 +35,7 @@ public class ContextMenuOptions implements Options { } @Override - public Map getRealOptions(Object webView) { + public Map getRealOptions(Object obj) { Map realOptions = toMap(); return realOptions; } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/FlutterWebView.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/FlutterWebView.java index 5ee0019a..da5d9789 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/FlutterWebView.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/FlutterWebView.java @@ -16,9 +16,10 @@ import androidx.webkit.WebViewFeature; import com.pichillilorenzo.flutter_inappwebview.InAppWebViewMethodHandler; import com.pichillilorenzo.flutter_inappwebview.Shared; +import com.pichillilorenzo.flutter_inappwebview.pull_to_refresh.PullToRefreshLayout; +import com.pichillilorenzo.flutter_inappwebview.pull_to_refresh.PullToRefreshOptions; import com.pichillilorenzo.flutter_inappwebview.types.URLRequest; import com.pichillilorenzo.flutter_inappwebview.types.UserScript; -import com.pichillilorenzo.flutter_inappwebview.Util; import com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js.JavaScriptBridgeJS; import java.io.IOException; @@ -38,6 +39,7 @@ public class FlutterWebView implements PlatformView { public InAppWebView webView; public final MethodChannel channel; public InAppWebViewMethodHandler methodCallDelegate; + public PullToRefreshLayout pullToRefreshLayout; public FlutterWebView(BinaryMessenger messenger, final Context context, Object id, HashMap params, View containerView) { channel = new MethodChannel(messenger, "com.pichillilorenzo/flutter_inappwebview_" + id); @@ -53,6 +55,7 @@ public class FlutterWebView implements PlatformView { Map contextMenu = (Map) params.get("contextMenu"); Integer windowId = (Integer) params.get("windowId"); List> initialUserScripts = (List>) params.get("initialUserScripts"); + Map pullToRefreshInitialOptions = (Map) params.get("pullToRefreshOptions"); InAppWebViewOptions options = new InAppWebViewOptions(); options.parse(initialOptions); @@ -74,6 +77,13 @@ public class FlutterWebView implements PlatformView { webView = new InAppWebView(context, channel, id, windowId, options, contextMenu, containerView, userScripts); displayListenerProxy.onPostWebViewInitialization(displayManager); + MethodChannel pullToRefreshLayoutChannel = new MethodChannel(messenger, "com.pichillilorenzo/flutter_inappwebview_pull_to_refresh_" + id); + PullToRefreshOptions pullToRefreshOptions = new PullToRefreshOptions(); + pullToRefreshOptions.parse(pullToRefreshInitialOptions); + pullToRefreshLayout = new PullToRefreshLayout(context, pullToRefreshLayoutChannel, pullToRefreshOptions); + pullToRefreshLayout.addView(webView); + pullToRefreshLayout.prepare(); + methodCallDelegate = new InAppWebViewMethodHandler(webView); channel.setMethodCallHandler(methodCallDelegate); @@ -117,7 +127,7 @@ public class FlutterWebView implements PlatformView { @Override public View getView() { - return webView; + return pullToRefreshLayout; } @Override @@ -145,6 +155,11 @@ public class FlutterWebView implements PlatformView { webView.dispose(); webView.destroy(); webView = null; + + if (pullToRefreshLayout != null) { + pullToRefreshLayout.dispose(); + pullToRefreshLayout = null; + } } }); WebSettings settings = webView.getSettings(); diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebViewChromeClient.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebViewChromeClient.java index 73e1ce38..39ce9ce9 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebViewChromeClient.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebViewChromeClient.java @@ -175,7 +175,7 @@ public class InAppWebViewChromeClient extends WebChromeClient implements PluginR channel.invokeMethod("onJsAlert", obj, new MethodChannel.Result() { @Override - public void success(Object response) { + public void success(@Nullable Object response) { String responseMessage = null; String confirmButtonTitle = null; @@ -203,8 +203,8 @@ public class InAppWebViewChromeClient extends WebChromeClient implements PluginR } @Override - public void error(String s, String s1, Object o) { - Log.e(LOG_TAG, s + ", " + s1); + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); result.cancel(); } @@ -290,8 +290,8 @@ public class InAppWebViewChromeClient extends WebChromeClient implements PluginR } @Override - public void error(String s, String s1, Object o) { - Log.e(LOG_TAG, s + ", " + s1); + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); result.cancel(); } @@ -393,8 +393,8 @@ public class InAppWebViewChromeClient extends WebChromeClient implements PluginR } @Override - public void error(String s, String s1, Object o) { - Log.e(LOG_TAG, s + ", " + s1); + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); result.cancel(); } @@ -507,8 +507,8 @@ public class InAppWebViewChromeClient extends WebChromeClient implements PluginR } @Override - public void error(String s, String s1, Object o) { - Log.e(LOG_TAG, s + ", " + s1); + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); result.cancel(); } @@ -599,6 +599,7 @@ public class InAppWebViewChromeClient extends WebChromeClient implements PluginR @Override public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); if (InAppWebViewChromeClient.windowWebViewMessages.containsKey(windowId)) { InAppWebViewChromeClient.windowWebViewMessages.remove(windowId); } @@ -638,7 +639,8 @@ public class InAppWebViewChromeClient extends WebChromeClient implements PluginR } @Override - public void error(String s, String s1, Object o) { + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); callback.invoke(origin, false, false); } @@ -1124,8 +1126,9 @@ public class InAppWebViewChromeClient extends WebChromeClient implements PluginR } @Override - public void error(String s, String s1, Object o) { - Log.e(LOG_TAG, s + ", " + s1); + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + request.deny(); } @Override diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebViewClient.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebViewClient.java index 455d9358..2cb50139 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebViewClient.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebViewClient.java @@ -150,8 +150,8 @@ public class InAppWebViewClient extends WebViewClient { } @Override - public void error(String s, String s1, Object o) { - Log.e(LOG_TAG, "ERROR: " + s + " " + s1); + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); allowShouldOverrideUrlLoading(webView, url, headers, isForMainFrame); } @@ -375,8 +375,8 @@ public class InAppWebViewClient extends WebViewClient { } @Override - public void error(String s, String s1, Object o) { - Log.e(LOG_TAG, s + ", " + s1); + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); } @Override @@ -428,8 +428,8 @@ public class InAppWebViewClient extends WebViewClient { } @Override - public void error(String s, String s1, Object o) { - Log.e(LOG_TAG, s + ", " + s1); + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); } @Override @@ -493,8 +493,8 @@ public class InAppWebViewClient extends WebViewClient { } @Override - public void error(String s, String s1, Object o) { - Log.e(LOG_TAG, s + ", " + s1); + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); } @Override @@ -553,8 +553,8 @@ public class InAppWebViewClient extends WebViewClient { } @Override - public void error(String s, String s1, Object o) { - Log.e(LOG_TAG, s + ", " + s1); + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); } @Override @@ -736,7 +736,7 @@ public class InAppWebViewClient extends WebViewClient { @Override public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { - Log.d(LOG_TAG, "ERROR: " + errorCode + " " + errorMessage); + Log.e(LOG_TAG, "ERROR: " + errorCode + " " + errorMessage); } @Override diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebViewRenderProcessClient.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebViewRenderProcessClient.java index 5dcf80db..59a30cff 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebViewRenderProcessClient.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebViewRenderProcessClient.java @@ -47,7 +47,7 @@ public class InAppWebViewRenderProcessClient extends WebViewRenderProcessClient @Override public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { - Log.d(LOG_TAG, "ERROR: " + errorCode + " " + errorMessage); + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); } @Override @@ -79,7 +79,7 @@ public class InAppWebViewRenderProcessClient extends WebViewRenderProcessClient @Override public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { - Log.d(LOG_TAG, "ERROR: " + errorCode + " " + errorMessage); + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); } @Override diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/pull_to_refresh/PullToRefreshLayout.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/pull_to_refresh/PullToRefreshLayout.java new file mode 100644 index 00000000..cec3aa62 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/pull_to_refresh/PullToRefreshLayout.java @@ -0,0 +1,132 @@ +package com.pichillilorenzo.flutter_inappwebview.pull_to_refresh; + +import android.content.Context; +import android.graphics.Color; +import android.util.AttributeSet; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import java.util.HashMap; +import java.util.Map; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +public class PullToRefreshLayout extends SwipeRefreshLayout implements MethodChannel.MethodCallHandler { + static final String LOG_TAG = "PullToRefreshLayout"; + + public MethodChannel channel; + public PullToRefreshOptions options; + + public PullToRefreshLayout(@NonNull Context context, @NonNull MethodChannel channel, @NonNull PullToRefreshOptions options) { + super(context); + this.channel = channel; + this.options = options; + } + + public PullToRefreshLayout(@NonNull Context context) { + super(context); + this.channel = null; + this.options = null; + } + + public PullToRefreshLayout(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + this.channel = null; + this.options = null; + } + + public void prepare() { + final PullToRefreshLayout self = this; + + if (channel != null) { + this.channel.setMethodCallHandler(this); + } + + setEnabled(options.enabled); + setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { + @Override + public void onRefresh() { + if (channel == null) { + self.setRefreshing(false); + return; + } + Map obj = new HashMap<>(); + channel.invokeMethod("onRefresh", obj); + } + }); + if (options.color != null) + setColorSchemeColors(Color.parseColor(options.color)); + if (options.backgroundColor != null) + setProgressBackgroundColorSchemeColor(Color.parseColor(options.backgroundColor)); + if (options.distanceToTriggerSync != null) + setDistanceToTriggerSync(options.distanceToTriggerSync); + if (options.slingshotDistance != null) + setSlingshotDistance(options.slingshotDistance); + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull final MethodChannel.Result result) { + switch (call.method) { + case "setEnabled": + { + Boolean enabled = (Boolean) call.argument("enabled"); + setEnabled(enabled); + } + result.success(true); + break; + case "setRefreshing": + { + Boolean refreshing = (Boolean) call.argument("refreshing"); + setRefreshing(refreshing); + } + result.success(true); + break; + case "isRefreshing": + result.success(isRefreshing()); + break; + case "setColor": + { + String color = (String) call.argument("color"); + setColorSchemeColors(Color.parseColor(color)); + } + result.success(true); + break; + case "setBackgroundColor": + { + String color = (String) call.argument("color"); + setProgressBackgroundColorSchemeColor(Color.parseColor(color)); + } + result.success(true); + break; + case "setDistanceToTriggerSync": + { + Integer distanceToTriggerSync = (Integer) call.argument("distanceToTriggerSync"); + setDistanceToTriggerSync(distanceToTriggerSync); + } + result.success(true); + break; + case "setSlingshotDistance": + { + Integer slingshotDistance = (Integer) call.argument("slingshotDistance"); + setSlingshotDistance(slingshotDistance); + } + result.success(true); + break; + case "getDefaultSlingshotDistance": + result.success(SwipeRefreshLayout.DEFAULT_SLINGSHOT_DISTANCE); + break; + default: + result.notImplemented(); + } + } + + public void dispose() { + removeAllViews(); + if (channel != null) { + channel.setMethodCallHandler(null); + } + } +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/pull_to_refresh/PullToRefreshOptions.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/pull_to_refresh/PullToRefreshOptions.java new file mode 100644 index 00000000..ac213071 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/pull_to_refresh/PullToRefreshOptions.java @@ -0,0 +1,69 @@ +package com.pichillilorenzo.flutter_inappwebview.pull_to_refresh; + +import androidx.annotation.Nullable; + +import com.pichillilorenzo.flutter_inappwebview.Options; + +import java.util.HashMap; +import java.util.Map; + +public class PullToRefreshOptions implements Options { + public static final String LOG_TAG = "PullToRefreshOptions"; + + public Boolean enabled = true; + @Nullable + public String color; + @Nullable + public String backgroundColor; + @Nullable + public Integer distanceToTriggerSync; + @Nullable + public Integer slingshotDistance; + + public PullToRefreshOptions parse(Map options) { + for (Map.Entry pair : options.entrySet()) { + String key = pair.getKey(); + Object value = pair.getValue(); + if (value == null) { + continue; + } + + switch (key) { + case "enabled": + enabled = (Boolean) value; + break; + case "color": + color = (String) value; + break; + case "backgroundColor": + backgroundColor = (String) value; + break; + case "distanceToTriggerSync": + distanceToTriggerSync = (Integer) value; + break; + case "slingshotDistance": + slingshotDistance = (Integer) value; + break; + } + } + + return this; + } + + public Map toMap() { + Map options = new HashMap<>(); + options.put("enabled", enabled); + options.put("color", color); + options.put("backgroundColor", backgroundColor); + options.put("distanceToTriggerSync", distanceToTriggerSync); + options.put("slingshotDistance", slingshotDistance); + return options; + } + + @Override + public Map getRealOptions(PullToRefreshLayout pullToRefreshLayout) { + Map realOptions = toMap(); + return realOptions; + } + +} \ No newline at end of file diff --git a/android/src/main/res/layout/activity_web_view.xml b/android/src/main/res/layout/activity_web_view.xml index 9246e66e..04d0d54d 100755 --- a/android/src/main/res/layout/activity_web_view.xml +++ b/android/src/main/res/layout/activity_web_view.xml @@ -10,10 +10,15 @@ tools:context=".in_app_browser.InAppBrowserActivity" android:focusable="true"> - + android:layout_height="match_parent"> + + (); + final pullToRefreshController = PullToRefreshController( + options: PullToRefreshOptions( + color: Colors.blue, + ), + onRefresh: () { + + }, + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: InAppWebView( + key: GlobalKey(), + initialUrlRequest: URLRequest(url: Uri.parse('https://github.com/flutter')), + pullToRefreshController: pullToRefreshController, + onWebViewCreated: (controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + final InAppWebViewController controller = + await controllerCompleter.future; + final String? currentUrl = (await controller.getUrl())?.toString(); + expect(currentUrl, 'https://github.com/flutter'); + }); + group('android methods', () { testWidgets('clearSslPreferences', (WidgetTester tester) async { final Completer controllerCompleter = @@ -4462,7 +4495,7 @@ setTimeout(function() { child: InAppWebView( key: GlobalKey(), initialUrlRequest: - URLRequest(url: Uri.parse('https://flutter.dev')), + URLRequest(url: Uri.parse('https://github.com/flutter')), onWebViewCreated: (controller) { controllerCompleter.complete(controller); }, @@ -4477,7 +4510,7 @@ setTimeout(function() { await controllerCompleter.future; await pageLoaded.future; var originUrl = (await controller.android.getOriginalUrl())?.toString(); - expect(originUrl, 'https://flutter.dev/'); + expect(originUrl, 'https://github.com/flutter'); }, skip: !Platform.isAndroid); testWidgets('pageDown/pageUp', (WidgetTester tester) async { @@ -4759,7 +4792,7 @@ setTimeout(function() { final Completer pageLoaded = Completer(); var headlessWebView = new HeadlessInAppWebView( - initialUrlRequest: URLRequest(url: Uri.parse("https://flutter.dev")), + initialUrlRequest: URLRequest(url: Uri.parse("https://github.com/flutter")), onWebViewCreated: (controller) { controllerCompleter.complete(controller); }, @@ -4774,7 +4807,7 @@ setTimeout(function() { await pageLoaded.future; final String? url = (await controller.getUrl())?.toString(); - expect(url, 'https://flutter.dev/'); + expect(url, 'https://github.com/flutter'); await headlessWebView.dispose(); @@ -4787,7 +4820,7 @@ setTimeout(function() { final Completer pageLoaded = Completer(); var headlessWebView = new HeadlessInAppWebView( - initialUrlRequest: URLRequest(url: Uri.parse("https://flutter.dev")), + initialUrlRequest: URLRequest(url: Uri.parse("https://github.com/flutter")), initialOptions: InAppWebViewGroupOptions( crossPlatform: InAppWebViewOptions(javaScriptEnabled: false)), onWebViewCreated: (controller) { @@ -4818,7 +4851,7 @@ setTimeout(function() { }); group('InAppBrowser', () { - test('open and close', () async { + test('openUrlRequest and close', () async { var inAppBrowser = new MyInAppBrowser(); expect(inAppBrowser.isOpened(), false); expect(() async { @@ -4826,13 +4859,87 @@ setTimeout(function() { }, throwsA(isInstanceOf())); await inAppBrowser.openUrlRequest( - urlRequest: URLRequest(url: Uri.parse("https://flutter.dev"))); + urlRequest: URLRequest(url: Uri.parse("https://github.com/flutter"))); await inAppBrowser.browserCreated.future; expect(inAppBrowser.isOpened(), true); expect(() async { await inAppBrowser.openUrlRequest( urlRequest: - URLRequest(url: Uri.parse("https://github.com/flutter"))); + URLRequest(url: Uri.parse("https://flutter.dev"))); + }, throwsA(isInstanceOf())); + + await inAppBrowser.firstPageLoaded.future; + var controller = inAppBrowser.webViewController; + + final String? url = (await controller.getUrl())?.toString(); + expect(url, 'https://github.com/flutter'); + + await inAppBrowser.close(); + expect(inAppBrowser.isOpened(), false); + expect(() async => await inAppBrowser.webViewController.getUrl(), + throwsA(isInstanceOf())); + }); + + test('openFile and close', () async { + var inAppBrowser = new MyInAppBrowser(); + expect(inAppBrowser.isOpened(), false); + expect(() async { + await inAppBrowser.show(); + }, throwsA(isInstanceOf())); + + await inAppBrowser.openFile(assetFilePath: "test_assets/in_app_webview_initial_file_test.html"); + await inAppBrowser.browserCreated.future; + expect(inAppBrowser.isOpened(), true); + expect(() async { + await inAppBrowser.openUrlRequest( + urlRequest: + URLRequest(url: Uri.parse("https://github.com/flutter"))); + }, throwsA(isInstanceOf())); + + await inAppBrowser.firstPageLoaded.future; + var controller = inAppBrowser.webViewController; + + final String? url = (await controller.getUrl())?.toString(); + expect(url, endsWith("in_app_webview_initial_file_test.html")); + + await inAppBrowser.close(); + expect(inAppBrowser.isOpened(), false); + expect(() async => await inAppBrowser.webViewController.getUrl(), + throwsA(isInstanceOf())); + }); + + test('openFile and close', () async { + var inAppBrowser = new MyInAppBrowser(); + expect(inAppBrowser.isOpened(), false); + expect(() async { + await inAppBrowser.show(); + }, throwsA(isInstanceOf())); + + await inAppBrowser.openData(data: """ + + + + + + + + + + + placeholder 100x50 + + +""", + encoding: 'utf-8', + mimeType: 'text/html', + androidHistoryUrl: Uri.parse("https://flutter.dev"), + baseUrl: Uri.parse("https://flutter.dev")); + await inAppBrowser.browserCreated.future; + expect(inAppBrowser.isOpened(), true); + expect(() async { + await inAppBrowser.openUrlRequest( + urlRequest: + URLRequest(url: Uri.parse("https://github.com/flutter"))); }, throwsA(isInstanceOf())); await inAppBrowser.firstPageLoaded.future; @@ -4850,7 +4957,7 @@ setTimeout(function() { test('set/get options', () async { var inAppBrowser = new MyInAppBrowser(); await inAppBrowser.openUrlRequest( - urlRequest: URLRequest(url: Uri.parse("https://flutter.dev")), + urlRequest: URLRequest(url: Uri.parse("https://github.com/flutter")), options: InAppBrowserClassOptions( crossPlatform: InAppBrowserOptions(hideToolbarTop: true))); await inAppBrowser.browserCreated.future; @@ -4875,12 +4982,12 @@ setTimeout(function() { var chromeSafariBrowser = new MyChromeSafariBrowser(); expect(chromeSafariBrowser.isOpened(), false); - await chromeSafariBrowser.open(url: Uri.parse("https://flutter.dev")); + await chromeSafariBrowser.open(url: Uri.parse("https://github.com/flutter")); await chromeSafariBrowser.browserCreated.future; expect(chromeSafariBrowser.isOpened(), true); expect(() async { await chromeSafariBrowser.open( - url: Uri.parse("https://github.com/flutter")); + url: Uri.parse("https://flutter.dev")); }, throwsA(isInstanceOf())); await expectLater(chromeSafariBrowser.firstPageLoaded.future, completes); diff --git a/example/lib/in_app_browser_example.screen.dart b/example/lib/in_app_browser_example.screen.dart index f0105008..f8e8e7eb 100755 --- a/example/lib/in_app_browser_example.screen.dart +++ b/example/lib/in_app_browser_example.screen.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:collection'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; @@ -23,16 +24,21 @@ class MyInAppBrowser extends InAppBrowser { @override Future onLoadStop(url) async { + pullToRefreshController?.endRefreshing(); print("\n\nStopped $url\n\n"); } @override void onLoadError(url, code, message) { + pullToRefreshController?.endRefreshing(); print("Can't load $url.. Error: $message"); } @override void onProgressChanged(progress) { + if (progress == 100) { + pullToRefreshController?.endRefreshing(); + } print("Progress: $progress"); } @@ -77,9 +83,27 @@ class InAppBrowserExampleScreen extends StatefulWidget { } class _InAppBrowserExampleScreenState extends State { + + late PullToRefreshController pullToRefreshController; + @override void initState() { super.initState(); + + pullToRefreshController = PullToRefreshController( + options: PullToRefreshOptions( + color: Colors.black, + ), + onRefresh: () async { + if (Platform.isAndroid) { + widget.browser.webViewController.reload(); + } else if (Platform.isIOS) { + widget.browser.webViewController.loadUrl( + urlRequest: URLRequest(url: await widget.browser.webViewController.getUrl())); + } + }, + ); + widget.browser.pullToRefreshController = pullToRefreshController; } @override diff --git a/example/lib/in_app_webiew_example.screen.dart b/example/lib/in_app_webiew_example.screen.dart index 3d3b473a..e2a36130 100755 --- a/example/lib/in_app_webiew_example.screen.dart +++ b/example/lib/in_app_webiew_example.screen.dart @@ -17,13 +17,26 @@ class InAppWebViewExampleScreen extends StatefulWidget { } class _InAppWebViewExampleScreenState extends State { + final GlobalKey webViewKey = GlobalKey(); - InAppWebViewController? webView; + InAppWebViewController? webViewController; + InAppWebViewGroupOptions options = InAppWebViewGroupOptions( + crossPlatform: InAppWebViewOptions( + mediaPlaybackRequiresUserGesture: false, + ), + android: AndroidInAppWebViewOptions( + useHybridComposition: true, + ), + ios: IOSInAppWebViewOptions( + allowsInlineMediaPlayback: true, + )); + + late PullToRefreshController pullToRefreshController; late ContextMenu contextMenu; String url = ""; double progress = 0; - // CookieManager _cookieManager = CookieManager.instance(); + final urlController = TextEditingController(); @override void initState() { @@ -37,15 +50,15 @@ class _InAppWebViewExampleScreenState extends State { title: "Special", action: () async { print("Menu item Special clicked!"); - print(await webView?.getSelectedText()); - await webView?.clearFocus(); + print(await webViewController?.getSelectedText()); + await webViewController?.clearFocus(); }) ], options: ContextMenuOptions(hideDefaultSystemContextMenuItems: false), onCreateContextMenu: (hitTestResult) async { print("onCreateContextMenu"); print(hitTestResult.extra); - print(await webView?.getSelectedText()); + print(await webViewController?.getSelectedText()); }, onHideContextMenu: () { print("onHideContextMenu"); @@ -59,6 +72,20 @@ class _InAppWebViewExampleScreenState extends State { " " + contextMenuItemClicked.title); }); + + pullToRefreshController = PullToRefreshController( + options: PullToRefreshOptions( + color: Colors.blue, + ), + onRefresh: () async { + if (Platform.isAndroid) { + webViewController?.reload(); + } else if (Platform.isIOS) { + webViewController?.loadUrl( + urlRequest: URLRequest(url: await webViewController?.getUrl())); + } + }, + ); } @override @@ -66,18 +93,6 @@ class _InAppWebViewExampleScreenState extends State { super.dispose(); } - var options = InAppWebViewGroupOptions( - crossPlatform: InAppWebViewOptions( - useShouldOverrideUrlLoading: false, - mediaPlaybackRequiresUserGesture: false, - ), - android: AndroidInAppWebViewOptions( - useHybridComposition: true, - ), - ios: IOSInAppWebViewOptions( - allowsInlineMediaPlayback: true, - )); - @override Widget build(BuildContext context) { return Scaffold( @@ -85,92 +100,106 @@ class _InAppWebViewExampleScreenState extends State { drawer: myDrawer(context: context), body: SafeArea( child: Column(children: [ - Container( - padding: EdgeInsets.all(20.0), - child: Text( - "CURRENT URL\n${(url.length > 50) ? url.substring(0, 50) + "..." : url}"), - ), - Container( - padding: EdgeInsets.all(10.0), - child: progress < 1.0 - ? LinearProgressIndicator(value: progress) - : Container()), - Expanded( - child: Container( - margin: const EdgeInsets.all(10.0), - decoration: - BoxDecoration(border: Border.all(color: Colors.blueAccent)), - child: InAppWebView( - key: webViewKey, - // contextMenu: contextMenu, - initialUrlRequest: - URLRequest(url: Uri.parse("https://flutter.dev")), - // initialFile: "assets/index.html", - initialUserScripts: UnmodifiableListView([]), - initialOptions: options, - onWebViewCreated: (controller) { - webView = controller; - print("onWebViewCreated"); - }, - onLoadStart: (controller, url) { - print("onLoadStart $url"); - setState(() { - this.url = url.toString(); - }); - }, - androidOnPermissionRequest: (InAppWebViewController controller, - String origin, List resources) async { - return PermissionRequestResponse( - resources: resources, - action: PermissionRequestResponseAction.GRANT); - }, - shouldOverrideUrlLoading: (controller, navigationAction) async { - var uri = navigationAction.request.url!; - - if (![ - "http", - "https", - "file", - "chrome", - "data", - "javascript", - "about" - ].contains(uri.scheme)) { - if (await canLaunch(url)) { - // Launch the App - await launch( - url, - ); - // and cancel the request - return NavigationActionPolicy.CANCEL; - } - } - - return NavigationActionPolicy.ALLOW; - }, - onLoadStop: (controller, url) async { - print("onLoadStop $url"); - setState(() { - this.url = url.toString(); - }); - webView = controller; - }, - onProgressChanged: (controller, progress) { - setState(() { - this.progress = progress / 100; - }); - }, - onUpdateVisitedHistory: (controller, url, androidIsReload) { - print("onUpdateVisitedHistory $url"); - setState(() { - this.url = url.toString(); - }); - }, - onConsoleMessage: (controller, consoleMessage) { - print(consoleMessage); - }, - ), + TextField( + decoration: InputDecoration( + prefixIcon: Icon(Icons.search) ), + controller: urlController, + keyboardType: TextInputType.url, + onSubmitted: (value) { + var url = Uri.parse(value); + if (url.scheme.isEmpty) { + url = Uri.parse("https://www.google.com/search?q=" + value); + } + webViewController?.loadUrl( + urlRequest: URLRequest(url: url)); + }, + ), + Expanded( + child: Stack( + children: [ + InAppWebView( + key: webViewKey, + // contextMenu: contextMenu, + initialUrlRequest: + URLRequest(url: Uri.parse("https://github.com/flutter")), + // initialFile: "assets/index.html", + initialUserScripts: UnmodifiableListView([]), + initialOptions: options, + pullToRefreshController: pullToRefreshController, + onWebViewCreated: (controller) { + webViewController = controller; + }, + onLoadStart: (controller, url) { + setState(() { + this.url = url.toString(); + urlController.text = this.url; + }); + }, + androidOnPermissionRequest: (InAppWebViewController controller, + String origin, List resources) async { + return PermissionRequestResponse( + resources: resources, + action: PermissionRequestResponseAction.GRANT); + }, + shouldOverrideUrlLoading: (controller, navigationAction) async { + var uri = navigationAction.request.url!; + + if (![ + "http", + "https", + "file", + "chrome", + "data", + "javascript", + "about" + ].contains(uri.scheme)) { + if (await canLaunch(url)) { + // Launch the App + await launch( + url, + ); + // and cancel the request + return NavigationActionPolicy.CANCEL; + } + } + + return NavigationActionPolicy.ALLOW; + }, + onLoadStop: (controller, url) async { + pullToRefreshController.endRefreshing(); + setState(() { + this.url = url.toString(); + urlController.text = this.url; + }); + }, + onLoadError: (controller, url, code, message) { + pullToRefreshController.endRefreshing(); + }, + onProgressChanged: (controller, progress) { + if (progress == 100) { + pullToRefreshController.endRefreshing(); + } + setState(() { + this.progress = progress / 100; + urlController.text = this.url; + }); + }, + onUpdateVisitedHistory: (controller, url, androidIsReload) { + setState(() { + this.url = url.toString(); + urlController.text = this.url; + }); + }, + onConsoleMessage: (controller, consoleMessage) { + print(consoleMessage); + }, + ), + progress < 1.0 + ? LinearProgressIndicator(value: progress) + : Container(), + ], + ), ), ButtonBar( alignment: MainAxisAlignment.center, @@ -178,19 +207,19 @@ class _InAppWebViewExampleScreenState extends State { ElevatedButton( child: Icon(Icons.arrow_back), onPressed: () { - webView?.goBack(); + webViewController?.goBack(); }, ), ElevatedButton( child: Icon(Icons.arrow_forward), onPressed: () { - webView?.goForward(); + webViewController?.goForward(); }, ), ElevatedButton( child: Icon(Icons.refresh), onPressed: () { - webView?.reload(); + webViewController?.reload(); }, ), ], diff --git a/example/pubspec.yaml b/example/pubspec.yaml index b02b0f72..2ce18c4a 100755 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -22,7 +22,7 @@ dependencies: cupertino_icons: ^1.0.2 flutter_downloader: ^1.5.2 path_provider: ^2.0.0-nullsafety - permission_handler: ^5.0.1+1 + permission_handler: ^5.1.0+2 url_launcher: ^6.0.0-nullsafety.4 # connectivity: ^0.4.5+6 flutter_inappwebview: diff --git a/ios/Classes/InAppBrowser/InAppBrowserManager.swift b/ios/Classes/InAppBrowser/InAppBrowserManager.swift index b2989b3c..defda931 100755 --- a/ios/Classes/InAppBrowser/InAppBrowserManager.swift +++ b/ios/Classes/InAppBrowser/InAppBrowserManager.swift @@ -36,38 +36,8 @@ public class InAppBrowserManager: NSObject, FlutterPlugin { let arguments = call.arguments as? NSDictionary switch call.method { - case "openUrlRequest": - let id = arguments!["id"] as! String - let urlRequest = arguments!["urlRequest"] as! [String:Any?] - let options = arguments!["options"] as! [String: Any?] - let contextMenu = arguments!["contextMenu"] as! [String: Any] - let windowId = arguments!["windowId"] as? Int64 - let initialUserScripts = arguments!["initialUserScripts"] as? [[String: Any]] - openUrlRequest(id: id, urlRequest: urlRequest, options: options, contextMenu: contextMenu, windowId: windowId, initialUserScripts: initialUserScripts) - result(true) - break - case "openFile": - let id = arguments!["id"] as! String - let assetFilePath = arguments!["assetFilePath"] as! String - let options = arguments!["options"] as! [String: Any?] - let contextMenu = arguments!["contextMenu"] as! [String: Any] - let windowId = arguments!["windowId"] as? Int64 - let initialUserScripts = arguments!["initialUserScripts"] as? [[String: Any]] - openFile(id: id, assetFilePath: assetFilePath, options: options, contextMenu: contextMenu, windowId: windowId, initialUserScripts: initialUserScripts) - result(true) - break - case "openData": - let id = arguments!["id"] as! String - let options = arguments!["options"] as! [String: Any?] - let data = arguments!["data"] as! String - let mimeType = arguments!["mimeType"] as! String - let encoding = arguments!["encoding"] as! String - let baseUrl = arguments!["baseUrl"] as! String - let contextMenu = arguments!["contextMenu"] as! [String: Any] - let windowId = arguments!["windowId"] as? Int64 - let initialUserScripts = arguments!["initialUserScripts"] as? [[String: Any]] - openData(id: id, options: options, data: data, mimeType: mimeType, encoding: encoding, baseUrl: baseUrl, - contextMenu: contextMenu, windowId: windowId, initialUserScripts: initialUserScripts) + case "open": + open(arguments: arguments!) result(true) break case "openWithSystemBrowser": @@ -98,37 +68,25 @@ public class InAppBrowserManager: NSObject, FlutterPlugin { return webViewController } - public func openUrlRequest(id: String, urlRequest: [String:Any?], options: [String: Any?], - contextMenu: [String: Any], windowId: Int64?, initialUserScripts: [[String: Any]]?) { - let webViewController = prepareInAppBrowserWebViewController(options: options) - - webViewController.id = id - webViewController.initialUrlRequest = URLRequest.init(fromPluginMap: urlRequest) - webViewController.contextMenu = contextMenu - webViewController.windowId = windowId - webViewController.initialUserScripts = initialUserScripts ?? [] - - presentViewController(webViewController: webViewController) - } - - public func openFile(id: String, assetFilePath: String, options: [String: Any?], - contextMenu: [String: Any], windowId: Int64?, initialUserScripts: [[String: Any]]?) { + public func open(arguments: NSDictionary) { + let id = arguments["id"] as! String + let urlRequest = arguments["urlRequest"] as? [String:Any?] + let assetFilePath = arguments["assetFilePath"] as? String + let data = arguments["data"] as? String + let mimeType = arguments["mimeType"] as? String + let encoding = arguments["encoding"] as? String + let baseUrl = arguments["baseUrl"] as? String + let options = arguments["options"] as! [String: Any?] + let contextMenu = arguments["contextMenu"] as! [String: Any] + let windowId = arguments["windowId"] as? Int64 + let initialUserScripts = arguments["initialUserScripts"] as? [[String: Any]] + let pullToRefreshInitialOptions = arguments["pullToRefreshOptions"] as! [String: Any?] + let webViewController = prepareInAppBrowserWebViewController(options: options) webViewController.id = id + webViewController.initialUrlRequest = urlRequest != nil ? URLRequest.init(fromPluginMap: urlRequest!) : nil webViewController.initialFile = assetFilePath - webViewController.contextMenu = contextMenu - webViewController.windowId = windowId - webViewController.initialUserScripts = initialUserScripts ?? [] - - presentViewController(webViewController: webViewController) - } - - public func openData(id: String, options: [String: Any?], data: String, mimeType: String, encoding: String, - baseUrl: String, contextMenu: [String: Any], windowId: Int64?, initialUserScripts: [[String: Any]]?) { - let webViewController = prepareInAppBrowserWebViewController(options: options) - - webViewController.id = id webViewController.initialData = data webViewController.initialMimeType = mimeType webViewController.initialEncoding = encoding @@ -136,6 +94,7 @@ public class InAppBrowserManager: NSObject, FlutterPlugin { webViewController.contextMenu = contextMenu webViewController.windowId = windowId webViewController.initialUserScripts = initialUserScripts ?? [] + webViewController.pullToRefreshInitialOptions = pullToRefreshInitialOptions presentViewController(webViewController: webViewController) } diff --git a/ios/Classes/InAppBrowser/InAppBrowserWebViewController.swift b/ios/Classes/InAppBrowser/InAppBrowserWebViewController.swift index ce71af97..f3026c01 100755 --- a/ios/Classes/InAppBrowser/InAppBrowserWebViewController.swift +++ b/ios/Classes/InAppBrowser/InAppBrowserWebViewController.swift @@ -36,6 +36,7 @@ public class InAppBrowserWebViewController: UIViewController, InAppBrowserDelega var initialBaseUrl: String? var previousStatusBarStyle = -1 var initialUserScripts: [[String: Any]] = [] + var pullToRefreshInitialOptions: [String: Any?] = [:] var methodCallDelegate: InAppWebViewMethodHandler? public override func loadView() { @@ -64,6 +65,15 @@ public class InAppBrowserWebViewController: UIViewController, InAppBrowserDelega methodCallDelegate = InAppWebViewMethodHandler(webView: webView!) channel!.setMethodCallHandler(LeakAvoider(delegate: methodCallDelegate!).handle) + let pullToRefreshLayoutChannel = FlutterMethodChannel(name: "com.pichillilorenzo/flutter_inappwebview_pull_to_refresh_" + id, + binaryMessenger: SwiftFlutterPlugin.instance!.registrar!.messenger()) + let pullToRefreshOptions = PullToRefreshOptions() + let _ = pullToRefreshOptions.parse(options: pullToRefreshInitialOptions) + let pullToRefreshControl = PullToRefreshControl(channel: pullToRefreshLayoutChannel, options: pullToRefreshOptions) + webView.pullToRefreshControl = pullToRefreshControl + pullToRefreshControl.delegate = webView + pullToRefreshControl.prepare() + prepareWebView() progressBar = UIProgressView(progressViewStyle: .bar) diff --git a/ios/Classes/InAppWebView/FlutterWebViewController.swift b/ios/Classes/InAppWebView/FlutterWebViewController.swift index 8ddb3c43..b927e5b1 100755 --- a/ios/Classes/InAppWebView/FlutterWebViewController.swift +++ b/ios/Classes/InAppWebView/FlutterWebViewController.swift @@ -23,13 +23,8 @@ public class FlutterWebViewController: NSObject, FlutterPlatformView { self.registrar = registrar self.viewId = viewId - var channelName = "" - if let id = viewId as? Int64 { - channelName = "com.pichillilorenzo/flutter_inappwebview_" + String(id) - } else if let id = viewId as? String { - channelName = "com.pichillilorenzo/flutter_inappwebview_" + id - } - channel = FlutterMethodChannel(name: channelName, binaryMessenger: registrar.messenger()) + channel = FlutterMethodChannel(name: "com.pichillilorenzo/flutter_inappwebview_" + String(describing: viewId), + binaryMessenger: registrar.messenger()) myView = UIView(frame: frame) myView!.clipsToBounds = true @@ -41,6 +36,7 @@ public class FlutterWebViewController: NSObject, FlutterPlatformView { let contextMenu = args["contextMenu"] as? [String: Any] let windowId = args["windowId"] as? Int64 let initialUserScripts = args["initialUserScripts"] as? [[String: Any]] + let pullToRefreshInitialOptions = args["pullToRefreshOptions"] as! [String: Any?] var userScripts: [UserScript] = [] if let initialUserScripts = initialUserScripts { @@ -69,6 +65,15 @@ public class FlutterWebViewController: NSObject, FlutterPlatformView { methodCallDelegate = InAppWebViewMethodHandler(webView: webView!) channel!.setMethodCallHandler(LeakAvoider(delegate: methodCallDelegate!).handle) + + let pullToRefreshLayoutChannel = FlutterMethodChannel(name: "com.pichillilorenzo/flutter_inappwebview_pull_to_refresh_" + String(describing: viewId), + binaryMessenger: registrar.messenger()) + let pullToRefreshOptions = PullToRefreshOptions() + let _ = pullToRefreshOptions.parse(options: pullToRefreshInitialOptions) + let pullToRefreshControl = PullToRefreshControl(channel: pullToRefreshLayoutChannel, options: pullToRefreshOptions) + webView!.pullToRefreshControl = pullToRefreshControl + pullToRefreshControl.delegate = webView! + pullToRefreshControl.prepare() webView!.autoresizingMask = [.flexibleWidth, .flexibleHeight] myView!.autoresizesSubviews = true diff --git a/ios/Classes/InAppWebView/InAppWebView.swift b/ios/Classes/InAppWebView/InAppWebView.swift index e4ee7085..0945e8bc 100755 --- a/ios/Classes/InAppWebView/InAppWebView.swift +++ b/ios/Classes/InAppWebView/InAppWebView.swift @@ -9,16 +9,20 @@ import Flutter import Foundation import WebKit -public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavigationDelegate, WKScriptMessageHandler, UIGestureRecognizerDelegate { +public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavigationDelegate, WKScriptMessageHandler, UIGestureRecognizerDelegate, PullToRefreshDelegate { var windowId: Int64? var inAppBrowserDelegate: InAppBrowserDelegate? var channel: FlutterMethodChannel? var options: InAppWebViewOptions? + var pullToRefreshControl: PullToRefreshControl? + static var sslCertificatesMap: [String: SslCertificate] = [:] // [URL host name : SslCertificate] static var credentialsProposed: [URLCredential] = [] + var lastScrollX: CGFloat = 0 var lastScrollY: CGFloat = 0 + var isPausedTimers = false var isPausedTimersCompletionHandler: (() -> Void)? @@ -250,10 +254,19 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi return super.canPerformAction(action, withSender: sender) } + + public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + // fix for pull-to-refresh jittering when the touch drag event is held + if let pullToRefreshControl = pullToRefreshControl, + pullToRefreshControl.shouldCallOnRefresh { + pullToRefreshControl.onRefresh() + } + } public func prepare() { - self.scrollView.addGestureRecognizer(self.longPressRecognizer) - self.scrollView.addGestureRecognizer(self.recognizerForDisablingContextMenuOnLinks) + scrollView.addGestureRecognizer(self.longPressRecognizer) + scrollView.addGestureRecognizer(self.recognizerForDisablingContextMenuOnLinks) + scrollView.addObserver(self, forKeyPath: #keyPath(UIScrollView.contentOffset), options: [.new, .old], context: nil) addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), @@ -269,14 +282,13 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi forKeyPath: #keyPath(WKWebView.title), options: [.new, .old], context: nil) - + NotificationCenter.default.addObserver( self, selector: #selector(onCreateContextMenu), name: UIMenuController.willShowMenuNotification, object: nil) - NotificationCenter.default.addObserver( self, selector: #selector(onHideContextMenu), @@ -566,7 +578,13 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi let newTitle = change?[NSKeyValueChangeKey.newKey] as? String onTitleChanged(title: newTitle) inAppBrowserDelegate?.didChangeTitle(title: newTitle) - } + } else if keyPath == #keyPath(UIScrollView.contentOffset) { + let newContentOffset = change?[NSKeyValueChangeKey.newKey] as? CGPoint + let oldContentOffset = change?[NSKeyValueChangeKey.oldKey] as? CGPoint + if scrollView.isDragging || scrollView.isDecelerating || newContentOffset != oldContentOffset { + onScrollChanged() + } + } replaceGestureHandlerIfNeeded() } @@ -1956,7 +1974,15 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi }) } - public func scrollViewDidScroll(_ scrollView: UIScrollView) { + /// UIScrollViewDelegate is somehow bugged: + /// if InAppWebView implements the UIScrollViewDelegate protocol and implement the scrollViewDidScroll event, + /// then, when the user scrolls the content, the webview content is not rendered (just white space). + /// Calling setNeedsLayout() resolves this problem, but, for some reason, the bounce effect is canceled. + /// + /// So, to track the same event, without implementing the scrollViewDidScroll event, we create + /// an observer that observes the scrollView.contentOffset property. + /// This way, we don't need to call setNeedsLayout() and all works fine. + public func onScrollChanged() { let disableVerticalScroll = options?.disableVerticalScroll ?? false let disableHorizontalScroll = options?.disableHorizontalScroll ?? false if disableVerticalScroll && disableHorizontalScroll { @@ -2635,6 +2661,23 @@ if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) { } } + public func enablePullToRefresh() { + if let pullToRefreshControl = pullToRefreshControl { + if #available(iOS 10.0, *) { + scrollView.refreshControl = pullToRefreshControl + } else { + scrollView.addSubview(pullToRefreshControl) + } + } + } + + public func disablePullToRefresh() { + pullToRefreshControl?.removeFromSuperview() + if #available(iOS 10.0, *) { + scrollView.refreshControl = nil + } + } + public func dispose() { if isPausedTimers, let completionHandler = isPausedTimersCompletionHandler { isPausedTimersCompletionHandler = nil @@ -2659,12 +2702,16 @@ if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) { for imp in customIMPs { imp_removeBlock(imp) } + scrollView.removeObserver(self, forKeyPath: #keyPath(UIScrollView.contentOffset)) longPressRecognizer.removeTarget(self, action: #selector(longPressGestureDetected)) longPressRecognizer.delegate = nil scrollView.removeGestureRecognizer(longPressRecognizer) recognizerForDisablingContextMenuOnLinks.removeTarget(self, action: #selector(longPressGestureDetected)) recognizerForDisablingContextMenuOnLinks.delegate = nil scrollView.removeGestureRecognizer(recognizerForDisablingContextMenuOnLinks) + disablePullToRefresh() + pullToRefreshControl?.dispose() + pullToRefreshControl = nil uiDelegate = nil navigationDelegate = nil scrollView.delegate = nil diff --git a/ios/Classes/InAppWebViewMethodHandler.swift b/ios/Classes/InAppWebViewMethodHandler.swift index dc5b205e..f8437fd6 100644 --- a/ios/Classes/InAppWebViewMethodHandler.swift +++ b/ios/Classes/InAppWebViewMethodHandler.swift @@ -8,7 +8,7 @@ import Foundation import WebKit -class InAppWebViewMethodHandler: FlutterMethodCallDelegate { +public class InAppWebViewMethodHandler: FlutterMethodCallDelegate { var webView: InAppWebView? init(webView: InAppWebView) { diff --git a/ios/Classes/LeakAvoider.swift b/ios/Classes/LeakAvoider.swift index 4723ce79..eebf7023 100755 --- a/ios/Classes/LeakAvoider.swift +++ b/ios/Classes/LeakAvoider.swift @@ -11,8 +11,8 @@ public class LeakAvoider: NSObject { weak var delegate : FlutterMethodCallDelegate? init(delegate: FlutterMethodCallDelegate) { - self.delegate = delegate super.init() + self.delegate = delegate } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { diff --git a/ios/Classes/PullToRefresh/PullToRefreshControl.swift b/ios/Classes/PullToRefresh/PullToRefreshControl.swift new file mode 100644 index 00000000..8753ce62 --- /dev/null +++ b/ios/Classes/PullToRefresh/PullToRefreshControl.swift @@ -0,0 +1,113 @@ +// +// File.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 03/03/21. +// + +import Foundation +import Flutter + +public class PullToRefreshControl : UIRefreshControl, FlutterPlugin { + + var channel: FlutterMethodChannel? + var options: PullToRefreshOptions? + var shouldCallOnRefresh = false + var delegate: PullToRefreshDelegate? + + public init(channel: FlutterMethodChannel?, options: PullToRefreshOptions?) { + super.init() + self.channel = channel + self.options = options + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + public static func register(with registrar: FlutterPluginRegistrar) { + + } + + public func prepare() { + self.channel?.setMethodCallHandler(self.handle) + if let options = options { + if options.enabled { + delegate?.enablePullToRefresh() + } + if let color = options.color, !color.isEmpty { + tintColor = UIColor(hexString: color) + } + if let backgroundTintColor = options.backgroundColor, !backgroundTintColor.isEmpty { + backgroundColor = UIColor(hexString: backgroundTintColor) + } + if let attributedTitleMap = options.attributedTitle { + attributedTitle = NSAttributedString.fromMap(map: attributedTitleMap) + } + } + addTarget(self, action: #selector(updateShouldCallOnRefresh), for: .valueChanged) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "setEnabled": + let enabled = arguments!["enabled"] as! Bool + if enabled { + delegate?.enablePullToRefresh() + } else { + delegate?.disablePullToRefresh() + } + result(true) + break + case "setRefreshing": + let refreshing = arguments!["refreshing"] as! Bool + if refreshing { + self.beginRefreshing() + } else { + self.endRefreshing() + } + result(true) + break + case "setColor": + let color = arguments!["color"] as! String + tintColor = UIColor(hexString: color) + result(true) + break + case "setBackgroundColor": + let color = arguments!["color"] as! String + backgroundColor = UIColor(hexString: color) + result(true) + break + case "setAttributedTitle": + let attributedTitleMap = arguments!["attributedTitle"] as! [String: Any?] + attributedTitle = NSAttributedString.fromMap(map: attributedTitleMap) + result(true) + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func onRefresh() { + shouldCallOnRefresh = false + let arguments: [String: Any?] = [:] + self.channel?.invokeMethod("onRefresh", arguments: arguments) + } + + @objc public func updateShouldCallOnRefresh() { + shouldCallOnRefresh = true + } + + public func dispose() { + channel?.setMethodCallHandler(nil) + removeTarget(self, action: #selector(updateShouldCallOnRefresh), for: .valueChanged) + delegate = nil + } + + deinit { + print("PullToRefreshControl - dealloc") + } +} diff --git a/ios/Classes/PullToRefresh/PullToRefreshDelegate.swift b/ios/Classes/PullToRefresh/PullToRefreshDelegate.swift new file mode 100644 index 00000000..bc2606bd --- /dev/null +++ b/ios/Classes/PullToRefresh/PullToRefreshDelegate.swift @@ -0,0 +1,13 @@ +// +// PullToRefreshDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 04/03/21. +// + +import Foundation + +public protocol PullToRefreshDelegate { + func enablePullToRefresh() + func disablePullToRefresh() +} diff --git a/ios/Classes/PullToRefresh/PullToRefreshOptions.swift b/ios/Classes/PullToRefresh/PullToRefreshOptions.swift new file mode 100644 index 00000000..58c3399a --- /dev/null +++ b/ios/Classes/PullToRefresh/PullToRefreshOptions.swift @@ -0,0 +1,33 @@ +// +// PullToRefreshOptions.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 03/03/21. +// + +import Foundation + +public class PullToRefreshOptions : Options { + + var enabled = true + var color: String? + var backgroundColor: String? + var attributedTitle: [String: Any?]? + + override init(){ + super.init() + } + + override func parse(options: [String: Any?]) -> PullToRefreshOptions { + let _ = super.parse(options: options) + if let attributedTitle = options["attributedTitle"] as? [String: Any?] { + self.attributedTitle = attributedTitle + } + return self + } + + override func getRealOptions(obj: PullToRefreshControl?) -> [String: Any?] { + let realOptions: [String: Any?] = toMap() + return realOptions + } +} diff --git a/ios/Classes/Types/NSAttributedString.swift b/ios/Classes/Types/NSAttributedString.swift new file mode 100644 index 00000000..64169abf --- /dev/null +++ b/ios/Classes/Types/NSAttributedString.swift @@ -0,0 +1,63 @@ +// +// NSAttributedString.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 05/03/21. +// + +import Foundation + +extension NSAttributedString { + public static func fromMap(map: [String:Any?]?) -> NSAttributedString? { + guard let map = map, let string = map["string"] as? String else { + return nil + } + + var attributes: [NSAttributedString.Key : Any] = [:] + + if let backgroundColor = map["backgroundColor"] as? String { + attributes[.backgroundColor] = UIColor(hexString: backgroundColor) + } + if let baselineOffset = map["baselineOffset"] as? Double { + attributes[.baselineOffset] = baselineOffset + } + if let expansion = map["expansion"] as? Double { + attributes[.expansion] = expansion + } + if let foregroundColor = map["foregroundColor"] as? String { + attributes[.foregroundColor] = UIColor(hexString: foregroundColor) + } + if let kern = map["kern"] as? Double { + attributes[.kern] = kern + } + if let ligature = map["ligature"] as? Int64 { + attributes[.ligature] = ligature + } + if let obliqueness = map["obliqueness"] as? Double { + attributes[.obliqueness] = obliqueness + } + if let strikethroughColor = map["strikethroughColor"] as? String { + attributes[.strikethroughColor] = UIColor(hexString: strikethroughColor) + } + if let strikethroughStyle = map["strikethroughStyle"] as? Int64 { + attributes[.strikethroughStyle] = strikethroughStyle + } + if let strokeColor = map["strokeColor"] as? String { + attributes[.strokeColor] = UIColor(hexString: strokeColor) + } + if let strokeWidth = map["strokeWidth"] as? Double { + attributes[.strokeWidth] = strokeWidth + } + if let textEffect = map["textEffect"] as? String { + attributes[.textEffect] = textEffect + } + if let underlineColor = map["underlineColor"] as? String { + attributes[.underlineColor] = UIColor(hexString: underlineColor) + } + if let underlineStyle = map["underlineStyle"] as? Int64 { + attributes[.underlineStyle] = underlineStyle + } + + return NSAttributedString(string: string, attributes: attributes) + } +} diff --git a/lib/src/in_app_browser/in_app_browser.dart b/lib/src/in_app_browser/in_app_browser.dart index f93f97e4..f273e1c3 100755 --- a/lib/src/in_app_browser/in_app_browser.dart +++ b/lib/src/in_app_browser/in_app_browser.dart @@ -3,6 +3,7 @@ import 'dart:collection'; import 'dart:typed_data'; import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_inappwebview/src/util.dart'; import '../context_menu.dart'; @@ -40,14 +41,17 @@ class InAppBrowserNotOpenedException implements Exception { ///This class uses the native WebView of the platform. ///The [webViewController] field can be used to access the [InAppWebViewController] API. class InAppBrowser { - ///Browser's UUID. - late String id; + ///View ID. + late final String id; ///Context menu used by the browser. It should be set before opening the browser. ContextMenu? contextMenu; + ///Represents the pull-to-refresh feature controller. + PullToRefreshController? pullToRefreshController; + ///Initial list of user scripts to be loaded at start or end of a page loading. - UnmodifiableListView? initialUserScripts; + final UnmodifiableListView? initialUserScripts; bool _isOpened = false; late MethodChannel _channel; @@ -68,13 +72,14 @@ class InAppBrowser { this._channel.setMethodCallHandler(handleMethod); _isOpened = false; webViewController = new InAppWebViewController.fromInAppBrowser( - id, this._channel, this, this.initialUserScripts); + this._channel, this, this.initialUserScripts); } Future handleMethod(MethodCall call) async { switch (call.method) { case "onBrowserCreated": this._isOpened = true; + this.pullToRefreshController?.initMethodChannel(id); onBrowserCreated(); break; case "onExit": @@ -106,7 +111,8 @@ class InAppBrowser { args.putIfAbsent('windowId', () => windowId); args.putIfAbsent('initialUserScripts', () => initialUserScripts?.map((e) => e.toMap()).toList() ?? []); - await _sharedChannel.invokeMethod('openUrlRequest', args); + args.putIfAbsent('pullToRefreshOptions', () => pullToRefreshController?.options.toMap() ?? PullToRefreshOptions(enabled: false).toMap()); + await _sharedChannel.invokeMethod('open', args); } ///Opens the given [assetFilePath] file in a new [InAppBrowser] instance. @@ -159,7 +165,8 @@ class InAppBrowser { args.putIfAbsent('windowId', () => windowId); args.putIfAbsent('initialUserScripts', () => initialUserScripts?.map((e) => e.toMap()).toList() ?? []); - await _sharedChannel.invokeMethod('openFile', args); + args.putIfAbsent('pullToRefreshOptions', () => pullToRefreshController?.options.toMap() ?? PullToRefreshOptions(enabled: false).toMap()); + await _sharedChannel.invokeMethod('open', args); } ///Opens a new [InAppBrowser] instance with [data] as a content, using [baseUrl] as the base URL for it. @@ -194,7 +201,8 @@ class InAppBrowser { args.putIfAbsent('windowId', () => windowId); args.putIfAbsent('initialUserScripts', () => initialUserScripts?.map((e) => e.toMap()).toList() ?? []); - await _sharedChannel.invokeMethod('openData', args); + args.putIfAbsent('pullToRefreshOptions', () => pullToRefreshController?.options.toMap() ?? PullToRefreshOptions(enabled: false).toMap()); + await _sharedChannel.invokeMethod('open', args); } ///This is a static method that opens an [url] in the system browser. You wont be able to use the [InAppBrowser] methods here! diff --git a/lib/src/in_app_webview/headless_in_app_webview.dart b/lib/src/in_app_webview/headless_in_app_webview.dart index a56e6e37..b5ba9320 100644 --- a/lib/src/in_app_webview/headless_in_app_webview.dart +++ b/lib/src/in_app_webview/headless_in_app_webview.dart @@ -9,13 +9,17 @@ import '../types.dart'; import 'webview.dart'; import 'in_app_webview_controller.dart'; import 'in_app_webview_options.dart'; +import '../pull_to_refresh/pull_to_refresh_controller.dart'; +import '../pull_to_refresh/pull_to_refresh_options.dart'; ///Class that represents a WebView in headless mode. ///It can be used to run a WebView in background without attaching an `InAppWebView` to the widget tree. /// ///Remember to dispose it when you don't need it anymore. class HeadlessInAppWebView implements WebView { - late String id; + ///View ID. + late final String id; + bool _isDisposed = true; static const MethodChannel _sharedChannel = const MethodChannel('com.pichillilorenzo/flutter_headless_inappwebview'); @@ -85,7 +89,8 @@ class HeadlessInAppWebView implements WebView { this.initialData, this.initialOptions, this.contextMenu, - this.initialUserScripts}) { + this.initialUserScripts, + this.pullToRefreshController}) { id = ViewIdGenerator.generateId(); webViewController = new InAppWebViewController(id, this); } @@ -93,6 +98,7 @@ class HeadlessInAppWebView implements WebView { Future handleMethod(MethodCall call) async { switch (call.method) { case "onHeadlessWebViewCreated": + pullToRefreshController?.initMethodChannel(id); if (onWebViewCreated != null) { onWebViewCreated!(webViewController); } @@ -123,6 +129,7 @@ class HeadlessInAppWebView implements WebView { 'windowId': this.windowId, 'initialUserScripts': this.initialUserScripts?.map((e) => e.toMap()).toList() ?? [], + 'pullToRefreshOptions': this.pullToRefreshController?.options.toMap() ?? PullToRefreshOptions(enabled: false).toMap() }); await _sharedChannel.invokeMethod('createHeadlessWebView', args); } @@ -177,6 +184,9 @@ class HeadlessInAppWebView implements WebView { @override final UnmodifiableListView? initialUserScripts; + @override + final PullToRefreshController? pullToRefreshController; + @override final void Function(InAppWebViewController controller, Uri? url)? onPageCommitVisible; diff --git a/lib/src/in_app_webview/in_app_webview.dart b/lib/src/in_app_webview/in_app_webview.dart index c6b0019e..b9af4eb9 100755 --- a/lib/src/in_app_webview/in_app_webview.dart +++ b/lib/src/in_app_webview/in_app_webview.dart @@ -8,6 +8,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter/gestures.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import '../context_menu.dart'; import '../types.dart'; @@ -15,6 +16,7 @@ import '../types.dart'; import 'webview.dart'; import 'in_app_webview_controller.dart'; import 'in_app_webview_options.dart'; +import '../pull_to_refresh/pull_to_refresh_controller.dart'; ///Flutter Widget for adding an **inline native WebView** integrated in the flutter widget tree. class InAppWebView extends StatefulWidget implements WebView { @@ -38,6 +40,7 @@ class InAppWebView extends StatefulWidget implements WebView { this.initialData, this.initialOptions, this.initialUserScripts, + this.pullToRefreshController, this.contextMenu, this.onWebViewCreated, this.onLoadStart, @@ -133,6 +136,9 @@ class InAppWebView extends StatefulWidget implements WebView { @override final UnmodifiableListView? initialUserScripts; + @override + final PullToRefreshController? pullToRefreshController; + @override final ContextMenu? contextMenu; @@ -379,6 +385,7 @@ class _InAppWebViewState extends State { 'initialUserScripts': widget.initialUserScripts?.map((e) => e.toMap()).toList() ?? [], + 'pullToRefreshOptions': widget.pullToRefreshController?.options.toMap() ?? PullToRefreshOptions(enabled: false).toMap() }, creationParamsCodec: const StandardMessageCodec(), ) @@ -405,6 +412,7 @@ class _InAppWebViewState extends State { 'windowId': widget.windowId, 'initialUserScripts': widget.initialUserScripts?.map((e) => e.toMap()).toList() ?? [], + 'pullToRefreshOptions': widget.pullToRefreshController?.options.toMap() ?? PullToRefreshOptions(enabled: false).toMap() }, creationParamsCodec: const StandardMessageCodec(), ); @@ -425,6 +433,7 @@ class _InAppWebViewState extends State { 'windowId': widget.windowId, 'initialUserScripts': widget.initialUserScripts?.map((e) => e.toMap()).toList() ?? [], + 'pullToRefreshOptions': widget.pullToRefreshController?.options.toMap() ?? PullToRefreshOptions(enabled: false).toMap() }, creationParamsCodec: const StandardMessageCodec(), ); @@ -445,6 +454,7 @@ class _InAppWebViewState extends State { void _onPlatformViewCreated(int id) { _controller = InAppWebViewController(id, widget); + widget.pullToRefreshController?.initMethodChannel(id); if (widget.onWebViewCreated != null) { widget.onWebViewCreated!(_controller); } diff --git a/lib/src/in_app_webview/in_app_webview_controller.dart b/lib/src/in_app_webview/in_app_webview_controller.dart index 271c5e48..bd116b8f 100644 --- a/lib/src/in_app_webview/in_app_webview_controller.dart +++ b/lib/src/in_app_webview/in_app_webview_controller.dart @@ -52,15 +52,9 @@ class InAppWebViewController { HashMap(); List _userScripts = []; - // ignore: unused_field - bool _isOpened = false; - // ignore: unused_field dynamic _id; - // ignore: unused_field - String? _inAppBrowserUuid; - InAppBrowser? _inAppBrowser; ///Android controller that contains only android-specific methods @@ -84,11 +78,9 @@ class InAppWebViewController { } InAppWebViewController.fromInAppBrowser( - String uuid, MethodChannel channel, InAppBrowser inAppBrowser, UnmodifiableListView? initialUserScripts) { - this._inAppBrowserUuid = uuid; this._channel = channel; this._inAppBrowser = inAppBrowser; this._userScripts = diff --git a/lib/src/in_app_webview/ios/in_app_webview_options.dart b/lib/src/in_app_webview/ios/in_app_webview_options.dart index 7d4672dd..1ca0b5f1 100755 --- a/lib/src/in_app_webview/ios/in_app_webview_options.dart +++ b/lib/src/in_app_webview/ios/in_app_webview_options.dart @@ -157,7 +157,7 @@ class IOSInAppWebViewOptions ///Set to `true` to enable Apple Pay API for the [WebView] at its first page load or on the next page load (using [InAppWebViewController.setOptions]). The default value is `false`. /// ///**IMPORTANT NOTE**: As written in the official [Safari 13 Release Notes](https://developer.apple.com/documentation/safari-release-notes/safari-13-release-notes#Payment-Request-API), - ///it won't work if any script injection APIs is used (such as [InAppWebViewController.evaluateJavascript] or [UserScript]). + ///it won't work if any script injection APIs are used (such as [InAppWebViewController.evaluateJavascript] or [UserScript]). ///So, when this attribute is `true`, all the methods, options, and events implemented using JavaScript won't be called or won't do anything and the result will always be `null`. /// ///Methods affected: diff --git a/lib/src/in_app_webview/main.dart b/lib/src/in_app_webview/main.dart index 5d7fe28a..ed772f00 100644 --- a/lib/src/in_app_webview/main.dart +++ b/lib/src/in_app_webview/main.dart @@ -3,5 +3,6 @@ export 'in_app_webview.dart'; export 'in_app_webview_controller.dart'; export 'in_app_webview_options.dart'; export 'headless_in_app_webview.dart'; +export '../pull_to_refresh/pull_to_refresh_controller.dart'; export 'android/main.dart'; export 'ios/main.dart'; diff --git a/lib/src/in_app_webview/webview.dart b/lib/src/in_app_webview/webview.dart index 6db7237a..b7b87856 100644 --- a/lib/src/in_app_webview/webview.dart +++ b/lib/src/in_app_webview/webview.dart @@ -1,6 +1,8 @@ import 'dart:collection'; import 'dart:typed_data'; +import '../pull_to_refresh/pull_to_refresh_controller.dart'; + import '../context_menu.dart'; import '../types.dart'; @@ -642,6 +644,9 @@ abstract class WebView { ///This is a limitation of the native iOS WebKit APIs. final UnmodifiableListView? initialUserScripts; + ///Represents the pull-to-refresh feature controller. + final PullToRefreshController? pullToRefreshController; + WebView( {this.windowId, this.onWebViewCreated, @@ -701,5 +706,6 @@ abstract class WebView { this.initialData, this.initialOptions, this.contextMenu, - this.initialUserScripts}); + this.initialUserScripts, + this.pullToRefreshController}); } diff --git a/lib/src/main.dart b/lib/src/main.dart index fe56afce..f11beba8 100644 --- a/lib/src/main.dart +++ b/lib/src/main.dart @@ -14,3 +14,4 @@ export 'in_app_localhost_server.dart'; export 'content_blocker.dart'; export 'http_auth_credentials_database.dart'; export 'context_menu.dart'; +export 'pull_to_refresh/main.dart'; \ No newline at end of file diff --git a/lib/src/pull_to_refresh/main.dart b/lib/src/pull_to_refresh/main.dart new file mode 100644 index 00000000..2cae0d6d --- /dev/null +++ b/lib/src/pull_to_refresh/main.dart @@ -0,0 +1,2 @@ +export 'pull_to_refresh_controller.dart'; +export 'pull_to_refresh_options.dart'; diff --git a/lib/src/pull_to_refresh/pull_to_refresh_controller.dart b/lib/src/pull_to_refresh/pull_to_refresh_controller.dart new file mode 100644 index 00000000..aa19ca5e --- /dev/null +++ b/lib/src/pull_to_refresh/pull_to_refresh_controller.dart @@ -0,0 +1,130 @@ +import 'dart:ui'; + +import 'package:flutter/services.dart'; +import '../in_app_webview/webview.dart'; +import '../in_app_browser/in_app_browser.dart'; +import '../util.dart'; +import '../types.dart'; +import 'pull_to_refresh_options.dart'; + +///A standard controller that can initiate the refreshing of a scroll view’s contents. +///This should be used whenever the user can refresh the contents of a WebView via a vertical swipe gesture. +/// +///All the methods should be called only when the WebView has been created or is already running +///(for example [WebView.onWebViewCreated] or [InAppBrowser.onBrowserCreated]). +class PullToRefreshController { + late PullToRefreshOptions options; + MethodChannel? _channel; + + ///Event called when a swipe gesture triggers a refresh. + final void Function()? onRefresh; + + PullToRefreshController({PullToRefreshOptions? options, this.onRefresh}) { + this.options = options ?? PullToRefreshOptions(); + } + + Future handleMethod(MethodCall call) async { + switch (call.method) { + case "onRefresh": + if (onRefresh != null) + onRefresh!(); + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + return null; + } + + ///Sets whether the pull-to-refresh feature is enabled or not. + Future setEnabled(bool enabled) async { + Map args = {}; + args.putIfAbsent('enabled', () => enabled); + await _channel?.invokeMethod('setEnabled', args); + } + + Future _setRefreshing(bool refreshing) async { + Map args = {}; + args.putIfAbsent('refreshing', () => refreshing); + await _channel?.invokeMethod('setRefreshing', args); + } + + ///Tells the controller that a refresh operation was started programmatically. + /// + ///Call this method when an external event source triggers a programmatic refresh of your scrolling view. + ///This method updates the state of the refresh control to reflect the in-progress refresh operation. + ///When the refresh operation ends, be sure to call the [endRefreshing] method to return the controller to its default state. + Future beginRefreshing() async { + return await _setRefreshing(true); + } + + ///Tells the controller that a refresh operation has ended. + /// + ///Call this method at the end of any refresh operation (whether it was initiated programmatically or by the user) + ///to return the refresh control to its default state. + ///If the refresh control is at least partially visible, calling this method also hides it. + ///If animations are also enabled, the control is hidden using an animation. + Future endRefreshing() async { + await _setRefreshing(false); + } + + ///Returns whether a refresh operation has been triggered and is in progress. + Future isRefreshing() async { + Map args = {}; + return await _channel?.invokeMethod('isRefreshing', args); + } + + ///Sets the color of the refresh control. + Future setColor(Color color) async { + Map args = {}; + args.putIfAbsent('color', () => color.toHex()); + await _channel?.invokeMethod('setColor', args); + } + + ///Sets the background color of the refresh control. + Future setBackgroundColor(Color color) async { + Map args = {}; + args.putIfAbsent('color', () => color.toHex()); + await _channel?.invokeMethod('setBackgroundColor', args); + } + + ///Set the distance to trigger a sync in dips. + /// + ///**NOTE**: Available only on Android. + Future setDistanceToTriggerSync(int distanceToTriggerSync) async { + Map args = {}; + args.putIfAbsent('distanceToTriggerSync', () => distanceToTriggerSync); + await _channel?.invokeMethod('setDistanceToTriggerSync', args); + } + + ///Sets the distance that the refresh indicator can be pulled beyond its resting position during a swipe gesture. + /// + ///**NOTE**: Available only on Android. + Future setSlingshotDistance(int slingshotDistance) async { + Map args = {}; + args.putIfAbsent('slingshotDistance', () => slingshotDistance); + await _channel?.invokeMethod('setSlingshotDistance', args); + } + + ///Gets the default distance that the refresh indicator can be pulled beyond its resting position during a swipe gesture. + /// + ///**NOTE**: Available only on Android. + Future getDefaultSlingshotDistance() async { + Map args = {}; + return await _channel?.invokeMethod('getDefaultSlingshotDistance', args); + } + + ///Sets the styled title text to display in the refresh control. + /// + ///**NOTE**: Available only on iOS. + Future setAttributedTitle(IOSNSAttributedString attributedTitle) async { + Map args = {}; + args.putIfAbsent('attributedTitle', () => attributedTitle.toMap()); + await _channel?.invokeMethod('setAttributedTitle', args); + } + + void initMethodChannel(dynamic id) { + this._channel = + MethodChannel('com.pichillilorenzo/flutter_inappwebview_pull_to_refresh_$id'); + this._channel?.setMethodCallHandler(handleMethod); + } +} \ No newline at end of file diff --git a/lib/src/pull_to_refresh/pull_to_refresh_options.dart b/lib/src/pull_to_refresh/pull_to_refresh_options.dart new file mode 100644 index 00000000..8ce04e0e --- /dev/null +++ b/lib/src/pull_to_refresh/pull_to_refresh_options.dart @@ -0,0 +1,58 @@ +import 'dart:ui'; +import '../util.dart'; +import '../types.dart'; + +class PullToRefreshOptions { + ///Sets whether the pull-to-refresh feature is enabled or not. + bool enabled; + + ///The color of the refresh control. + Color? color; + + ///The background color of the refresh control. + Color? backgroundColor; + + ///The distance to trigger a sync in dips. + /// + ///**NOTE**: Available only on Android. + int? distanceToTriggerSync; + + ///The distance in pixels that the refresh indicator can be pulled beyond its resting position. + /// + ///**NOTE**: Available only on Android. + int? slingshotDistance; + + ///The title text to display in the refresh control. + /// + ///**NOTE**: Available only on iOS. + IOSNSAttributedString? attributedTitle; + + PullToRefreshOptions({ + this.enabled = true, + this.color, + this.backgroundColor, + this.distanceToTriggerSync, + this.slingshotDistance, + this.attributedTitle + }); + + Map toMap() { + return { + "enabled": enabled, + "color": color?.toHex(), + "backgroundColor": backgroundColor?.toHex(), + "distanceToTriggerSync": distanceToTriggerSync, + "slingshotDistance": slingshotDistance, + "attributedTitle": attributedTitle?.toMap() ?? {} + }; + } + + Map toJson() { + return this.toMap(); + } + + @override + String toString() { + return toMap().toString(); + } +} \ No newline at end of file diff --git a/lib/src/types.dart b/lib/src/types.dart index 871115cc..79e08ea4 100755 --- a/lib/src/types.dart +++ b/lib/src/types.dart @@ -2,6 +2,7 @@ import 'dart:collection'; import 'dart:io'; import 'dart:typed_data'; import 'dart:convert'; +import 'dart:ui'; import 'package:flutter/foundation.dart'; @@ -13,6 +14,9 @@ import 'in_app_webview/in_app_webview_controller.dart'; import 'http_auth_credentials_database.dart'; import 'cookie_manager.dart'; import 'web_storage/web_storage.dart'; +import 'pull_to_refresh/pull_to_refresh_controller.dart'; +import 'pull_to_refresh/pull_to_refresh_options.dart'; +import 'util.dart'; ///This type represents a callback, added with [InAppWebViewController.addJavaScriptHandler], that listens to post messages sent from JavaScript. /// @@ -6495,3 +6499,289 @@ class IOSWKWindowFeatures { return toMap().toString(); } } + +///An iOS-specific class that represents a string with associated attributes +///used by the [PullToRefreshController] and [PullToRefreshOptions] classes. +class IOSNSAttributedString { + ///The characters for the new object. + String string; + + ///The color of the background behind the text. + /// + ///The value of this attribute is a [Color] object. + ///Use this attribute to specify the color of the background area behind the text. + ///If you do not specify this attribute, no background color is drawn. + Color? backgroundColor; + + ///The vertical offset for the position of the text. + /// + ///The value of this attribute is a number containing a floating point value indicating the character’s offset from the baseline, in points. + ///The default value is `0`. + double? baselineOffset; + + ///The expansion factor of the text. + /// + ///The value of this attribute is a number containing a floating point value indicating the log of the expansion factor to be applied to glyphs. + ///The default value is `0`, indicating no expansion. + double? expansion; + + ///The color of the text. + /// + ///The value of this attribute is a [Color] object. + ///Use this attribute to specify the color of the text during rendering. + ///If you do not specify this attribute, the text is rendered in black. + Color? foregroundColor; + + ///The kerning of the text. + /// + ///The value of this attribute is a number containing a floating-point value. + ///This value specifies the number of points by which to adjust kern-pair characters. + ///Kerning prevents unwanted space from occurring between specific characters and depends on the font. + ///The value `0` means kerning is disabled. The default value for this attribute is `0`. + double? kern; + + ///The ligature of the text. + /// + ///The value of this attribute is a number containing an integer. + ///Ligatures cause specific character combinations to be rendered using a single custom glyph that corresponds to those characters. + ///The value `0` indicates no ligatures. The value `1` indicates the use of the default ligatures. + ///The value `2` indicates the use of all ligatures. + ///The default value for this attribute is `1`. (Value `2` is unsupported on iOS.) + int? ligature; + + ///The obliqueness of the text. + /// + ///The value of this attribute is a number containing a floating point value indicating skew to be applied to glyphs. + ///The default value is `0`, indicating no skew. + double? obliqueness; + + ///The color of the strikethrough. + /// + ///The value of this attribute is a [Color] object. The default value is `null`, indicating same as foreground color. + Color? strikethroughColor; + + ///The strikethrough style of the text. + /// + ///This value indicates whether the text has a line through it and corresponds to one of the constants described in [IOSNSUnderlineStyle]. + ///The default value for this attribute is [IOSNSUnderlineStyle.STYLE_NONE]. + IOSNSUnderlineStyle? strikethroughStyle; + + ///The color of the stroke. + /// + ///The value of this parameter is a [Color] object. + ///If it is not defined (which is the case by default), it is assumed to be the same as the value of foregroundColor; + ///otherwise, it describes the outline color. + Color? strokeColor; + + ///The width of the stroke. + /// + ///The value of this attribute is a number containing a floating-point value. + ///This value represents the amount to change the stroke width and is specified as a percentage of the font point size. + ///Specify `0` (the default) for no additional changes. + ///Specify positive values to change the stroke width alone. + ///Specify negative values to stroke and fill the text. + ///For example, a typical value for outlined text would be `3.0`. + double? strokeWidth; + + ///The text effect. + /// + ///The value of this attribute is a [IOSNSAttributedStringTextEffectStyle] object. + ///The default value of this property is `null`, indicating no text effect. + IOSNSAttributedStringTextEffectStyle? textEffect; + + ///The color of the underline. + /// + ///The value of this attribute is a [Color] object. + ///The default value is `null`, indicating same as foreground color. + Color? underlineColor; + + ///The underline style of the text. + /// + ///This value indicates whether the text is underlined and corresponds to one of the constants described in [IOSNSUnderlineStyle]. + ///The default value for this attribute is [IOSNSUnderlineStyle.STYLE_NONE]. + IOSNSUnderlineStyle? underlineStyle; + + IOSNSAttributedString({ + required this.string, + this.backgroundColor, + this.baselineOffset, + this.expansion, + this.foregroundColor, + this.kern, + this.ligature, + this.obliqueness, + this.strikethroughColor, + this.strikethroughStyle, + this.strokeColor, + this.strokeWidth, + this.textEffect, + this.underlineColor, + this.underlineStyle, + }); + + Map toMap() { + return { + "string": this.string, + "backgroundColor": this.backgroundColor?.toHex(), + "baselineOffset": this.baselineOffset, + "expansion": this.expansion, + "foregroundColor": this.foregroundColor?.toHex(), + "kern": this.kern, + "ligature": this.ligature, + "obliqueness": this.obliqueness, + "strikethroughColor": this.strikethroughColor?.toHex(), + "strikethroughStyle": this.strikethroughStyle?.toValue(), + "strokeColor": this.strokeColor?.toHex(), + "strokeWidth": this.strokeWidth, + "textEffect": this.textEffect?.toValue(), + "underlineColor": this.underlineColor?.toHex(), + "underlineStyle": this.underlineStyle?.toValue(), + }; + } + + Map toJson() { + return this.toMap(); + } + + @override + String toString() { + return toMap().toString(); + } +} + +///An iOS-specific Class that represents the constants for the underline style and strikethrough style attribute keys. +class IOSNSUnderlineStyle { + final int _value; + + const IOSNSUnderlineStyle._internal(this._value); + + static final Set values = [ + IOSNSUnderlineStyle.STYLE_NONE, + IOSNSUnderlineStyle.SINGLE, + IOSNSUnderlineStyle.THICK, + IOSNSUnderlineStyle.DOUBLE, + IOSNSUnderlineStyle.PATTERN_DOT, + IOSNSUnderlineStyle.PATTERN_DASH, + IOSNSUnderlineStyle.PATTERN_DASH_DOT, + IOSNSUnderlineStyle.PATTERN_DASH_DOT_DOT, + IOSNSUnderlineStyle.BY_WORD, + ].toSet(); + + static IOSNSUnderlineStyle? fromValue(int? value) { + if (value != null) { + try { + return IOSNSUnderlineStyle.values + .firstWhere((element) => element.toValue() == value); + } catch (e) { + return null; + } + } + return null; + } + + int toValue() => _value; + + @override + String toString() { + switch (_value) { + case 1: + return "SINGLE"; + case 2: + return "THICK"; + case 9: + return "DOUBLE"; + case 256: + return "PATTERN_DOT"; + case 512: + return "PATTERN_DASH"; + case 768: + return "PATTERN_DASH_DOT"; + case 1024: + return "PATTERN_DASH_DOT_DOT"; + case 32768: + return "BY_WORD"; + case 0: + default: + return "STYLE_NONE"; + } + } + + ///Do not draw a line. + static const STYLE_NONE = + const IOSNSUnderlineStyle._internal(0); + + ///Draw a single line. + static const SINGLE = + const IOSNSUnderlineStyle._internal(1); + + ///Draw a thick line. + static const THICK = + const IOSNSUnderlineStyle._internal(2); + + ///Draw a double line. + static const DOUBLE = + const IOSNSUnderlineStyle._internal(9); + + ///Draw a line of dots. + static const PATTERN_DOT = + const IOSNSUnderlineStyle._internal(256); + + ///Draw a line of dashes. + static const PATTERN_DASH = + const IOSNSUnderlineStyle._internal(512); + + ///Draw a line of alternating dashes and dots. + static const PATTERN_DASH_DOT = + const IOSNSUnderlineStyle._internal(768); + + ///Draw a line of alternating dashes and two dots. + static const PATTERN_DASH_DOT_DOT = + const IOSNSUnderlineStyle._internal(1024); + + ///Draw the line only beneath or through words, not whitespace. + static const BY_WORD = + const IOSNSUnderlineStyle._internal(32768); + + bool operator ==(value) => value == _value; + + @override + int get hashCode => _value.hashCode; +} + +///An iOS-specific Class that represents the supported proxy types. +class IOSNSAttributedStringTextEffectStyle { + final String _value; + + const IOSNSAttributedStringTextEffectStyle._internal(this._value); + + static final Set values = [ + IOSNSAttributedStringTextEffectStyle.LETTERPRESS_STYLE, + ].toSet(); + + static IOSNSAttributedStringTextEffectStyle? fromValue(String? value) { + if (value != null) { + try { + return IOSNSAttributedStringTextEffectStyle.values + .firstWhere((element) => element.toValue() == value); + } catch (e) { + return null; + } + } + return null; + } + + String toValue() => _value; + + @override + String toString() => _value; + + ///A graphical text effect that gives glyphs the appearance of letterpress printing, which involves pressing the type into the paper. + static const LETTERPRESS_STYLE = + const IOSNSAttributedStringTextEffectStyle._internal( + "letterpressStyle"); + + bool operator ==(value) => value == _value; + + @override + int get hashCode => _value.hashCode; +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 128540c8..93adf074 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_inappwebview description: A Flutter plugin that allows you to add an inline webview, to use an headless webview, and to open an in-app browser window. -version: 5.0.5+3 +version: 5.1.0 homepage: https://github.com/pichillilorenzo/flutter_inappwebview environment: