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,敬請期待!

相關文章
相關標籤/搜索