前言
Fresco有多叼我就不说了!
SimpleDraweeView的不足
用过Fresco的同学都知道,使用它有一定的成本,必须把所有ImageView替换为SimpleDraweeView,且是侵入布局式的修改。
稍微好一点的方式是修改项目中的ImageView的继承关系。如下:
在一些大型项目中,可能业务上并没有直接在布局中使用android.widget.ImageView,而是自定义一层,实现业务上的部分需求(如webp转换/降级支持,宽高裁剪支持等等),同时方便到时候可直接通过修改继承关系来随意替换图片库,如下:
1 | public class MyImageView extends ImageView { |
在布局里并不是直接使用android.widget.ImageView,而是使用MyImageView
1 | <io.github.lizhangqu.sample.MyImageView |
所以这种方式使用Fresco,只需要将MyImageView的继承关系修改为继承SimpleDraweeView即可,无需改动业务代码。
事物总是有两面性的,这种方式虽然有其优点,但是也有弊端,就是用到图片加载的地方,都必须使用MyImageView而不是ImageView(虽然SimpleDraweeView也一样)。但是他比直接在布局中使用SimpleDraweeView要很多,收敛了图片的入口,方便替换。还有一点就是各个基础库得依赖这个继承了的View。
图片选择器引发的问题
最近想把项目中的图片选择器给换掉,因为目前使用的图片选择器效果并不是很好,代码修改自github上一个项目ImageChoose,项目比较老,目前已经没人维护了。于是打算换一个图片选择器,而前段时间,知乎刚好开源了一个图片选择器,就拿它开刀了,项目地址 Matisse。
将项目clone下来一看,懵逼了,没有对Fresco的支持,而项目中使用的是Fresco,总不能因为一个图片选择器引入两套图片库吧,这就是坑爹的做法。于是看了下issue,发现还是有很多这样的诉求的,如下:
看一个回复
@Logan676 As you said, why not add FrescoEngine? I’ve asked myself the same question. The truth is that Fresco has a lot of customizations, it defines its own view which is
SimpleDraweeView. So, if we want to support Fresco, it’s not that simple you just implementImageEngine, a lot more work needed.
看来知乎不支持Fresco也是有原因的。
寻找解决方式
是否存在一种不使用SimpleDraweeView而能加载图片的方式,通过查看文档 writing-custom-views 可以发现,Fresco是支持自定义View的。主要需要处理以下几件事情
- 处理好DraweeHolder的attach/detach事件
- 如果开启了加载失败点击重试的功能,需要处理好View的onTouchEvent事件
- DraweeHolder和DraweeHierarchy对象的创建尽量只创建一次,因为开销比较大
- 通过DraweeHolder.getTopLevelDrawable()方法可以拿到Drawable对象,直接使用即可,但是你不能随意的变换这个Drawable对象
但是我们现在没有继承ImageView,所以没办法处理attach/detach事件,唯一可行的就是View.OnAttachStateChangeListener事件
1 | //remove listener if needed |
但是在调用addOnAttachStateChangeListener前,可能该View已经AttachedToWindow了,因此需要判断是否已经AttachedToWindow了,如果是的话,手动调用一遍onViewAttachedToWindow.
对于点击重试的功能,熟悉View的事件机制,就很好办了,如果对View设置了OnTouchListener,那么OnTouchListener会优先于onTouchEvent方法调用
对应的事件分发的代码如下:
1 | ListenerInfo li = mListenerInfo; |
于是我们直接设置OnTouchListener即可,注意,这会覆盖已有的OnTouchListener事件,这里是一个坑点。
1 | targetView.setOnTouchListener(mDraweeHolderDispatcher); |
mDraweeHolderDispatcher是DraweeHolderDispatcher的实例,其作用主要做各种事件的分发,分发到mDraweeHolder对应的事件上去,其实现如下:
1 | private class DraweeHolderDispatcher implements View.OnAttachStateChangeListener, View.OnTouchListener { |
通过翻看SimpleDraweeView及其父类源码,发现Fresco还重写了View的另外两个方法。
1 |
|
这两个方法有什么用呢,参考这篇文章Android中判断子View从ListView中移除,如果使用了AbstractListView其子类,如ListView或者GridView,你最好处理一下这个问题,因为他们的item被移出屏幕的时候,其detach并没有调用,而是调用onStartTemporaryDetach和onFinishTemporaryDetach。但是RecyclerView不存在这个问题。处理方式如下:
问题的最大阻碍是这两个方法并没有向外分发,唯一的途径就是继承重写,于是牺牲下,让其可有可无吧,如果他存在,我们就调用对应的事件,如果不存在,就无视,也避免了侵入。采用接口的方式,如果传进来的View是我们对应的接口,则让其自己分发这两个事件出来,然后我们调用对应的attch和detach事件。
1 | public interface TemporaryDetachListener { |
1 | //如果view是TemporaryDetachListener的实例,则将mDraweeHolderDispatcher传入,让View持有,然后在对应的方法中外调,外调后在我们的mDraweeHolderDispatcher中分发。 |
对应的ImageView的子类实现如下:
1 | public class MyImageView extends ImageView implements FrescoLoader.TemporaryDetachListener { |
补充DraweeHolderDispatcher实现
1 | private class DraweeHolderDispatcher implements View.OnAttachStateChangeListener, View.OnTouchListener, TemporaryDetachListener { |
值得注意的是,onStartTemporaryDetach和onFinishTemporaryDetach并不是必要的,只有你使用了AbstractListView其子类,你最好处理一下,其他情况无视即可,因此我们姑且就无视他了,上面就当提供一种实现的参考吧。当然,最好的情况就是处理一下咯,也不排除AbstractListView子类之外的类存在这个问题,导致的直接问题就是内存无法及时回收,占用过高.
此外,我的御用挖坑小能手提供了一种黑科技用于检测这两个事件,贴出代码仅供参考。原理是onStartTemporaryDetach的时候view.getParent()为null,那么只要在View的onPreDraw中进行检测,一直往上搜索rootView,找到rootView与view.getRootView()不一致,就说明getParent()为null,中间断了一层关系。虽然是attach状态,但是却拿不到parent,即onStartTemporaryDetach或onFinishTemporaryDetach,如果是正常的attach或者detach,则最终找到的rootView是一致的
1 | /** |
就是将onStartTemporaryDetach和onFinishTemporaryDetach事件等效为onViewAttachedToWindow为onViewDetachedFromWindow事件外发,就可以无视onStartTemporaryDetach和onFinishTemporaryDetach事件了。
最终加一个参数控制这种兼容逻辑,等到需要开启时主动传入参数开启,代码如下
1 | if (mCompatTemporaryDetach) { |
接着就可以愉快的使用了。
对mDraweeHolder设置了各种参数后,就可以直接调用对应的getTopLevelDrawable方法,返回Drawable对象,将其设置到ImageView上。
1 | //set image drawable |
最后一个问题,如何保证DraweeHolder对象只创建一遍,并且保证addOnAttachStateChangeListener只会调用一次,实在是太难了,必须牺牲一下,不然不优雅,注意,这种方式必须牺牲掉View的tag相关的功能,如果你的ImageView用了Tag,那么你的Tag会被我们覆盖,请转移你的Tag相关功能,使用其他方式。因为为了让View持有DraweeHolder对象,避免重复创建,只能这么做了,先从Tag中取DraweeHolder,如果取不到,则创建,否则,复用现有的DraweeHolder对象。
1 | //we should use tag |
这些事情搞定了就是参数传递的事情了,将Fresco需要的参数封装成链式接口调用对应的Fresco方法设置即可,其完整实现我已经push到github上,传送门FrescoLoader.java
这里也贴一下实现,方便查看
1 | //Copyright 2017 区长. All rights reserved. |
最后一个坑
当你看到这里的时候,其实还有一个坑,就是虽然DraweeHolder一个View只创建了一个,但是GenericDraweeHierarchy,ImageRequest,PipelineDraweeControllerBuilder等对象还是多次创建了,其实这些也是可以复用的,这里就姑且先不复用了
总结
- 牺牲了View的Tag相关功能,如果你用了,必须使用其他方式实现你的Tag相关功能(如果没有使用Tag,那更好了)
- 牺牲了View的OnTouchListener事件(如果没有使用OnTouchListener,那更好了)
- GenericDraweeHierarchy,ImageRequest,PipelineDraweeControllerBuilder等对象的多次创建问题(可继续优化代码,只创建一次,反正我是懒得优化了)
- onStartTemporaryDetach和onFinishTemporaryDetach事件外发的接口侵入性(如果没有使用AbstractListView其子类则可不实现,否则最好将item中使用到的ImageView实现TemporaryDetachListener接口,这不得不加入一层接口实现关系),可使用mCompatTemporaryDetach参数主动开启黑科技,这样也就不用侵入了
如果你接受得了以上几点问题,那么就放心使用吧,否则,还是老老实实的用最开头提到的继承关系式去实现。不过这对于实现知乎图片选择器的FrescoEngine已经绰绰有余了。
最后使用FrescoLoader
1 | FrescoLoader.with(view.getContext()) |
知乎图片选择器Fresco的实现也就很简单了
1 | public class FrescoEngine implements ImageEngine { |
