[译]Android:自定义Drawable教程

#这篇教程一共分为三个部分。

1 Drawable与View

Drawable是什么?API文档的定义:A Drawable is a general abstraction for "something that can be drawn."。就是说Drawable表示这类可以被绘制的事物。
那么,如何使用,怎么把它添加到View上?我们来一步一步回答这个问题。
现在,我们有个需求,给图片添加边框,效果如下,

border imageview

BorderDrawable

首先,我们创建一个Drawable的子类,并创建带参的构造器。

public class BorderDrawable extends Drawable {
    Paint mPaint;
    int mColor;
    int mBorderWidth;
    int mBorderRadius;
    RectF mRect;
    Path mPath;

    public BorderDrawable(int color, int borderWidth, int borderRadius) {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.FILL);

        mPath = new Path();
        mPath.setFillType(Path.FillType.EVEN_ODD);

        mRect = new RectF();

        mColor = color;
        mBorderWidth = borderWidth;
        mBorderRadius = borderRadius;
    }
}

onBoundsChange(Rect)的时候计算mPath;
draw(Canvas)绘制mPath。

@Override protected void onBoundsChange(Rect bounds) {
    mPath.reset();

    mPath.addRect(bounds.left, bounds.top, bounds.right, bounds.bottom, Path.Direction.CW);
    mRect.set(bounds.left + mBorderWidth, bounds.top + mBorderWidth, bounds.right - mBorderWidth, bounds.bottom - mBorderWidth);
    mPath.addRoundRect(mRect, mBorderRadius, mBorderRadius, Path.Direction.CW);
}

@Override public void draw(Canvas canvas) {
    mPaint.setColor(mColor);
    canvas.drawPath(mPath, mPaint);
}

@Override public void setAlpha(int alpha) {
    mPaint.setAlpha(alpha);
}

@Override public void setColorFilter(ColorFilter cf) {
    mPaint.setColorFilter(cf);
}

@Override public int getOpacity() {
    return PixelFormat.TRANSLUCENT;
}

BorderImageView

然后处理ImageView,

public class BorderImageView extends ImageView {

    BorderDrawable mBorder;

    public BorderImageView(Context context) {
        super(context);

        init(context, null, 0, 0);
    }

    public BorderImageView(Context context, AttributeSet attrs) {
        super(context, attrs);

        init(context, attrs, 0, 0);
    }

    //another constructors ...
    private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        setWillNotDraw(false);
        mBorder = new BorderDrawable(context.getResources().getColor(R.color.primary), getPaddingLeft(), getPaddingLeft() / 2);
    }

}

上面调用setWillNotDraw(false)传入了false,保证自定义View会执行onDraw(canvas),否则不会执行。然后把ImageView的padding 值当作border的宽度。
然后重写onSizeChanged(int, int, int, int),设置drawable的尺寸。

@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    mBorder.setBounds(0, 0, w, h);
}

然后在onDraw(canvas)里调用drawable的draw(Canvas)

Override protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    mBorder.draw(canvas);
}

最后,

<com.rey.tutorial.widget.BorderImageView
    android:layout_width="96dp"
    android:layout_height="96dp"
    android:src="@drawable/avatar"
    android:scaleType="centerCrop"
    android:padding="8dp"/>

其实,可以直接在ImageView 的onDraw(Canvas)里绘制边框,但是使用drawable便于复用。

2-创建带状态的drawable

现在,新需求来了,点击View边框颜色改变,效果如下,


state-based

StateBorderDrawable

我们来改写BorderDrawable。
首先,把int color参数改成ColorStateList 。

public class StateBorderDrawable extends Drawable {

    Paint mPaint;
    ColorStateList mColorStateList;
    int mColor;
    int mBorderWidth;
    int mBorderRadius;

    RectF mRect;
    Path mPath;

    public BorderDrawable(ColorStateList colorStateList, int borderWidth, int borderRadius) {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.FILL);

        mPath = new Path();
        mPath.setFillType(Path.FillType.EVEN_ODD);

        mRect = new RectF();

        mColorStateList = colorStateList;
        mColor = mColorStateList.getDefaultColor();
        mBorderWidth = borderWidth;
        mBorderRadius = borderRadius;
    }
}

isStateful()返回true,表明当view的状态改变的时候会通知这个Drawable。在onStateChange(int)方法里处理状态改变事件。

@Override
public boolean isStateful() {
    return true;
}

@Override
protected boolean onStateChange(int[] state) {
    int color = mColorStateList.getColorForState(state, mColor);
    if(mColor != color){
        mColor = color;
        invalidateSelf();
        return true;
    }

    return false;
}

如果当前drawable的颜色与view当前状态对应的颜色不一样,调用invalidateSelf()重新绘制。

StateBorderImageView

改写BorderImageView。
首先,init()方法:

private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
    setWillNotDraw(false);
    int[][] states = new int[][]{
            {-android.R.attr.state_pressed},
            {android.R.attr.state_pressed}
    };
    int[] colors = new int[]{
            context.getResources().getColor(R.color.primary),
            context.getResources().getColor(R.color.accent)
    };
    ColorStateList colorStateList = new ColorStateList(states, colors);

    mBorder = new StateBorderDrawable(colorStateList, getPaddingLeft(), getPaddingLeft() / 2);
    mBorder.setCallback(this);
}

Drawable对象必须调用setCallback(Callback),才保证Drawable状态invalidated的时候,回调ImageView的重绘,进而重绘Drawable。

@Override
protected void drawableStateChanged() {
    super.drawableStateChanged();
    mBorder.setState(getDrawableState());
}
@Override
protected boolean verifyDrawable(Drawable dr) {
    return super.verifyDrawable(dr) || dr == mBorder;
}

drawableStateChanged()当view状态改变,这个方法去通知drawable。
verifyDrawable()当drawable请求view重绘自己时(重绘是通过Callback的invalidateDrawable(Drawable)方法),view会先检查这个drawable是不是属于自己。

最后,

<com.rey.tutorial.widget.StateBorderImageView
    android:layout_width="96dp"
    android:layout_height="96dp"
    android:src="@drawable/avatar"
    android:scaleType="centerCrop"
    android:padding="8dp"/>

3-创建带动画的drawable

现在,新需求,逃不过的动画,


animated drawable

AnimatedStateBorderDrawable

来改写StateBorderDrawable。
首先,我们需要一个duration参数。

public class AnimatedStateBorderDrawable extends Drawable {

    private boolean mRunning = false;
    private long mStartTime;
    private int mAnimDuration;

    Paint mPaint;
    ColorStateList mColorStateList;
    int mPrevColor;
    int mMiddleColor;
    int mCurColor;
    int mBorderWidth;
    int mBorderRadius;

    RectF mRect;
    Path mPath;

    public BorderDrawable(ColorStateList colorStateList, int borderWidth, int borderRadius, int duration) {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.FILL);

        mPath = new Path();
        mPath.setFillType(Path.FillType.EVEN_ODD);

        mRect = new RectF();

        mColorStateList = colorStateList;
        mCurColor = mColorStateList.getDefaultColor();
        mPrevColor = mCurColor;
        mBorderWidth = borderWidth;
        mBorderRadius = borderRadius;
        mAnimDuration = duration;
    }
}

添加了一些新变量,比如mPrevColor,mCurColor,mMiddeColor。需要知道之前和当前状态的颜色值,才能在两个状态之间添加颜色动画。变量mRunning,mStartTime为了记录动画数据。
然后实现android.graphics.drawable.Animatable接口,重写3个方法。

@Override
public boolean isRunning() {
    return mRunning;
}

@Override
public void start() {
    resetAnimation();
    scheduleSelf(mUpdater, SystemClock.uptimeMillis() + FRAME_DURATION);
    invalidateSelf();
}

@Override
public void stop() {
    mRunning = false;
    unscheduleSelf(mUpdater);
    invalidateSelf();
}

调用start()开始动画。3行代码干3件事。
先重置动画数据,mStartTime记录动画开始时间,mMiddleColor记录动画执行过程中绘制的颜色。
然后scheduleSelf ()方法,将在指定的时间执行第一个参数Runnable。
invalidateSelf()使Drawable状态invalidated,这会通知Callback。

private void resetAnimation(){
    mStartTime = SystemClock.uptimeMillis();
    mMiddleColor = mPrevColor;
}

private final Runnable mUpdater = new Runnable() {

    @Override
    public void run() {
        update();
    }

};

private void update(){
    long curTime = SystemClock.uptimeMillis();
    float progress = Math.min(1f, (float) (curTime - mStartTime) / mAnimDuration);
    mMiddleColor = getMiddleColor(mPrevColor, mCurColor, progress);

    if(progress == 1f)
        mRunning = false;

    if(isRunning())
        scheduleSelf(mUpdater, SystemClock.uptimeMillis() + FRAME_DURATION);

    invalidateSelf();
}

update()方法,通过动画进度和两个状态颜色值计算mMiddleColor,然后根据动画是否执行完毕来决定是否继续安排任务mUpdater

@Override
protected boolean onStateChange(int[] state) {
    int color = mColorStateList.getColorForState(state, mCurColor);

    if(mCurColor != color){
        if(mAnimDuration > 0){
            mPrevColor = isRunning() ? mMiddleColor : mCurColor;
            mCurColor = color;
            start();
        }
        else{
            mPrevColor = color;
            mCurColor = color;
            invalidateSelf();
        }
         return true;
    }

    return false;
}

@Override
public void draw(Canvas canvas) {
    mPaint.setColor(isRunning() ? mMiddleColor : mCurColor);
    canvas.drawPath(mPath, mPaint);
}

@Override
public void jumpToCurrentState() {
    super.jumpToCurrentState();
    stop();
}

@Override
public void scheduleSelf(Runnable what, long when) {
    mRunning = true;
    super.scheduleSelf(what, when);
}

当view想让drawable无动画直接转变状态时,jumpToCurrentState()会被调用,所以我们stop()动画。

AnimatedStateBorderImageView

改写StateBorderImageView.

mBorder = new AnimatedStateBorderDrawable(colorStateList, 
        getPaddingLeft(), 
        getPaddingLeft() / 2, 
        context.getResources().getInteger(android.R.integer.config_mediumAnimTime));

@Override
public void jumpDrawablesToCurrentState() {
    super.jumpDrawablesToCurrentState();
    mBorder.jumpToCurrentState();
}

jumpDrawablesToCurrentState()通知drawable状态改变。

<com.rey.tutorial.widget.AnimatedStateBorderImageView
    android:layout_width="96dp"
    android:layout_height="96dp"
    android:src="@drawable/avatar"
    android:scaleType="centerCrop"
    android:padding="8dp"/>

相关代码
https://github.com/rey5137/tutorials/tree/add_drawable_to_view
https://github.com/YoungPeanut/ApiDemos/blob/3bd9112f79bfa8a7ee006293913f947d6888514c/app/src/main/java/com/example/android/graphics/CircleDrawable.java

英文博客原文
https://medium.com/@rey5137/custom-drawable-part-3-b7adfd97d0b3

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

推荐阅读更多精彩内容