在项目中遇到的第二个坑: android webview 里点击会秒退。
出现这个问题的原因是,我使用的官方的webview_flutter插件没有针对这一功能做适配。而我在网上找到另一款flutter webview插件flutter webview plugin.这个插件针对android端webview上传文件的功能做了适配。
这里第一种思路是直接摒弃原版的webview_flutter插件。发现行不通,因为后者没有实现js channel和一些其他的功能。
第二种思路是把flutter webview plugin插件的文件上传部分的代码迁移到webview_flutter中。
在代码迁移之前,首先说一下插件的代码是从哪开始走的。
在flutter工程的android/app/src/main/java/…/mainActivity里边有这一行代码
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//注册插件
GeneratedPluginRegistrant.registerWith(this);
}
在下面目录的GeneratedPluginRegistrant类里
FlutterWebviewPlugin.registerWith(registry.registrarFor("com.flutter_webview_plugin.FlutterWebviewPlugin"));
WebViewFlutterPlugin.registerWith(registry.registrarFor("io.flutter.plugins.webviewflutter.WebViewFlutterPlugin"));
然后去插件文件夹里找registerWith这个静态方法。
public static void registerWith(Registrar registrar) {
factory = new WebViewFactory(registrar.messenger(), registrar.view());
registrar
.platformViewRegistry()
.registerViewFactory(
"plugins.flutter.io/webview",
factory);
final WebViewFlutterPlugin instance = new WebViewFlutterPlugin(registrar.activity(),registrar.activeContext());
registrar.addActivityResultListener(instance);
FlutterCookieManager.registerWith(registrar.messenger());
}
然后就是插件的内部逻辑了。
不论代码怎么变,Android webview文件上传始终是那个思路。
点击选择文件的时候,会调用 webviewClient 下的openFileChooser()
(5.0及以上系统回调onShowFileChooser()
),这个时候我们在openFileChooser
方法中通过Intent
打开系统相册或者支持该Intent
的第三方应用来拿到选择文件的uri,最后在onActivityResult()中将选择的文件uri通过ValueCallback
的onReceiveValue
方法返回给WebView
,然后通过js上传。
我在这里简单介绍一下代码迁移的思路方法。我贴出的代码直接拷贝可能不能用,因为在其他细节的地方我还做了一些类型的适配,在这里不一一介绍。在文章尾我会贴出我修改后的webview_flutter插件的代码,可以拿来直接用。
在webview_flutter插件目录/plugins/webviewflutter/flutterWebview.java中。找到
webView = new InputAwareWebView(activityContext, containerView);
webview的定义,然后在后面实现并根据各android版本重写onShowFileChooser();
webView.setWebChromeClient(new WebChromeClient()
{
//The undocumented magic method override
//Eclipse will swear at you if you try to put @Override here
// For Android 3.0+
public void openFileChooser(ValueCallback uploadMsg) {
mUploadMessage = uploadMsg;
Intent i = new Intent(Intent.ACTION_GET_CONTENT);
i.addCategory(Intent.CATEGORY_OPENABLE);
i.setType("image/*");
activity.startActivityForResult(Intent.createChooser(i,"File Chooser"), FILECHOOSER_RESULTCODE);
}
// For Android 3.0+
public void openFileChooser( ValueCallback uploadMsg, String acceptType ) {
mUploadMessage = uploadMsg;
Intent i = new Intent(Intent.ACTION_GET_CONTENT);
i.addCategory(Intent.CATEGORY_OPENABLE);
i.setType("*/*");
activity.startActivityForResult(
Intent.createChooser(i, "File Browser"),
FILECHOOSER_RESULTCODE);
}
//For Android 4.1
public void openFileChooser(ValueCallback uploadMsg, String acceptType, String capture){
mUploadMessage = uploadMsg;
Intent i = new Intent(Intent.ACTION_GET_CONTENT);
i.addCategory(Intent.CATEGORY_OPENABLE);
i.setType("image/*");
activity.startActivityForResult( Intent.createChooser( i, "File Chooser" ), FILECHOOSER_RESULTCODE );
}
//For Android 5.0+
public boolean onShowFileChooser(
WebView webView, ValueCallback filePathCallback,
FileChooserParams fileChooserParams){
if(mUploadMessageArray != null){
mUploadMessageArray.onReceiveValue(null);
}
mUploadMessageArray = filePathCallback;
final String[] acceptTypes = getSafeAcceptedTypes(fileChooserParams);
List intentList = new ArrayList();
fileUri = null;
videoUri = null;
if (acceptsImages(acceptTypes)) {
Intent takePhotoIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
fileUri = getOutputFilename(MediaStore.ACTION_IMAGE_CAPTURE);
takePhotoIntent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
intentList.add(takePhotoIntent);
}
if (acceptsVideo(acceptTypes)) {
Intent takeVideoIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
videoUri = getOutputFilename(MediaStore.ACTION_VIDEO_CAPTURE);
takeVideoIntent.putExtra(MediaStore.EXTRA_OUTPUT, videoUri);
intentList.add(takeVideoIntent);
}
Intent contentSelectionIntent;
if (Build.VERSION.SDK_INT >= 21) {
final boolean allowMultiple = fileChooserParams.getMode() == FileChooserParams.MODE_OPEN_MULTIPLE;
contentSelectionIntent = fileChooserParams.createIntent();
contentSelectionIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple);
} else {
contentSelectionIntent = new Intent(Intent.ACTION_GET_CONTENT);
contentSelectionIntent.addCategory(Intent.CATEGORY_OPENABLE);
contentSelectionIntent.setType("*/*");
}
Intent[] intentArray = intentList.toArray(new Intent[intentList.size()]);
Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER);
chooserIntent.putExtra(Intent.EXTRA_INTENT, contentSelectionIntent);
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, intentArray);
activity.startActivityForResult(chooserIntent, FILECHOOSER_RESULTCODE);
return true;
}
@Override
public void onProgressChanged(WebView view, int progress) {
Map args = new HashMap<>();
args.put("progress", progress / 100.0);
FlutterWebviewPlugin.channel.invokeMethod("onProgressChanged", args);
}
public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
callback.invoke(origin, true, false);
}
这样,就打通了第一个环节,能调用原生的选择文件功能了。
接下来参考flutter_webview_plugin的思路,写一个内部类拿到这个选择文件的uri。
@TargetApi(7)
class ResultHandler {
public boolean handleResult(int requestCode, int resultCode, Intent intent){
boolean handled = false;
if(Build.VERSION.SDK_INT >= 21){
if(requestCode == FILECHOOSER_RESULTCODE){
Uri[] results = null;
if (resultCode == Activity.RESULT_OK) {
if (fileUri != null && getFileSize(fileUri) > 0) {
results = new Uri[] { fileUri };
} else if (videoUri != null && getFileSize(videoUri) > 0) {
results = new Uri[] { videoUri };
} else if (intent != null) {
results = getSelectedFiles(intent);
}
}
if(mUploadMessageArray != null){
mUploadMessageArray.onReceiveValue(results);
mUploadMessageArray = null;
}
handled = true;
}
}else {
if (requestCode == FILECHOOSER_RESULTCODE) {
Uri result = null;
if (resultCode == RESULT_OK && intent != null) {
result = intent.getData();
}
if (mUploadMessage != null) {
mUploadMessage.onReceiveValue(result);
mUploadMessage = null;
}
handled = true;
}
}
return handled;
}
}
这样实现了第二步,拿到uri,
然后就是通过onActivityResult()中将选择的文件uri通过ValueCallback
的onReceiveValue
方法返回给WebView
,然后通过js上传。
onActivityResult()方法需要在插件的注册类中实现。
webview_flutter的注册类是WebViewFlutterPlugin,在这个类中实现PluginRegistry.ActivityResultListener接口,然后重写onActivityResult()方法。
/** WebViewFlutterPlugin */
public class WebViewFlutterPlugin implements PluginRegistry.ActivityResultListener{
/** Plugin registration. */
private Activity activity;
private Context context;
//add for file
// private ValueCallback mUploadMessage;
// private ValueCallback mUploadMessageArray;
private final static int FILECHOOSER_RESULTCODE=1;
// private Uri fileUri;
// private Uri videoUri;
private static WebViewFactory factory;
private WebViewFactory factory2;
public static void registerWith(Registrar registrar) {
factory = new WebViewFactory(registrar.messenger(), registrar.view());
registrar
.platformViewRegistry()
.registerViewFactory(
"plugins.flutter.io/webview",
factory);
final WebViewFlutterPlugin instance = new WebViewFlutterPlugin(registrar.activity(),registrar.activeContext());
registrar.addActivityResultListener(instance);
FlutterCookieManager.registerWith(registrar.messenger());
}
private WebViewFlutterPlugin(Activity activity, Context context){
this.activity = activity;
this.context = context;
}
@Override
public boolean onActivityResult(int i, int i1, Intent intent) {
if(factory!=null){
factory2 = factory;
System.out.println(factory2.getFlutterWebView() != null);
if (factory2.getFlutterWebView() != null) {
return factory2.getFlutterWebView().resultHandler.handleResult(i, i1, intent);
}
return false;
}else{
WebViewFactory flutterWebViewFactory = new WebViewFactory(null,null);
if(flutterWebViewFactory!=null){
return flutterWebViewFactory.getFlutterWebView().resultHandler.handleResult(i, i1, intent);
}
return false;
}
}
}
第三步完成,写web的小伙伴终于可以拿到你选的文件了,喜大普奔。
完整的webview_flutter插件目录:百度云
本文链接: https://shuai0034.top/posts/flutter_webview_upload_bug/