做者:闲鱼技术-尘萧java
在漫长的从Native向Flutter过渡的混合工程时期,要想平滑地过渡,在Flutter中使用Native中较为完善的控件会是一个很好的选择。本文但愿向你们介绍AndroidView的使用方式以及在此基础之上拓展的双端嵌入Native组件的解决方案。android
嵌入地图这一场景可能在不少App中都会存在,可是如今的地图SDK都没有提供Flutter的库,而本身开发一套地图显然不太现实。这种场景下,使用混合栈的形式是一个比较好的选择。咱们能够直接在Native的绘图树中嵌入一个Map,可是这个方案嵌入的View并不在Flutter的绘图树中,是一种比较暴力且不优雅的方式,使用起来也很费劲。web
这时候,使用Flutter官方提供的控件AndroidView就是一种比较优雅的解决方案了。这里作了一个简单的嵌入高德地图的demo,就让咱们跟着这个应用场景,看一下AndroidView的使用方式和实现原理。api
AndroidView的使用方式和MethodChannel相似,比较简单,主要分为三个步骤:markdown
第一步:在dart代码的相应位置使用AndroidView,使用时须要传入一个viewType
,这个String将用于惟一标识该Widget,用于和Native的View创建关联。网络
第二步:在native侧添加代码,写一个PlatformViewFactory,PlatformViewFactory的主要任务是,在create()
方法中建立一个View并把它传给Flutter(这个说法并不许确,可是咱们姑且能够这么理解,后续会进行解释)app
第三步:使用registerViewFactory()
方法注册刚刚写好的PlatformViewFactory,该方法须要传入两个参数,第一个参数须要和以前在Flutter端写的viewType
对应,第二个参数是刚刚写好的的PlatformViewFactory。ide
配置高德地图的部分这里就省略不说了,官方有比较详细的文档,能够去高德开发者平台进行查阅。性能
以上即是使用AndroidView的全部操做,整体看起来仍是比较简单的,可是真正要用起来,仍是有两个没法忽视的问题:优化
下面就让小闲鱼来给各位一一解答。
想要解决上面的两个问题,首先必须得理解所谓"传View"的本质是什么?
要解决这个问题,天然避免不了的须要去阅读源码,从更深的层面去看这个传递的整个过程,能够整理出一张这样的流程图:
咱们能够看到,Flutter最终拿到的是native层返回的一个textureId。根据native的知识ky h这个textureId是已经在native侧渲染好了的view的绘图数据对应的ID,经过这个ID能够直接在GPU中找到相应的绘图数据并使用,那么Flutter是如何去利用这个ID的呢?
在以前的深刻了解Flutter界面开发中,也给你们介绍了Flutter的绘图流程。我这里也给你们再简单整理一下
Flutter的Framework层最后会递交给Engine层一个layerTree,在管线中会遍历layertree的每个叶子节点,每个叶子节点最终会调用Skia引擎完成界面元素的绘制,在遍历完成后,在调用glPresentRenderBuffer(IOS)或者glSwapBuffer(Android)按完成上屏操做。
Layer的种类有不少,而AndroidView则使用的是其中的TextureLayer。TextureLayer在以前的《Flutter外接纹理》中有更为详细的介绍,这里就再也不赘述。TextureLayer在被遍历到时,会调用一个engine层的方法SceneBuilder::addTexture()
将textureId做为参数传入。最终在绘制的时候,skia会直接在GPU中根据textureId找到相应的绘制数据,并将其绘制到屏幕上。
那么是否是谁拿到这个ID均可以进行这样的操做呢?答案固然是否认的,Texture数据存储在建立它的EGLContext对应的线程中,因此若是在别的线程进行操做是没法获取到对应的数据的。这里须要引入几个概念:
这里不展开讲解Presentation,咱们只须要明白Flutter是经过Presentation实现了外接纹理,在建立Presentation时,传入FlutterView对应的Context和建立出来的一个虚拟显示屏对象,使得Flutter能够直接经过ID找到并使用Native建立出来的纹理数据。
经过上面的流程你们应该都能想到,显示尺寸看起来像是由两部分决定的:AndroidView的大小,Android端View的大小。那么实际上究竟是有谁来决定的呢,让咱们来作一个实验?
直接新建一个Flutter工程,并把中间改为一个AndroidView。
//Flutter
class _MyHomePageState extends State<MyHomePage> {
double size = 200.0;
void _changeSize() {
setState(() {
size = 100.0;
});
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: Container(
color: Color(0xff0000ff),
child: SizedBox(
width: size,
height: size,
child: AndroidView(
viewType: 'testView',
),
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _changeSize,
child: new Icon(Icons.add),
),
);
}
}
复制代码
在Android端也要加上对应的代码,为了更好地看出裁切效果,这里使用ImageView。
//Android
@Override
public PlatformView create(final Context context, int i, Object o) {
final ImageView imageView = new ImageView(context);
imageView.setLayoutParams(new ViewGroup.LayoutParams(500,500));
imageView.setBackground(context.getResources().getDrawable(R.drawable.idle_fish));
return new PlatformView() {
@Override
public View getView() {
return imageView;
}
@Override
public void dispose() {
}
};
}
复制代码
首先先看AndroidView,AndroidView对应的RenderObject是RenderAndroidView,而一个RenderObject的最终大小的肯定是存在两种可能,一种是由父节点所指定,还有一种是在父节点指定的范围中根据自身状况肯定大小。打开对应的源码,能够看到其中有个很重要的属性sizedByParent = true
,也就是说AndroidView的大小是由其父节点所决定的,咱们可使用Container、SizedBox等控件控制AndroidView的大小。
AndroidView的绘图数据是Native层所提供的,那么当Native中渲染的View的实际像素大小大于AndroidView的大小时,会发生什么呢?一般状况下,这种状况的处理思路无非就两种选择,一种是裁切,另外一种是缩放。Flutter保持了其一向的作法,全部out of the bounds的Widget统一使用裁切的方式进行展现,上面所描述的状况就被看成是一种out of the bounds。
当这个View的实际像素大小小于AndroidView的时候,会发现View并不会相应地变小(Container的背景色并无显露出来),没有内容的地方会被白色填充。这其中的缘由是SingleViewPresentation::onCreate中,会使用一个FrameLayout做为rootView。
Android的事件流你们应该都很熟悉了,自顶向下传递,自底向上处理或回流。Flutter一样是使用这一规则,可是其中AndroidView经过两个类来去处理手势:
MotionEventsDispatcher:负责将事件封装成Native的事件并向Native传递;
AndroidViewGestureRecognizer:负责识别出相应的手势,其中有两个属性:
cachedEvents
和forwardedPointers
,只有当PointerEvent的pointer属性在forwardedPointers中时才会去进行分发,不然会存在cacheEvents中。这里的实现主要是为了解决一些事件的冲突,好比滑动事件,能够经过gestureRecognizers来进行处理,这里能够参考官方注释。
/// For example, with the following setup vertical drags will not be dispatched to the Android view as the vertical drag gesture is claimed by the parent [GestureDetector].
///
/// GestureDetector(
/// onVerticalDragStart: (DragStartDetails d) {},
/// child: AndroidView(
/// viewType: 'webview',
/// gestureRecognizers: <OneSequenceGestureRecognizer>[],
/// ),
/// )
///
/// To get the [AndroidView] to claim the vertical drag gestures we can pass a vertical drag gesture recognizer in [gestureRecognizers] e.g:
///
/// GestureDetector(
/// onVerticalDragStart: (DragStartDetails d) {},
/// child: SizedBox(
/// width: 200.0,
/// height: 100.0,
/// child: AndroidView(
/// viewType: 'webview',
/// gestureRecognizers: <OneSequenceGestureRecognizer>[ new VerticalDragGestureRecognizer() ],
/// ),
/// ),
/// )
复制代码
因此总结起来,这部分流程总结起来其实也很简单:事件最初从Native到Flutter这一阶段不在本文的讨论范围以内,Flutter按照本身的规则去处理事件,若是AndroidView赢得了事件,事件就会被封装成相应的Native端的事件而且经过方法通道传回Native,Native再根据本身的处理事件的规则去处理。
往大里说:这套方案是Google为了解决开发者日益增加的业务需求与落后的生态环境之间的矛盾而产生的,这一矛盾是一个新生态必然须要去面对的主要矛盾。为了解决这一个问题,最简单的方式固然就是容许开发者使用老生态中已经很是成熟的控件。固然,这样是能够临时解决Flutter生态发展不全面的问题,可是使用这套方案不可避免的须要去编写双端代码(甚至如今iOS尚未对应的控件,固然以后确定会更新),不能作到真正的跨端。
往小里说:这套方案存在着性能上的缺陷,在AndroidView这个类的第三句注释中,官方就已经提到了这是一套比较昂贵的方案,避免在使用Flutter控件也能实现的状况下去使用它。若是以前有看过《Flutter外接纹理》这一文章的同窗应该知道,Flutter实现外接纹理的方案中,数据从GPU->CPU->GPU的过程代价是比较大的,在大量使用的场景会形成明显的性能缺陷。咱们经过一些手段绕过了中间CPU这一步,而且将这项技术在APP中落地,用于处理图片资源。
目前闲鱼从Native向Flutter的迁移工做遇到了Native的本地图片资源在Flutter侧没法访问的问题,在如今Flutter和Native必将长期共存的状况下,从新拷贝一份资源以Flutter的规则来存储固然能够,可是不可避免地增大了包体积,并且很差管理。
面对这个问题,咱们的解法即是借鉴了AndroidView使用Texture的思路并在将其优化。实现了Native和Flutter的图片资源归一化。除了用于加载位于Native资源目录下的本地图片以外,还能够利用Native的图片库来加载网络图片。
咱们这么去作的缘由是咱们在Native侧的图片库较为完善而且经受过大量的线上考验,如今这一阶段,咱们不但愿将过多的精力投入到重复造轮子这一件事上,而处理网络图片资源和处理本地图片资源的思路实际上是同样的,因此咱们选择将图片资源进行了统一地整合,在与官方的团队进行沟通并完善后会和你们同步,敬请关注咱们的公众号。