• 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

  • 相关阅读:
    do...while(0)的妙用
    2013-07-23工作记录
    2013-07-22工作记录
    完全零基础入门——第二天
    【转】学习Flex ActionScript 3.0 强烈推荐电子书
    完全零基础入门——第一天
    【转】待整理
    【luogu P7599】雨林跳跃
    【ybt金牌导航5-4-4】【luogu P4842】城市旅行
    【ybt金牌导航5-4-3】【luogu P2387】魔法森林
  • 原文地址:https://www.cnblogs.com/mauiie/p/13339386.html
Copyright © 2020-2023  润新知