#這篇教程一共分為三個部分。
1 Drawable與View
Drawable是什么?API文檔的定義:A Drawable is a general abstraction for "something that can be drawn."。就是說Drawable表示這類可以被繪制的事物。
那么,如何使用,怎么把它添加到View上?我們來一步一步回答這個問題。
現在,我們有個需求,給圖片添加邊框,效果如下,
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邊框顏色改變,效果如下,
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
現在,新需求,逃不過的動畫,
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