Android Drawable完全解析(一):Drawable源码分析(下)

Android Drawable完全解析(一):Drawable源码分析(上)
Android Drawable完全解析(一):Drawable源码分析(中)
Android Drawable完全解析(一):Drawable源码分析(下)

昨天下班前,分析了View实例将Drawable作为背景绘制到屏幕上面的流程,今天继续分析Drawable在ImageView中的绘制流程!

3:Drawable绘制流程

3.3:Drawable在ImageView中的绘制流程

ImageView使用Drawable的方式大体以下几种:

  • 在xml中直接设置android:background="@mipmap/voice"
    <ImageView
    android:layout_width="200dp"
    android:layout_height="100dp"
    android:background="@mipmap/voice"
    />
  • 在xml中直接设置android:src="@mipmap/voice"
    <ImageView
    android:layout_width="200dp"
    android:layout_height="100dp"
    android:src="@mipmap/voice"
    />
  • Java代码中调用 setImageResource(@DrawableRes int resId)
  • Java代码中调用 setImageDrawable(@Nullable Drawable drawable)
  • Java代码中调用 setBackgroundDrawable,实质是调用View.setBackgroundDrawable,上篇文章已分析。

下面就这几种方式逐一分析:
首先上原图:


voice.png

3.3.1:android:background="@mipmap/voice"

        <ImageView
            android:layout_width="200dp"
            android:layout_height="100dp"
            android:background="@mipmap/voice"
            />

实际效果:


background.png

可见直接使用android:background,图片作为背景完全铺满ImageView尺寸,会根据ImageView的范围缩放。
既然在xml中布局ImageView,那么肯定是调用ImageView(Context context, @Nullable AttributeSet attrs),看一下关键代码:

public class ImageView extends View {
    public ImageView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }
****
    public ImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
            int defStyleRes) {
        //调用View的构造函数
        super(context, attrs, defStyleAttr, defStyleRes);
        ****
    }
}
一路追踪下去:
public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
    public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        this(context);
        final TypedArray a = context.obtainStyledAttributes(
                attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
        ****
        Drawable background = null;
        ****
        final int N = a.getIndexCount();
        for (int i = 0; i < N; i++) {
            int attr = a.getIndex(i);
            switch (attr) {
                //获取在xml中设置的android:background="@mipmap/voice"
                case com.android.internal.R.styleable.View_background:
                    background = a.getDrawable(attr);
                    break; 
                *****
            }
        }
        ****
        if (background != null) {
            setBackground(background);
        }
        ****
    }
    public void setBackground(Drawable background) {
        //[Android Drawable完全解析(一):Drawable源码分析(中)](http://www.jianshu.com/p/2213c62e4738)
        setBackgroundDrawable(background);
    }
}

可见:
在xml中直接设置android:background="@mipmap/voice"实质是通过调用View.setBackgroundDrawable(Drawable background)将图片绘制到屏幕上!

View.setBackgroundDrawable(Drawable background)在上一篇文章:Android Drawable完全解析(一):Drawable源码分析(中)有过分析!
为什么背景图会铺满整个ImageView,是因为在View绘制过程中,将背景Drawable的绘制范围设置为和View的尺寸一致:

    void setBackgroundBounds() {
        if (mBackgroundSizeChanged && mBackground != null) {
            mBackground.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
            mBackgroundSizeChanged = false;
            rebuildOutline();
        }
    }

3.3.2:android:src="@mipmap/voice"

        <ImageView
            android:layout_width="200dp"
            android:layout_height="100dp"
            android:src="@mipmap/voice"
            />

实际效果:


src.png

可见直接使用android:src,默认情况下图片会根据ImageView的尺寸在保留自身宽高比例下进行缩放,最后在ImageView的中心显示。
既然在xml中布局ImageView,那么肯定是调用ImageView(Context context, @Nullable AttributeSet attrs),同样看一下关键代码:

public class ImageView extends View {
    public ImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
            int defStyleRes) {
        //super上面分析过了,绘制的是android:background="@mipmap/voice"
        super(context, attrs, defStyleAttr, defStyleRes);
        //主要设置了 ImageView实例中图像边界 与 ImageView边界间的缩放关系
        initImageView();
        final TypedArray a = context.obtainStyledAttributes(
                attrs, R.styleable.ImageView, defStyleAttr, defStyleRes);
        //将xml中使用android:src="@mipmap/voice"设置的图片生成Drawable实例
        final Drawable d = a.getDrawable(R.styleable.ImageView_src);
        if (d != null) {
            //将src生成的Drawable实例设置为ImageView的内容
            setImageDrawable(d);
        }
        ****
        //在我们的例子中,没有设置scaleType属性,则index = -1;
        final int index = a.getInt(R.styleable.ImageView_scaleType, -1);
        if (index >= 0) {
            //在我们例子中,index = -1,下面代码不执行
            setScaleType(sScaleTypeArray[index]);
        }
        //解析在xml中设置的tint和tintMode属性值
        if (a.hasValue(R.styleable.ImageView_tint)) {
            mDrawableTintList = a.getColorStateList(R.styleable.ImageView_tint);
            mHasDrawableTint = true;
            // Prior to L, this attribute would always set a color filter with
            // blending mode SRC_ATOP. Preserve that default behavior.
            mDrawableTintMode = PorterDuff.Mode.SRC_ATOP;
            mHasDrawableTintMode = true;
        }
        if (a.hasValue(R.styleable.ImageView_tintMode)) {
            mDrawableTintMode = Drawable.parseTintMode(a.getInt(
                    R.styleable.ImageView_tintMode, -1), mDrawableTintMode);
            mHasDrawableTintMode = true;
        }
        //根据当前ImageView的ColorStateList对 通过src生成的Drawable实例进行着色
        applyImageTint();
        final int alpha = a.getInt(R.styleable.ImageView_drawableAlpha, 255);
        //设置透明度
        if (alpha != 255) {
            setImageAlpha(alpha);
        }
        mCropToPadding = a.getBoolean(
                R.styleable.ImageView_cropToPadding, false);
        a.recycle();
        //need inflate syntax/reader for matrix
    }
    private void initImageView() {
        ****
        //设置mScaleType = ScaleType.FIT_CENTER;可见ImageView中
        //mScaleType默认就是ScaleType.FIT_CENTER
        mScaleType = ScaleType.FIT_CENTER;
        ****
    }
    public void setImageDrawable(@Nullable Drawable drawable) {
        if (mDrawable != drawable) {
            ****
            //对src生成的Drawable实例设置一系列属性
            updateDrawable(drawable);
            ****
            //最后调用invalidate()触发draw
            invalidate();
        }
    }
    private void updateDrawable(Drawable d) {
        ****
        //将ImageView实例之前关联的Drawable实例的动画监听移除,
        //并停止其已经在执行的动画,解除其所有事件
        if (mDrawable != null) {
            mDrawable.setCallback(null);
            unscheduleDrawable(mDrawable);
            if (isAttachedToWindow()) {
                mDrawable.setVisible(false, false);
            }
        }
        //将mDrawable赋值为通过src属性生成的Drawable实例
        mDrawable = d;
        if (d != null) {
            //为通过src生成的Drawable实例设置动画监听为ImageView实例自身;
            //并设置其布局方向,状态数组,Drawable动画是否开启,Drawable的level值。
            d.setCallback(this);
            d.setLayoutDirection(getLayoutDirection());
            if (d.isStateful()) {
                d.setState(getDrawableState());
            }
            if (isAttachedToWindow()) {
                d.setVisible(getWindowVisibility() == VISIBLE && isShown(), true);
            }
            d.setLevel(mLevel);
            mDrawableWidth = d.getIntrinsicWidth();
            mDrawableHeight = d.getIntrinsicHeight();
            //根据当前ImageView实例的ColorStateList对其进行着色
            applyImageTint();
            //未执行实质代码
            applyColorMod();
            //设置Drawable实例的绘制范围不变,并根据ImageView实例内容区域和
            //Drawable实例原始绘制范围,确定Drawable实例在实际绘制时候的缩放。
            configureBounds();
        } else {
            mDrawableWidth = mDrawableHeight = -1;
        }
    }
    private void applyImageTint() {
        ****
                //根据当前ImageView的ColorStateList对 通过src生成的Drawable实例进行着色
                mDrawable.setTintList(mDrawableTintList);
        ****
    }
    private void applyColorMod() {
        //对应通过 src生成的Drawble实例来说,ImageView并未调用setColorFilter
        //mColorMod也为默认的false值,所以下面代码实质未执行
        if (mDrawable != null && mColorMod) {
            mDrawable = mDrawable.mutate();
            //如果当前ImageView实例调用过setColorFilter,
            //则对 通过src生成的Drawable实例设置相同的ColorFilter
            if (mHasColorFilter) {
                mDrawable.setColorFilter(mColorFilter);
            }
            mDrawable.setXfermode(mXfermode);
            mDrawable.setAlpha(mAlpha * mViewAlphaScale >> 8);
        }
    }
    private void configureBounds() {
        //通过src生成的Drawable实例 原始宽高
        final int dwidth = mDrawableWidth;
        final int dheight = mDrawableHeight;
        //ImageView实例的内容区域宽高(去除了padding值)
        final int vwidth = getWidth() - mPaddingLeft - mPaddingRight;
        final int vheight = getHeight() - mPaddingTop - mPaddingBottom;
        ****
        if (dwidth <= 0 || dheight <= 0 || ScaleType.FIT_XY == mScaleType) {
            //当ImageView设置过android:scaleType="fitXY" 或setScaleType(ScaleType.FIT_XY),
            //则将此Drawable实例的绘制范围设定为ImageView实例的内容区域
            mDrawable.setBounds(0, 0, vwidth, vheight);
            mDrawMatrix = null;
        } else {
            //对应我们例子中,未设置android:scaleType情况下,
            //通过src生成的Drawable实例的绘制范围就是其原始范围
            mDrawable.setBounds(0, 0, dwidth, dheight);
            //下面代码设置了mDrawMatrix的属性
            //在initImageView()方法中已知:
            //ImageView中mScaleType默认就是ScaleType.FIT_CENTER
            if (ScaleType.MATRIX == mScaleType) {
                ****
            } else if (fits) {
                ****
            } else if (ScaleType.CENTER == mScaleType) {
                ****
            } else if (ScaleType.CENTER_CROP == mScaleType) {
                ****
            } else if (ScaleType.CENTER_INSIDE == mScaleType) {
                ****
            } else {
                //ImageView中mScaleType默认就是ScaleType.FIT_CENTER
                //则根据ImageView实例内容区域的范围和Drawable实例实际宽高来设置mDrawMatrix
                mTempSrc.set(0, 0, dwidth, dheight);
                mTempDst.set(0, 0, vwidth, vheight);
                mDrawMatrix = mMatrix;
                mDrawMatrix.setRectToRect(mTempSrc, mTempDst, scaleTypeToScaleToFit(mScaleType));
            }
        }
    }
}

ImageView实例生成后,肯定还是执行onDraw方法将自身绘制到屏幕上,继续追踪代码:

public class ImageView extends View {
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        ****
        //在上面分析过 mDrawMatrix不为null
        //mDrawMatrix的属性根据ImageView实例内容区域的范围和Drawable实例实际宽高来配置
        if (mDrawMatrix == null && mPaddingTop == 0 && mPaddingLeft == 0) {
            //如果矩阵mDrawMatrix为空,且ImageView的上下padding值都为0
            //则直接将Drawable实例绘制到画布上
            mDrawable.draw(canvas);
        } else {
            ****
            //我们例子中,矩阵mDrawMatrix不为空,则将其设置到ImageView的画布上
            if (mDrawMatrix != null) {
                canvas.concat(mDrawMatrix);
            }
            //然后在画布上面绘制Drawable实例
            mDrawable.draw(canvas);
            canvas.restoreToCount(saveCount);
        }
    }
}

至此,
android:src="@mipmap/voice"整个流程就分析完了,流程总结如下:

在ImageView构造函数中:

1:设置缩放类型默认为 ScaleType.FIT_CENTER(图像居中等比例缩放)
2:在ImageView构造函数中,解析xml中android:src属性获取Drawable实例;
3:为生成的Drawable实例设置一系列属性:

  • 设置动画监听为ImageView实例自身:d.setCallback(this);
  • 设置布局方向和ImageView实例一致:d.setLayoutDirection(getLayoutDirection());
  • 设置状态数组和ImageView实例一致:d.setState(getDrawableState());
  • 设置动画是否可见和 ImageView可见性一致:d.setVisible(getWindowVisibility() == VISIBLE && isShown(), true);
  • 设置动画当前Level值和ImageView的mLevel值一致:d.setLevel(mLevel);
  • 根据当前ImageView实例的ColorStateList对其进行着色:applyImageTint();
  • 设置绘制范围为原始绘制范围setBounds 且 根据ImageView、Drawable实例的范围 和 缩放类型 来设置Matrix mDrawMatrix(用于onDraw):configureBounds();

4:如果我们在xml中还设置了缩放类型,着色,着色模式,透明度,
则为mScaleType重新赋值,并为生成的Drawable实例逐一设置着色,着色模式,透明度

在ImageView的onDraw方法中:

1:如果矩阵mDrawMatrix为空,且ImageView的上下padding值都为0,则直接将Drawable实例绘制到画布上
2:其余情况下:

  • 如果矩阵mDrawMatrix不为空,则将其设置到ImageView的画布上;
  • 然后在画布上面绘制Drawable实例

本质上还是执行了Drawable.draw(@NonNull Canvas canvas)将src生成的Drawable实例绘制到ImageView实例所在的画布

3.3.3:setImageDrawable(@Nullable Drawable drawable)

setImageDrawable在上面分析过程中出现过

    public void setImageDrawable(@Nullable Drawable drawable) {
        if (mDrawable != drawable) {
            mResource = 0;
            mUri = null;
            final int oldWidth = mDrawableWidth;
            final int oldHeight = mDrawableHeight;
            updateDrawable(drawable);
            if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
                requestLayout();
            }
            //invalidate会引发重绘,调用onDraw方法,直接看上面onDraw的流程分析即可
            invalidate();
        }
    }

3.3.4:setImageResource(@DrawableRes int resId)

    public void setImageResource(@DrawableRes int resId) {
        ****
        //在updateDrawable(Drawable d)中:mDrawable = d;
        //此处将mDrawable重置为null
        updateDrawable(null);
        //为mResource赋值为传入的资源ID,mUri重置为null
        mResource = resId;
        mUri = null;
        resolveUri();
        ****
        //引发重绘
        invalidate();
    }
    private void resolveUri() {
        //updateDrawable(null)已经将mDrawable重置为null
        if (mDrawable != null) {
            return;
        }
        if (getResources() == null) {
            return;
        }
        Drawable d = null;
        //在中setImageResource(@DrawableRes int resId)已知:mResource = resId;
        if (mResource != 0) {
            //通过setImageResource传入的resId通常不为0,执行如下:
            try {
                //通过传入的图片资源ID获取Drawable实例
                d = mContext.getDrawable(mResource);
            } catch (Exception e) {
                Log.w(LOG_TAG, "Unable to find resource: " + mResource, e);
                // Don't try again.
                mUri = null;
            }
        } else if (mUri != null) {
            d = getDrawableFromUri(mUri);
            if (d == null) {
                Log.w(LOG_TAG, "resolveUri failed on bad bitmap uri: " + mUri);
                // Don't try again.
                mUri = null;
            }
        } else {
            return;
        }
        //将通过传入的图片资源ID生成的Drawable实例作为参数,
        //调用updateDrawable,上面已经分析过此方法
        updateDrawable(d);
    }

setImageResource(@DrawableRes int resId)整个流程就分析完了,流程总结如下:

  • 1:首先执行updateDrawable(null),将已经绘制完毕的mDrawable动画停止,移除所有事件及动画监听,并重置为null
  • 2:用Resource实例通过resId获取Drawable实例,作为参数执行updateDrawable
  • 3:ImageView实例执行重绘,详见之前onDraw的分析

至此,Drawable在ImageView中的绘制流程就分析完毕了!Drawable源码分析也告一段落,如有错误或者翻译问题请各位大神留言!

'Android Drawable完全解析' 系列未完待续...

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

推荐阅读更多精彩内容