引言

在漫长的从Native向Flutter过渡的混合工程时期,要想平滑地过渡,在Flutter中使用Native中较为完善的控件会是一个很好的选择。本文希望向大家介绍AndroidView的使用方式以及在此基础之上拓展的双端嵌入Native组件的解决方案。

1. 使用教程

1.1. DemoRun

嵌入地图这一场景可能在很多App中都会存在,但是现在的地图SDK都没有提供Flutter的库,而自己开发一套地图显然不太现实。这种场景下,使用混合栈的形式是一个比较好的选择。我们可以直接在Native的绘图树中嵌入一个Map,但是这个方案嵌入的View并不在Flutter的绘图树中,是一种比较暴力且不优雅的方式,使用起来也很费劲。

这时候,使用Flutter官方提供的控件AndroidView就是一种比较优雅的解决方案了。这里做了一个简单的嵌入高德地图的demo,就让我们跟着这个应用场景,看一下AndroidView的使用方式和实现原理。

1.2. AndroidView使用方式

AndroidView的使用方式和MethodChannel类似,比较简单,主要分为三个步骤:

第一步:在dart代码的相应位置使用AndroidView,使用时需要传入一个viewType,这个String将用于唯一标识该Widget,用于和Native的View建立关联。

第二步:在native侧添加代码,写一个PlatformViewFactory,PlatformViewFactory的主要任务是,在create()方法中创建一个View并把它传给Flutter(这个说法并不准确,但是我们姑且可以这么理解,后续会进行解释)

第三步:使用registerViewFactory()方法注册刚刚写好的PlatformViewFactory,该方法需要传入两个参数,第一个参数需要和之前在Flutter端写的viewType对应,第二个参数是刚刚写好的的PlatformViewFactory。

配置高德地图的部分这里就省略不说了,官方有比较详细的文档,可以去高德开发者平台进行查阅。

以上便是使用AndroidView的所有操作,总体看起来还是比较简单的,但是真正要用起来,还是有两个无法忽视的问题:

  1. View最终的显示尺寸由谁决定?
  2. 触摸事件是如何处理的?

下面就让小闲鱼来给各位一一解答。

2. 原理讲解

想要解决上面的两个问题,首先必须得理解所谓"传View"的本质是什么?

2.1. 所谓"传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对应的线程中,所以如果在别的线程进行操作是无法获取到对应的数据的。这里需要引入几个概念:

  • 显示屏对象(Display):提供合理的显示器的像素密度和大小的信息
  • Presentation:它给Android提供了在对应的上下文(Context)和显示屏对象(Display)上绘制的能力,通常用于双屏异显。

这里不展开讲解Presentation,我们只需要明白Flutter是通过Presentation实现了外接纹理,在创建Presentation时,传入FlutterView对应的Context和创建出来的一个虚拟显示屏对象,使得Flutter可以直接通过ID找到并使用Native创建出来的纹理数据。

2.2. View最终的显示尺寸由谁决定?

通过上面的流程大家应该都能想到,显示尺寸看起来像是由两部分决定的:AndroidView的大小,Android端View的大小。那么实际上到底是有谁来决定的呢,让我们来做一个实验?

直接新建一个Flutter工程,并把中间改成一个AndroidView。

//Flutter
class _MyHomePageState extends State<MyHomePage> {double size = 200.0;void _changeSize() {setState(() {size = 100.0;});}@overrideWidget 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() {@Overridepublic View getView() {return imageView;}@Overridepublic 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。

2.3. 触摸事件如何传递

Android的事件流大家应该都很熟悉了,自顶向下传递,自底向上处理或回流。Flutter同样是使用这一规则,但是其中AndroidView通过两个类来去处理手势:

MotionEventsDispatcher:负责将事件封装成Native的事件并向Native传递;

AndroidViewGestureRecognizer:负责识别出相应的手势,其中有两个属性:

cachedEventsforwardedPointers,只有当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再根据自己的处理事件的规则去处理。

在Flutter中嵌入Native组件的正确姿势相关推荐

  1. 转载:QTableView中嵌入可视化组件

    出处:http://qimo601.iteye.com/blog/1538364 QTableView中嵌入可视化组件方法有四种: 第一种不能之前显示,必须双击/选中后才能显示,不适用. 第二种比较简 ...

  2. react如何控制全局loading_React Loading组件的正确姿势

    React Loading组件的正确姿势 页面异步加载的时候为缓解用户等待的焦虑情绪,我们通常会采取加载中...等文字或图标提示. (图例,各种加载中状态) 日常写法 直接在state中存一个load ...

  3. Flutter中嵌入Android 原生TextView

    更多文章请查看 flutter从入门 到精通 本篇文章 中写到的是 flutter 调用了Android 原生的 TextView 案例 添加原生组件的流程基本上可以描述为: 1 android 端实 ...

  4. Flutter中的Scaffold组件

    概述 在开始说明Scaffold组件之前,先大致讲述一下Material Design风格.Material Design也成为纸墨设计风格,是由Google退出的全新的设计语言,这种设计语言旨在为手 ...

  5. Flutter 中的 Image 组件 如何加载网络图片和本地图片

    如下,直接上代码说明: mian(){ runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build( ...

  6. Flutter中Stack层叠组件以及与Align、Positioned组件实现定位布局

    1. Stack 组件 Stack 表示堆的意思,用此组件修饰的子组件会"堆"在一起. 常见属性: 1. alignment  对齐方式: 2. children 子组件: 代码示 ...

  7. Flutter中常用的组件-Expanded

    //将 Row 子部件包装在 Expanded 对象中有助于它水平扩展空间并占用其余的 Row 空间.new Row(children: [//Expanded 是一个小部件,有助于根据主轴扩展 Ro ...

  8. 在 Java 项目中打印错误日志的正确姿势,排查问题更方便,非常实用!

    在程序中打错误日志的主要目标是为更好地排查问题和解决问题提供重要线索和指导.但是在实际中打的错误日志内容和格式变化多样,错误提示上可能残缺不全.没有相关背景.不明其义,使得排查解决问题成为非常不方便或 ...

  9. Linux中查看日志文件的正确姿势,求你别tail走天下了!

    作为一个后端开发工程师,在Linux中查看查看文件内容是基本操作了.尤其是通常要分析日志文件排查问题,那么我们应该如何正确打开日志文件呢?对于我这种小菜鸡来说,第一反应就是 cat,tail,vi(或 ...

最新文章

  1. 怎么看android底层源码,Android 底层按键获取
  2. 《ASP.NET MVC企业实战》(二) MVC开发前奏
  3. 2016年DDoS攻击趋势分析报告
  4. ACM之【运算符重载结合STL】
  5. redis 双写一致性 看一篇成高手系列 一
  6. zabbix报警收到tcmime.1456.1456.1878.bin附件邮件
  7. 最全多线程经典面试题和答案
  8. python 批量查询网页导出结果_python批量查询网页的HTTP状态码
  9. 最新cuDNN 7[Linux]百度云下载[免费] 适用于CUDA 10.0
  10. 网页另存显示不全_word另存为选项没有PDF格式怎么办?别忘了还有这招!
  11. 安卓开发中Theme.AppCompat.Light的解决方法
  12. 装备制造新亮点 机器人红利时代到来
  13. windows 快捷方式(.lnk)代码执行漏洞(CVE-2017-8464 )[附EXP生成工具]
  14. System x Windows Server驱动下载
  15. 2020年深圳杯数学建模竞赛A题
  16. 用html代码制作一个表单页面,HTML网页表单制作详细讲解
  17. 古剑奇谭如何修改服务器,寻木枝改方向了!《古剑奇谭网络版》这波更新太值得去体验了...
  18. 我们为什么要进行时间管理?
  19. 怎样为Windows7系统设置快速启动栏
  20. ruoyi-cloud代码生成(跨数据库)

热门文章

  1. AMF3通讯协议实例
  2. 来自星星的你,我要代表月亮消灭你一
  3. virt-manager管理kvm
  4. sql server insert values 多值 与oracle 的不同
  5. Linux高可用集群(Corosync+Pacemaker)
  6. 十年——透过BILL的眼睛
  7. HADOOP常见错误
  8. 前端猎奇系列之探索Python来反补JavaScript——上篇
  9. Mac下使用Homebrew 安装MySQL
  10. Exception in thread main java.lang.Error: Unresolved compilation problem