一个优秀的可定制化Flutter相册组件,看这一篇就够了

背景

在作图片、视频相关功能的时候,相册是一个绕不开的话题,由于你们基本都有从相册获取图片或者视频的需求。最直接的方式是调用系统相册接口,基本功能是知足的,一些高级功能就不行了,例如自定义UI、多选图片等。java

咱们调研了官方的image_picker,它也是调用系统的相册接口来处理的,可定制程度不高,不能知足咱们的要求。因此咱们选择本身来开发Flutter相册组件。缓存

咱们的组件须要有以下的功能:架构

  • 在app内完成图片、视频的选取,彻底不用依赖系统相册组件
  • 能够多选图片,支持指定选定图片的总数目
  • 在多选的时候UI反应出选择的序号。
  • 能够控制视频、图片的选择。例如:只让用户选择视频,图片是灰色的。
  • 大图预览的时候能够放大缩小,也可直接加入到选取列表。

设计思路

API使用简单,功能丰富灵活,具备较高的订制性。业务方能够选择彻底接入组件,也能够选择在组件上面进行UI定制。app

Flutter作UI展示层,具体的数据由各Native平台提供。这种模式,自然从工程上把UI代码和数据代码进行了隔离。咱们在开发一个native组件的时候经常会使用MVC架构。Flutter组件的开发的思路也基本相似。总体架构以下:函数

能够看出,在Flutter侧是一个典型的MVC架构,这里Widget就是View,View和Model绑定,在Model改变的时候View会从新build反映出Model的变化。View的事件会触发Controller去Native获取数据而后更新Model。Native和Flutter经过Method Channel进行通讯,两层之间没有强依赖关系,只须要按约定的协议进行通讯便可。性能

Native侧的组成部分,UIAdapter主要是负责机型的适配、刘海屏、全面屏之类的识别。Permission负责媒体读写权限的申请处理。Cache主要负责缓存GPU纹理,在大图预览的时候提升响应速度。Decoder负责解析Bitmap,OpenGL负责Bitmap转纹理。测试

须要说明的是:咱们的这一套实现依赖于flutter外接纹理。在整个相册组件看到的大多数图片都是一个GPU纹理,这样给java堆内存的占用相对于之前的相册实现有大幅的下降。在低端机上面若是使用原生的系统相册,因为内存的缘由,app有被系统杀掉的风险。现象就是,从系统相册返回,app从新启动了。使用Flutter相册组件,在低端机上面体验会有所改观。ui

一些细节

1. 分页加载spa

相册列表须要加载大量图片,Flutter的GridView组件有好几个构造函数,比较容易犯的错误是使用了第一个函数,这须要在一开始就提供大量的widget。应该选择第二个构造函数,GridView在滑动的时候会回调IndexedWidgetBuilder来获取widget,至关于一种懒加载。线程

GridView.builder({
    ...
    List<Widget> children = const <Widget>[],
    ...
  })
GridView.builder({
    ...
    @required IndexedWidgetBuilder itemBuilder,
    int itemCount,
    ...
  })

滑动过程当中,图片滑事后,也就是不可见的时候要进行资源的回收,咱们这里这里对应的就是纹理的删除。不断的滑动GridView,内存在上升后会处于稳定,不会一直增加。若是快速的来回滑动纹理会反复的建立和删除,这样会有内存的抖动,体验不是很好。

因而,咱们维护了一个图片的状态机,状态有None,Loading,Loaded,Wait_Dispose,Disposed。开始加载的时候,状态从None进入Loading,这个时候用户看到的是空白或者是占位图,当数据回调回来会把状态设置为Loaded的这时候会从新build widget树来显示图片icon,当用户滑走的时候状态进入 Wait_Dispose,这时候并不会立刻Dispose,若是用户又滑回来则会从Wait_Dispose进入Loaded状态,不会继续Dispose。若是用户没有往回滑则会从Wait_Dispose进入Disposed状态。当进入Disposed状态后,再须要显示该图片的时候就须要从新走加载流程了。

2. 相册大图展现:

当点击GridView的某张图片的时候会进行这张图片的大图展现,方便用户查看的更清楚。咱们知道相机拍摄的图片分辨率都是很高的,若是彻底加载,内存会有很大的开销,因此咱们在Decode Bitmap的时候进行了缩放,最高只到1080p。大图展现能够归纳为三个步骤。

  • 1 从文件Decode出Bitmap
  • 2 Bitmap转换成为纹理,并释放Bitmap
  • 3 纹理交给Flutter进行展现

在步骤1中,Android原生的Bitmap Decode经验一样适用,先Decode出Bitmap的宽高,而后根据要展现的大小计算出缩放倍数, 而后Decode出须要的Bitmap。

Android相册的图片大可能是有旋转角度的,若是不处理直接显示,会出现照片旋转90度的问题,因此须要对Bitmap进行旋转,采用Matrix旋转一张1080p的图片在个人测试机器上面大概须要200ms,若是使用OpenGL的纹理坐标进行旋转,大于只须要10ms左右,因此采用OpenGl进行纹理的旋转是一个较好的选择。

在进行大图预览的时候会进入一个水平滑动的PageView,Flutter的PageView通常来讲是不会去主动加载相邻的page的。举个例子,在显示index是5的page的时候index为4,6的page也不会提早建立的。这里有一个取巧的办法,对于PageController的viewportFraction参数咱们能够设置成为0.9999。对于前面这个例子,就是在显示index是5的page的时候,index为4,6的page也须要显示0.0001。这样index为4,6的page显示不到1个像素,基本上看不出来:

PageController(viewportFraction=0.9999)

还有另一种办法,就是在Native侧作预加载。例如:在加载第5张图片的时候,相邻的4,6的图片纹理提早进行加载,当滑动到4,6的时候直接使用缓存的纹理。

纹理缓存后,一个直接的问题:何时释放纹理?等到预览页面退出的时候释放全部的纹理显示不是很合适,若是用户一直浏览内存则会无限增加。因此,咱们维护了一个5个纹理的LRU缓存,在滑动过程当中,最老的纹理会被释放掉。在页面退出的时候整个LRU的缓存会进行销毁。

3. 关于内存

相册图片使用GPU纹理,会大幅减小Java堆内存的占用,对整个app的性能有必定的提高。须要注意的是,GPU的内存是有限的须要在使用完毕后及时删除,否则会有内存的泄漏的风险。另外,在Android平台删除纹理的时候须要保证在GPU线程进行,否则删除是没有效果的。

在华为P8,Android5.0上面进行了对比测试,Flutter相册和原native相册总内存占用基本一致,在GridView列表页面,新增最大内存13M左右。它们的区别在于原native相册使用的是Java堆内存,Flutter相册使用的是Native内存。

总结

相册组件API简单、易用,高度可定制。Flutter侧井井有条,有UI订制需求的能够重写Widget来达到目的。另外这是一个不依赖于系统相册的相册组件,自身是完备的,可以和现有的app保持UI、交互的一致性。同时为后面支持更多和相册相关的玩法打好基础。

后续计划

因为咱们使用的是GPU纹理,能够考虑支持显示高清4K图片,并且客户端内存不会有太大的压力。可是4k图片的Bitmap转纹理需消耗更多的时间,UI交互上面须要作些loading状态的支持。

组件功能丰富,稳定后,进行开源,回馈给社区。



本文做者:闲鱼技术-邻云

阅读原文

本文为云栖社区原创内容,未经容许不得转载。

相关文章
相关标签/搜索