热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

在Flutter中嵌入Native组件的正确姿势是…

作者:闲鱼技术-尘萧引言在漫长的从Native向Flutter过渡的混合工程时期,要想平滑地过渡,在Flutter中使用Native中较为完善的控件会是一

作者:闲鱼技术-尘萧

引言

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

1. 使用教程

1.1. DemoRun

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

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

在Flutter中嵌入Native组件的正确姿势是...
demo_pic

1.2. AndroidView使用方式

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

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

在Flutter中嵌入Native组件的正确姿势是...
image

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

在Flutter中嵌入Native组件的正确姿势是...
image

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

在Flutter中嵌入Native组件的正确姿势是...
image

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

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

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

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

2. 原理讲解

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

2.1. 所谓”传View”的本质是什么?

要解决这个问题,自然避免不了的需要去阅读源码,从更深的层面去看这个传递的整个过程,可以整理出一张这样的流程图:

在Flutter中嵌入Native组件的正确姿势是...
image

我们可以看到,Flutter最终拿到的是native层返回的一个textureId。根据native的知识ky h这个textureId是已经在native侧渲染好了的view的绘图数据对应的ID,通过这个ID可以直接在GPU中找到相应的绘图数据并使用,那么Flutter是如何去利用这个ID的呢?

在之前的深入了解Flutter界面开发中,也给大家介绍了Flutter的绘图流程。我这里也给大家再简单整理一下

在Flutter中嵌入Native组件的正确姿势是...
image

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 { 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() { } }; }

在Flutter中嵌入Native组件的正确姿势是...
image-20181114170348189

首先先看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:负责识别出相应的手势,其中有两个属性:

在Flutter中嵌入Native组件的正确姿势是...
image

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: [], /// ), /// ) /// /// 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: [ new VerticalDragGestureRecognizer() ], /// ), /// ), /// )

所以总结起来,这部分流程总结起来其实也很简单:事件最初从Native到Flutter这一阶段不在本文的讨论范围之内,Flutter按照自己的规则去处理事件,如果AndroidView赢得了事件,事件就会被封装成相应的Native端的事件并且通过方法通道传回Native,Native再根据自己的处理事件的规则去处理。

3. 总结

3.1. 方案局限性

往大里说:这套方案是Google为了解决开发者日益增长的业务需求与落后的生态环境之间的矛盾而产生的,这一矛盾是一个新生态必然需要去面对的主要矛盾。为了解决这一个问题,最简单的方式当然就是允许开发者使用老生态中已经非常成熟的控件。当然,这样是可以临时解决Flutter生态发展不全面的问题,但是使用这套方案不可避免的需要去编写双端代码(甚至现在iOS还没有对应的控件,当然之后肯定会更新),不能做到真正的跨端。

往小里说:这套方案存在着性能上的缺陷,在AndroidView这个类的第三句注释中,官方就已经提到了这是一套比较昂贵的方案,避免在使用Flutter控件也能实现的情况下去使用它。如果之前有看过《Flutter外接纹理》这一文章的同学应该知道,Flutter实现外接纹理的方案中,数据从GPU->CPU->GPU的过程代价是比较大的,在大量使用的场景会造成明显的性能缺陷。我们通过一些手段绕过了中间CPU这一步,并且将这项技术在APP中落地,用于处理图片资源。

3.2. 实际应用

目前闲鱼从Native向Flutter的迁移工作遇到了Native的本地图片资源在Flutter侧无法访问的问题,在现在Flutter和Native必将长期共存的情况下,重新拷贝一份资源以Flutter的规则来存储当然可以,但是不可避免地增大了包体积,而且不好管理。

面对这个问题,我们的解法便是借鉴了AndroidView使用Texture的思路并在将其优化。实现了Native和Flutter的图片资源归一化。除了用于加载位于Native资源目录下的本地图片之外,还可以利用Native的图片库来加载网络图片。

我们这么去做的原因是我们在Native侧的图片库较为完善并且经受过大量的线上考验,现在这一阶段,我们不希望将过多的精力投入到重复造轮子这一件事上,而处理网络图片资源和处理本地图片资源的思路其实是一样的,所以我们选择将图片资源进行了统一地整合,在与官方的团队进行沟通并完善后会和大家同步,敬请关注我们的公众号。

3.4. 引用

高德地图SDK文档

万万没想到——Flutter外接纹理

Android7.1 Presentation双屏异显原理分析


推荐阅读
  • 本文介绍了C#中生成随机数的三种方法,并分析了其中存在的问题。首先介绍了使用Random类生成随机数的默认方法,但在高并发情况下可能会出现重复的情况。接着通过循环生成了一系列随机数,进一步突显了这个问题。文章指出,随机数生成在任何编程语言中都是必备的功能,但Random类生成的随机数并不可靠。最后,提出了需要寻找其他可靠的随机数生成方法的建议。 ... [详细]
  • [译]技术公司十年经验的职场生涯回顾
    本文是一位在技术公司工作十年的职场人士对自己职业生涯的总结回顾。她的职业规划与众不同,令人深思又有趣。其中涉及到的内容有机器学习、创新创业以及引用了女性主义者在TED演讲中的部分讲义。文章表达了对职业生涯的愿望和希望,认为人类有能力不断改善自己。 ... [详细]
  • Android Studio Bumblebee | 2021.1.1(大黄蜂版本使用介绍)
    本文介绍了Android Studio Bumblebee | 2021.1.1(大黄蜂版本)的使用方法和相关知识,包括Gradle的介绍、设备管理器的配置、无线调试、新版本问题等内容。同时还提供了更新版本的下载地址和启动页面截图。 ... [详细]
  • 本文整理了Java面试中常见的问题及相关概念的解析,包括HashMap中为什么重写equals还要重写hashcode、map的分类和常见情况、final关键字的用法、Synchronized和lock的区别、volatile的介绍、Syncronized锁的作用、构造函数和构造函数重载的概念、方法覆盖和方法重载的区别、反射获取和设置对象私有字段的值的方法、通过反射创建对象的方式以及内部类的详解。 ... [详细]
  • macOS Big Sur全新设计大版本更新,10+个值得关注的新功能
    本文介绍了Apple发布的新一代操作系统macOS Big Sur,该系统采用全新的界面设计,包括图标、应用界面、程序坞和菜单栏等方面的变化。新系统还增加了通知中心、桌面小组件、强化的Safari浏览器以及隐私保护等多项功能。文章指出,macOS Big Sur的设计与iPadOS越来越接近,结合了去年iPadOS对鼠标的完善等功能。 ... [详细]
  • 本文介绍了H5游戏性能优化和调试技巧,包括从问题表象出发进行优化、排除外部问题导致的卡顿、帧率设定、减少drawcall的方法、UI优化和图集渲染等八个理念。对于游戏程序员来说,解决游戏性能问题是一个关键的任务,本文提供了一些有用的参考价值。摘要长度为183字。 ... [详细]
  • 20211101CleverTap参与度和分析工具功能平台学习/实践
    1.应用场景主要用于学习CleverTap的使用,该平台主要用于客户保留与参与平台.为客户提供价值.这里接触到的原因,是目前公司用到该平台的服务~2.学习操作 ... [详细]
  • 拥抱Android Design Support Library新变化(导航视图、悬浮ActionBar)
    转载请注明明桑AndroidAndroid5.0Loollipop作为Android最重要的版本之一,为我们带来了全新的界面风格和设计语言。看起来很受欢迎࿰ ... [详细]
  • 自动轮播,反转播放的ViewPagerAdapter的使用方法和效果展示
    本文介绍了如何使用自动轮播、反转播放的ViewPagerAdapter,并展示了其效果。该ViewPagerAdapter支持无限循环、触摸暂停、切换缩放等功能。同时提供了使用GIF.gif的示例和github地址。通过LoopFragmentPagerAdapter类的getActualCount、getActualItem和getActualPagerTitle方法可以实现自定义的循环效果和标题展示。 ... [详细]
  • 利用Visual Basic开发SAP接口程序初探的方法与原理
    本文介绍了利用Visual Basic开发SAP接口程序的方法与原理,以及SAP R/3系统的特点和二次开发平台ABAP的使用。通过程序接口自动读取SAP R/3的数据表或视图,在外部进行处理和利用水晶报表等工具生成符合中国人习惯的报表样式。具体介绍了RFC调用的原理和模型,并强调本文主要不讨论SAP R/3函数的开发,而是针对使用SAP的公司的非ABAP开发人员提供了初步的接口程序开发指导。 ... [详细]
  • 本文介绍了Android 7的学习笔记总结,包括最新的移动架构视频、大厂安卓面试真题和项目实战源码讲义。同时还分享了开源的完整内容,并提醒读者在使用FileProvider适配时要注意不同模块的AndroidManfiest.xml中配置的xml文件名必须不同,否则会出现问题。 ... [详细]
  • 欢乐的票圈重构之旅——RecyclerView的头尾布局增加
    项目重构的Git地址:https:github.comrazerdpFriendCircletreemain-dev项目同步更新的文集:http:www.jianshu.comno ... [详细]
  • 本文介绍了RxJava在Android开发中的广泛应用以及其在事件总线(Event Bus)实现中的使用方法。RxJava是一种基于观察者模式的异步java库,可以提高开发效率、降低维护成本。通过RxJava,开发者可以实现事件的异步处理和链式操作。对于已经具备RxJava基础的开发者来说,本文将详细介绍如何利用RxJava实现事件总线,并提供了使用建议。 ... [详细]
  • 背景应用安全领域,各类攻击长久以来都危害着互联网上的应用,在web应用安全风险中,各类注入、跨站等攻击仍然占据着较前的位置。WAF(Web应用防火墙)正是为防御和阻断这类攻击而存在 ... [详细]
  • 本文介绍了pack布局管理器在Perl/Tk中的使用方法及注意事项。通过调用pack()方法,可以控制部件在显示窗口中的位置和大小。同时,本文还提到了在使用pack布局管理器时,应注意将部件分组以便在水平和垂直方向上进行堆放。此外,还介绍了使用Frame部件或Toplevel部件来组织部件在窗口内的方法。最后,本文强调了在使用pack布局管理器时,应避免在中间切换到grid布局管理器,以免造成混乱。 ... [详细]
author-avatar
莫轻松
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有