作为系列文章的第二十篇,本篇将结合​​官方的技术文档​​科普 Android 上 ​​PlatformView​​​ 的实现逻辑,并且解释为什么在 Android 上 ​​PlatformView​​ 的键盘总是有问题。


为什么 iOS 上相对稳定,文中也做了对应介绍。


文章汇总地址:


​​Flutter 完整实战实战系列文章专栏​​

​​Flutter 番外的世界系列文章专栏​​


1、为什么有 PlatformView

因为 Flutter 的实现在概念上类似于 Android 上的 ​​WebView​​​,Flutter 是通过将 ​​Widget Tree​​ 转化为纹理后通过 Skia 实现控件绘制,这造就了优秀的跨平台效果的同时,也带来了不可逆的兼容问题。

1.1、无法集成原生平台控件

这就像 WebView 一样,Flutter UI 不会转换为 Android 控件,而是由 Flutter Engine 使用 Skia 直接在 SurfaceView 上渲染出来

这意味着默认情况下 Flutter UI 永远不会包含 Android Native 的控件,也就是说无法在 Flutter 中集成如 ​​WebView​​​ 或 ​​MapView​​ 这些常用的控件。

所以为解决这个问题,Flutter 创建了一个叫 AndroidView 的控件逻辑, 开发者使用该 Widget 可以将 Android Native 组件嵌入到 Flutter UI 中

1.2、AndroidView 的实现

​AndroidView​​ 这个 Widget 需要和 Flutter 相结合才能完整显示:在 Flutter 中通过将 AndroidView 需要渲染的内容绘制到 ​VirtualDisplays​ 中 ,然后在 ​VirtualDisplay​ 对应的内存中,绘制的画面就可以通过其 ​Surface​ 获取得到


​VirtualDisplay​​​ 类似于一个虚拟显示区域,需要结合 ​​DisplayManager​​​ 一起调用,一般在副屏显示或者录屏场景下会用到。​​VirtualDisplay​​​ 会将虚拟显示区域的内容渲染在一个 ​​Surface​​ 上。


Flutter完整开发实战详解(二十、 Android PlatformView 和键盘问题)_前端

如上图所示,简单来说就是原生控件的内容被绘制到内存里,然后 Flutter Engine 通过相对应的 textureId 就可以获取到控件的渲染数据并显示出来

通过从 ​​VirtualDisplay​​ 输出中获取纹理,并将其和 Flutter 原有的 UI 渲染树混合,使得 Flutter 可以在自己的 Flutter Widget tree 中以图形方式插入 Android 原生控件。

1.3、 有其他可以实现的方式吗?

在 iOS 平台上就不使用类似 ​​VirtualDisplay​​ 的方法,而是通过将 Flutter UI 分为两个透明纹理来完成组合:一个在 iOS 平台视图之下,一个在其上面

所以这样的好处就是:需要在“iOS平台”视图下方呈现的Flutter UI,最终会被绘制到其下方的纹理上;而需要在“平台”上方呈现的Flutter UI,最终会被绘制在其上方的纹理。它们只需要在最后组合起来就可以了

通常这种方法更好,因为这意味着 Android Native View 可以直接添加到 Flutter 的 UI 层次结构中。

但是,Android 平台并不支持这种模式,因为在 iOS 上框架渲染后系统会有回调通知,例如:当 iOS 视图向下移动 2px 时,我们也可以将其列表中的所有其他 Flutter 控件也向下渲染 ​2px​

但是在 Android 上就没有任何有关的系统 API,因此无法实现同步输出的渲染。如果强行以这种方式在 Android 上使用,最终将产生很多如 AndroidView 与 Flutter UI 不同步的问题


有关此替代方法的详细讨论,详见 ​​flutter.dev/go/nshc​​


2、相关问题和解决方法

尽管前面可以使用 ​​VirtualDisplay​​​ 将 Android 控件嵌入到 Flutter UI 中 ,但这种 ​​VirtualDisplay​​ 的介入还有其他麻烦的问题需要处理。

2.1、触摸事件

默认情况下, PlatformViews 是没办法接收触摸事件

因为 ​​AndroidView​​​ 其实是被渲染在 ​​VirtualDisplay​​​ 中 ,而每当用户点击看到的 ​​"AndroidView"​​​ 时,其实他们就真正”点击的是正在渲染的 ​​Flutter​​ 纹理 。用户产生的触摸事件是直接发送到 Flutter View 中,而不是他们实际点击的 AndroidView​。

2.1.1、解决方法

  • ​AndroidView​​ 使用 Flutter Framework 中的点击测试逻辑来检测用户的触摸是否在需要特殊处理的区域内。


类似可见:​​《Flutter完整开发实战详解(十三、全面深入触摸和滑动原理)》​​


  • 当触摸成功时会向 Android embedding 发送一条消息,其中包含 touch 事件的详细信息。
  • 在 Android embedding 中,该事件的坐标最后会匹配到 ​​AndroidView​​ 在 ​​VirtualDisplay​​ 中的坐标,然后会创建一个 ​​MotionEvent​​ 用于 描述触摸的新控件,并将其转发到内部 ​​VirtualDisplay​​ 中真实的 ​​AndroidView​​ 中进行响应。

2.1.2、局限性

  • 该实现逻辑会将新的 ​​MotionEvent​​ 直接分发给 ​​AndroidView​​ ,如果这个 View 又派生了其他视图,那么就可能会出现触摸信息被发送到错误的位置。
  • ​MotionEvent​​ 的转化过程中可能会因为机制的不同,存在某些信息没办法完整转化的丢失。

2.2、文字输入

通常,AndroidView 是无法获取到文本输入,因为 ​VirtualDisplay​ 所在的位置会始终被认为是 ​unfocused​ 的状态

Android 目前不提供任何 API 来动态设置或更改的焦点 ​​Window​​​,​​Flutter​​​ 中​​focused​​​ 的 ​​Window​​ 通常是实际持有“真实的” Flutter 纹理和 UI ,并且对于用户直接可见。

而 ​InputConnections(如何在 Android 中 输入文本)在 ​unfocused​ 的 View 中通常是会被丢弃

2.2.1、解决方法

  • Flutter 重写了 checkInputConnectionProxy 方法,这样 Android 会认为 Flutter View 是作为 ​AndroidView​ 和输入法编辑器(IME)的代理,这样 Android 就可以从 Flutter View 中获取到 ​​InputConnections​​ 然后作用于 ​​AndroidView​​ 上面。
  • 在 Android Q 开始 InputMethodManager(IMM)改为每个 ​Window​ 自己实例化而不是全局单例。因此之前幼稚的“设置代理”的模式在 Q 开始不起作用。为了进一步解决这个问题,Flutter 创建了一个 Context 的子类, 该子类返回的内容与 Flutter View 中的 ​IMM​ 相同,这样就不会需要在查询 ​IMM​ 时需要返回的真实的 ​Window​。这意味着当 Android 需要 ​​IMM​​ 时,​​VirtualDisplay​​ 仍然会使用 Flutter View 的 ​​IMM​​ 作为代理。
  • 当要求 ​​AndroidView​​ 提供 ​​InputConnection​​ 时,它会检查 ​​AndroidView​​ 是否确实是输入的目标。如果是,那 AndroidView 中的 InputConnection 将被获取并返回给 Android 。
  • Android 认为 Flutter View 是 ​​focused​​ 且可用的,因此 ​​AndroidView​​ 的 ​​InputConnection​​ 可以成功被获取并使用。

2.2.2、 Platforview 中的 WebView 键盘输入

在 Android N 之前的版本上 WebView 输入比较复杂,因为它们具有自己内部的逻辑来创建和设置输入连接,而这些输入连接并没有完全遵循 Android 的协议。在 ​​flutter_webview​​​ 插件中,还需要添加其他解决方法以便在可以在 ​​WebView​​ 启用文本输入。

  • ​​设置一个代理 View ,该 View 与 WebView 在相同的线程上侦听输入连接​​。如果没有此功能,​​WebView​​​ 将在内部消耗所有​​InputConnection​​ 的呼叫,而不会通知 Flutter View 代理。
  • ​​在代理线程中,返回 Flutter View 以创建输入。​​。
  • ​​WebView 失去焦点时,将输入连接重置回 Flutter 线程​​。这样可以防止文本输入“卡”在 WebView 内。

2.2.3、局限性

  • 通常这个逻辑取决于 Android 的内部行为,并且可能会十分脆弱,比如: ​1.12 版本下针对华为等设备出现的键盘输入异常等问题​。
  • 某些文本功能仍然不可用,例如:“复制”和“共享”对话框当前不可用

3、总结

​PlatformView​​ 的实现模式增加了 Flutter 的生命力和活力,但是相对的也引出了很多问题,比如 ​​#webview-keyboard​​、​​#webview​​、​​#platform-views​​ 相关的 issue 专题高居不下,并且如 ​​webview_flutter​​ 插件的文档所述:


该插件依赖 Flutter 的新机制来嵌入 Android 和 iOS 视图。由于该机制当前处于开发人员预览中,因此该插件也应被视为开发人员预览。

​webview_flutter​​ 的键盘支持也尚未准备好用于生产,因为 Webview 中的键盘支持目前还处于实验性的阶段。


所以到这里相信你应该知道,为什么 Flutter 中的 PlatforView 在 Android 上如此之难兼容,并且键盘输入问题会那么多坑了


自此,第二十篇终于结束了!(///▽///)


资源推荐

  • Github :​​github.com/CarGuo​​
  • 开源 Flutter 完整项目:​​github.com/CarGuo/GSYG…​​
  • 开源 Flutter 多案例学习型项目: ​​github.com/CarGuo/GSYF…​​
  • 开源 Fluttre 实战电子书项目:​​github.com/CarGuo/GSYF…​​
  • 开源 React Native 项目:​​github.com/CarGuo/GSYG…​​

Flutter完整开发实战详解(二十、 Android PlatformView 和键盘问题)_Flutter_02