[譯]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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容