https://github.com/flutter/flutter/wiki/Android-Platform-Views

背景

Flutter UI在Android上的工作方式和WebView类似。Flutter Framework根据业务构造出来的widget tree生成内部widget结构,然后根据内部结构来进行渲染。有web开发经验的人可以认为html/css就是widget,DOM就是内部树结构,framework和engine随后根据此结构来渲染。另一点和webview一致的是,flutter并不依赖任何系统View控件,而是直接构造并使用一个Texture(Android中Texture是使用Surface来封装的),并利用Skia来直接将widget渲染到Texture上。
这意味着,和Webview一样,Flutter默认情况下界面上的widget树里不包含任何Android View。由于Flutter界面是直接渲染到Texture上的,并且widget树是完全由flutter内部管理的,因此没有地方可供View来“插入”到flutter的内部widget模型中,无法和其他widget一同渲染成层次化的界面。对于需要使用复杂的,已经存在的原生View的开发者而言,例如需要使用webview或者map的,缺失这种能力是个问题。
为了解决这个问题flutter创建了AndroidView widget供开发者来嵌入Android View到Flutter UI中显示。

方案

AndroidView widget需要和其他widget组合,需要能够和其他widget配合来完成正常widget的显示效果(就是AndroidView可以压盖其他widget,其他widget也可以压盖它)。然而,整个flutter界面都是直接渲染到单独的一个Texture上的。
为了解决问题,flutter将AndroidView widget在一个VirtualDisplay中渲染,而不是尝试将它添加到Android View层次结构中。这个VirtualDisplay将渲染结果输出到原始图形缓存中(也就是由Surface封装的buffer)。这样flutter就能够将VirtualDisplay渲染结果作为一个纹理使用,并能够将纹理同其他widget渲染结果一同组合起来渲染界面。

备注

flutter使用的Android API实际上是Presentation类,这是个用于支持多屏显示不同界面的机制。

还有其他方案吗?

内嵌的iOS platform view不使用这种VirtualDisplay方案,而是将Flutter UI分割成两部分,分别渲染到两个背景透明的纹理上:一部分是所有位于platform view之上的Widget,另一部分是所有位于platform view之下的widget。这样,iSO在需要内嵌platform view时,需要将plafrofm view放到中间,然后分别在view之上和之下来渲染这两个flutter ui纹理。我们倾向于这种方案,因为如果利用这种方案,那么Android的原生View也可以直接添加到Flutter的原生View层次中,这样能够避免后面的章节里介绍的bug。
既然如此,什么Android不能直接渲染到原生View层次,来规避接下来的章节提到的bug呢?
在Android中,flutter缺少关键的API,因此无法使用这里介绍的方案。在ISO中,使用这种方案,要求在每一帧渲染完毕后立即接收到回调。这样,当iOS的list widget移动了2px后,我们可以将它的列表中的platform view也移动2px。然而,在Android中,没有办法知道每一帧什么时候渲染完毕,因此无法在flutter帧被绘制出来的同时同步修改原生View。因此,Android无法采用这种方案,不然由于flutter界面和原生view无法同步,会导致原生view总是比flutter界面快或者慢一到两帧。
某些场景下可能还是这种方案更合适。参考http://flutter.dev/go/nshc来进一步了解这种方案。
(广告sdk内一般有一些反作弊检测,会要求必须在可见Window中渲染,因此做好测试,别展示了广告最后广告sdk还判定你作弊。)

问题和规避方案

虽然这种方案允许将Android view嵌入到flutter界面中,但VirtualDisplay会导致一细节需要规避的问题。
问题的核心在于,VirtualDisplay中渲染的原生View对于Android系统而言是在一个不可见的Display设备中的,和Flutter渲染使用的Texture完全没关系。
Android内部的很多功能都是需要遍历View层次结构或者获取View所属的Window,查询相关信息。由于在这种VirtualDisplay场景下,不论是View的层次结构还是Window信息都不符合一般原生逻辑的预期,因此可能导致异常。更糟的是Android系统不同版本,内部实现也不一样,因此很多问题都需要使用特定版本的Android系统才能复现。
下面介绍相关的某些问题和规避方案。

Touch Events

默认情况下手势事件无法在PlatformView中工作。因为Android View是在VirtualDisplay中渲染的。当用户点击时点击的是主Display,点击的是由Flutter显示出来的由VirtualDisplay渲染的纹理。点击事件被分发到Flutter View上,而不是用户看到的那个Android原生的View。

规避方案

fluuter利用flutter framework的hit testing逻辑检测手势是否是发生在AndroidView上面的;
当发生应该由AndroidView处理的手势时,将包含手势信息的消息分发到Android engine embeding
Android engine embeding内,将消息内的手势信息进行处理,将手势位置从flutter界面坐标转为VietualDisplay中的原生View的坐标,然后据此创建一个MotionEvent对象并分发给VirtualDisplay中的View。

方案限制

(这块没完全看明白,似乎说的是可能存在的问题,而不是目前实际上存在的问题?)

  • We’re dispatching the new MotionEvent directly to the known Android View embedded by the user. In cases where the View spawns other Views this dispatch may be sent to the wrong place.
  • We’re synthesizing a new MotionEvent based on the information handed to us by the Flutter framework. It’s possible that there’s data being lost and not totally carried over to our new MotionEvent, or some other general problem with synthesizing MotionEvents.

    Accessibility

    Accessibility,因为单词太长,一般都用a11y来代指。国内不太关注这块,先略过。

    Text Input

    通常内嵌的Android View并不需要支持文字输入,并且由于处于VirtualDisplay中这个View永远处于unfocused状态。Android没有提供任何API来设置当前处于fucos状态的window。一般情况下处于focus状态的window都是正在显示的FlutterView所处的window。unfocused状态的Window上的InputConnection会被系统忽略。

    规避方案

方案限制