本篇文章将这几个类放在一起,也是有原因的,因为这几个是存在多个层级的;而且两个ListDrawable并不是Drawable的直接子类。
(一) LayerDrawable源码解析
看名字可以分析出这是一个可绘制的层级关系。从官方注释中,也对其有明确的说明:其由一个Drawable数组组成,并且按照数组中的顺序绘制,所以最后一个会被绘制在最顶层。 说白了,就和FrameLayout很类似,是一层一层盖上去的。
1. 静态内部类 -> ChildDrawable
因为LayerDrawable是数组形式保存的Drawable,在其内部定义了静态类ChildDrawable用于存储每一个Drawable的状态。
我们先来看一下这个类的定义
static class ChildDrawable {
public Drawable mDrawable; // 持有一个Drawable的引用
public int[] mThemeAttrs;
public int mDensity = DisplayMetrics.DENSITY_DEFAULT;
// 需要注意的是,这几个参数并不会传递给mDrawable,是和mDrawable里面的padding是相互独立的
public int mInsetL, mInsetT, mInsetR, mInsetB;// 每一个Drawable和左上右下边界的距离,有对应的function
public int mInsetS = INSET_UNDEFINED; // 这里S代表Start,即对应LtR/RtL时使用
public int mInsetE = INSET_UNDEFINED; // E表示End
public int mWidth = -1;
public int mHeight = -1;
public int mGravity = Gravity.NO_GRAVITY;
public int mId = View.NO_ID; // 当前Drawable的ID
……………………
}
这些属性即可以在xml中定义,也可以在代码中通过对应的function来设置,比如
/**
* @param index 需要调整的layer层
* @param l 距离左边界的像素值
*/
public void setLayerInsetLeft(int index, int l) {
final ChildDrawable childDrawable = mLayerState.mChildren[index];// LayerState中有一个ChildDrawable数组,用于保存全部layer
childDrawable.mInsetL = l;
}
// 同样的,也存在对应的get方法。需要注意的是,这些API只有在>=23才能使用
2. 层级状态 -> LayerState
和之前讲述的State差异不大,只是内部多了几个变量,用于保存要绘制的Drawable数量/Drawable数组等。
static class LayerState extends ConstantState {
int mNumChildren;
ChildDrawable[] mChildren;
// 整体的padding值
int mPaddingTop = -1;
int mPaddingBottom = -1;
int mPaddingLeft = -1;
int mPaddingRight = -1;
int mPaddingStart = -1;
int mPaddingEnd = -1;
……
}
3. 实际使用
3.1 使用xml进行配置
通过<layer-list>标签进行配置,代码如下
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:paddingMode="nest">
<item android:drawable="@drawable/color_drawable" android:width="300dp" android:height="300dp"/>
<item android:drawable="@drawable/gradient_drawable" android:width="100dp" android:height="100dp"/>
</layer-list>
在上述代码片段中,需要注意的有几点:
- paddingMode一共有两种,分别是nest和stack,对应代码中的PADDING_MODE_NEST和PADDING_MODE_STACK。nest表示新添加的图层在原有图层的padding内(即除去padding的部分)进行绘制;而stack则是无视padding直接盖在上一层。
- 在item中设置的left等参数,只是在layerDrawable中呈现的padding,和每一个drawable自身的padding是不相关的。因此,如果每一个drawable中不存在padding,那么前一条所说的两种paddingMode的展示效果是一样的
- 和Drawable使用时一样,如果不设置大小,会根据其所设置的组件大小来计算
当第一层不设置自己的padding时,建议读者尝试一下切换两种mode,看看是否有区别
3.2 在代码中设置
类比于xml中的设置,Java代码的示例代码如下
// 这里为了示例方便才用的d0/d1的命名方式
d0 = getResources().getDrawable(R.drawable.shape_drawable);
d1 = getResources().getDrawable(R.drawable.color_drawable);
// drawable = (LayerDrawable) getResources().getDrawable(R.drawable.layer_drawable);
Drawable[] ds = new Drawable[2];
ds[0] = d0;
ds[1] = d1;
drawable = new LayerDrawable(ds);
drawable.setLayerHeight(0, 300);// 设置第0层的高度
drawable.setLayerWidth(0, 300);
drawable.setLayerHeight(1, 450);
drawable.setLayerWidth(1, 150);
drawable.setPaddingMode(LayerDrawable.PADDING_MODE_NEST);
由此可见,对于drawable这类视图资源还是使用xml定义更好
4. 实现了回调接口 -> Drawable.Callback
我们先来看一下这个接口是做什么的?
// 当你需要实现动画效果时,需要设置这个回调
public interface Callback {
// 需要重绘时回调
void invalidateDrawable(@NonNull Drawable who);
// 执行下一帧动画时调用
void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when);
// 取消之前scheduleDrawable要执行的动作
void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what);
}
也就是说,诸如进度条这一类需要利用动画进行视图变化时,就会触发该回调接口。
5. 总结
对于LayerDrawable,其最常用的就是SeekBar的进度条背景颜色,一般常用两层分别表示progress和secondaryProgress,如果还需要缓冲,则再添加一个background即可。 当然,SeekBar里面有很多坑,在后续会专门讲一下系统提供的UI,在做业务时应该如何自定义更改。
对于每一层的drawable,其内部属性和创建时相关。通过代码等设置的left/right等参数只是改变其在LayerDrawable中的表现
(二) LevelListDrawable源码解析
从名字可以看出,这个类是和“等级/级别”相关。
当某一个展示内容根据设置的级别不同,展示不同的效果,最常见的就是手机电量。当电量充足时是绿色,电量一半时是黄色,手机快没电时是红色。这就可以通过LevelListDrawable来实现,通过设置不同的等级从而实现切换显示。
该类并不是Drawable的直接子类,而是DrawableContainer的子类,通过中间层实现状态的选择
1. Drawable容器 -> DrawableContainer
该类是一个帮助类,用于存储Drawables并选择其中一个进行展示。因此其和LayerList的差别就在于:LayerList是把所有的Drawable按从前到后的顺序依次铺在视图上;DrawableContainer的子类则是从中选择一个Drawable进行展示。
和LayerList不同的是,其没有ChildDrawable这一辅助类存储每一个子Drawable,而是直接使用ConstantState进行保存,因为每次只需要展示一个Drawable,所以不需要把所有的数据都一次性存储到类中。
1.1 抽象内部静态类 -> DrawableContainerState
这里也有和以往不同的,之前我们遇到的都是ConstantState的实子类,而这一次是抽象子类,其只提供基本属性,DrawableContainer的每一个子类中,又继承DrawableContainerState进行对应的拓展。
除了之前讲过的一些方法外,这里新增加了一些方法。
// 当切换Drawable时,进入动画时长。同样的也有设置退出时长的方法
public final void setEnterFadeDuration(int duration) {
mEnterFadeDuration = duration;
}
// 切换时会调用的方法
public final Drawable getChild(int index) {
// 获取到数组中对应的Drawable
final Drawable result = mDrawables[index];
// 如果存在,则直接返回
if (result != null) {
return result;
}
// 如果Drawable不存在,但是对应的State存在
if (mDrawableFutures != null) {
// mDrawableFutures是一个稀疏数组
final int keyIndex = mDrawableFutures.indexOfKey(index);
if (keyIndex >= 0) {
final ConstantState cs = mDrawableFutures.valueAt(keyIndex);
// 根据保存的ConstantState创建Drawable
final Drawable prepared = prepareDrawable(cs.newDrawable(mSourceRes));
mDrawables[index] = prepared;
mDrawableFutures.removeAt(keyIndex);
if (mDrawableFutures.size() == 0) {
mDrawableFutures = null;
}
return prepared;
}
}
// 如果都不存在,则返回null
return null;
}
对应的,还有addChild方法,这里略过。原理是想通的。
1.2 阻止回调的实现 -> BlockInvalidateCallback
这是个啥?我们先来看一下它的实现
private static class BlockInvalidateCallback implements Drawable.Callback {
private Drawable.Callback mCallback;
// 包装callback
public BlockInvalidateCallback wrap(Drawable.Callback callback) {
mCallback = callback;
return this;
}
public Drawable.Callback unwrap() {
final Drawable.Callback callback = mCallback;
mCallback = null;
return callback;
}
@Override
public void invalidateDrawable(@NonNull Drawable who) {
// Ignore invalidation.空实现,原因后续会讲
}
@Override
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
if (mCallback != null) {
mCallback.scheduleDrawable(who, what, when);
}
}
@Override
public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
if (mCallback != null) {
mCallback.unscheduleDrawable(who, what);
}
}
}
通过代码我们可以发现,它只是将原来的callback保存为内部的mCallback。当调用wrap方法的时候,drawable设置的callback就是BlockInvalidateCallback,当触发invalidateDrawable时,因为是空实现,所以就不会产生重绘;而当另外两个回调触发时,由于callback被赋值给了mCallback,所以会正常触发。但是为什么要来这么一手呢?看一下应用场景。
private void initializeDrawableForDisplay(Drawable d) {
if (mBlockInvalidateCallback == null) {
mBlockInvalidateCallback = new BlockInvalidateCallback();
}
// 替换callback为Block中的mCallback
d.setCallback(mBlockInvalidateCallback.wrap(d.getCallback()));
try {
// 省略了大量的d.setXXX方法
} finally {
// 最后再换回原来的callback
d.setCallback(mBlockInvalidateCallback.unwrap());
}
}
之前的Block实现中,我们可以看到,其invalidateDrawable是一个空实现,将callback替换之后,每次触发重绘就会使用BlockInvalidateCallback中的invalidateDrawable方法,从而阻止了重绘的发生。而在initializeDrawableForDisplay方法中,为了避免初始化过程中触发invalidate,所以使用BlockInvalidateCallback来包装一层,避免触发重绘操作(setXXX方法最终会触发invalidateDrawable回调方法)。
2. LevelListDrawable的具体实现
2.1 LevelListState
在使用过程中,我们知道一个level是存在上下界的,当level处于区间内时,就会切换到对应level的视图。在前面我们也讲过,LevelListState是DrawableContainerState的子类,其内部只新增了两个变量,很明显,是保存每个drawable的上下界的数组变量
private int[] mLows;
private int[] mHighs;
也因此,其对应的新增加了几个方法
private void mutate() {
mLows = mLows.clone();
mHighs = mHighs.clone();
}
public void addLevel(int low, int high, Drawable drawable) {
// 调用父类方法,如果大小不够,则数组大小增加10,由于多态就会调用LevelListDrawable中的growArray方法,更新了mLows和mHighs大小
// 该方法会触发mutate方法,因为多态特性就会调用LevelListDrawable中的mutate方法,进而调用state中的mutate方法
int pos = addChild(drawable);
mLows[pos] = low;
mHighs[pos] = high;
}
public int indexOfLevel(int level) {
final int[] lows = mLows;
final int[] highs = mHighs;
final int N = getChildCount();
for (int i = 0; i < N; i++) {
// 这里可以看出,返回的是第一个符合条件的drawable
if (level >= lows[i] && level <= highs[i]) {
return i;
}
}
return -1;
}
2.2 使用
在代码的开头注释中,就写到一般是ImageView的setImageLevel中使用,那么我们就按照这个流程看一下
// ImageView.java
public void setImageLevel(int level) {
mLevel = level;
if (mDrawable != null) {
mDrawable.setLevel(level);// 调用Drawable#setLevel方法,为啥不是多态?请看后文
resizeFromDrawable();// 调整大小
}
}
// Drawable.java
// 因为是final方法,所以无法重写
public final boolean setLevel(@IntRange(from=0,to=10000) int level) {
if (mLevel != level) {
mLevel = level;
return onLevelChange(level);// 这里会有多态哦
}
return false;
}
// LevelListDrawable.java
protected boolean onLevelChange(int level) {
int idx = mLevelListState.indexOfLevel(level);
if (selectDrawable(idx)) {// 如果选择成功,则返回true
return true;
}
return super.onLevelChange(level);
}
// DrawableContainer.java
// 这里就是最后一步,展示出了对应的drawable
public boolean selectDrawable(int index) {
// 如果没变,则不进行处理,节约资源
if (index == mCurIndex) {
return false;
}
final long now = SystemClock.uptimeMillis();
if (DEBUG) android.util.Log.i(TAG, toString() + " from " + mCurIndex + " to " + index
+ ": exit=" + mDrawableContainerState.mExitFadeDuration
+ " enter=" + mDrawableContainerState.mEnterFadeDuration);
// 动画时间相关
if (mDrawableContainerState.mExitFadeDuration > 0) {
......
} else if (mCurrDrawable != null) {
......
}
// 如果index是存在的,则准备对应的属性
if (index >= 0 && index < mDrawableContainerState.mNumChildren) {
final Drawable d = mDrawableContainerState.getChild(index);
mCurrDrawable = d;
mCurIndex = index;
if (d != null) {
if (mDrawableContainerState.mEnterFadeDuration > 0) {
mEnterAnimationEnd = now + mDrawableContainerState.mEnterFadeDuration;
}
initializeDrawableForDisplay(d);
}
} else {
mCurrDrawable = null;
mCurIndex = -1;
}
// 如果是有动画的,则通过animate方法实现切换,内部是通过alpha变化实现的
if (mEnterAnimationEnd != 0 || mExitAnimationEnd != 0) {
if (mAnimationRunnable == null) {
mAnimationRunnable = new Runnable() {
@Override public void run() {
animate(true);
invalidateSelf();
}
};
} else {
unscheduleSelf(mAnimationRunnable);
}
// Compute first frame and schedule next animation.
animate(true);
}
invalidateSelf();// 重绘
return true;
}
2.3 xml和Java代码
类比前面的LayerListDrawable,这里同样的在xml中使用<level-list>来实现,示例代码如下
<?xml version="1.0" encoding="utf-8"?>
<level-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/shape_drawable" android:minLevel="1" android:maxLevel="20"/>
<item android:drawable="@drawable/color_drawable" android:minLevel="21" android:maxLevel="60"/>
</level-list>
当level处于1-20之间时,使用shape_drawable,当level处于21-60时,使用color_drawable。这就对应了电池电量的应用场景。 对于Java代码,仍旧是不太推荐动态设置,除非是万不得已需要动态设置时才使用。
至此,LevelListDrawable的源码流程就简略的分析完毕了。还有一些细节问题暂时不需要我们去考虑。
3.总结
一般来讲,LevelListDrawable用于根据不同时机展示不同视图的场景,典型的就是电池电量场景、涉及到同一个组件不同时机(非状态响应)时,都可以使用LevelListDrawable来处理。
(三) StateListDrawable源码解析
看了前面的LevelListDrawable之后,StateListDrawable就可以类比来看。LevelList是根据level进行切换的,那么StateList就是根据state进行切换的。
在xml中使用时,通过<selector>标签进行选择,其中有很多状态可以设置
StateListDrawable_visible
StateListDrawable_variablePadding
StateListDrawable_constantSize
DrawableStates_state_focused
DrawableStates_state_window_focused
DrawableStates_state_enabled
DrawableStates_state_checkable
DrawableStates_state_checked
DrawableStates_state_selected
DrawableStates_state_activated
DrawableStates_state_active
DrawableStates_state_single
DrawableStates_state_first
DrawableStates_state_middle
DrawableStates_state_last
DrawableStates_state_pressed
以上的属性通过名字可以很明显的知道其触发条件,那么如果我写了全部的条件,有什么触发顺序限制么?
StateListState
类比levelListState,其内部也新增了其切换视图所需的变量
int[][] mStateSets;
而当触发某一种状态时,则会返回第一个匹配的index,这也就解决了前面提出的疑问:当几个状态同时匹配时,应该显示哪一个的问题。
protected boolean onStateChange(int[] stateSet) {
// 省略部分代码
return selectDrawable(idx) || changed;
}
int indexOfStateSet(int[] stateSet) {
final int[][] stateSets = mStateSets;
final int N = getChildCount();
for (int i = 0; i < N; i++) {
// 同样的,第一个符合state条件的返回
if (StateSet.stateSetMatches(stateSets[i], stateSet)) {
return i;
}
}
// 如果没有则返回-1
return -1;
}
// StateSet.java
public static boolean stateSetMatches(int[] stateSpec, int[] stateSet) {
// 大量逻辑判断是否符合条件,有需要的同学可以去看全部源码
}
至此,StateListDrawable需要了解的内容就这么多,因为其和LevelListDrawable相似的内容较多,故而不过多赘述。
由于本人水平欠佳,有不正确的地方或者不清楚的地方,欢迎拍砖。
下一篇将讲述十分重要的bitmap以及相关的Drawable,敬请期待!