[关闭]
@linux1s1s 2016-07-19T10:08:24.000000Z 字数 6676 阅读 2283

Fresco源码分析(1) - 图像层次与各类Drawable

Fresco 2016-07


转载 作者:Desmond

首先介绍几种Fresco中的图像层次,了解它们会帮助你理解Fresco加载图像的原理。

1 引论:给图像分层次是什么作用?

如果你使用过Fresco这个强大的库之后,你就知道它可以在一个图像的加载、绘制过程中实现极大的定制化。你可以设置进度条来显示图片加载/下载的进度,可以设置占位图等到图片加载/下载成功后再显示目标图片,可以让在加载/下载失败后显示失败图片(更多功能参考Fresco中文文档)。Fresco将进度条、占位图、失败图都作为图像的一层视图来管理,这部分仅仅负责视图层次绘制,将负责视图功能部分与逻辑部分尽可能实现解耦。

Fresco中定义了许多Drawable,它们都直接或间接继承了Drawable,但是各自的功能是不一样的。经过总结,我认为其中一共有三种功能的Drawable:层次型、容器型和视图型。直接上源码的一个视图例子就好理解了,作者进行了适当修改与翻译。

o 层次型Drawable(维持图层)
|
------ 容器型Drawable(可对内容进行缩放)
|    |
|    --- 视图型Drawable(存放占位图)
|
------ 容器型Drawable(可对内容进行缩放)
|    |
|    ----- 容器型Drawable(可多次设置内容)
|       |
|       --- 视图型Drawable(存放目标显示图片)
|
------ 容器型Drawable(可对内容进行缩放)
|    |
|    --- 视图型Drawable(存放重试图片)
|
------ 容器型Drawable(可对内容进行缩放)
    |
    --- 视图型Drawable(存放失败图片)

该例位于com.facebook.drawee.generic.GenericDraweeHierarchy的类注释中。

这个例子充分描述了一个图像的层次,当然也可以在设置的时候往里面自行设置所需要的图层。

2 层次型Drawable

在这一节中介绍的Drawable并不直接负责具体图像绘制,而是负责组建图像层次。

2.1 ArrayDrawable

ArrayDrawable内部存储着一个Drawable数组,它与Android内置的LayerDrawable很相似,可见它将数组中的Drawable当做它的图层,在绘制的时候ArrayDrawable会按照数组顺序绘制其中的图层,数组最后的成员会显示在最上方。不过与LayerDrawable最大的不同的点有两处:
- 绘制顺序虽然是数组顺序,但是ArrayDrawable在绘制时会跳过暂时不需要绘制的图层;
- 在ArrayDrawable中不支持动态的添加/删除图层,只能在初始化时通过传入的数组决定图层数。不过好在它能够为存在的图层更换Drawable。(关于LayerDrawable可以参考我翻译的一文章:Android LayerDrawable。)

2.2 FadeDrawable

FadeDrawable继承了ArrayDrawable。它除了具有ArrayDrawable本身的功能之外,还提供隐藏/显示图层的功能(可设置渐变)。具体的几个核心函数有:

它内部维护着一个boolean数组来维持需要显示的图层(可以调用isLayerOn(int inxex)查看指定图层是否显示)。

3 容器型Drawable

ForwardingDrawable

ForwardingDrawable通俗的来说就是图片容器。它内部维护一个Drawable变量mCurrentDelegate,将Drawable的基本函数以及一些回调函数传递给目标图片,并在draw(Canvas)函数中调用mCurrentDeletate.draw(Canvas)函数将目标图片绘制出来。
它可以通过getCurrent()来获取容器内容,起到一个相当于是传递树的作用。它是所有容器型Drawable的基类,以下介绍几个它的子类,他们实现了不同功能的容器包装。

ScaleTypeDrawable

ScaleTypeDrawable封装了对代理图片的缩放处理,具体的缩放参数(ScaleType)与Android ScaleType的名字、功能相同。它在处理图片缩放的时候与ImageView的处理方式相似,我们来看一下它是怎么处理的:

  1. private void configureBoundsIfUnderlyingChanged() {
  2. /* 当图像尺寸改变后,就重新确定图像边缘 */
  3. if (mUnderlyingWidth != getCurrent().getIntrinsicWidth() ||
  4. mUnderlyingHeight != getCurrent().getIntrinsicHeight()) {
  5. configureBounds();
  6. }
  7. }

其中mUnderLyingWidthmUnderLyingHeight维护了已知的上一张图片宽高(初始均为0),当要绘制时调用这个函数,如果与上一张绘制的图像不一样时,就重新确认绘制边缘与缩放矩阵。

再来看一下configureBounds()是怎么确定转换矩阵的吧:

  1. void configureBounds() {
  2. ...
  3. // 特殊情况判断:绘制图片是否为空白图或与之前绘制的图片尺寸相同
  4. ...
  5. // 当要往X、Y方向上填充容器时,直接将目标图片边界设置成容器图片的边界即可,Drawable会在绘制的时候自己调整。
  6. if (mScaleType == ScalingUtils.ScaleType.FIT_XY) {
  7. underlyingDrawable.setBounds(bounds);
  8. mDrawMatrix = null;
  9. return;
  10. }
  11. //处理其他缩放情况
  12. underlyingDrawable.setBounds(0, 0, underlyingWidth, underlyingHeight);
  13. ScalingUtils.getTransform(
  14. mTempMatrix,
  15. bounds,
  16. underlyingWidth,
  17. underlyingHeight,
  18. (mFocusPoint != null) ? mFocusPoint.x : 0.5f,
  19. (mFocusPoint != null) ? mFocusPoint.y : 0.5f,
  20. mScaleType);
  21. mDrawMatrix = mTempMatrix;
  22. }

这里通过ScalingUtils.getTransform来计算出变换矩阵,我们以CENTER_INSIDE为例研究一下它的工作机制:

  1. public static Matrix getTransform(
  2. final Matrix transform,
  3. final Rect parentBounds,
  4. final int childWidth,
  5. final int childHeight,
  6. final float focusX,
  7. final float focusY,
  8. final ScaleType scaleType) {
  9. ...
  10. final float scaleX = (float) parentWidth / (float) childWidth;
  11. final float scaleY = (float) parentHeight / (float) childHeight;
  12. ...
  13. switch(scaleType){
  14. ...
  15. case CENTER_INSIDE:
  16. //计算缩放倍数
  17. scale = Math.min(Math.min(scaleX, scaleY), 1.0f);
  18. //计算平移距离
  19. dx = parentBounds.left + (parentWidth - childWidth * scale) * 0.5f;
  20. dy = parentBounds.top + (parentHeight - childHeight * scale) * 0.5f;
  21. //设置缩放矩阵
  22. transform.setScale(scale, scale);
  23. //设置评议距离
  24. transform.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f));
  25. break;
  26. }
  27. return transform;
  28. }

其他具体的ScaleType处理就不赘述了,有兴趣的同学可以自己看源码再研究。默认的ScaleType是CENTER_CROP。

在计算好矩阵之后,我们来看一下这个容器是怎么将它的内容绘制出来的:

  1. public void draw(Canvas canvas) {
  2. //计算矩阵
  3. configureBoundsIfUnderlyingChanged();
  4. if (mDrawMatrix != null) {
  5. int saveCount = canvas.save();
  6. canvas.clipRect(getBounds());
  7. canvas.concat(mDrawMatrix);
  8. super.draw(canvas);
  9. canvas.restoreToCount(saveCount);
  10. } else {
  11. //无变换矩阵时,直接让Drawable绘制到确定的边缘中。
  12. super.draw(canvas);
  13. }
  14. }

我们可以看到它将矩阵应用到Canvas中,并调用ForwardingDrawabledraw(Canvas)让它将目标视图绘制出来,之后还原Canvas的缩放属性防止累加缩放。

暂时先介绍各类容器Drawable的功能,为方便后续理解。待分析完Fresco的架构之后再为分析。

4 视图型Drawable

大多数情况下,Fresco用于表现图片的视图型Drawable使用的就是Android原生Drawable来做图像的载体。不过也有两个例外:

ProgressBarDrawable

ProgressBarDrawable是负责绘制进度条的Drawable。它内部维持一个level用来描述进度(0<=level<=10000),并自己实现了绘制过程,我们首先通过源码来看一下是怎么绘制的:

  1. @Override
  2. public void draw(Canvas canvas) {
  3. if (mHideWhenZero && mLevel == 0) {
  4. return;
  5. }
  6. drawBar(canvas, 10000, mBackgroundColor);
  7. drawBar(canvas, mLevel, mColor);
  8. }
  9. private void drawBar(Canvas canvas, int level, int color) {
  10. Rect bounds = getBounds();
  11. int length = (bounds.width() - 2 * mPadding) * level / 10000;
  12. int xpos = bounds.left + mPadding;
  13. int ypos = bounds.bottom - mPadding - mBarWidth;
  14. mPaint.setColor(color);
  15. canvas.drawRect(xpos, ypos, xpos + length, ypos + mBarWidth, mPaint);
  16. }

可以看出,它先将整个进度条填充满backgroundColor颜色(可以通过setBackgroundColor设置),再将进度覆盖区域矩形填充满color颜色(可以通过setColor设置)。

RoundedBitmapDrawable

这个Drawable与上面的容器型RoundedCornersDrawable有几个区别:

RoundedBitmapDrawable是将自身内容修剪成圆角矩形边绘制出来,并且可以使用Bitmap作为对象,返回一个BitmapDrawable。而RoundedCornersDrawable是将容器内容修剪成圆角矩形边,并且可以选择是否用指定颜色覆盖容器内容,可以使用任何Drawable当做容器。

待分析完Fresco的架构之后,会回来分析圆角图片的实现机制。它与正常的使用Xfermode实现方式不同。

实际上这两个类的功能是有一定重合的,我认为是由于RoundedCornerDrawable目前只能做到用圆角矩形覆盖内容,而无法将内容修剪成圆角矩形,所以才使用了RoundedBitmapDrawable。关于RoundedCornersDrawable的功能Fresco也在改进中。期待后续它能将两个功能合并起来。

5 特殊Drawable - TransformAwareDrawable 和 VisibilityAwareDrawable

为什么说它们特殊呢,因为他们只是接口!

TransformAwareDrawable要和TransfromCallback一起使用。TransformAwareDrawable的作用很简单,就是提供设置TransfromCallback的回调函数,那我们来看看TransfromCallback的作用是什么:

  1. public interface TransformCallback {
  2. // 获取已经应用在自身的变换Matrix,储存在transfrom中。
  3. public void getTransform(Matrix transform);
  4. // 获取根节点边界,储存在bounds中。
  5. public void getRootBounds(RectF bounds);
  6. }

之所以要设置这个回调,是因为本篇中的Drawable是有层次的。如果B 是 A的子图层,那应用在A上的变换矩阵自然应该应用到B上,所以提供这个回调可以让B获取应用在A上的变换矩阵,从而正确地进行绘制。

在本篇文章中出现的所有Drawable都实现了TransformAwareDrawableTransfromCallbackArrayDrawable中的getTransfrom中可以看出它的工作机制(实际上除了个别自身有缩放的图层如ScaleTypeDrawable, MatrixDrawable外的实现都是想以下这段代码一样):

  1. @Override
  2. public void getTransform(Matrix transform) {
  3. //如果有父图层,则获取应用在父图层上的变换矩阵
  4. if (mTransformCallback != null) {
  5. mTransformCallback.getTransform(transform);
  6. } else {
  7. //如果没有父图层,就获取单位矩阵
  8. transform.reset();
  9. }
  10. }

ScaleTypeDrawableMatrixDrawable中会将自身的变换矩阵通过Matrix.confat(Matrix m)传给transform。如此一来就实现了变换矩阵向下传递的功能。

VisibilityAwareDrawableVisibilityCallback搭配使用,它提供了在自身可见度改变的时候的通知函数(onVisibilityChange(boolean visible))和在自身绘制时发生通知的回调(onDraw())。仅仅GenericDraweeHierarchy.RootDrawable实现了它。

6 类图

由于类中方法、变量过多,作者对其做了大量精简,仅用于参考设计层次。

Class Diagram

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注