引言
前面文章我們學了Canvas、Paint等繪製知識瞭解繪製流程的基本要素以及核心流程,事實上繪製不僅僅只是在Canvas上進行,Android 因此還抽象了在Canvas基礎上的Drawable,那麼Drawable是一個Bitmap麼?
相關文章鏈接如下:
- Android進階——高級UI必知必會之2D繪製與Paint的基礎應用(一)
- Android進階——高級UI必知必會之2D繪製與使用Paint對圖形進行渲染和濾鏡混合處理(二)
- Android進階——高級UI必知必會之Android座標系與Canvas小結(三)
- Android 進階——高級UI必知必會之統一可繪製概念Drawable詳解(四)
一、Drawable概述
Drawable是一個抽象的概念,表示一個可繪製的對象,可以通過Resource類的getDrawable(int id,int theme)獲取對應的Drawable對象,可以在Canvas上進行繪製的頂級抽象概念。在Android中Drawable可能是一張位圖(BitmapDrawable),可能是一個圖形(ShapeDrawable),也可能是一個圖層(LayerDrawable)等等,正如源碼顯示Drawable是一個抽象類,通常在開發中不直接使用,往往都是使用它的派生類或者自定義的Drawable子類,Android中已經實現了派生類有:ClipDrawable, ColorDrawable, DrawableContainer, GradientDrawable, InsetDrawable, LayerDrawable, NinePatchDrawable, PictureDrawable, RotateDrawable, ScaleDrawable, ShapeDrawable,AnimationDrawable, LevelListDrawable, PaintDrawable, StateListDrawable, TransitionDrawable,每一種子類就是Drawable在Android裏的體現形式。我們根據畫圖的需求,創建相應的可繪製對象(Drawable子類),就可以將這個可繪製對象當作一塊“畫布”,在其上面操作可繪製對象,並最終將這種可繪製對象顯示在畫布上(Drawable#draw(Canvas canvas)),也有點類似於“內存畫布“,相當於是把Drawable繪製到屏幕上。
二、Drawable系設計思想淺析
從源碼角度上分析Drawable 只是提供了一個統一的頂層概念以及一些共有的操作API,並不負責任何涉及到繪製的具體工作,針對不同類型的Drawable交由對應的子類去重寫draw方法去真正實現繪製工作,其實與View系的設計有點類似(把draw的操作延遲到子類),在進行繪製時在子View的onDraw方法中被調用。不過Drawable並不屬於View,所以不能接收任何事件,自然也不能直接與用戶交互,爲了與當前正在繪製的內容進行交互Drawable 定義了一些通用機制,除了Callback還有setLevel方法等(通過setLevel方法改變mLevel值來實現動態回調onLevelChanged),當然還有緩存機制,當你新生成一個Drawable的時候,就會將Drawable的ConstantState從Drawable中取出,然後放入你Cache池中。
public abstract class Drawable {
...
private static final Rect ZERO_BOUNDS_RECT = new Rect();
static final PorterDuff.Mode DEFAULT_TINT_MODE = PorterDuff.Mode.SRC_IN;
private int[] mStateSet = StateSet.WILD_CARD;
private int mLevel = 0;
private Rect mBounds = ZERO_BOUNDS_RECT; // lazily becomes a new Rect()
private WeakReference<Callback> mCallback = null;
//Drawable的繪製效果的核心方法,在setBounds方法指定的矩形區域內進行繪製
public abstract void draw(@NonNull Canvas canvas);
/**
* 用於指定Drawable實例繪製的位置和大小。所有的Drawable實例都會生成請求的尺寸,爲當前Drawable實例設置一個矩形範圍,
* 在draw方法調用時候,Drawable實例將被繪製到這個矩形範圍內。
*/
public void setBounds(int left, int top, int right, int bottom) {
Rect oldBounds = mBounds;
if (oldBounds == ZERO_BOUNDS_RECT) {
oldBounds = mBounds = new Rect();
}
if (oldBounds.left != left || oldBounds.top != top ||
oldBounds.right != right || oldBounds.bottom != bottom) {
if (!oldBounds.isEmpty()) {
// first invalidate the previous bounds
invalidateSelf();
}
mBounds.set(left, top, right, bottom);
onBoundsChange(mBounds);
}
}
public void setBounds(@NonNull Rect bounds) {
setBounds(bounds.left, bounds.top, bounds.right, bounds.bottom);
}
//將當前Drawable實例通過setBounds設置的繪製範圍拷貝到客戶端提供的Rect實例中返回
public final void copyBounds(@NonNull Rect bounds) {
bounds.set(mBounds);
}
public final Rect copyBounds() {
return new Rect(mBounds);
}
/**
*返回當前Drawable實例的矩形繪製範圍,所以如果是需要一個拷貝的矩形範圍,
*應該調用copyBounds來代替,而且調用getBounds時不能修改返回的矩形,因爲這會影響Drawable實例。
*/
@NonNull
public final Rect getBounds() {
if (mBounds == ZERO_BOUNDS_RECT) {
mBounds = new Rect();
}
return mBounds;
}
/**
*當設置爲true,則該Drawable實例在縮放或者旋轉時候將對它關聯的bitmap進行濾波過濾。可以提升旋轉時的繪製效果。
*如果該Drawable實例未使用bitmap,這個方法無作用。
*/
public void setFilterBitmap(boolean filter) {}
public boolean isFilterBitmap() {
return false;
}
//一個回調接口,用於調度和執行Drawable實例的動畫。比如實現自定義的動畫Drawable時就需要實現這個接口。
public interface Callback {
//Drawable實例被重繪時候調用。在當前Drawable實例位置的View實例需要重繪,或者至少部分重繪。
void invalidateDrawable(@NonNull Drawable who);
//一個Drawable實例可以調用這個方法預先安排動畫的下一幀,也可以通過Handler.postAtTime實現。
void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when);
//一個Drawable實例可以調用這個方法取消之前安排的某一幀。
也可以通過Handler.removeCallbacks實現。
void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what);
}
public void invalidateSelf() {
final Callback callback = getCallback();
if (callback != null) {
callback.invalidateDrawable(this);
}
}
public void scheduleSelf(@NonNull Runnable what, long when) {
final Callback callback = getCallback();
if (callback != null) {
callback.scheduleDrawable(this, what, when);
}
}
public void unscheduleSelf(@NonNull Runnable what) {
final Callback callback = getCallback();
if (callback != null) {
callback.unscheduleDrawable(this, what);
}
}
//獲取當前Drawable實例的佈局方向。
public @View.ResolvedLayoutDir int getLayoutDirection() {
return mLayoutDirection;
}
//設置當前Drawable實例的佈局方向。
public final boolean setLayoutDirection(@View.ResolvedLayoutDir int layoutDirection) {
if (mLayoutDirection != layoutDirection) {
//如果當前Drawable佈局方向和layoutDirection不一致,
//則修改佈局方向爲layoutDirection,然後執行onLayoutDirectionChanged
mLayoutDirection = layoutDirection;
return onLayoutDirectionChanged(layoutDirection);
}
return false;
}
//當調用setLayoutDirection方法,Drawable佈局方向發生變化後調用
public boolean onLayoutDirectionChanged(@View.ResolvedLayoutDir int layoutDirection) {
return false;
}
// 設置Drawable實例的透明度。(0:完全透明;255:完全不透明)
public abstract void setAlpha(@IntRange(from=0,to=255) int alpha);
/**
*爲當前Drawable實例設置顏色濾鏡
*/
public abstract void setColorFilter(@Nullable ColorFilter colorFilter);
/**
*爲當前Drawable實例設置濾鏡效果
*/
public void setColorFilter(@ColorInt int color, @NonNull PorterDuff.Mode mode) {
setColorFilter(new PorterDuffColorFilter(color, mode));
}
//爲當前Drawable實例着色
public void setTint(@ColorInt int tintColor) {
setTintList(ColorStateList.valueOf(tintColor));
}
//根據ColorStateList對當前Drawable實例進行着色,空方法是交由子類去實現的
public void setTintList(@Nullable ColorStateList tint) {}
// 設置當前Drawable實例着色的混合過濾模式
public void setTintMode(@NonNull PorterDuff.Mode tintMode) {}
//設置當前Drawable實例熱點區域的中心點座標
public void setHotspot(float x, float y) {}
//爲當前Drawable實例設置一個狀態值集合。當現有狀態和stateSet不同時候,觸發onStateChange(stateSet)方法。
public boolean setState(@NonNull final int[] stateSet) {
if (!Arrays.equals(mStateSet, stateSet)) {
mStateSet = stateSet;
return onStateChange(stateSet);
}
return false;
}
...
/**
*將當前Drawable實例的padding值作爲參數設置爲Rect實例padding
*的邊界值。如果當前實例有padding值,返回true,否則返回false;
*當返回false,則Recti實例padding的邊界值都設置爲0;
*/
public boolean getPadding(@NonNull Rect padding) {
padding.set(0, 0, 0, 0);
return false;
}
//僅僅是一個標記值的作用,每調用一次就改變mLevel的值,兩次值不一樣時就會觸發onLevelChange回調
public final boolean setLevel(@IntRange(from=0,to=10000) int level) {
if (mLevel != level) {
mLevel = level;
return onLevelChange(level);
}
return false;
}
//當通過調用{@link #setLevel}值改變mLevel值時就會觸發這個回調方法
protected boolean onLevelChange(int level) {
return false;
}
...
}
當需要使用ImageView繪製Bitmap時就會調用到BitmapDrawable中的draw方法,把要繪製的Bitmap繪製到Canvas上,而Drawable相當於是起到了一個“容器工具”的作用,把不同類型的圖形、圖像的繪製統一起來。
@Override
public void draw(Canvas canvas) {
final Bitmap bitmap = mBitmapState.mBitmap;
if (bitmap == null) {
return;
}
final BitmapState state = mBitmapState;
final Paint paint = state.mPaint;
if (state.mRebuildShader) {
final Shader.TileMode tmx = state.mTileModeX;
final Shader.TileMode tmy = state.mTileModeY;
if (tmx == null && tmy == null) {
paint.setShader(null);
} else {
paint.setShader(new BitmapShader(bitmap,
tmx == null ? Shader.TileMode.CLAMP : tmx,
tmy == null ? Shader.TileMode.CLAMP : tmy));
}
state.mRebuildShader = false;
}
final int restoreAlpha;
if (state.mBaseAlpha != 1.0f) {
final Paint p = getPaint();
restoreAlpha = p.getAlpha();
p.setAlpha((int) (restoreAlpha * state.mBaseAlpha + 0.5f));
} else {
restoreAlpha = -1;
}
final boolean clearColorFilter;
if (mTintFilter != null && paint.getColorFilter() == null) {
paint.setColorFilter(mTintFilter);
clearColorFilter = true;
} else {
clearColorFilter = false;
}
updateDstRectAndInsetsIfDirty();
final Shader shader = paint.getShader();
final boolean needMirroring = needMirroring();
if (shader == null) {
if (needMirroring) {
canvas.save();
// Mirror the bitmap
canvas.translate(mDstRect.right - mDstRect.left, 0);
canvas.scale(-1.0f, 1.0f);
}
canvas.drawBitmap(bitmap, null, mDstRect, paint);
if (needMirroring) {
canvas.restore();
}
} else {
updateShaderMatrix(bitmap, paint, shader, needMirroring);
canvas.drawRect(mDstRect, paint);
}
if (clearColorFilter) {
paint.setColorFilter(null);
}
if (restoreAlpha >= 0) {
paint.setAlpha(restoreAlpha);
}
}
三、自定義Drawable的簡單實例
繼承Drawable實現自定義比例的摳圖效果。
package com.dn_alan.myapplication;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.view.Gravity;
/**
* @author cmo
*/
public class MoClipDrawable extends Drawable {
private final Rect mTmpRect = new Rect();
private Drawable srcDrawable;
public MoClipDrawable(Drawable src) {
srcDrawable = src;
}
@Override
public void draw(Canvas canvas){
//得到當前自身Drawable的矩形區域
Rect bounds=getBounds();
Rect targetRect=new Rect();
int w = bounds.width();
int h=bounds.height()-2;
int ratio=-1;
int gravity = ratio < 0 ? Gravity.LEFT : Gravity.RIGHT;
//從一個已有的bounds矩形邊界範圍中摳出一個矩形r
Gravity.apply(
gravity,//從左邊還是右邊開始摳
w/2,//目標矩形的寬
h, //目標矩形的高
bounds, //被摳出來的rect
targetRect);//目標rect
canvas.save();
canvas.clipRect(targetRect);//切割
srcDrawable.draw(canvas);//畫
canvas.restore();//恢復之前保存的畫布
}
@Override
protected void onBoundsChange(Rect bounds) {
// 定好Drawable圖片的寬高---邊界bounds
srcDrawable.setBounds(bounds);
}
@Override
public int getIntrinsicWidth() {
//得到Drawable的實際寬度
return srcDrawable.getIntrinsicWidth();
}
@Override
public int getIntrinsicHeight() {
//得到Drawable的實際高度
return srcDrawable.getIntrinsicHeight();
}
@Override
protected boolean onLevelChange(int level) {
// 當設置level的時候回調---提醒自己重新繪製
invalidateSelf();
return true;
}
@Override
public void setAlpha(int i) {
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
}
@Override
public int getOpacity() {
return PixelFormat.UNKNOWN;
}
}
Drawable不能單獨使用必須要配置到View上纔有效果
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.widget.ImageView;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
private ImageView iv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
iv=findViewById(R.id.iv_love_left);
///獲取Drawable實例
MoClipDrawable drawable=new MoClipDrawable(getResources().getDrawable(R.mipmap.mn));
///把Drawable設置到View上
iv.setImageDrawable(drawable);
}
}