佛系源码解读系列 —— RoundedImageView

最近闲来无事,项目写得也是耦合度非常之高;所以特地进行一个源码解读系列来烧脑一番,希望在阅读优秀框架和源码的同时来提升自己的代码抽象程度,为什么叫佛系呢?? 因为我本人比较懒,更新博客的频率大概也就一周一篇的样子,大部分时间都在探索新的领域去了,所以。。。

阅读的源码都是Github上star数量比较多的项目,其中有一位优秀得开发者整合了star比较多的框架:https://github.com/SenhLinsh/Android-Hot-Libraries

下面看看今天第一个分析的源码—— RoundedImageView, 该源码是一个自定义view,源码只有一个类文件,一开始我也不准备读多么复杂的源码,从简单的开始; 我再Github托管了一个新的项目,上面的将会存放所有分析源码的文章和使用这些开源框架的 demo~

主页

RoundedImageView使用

RoundedImageView 使用

一、了解 Drawable

RoundedImageView 是继承自ImageView的自定义View,在了解该类之前,我们应该
了解到Drawable这个类的子类及其应用,打开源码,我们看到Drawable的子类非常多,
这也就是我们在使用drawable文件夹中的资源的时候,可以使用类似图片、shapeSelector
等资源,其实系统就是将其中的资源转换成了对应的Drawable:

Drawable子类.png

在这里,不再赘述Drawable中的源码,现在也没有分析到Drawable的源码,所以借鉴网上大佬的文章:
https://blog.csdn.net/monkey646812329/article/details/52947966

查看Drawable源码, 发现它是一个抽象类,其中还包含一个抽象内部类,所有实现Drawable类并且具有不同状态的子类,都需要实现其内部子类ConstantState

drawable_method_innerclass.png

源码中对此内部类的注释为: 使用该抽象类来保存共享的常量状态和数据,在同一资源创建的(BitmapDrawable)唯一位图保存在其中。
newDrawable() 可以看作是工厂方法来创建Drawable实例
getConstantState() 来检索DrawableConstantState,调用Drawable中的mutate() 通常是为其创建一个新的ConstantState

下面先分析几个我们在项目中常用到的:

Selector —— StateListDrawable
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/bubble_gray_right" 
          android:state_pressed="true"/>
    <item android:drawable="@drawable/bubble_gray_right"/>
</selector>

这是一个最常用的selectorview的不同状态显示不同的item, 而在系统中,最终生成的是StateListDrawableselector的常用状态如下:
android:state_pressed 是否按下,如一个按钮触摸或者点击。
android:state_focused 是否取得焦点,比如用户选择了一个文本框。
android:state_hovered 光标是否悬停,通常与focused state相同,它是4.0的新特性
android:state_selected 被选中,它与focus state并不完全一样,如一个list view 被选中的时候,它里面的各个子组件可能通过方向键,被选中了。
android:state_checkable 组件是否能被check。如:RadioButton是可以被check的。
android:state_checked 被checked了,如:一个RadioButton可以被check了。
android:state_enabled 能够接受触摸或者点击事件
android:state_activated 是否被激活
android:state_window_focused 应用程序是否在前台,当有通知栏被拉下来或者一个对话框弹出的时候应用程序就不在前台了

注意:

如果有多个item,那么系统将自动从上到下进行匹配,最先匹配的将得到的item。(不是通过最佳匹配)如果一个item没有任何的状态说明,那么它将可以被任何一个状态匹配。

Shape —— StateListDrawable
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="@color/white" />
    <corners android:radius="@dimen/dp_10" />
</shape>

Shape顾名思义是形状的意思,如果你想要画一个矩形、圆形或者椭圆形等二位图片,则可以使用Shape这个标签在xml中。

animation-list —— AnimationDrawable
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="false">
    <item
        android:drawable="@drawable/guess_number_1"
        android:duration="100" />
    <item
        android:drawable="@drawable/guess_number_2"
        android:duration="100" />
    <item
        android:drawable="@drawable/guess_number_3"
        android:duration="100" />
</animation-list>

animation-list 是一系列的item作为节点,每一个节点都是做为一个帧。

layer-list —— LayerDrawable
<?xml version="1.0" encoding="utf-8"?>
<layer-list
  xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@android:id/background">
        <shape>
            <scale android:scaleWidth="100%" />
            <solid android:color="#1affffff" />
            <corners android:radius="2.0dip" />
        </shape>
    </item>
    <item android:id="@android:id/secondaryProgress">
        <shape>
            <scale android:scaleWidth="100%" />
            <solid android:color="#1affffff" />
            <corners android:radius="2.0dip" />
        </shape>
    </item>
    <item android:id="@android:id/progress">
        <clip>
            <shape>
                <solid android:color="#ffffffff" />
                <corners android:radius="2.0dip" />
            </shape>
        </clip>
    </item>
</layer-list>

当我们设置ProgressBar的背景时,通常会用到一个layer-list来设置背景、进度、第二进度的颜色,系统在加载时就会直接

二、正式理解 RoundedDrawable

自定义view在创建的时候,会调用构造方法:

public RoundedImageView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    Log.d(TAG, "RoundedImageView(Context context, AttributeSet attrs, int defStyle)");
    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RoundedImageView, defStyle, 0);
    // 获取到拉伸模式
    int index = a.getInt(R.styleable.RoundedImageView_android_scaleType, -1);
    if (index >= 0) {
        setScaleType(SCALE_TYPES[index]);
    } else {
        // 默认的拉伸模式 FIT_CENTER
        setScaleType(ScaleType.FIT_CENTER);
    }
    // 获取到圆角大小
    float cornerRadiusOverride =
            a.getDimensionPixelSize(R.styleable.RoundedImageView_riv_corner_radius, -1);
    // 获取到左上、左下、右上、右下的圆角大小
    mCornerRadii[Corner.TOP_LEFT] =
            a.getDimensionPixelSize(R.styleable.RoundedImageView_riv_corner_radius_top_left, -1);
    mCornerRadii[Corner.TOP_RIGHT] =
            a.getDimensionPixelSize(R.styleable.RoundedImageView_riv_corner_radius_top_right, -1);
    mCornerRadii[Corner.BOTTOM_RIGHT] =
            a.getDimensionPixelSize(R.styleable.RoundedImageView_riv_corner_radius_bottom_right, -1);
    mCornerRadii[Corner.BOTTOM_LEFT] =
            a.getDimensionPixelSize(R.styleable.RoundedImageView_riv_corner_radius_bottom_left, -1);
    // 判断是否设置 左上、左下、右上、右下 的大小,没有就直接设置所有的
    boolean any = false;
    for (int i = 0, len = mCornerRadii.length; i < len; i++) {
        if (mCornerRadii[i] < 0) {
            mCornerRadii[i] = 0f;
        } else {
            any = true;
        }
    }
    if (!any) {
        if (cornerRadiusOverride < 0) {
            cornerRadiusOverride = DEFAULT_RADIUS;
        }
        for (int i = 0, len = mCornerRadii.length; i < len; i++) {
            mCornerRadii[i] = cornerRadiusOverride;
        }
    }
    // 边框大小
    mBorderWidth = a.getDimensionPixelSize(R.styleable.RoundedImageView_riv_border_width, -1);
    if (mBorderWidth < 0) {
        mBorderWidth = DEFAULT_BORDER_WIDTH;
    }
    // 边框颜色
    mBorderColor = a.getColorStateList(R.styleable.RoundedImageView_riv_border_color);
    if (mBorderColor == null) {
        mBorderColor = ColorStateList.valueOf(RoundedDrawable.DEFAULT_BORDER_COLOR);
    }
    mMutateBackground = a.getBoolean(R.styleable.RoundedImageView_riv_mutate_background, false);
    // 是否是圆形图片
    mIsOval = a.getBoolean(R.styleable.RoundedImageView_riv_oval, false);
    // 平铺方式
    final int tileMode = a.getInt(R.styleable.RoundedImageView_riv_tile_mode, TILE_MODE_UNDEFINED);
    if (tileMode != TILE_MODE_UNDEFINED) {
        setTileModeX(parseTileMode(tileMode));
        setTileModeY(parseTileMode(tileMode));
    }
    // X 轴平铺方式
    final int tileModeX =
            a.getInt(R.styleable.RoundedImageView_riv_tile_mode_x, TILE_MODE_UNDEFINED);
    if (tileModeX != TILE_MODE_UNDEFINED) {
        setTileModeX(parseTileMode(tileModeX));
    }
    // Y 轴平铺方式
    final int tileModeY =
            a.getInt(R.styleable.RoundedImageView_riv_tile_mode_y, TILE_MODE_UNDEFINED);
    if (tileModeY != TILE_MODE_UNDEFINED) {
        setTileModeY(parseTileMode(tileModeY));
    }
    updateDrawableAttrs();
    updateBackgroundDrawableAttrs(true);
    if (mMutateBackground) {
        //noinspection deprecation
        super.setBackgroundDrawable(mBackgroundDrawable);
    }
    a.recycle();
}

上面主要是一些初始化的操作,最下面调用了RoundedImageView#updateDrawableAttrs():

private void updateAttrs(Drawable drawable, ScaleType scaleType) {
    Log.d(TAG, "updateAttrs(Drawable drawable, ScaleType scaleType)");
    if (drawable == null) {
        return;
    }
    if (drawable instanceof RoundedDrawable) {
// 初始化 RoundedDrawable
        ((RoundedDrawable) drawable)
                .setScaleType(scaleType)
                .setBorderWidth(mBorderWidth)
                .setBorderColor(mBorderColor)
                .setOval(mIsOval)
                .setTileModeX(mTileModeX)
                .setTileModeY(mTileModeY);
        if (mCornerRadii != null) {
            ((RoundedDrawable) drawable).setCornerRadius(
                    mCornerRadii[Corner.TOP_LEFT],
                    mCornerRadii[Corner.TOP_RIGHT],
                    mCornerRadii[Corner.BOTTOM_RIGHT],
                    mCornerRadii[Corner.BOTTOM_LEFT]);
        }
        applyColorMod();
    } else if (drawable instanceof LayerDrawable) {
        // loop through layers to and set drawable attrs
        LayerDrawable ld = ((LayerDrawable) drawable);
        for (int i = 0, layers = ld.getNumberOfLayers(); i < layers; i++) {
            updateAttrs(ld.getDrawable(i), scaleType);
        }
    }
}

如果在分析源码之前,不知道源码的运行过程,可以先使用log来打印每个方法的日志,然后根据日志来分析每个方法,当然这不是很好的方法,但是目前也没有精力去写一个aop的切面来打印日志;后面要是写了再补充吧!下面看下这个库的日志打印情况:

方法运行.png

自定义view的流程就再描述了,我们先看到RoundedImageView#drawableStateChanged()方法,该方法中调用了invalidate()方法,这个方法是view中的方法,追溯到源码中,最终会调用RoundedImageView中的onDraw()方法,因为它继承自ImageView,所以直接看到ImageView#onDraw()方法:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
// 如果当前的drawable为空则不绘制
    if (mDrawable == null) {
        return; // 无法解析URI
    }
    if (mDrawableWidth == 0 || mDrawableHeight == 0) {
        return;     // 没有东西可以绘制
    }
    if (mDrawMatrix == null && mPaddingTop == 0 && mPaddingLeft == 0) {
        mDrawable.draw(canvas);
    } else {
        final int saveCount = canvas.getSaveCount();
        canvas.save();
        if (mCropToPadding) {
            final int scrollX = mScrollX;
            final int scrollY = mScrollY;
            canvas.clipRect(scrollX + mPaddingLeft, scrollY + mPaddingTop,
                    scrollX + mRight - mLeft - mPaddingRight,
                    scrollY + mBottom - mTop - mPaddingBottom);
        }
        canvas.translate(mPaddingLeft, mPaddingTop);
        if (mDrawMatrix != null) {
            canvas.concat(mDrawMatrix);
        }
        /**
        * 调用到  Drawable 的draw 方法中
        */
        mDrawable.draw(canvas);
        canvas.restoreToCount(saveCount);
    }
}

可以看到在ImageView#onDraw() 方法中,后面会调用 Drawabledraw()方法,这也正好是我们看到的log中打印的方法,接下来我们分析RoundedDrawable#draw()方法:

@Override
public void draw(@NonNull Canvas canvas) {
    Log.d(TAG, "draw(@NonNull Canvas canvas)");
    if (mRebuildShader) {
        // 是否需要重新绘制阴影
        BitmapShader bitmapShader = new BitmapShader(mBitmap, mTileModeX, mTileModeY);
        if (mTileModeX == Shader.TileMode.CLAMP && mTileModeY == Shader.TileMode.CLAMP) {
            bitmapShader.setLocalMatrix(mShaderMatrix);
        }
        mBitmapPaint.setShader(bitmapShader);
        mRebuildShader = false;
    }
    if (mOval) {
        // 如果当前图片是圆形,则使用canvas绘制圆形
        if (mBorderWidth > 0) {
            canvas.drawOval(mDrawableRect, mBitmapPaint);
            canvas.drawOval(mBorderRect, mBorderPaint);
        } else {
            canvas.drawOval(mDrawableRect, mBitmapPaint);
        }
    } else {
        // 绘制每个角的圆角情况
        if (any(mCornersRounded)) {
            float radius = mCornerRadius;
            if (mBorderWidth > 0) {
                // 如果有边框
                canvas.drawRoundRect(mDrawableRect, radius, radius, mBitmapPaint);
                canvas.drawRoundRect(mBorderRect, radius, radius, mBorderPaint);
                redrawBitmapForSquareCorners(canvas);
                redrawBorderForSquareCorners(canvas);
            } else {
                // 没有边框直接绘制内容
                canvas.drawRoundRect(mDrawableRect, radius, radius, mBitmapPaint);
                redrawBitmapForSquareCorners(canvas);
            }
        } else {
            // 如果没有一个角有圆角,则直接绘制内容
            canvas.drawRect(mDrawableRect, mBitmapPaint);
            if (mBorderWidth > 0) {
                // 同上,绘制边框
                canvas.drawRect(mBorderRect, mBorderPaint);
            }
        }
    }
}

draw()方法中,先绘制shader,然后判断是否当前图片是否是圆形,如果是圆形则绘制内容和边框(如果边框宽度大于0),不是圆形则依次判断每个圆角是否有大小,有则绘制圆角内容和边框内容,但是我们看到在绘制的时候,不断的调用了isStateful()drawableStateChanged()方法,这是因为在绘制边框和圆角的时候,因为改变了内容和边框的颜色值,所以才会回调到isStateful()方法:

RoundedDrawable#draw().png

这里看到当调用到redrawBitmapForSquareCorners(Canvas canvas)方法:

private void redrawBitmapForSquareCorners(Canvas canvas) {
    Log.d(TAG, "redrawBitmapForSquareCorners(Canvas canvas)");
    if (all(mCornersRounded)) {
        // no square corners
        return;
    }
    if (mCornerRadius == 0) {
        return; // no round corners
    }
    float left = mDrawableRect.left;
    float top = mDrawableRect.top;
    float right = left + mDrawableRect.width();
    float bottom = top + mDrawableRect.height();
    float radius = mCornerRadius;
    if (!mCornersRounded[Corner.TOP_LEFT]) {
        mSquareCornersRect.set(left, top, left + radius, top + radius);
        canvas.drawRect(mSquareCornersRect, mBitmapPaint);
    }
    if (!mCornersRounded[Corner.TOP_RIGHT]) {
        mSquareCornersRect.set(right - radius, top, right, radius);
        canvas.drawRect(mSquareCornersRect, mBitmapPaint);
    }
    if (!mCornersRounded[Corner.BOTTOM_RIGHT]) {
        mSquareCornersRect.set(right - radius, bottom - radius, right, bottom);
        canvas.drawRect(mSquareCornersRect, mBitmapPaint);
    }
    if (!mCornersRounded[Corner.BOTTOM_LEFT]) {
        mSquareCornersRect.set(left, bottom - radius, left + radius, bottom);
        canvas.drawRect(mSquareCornersRect, mBitmapPaint);
    }
}

绘制完Drawable之后,接着就调用了RoundedImageView # setImageDrawable(Drawable drawable),这是因为ImageView # setImageDrawable()方法回调到子类的方法中,接着就是拿到图片资源,重复上面的流程调用,绘制内容和边框:

image.png

到此,RoundedImageView 就绘制到面板上了~

感谢:

[译]Android: 自定义 Drawable 教程

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

推荐阅读更多精彩内容