Android webview_flutter外掛的優化與完善
Android webview_flutter外掛的優化與完善
Android webview_flutter 官方最新版本外掛存在的問題:
在我們專案開發過程中使用webview_flutter的時候主要遇到了以下問題:
- 長按 選擇、全選、複製 無法正常使用
- 視訊播放無法全屏,前後臺切換無法停止、繼續播放,按物理鍵返回的時候無法退出全屏
- 無法支援前端定位
- 不支援檔案選擇
- 不能使用select標籤
- 首次載入webview會顯示黑屏
- 預設錯誤頁面顯示、注入自定義字型
- 密碼輸入在Android 10的部分機型上無法正常使用,鍵盤出不來或崩潰
前面7個都已經解決了, 第八個仍然沒有好的方案,只做到了規避崩潰,前面7個的解決方案我分享給大家,有需要的可以自取。
第八個希望能與大家交流歡迎指教。
密碼的問題分支得出的原因是國內 手機的 安全密碼鍵盤導致的失焦問題
感興趣的可關注一下issue:
https://github.com/flutter/flutter/issues/21911
https://github.com/flutter/flutter/issues/19718
https://github.com/flutter/flutter/issues/58943
和 libo1223同學一直探討方案,最接近解決的方案是巢狀一層SingleChildScrollView,但是仍不是完美的解決方案,仍然會有問題
前面幾個問題的解決方法
長按 選擇、全選、複製 無法正常使用
這塊問題很早發現了,大家也都提出了 issue 比如:
https://github.com/flutter/flutter/issues/24584
https://github.com/flutter/flutter/issues/24585
其中yenole給出瞭解決方案https://github.com/yenole/plugins,我這裡解決此問題也是按照他的方案實現的,目前標籤還基本正常,部分裝置小概率會引起UI異常,但總體是可以的
具體實現步驟:
-
重寫外掛中 InputAwareWebView 的 startActionMode方法,原生自定義長按操作框
InputAwareWebView中的關鍵code:-
private MotionEvent ev; @Override public boolean dispatchTouchEvent(MotionEvent ev) { this.ev = ev; return super.dispatchTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { //手勢攔截,取消之前的彈框 if (event.getAction() == MotionEvent.ACTION_DOWN && floatingActionView != null) { this.removeView(floatingActionView); floatingActionView = null; } return super.onTouchEvent(event); } @Override public ActionMode startActionMode(ActionMode.Callback callback) { return rebuildActionMode(super.startActionMode(callback), callback); } @Override public ActionMode startActionMode(ActionMode.Callback callback, int type) { return rebuildActionMode(super.startActionMode(callback, type), callback); } private LinearLayout floatingActionView; /** 自定義長按彈框 */ private ActionMode rebuildActionMode( final ActionMode actionMode, final ActionMode.Callback callback) { if (floatingActionView != null) { this.removeView(floatingActionView); floatingActionView = null; } floatingActionView = (LinearLayout) LayoutInflater.from(getContext()).inflate(R.layout.floating_action_mode, null); for (int i = 0; i < actionMode.getMenu().size(); i++) { final MenuItem menu = actionMode.getMenu().getItem(i); TextView text = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.floating_action_mode_item, null); text.setText(menu.getTitle()); floatingActionView.addView(text); text.setOnClickListener( new OnClickListener() { @Override public void onClick(View view) { InputAwareWebView.this.removeView(floatingActionView); floatingActionView = null; callback.onActionItemClicked(actionMode, menu); } }); // supports up to 4 options if (i >= 4) break; } final int x = (int) ev.getX(); final int y = (int) ev.getY(); floatingActionView .getViewTreeObserver() .addOnGlobalLayoutListener( new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { if (Build.VERSION.SDK_INT >= 16) { floatingActionView.getViewTreeObserver().removeOnGlobalLayoutListener(this); } else { floatingActionView.getViewTreeObserver().removeGlobalOnLayoutListener(this); } onFloatingActionGlobalLayout(x, y); } }); this.addView(floatingActionView, new AbsoluteLayout.LayoutParams(-2, -2, x, y)); actionMode.getMenu().clear(); return actionMode; } /** 定位長按彈框的位置 */ private void onFloatingActionGlobalLayout(int x, int y) { int maxWidth = InputAwareWebView.this.getWidth(); int maxHeight = InputAwareWebView.this.getHeight(); int width = floatingActionView.getWidth(); int height = floatingActionView.getHeight(); int curx = x - width / 2; if (curx < 0) { curx = 0; } else if (curx + width > maxWidth) { curx = maxWidth - width; } int cury = y + 10; if (cury + height > maxHeight) { cury = y - height - 10; } InputAwareWebView.this.updateViewLayout( floatingActionView, new AbsoluteLayout.LayoutParams(-2, -2, curx, cury + InputAwareWebView.this.getScrollY())); floatingActionView.setAlpha(1); }
-
-
webview 的手勢識別給到最大的EagerGestureRecognizer 否則會出現無法識別長按手機的問題
在使用webview_flutter 的地方或者直接擴充套件到外掛裡的 AndroidWebView 中:gestureRecognizers: Platform.isAndroid ? (Set()..add(Factory<EagerGestureRecognizer>(() => EagerGestureRecognizer()))) : null,
視訊播放無法全屏,前後臺切換無法停止、繼續播放,按物理鍵返回的時候無法退出全屏 以及無法定位
-
關於不能全屏、無法定位
這塊應該是和原生中初始的webview一致,預設不支援視訊全屏,解決辦法與原生中擴充套件類似
FlutterWebView內新增自定義的WebChromeClient ,關鍵code:-
class CustomWebChromeClient extends WebChromeClient { View myVideoView; CustomViewCallback callback; @Override public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) { callback.invoke(origin, true, false); super.onGeolocationPermissionsShowPrompt(origin, callback); } @Override public void onShowCustomView(View view, CustomViewCallback customViewCallback) { webView.setVisibility(View.GONE); ViewGroup rootView = mActivity.findViewById(android.R.id.content); rootView.addView(view); myVideoView = view; callback = customViewCallback; isFullScreen = true; } @Override public void onHideCustomView() { if (callback != null) { callback.onCustomViewHidden(); callback = null; } if (myVideoView != null) { ViewGroup rootView = mActivity.findViewById(android.R.id.content); rootView.removeView(myVideoView); myVideoView = null; webView.setVisibility(View.VISIBLE); } isFullScreen = false; } public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType) { Log.i("test", "openFileChooser"); FlutterWebView.this.uploadFile = uploadMsg; openFileChooseProcess(); } public void openFileChooser(ValueCallback<Uri> uploadMsgs) { Log.i("test", "openFileChooser 2"); FlutterWebView.this.uploadFile = uploadMsgs; openFileChooseProcess(); } // For Android > 4.1.1 public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) { Log.i("test", "openFileChooser 3"); FlutterWebView.this.uploadFile = uploadMsg; openFileChooseProcess(); } public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, WebChromeClient.FileChooserParams fileChooserParams) { Log.i("test", "openFileChooser 4:" + filePathCallback.toString()); FlutterWebView.this.uploadFiles = filePathCallback; openFileChooseProcess(); return true; } private void openFileChooseProcess() { Intent i = new Intent(Intent.ACTION_GET_CONTENT); i.addCategory(Intent.CATEGORY_OPENABLE); i.setType("*/*"); mActivity.startActivityForResult(Intent.createChooser(i, "test"), 1303); } @Override public Bitmap getDefaultVideoPoster() { return Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); } }
-
-
按物理鍵返回的時候無法退出全屏,前後臺切換無法停止、繼續播放
按物理鍵返回的時候無法退出全屏,這塊主要是因為屋裡返回鍵在flutter中,被flutter捕獲消耗了,解決方案,webview 外掛攔截物理返回鍵,自定義退出全屏的方法,呼叫
關鍵code如下:
FlutterWebView :-
private void exitFullScreen(Result result) { if (isFullScreen && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if ((null != (webView.getWebChromeClient()))) { (webView.getWebChromeClient()).onHideCustomView(); result.success(false); return; } } result.success(true); }
在互動方法 onMethodCall 中擴充套件 exitFullScreen 方法
case "exitFullScreen":
exitFullScreen(result);
break;
給 controller 擴充套件 exitFullScreen 方法 code 略
在 webView_flutter.dart的Webview中修改build方法,
關鍵code:@override Widget build(BuildContext context) { Widget _webview = WebView.platform.build( context: context, onWebViewPlatformCreated: _onWebViewPlatformCreated, webViewPlatformCallbacksHandler: _platformCallbacksHandler, gestureRecognizers: widget.gestureRecognizers, creationParams: _creationParamsfromWidget(widget), ); if (Platform.isAndroid) { return WillPopScope( child: _webview, onWillPop: () async { try { var controller = await _controller.future; if (null != controller) { return await controller.exitFullScreen(); } } catch (e) {} return true; }, ); } return _webview; }
-
-
前後臺切換無法停止、繼續播放
前後臺切換無法暫停、繼續播放視訊,主要是因為 webview_flutter 感知不到應用前後臺的切換,這塊的解決方案是 外掛擴充套件對前後臺的監聽,主動呼叫 webview 的暫停和繼續播放的方法
這裡需要引入flutter_plugin_android_lifecycle外掛,用來監聽應用的宣告週期:
引入方式 yaml檔案中新增 flutter_plugin_android_lifecycle: ^1.0.6
之後再 WebViewFlutterPlugin 檔案擴充套件宣告週期的監聽,關鍵code如下:@Override public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { BinaryMessenger messenger = mBinding.getBinaryMessenger(); //關注一下這裡的空的問題 final WebViewFactory factory = new WebViewFactory(messenger, /*containerView=*/ null, binding.getActivity()); mBinding.getPlatformViewRegistry() .registerViewFactory( "plugins.flutter.io/webview", factory); flutterCookieManager = new FlutterCookieManager(messenger); Lifecycle lifeCycle = FlutterLifecycleAdapter.getActivityLifecycle(binding); lifeCycle.addObserver(new DefaultLifecycleObserver() { @Override public void onPause(@NonNull LifecycleOwner owner) { factory.onPause(); } @Override public void onResume(@NonNull LifecycleOwner owner) { factory.onResume(); } @Override public void onDestroy(@NonNull LifecycleOwner owner) { } @Override public void onCreate(@NonNull LifecycleOwner owner) { } @Override public void onStop(@NonNull LifecycleOwner owner) { } @Override public void onStart(@NonNull LifecycleOwner owner) { } }); }
WebViewFactory中擴充套件onPause,onResume 方法,用來下傳應用的前後臺切換事件:
WebViewFactory 中關鍵code:-
public PlatformView create(Context context, int id, Object args) { Map<String, Object> params = (Map<String, Object>) args; flutterWebView = new FlutterWebView(context, messenger, id, params, containerView, mActivity); return flutterWebView; } public void onPause() { if (null != flutterWebView && null != flutterWebView.webView) { flutterWebView.webView.onPause(); } } public void onResume() { if (null != flutterWebView && null != flutterWebView.webView) { flutterWebView.webView.onResume(); } }
不支援檔案選擇
- 同樣需要自定義WebChromeClient,擴充套件檔案選擇的方法,參照CustomWebChromeClient中的openFilexxx 方法,主要使用intent 開啟檔案選擇
-
重點是獲取 intent 傳遞回來的資料, 使用webview_flutter 外掛的地方dart中無法直接像在android中那樣,重寫Activity的onActivityResult方法,好在 ActivityPluginBinding 中可以注入ActivityResult監聽,這樣我們就能直接在外掛中處理了。
WebViewFlutterPlugin 關鍵code如下:-
public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { BinaryMessenger messenger = mBinding.getBinaryMessenger(); //關注一下這裡的空的問題 final WebViewFactory factory = new WebViewFactory(messenger, /*containerView=*/ null, binding.getActivity()); mBinding.getPlatformViewRegistry().registerViewFactory("plugins.flutter.io/webview", factory); binding.addActivityResultListener(factory); }
binding.addActivityResultListener(factory); 即為重點,
WebViewFactory 需要實現 PluginRegistry.ActivityResultListener 介面,
並重寫onActivityResult方法,把我們選擇的資料傳遞給 webview,FlutterWebView需要擴充套件onActivityResult方法
WebViewFactory 關鍵code:@Override public boolean onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == 1303) { flutterWebView.onActivityResult(requestCode, resultCode, data); return true; } return false; }
FlutterWebView需要擴充套件onActivityResult方法
-
public void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == Activity.RESULT_OK) { switch (requestCode) { case 1303: if (null != uploadFile) { Uri result = data == null || resultCode != Activity.RESULT_OK ? null : data.getData(); uploadFile.onReceiveValue(result); uploadFile = null; } if (null != uploadFiles) { Uri result = data == null || resultCode != Activity.RESULT_OK ? null : data.getData(); uploadFiles.onReceiveValue(new Uri[]{result}); uploadFiles = null; } break; default: break; } } else { if (null != uploadFile) { uploadFile.onReceiveValue(null); uploadFile = null; } if (null != uploadFiles) { uploadFiles.onReceiveValue(null); uploadFiles = null; } } }
-
不能使用select標籤
不能使用select標籤 是引起外掛中webview構造的時候傳遞的是 Context不是Activity 在展示Dialog的時候出現了異常, 修改方法也就是將webview的時候傳遞Activity進去 ,關鍵code 略
初次載入webview會顯示黑屏
這塊可能是繪製的問題,檢視flutter中的AndroidView程式碼追蹤可最終發現 外掛view在flutter中顯示的奧祕:
解決辦法,沒有好的辦法,不能直接解決,可以曲線搞定,
我的方案是 在專案中封裝一層自己的 webview widget , 稱之為 progress_webveiw
重點在於,預設頁面載入的時候 使用進度頁面 來覆蓋住webview,直到頁面載入完成,這樣就可以規避webview的黑屏問題
大致的程式碼可參照下圖:
預設錯誤頁面顯示、注入自定義字型
-
錯誤頁面的顯示,需要dart和Java層同時處理,否則容易看到 webview預設的醜醜的錯誤頁面,但是個人覺得webview預設的醜醜的錯誤頁 顯示出來也不是啥問題,奈何產品非得要臉…
flutter 層就是封裝的progress_webveiw,載入中,加載出錯都是用 placehoder 層遮罩處理。
但是在重新載入的時候setState 的一瞬間還是可以看到 webview預設的醜醜的錯誤頁,這就需要外掛的Java層也處理一下了
2.關於 注入自定義字型 我們的方案仍是通過原生注入, 測試對別在flutter中注入的時候 感覺效率有點低,頁面會有二次刷字型的感覺,放到原生則相對好一些,可能是io讀寫字型檔案的效率不一樣或者是 flutter 中讀寫的時候會影像主執行緒的繪製
我們注入字型的精髓如下:
補充
可能大家會發現 視訊退出全屏的時候 頁面會回到頂部,
這塊我們也遇到了,解決方案不是特比好,就是 記錄頁面位置,在頁面退出全屏回來的時候讓webview從新回到之前的位置
大致的code可參考: