Added support for pull-to-refresh feature (fix #395), Fixed issue not rendering WebView content when scrolling on iOS (fix #704), Fixed InAppBrowser.openData method, InAppBrowser.initialUserScripts InAppBrowser.id HeadlessInAppWebView.id properties are final now

This commit is contained in:
Lorenzo Pichilli 2021-03-05 23:19:50 +01:00
parent da94374dda
commit b6d4fb9596
43 changed files with 1615 additions and 426 deletions

View File

@ -133,7 +133,7 @@
<entry key="file">
<value>
<list>
<option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/file-6.0.0/lib" />
<option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/file-6.1.0/lib" />
<option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/file-6.0.0-nullsafety.4/lib" />
</list>
</value>
@ -377,7 +377,7 @@
<entry key="process">
<value>
<list>
<option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/process-4.0.0/lib" />
<option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/process-4.1.0/lib" />
<option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/process-4.0.0-nullsafety.4/lib" />
</list>
</value>
@ -441,7 +441,7 @@
<entry key="source_span">
<value>
<list>
<option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/source_span-1.8.0/lib" />
<option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/source_span-1.8.1/lib" />
<option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/source_span-1.8.0-nullsafety.4/lib" />
</list>
</value>
@ -647,7 +647,7 @@
<root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/fake_async-1.2.0/lib" />
<root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/ffi-1.0.0/lib" />
<root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/file-6.0.0-nullsafety.4/lib" />
<root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/file-6.0.0/lib" />
<root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/file-6.1.0/lib" />
<root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_downloader-1.5.2/lib" />
<root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/glob-1.2.0/lib" />
<root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/http_multi_server-2.2.0/lib" />
@ -682,7 +682,7 @@
<root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/plugin_platform_interface-1.1.0-nullsafety.1/lib" />
<root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/pool-1.5.0-nullsafety.3/lib" />
<root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/process-4.0.0-nullsafety.4/lib" />
<root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/process-4.0.0/lib" />
<root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/process-4.1.0/lib" />
<root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/pub_semver-1.4.4/lib" />
<root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/shelf-0.7.5/lib" />
<root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/shelf_packages_handler-2.0.0/lib" />
@ -691,7 +691,7 @@
<root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/source_map_stack_trace-2.1.0-nullsafety.4/lib" />
<root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/source_maps-0.10.10-nullsafety.3/lib" />
<root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/source_span-1.8.0-nullsafety.4/lib" />
<root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/source_span-1.8.0/lib" />
<root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/source_span-1.8.1/lib" />
<root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/stack_trace-1.10.0-nullsafety.6/lib" />
<root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/stack_trace-1.10.0/lib" />
<root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/stream_channel-2.1.0-nullsafety.3/lib" />

View File

@ -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`

237
README.md
View File

@ -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<MyApp> {
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<MyApp> {
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('InAppWebView Example'),
),
body: Container(
child: Column(children: <Widget>[
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: <Widget>[
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<String> 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: <Widget>[
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: <Widget>[
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<MyApp> {
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

View File

@ -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'
}
}

View File

@ -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<String, Object> pullToRefreshInitialOptions = (Map<String, Object>) 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) {

View File

@ -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<String, Object> urlRequest = (Map<String, Object>) call.argument("urlRequest");
Map<String, Object> options = (Map<String, Object>) call.argument("options");
Map<String, Object> contextMenu = (Map<String, Object>) call.argument("contextMenu");
Integer windowId = (Integer) call.argument("windowId");
List<Map<String, Object>> initialUserScripts = (List<Map<String, Object>>) 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<String, Object> options = (Map<String, Object>) call.argument("options");
Map<String, Object> contextMenu = (Map<String, Object>) call.argument("contextMenu");
Integer windowId = (Integer) call.argument("windowId");
List<Map<String, Object>> initialUserScripts = (List<Map<String, Object>>) call.argument("initialUserScripts");
openFile(activity, id, assetFilePath, options, contextMenu, windowId, initialUserScripts);
}
result.success(true);
break;
case "openData":
{
String id = (String) call.argument("id");
Map<String, Object> options = (Map<String, Object>) 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<String, Object> contextMenu = (Map<String, Object>) call.argument("contextMenu");
Integer windowId = (Integer) call.argument("windowId");
List<Map<String, Object>> initialUserScripts = (List<Map<String, Object>>) call.argument("initialUserScripts");
openData(activity, id, options, data, mimeType, encoding, baseUrl, historyUrl, contextMenu, windowId, initialUserScripts);
}
case "open":
open(activity, (Map<String, Object>) 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<String, Object> urlRequest, Map<String, Object> options,
Map<String, Object> contextMenu, Integer windowId, List<Map<String, Object>> initialUserScripts) {
public void open(Activity activity, Map<String, Object> arguments) {
String id = (String) arguments.get("id");
Map<String, Object> urlRequest = (Map<String, Object>) 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<String, Object> options = (Map<String, Object>) arguments.get("options");
Map<String, Object> contextMenu = (Map<String, Object>) arguments.get("contextMenu");
Integer windowId = (Integer) arguments.get("windowId");
List<Map<String, Object>> initialUserScripts = (List<Map<String, Object>>) arguments.get("initialUserScripts");
Map<String, Object> pullToRefreshInitialOptions = (Map<String, Object>) 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<String, Object> options,
Map<String, Object> contextMenu, Integer windowId, List<Map<String, Object>> 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<String, Object> options, String data, String mimeType, String encoding,
String baseUrl, String historyUrl, Map<String, Object> contextMenu, Integer windowId, List<Map<String, Object>> 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);
}

View File

@ -15,7 +15,7 @@ public class InAppBrowserOptions implements Options<InAppBrowserActivity> {
public Boolean hidden = false;
public Boolean hideToolbarTop = false;
@Nullable
public String toolbarTopBackgroundColor = null;
public String toolbarTopBackgroundColor;
@Nullable
public String toolbarTopFixedTitle;
public Boolean hideUrlBar = false;

View File

@ -35,7 +35,7 @@ public class ContextMenuOptions implements Options<Object> {
}
@Override
public Map<String, Object> getRealOptions(Object webView) {
public Map<String, Object> getRealOptions(Object obj) {
Map<String, Object> realOptions = toMap();
return realOptions;
}

View File

@ -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<String, Object> params, View containerView) {
channel = new MethodChannel(messenger, "com.pichillilorenzo/flutter_inappwebview_" + id);
@ -53,6 +55,7 @@ public class FlutterWebView implements PlatformView {
Map<String, Object> contextMenu = (Map<String, Object>) params.get("contextMenu");
Integer windowId = (Integer) params.get("windowId");
List<Map<String, Object>> initialUserScripts = (List<Map<String, Object>>) params.get("initialUserScripts");
Map<String, Object> pullToRefreshInitialOptions = (Map<String, Object>) 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();

View File

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

View File

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

View File

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

View File

@ -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<String, Object> 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);
}
}
}

View File

@ -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<PullToRefreshLayout> {
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<String, Object> options) {
for (Map.Entry<String, Object> 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<String, Object> toMap() {
Map<String, Object> 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<String, Object> getRealOptions(PullToRefreshLayout pullToRefreshLayout) {
Map<String, Object> realOptions = toMap();
return realOptions;
}
}

View File

@ -10,10 +10,15 @@
tools:context=".in_app_browser.InAppBrowserActivity"
android:focusable="true">
<com.pichillilorenzo.flutter_inappwebview.in_app_webview.InAppWebView
android:id="@+id/webView"
<com.pichillilorenzo.flutter_inappwebview.pull_to_refresh.PullToRefreshLayout
android:id="@+id/pullToRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent">
<com.pichillilorenzo.flutter_inappwebview.in_app_webview.InAppWebView
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.pichillilorenzo.flutter_inappwebview.pull_to_refresh.PullToRefreshLayout>
<ProgressBar
android:id="@+id/progressBar"

View File

@ -1 +1 @@
{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"flutter_downloader","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_downloader-1.5.2/","dependencies":[]},{"name":"flutter_inappwebview","path":"/Users/lorenzopichilli/Desktop/flutter_inappwebview/","dependencies":[]},{"name":"integration_test","path":"/Users/lorenzopichilli/flutter/packages/integration_test/","dependencies":[]},{"name":"path_provider","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-2.0.0-nullsafety/","dependencies":[]},{"name":"permission_handler","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/permission_handler-5.1.0+2/","dependencies":[]},{"name":"url_launcher","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher-6.0.0-nullsafety.6/","dependencies":[]}],"android":[{"name":"flutter_downloader","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_downloader-1.5.2/","dependencies":[]},{"name":"flutter_inappwebview","path":"/Users/lorenzopichilli/Desktop/flutter_inappwebview/","dependencies":[]},{"name":"integration_test","path":"/Users/lorenzopichilli/flutter/packages/integration_test/","dependencies":[]},{"name":"path_provider","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-2.0.0-nullsafety/","dependencies":[]},{"name":"permission_handler","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/permission_handler-5.1.0+2/","dependencies":[]},{"name":"url_launcher","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher-6.0.0-nullsafety.6/","dependencies":[]}],"macos":[{"name":"path_provider_macos","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_macos-0.0.5-nullsafety/","dependencies":[]},{"name":"url_launcher_macos","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_macos-0.1.0-nullsafety.2/","dependencies":[]}],"linux":[{"name":"path_provider_linux","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_linux-0.2.0-nullsafety/","dependencies":[]},{"name":"url_launcher_linux","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_linux-0.1.0-nullsafety.3/","dependencies":[]}],"windows":[{"name":"path_provider_windows","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_windows-0.1.0-nullsafety.3/","dependencies":[]},{"name":"url_launcher_windows","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_windows-0.1.0-nullsafety.2/","dependencies":[]}],"web":[]},"dependencyGraph":[{"name":"flutter_downloader","dependencies":[]},{"name":"flutter_inappwebview","dependencies":[]},{"name":"integration_test","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_macos","path_provider_linux","path_provider_windows"]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_macos","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"permission_handler","dependencies":[]},{"name":"url_launcher","dependencies":["url_launcher_linux","url_launcher_macos","url_launcher_windows"]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2021-03-02 01:01:13.621928","version":"1.27.0-4.0.pre"}
{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"flutter_downloader","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_downloader-1.5.2/","dependencies":[]},{"name":"flutter_inappwebview","path":"/Users/lorenzopichilli/Desktop/flutter_inappwebview/","dependencies":[]},{"name":"integration_test","path":"/Users/lorenzopichilli/flutter/packages/integration_test/","dependencies":[]},{"name":"path_provider","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-2.0.0-nullsafety/","dependencies":[]},{"name":"permission_handler","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/permission_handler-5.1.0+2/","dependencies":[]},{"name":"url_launcher","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher-6.0.0-nullsafety.6/","dependencies":[]}],"android":[{"name":"flutter_downloader","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_downloader-1.5.2/","dependencies":[]},{"name":"flutter_inappwebview","path":"/Users/lorenzopichilli/Desktop/flutter_inappwebview/","dependencies":[]},{"name":"integration_test","path":"/Users/lorenzopichilli/flutter/packages/integration_test/","dependencies":[]},{"name":"path_provider","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-2.0.0-nullsafety/","dependencies":[]},{"name":"permission_handler","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/permission_handler-5.1.0+2/","dependencies":[]},{"name":"url_launcher","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher-6.0.0-nullsafety.6/","dependencies":[]}],"macos":[{"name":"path_provider_macos","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_macos-0.0.5-nullsafety/","dependencies":[]},{"name":"url_launcher_macos","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_macos-0.1.0-nullsafety.2/","dependencies":[]}],"linux":[{"name":"path_provider_linux","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_linux-0.2.0-nullsafety/","dependencies":[]},{"name":"url_launcher_linux","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_linux-0.1.0-nullsafety.3/","dependencies":[]}],"windows":[{"name":"path_provider_windows","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_windows-0.1.0-nullsafety.3/","dependencies":[]},{"name":"url_launcher_windows","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_windows-0.1.0-nullsafety.2/","dependencies":[]}],"web":[]},"dependencyGraph":[{"name":"flutter_downloader","dependencies":[]},{"name":"flutter_inappwebview","dependencies":[]},{"name":"integration_test","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_macos","path_provider_linux","path_provider_windows"]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_macos","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"permission_handler","dependencies":[]},{"name":"url_launcher","dependencies":["url_launcher_linux","url_launcher_macos","url_launcher_windows"]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2021-03-05 22:47:49.023558","version":"2.1.0-10.0.pre"}

View File

@ -86,7 +86,7 @@ void main() {
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);
},
@ -96,7 +96,7 @@ void main() {
final InAppWebViewController controller =
await controllerCompleter.future;
final String? currentUrl = (await controller.getUrl())?.toString();
expect(currentUrl, 'https://flutter.dev/');
expect(currentUrl, 'https://github.com/flutter');
});
testWidgets('set/get options', (WidgetTester tester) async {
@ -109,7 +109,7 @@ void main() {
child: InAppWebView(
key: GlobalKey(),
initialUrlRequest:
URLRequest(url: Uri.parse('https://flutter.dev/')),
URLRequest(url: Uri.parse('https://github.com/flutter')),
initialOptions: InAppWebViewGroupOptions(
crossPlatform: InAppWebViewOptions(javaScriptEnabled: false)),
onWebViewCreated: (controller) {
@ -318,7 +318,7 @@ void main() {
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);
},
@ -331,7 +331,7 @@ void main() {
final InAppWebViewController controller =
await controllerCompleter.future;
var url = await pageLoads.stream.first;
expect(url, 'https://flutter.dev/');
expect(url, 'https://github.com/flutter');
await controller.loadUrl(
urlRequest: URLRequest(url: Uri.parse('https://www.google.com/')));
@ -353,7 +353,7 @@ void main() {
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);
},
@ -1528,7 +1528,7 @@ void main() {
child: InAppWebView(
key: GlobalKey(),
initialUrlRequest:
URLRequest(url: Uri.parse('https://flutter.dev/')),
URLRequest(url: Uri.parse('https://github.com/flutter')),
initialOptions: InAppWebViewGroupOptions(
ios: IOSInAppWebViewOptions(
allowsBackForwardNavigationGestures: true)),
@ -1542,8 +1542,8 @@ void main() {
final InAppWebViewController controller =
await controllerCompleter.future;
final String? currentUrl = (await controller.getUrl())?.toString();
expect(currentUrl, contains('flutter.dev'));
});
expect(currentUrl, 'https://github.com/flutter');
}, skip: !Platform.isIOS);
testWidgets('target _blank opens in same window',
(WidgetTester tester) async {
@ -2497,7 +2497,7 @@ setTimeout(function() {
child: InAppWebView(
key: GlobalKey(),
initialUrlRequest:
URLRequest(url: Uri.parse('https://flutter.dev/')),
URLRequest(url: Uri.parse('https://github.com/flutter')),
initialOptions: InAppWebViewGroupOptions(
crossPlatform: InAppWebViewOptions(
clearCache: true,
@ -2560,7 +2560,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);
},
@ -2634,7 +2634,7 @@ setTimeout(function() {
child: InAppWebView(
key: GlobalKey(),
initialUrlRequest:
URLRequest(url: Uri.parse('https://flutter.dev/')),
URLRequest(url: Uri.parse('https://github.com/flutter')),
onLoadStop: (controller, url) async {
await controller.evaluateJavascript(source: "window.print();");
},
@ -2645,7 +2645,7 @@ setTimeout(function() {
),
);
final String url = await onPrintCompleter.future;
expect(url, 'https://flutter.dev/');
expect(url, 'https://github.com/flutter');
}, skip: true);
testWidgets('onWindowFocus', (WidgetTester tester) async {
@ -2656,7 +2656,7 @@ setTimeout(function() {
child: InAppWebView(
key: GlobalKey(),
initialUrlRequest:
URLRequest(url: Uri.parse('https://flutter.dev/')),
URLRequest(url: Uri.parse('https://github.com/flutter')),
onLoadStop: (controller, url) async {
await controller.evaluateJavascript(
source: 'window.dispatchEvent(new Event("focus"));');
@ -2678,7 +2678,7 @@ setTimeout(function() {
child: InAppWebView(
key: GlobalKey(),
initialUrlRequest:
URLRequest(url: Uri.parse('https://flutter.dev/')),
URLRequest(url: Uri.parse('https://github.com/flutter')),
onLoadStop: (controller, url) async {
await controller.evaluateJavascript(
source: 'window.dispatchEvent(new Event("blur"));');
@ -2703,7 +2703,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);
},
@ -2715,7 +2715,7 @@ setTimeout(function() {
);
final String? url = await onPageCommitVisibleCompleter.future;
expect(url, 'https://flutter.dev/');
expect(url, 'https://github.com/flutter');
});
testWidgets('onTitleChanged', (WidgetTester tester) async {
@ -2729,7 +2729,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);
},
@ -2903,7 +2903,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);
},
@ -2973,7 +2973,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);
},
@ -3019,7 +3019,7 @@ setTimeout(function() {
child: InAppWebView(
key: GlobalKey(),
initialUrlRequest:
URLRequest(url: Uri.parse('https://flutter.dev/')),
URLRequest(url: Uri.parse('https://github.com/flutter')),
initialOptions: InAppWebViewGroupOptions(
ios: IOSInAppWebViewOptions(useOnNavigationResponse: true)),
onWebViewCreated: (controller) {
@ -3039,7 +3039,7 @@ setTimeout(function() {
await pageLoaded.future;
final String url = await onNavigationResponseCompleter.future;
expect(url, 'https://flutter.dev/');
expect(url, 'https://github.com/flutter');
}, skip: !Platform.isIOS);
testWidgets('cancel navigation', (WidgetTester tester) async {
@ -3055,7 +3055,7 @@ setTimeout(function() {
child: InAppWebView(
key: GlobalKey(),
initialUrlRequest:
URLRequest(url: Uri.parse('https://flutter.dev/')),
URLRequest(url: Uri.parse('https://github.com/flutter')),
initialOptions: InAppWebViewGroupOptions(
ios: IOSInAppWebViewOptions(useOnNavigationResponse: true)),
onWebViewCreated: (controller) {
@ -3074,7 +3074,7 @@ setTimeout(function() {
);
final String url = await onNavigationResponseCompleter.future;
expect(url, 'https://flutter.dev/');
expect(url, 'https://github.com/flutter');
expect(pageLoaded.future, doesNotComplete);
}, skip: !Platform.isIOS);
}, skip: !Platform.isIOS);
@ -3374,7 +3374,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);
},
@ -3387,11 +3387,11 @@ setTimeout(function() {
final InAppWebViewController controller =
await controllerCompleter.future;
String? url = await pageLoads.stream.first;
expect(url, 'https://flutter.dev/');
expect(url, 'https://github.com/flutter');
await controller.reload();
url = await pageLoads.stream.first;
expect(url, 'https://flutter.dev/');
expect(url, 'https://github.com/flutter');
pageLoads.close();
});
@ -3443,6 +3443,7 @@ setTimeout(function() {
expect(webHistory.list![0].url.toString(), 'https://flutter.dev/');
expect(webHistory.list![1].url.toString(), 'https://github.com/flutter');
await Future.delayed(Duration(seconds: 1));
await controller.goBack();
url = await pageLoads.stream.first;
webHistory = await controller.getCopyBackForwardList();
@ -3456,6 +3457,7 @@ setTimeout(function() {
expect(webHistory.list![0].url.toString(), 'https://flutter.dev/');
expect(webHistory.list![1].url.toString(), 'https://github.com/flutter');
await Future.delayed(Duration(seconds: 1));
await controller.goForward();
url = await pageLoads.stream.first;
webHistory = await controller.getCopyBackForwardList();
@ -3469,6 +3471,7 @@ setTimeout(function() {
expect(webHistory.list![0].url.toString(), 'https://flutter.dev/');
expect(webHistory.list![1].url.toString(), 'https://github.com/flutter');
await Future.delayed(Duration(seconds: 1));
await controller.goTo(historyItem: webHistory.list![0]);
url = await pageLoads.stream.first;
webHistory = await controller.getCopyBackForwardList();
@ -4392,6 +4395,36 @@ setTimeout(function() {
expect(await InAppWebViewController.getDefaultUserAgent(), isNotNull);
});
testWidgets('launches with pull-to-refresh feature', (WidgetTester tester) async {
final Completer controllerCompleter = Completer<InAppWebViewController>();
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<void> pageLoaded = Completer<void>();
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<void> pageLoaded = Completer<void>();
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<InAppBrowserNotOpenedException>()));
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<InAppBrowserAlreadyOpenedException>()));
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<MissingPluginException>()));
});
test('openFile and close', () async {
var inAppBrowser = new MyInAppBrowser();
expect(inAppBrowser.isOpened(), false);
expect(() async {
await inAppBrowser.show();
}, throwsA(isInstanceOf<InAppBrowserNotOpenedException>()));
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<InAppBrowserAlreadyOpenedException>()));
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<MissingPluginException>()));
});
test('openFile and close', () async {
var inAppBrowser = new MyInAppBrowser();
expect(inAppBrowser.isOpened(), false);
expect(() async {
await inAppBrowser.show();
}, throwsA(isInstanceOf<InAppBrowserNotOpenedException>()));
await inAppBrowser.openData(data: """
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="https://getbootstrap.com/docs/4.3/dist/css/bootstrap.min.css">
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
</head>
<body>
<img src="https://via.placeholder.com/100x50" alt="placeholder 100x50">
</body>
</html>
""",
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<InAppBrowserAlreadyOpenedException>()));
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<ChromeSafariBrowserAlreadyOpenedException>()));
await expectLater(chromeSafariBrowser.firstPageLoaded.future, completes);

View File

@ -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<InAppBrowserExampleScreen> {
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

View File

@ -17,13 +17,26 @@ class InAppWebViewExampleScreen extends StatefulWidget {
}
class _InAppWebViewExampleScreenState extends State<InAppWebViewExampleScreen> {
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<InAppWebViewExampleScreen> {
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<InAppWebViewExampleScreen> {
" " +
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<InAppWebViewExampleScreen> {
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<InAppWebViewExampleScreen> {
drawer: myDrawer(context: context),
body: SafeArea(
child: Column(children: <Widget>[
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<UserScript>([]),
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<String> 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<UserScript>([]),
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<String> 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<InAppWebViewExampleScreen> {
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();
},
),
],

View File

@ -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:

View File

@ -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)
}

View File

@ -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)

View File

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

View File

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

View File

@ -8,7 +8,7 @@
import Foundation
import WebKit
class InAppWebViewMethodHandler: FlutterMethodCallDelegate {
public class InAppWebViewMethodHandler: FlutterMethodCallDelegate {
var webView: InAppWebView?
init(webView: InAppWebView) {

View File

@ -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) {

View File

@ -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")
}
}

View File

@ -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()
}

View File

@ -0,0 +1,33 @@
//
// PullToRefreshOptions.swift
// flutter_inappwebview
//
// Created by Lorenzo Pichilli on 03/03/21.
//
import Foundation
public class PullToRefreshOptions : Options<PullToRefreshControl> {
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
}
}

View File

@ -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)
}
}

View File

@ -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<UserScript>? initialUserScripts;
final UnmodifiableListView<UserScript>? 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<dynamic> 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!

View File

@ -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<dynamic> 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<UserScript>? initialUserScripts;
@override
final PullToRefreshController? pullToRefreshController;
@override
final void Function(InAppWebViewController controller, Uri? url)?
onPageCommitVisible;

View File

@ -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<UserScript>? initialUserScripts;
@override
final PullToRefreshController? pullToRefreshController;
@override
final ContextMenu? contextMenu;
@ -379,6 +385,7 @@ class _InAppWebViewState extends State<InAppWebView> {
'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<InAppWebView> {
'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<InAppWebView> {
'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<InAppWebView> {
void _onPlatformViewCreated(int id) {
_controller = InAppWebViewController(id, widget);
widget.pullToRefreshController?.initMethodChannel(id);
if (widget.onWebViewCreated != null) {
widget.onWebViewCreated!(_controller);
}

View File

@ -52,15 +52,9 @@ class InAppWebViewController {
HashMap<String, JavaScriptHandlerCallback>();
List<UserScript> _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<UserScript>? initialUserScripts) {
this._inAppBrowserUuid = uuid;
this._channel = channel;
this._inAppBrowser = inAppBrowser;
this._userScripts =

View File

@ -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:

View File

@ -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';

View File

@ -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<UserScript>? 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});
}

View File

@ -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';

View File

@ -0,0 +1,2 @@
export 'pull_to_refresh_controller.dart';
export 'pull_to_refresh_options.dart';

View File

@ -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 views 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<dynamic> 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<void> setEnabled(bool enabled) async {
Map<String, dynamic> args = <String, dynamic>{};
args.putIfAbsent('enabled', () => enabled);
await _channel?.invokeMethod('setEnabled', args);
}
Future<void> _setRefreshing(bool refreshing) async {
Map<String, dynamic> args = <String, dynamic>{};
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<void> 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<void> endRefreshing() async {
await _setRefreshing(false);
}
///Returns whether a refresh operation has been triggered and is in progress.
Future<bool> isRefreshing() async {
Map<String, dynamic> args = <String, dynamic>{};
return await _channel?.invokeMethod('isRefreshing', args);
}
///Sets the color of the refresh control.
Future<void> setColor(Color color) async {
Map<String, dynamic> args = <String, dynamic>{};
args.putIfAbsent('color', () => color.toHex());
await _channel?.invokeMethod('setColor', args);
}
///Sets the background color of the refresh control.
Future<void> setBackgroundColor(Color color) async {
Map<String, dynamic> args = <String, dynamic>{};
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<void> setDistanceToTriggerSync(int distanceToTriggerSync) async {
Map<String, dynamic> args = <String, dynamic>{};
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<void> setSlingshotDistance(int slingshotDistance) async {
Map<String, dynamic> args = <String, dynamic>{};
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<int> getDefaultSlingshotDistance() async {
Map<String, dynamic> args = <String, dynamic>{};
return await _channel?.invokeMethod('getDefaultSlingshotDistance', args);
}
///Sets the styled title text to display in the refresh control.
///
///**NOTE**: Available only on iOS.
Future<void> setAttributedTitle(IOSNSAttributedString attributedTitle) async {
Map<String, dynamic> args = <String, dynamic>{};
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);
}
}

View File

@ -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<String, dynamic> toMap() {
return {
"enabled": enabled,
"color": color?.toHex(),
"backgroundColor": backgroundColor?.toHex(),
"distanceToTriggerSync": distanceToTriggerSync,
"slingshotDistance": slingshotDistance,
"attributedTitle": attributedTitle?.toMap() ?? {}
};
}
Map<String, dynamic> toJson() {
return this.toMap();
}
@override
String toString() {
return toMap().toString();
}
}

View File

@ -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 characters 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<String, dynamic> 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<String, dynamic> 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<IOSNSUnderlineStyle> 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<IOSNSAttributedStringTextEffectStyle> 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;
}

View File

@ -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: