Android的LayerDrawable/LevelListDrawable/StateListDrawable源码解析

本篇文章将这几个类放在一块儿,也是有缘由的,由于这几个是存在多个层级的;并且两个ListDrawable并非Drawable的直接子类。java

(一) LayerDrawable源码解析

看名字能够分析出这是一个可绘制的层级关系。从官方注释中,也对其有明确的说明:其由一个Drawable数组组成,而且按照数组中的顺序绘制,因此最后一个会被绘制在最顶层。 说白了,就和FrameLayout很相似,是一层一层盖上去的。android

1. 静态内部类 -> ChildDrawable

由于LayerDrawable是数组形式保存的Drawable,在其内部定义了静态类ChildDrawable用于存储每个Drawable的状态。数组

咱们先来看一下这个类的定义bash

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来设置,好比ide

/**
 * @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>标签进行配置,代码以下ui

<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>
复制代码

在上述代码片断中,须要注意的有几点:this

  • 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,看看是否有区别spa

3.2 在代码中设置

类比于xml中的设置,Java代码的示例代码以下3d

// 这里为了示例方便才用的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,敬请期待!

相关文章
相关标签/搜索