本篇文章將這幾個類放在一塊兒,也是有緣由的,由於這幾個是存在多個層級的;並且兩個ListDrawable並非Drawable的直接子類。java
看名字能夠分析出這是一個可繪製的層級關係。從官方註釋中,也對其有明確的說明:其由一個Drawable數組組成,而且按照數組中的順序繪製,因此最後一個會被繪製在最頂層。 說白了,就和FrameLayout很相似,是一層一層蓋上去的。android
由於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才能使用
複製代碼
和以前講述的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;
……
}
複製代碼
經過<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
當第一層不設置本身的padding時,建議讀者嘗試一下切換兩種mode,看看是否有區別spa
類比於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定義更好
咱們先來看一下這個接口是作什麼的?
// 當你須要實現動畫效果時,須要設置這個回調
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);
}
複製代碼
也就是說,諸如進度條這一類須要利用動畫進行視圖變化時,就會觸發該回調接口。
對於LayerDrawable,其最經常使用的就是SeekBar的進度條背景顏色,通常經常使用兩層分別表示progress和secondaryProgress,若是還須要緩衝,則再添加一個background便可。 固然,SeekBar裏面有不少坑,在後續會專門講一下系統提供的UI,在作業務時應該如何自定義更改。
對於每一層的drawable,其內部屬性和建立時相關。經過代碼等設置的left/right等參數只是改變其在LayerDrawable中的表現
從名字能夠看出,這個類是和「等級/級別」相關。
當某一個展現內容根據設置的級別不一樣,展現不一樣的效果,最多見的就是手機電量。當電量充足時是綠色,電量一半時是黃色,手機快沒電時是紅色。這就能夠經過LevelListDrawable來實現,經過設置不一樣的等級從而實現切換顯示。
該類並非Drawable的直接子類,而是DrawableContainer的子類,經過中間層實現狀態的選擇
該類是一個幫助類,用於存儲Drawables並選擇其中一個進行展現。所以其和LayerList的差異就在於:LayerList是把全部的Drawable按從前到後的順序依次鋪在視圖上;DrawableContainer的子類則是從中選擇一個Drawable進行展現。
和LayerList不一樣的是,其沒有ChildDrawable這一輔助類存儲每個子Drawable,而是直接使用ConstantState進行保存,由於每次只須要展現一個Drawable,因此不須要把全部的數據都一次性存儲到類中。
這裏也有和以往不一樣的,以前咱們遇到的都是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方法,這裏略過。原理是想通的。
複製代碼
這是個啥?咱們先來看一下它的實現
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回調方法)。
在使用過程當中,咱們知道一個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;
}
複製代碼
在代碼的開頭註釋中,就寫到通常是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;
}
複製代碼
類比前面的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的源碼流程就簡略的分析完畢了。還有一些細節問題暫時不須要咱們去考慮。
通常來說,LevelListDrawable用於根據不一樣時機展現不一樣視圖的場景,典型的就是電池電量場景、涉及到同一個組件不一樣時機(非狀態響應)時,均可以使用LevelListDrawable來處理。
看了前面的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
複製代碼
以上的屬性經過名字能夠很明顯的知道其觸發條件,那麼若是我寫了所有的條件,有什麼觸發順序限制麼?
類比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,敬請期待!