一、引言
对于Drawable,相比每个Android 开发者都无比熟悉,在开发过程中我们经常setBackground设置背景,那么对于Drawable你了解多少呢?对于View是怎样把Drawable绘制出来又了解多少呢?对View根据不同状态绘制不同的背景又了解多少呢?也就是我们经常使用的selector,今天我们从源码上来深度剖析这些原理,从本质上卸下Drawable的神秘面纱。
二、背景介绍
在源码路径:frameworks/base/graphics/java/android/graphics/drawable/Drawable.java
进入Drawable.java里面去看源码,你会看到这样一段描述文字:
A Drawable is a general abstraction for "something that can be drawn." Most
1. often you will deal with Drawable as the type of resource retrieved for
2. drawing things to the screen; the Drawable class provides a generic API for
3. dealing with an underlying visual resource that may take a variety of forms.
4. Unlike a {@link android.view.View}, a Drawable does not have any facility to
5. receive events or otherwise interact with the user.
也就是说Drawable是Android开发中的通用可绘制对象,View类默认针对Drawable进行一些必要的绘制,如背景。
三、Drawable的核心分析
1、先来介绍背景选择器selector
对Android开发有经验的同学,对 节点的使用一定很熟悉,该节点的作用就是定义一组状态资源图片,使其能够在不同的状态下更换某个View的背景图片。比如demo_selector.xml
<?xml version="1.0" encoding="utf-8" ?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 触摸时并且当前窗口处于交互状态 -->
<item android:state_pressed="true" android:state_window_focused="true" android:drawable= "@drawable/pic1" />
<!-- 触摸时并且没有获得焦点状态 -->
<item android:state_pressed="true" android:state_focused="false" android:drawable="@drawable/pic2" />
<!--选中时的图片背景-->
<item android:state_selected="true" android:drawable="@drawable/pic3" />
<!--获得焦点时的图片背景-->
<item android:state_focused="true" android:drawable="@drawable/pic4" />
<!-- 窗口没有处于交互时的背景图片 -->
<item android:drawable="@drawable/pic5" />
</selector>
其实这个xml文件会被Android框架解析成StateListDrawable类对象。通常我们在XML文件中设置View的background如下:
android:background="@drawable/drawable_bg"/*设置其他Drawable背景*/
android:background="#ff124a54"/*设置颜色背景*/
对于background我们应该明确一下几点知识:android:background属性值在实际操作中都会实例化为Drawable,常用的属性值与Drawable对象关系如下:
- “#ffffffff”=>ColorDrawable
- @drawable/shape_bg”=>GradientDrawable(假设shape_bg.xml文件为顶层为shape标签)
- “@drawable/bmp_bg”=>BitmapDrawable(假设bmp_bg为.png/.jpg/.bmp文件)
- “@drawable/9patch_bg”=>NinePatchDrawable(假设9patch_bg为.9.png文件)
2、接下来分析Drawable的ConstantState
在用代码生成Drawable的时候,相信你会经常看看ConstantState的身影,那么ConstantState到底是什么鬼呢?下面我们来揭开它的神秘面纱。
如果把Drawable比作一个绘制容器,那么ConstantState就是容器中真正的内容,Drawable的源码如下:
/**
* 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.
*
* <p>
* {@link #newDrawable(Resources)} can be used as a factory to create new Drawable instances
* from this ConstantState.
* </p>
*
* Use {@link Drawable#getConstantState()} to retrieve the ConstantState of a Drawable. Calling
* {@link Drawable#mutate()} on a Drawable should typically create a new ConstantState for that
* Drawable.
*/
public static abstract class ConstantState {
/**
* Create a new drawable without supplying resources the caller
* is running in. Note that using this means the density-dependent
* drawables (like bitmaps) will not be able to update their target
* density correctly. One should use {@link #newDrawable(Resources)}
* instead to provide a resource.
*/
public abstract Drawable newDrawable();
/**
* Create a new Drawable instance from its constant state. This
* must be implemented for drawables that change based on the target
* density of their caller (that is depending on whether it is
* in compatibility mode).
*/
public Drawable newDrawable(Resources res) {
return newDrawable();
}
/**
* Create a new Drawable instance from its constant state. This must be
* implemented for drawables that can have a theme applied.
*/
public Drawable newDrawable(Resources res, Theme theme) {
return newDrawable(null);
}
/**
* Return a bit mask of configuration changes that will impact
* this drawable (and thus require completely reloading it).
*/
public abstract int getChangingConfigurations();
/**
* @return Total pixel count
* @hide
*/
public int addAtlasableBitmaps(Collection<Bitmap> atlasList) {
return 0;
}
/** @hide */
protected final boolean isAtlasable(Bitmap bitmap) {
return bitmap != null && bitmap.getConfig() == Bitmap.Config.ARGB_8888;
}
/**
* Return whether this constant state can have a theme applied.
*/
public boolean canApplyTheme() {
return false;
}
}
/**
* Return a {@link ConstantState} instance that holds the shared state of this Drawable.
*
* @return The ConstantState associated to that Drawable.
* @see ConstantState
* @see Drawable#mutate()
*/
public ConstantState getConstantState() {
return null;
}
可以看出,ConstantState其实是Drawable中的静态抽象类,并且getConstantState()方法默认返回null,这我们可以推测ConstantState类和getConstantState()方法会被具体的Drawable实现类继承和重写。这样不难想象,由于不同Drawbale如BitmapDrawable、GradientDrawable这些自身存储的绘制内容数据原则上是不一样的,这就意味着Drawable为了易于扩展,ConstantState对象应该会存储属性可变的数据。
参考StateListDrawable的源码,确实是有DrawableContainer继承ConstantState的身影。
static class StateListState extends DrawableContainerState {
int[] mThemeAttrs;
int[][] mStateSets;
StateListState(StateListState orig, StateListDrawable owner, Resources res) {
super(orig, owner, res);
if (orig != null) {
// Perform a shallow copy and rely on mutate() to deep-copy.
mThemeAttrs = orig.mThemeAttrs;
mStateSets = orig.mStateSets;
} else {
mThemeAttrs = null;
mStateSets = new int[getCapacity()][];
}
}
void mutate() {
mThemeAttrs = mThemeAttrs != null ? mThemeAttrs.clone() : null;
final int[][] stateSets = new int[mStateSets.length][];
for (int i = mStateSets.length - 1; i >= 0; i--) {
stateSets[i] = mStateSets[i] != null ? mStateSets[i].clone() : null;
}
mStateSets = stateSets;
}
int addStateSet(int[] stateSet, Drawable drawable) {
final int pos = addChild(drawable);
mStateSets[pos] = stateSet;
return pos;
}
int indexOfStateSet(int[] stateSet) {
final int[][] stateSets = mStateSets;
final int N = getChildCount();
for (int i = 0; i < N; i++) {
if (StateSet.stateSetMatches(stateSets[i], stateSet)) {
return i;
}
}
return -1;
}
@Override
public Drawable newDrawable() {
return new StateListDrawable(this, null);
}
@Override
public Drawable newDrawable(Resources res) {
return new StateListDrawable(this, res);
}
@Override
public boolean canApplyTheme() {
return mThemeAttrs != null || super.canApplyTheme();
}
@Override
public void growArray(int oldSize, int newSize) {
super.growArray(oldSize, newSize);
final int[][] newStateSets = new int[newSize][];
System.arraycopy(mStateSets, 0, newStateSets, 0, oldSize);
mStateSets = newStateSets;
}
}
那么Drawable跟ConstantState之间有什么关系呢?
大家可以看看国外这篇文章http://www.curious-creature.com/2009/05/02/drawable-mutations/
其实两者之间的关系如下图所示:
由同一个Resource实例化生成的多个Drawable公用一个ConstanteState,这是出于对内存节省策略的考虑。那么当我们需要修改多个Drawable中的其中一个的属性时,就会出现修改后的Drawable影响到了其他Drawable的情况,针对这种情况,我们的想法自然是复制一份ConstantState从而避免相互间的干扰。
从根源上复制Drawable的方法可以使用:
/*假设dr为原有Drawable*/
Drawable newDr = dr.getConstantState().newDrawable();
/*或者*/
Drawable newDr = dr.getConstantState().newDrawable().mutate();
例如,我在Switch.java中有这样子的定义:
if (mTrackDrawable != null) {
mTrackDrawable.setCallback(this);
if (mTrackDrawable.getConstantState() != null) {
mTrackOnDrawable = mTrackDrawable.getConstantState().newDrawable().mutate();
mTrackOnDrawable.setState(new int[]{android.R.attr.state_checked});
}
if (mTrackDrawable.getConstantState() != null) {
mTrackOffDrawable = mTrackDrawable.getConstantState().newDrawable().mutate();
mTrackOffDrawable.setState(new int[]{-android.R.attr.state_checked});
}
}
3、StateListDrawable类简介
类功能说明:该类定义了不同状态值下与之对应的图片资源,即我们可以利用该类保存多种状态值,多种图片资源。
常用方法为:
public void addState (int[] stateSet, Drawable drawable)
功能: 给特定的状态集合设置drawable图片资源
还记得我在前面介绍Selector的时候有说过XML最终会转换成StateListDrawable类,所以我们还是以demo_selector.xml来分析。
//初始化一个空对象
StateListDrawable stalistDrawable = new StateListDrawable();
//获取对应的属性值 Android框架自带的属性 attr
int pressed = android.R.attr.state_pressed;
int window_focused = android.R.attr.state_window_focused;
int focused = android.R.attr.state_focused;
int selected = android.R.attr.state_selected;
stalistDrawable.addState(new int []{pressed , window_focused}, getResources().getDrawable(R.drawable.pic1));
stalistDrawable.addState(new int []{pressed , -focused}, getResources().getDrawable(R.drawable.pic2);
stalistDrawable.addState(new int []{selected }, getResources().getDrawable(R.drawable.pic3);
stalistDrawable.addState(new int []{focused }, getResources().getDrawable(R.drawable.pic4);
//没有任何状态时显示的图片,我们给它设置我空集合
stalistDrawable.addState(new int []{}, getResources().getDrawable(R.drawable.pic5);
上面的“-”负号表示对应的属性值为false
当我们为某个View使用其作为背景色时,会根据状态进行背景图的转换。
public boolean isStateful ()
功能: 表明该状态改变了,对应的drawable图片是否会改变。
注:在StateListDrawable类中,该方法返回为true,显然状态改变后,我们的图片会跟着改变
说到状态那么不得不说在Android中View的各种状态, 一般来说,Android框架为View定义了四种不同的状态,这些状态值的改变会引发View相关操作,例如:更换背景图片、是否触发点击事件等;我们来看下图:(注明:图片表格是借鉴网上资源)
注意:selected不同于focus状态,通常在AdapterView类群下例如ListView或者GridView会使某个View处于
selected状态,并且获得该状态的View处于高亮状态。而一个窗口只能有一个视图获得焦点(focus),而一个窗口可以有多个视图处于”selected”状态中。
总结:focused状态一般是由按键操作引起的;
pressed状态是由触摸消息引起的;
selected则完全是由应用程序主动调用setSelected()进行控制。
4、View如何根据状态值的改变去绘制/显示对应的背景图?
当View任何状态值发生改变时,都会调用refreshDrawableList()方法去更新对应的背景Drawable对象。
源码路径:frameworks\base\core\java\android\view\View.java
/**
* Call this to force a view to update its drawable state. This will cause
* drawableStateChanged to be called on this view. Views that are interested
* in the new state should call getDrawableState.
*
* @see #drawableStateChanged
* @see #getDrawableState
*/
public void refreshDrawableState() {
mPrivateFlags |= PFLAG_DRAWABLE_STATE_DIRTY;
//改变drawable状态
drawableStateChanged();
ViewParent parent = mParent;
if (parent != null) {
//如果该View之上仍有父View,则一直递归通知
parent.childDrawableStateChanged(this);
}
}
该方法主要功能是根据当前的状态值去更换对应的背景Drawable对象。重点看看drawableStateChanged()
/**
* This function is called whenever the state of the view changes in such
* a way that it impacts the state of drawables being shown.
* <p>
* If the View has a StateListAnimator, it will also be called to run necessary state
* change animations.
* <p>
* Be sure to call through to the superclass when overriding this function.
*
* @see Drawable#setState(int[])
*/
@CallSuper
protected void drawableStateChanged() {
//获取当前Drawable状态(pressed),刷新mBackground的状态
final int[] state = getDrawableState();
final Drawable bg = mBackground;
if (bg != null && bg.isStateful()) {
bg.setState(state);
}
final Drawable fg = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
if (fg != null && fg.isStateful()) {
fg.setState(state);
}
if (mScrollCache != null) {
final Drawable scrollBar = mScrollCache.scrollBar;
if (scrollBar != null && scrollBar.isStateful()) {
scrollBar.setState(state);
}
}
if (mStateListAnimator != null) {
mStateListAnimator.setState(state);
}
}
该方法获得当前的状态属性— 整型集合 ; 调用Drawable类的setState方法去获取资源,最后进去看看setState()方法的具体实现:
public boolean setState(final int[] stateSet) {
if (!Arrays.equals(mStateSet, stateSet)) {
mStateSet = stateSet;
return onStateChange(stateSet);
}
return false;
}
判断状态值是否发生了变化,如果发生了变化,就调用onStateChange()方法进一步处理。
@Override
protected boolean onStateChange(int[] stateSet) {
final boolean changed = super.onStateChange(stateSet);
int idx = mStateListState.indexOfStateSet(stateSet);
if (DEBUG) android.util.Log.i(TAG, "onStateChange " + this + " states "
+ Arrays.toString(stateSet) + " found " + idx);
if (idx < 0) {
idx = mStateListState.indexOfStateSet(StateSet.WILD_CARD);
}
return selectDrawable(idx) || changed;
}
根据新的状态值,从StateListDrawable实例对象中,找到第一个完全吻合该新状态值的索引下标处
继而,调用selectDrawable()方法去获取索引下标的当前Drawable对象 具体查找算法在 mStateListState.indexOfStateSet(stateSet) 里实现了。基本思路是:查找第一个能完全吻合该新状态的索引下标,如果找到了,则立即返回。
private int indexOfStateSet(int[] stateSet) {
int[][] stateSets = this.mStateSets;
int N = this.getChildCount();
for(int i = 0; i < N; ++i) {
if(StateSet.stateSetMatches(stateSets[i], stateSet)) {
return i;
}
}
return -1;
}
再来看看selectDrawable的具体实现
public boolean selectDrawable(int idx)
{
if (idx >= 0 && idx < mDrawableContainerState.mNumChildren) {
//获取对应索引位置的Drawable对象
Drawable d = mDrawableContainerState.mDrawables[idx];
...
mCurrDrawable = d; //mCurrDrawable即使当前Drawable对象
mCurIndex = idx;
...
} else {
...
}
//请求该View刷新自己,这个方法我们稍后讲解。
invalidateSelf();
return true;
}
该函数的主要功能是选择当前索引下标处的Drawable对象,并保存在mCurrDrawable中。
5、View如何绘制Drawable
经过上面一系列的Drawable状态切换和匹配Drawale,最终我们有了需要重新绘制的pressed状态下的Drawable,此时请求绘制自身并最终会调用View的draw()方法。
public void draw(Canvas canvas) {
...
// draw方法会先绘制背景
// 不透明才绘制背景
if (!dirtyOpaque) {
final Drawable background = mBackground;
if (background != null) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if (mBackgroundSizeChanged) {
background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
}
if ((scrollX | scrollY) == 0) {
background.draw(canvas);//开始绘制的动作
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);//开始绘制的动作
canvas.translate(-scrollX, -scrollY);
}
}
}
由于Drawable的draw()方法由其实现类完成,绘制动作各有不同,但最终还是调用canvas.draw…的方法去绘制。
6、关于Drawable.Callback接口
/**
* Implement this interface if you want to create an animated drawable that
* extends {@link android.graphics.drawable.Drawable Drawable}.
* Upon retrieving a drawable, use
* {@link Drawable#setCallback(android.graphics.drawable.Drawable.Callback)}
* to supply your implementation of the interface to the drawable; it uses
* this interface to schedule and execute animation changes.
*/
public static interface Callback {
/**
* Called when the drawable needs to be redrawn. A view at this point
* should invalidate itself (or at least the part of itself where the
* drawable appears).
*
* @param who The drawable that is requesting the update.
*/
public void invalidateDrawable(Drawable who);
/**
* A Drawable can call this to schedule the next frame of its
* animation. An implementation can generally simply call
* {@link android.os.Handler#postAtTime(Runnable, Object, long)} with
* the parameters <var>(what, who, when)</var> to perform the
* scheduling.
*
* @param who The drawable being scheduled.
* @param what The action to execute.
* @param when The time (in milliseconds) to run. The timebase is
* {@link android.os.SystemClock#uptimeMillis}
*/
public void scheduleDrawable(Drawable who, Runnable what, long when);
/**
* A Drawable can call this to unschedule an action previously
* scheduled with {@link #scheduleDrawable}. An implementation can
* generally simply call
* {@link android.os.Handler#removeCallbacks(Runnable, Object)} with
* the parameters <var>(what, who)</var> to unschedule the drawable.
*
* @param who The drawable being unscheduled.
* @param what The action being unscheduled.
*/
public void unscheduleDrawable(Drawable who, Runnable what);
}
对于CallBack接口,主要看看invalidateDrawable(Drawable who)方法,这个方法的功能是如果Drawable对象的状态发生了变化,会请求View重新绘制,因此我们对应于该View的背景Drawable对象能够重新”绘制“出来。
7、自定义View绘制背景Drawable
public class MyView extends View
{
private Context mContext = null;
private Drawable mBackground = null;
private boolean mSizeChanged = true; //视图View布局(layout)大小是否发生变化
public MyView(Context context)
{
super(context);
mContext = context;
initStateListDrawable(); // 初始化图片资源
}
// 初始化图片资源
private void initStateListDrawable()
{
//有两种方式获取我们的StateListDrawable对象:
// 1、代码构建一个StateListDrawable对象
StateListDrawable statelistDrawable = new StateListDrawable();
int pressed = android.R.attr.state_pressed;
int windowfocused = android.R.attr.state_window_focused;
int enabled = android.R.attr.state_enabled;
int stateFoucesd = android.R.attr.state_focused;
//匹配状态时,是一种优先包含的关系。
// "-"号表示该状态值为false .即不匹配
statelistDrawable.addState(new int[] { pressed, windowfocused },
mContext.getResources().getDrawable(R.drawable.btn_power_on_pressed));
statelistDrawable.addState(new int[]{ -pressed, windowfocused },
mContext.getResources().getDrawable(R.drawable.btn_power_on_nor));
mBackground = statelistDrawable;
//必须设置回调,当改变状态时,会回掉该View进行invalidate()刷新操作.
mBackground.setCallback(this);
//取消默认的背景图片,因为我们设置了自己的背景图片了,否则可能造成背景图片重叠。
this.setBackgroundDrawable(null);
// 获取方式二、、使用XML获取StateListDrawable对象
// mBackground = mContext.getResources().getDrawable(R.drawable.tv_background);
}
protected void drawableStateChanged()
{
Drawable d = mBackground;
if (d != null && d.isStateful())
{
d.setState(getDrawableState());
}
super.drawableStateChanged();
}
//验证图片是否相等 , 在invalidateDrawable()会调用此方法,我们需要重写该方法。
protected boolean verifyDrawable(Drawable who)
{
return who == mBackground || super.verifyDrawable(who);
}
//draw()过程,绘制背景图片...
public void draw(Canvas canvas)
{
if (mBackground != null)
{
if(mSizeChanged)
{
//设置边界范围
mBackground.setBounds(0, 0, getRight() - getLeft(), getBottom() - getTop());
mSizeChanged = false ;
}
if ((getScrollX() | getScrollY()) == 0) //是否偏移
{
mBackground.draw(canvas); //绘制当前状态对应的图片
}
else
{
canvas.translate(getScrollX(), getScrollY());
mBackground.draw(canvas); //绘制当前状态对应的图片
canvas.translate(-getScrollX(), -getScrollY());
}
}
super.draw(canvas);
}
public void onDraw(Canvas canvas) {
...
}
}