Drawable对于Android开发工程师来说非常熟悉,最常用的用法是在drawable目录里放入png或其他格式的图片,然后在代码里就可以用resources访问到如:
// 访问test图片资源
getResources().getDrawable(R.drwable.test);
这里不是要讲Drawable资源怎么使用,而是来看一下这个类实现的一些原理以及它相关的一些子类的实现原理。
Drawable类在SDK中是这么描述的:
A Drawable is a general abstraction for “something that can be drawn.” Most
often you will deal with Drawable as the type of resource retrieved for
drawing things to the screen; the Drawable class provides a generic API for
dealing with an underlying visual resource that may take a variety of forms.
Unlike a {@link android.view.View}, a Drawable does not have any facility to
receive events or otherwise interact with the user.
In addition to simple drawing, Drawable provides a number of generic
mechanisms for its client to interact with what is being drawn:
SDK中描述得比较清楚,Drawable是一个可以被画的抽象类,它仅仅是处理可以画的东西,它不像View,它没有接收事件等跟用户交互的机制。实际上,我们通常看到的控件中的视觉实现就是由Drawable来实现的,最常见的便是在控件中使用bitmap资源。
首先介绍Drawable和View之间的关系:
一、Drawable和View的关系
说到这个,得了解到Android控件的绘制过程,Android控件的绘制起点是ViewRootImpl的performTraversals:
private void performTraversals() {
// ......
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// ......
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
// ......
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
// ......
mView.draw(canvas);
// ......
}
关于ViewRootImpl怎么创建的,可以去了解下Activity的启动过程,上面的代码mView是就是 Activity顶层的View,在绘制前会先测量控件大小和计算控件位置,接着就调用View的draw方法:
/**
* Manually render this view (and all of its children) to the given Canvas.
* The view must have already done a full layout before this function is
* called. When implementing a view, implement
* {@link #onDraw(android.graphics.Canvas)} instead of overriding this method.
* If you do need to override this method, call the superclass version.
*
* @param canvas The Canvas to which the View is rendered.
*/
@CallSuper
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
// ......
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
final Paint p = scrollabilityCache.paint;
final Matrix matrix = scrollabilityCache.matrix;
final Shader fade = scrollabilityCache.shader;
// ......
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
}
控件的draw分为几个步骤,具体可以看源码的注释,这其中的步骤3会调用onDraw方法,这就是一般在自定义View的时候要实现的方法,可以看到在绘制过程最终是要将内容绘制到Canvas上,我们再来看跟Drawable相关的方法之一,drawBackground:
/**
* Draws the background onto the specified canvas.
*
* @param canvas Canvas on which to draw the background
*/
private void drawBackground(Canvas canvas) {
final Drawable background = mBackground;
if (background == null) {
return;
}
// ......
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
这里可以看到在绘制背景的时候是调用drawable的draw方法,drawable的draw方法是一个抽象方法,具体的实现由它的子类来实现,可以看一下最常用的BitmapDrawable类的实现:
@Override
public void draw(Canvas canvas) {
final Bitmap bitmap = mBitmapState.mBitmap;
// ......
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);
}
// ......
}
可以看到最终是调用了canvas的drawBitmap方法。
小结一下:
Drawable是抽象了绘制逻辑的类,它在View中充当绘制的功能,跟绘制有关的逻辑都可以放到drawable中来实现,而最终的绘制都是通过canvas来处理的。
二、Drawable有哪些子类
上面分析了Drawable类与View之间的关系和绘制实现原理,因为Drawable是一个抽象类,下面我们来看一下Drawable有哪些子类,用好Drawable其实可以非常灵活地实现一些特别的效果。
Drawable在android.graphic包下类,打开这个包就可以看到它下面的所有的子类,子类有20多种,由于篇幅关系不一一介绍,有很多子类可能很多人并没有在代码中真实使用过,但是其实已经不知不觉的用了很久,比如.9 png是NinePatchDrawable,有按下状态的是StateListDrawable等等,因为这些子类基本上都跟drawable中放的xml文件对应起来了,可以看一下Resources的getDrawable方法,它会调loadDrawable和loadDrawableForCookie(API:28),最终会调到DrawableInfator的inflateFromTag:
// Resources.java
@Nullable
Drawable loadDrawable(@NonNull Resources wrapper, @NonNull TypedValue value, int id,
int density, @Nullable Resources.Theme theme)
throws NotFoundException {
// .......
final Drawable.ConstantState cs;
// ......
Drawable dr;
boolean needsNewDrawableAfterCache = false;
if (cs != null) {
dr = cs.newDrawable(wrapper);
} else if (isColorDrawable) {
dr = new ColorDrawable(value.data);
} else {
dr = loadDrawableForCookie(wrapper, value, id, density);
}
// .......
}
/**
* Loads a drawable from XML or resources stream.
*
* @return Drawable, or null if Drawable cannot be decoded.
*/
@Nullable
private Drawable loadDrawableForCookie(@NonNull Resources wrapper, @NonNull TypedValue value,
try {
// ......
try {
if (file.endsWith(".xml")) {
final XmlResourceParser rp = loadXmlResourceParser(
file, id, value.assetCookie, "drawable");
dr = Drawable.createFromXmlForDensity(wrapper, rp, density, null);
rp.close();
} else {
final InputStream is = mAssets.openNonAsset(
value.assetCookie, file, AssetManager.ACCESS_STREAMING);
AssetInputStream ais = (AssetInputStream) is;
dr = decodeImageDrawable(ais, wrapper, value);
}
} finally {
stack.pop();
}
} catch (Exception | StackOverflowError e) {
// ......
throw rnf;
}
return dr;
}
// DrawableInflater.java
@NonNull
@SuppressWarnings("deprecation")
private Drawable inflateFromTag(@NonNull String name) {
switch (name) {
case "selector":
return new StateListDrawable();
case "animated-selector":
return new AnimatedStateListDrawable();
case "level-list":
return new LevelListDrawable();
case "layer-list":
return new LayerDrawable();
case "transition":
return new TransitionDrawable();
case "ripple":
return new RippleDrawable();
case "adaptive-icon":
return new AdaptiveIconDrawable();
case "color":
return new ColorDrawable();
case "shape":
return new GradientDrawable();
case "vector":
return new VectorDrawable();
case "animated-vector":
return new AnimatedVectorDrawable();
case "scale":
return new ScaleDrawable();
case "clip":
return new ClipDrawable();
case "rotate":
return new RotateDrawable();
case "animated-rotate":
return new AnimatedRotateDrawable();
case "animation-list":
return new AnimationDrawable();
case "inset":
return new InsetDrawable();
case "bitmap":
return new BitmapDrawable();
case "nine-patch":
return new NinePatchDrawable();
case "animated-image":
return new AnimatedImageDrawable();
default:
return null;
}
}
可以看到,当你调用resouces.getDrawable的时候,实际上是解析了对应的xml资源并实例化对应的Drawable对象的。
三、Drawable资源复用设计
不知道你有没留意到loadDrawable方法里有个dr = cs.newDrawable(wrapper);cs是Drawable.ConstantState对象,这个是什么呢,这里就要介绍一下Drawable资源复用的设计了:
大家都知道手机的内存资源是很宝贵的,在很多场景下都要避免资源的重复创建,比如有两个ImageView想显示同一个图片资源,如果两个ImageView都decode一下生成两个bitmap,这肯定是浪费的,在我们常用的一些图片加载库时通常是使用内存缓存一保存bitmap对象来防止内存浪费,但在没有使用图片加载库的时候比如通常我们用getResources获得drawable或者就在layout里指定drawable资源作为background时,这两个ImageView使用的bitmap其实是同一份,这样就避免了浪费,这是如何实现的呢,这就是ConstantState来做的,Drawable里有个ConstantState内部类,定义如下:
/**
* This abstract class is used by {@link Drawable}s to store shared constant state and data
* between Drawables. {@link BitmapDrawable}s created from the same resource will for instance
* share a unique bitmap stored in their ConstantState.
*
*/
public static abstract class ConstantState {
public abstract @NonNull Drawable newDrawable();
/**
* Creates a new Drawable instance from its constant state using the
* specified resources. This method should be implemented for drawables
* that have density-dependent properties.
* <p>
* The default implementation for this method calls through to
* {@link #newDrawable()}.
*
* @param res the resources of the context in which the drawable will
* be displayed
* @return a new drawable object based on this constant state
*/
public @NonNull Drawable newDrawable(@Nullable Resources res) {
return newDrawable();
}
public @NonNull Drawable newDrawable(@Nullable Resources res,
@Nullable @SuppressWarnings("unused") Theme theme) {
return newDrawable(res);
}
public abstract @Config int getChangingConfigurations();
public boolean canApplyTheme() {
return false;
}
}
这个类是用来在Drawable之间共享资源的,比如刚才举的两个ImageView的例子,他们使用background的Drawable对象是不同的,但他们的内部资源是一样的,在要用同一份资源来用在不同地方的时候,会使用ConstantState的newDrawable方法来创建Drawable对象,这也就是Resources.loadDrawble那段用newDrawable创建Drawable的代码,这样就达到了不同地方使用的Drawable但内享的资源是一样的,达到了资源复用的目的。这个类是一个抽象类,不同的Drawable子类实现对应的方法,比如BitmapDrawable共享的是bitmap而ColorDrawable共享的是color,NinePatchDrawable共享的是NinePatchState,里面保存了NinePatch对象表达.9png的配置信息,其他的Drawable则共享的是他们自己的状态。这里你可能会有个问题是:Drawable共享了这些状态,如果修复了这个状态就会修改所有的地方?是的,但如果想达到不同地方有不同的显示效果比如bitmap有的想加个透明度有的不想加,那么就要调用Drawable的mutate方法来重新生成状态并创建并的Drawable来达到目的。
总结
Drawable类是一个非常用的类,它使绘制逻辑和View实现了解耦,而ConstantState的设计实现的资源的共享,它实现原理值得我们深入学习和研究。用好了Drawable有时能在控件的绘制上做到事半功倍的效果。