在上篇中我们介绍了drawble类的一些内容(链接地址: http://blog.csdn.net/ieyudeyinji/article/details/48879647),下面我们会介绍drawable的核心部分
muatate方法是drawble比较有意思的一个方法,这个方法也是需要我们格外注意的一个方法,以下内容为个人翻译自google developer的文章(原文链接地址:http://android-developers.blogspot.com/2009/05/drawable-mutations.html):
Android的drawables在编写程序时是相当有用的.Drwable是一个插件化的绘制容器,一般是跟view进行关联的.比如说,BitmapDrawable是用来显示图片的,ShapeDrawable是用来绘制形状和元素的.我们甚至都可以来结合使用它们来创建复杂的显示效果.
Drawables允许我们在不继承它们的情况下,方便的定制控件的显示效果.实际上,因为drawables使用这么方便,在Android大部分原生的app中和控件中使用了drawables;在Android的Framework层大概用到了700处drawables.由于Drwables在系统中广泛的使用,Andorid在从资源中加载时,做了优化.例如,在每次我们创建Button的时候,就从Framework层的资源库(ndroid.R.drawable.btn_default)中加载了一个新的drawable.这意味着,在应用中所有的按钮使用的是不同的drawable作为他们的背景.但是,这些drawables却是拥有相同的状态,称作”constant state”.state中包含的内容是根据使用的drawable类型而定的,但是通常情况下,是包含我们在资源中定义的所有的属性.以Button为例,恒定的状态”constant state”包含一个bitmap image.通过这种方式.在所有的应用中所有的按钮共享一个bitmap,这样节省了很多的内存.
下面的图片展示了在我们指定相同的图片资源作为不同的两个view的背景时,创建了那些类.正如我们所见,创建了两个drawable,但是他们共享着相同的状态,因此使用了相同的bitmap.
共享状态的特征对于避免浪费内存而言,是不错的,但是当你尝试修改drawable的状态时,会带来一些问题的.假如: 一个应用有一个书籍的列表,每本书的名字后面有一颗星,当用户标记为喜欢的时候,是完全不透明的,在用户没有标记为喜欢的时候,是透明的.为了实现这个效果,我们很可能在adapter的getView()的方法中书写如下的代码
Book book = ...;
TextView listItem = ...;
listItem.setText(book.getTitle());
Drawable star = context.getResources().getDrawable(R.drawable.star);
if (book.isFavorite()) {
star.setAlpha(255); // opaque
} else {
star.setAlpha(70); // translucent
}
不幸的是,这部分代码会产生一个奇怪的结果,所有的drawables都是有用相同的透明度.
用”constant state” 可以解释这样的结果.尽管,我们为每一项得到的是一个新的drawable,但是constant state 仍然是相同的,对于BitmapDrawable而言,透明度是”constant state”的一部分.因此,更改一个drawable的透明度,就会更改所有其他的drawable的透明度.更糟糕的是,想要在Android 1.0 和Android1.1版本上解决这个问题,是没那么容易的.
Android 1.5提供了一个合适的方法来解决这个方法,那就是使用mutate()方法.当你调用drawable的这个方法时,更改当前drawable的”constant state”,是不会影响其他的drawables.注意: 就算是mutate()了一个drawable,bitmaps仍然是共享的.下图显示了当我们调用mutate()之后,drawable产生了什么变化.
既然是这样的,那么我们就用mutate()来更改之前的程序
Drawable star = context.getResources().getDrawable(R.drawable.star);
if (book.isFavorite()) {
star.mutate().setAlpha(255); // opaque
} else {
star. mutate().setAlpha(70); // translucent
}
为了使用方便,mutate()方法返回了drawable自身,这样允许我们使用链式调用.但是这不会产生一个新的drawable实例.采用上述的代码后,我们的应用会表现的正常.
以上的部分是个人的翻译.
这个只是google在developer中的介绍,那么在程序中是如何体现出来的呢?这个我们就来看看android中是如何加载drawable的xml文件即可.
这里我们还要看之前看过的一段程序
Resources.loadDrawable(TypedValue value, int id)部分源码分析
再次分析这段逻辑部分
1. 从缓存中获取drawable,如果不是null,直接返回
2. 如果是null,从PreloadedDrawables中寻找这个key
3. 如果找到了,那么根据ConstantState创建一个新的这样的drawable
4. 如果没有找到,执行5-6以下的逻辑
5. 如果是颜色,生成ColorDrawable
6. 如果不是颜色,根据xml或者assets的类型做对应的解析
7. 如果这时drawable不是null的话,设置drawable的状态,并且缓存drawable,如果是preloading,那么缓存到sPreloadedDrawables中,否则,缓存到sDrawableCache中(在android的系统启动中preloading是true的,缓存的是系统级别的drawable:sPreloadedDrawables,否则,正常应用启动时,preloading是false的,缓存的就是应用级别的drawable:sDrawableCache,至于详细的分析逻辑请参见私房菜的博客:android 系统资源的加载和获取 : http://blog.csdn.net/shift_wwx/article/details/39502463)
/*package*/ Drawable loadDrawable(TypedValue value, int id)
throws NotFoundException {
//start log part
......
//end log part
final long key = (((long) value.assetCookie) << 32) | value.data;
Drawable dr = getCachedDrawable(key);
if (dr != null) {
return dr;
}
Drawable.ConstantState cs = sPreloadedDrawables.get(key);
if (cs != null) {
dr = cs.newDrawable(this);
} else {
if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT &&
value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
dr = new ColorDrawable(value.data);
}
if (dr == null) {
if (value.string == null) {
throw new NotFoundException(
"Resource is not a Drawable (color or path): " + value);
}
String file = value.string.toString();
......
if (file.endsWith(".xml")) {
try {
XmlResourceParser rp = loadXmlResourceParser(
file, id, value.assetCookie, "drawable");
dr = Drawable.createFromXml(this, rp);
rp.close();
} catch (Exception e) {
NotFoundException rnf = new NotFoundException(
"File " + file + " from drawable resource ID #0x"
+ Integer.toHexString(id));
rnf.initCause(e);
throw rnf;
}
} else {
try {
InputStream is = mAssets.openNonAsset(
value.assetCookie, file, AssetManager.ACCESS_STREAMING);
dr = Drawable.createFromResourceStream(this, value, is,
file, null);
is.close();
} catch (Exception e) {
NotFoundException rnf = new NotFoundException(
"File " + file + " from drawable resource ID #0x"
+ Integer.toHexString(id));
rnf.initCause(e);
throw rnf;
}
}
}
}
if (dr != null) {
dr.setChangingConfigurations(value.changingConfigurations);
cs = dr.getConstantState();
if (cs != null) {
if (mPreloading) {
sPreloadedDrawables.put(key, cs);
} else {
synchronized (mTmpValue) {
//Log.i(TAG, "Saving cached drawable @ #" +
// Integer.toHexString(key.intValue())
// + " in " + this + ": " + cs);
mDrawableCache.put(key, new WeakReference(cs));
}
}
}
}
return dr;
}
以上介绍了那么多,其实都只是知识的铺垫而已,drawable最核心的还是要绘制一些东西.翻看Drawable类的draw(Canvas canvas)发现这个方法是抽象的,这是理所当然的事情喽,因为Drawable根本不知道应该绘制什么,至于需要绘制出什么效果,其实是应该交由具体的子类来实现的.Drawable的子类很多,我们就挑选两个来分析一下.
挑选的呢,就选择BitmapDrawable和StateListDrawable吧,因为这两个使用的频率是很高的.
我们直接来分析BitmapDrawable的绘制方法,中间遇到一些技术点,再看相关的技术点
BitmapDrawable.draw(Canvas canvas)分析
1. 获取当前的bitmap
2. 如果bitmap为null,不做逻辑的操作
3. bitmap不为null时,做如下的操作
4. 获取当前的BitmapState
5. 如果需要重新构造Shader,做6-8如下的操作
6. 获取到x轴底纹和y轴底纹,如果都是null的话,设置mPaint的Shader为null
7. 否则,重新生成一个BitmapShader,并且给mPaint
8. 设置是否要重新构造Shader为false,并且将边界拷贝到mDstRect中
9. 获取到mPaint的Shader,如果shader是null,做10-11如下的操作,否则,做12-13如下的操作
10. 如果要应用Gravity,那么计算Gravity,并且赋值给mDstRect
11. 在canvas中用mPaint绘制mDstRect大小的bitmap
12. 如果要应用Gravity,那么设置mDstRect大小为边界的大小,并且设置应用Gravity为false
13. 用画笔在canvas中绘制目标区域即可
@Override
public void draw(Canvas canvas) {
Bitmap bitmap = mBitmap;
if (bitmap != null) {
final BitmapState state = mBitmapState;
if (mRebuildShader) {
Shader.TileMode tmx = state.mTileModeX;
Shader.TileMode tmy = state.mTileModeY;
if (tmx == null && tmy == null) {
state.mPaint.setShader(null);
} else {
Shader s = new BitmapShader(bitmap,
tmx == null ? Shader.TileMode.CLAMP : tmx,
tmy == null ? Shader.TileMode.CLAMP : tmy);
state.mPaint.setShader(s);
}
mRebuildShader = false;
copyBounds(mDstRect);
}
Shader shader = state.mPaint.getShader();
if (shader == null) {
if (mApplyGravity) {
Gravity.apply(state.mGravity, mBitmapWidth, mBitmapHeight,
getBounds(), mDstRect);
mApplyGravity = false;
}
canvas.drawBitmap(bitmap, null, mDstRect, state.mPaint);
} else {
if (mApplyGravity) {
mDstRect.set(getBounds());
mApplyGravity = false;
}
canvas.drawRect(mDstRect, state.mPaint);
}
}
}
在查看Drawable类的体系结构时,发现StateListDrawable并非直接继承自Drawable,而是继承自DrawableContainer,然后发现DrawableContainer的子类有几个
DrawableContainer
–| AnimationDrawable 帧动画
–| LevelListDrawable 层级的
–| StateListDrawable 状态相关
分析子类之前,还是要先了解一下父类做的操作的
DrawableContainer正如其名字,是一个Drawable的容器,继承自这个类,可以实现多个drawable的切换,但是在外界调用者看来,是没有什么区别的.
既然是一个容器,那么肯定有增删改查的一些操作,但是由于Drawable是用于填充的后台操作,也就不需要删除的操作,所以,在这里,有的是Drawable的增加,修改和查询的操作
增加 : 容器中增加一个drawable,用于填充drawable时的操作
修改: 修改当前展示的drawable.对于客户端而言,通知外界的更改方式,然后内部实现更改展示的drawable即可.
查询:获得当前的所有和单个的drawable.对于客户端而言,不需要知道所有的drawable,只需要获取当前展示的drawable即可(这也是为什么基类Drawable中会出现一个方法:getCurrent()).
那么从以上的三个方面而言,我们最关心的就是修改的实现,这便是位于DrawableContainer的selectDrawable(int idx)方法
DrawableContainer.selectDrawable(int idx)分析
返回true
public boolean selectDrawable(int idx)
{
if (idx == mCurIndex) {
return false;
}
if (idx >= 0 && idx < mDrawableContainerState.mNumChildren) {
Drawable d = mDrawableContainerState.mDrawables[idx];
if (mCurrDrawable != null) {
mCurrDrawable.setVisible(false, false);
}
mCurrDrawable = d;
mCurIndex = idx;
if (d != null) {
d.setVisible(isVisible(), true);
d.setAlpha(mAlpha);
d.setDither(mDrawableContainerState.mDither);
d.setColorFilter(mColorFilter);
d.setState(getState());
d.setLevel(getLevel());
d.setBounds(getBounds());
}
} else {
if (mCurrDrawable != null) {
mCurrDrawable.setVisible(false, false);
}
mCurrDrawable = null;
mCurIndex = -1;
}
invalidateSelf();
return true;
}
说归说,但是在DrawableContainer类中没有发现调用这个方法的地方,这也是设计的优雅之处,我只是向外提供了这样的一个方法,告诉你如何通知容器发生了变化,但是何时调用,这个是需要具体的类来实现的.
下面我们就以StateListDrawable为例,来分析这个过程.
这个在分析之前,我们可以想象一下,StateListDrawable,就是一群状态的结合,最常用的方式,就是在drawable的xml中,我们写selector的xml.也就是说状态在更改后,会通知外界更改.那么响应state的变化,在Drawable的类中,看到使用的是如下的方法.;
Drawable.setState(final int[] stateSet)源码分析
如果当前的状态更改了,返回的是onStateChange
/**
* Specify a set of states for the drawable. These are use-case specific,
* so see the relevant documentation. As an example, the background for
* widgets like Button understand the following states:
* [{@link android.R.attr#state_focused},
* {@link android.R.attr#state_pressed}].
*
* If the new state you are supplying causes the appearance of the
* Drawable to change, then it is responsible for calling
* {@link #invalidateSelf} in order to have itself redrawn, and
* true will be returned from this function.
*
*
Note: The Drawable holds a reference on to stateSet
* until a new state array is given to it, so you must not modify this
* array during that time.
*
* @param stateSet The new set of states to be displayed.
*
* @return Returns true if this change in state has caused the appearance
* of the Drawable to change (hence requiring an invalidate), otherwise
* returns false.
*/
public boolean setState(final int[] stateSet) {
if (!Arrays.equals(mStateSet, stateSet)) {
mStateSet = stateSet;
return onStateChange(stateSet);
}
return false;
}
那么我们需要关心的就是onStateChange的方法喽,在StateListDrawable中复写了这个方法,我们来看看
StateListDrawable.onStateChange(int[] stateSet)源码分析
如果是false,返回基类的onStateChange
@Override
protected boolean onStateChange(int[] stateSet) {
int idx = mStateListState.indexOfStateSet(stateSet);
if (idx < 0) {
idx = mStateListState.indexOfStateSet(StateSet.WILD_CARD);
}
if (selectDrawable(idx)) {
return true;
}
return super.onStateChange(stateSet);
}
其实,分析到这里,我们还没有提到是如何绘制的,由于DrawableContainer是个容器,其实直接调用当前的drawable的绘制方法即可,只是一个传递的作用.
扩展: 看到这里,我们便可以想象到,LevelListDrawable实现的核心方法就是onLevelChange的时候,判断是否调用DrawableContainer.selectDrawable(int idx)的方法即可,实时确实如此.
view与drawable的关系,其实就类似于调用者和被调用者一样.view中的一些绘制信息,如background和ImageView的src的图片绘制,会交给Drawable来实现,然后呢,view的一些状态的更改,如可见,不可见,选中,未选中,透明度等等状态信息的更改会通知Drawable,然后不同的drawable会相应不同的变化,并且判断是否要通知callback来做对应的更改即可.view实现这些callback的方法,然后view校验drawable的信息,并且判断是否要重绘当前的view.
介绍了这么多比较抽象的内容,下篇博客我们介绍drawable的相关使用范例.