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

7,755 阅读7分钟

作为系列文章的第二十篇,本篇将结合官方的技术文档科普 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 中集成如 WebViewMapView 这些常用的控件。

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

1.2、AndroidView 的实现

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

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

如上图所示,简单来说就是原生控件的内容被绘制到内存里,然后 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 中,该事件的坐标最后会匹配到 AndroidViewVirtualDisplay 中的坐标,然后会创建一个 MotionEvent 用于 描述触摸的新控件,并将其转发到内部 VirtualDisplay 中真实的 AndroidView 中进行响应。

2.1.2、局限性

  • 该实现逻辑会将新的 MotionEvent 直接分发给 AndroidView ,如果这个 View 又派生了其他视图,那么就可能会出现触摸信息被发送到错误的位置。

  • MotionEvent 的转化过程中可能会因为机制的不同,存在某些信息没办法完整转化的丢失。

2.2、文字输入

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

Android 目前不提供任何 API 来动态设置或更改的焦点 WindowFlutterfocusedWindow 通常是实际持有“真实的” 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 且可用的,因此 AndroidViewInputConnection 可以成功被获取并使用。

2.2.2、 Platforview 中的 WebView 键盘输入

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

2.2.3、局限性

3、总结

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

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

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

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

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

资源推荐