Android的LayerDrawable/LevelListDrawable/StateListDrawable源码解析

2,381 阅读11分钟

本篇文章将这几个类放在一起,也是有原因的,因为这几个是存在多个层级的;而且两个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,敬请期待!