GSYFlutterBook/Flutter-20.md

136 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

作为系列文章的第二十篇,本篇将结合[官方的技术文档](https://github.com/flutter/flutter/wiki/Android-Platform-Views#text-input)科普 Android 上 `PlatformView` 的实现逻辑,并且解释为什么在 Android 上 `PlatformView` 的键盘总是有问题。
> 为什么 iOS 上相对稳定,文中也做了对应介绍。
## 文章汇总地址:
> [Flutter 完整实战实战系列文章专栏](https://juejin.im/collection/5db25bcff265da06a19a304e)
>
> [Flutter 番外的世界系列文章专栏](https://juejin.im/collection/5db25d706fb9a069f422c374)
## 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` 上。
![](http://img.cdn.guoshuyu.cn/20200225_Flutter-20/image1)
如上图所示,**简单来说就是原生控件的内容被绘制到内存里,然后 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 不同步的问题**。
> 有关此替代方法的详细讨论,详见 https://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完整开发实战详解(十三、全面深入触摸和滑动原理)》](https://juejin.im/post/5cd54839f265da03b2044c32)
- 当触摸成功时会向 [Android embedding](https://github.com/flutter/flutter/blob/068fa84/packages/flutter/lib/src/rendering/platform_view.dart#L595) 发送一条消息,其中包含 touch 事件的详细信息。
- 在 [Android embedding](https://github.com/flutter/flutter/blob/068fa84/packages/flutter/lib/src/rendering/platform_view.dart#L595) 中,该事件的坐标最后会匹配到 `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 ](https://github.com/flutter/engine/blob/036ddbb0ee6858ae532df82a2747aa93faee4487/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java#L206) 。
- Android 认为 Flutter View 是 `focused` 且可用的,因此 `AndroidView``InputConnection` 可以成功被获取并使用。
#### 2.2.2、 Platforview 中的 WebView 键盘输入
**在 Android N 之前的版本上 `WebView` 输入比较复杂,因为它们具有自己内部的逻辑来创建和设置输入连接,而这些输入连接并没有完全遵循 Android 的协议**。在 `flutter_webview` 插件中,还需要添加其他解决方法以便在可以在 `WebView` 启用文本输入。
- [设置一个代理 View ,该 View 与 `WebView` 在相同的线程上侦听输入连接](https://github.com/flutter/plugins/blob/27f3de3/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java#L113)。如果没有此功能,`WebView` 将在内部消耗所有 `InputConnection` 的呼叫,而不会通知 Flutter View 代理。
- [在代理线程中,返回 Flutter View 以创建输入。](https://github.com/flutter/plugins/blob/27f3de3e1e6ed1c0f2cd23b0d1477ff3f0955aaa/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java#L67)。
- [`WebView` 失去焦点时,将输入连接重置回 Flutter 线程](https://github.com/flutter/plugins/blob/27f3de3/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java#L128)。这样可以防止文本输入“卡”在 WebView 内。
#### 2.2.3、局限性
- 通常这个逻辑取决于 Android 的内部行为,并且可能会十分脆弱,比如: *[1.12 版本下针对华为等设备出现的键盘输入异常等问题](https://github.com/flutter/flutter/issues/51254)*
- 某些文本功能仍然不可用,例如:*“复制”和“共享”对话框当前不可用*。
## 3、总结
`PlatformView` 的实现模式增加了 Flutter 的生命力和活力,但是相对的也引出了很多问题,比如 [#webview-keyboard](https://github.com/flutter/flutter/labels/p%3A%20webview-keyboard)、[#webview](https://github.com/flutter/flutter/labels/p%3A%20webview)、[#platform-views](https://github.com/flutter/flutter/labels/a%3A%20platform-views) 相关的 issue 专题高居不下,并且如 [webview_flutter](https://pub.dev/packages/webview_flutter) 插件的文档所述:
> 该插件依赖 Flutter 的新机制来嵌入 Android 和 iOS 视图。由于该机制当前处于开发人员预览中,因此该插件也应被视为开发人员预览。
>
> `webview_flutter` 的键盘支持也尚未准备好用于生产,因为 Webview 中的键盘支持目前还处于实验性的阶段。
**所以到这里相信你应该知道,为什么 Flutter 中的 `PlatforView` 在 Android 上如此之难兼容,并且键盘输入问题会那么多坑了**
> 自此,第二十篇终于结束了!(///▽///)
### 资源推荐
* Github https://github.com/CarGuo
* **开源 Flutter 完整项目https://github.com/CarGuo/GSYGithubAppFlutter**
* **开源 Flutter 多案例学习型项目: https://github.com/CarGuo/GSYFlutterDemo**
* **开源 Fluttre 实战电子书项目https://github.com/CarGuo/GSYFlutterBook**
* 开源 React Native 项目https://github.com/CarGuo/GSYGithubApp
![](http://img.cdn.guoshuyu.cn/20200225_Flutter-20/image2)