文章大纲
- 引言
- 一、Drawable概述
- 二、Drawable系设计思想浅析
- 三、自定义Drawable的简单实例
引言
前面文章我们学了Canvas、Paint等绘制知识了解绘制流程的基本要素以及核心流程,事实上绘制不仅仅只是在Canvas上进行,Android 因此还抽象了在Canvas基础上的Drawable,那么Drawable是一个Bitmap么?
一、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);
}
}