Fresco图片显示原理浅析

(第一篇)Fresco架构设计赏析

(第二篇)Fresco缓存架构分析

本文是Fresco源码分析系列第三篇文章,主要来分析一下Fresco UI层的实现,包括下面这些点:

  1. 图片显示原理,图片加载过程中各个阶段的图片切换原理。(比如由占位图->目标图片)
  2. 圆角的实现
  3. ScaleType的实现

图片显示原理与多状态切换逻辑

Fresco中负责图片展示工作的是DraweeHierarchy,它内部维护着一个Drawable序列,在图片加载过程中的不同阶段可以显示不同状态的Drawable

图片显示原理

我们一般直接使用SimpleDraweeView,它继承自DraweeView:

DraweeView.java

public class DraweeView<DH extends DraweeHierarchy> extends ImageView {

    public void setController(@Nullable DraweeController draweeController) {
        mDraweeHolder.setController(draweeController);
        super.setImageDrawable(mDraweeHolder.getTopLevelDrawable()); 
    }
}

在调用SimpleDraweeView.setImageUri()时会调用到DraweeView.setController(),即此时是直接显示的mDraweeHolder.getTopLevelDrawable():

DraweeHolder.java

public @Nullable Drawable getTopLevelDrawable() {
    return mHierarchy == null ? null : mHierarchy.getTopLevelDrawable();
}

所以最终的显示的DrawablemHierarchy.getTopLevelDrawable()mHierarchy的实现是GenericDraweeHierarchymHierarchy.getTopLevelDrawable()获取的Drawable实际上可以理解为FadeDrawable:

GenericDraweeHierarchy.java

    GenericDraweeHierarchy(GenericDraweeHierarchyBuilder builder) {
        mFadeDrawable = new FadeDrawable(layers);
        Drawable maybeRoundedDrawable = WrappingUtils.maybeWrapWithRoundedOverlayColor(mFadeDrawable, mRoundingParams);
        mTopLevelDrawable = new RootDrawable(maybeRoundedDrawable); //RootDrawable 只是一个装饰类
    }

FadeDrawable内部维护着一个Drawable数组,它可以由一个Drawable切换到另一个DrawableDrawable的切换过程中伴有着透明度改变的动画:

public class FadeDrawable extends ArrayDrawable {
    private final Drawable[] mLayers;
    
    @Override
    public void draw(Canvas canvas) {
        ...更新Drawable的透明度

        //从前往后一层一层的画出来
        for (int i = 0; i < mLayers.length; i++) {
            drawDrawableWithAlpha(canvas, mLayers[i], mAlphas[i] * mAlpha / 255);
        }
    }
}

Fresco中一共有多少层Drawable(layer)呢?我们看一下GenericDraweeHierarchy的初始化代码:

GenericDraweeHierarchy.java

    private static final int BACKGROUND_IMAGE_INDEX = 0;
    private static final int PLACEHOLDER_IMAGE_INDEX = 1;
    private static final int ACTUAL_IMAGE_INDEX = 2;
    private static final int PROGRESS_BAR_IMAGE_INDEX = 3;
    private static final int RETRY_IMAGE_INDEX = 4;
    private static final int FAILURE_IMAGE_INDEX = 5;
    private static final int OVERLAY_IMAGES_INDEX = 6;

    GenericDraweeHierarchy(GenericDraweeHierarchyBuilder builder) {
        Drawable[] layers = new Drawable[numLayers];  // 一般是6层
        layers[BACKGROUND_IMAGE_INDEX] = ...
        layers[PLACEHOLDER_IMAGE_INDEX] = ...
        layers[ACTUAL_IMAGE_INDEX] = ...
        layers[PROGRESS_BAR_IMAGE_INDEX] = ...
        layers[RETRY_IMAGE_INDEX] = ...
        layers[FAILURE_IMAGE_INDEX] = ...
        ...这里还有一个overlayer层
    }

即在构造GenericDraweeHierarchy就确定了有几层Drawable(FresconumLayers的值一般为6)。当然如果没有这一层Drawable(比如没有提供Progress Drawable),那么这一层Drawable就是null。通过FadeDrawable.draw()已经知道会按照顺序把这些Drawable都画出来(Drawable为null的话就不会画, 透明度为0也不会画)。

可以看到我们实际上要显示的图片位于第3层级。那么如果图片加载完成,如何从加载进度的Drawable切换到实际的图片呢?:

GenericDraweeHierarchy.java

public void setImage(Drawable drawable, float progress, boolean immediate) {
    drawable = WrappingUtils.maybeApplyLeafRounding(drawable, mRoundingParams, mResources); //包裹上圆形参数
    ...
    fadeOutBranches();
    fadeInLayer(ACTUAL_IMAGE_INDEX);
    ...
}

private void fadeOutBranches() {
    fadeOutLayer(PLACEHOLDER_IMAGE_INDEX);
    fadeOutLayer(ACTUAL_IMAGE_INDEX);
    fadeOutLayer(PROGRESS_BAR_IMAGE_INDEX);
    fadeOutLayer(RETRY_IMAGE_INDEX);
    fadeOutLayer(FAILURE_IMAGE_INDEX);
}

当加载完成完最终图片后就会调用到GenericDraweeHierarchy.setImage(),上面逻辑其实涉及到的代码很多,但是逻辑很简单就不深入看了。上面的两个核心方法可以这样理解:

  • fadeOutLayer() : 把这一层Drawable(layer)的透明度设置为0
  • fadeInLayer() : 把这一层的透明度设置为1

到这里,基本上就叙述了Fresco的图片显示原理。其实整体流程可以用下图表示:

Fresco图片显示原理.png

圆角的实现

直接来看具体的实现代码:

WrappingUtils.java

private static Drawable applyLeafRounding(Drawable drawable, RoundingParams roundingParams, Resources resources) {
    if (drawable instanceof BitmapDrawable) {
        final BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
        RoundedBitmapDrawable roundedBitmapDrawable = new RoundedBitmapDrawable(resources,bitmapDrawable.getBitmap(),bitmapDrawable.getPaint());
        applyRoundingParams(roundedBitmapDrawable, roundingParams);
        return roundedBitmapDrawable;
    }
    ...
    return drawable;
}

RoundedBitmapDrawable.java

    @Override
    public void draw(Canvas canvas) {
        if (!shouldRound()) {
            super.draw(canvas);
            return;
        }
        ...
        updatePath(); //更新圆角path or 圆形path
        updatePaint();
        ...
        canvas.drawPath(mPath, mPaint);
    }

    private void updatePaint() {
        if (mLastBitmap == null || mLastBitmap.get() != mBitmap) {
            mLastBitmap = new WeakReference<>(mBitmap);
            mPaint.setShader(new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
            mIsShaderTransformDirty = true;
        }
        ...
    }

Fresco的圆角实际上是使用BitmapShader来实现的。

ScaleType的实现

FrescoScaleType的实现原理其实和ImageView是相同的。但由于SimpleDraweeView内部是维护了多个Drawable,所以它并不能直接使用ImageView的实现方式,它需要把它维护的每一个Drawable都做对应的ScaleType操作。我们先来看一下ImageViewScaleType的实现:

ImageView ScaleType的实现

ImageView中 CENTER_CROP 的实现

    private void configureBounds() {
        //drawable 的宽高
        final int dwidth = mDrawableWidth;  
        final int dheight = mDrawableHeight;

        //当前view的宽高
        final int vwidth = getWidth() - mPaddingLeft - mPaddingRight;
        final int vheight = getHeight() - mPaddingTop - mPaddingBottom;

        ...
        if (ScaleType.CENTER_CROP == mScaleType) {
            mDrawMatrix = mMatrix;

            float scale;
            float dx = 0, dy = 0;

            if (dwidth * vheight > vwidth * dheight) {
                scale = (float) vheight / (float) dheight;
                dx = (vwidth - dwidth * scale) * 0.5f;
            } else {
                scale = (float) vwidth / (float) dwidth;
                dy = (vheight - dheight * scale) * 0.5f;
            }

            mDrawMatrix.setScale(scale, scale);
            mDrawMatrix.postTranslate(Math.round(dx), Math.round(dy)); // 从哪个坐标开始画 Drawable
        }
        ...
    }

Fresco的实现

Fresco中如果对图片设置了ScaleType,那么就会把对应的Drawable封装为ScaleTypeDrawable, 它的draw():

    public void draw(Canvas canvas) {
        // 这个方法类似于 ImageView configureBounds的实现, 配置了 mDrawMatrix
        configureBoundsIfUnderlyingChanged(); 
        if (mDrawMatrix != null) {
            int saveCount = canvas.save();
            canvas.clipRect(getBounds());
            canvas.concat(mDrawMatrix);
            super.draw(canvas);
            canvas.restoreToCount(saveCount);
        } else {
            super.draw(canvas);
        }
    }

欢迎关注我的Android进阶计划看更多干货

欢迎关注我的微信公众号:susion随心

微信公众号.jpeg
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342