Android webview_flutter插件的優化與完善


Android webview_flutter插件的優化與完善

Android webview_flutter 官方最新版本插件存在的問題:

在我們項目開發過程中使用webview_flutter的時候主要遇到了以下問題:

  1. 長按 選擇、全選、復制 無法正常使用
  2. 視頻播放無法全屏,前后台切換無法停止、繼續播放,按物理鍵返回的時候無法退出全屏
  3. 無法支持前端定位
  4. 不支持文件選擇
  5. 不能使用select標簽
  6. 首次加載webview會顯示黑屏
  7. 默認錯誤頁面顯示、注入自定義字體
  8. 密碼輸入在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/37163
https://github.com/flutter/flutter/issues/24584
https://github.com/flutter/flutter/issues/24585

其中yenole給出了解決方案https://github.com/yenole/plugins ,我這里解決此問題也是按照他的方案實現的,目前標簽還基本正常,部分設備小概率會引起UI異常,但總體是可以的
具體實現步驟:

  1. 重寫插件中 InputAwareWebView 的 startActionMode方法,原生自定義長按操作框
    InputAwareWebView中的關鍵code:

    1.  

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

       

  2. webview 的手勢識別給到最大的EagerGestureRecognizer 否則會出現無法識別長按手機的問題
    在使用webview_flutter 的地方或者直接擴展到插件里的 AndroidWebView 中:

    gestureRecognizers:
                Platform.isAndroid ? (Set()..add(Factory<EagerGestureRecognizer>(() => EagerGestureRecognizer()))) : null,

視頻播放無法全屏,前后台切換無法停止、繼續播放,按物理鍵返回的時候無法退出全屏 以及無法定位

  1. 關於不能全屏、無法定位
    這塊應該是和原生中初始的webview一致,默認不支持視頻全屏,解決辦法與原生中擴展類似
    FlutterWebView內添加自定義的WebChromeClient ,關鍵code:

    1. 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);
              }
          }

       


  2. 按物理鍵返回的時候無法退出全屏,前后台切換無法停止、繼續播放
    按物理鍵返回的時候無法退出全屏,這塊主要是因為屋里返回鍵在flutter中,被flutter捕獲消耗了,解決方案,webview 插件攔截物理返回鍵,自定義退出全屏的方法,調用
    關鍵code如下:
    FlutterWebView :

    1. 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 方法

    1. case "exitFullScreen":
    2. exitFullScreen(result);
    3. break;

    給 controller 擴展 exitFullScreen 方法 code 略

    在 webView_flutter.dart的Webview中修改build方法,
    關鍵code:

    1. @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;
        }
  3. 前后台切換無法停止、繼續播放
    前后台切換無法暫停、繼續播放視頻,主要是因為 webview_flutter 感知不到應用前后台的切換,這塊的解決方案是 插件擴展對前后台的監聽,主動調用 webview 的暫停和繼續播放的方法
    這里需要引入flutter_plugin_android_lifecycle插件,用來監聽應用的聲明周期:
    引入方式 yaml文件中添加 flutter_plugin_android_lifecycle: ^1.0.6
    之后再 WebViewFlutterPlugin 文件擴展聲明周期的監聽,關鍵code如下:

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

    1. 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();
              }
          }

不支持文件選擇

  1. 同樣需要自定義WebChromeClient,擴展文件選擇的方法,參照CustomWebChromeClient中的openFilexxx 方法,主要使用intent 打開文件選擇
  2. 重點是獲取 intent 傳遞回來的數據, 使用webview_flutter 插件的地方dart中無法直接像在android中那樣,重寫Activity的onActivityResult方法,好在 ActivityPluginBinding 中可以注入ActivityResult監聽,這樣我們就能直接在插件中處理了。
    WebViewFlutterPlugin 關鍵code如下:

    1. 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:

    1. @Override
          public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
              if (requestCode == 1303) {
                  flutterWebView.onActivityResult(requestCode, resultCode, data);
                  return true;
              }
              return false;
          }

    FlutterWebView需要擴展onActivityResult方法

    1. 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的黑屏問題
大致的代碼可參照下圖:


默認錯誤頁面顯示、注入自定義字體

  1. 錯誤頁面的顯示,需要dart和Java層同時處理,否則容易看到 webview默認的丑丑的錯誤頁面,但是個人覺得webview默認的丑丑的錯誤頁 顯示出來也不是啥問題,奈何產品非得要臉…
    flutter 層就是封裝的progress_webveiw,加載中,加載出錯都是用 placehoder 層遮罩處理。
    但是在重新加載的時候setState 的一瞬間還是可以看到 webview默認的丑丑的錯誤頁,這就需要插件的Java層也處理一下了
    2.關於 注入自定義字體 我們的方案仍是通過原生注入, 測試對別在flutter中注入的時候 感覺效率有點低,頁面會有二次刷字體的感覺,放到原生則相對好一些,可能是io讀寫字體文件的效率不一樣或者是 flutter 中讀寫的時候會影像主線程的繪制
    我們注入字體的精髓如下:


補充

可能大家會發現 視頻退出全屏的時候 頁面會回到頂部,
這塊我們也遇到了,解決方案不是特比好,就是 記錄頁面位置,在頁面退出全屏回來的時候讓webview從新回到之前的位置
大致的code可參考:


整個修改后的插件 我暫時整理了一部分出來托管到了 gitee上,后續完善了之后會推到github上

地址: https://gitee.com/Mauiie/webview_flutter


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM