Fresco前传(2):源码分析 DraweeHierarchy/DraweeView/DraweeController

前言

Fresco的中文文档,最正宗的使用方法,当然在文档中寻找。

正文

DraweeHierarchy/DraweeView/DraweeController

Fresco是一个图片请求加载处理框架,整体架构是MVC模式 (DraweeHierarchy,DraweeView,DraweeController)。

想要使用Fresco非常简单,但是要理解就首先要明白一下一些关键概念。

DraweeHierarchy是一个树状的层次结构,用于组织和维护最终绘制和呈现的Drawable对象。

DraweeView用于显示DraweeHierarchy最顶层的图像(getTopLevelDrawable()),在使用DraweeView之前,要使用setHierarchy()方法为其提供一个DraweeHierarchy,使用setController()为其提供一个DraweeController。

DraweeController根据DraweeView转发到DraweeController的事件,来控制DraweeHierarchy最顶的图像。

三者交互关系

DraweeView把获得事件Event转发给DraweeController,然后DraweeController根据事件Event来决定显示的图像,而这些图像都存储在DraweeHierarchy中,最后DraweeView绘制时直接通过DraweeHierarchy.getTopLevelDraweeable()获取需要显示的图像。

通过交互关系可以知道,DrweeView一方面从DraweeHierarchy中获取图像用于展示,另一方面将事件转发给DraweeController。这样是不是代表在DraweeView中有着DraweeHierarchy和DraweeController的相关的代码呢?看一下代码:

public class DraweeView<DH extends DraweeHierarchy> extends ImageView {

  private DraweeHolder<DH> mDraweeHolder;
  // 省略代码...

  /** Sets the hierarchy. */
  public void setHierarchy(DH hierarchy) {
    mDraweeHolder.setHierarchy(hierarchy);
    super.setImageDrawable(mDraweeHolder.getTopLevelDrawable());
  }
  /** Sets the controller. */
  public void setController(@Nullable DraweeController draweeController) {
    mDraweeHolder.setController(draweeController);
    super.setImageDrawable(mDraweeHolder.getTopLevelDrawable());
  }
}

可以看到,在DraweeView中通过DraweeHolder来存储了DraweeHierarchy和DraweeController。下面是DraweeView的代码:

/**
 * A holder class for Drawee controller and hierarchy.
 *
 * Drawee users, should, as a rule, use {@link DraweeView} or its subclasses. There are
 * situations where custom views are required, however, and this class is for those circumstances.
 */
public class DraweeHolder<DH extends DraweeHierarchy> implements VisibilityCallback {

  private DH mHierarchy;
  private DraweeController mController = null;

}

从DraweeHolder的代码和注释中可以明白这么做的原因:这个类持有了DraweeHierarchy和DraweeController,当你想要自定义View时,可以让自定义View持有DraweHolder对象,进而操作DraweeController和DraweeHierarchy两个对象。(后面将PhotoView和Fresco结合用到的就是DraweeHolder)

废话这么多,说白了就是为了解偶和更大的灵活性而做的设计。

此外,值得注意的是在DraweeView中,使用setHierarchy()或者setController()时,内部都会调用这样一句话:

super.setImageDrawable(mDraweeHolder.getTopLevelDrawable());

这样做的目的在于,更新需要显示的图像。也就是说,通过DraweeHierarchy.getTopLevelDrawable()可以拿到需要被显示的图像,而该图像究竟如何被显示,则在于上面这句代码。要知道super.setImageDrawable()调用的是ImageView中的方法,为ImageView设置图像内容。

DraweeView详细分析

前文说过,想要使用DraweeView,必须为其提供DraweeHierarchy和DraweeController,DraweeHierarchy提供需要显示的图像,DraweeController负责获取图像。出于解耦和灵活性,FB将DraweeHierarchy和DraweeController的实例放在了DraweeHolder中。看一下在DraweeView是如何构建DraweeHoler的。

private void init(Context context) {
 mDraweeHolder = DraweeHolder.create(null, context);
}
在DraweeView的构造函数中调用了init()方法,使用DraweeHolder.create(null, context)来构建一个DraweeHolder,create()的方法声明如下:

public static <DH extends DraweeHierarchy> DraweeHolder<DH> create(
    @Nullable DH hierarchy,
    Context context) {
  DraweeHolder<DH> holder = new DraweeHolder<DH>(hierarchy);
  ..
  return holder;
}
public DraweeHolder(@Nullable DH hierarchy) {
  if (hierarchy != null) {
    setHierarchy(hierarchy);
  }
  ...
}

可以看到,在构建DraweeHoler的时候,并没有第一时间传入DraweeHierarchy对象,此时DraweeView还处于不可用状态。同样的,在 DraweeHolder中的DraweeController对象,也未在第一时间传入。

那么,DraweeHierarchy和DraweeController是在哪里被实例化和赋给DraweeHolder的呢?

DraweeView的继承体系如下:

-- DraweeView
-------GenericDraweeView
-----------SimpleDraweeView

在SimpleDraweeView中只是简单的提供了setImageUri等方法,也就说明在GenericDraweeView中,提供了默认的DraweeHierarchy和DraweeController。

public class GenericDraweeView extends DraweeView<GenericDraweeHierarchy> {

  public GenericDraweeView(Context context, GenericDraweeHierarchy hierarchy) {
    super(context);
    setHierarchy(hierarchy);
  }

  public GenericDraweeView(Context context) {
    super(context);
    inflateHierarchy(context, null);
  }

  public GenericDraweeView(Context context, AttributeSet attrs) {
    super(context, attrs);
    inflateHierarchy(context, attrs);
  }

  public GenericDraweeView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    inflateHierarchy(context, attrs);
  }

  private void inflateHierarchy(Context context, @Nullable AttributeSet attrs) {
    Resources resources = context.getResources();
    }
}    

在GenericDraweeView有四个构造函数,其中一个是通过外部的GenericDraweeHierarchy来提供DrweeHierarchy,剩下的都是通过解析XML文件读取属性,然后根据建造者模式类GenericDraweeHierarchyBuilder,来构建一个GenericDraweeHierarchy,在inflateHierarchy方法的最后,通过setHierarchy(builder.build());语句,将DraweeHierarchy对象赋给了DraweeHolder的成员变量。

而DraweeController则是在SimpleDraweeView中的setImageURI()方法出构建出一个默认的,再通过setController()方法,赋给DraweeHolder的成员变量的。通过这两步,就完成了使用DraweeView的必备条件。

此外,在GenericDraweeView中还提供了设置期望宽高比的方法。
其中会调用requestLayout(),最后会调用onMeasure()方法从新设置SimpleDraweeView控件的大小比例。

/**
 * Sets the desired aspect ratio (w/h).
 */
 public void setAspectRatio(float aspectRatio) {
   if (aspectRatio == mAspectRatio) {
     return;
   }
   mAspectRatio = aspectRatio;
   requestLayout();
 }

在onMeasure()中调用了FB的一个用于帮助重新测量宽高的工具类。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  mMeasureSpec.width = widthMeasureSpec;
  mMeasureSpec.height = heightMeasureSpec;
  AspectRatioMeasure.updateMeasureSpec(
      mMeasureSpec,
      mAspectRatio,
      getLayoutParams(),
      getPaddingLeft() + getPaddingRight(),
      getPaddingTop() + getPaddingBottom());
  super.onMeasure(mMeasureSpec.width, mMeasureSpec.height);
}

通过updateMeasureSpec()方法的注释可以得知,想要让宽高比生效,那么,宽 或者 高其中之一必须被指定为0dp或者wrap_content(warp_content不推荐使用)。
假设:宽被指定为0dp,那么宽的实际尺寸,就会根据高的尺寸和宽高比计算出来。

public static void updateMeasureSpec(
    Spec spec,
    float aspectRatio,
    ViewGroup.LayoutParams layoutParams,
    int widthPadding,
    int heightPadding) {
  if (aspectRatio <= 0) {
    return;
  }
  if (shouldAdjust(layoutParams.height)) {
    int widthSpecSize = View.MeasureSpec.getSize(spec.width);
    int desiredHeight = (int) ((widthSpecSize - widthPadding) / aspectRatio + heightPadding);
    int resolvedHeight = View.resolveSize(desiredHeight, spec.height);
    spec.height = View.MeasureSpec.makeMeasureSpec(resolvedHeight, View.MeasureSpec.EXACTLY);
  } else if (shouldAdjust(layoutParams.width)) {
    int heightSpecSize = View.MeasureSpec.getSize(spec.height);
    int desiredWidth = (int) ((heightSpecSize - heightPadding) * aspectRatio + widthPadding);
    int resolvedWidth = View.resolveSize(desiredWidth, spec.width);
    spec.width = View.MeasureSpec.makeMeasureSpec(resolvedWidth, View.MeasureSpec.EXACTLY);
  }
}

// 判定是否需要根据比例适应宽或高
// 假设:高为0dp,那么代表控件的实际高度需要通过比例来计算。
// 假设:宽为0dp,那么代表控件的实际宽度需要通过比例来计算。
private static boolean shouldAdjust(int layoutDimension) {
  // 注意:wrap_content 是向后兼容的,但不应该被使用
  return layoutDimension == 0 || layoutDimension == ViewGroup.LayoutParams.WRAP_CONTENT;
}

以计算高为例,那么会执行以下代码,注释很清楚,看不懂的童鞋可以取补充下测量规格相关的知识:

// 获取父控件期望的宽的测量宽度
int widthSpecSize = View.MeasureSpec.getSize(spec.width);
// 根据父控件期望的宽的测量宽度和宽高比计算出咱们期望高的高度
int desiredHeight = (int) ((widthSpecSize - widthPadding) / aspectRatio + heightPadding);
// 期望的高度与父控件期望的高度两者取小的
int resolvedHeight = View.resolveSize(desiredHeight, spec.height);
// 最后重设高的测量规格
spec.height = View.MeasureSpec.makeMeasureSpec(resolvedHeight, View.MeasureSpec.EXACTLY);

最后

这里写图片描述

欢迎拍砖和讨论。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值