在 Android 開發(fā)中芹血,View 和 Drawable 之間關(guān)系十分緊密,例如我們經(jīng)常用 Drawable 作為一個(gè) View 的背景楞慈。View 常常會(huì)有狀態(tài)的改變幔烛,例如被按下、例如禁用囊蓝,而不同的狀態(tài)下 Drawable 也常有不同的表現(xiàn)饿悬。今天要探索的問題是 View 的狀態(tài)改變是如何影響 Drawable 的表現(xiàn)的。
以下將簡(jiǎn)單介紹我們平時(shí)如何在 View 上使用 Drawable聚霜,做到在不同狀態(tài)下表現(xiàn)不一樣狡恬。接著分析系統(tǒng)源碼探索其中的原理。最后以系統(tǒng)的控件和自定義控件 2 個(gè)例子來驗(yàn)證和實(shí)踐在 View 中自定義狀態(tài)的做法蝎宇。
注:
- 本文的源碼分析基于 Android API Level 23弟劲,并省略掉部分與本文關(guān)系不大的代碼。
- 在代碼中加入了個(gè)人對(duì)源碼的理解姥芥,以注釋形式呈現(xiàn)兔乞。
- 本文最后的 DEMO 項(xiàng)目源碼托管到 Github 上。
如何給 View 在不同狀態(tài)下設(shè)置不同背景色
可以對(duì)一個(gè) View 設(shè)置 background 屬性凉唐,傳進(jìn)去的是一個(gè) Drawable庸追。 如果該 Drawable 是一個(gè) StateListDrawable
(對(duì)應(yīng)的 xml 標(biāo)簽為 <selctor>
),那么它能在不同狀態(tài)下顯示不同的表現(xiàn)台囱。例如一個(gè) Button淡溯,可以在 normal、pressed簿训、disabled 等狀態(tài)下顯示不同的背景色咱娶,像這樣:
<!-- button_bg.xml -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="#CCCCCC" android:state_enabled="false"/>
<item android:drawable="#666666" android:state_pressed="true"/>
<item android:drawable="#999999"/>
</selector>
<!-- layout.xml -->
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/button_bg"
android:text="Test Button"/>
這樣即可實(shí)現(xiàn)使 Button 在不同狀態(tài)下顏色不一樣,normal 為 #999999强品,pressed 為 #666666豺总,disabled 時(shí)為 #CCCCCC。
原理
以上是經(jīng)常用來設(shè)置 Button 背景的用法择懂,那么實(shí)際上 Button(View)的不同狀態(tài)是如何和 Drawable 關(guān)聯(lián)起來的喻喳?除了上面說的 pressed 和 enabled 狀態(tài),我們可以設(shè)置的狀態(tài)還有哪些困曙?如果系統(tǒng)提供的狀態(tài)不夠用表伦,我們能否自己定義狀態(tài)?帶著這幾個(gè)問題慷丽,我們來看 Android FrameWork 的源碼蹦哼。
// View.java
// 首先,按鈕被按下的時(shí)候要糊,setPressed(boolean pressed) 會(huì)被調(diào)用纲熏。
// 注1:這里以 pressed 狀態(tài)改變?yōu)槔瑥?setPressed 方法為入口。
// 同理當(dāng) enabled 或其他狀態(tài)改變時(shí)局劲,可以看 setEnabled 方法或其他對(duì)應(yīng)方法勺拣。
public void setPressed(boolean pressed) {
final boolean needsRefresh =
pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED);
if (pressed) {
mPrivateFlags |= PFLAG_PRESSED;
} else {
mPrivateFlags &= ~PFLAG_PRESSED;
}
// 調(diào)用 refreshDrawableState() 方法來刷新 View 的狀態(tài)
if (needsRefresh) {
refreshDrawableState();
}
dispatchSetPressed(pressed);
}
// 接著看 refreshDrawableState() 方法。
// 該方法會(huì)使 View 更新它的 Drawable 的狀態(tài)鱼填,并調(diào)用 drawableStateChanged() 方法药有。
public void refreshDrawableState() {
// 設(shè)置 PFLAG_DRAWABLE_STATE_DIRTY 標(biāo)志位,后面會(huì)用到苹丸,并調(diào)用 drawableStateChanged() 方法
mPrivateFlags |= PFLAG_DRAWABLE_STATE_DIRTY;
drawableStateChanged();
ViewParent parent = mParent;
if (parent != null) {
parent.childDrawableStateChanged(this);
}
}
// 接著看 drawableStateChanged() 方法愤惰。
protected void drawableStateChanged() {
// 調(diào)用 getDrawableState() 方法得到當(dāng)前 View 的狀態(tài)合集,以一個(gè) int 數(shù)組的形式存在赘理。
final int[] state = getDrawableState();
// 將狀態(tài)合集設(shè)置給 background宦言,那么 Drawable 就會(huì)自己更新狀態(tài)并通知 View 重新繪制它。
final Drawable bg = mBackground;
if (bg != null && bg.isStateful()) {
bg.setState(state);
}
// 此處省略其他無關(guān)源代碼...
}
// 接著看 getDrawableState() 方法商模,它會(huì)返回一個(gè) resource ID 數(shù)組來表示 View 的當(dāng)前狀態(tài)蜡励。
public final int[] getDrawableState() {
// 因?yàn)?PFLAG_DRAWABLE_STATE_DIRTY 標(biāo)志位在上面 refreshDrawableState() 方法中已經(jīng)被設(shè)置,
// 所以從 refreshDrawableState() 方法調(diào)用進(jìn)來時(shí)肯定會(huì)進(jìn)入下面的 else 分支阻桅,
// 從 onCreateDrawableState(0) 方法取得 drawableState
if ((mDrawableState != null) && ((mPrivateFlags & PFLAG_DRAWABLE_STATE_DIRTY) == 0)) {
return mDrawableState;
} else {
mDrawableState = onCreateDrawableState(0);
mPrivateFlags &= ~PFLAG_DRAWABLE_STATE_DIRTY;
return mDrawableState;
}
}
// 接著看 onCreateDrawableState(int extraSpace) 方法凉倚。它的作用是生成這個(gè) View 的 Drawable State。
protected int[] onCreateDrawableState(int extraSpace) {
// 如果這個(gè) View 設(shè)置了 DUPLICATE_PARENT_STATE 標(biāo)志位(可通過 setDuplicateParentStateEnabled(boolean enabled)方法來設(shè)置)嫂沉,
// 則直接通過父View的狀態(tài)獲得state稽寒,并返回。一般的 View 都沒有設(shè)置這個(gè)標(biāo)志位趟章,所以這個(gè)條件一般不滿足杏糙。
if ((mViewFlags & DUPLICATE_PARENT_STATE) == DUPLICATE_PARENT_STATE &&
mParent instanceof View) {
return ((View) mParent).onCreateDrawableState(extraSpace);
}
int[] drawableState;
int privateFlags = mPrivateFlags;
// 檢查這個(gè) View 的 pressed、enabled蚓土、focuesed 等狀態(tài)(系統(tǒng)提供的 View 的狀態(tài)都會(huì)在這里被檢查一遍)宏侍,
// 通過位運(yùn)算記錄在 viewStateIndex 這個(gè)整型變量的各個(gè)位上
int viewStateIndex = 0;
if ((privateFlags & PFLAG_PRESSED) != 0) viewStateIndex |= StateSet.VIEW_STATE_PRESSED;
if ((mViewFlags & ENABLED_MASK) == ENABLED) viewStateIndex |= StateSet.VIEW_STATE_ENABLED;
if (isFocused()) viewStateIndex |= StateSet.VIEW_STATE_FOCUSED;
if ((privateFlags & PFLAG_SELECTED) != 0) viewStateIndex |= StateSet.VIEW_STATE_SELECTED;
if (hasWindowFocus()) viewStateIndex |= StateSet.VIEW_STATE_WINDOW_FOCUSED;
if ((privateFlags & PFLAG_ACTIVATED) != 0) viewStateIndex |= StateSet.VIEW_STATE_ACTIVATED;
if (mAttachInfo != null && mAttachInfo.mHardwareAccelerationRequested &&
HardwareRenderer.isAvailable()) {
// This is set if HW acceleration is requested, even if the current
// process doesn't allow it. This is just to allow app preview
// windows to better match their app.
viewStateIndex |= StateSet.VIEW_STATE_ACCELERATED;
}
if ((privateFlags & PFLAG_HOVERED) != 0) viewStateIndex |= StateSet.VIEW_STATE_HOVERED;
final int privateFlags2 = mPrivateFlags2;
if ((privateFlags2 & PFLAG2_DRAG_CAN_ACCEPT) != 0) {
viewStateIndex |= StateSet.VIEW_STATE_DRAG_CAN_ACCEPT;
}
if ((privateFlags2 & PFLAG2_DRAG_HOVERED) != 0) {
viewStateIndex |= StateSet.VIEW_STATE_DRAG_HOVERED;
}
// 將 viewStateIndex 變量中記錄的各個(gè)狀態(tài)轉(zhuǎn)化為一個(gè)數(shù)組,具體如何轉(zhuǎn)化可以看 StateSet.get 方法蜀漆,這里不做延伸討論谅河。
drawableState = StateSet.get(viewStateIndex);
// 如果參數(shù) extraSpace 為 0,那么這個(gè)數(shù)組就是最終要返回的數(shù)組了确丢。
if (extraSpace == 0) {
return drawableState;
}
// 如果 extraSpace 不為 0绷耍,那么會(huì)將 drawableState 數(shù)組的長(zhǎng)度擴(kuò)大 extraSpace 后返回。
final int[] fullState;
if (drawableState != null) {
fullState = new int[drawableState.length + extraSpace];
System.arraycopy(drawableState, 0, fullState, 0, drawableState.length);
} else {
fullState = new int[extraSpace];
}
return fullState;
}
到此鲜侥,我們從 View 的 pressed 狀態(tài)改變開始褂始,根據(jù)源碼看完了 View 內(nèi)部如何改變 backgroundDrawable 的狀態(tài)。簡(jiǎn)單總結(jié)一下:
- View 的 pressed 狀態(tài)改變會(huì)調(diào)用
setPressed
方法描函。 -
setPressed
方法會(huì)調(diào)用refreshDrawableState
方法崎苗。 - 在
refreshDrawableState
中會(huì)調(diào)用drawableStateChanged
狐粱,去更新 drawable 的狀態(tài),其中就包括 backgroundDrawable胆数。 - 在
drawableStateChanged
方法中肌蜻,通過getDrawableState
方法得到 DrawableState 并設(shè)置為 backgroundDrawable,那么 Drawable 就會(huì)自己更新狀態(tài)并通知 View 重新繪制幅慌。 - 而
getDrawableState
方法是通過onCreateDrawableState(int extraSpace)
方法來得到 DrawableState 的宋欺。
所以轰豆,View 的 backgroundDrawable 狀態(tài)其實(shí)是由onCreateDrawableState(int extraSpace)
方法決定的胰伍,而setPressed
方法只是作為狀態(tài)改變的整個(gè)流程的起點(diǎn)。
看完了源碼酸休,我們應(yīng)該可以解決上面提出的幾個(gè)問題:
Button(View)的不同狀態(tài)是如何和 Drawable 關(guān)聯(lián)起來的骂租?
View 在狀態(tài)改變時(shí)調(diào)用refreshDrawableState
去刷新 Drawable 的狀態(tài),而這些狀態(tài)最終由onCreateDrawableState(int extraSpace)
方法返回斑司。除了上面說的 pressed 和 enabled 狀態(tài)渗饮,我們可以設(shè)置的狀態(tài)還有哪些?
見View#onCreateDrawableState(int extraSpace)
方法宿刮,其中檢查了 pressed互站、enabled、focused僵缺、selected胡桃、window_focused、activated磕潮、hardware_accelerated翠胰、hovered、drag_can_accept自脯、drag_hovered 狀態(tài)之景。所以,對(duì)于 View膏潮,我們可以控制這些狀態(tài)锻狗。如果系統(tǒng)提供的狀態(tài)不夠用,我們能否自己定義狀態(tài)焕参?
當(dāng)然可以屋谭,不可以的話我怎么會(huì)在這篇文章提出這個(gè)問題?其實(shí) View 提供的狀態(tài)很有限龟糕,而很多時(shí)候更底層的控件都需要定義更多狀態(tài)欄滿足特定的需求桐磁。接下來我們看自定義狀態(tài)。
自定義狀態(tài)在系統(tǒng)控件中的使用
我們先來看看系統(tǒng)控件自定義狀態(tài)的做法讲岁。以 CheckBox 為例我擂,CheckBox 是 View 的間接子類(兩者中間還有好幾層繼承關(guān)系)衬以,提供了一個(gè)可勾選框的功能,它可以被 setChecked(boolean checked)校摩,并在 checked 為 true/false 時(shí)有不同的表現(xiàn)看峻,那么 CheckBox 是如何在 View 的基礎(chǔ)上實(shí)現(xiàn) checked 狀態(tài)的?
搜一下 CheckBox 的 setChecked
方法衙吩,實(shí)際上這個(gè)方法在其父類 CompoundButton 實(shí)現(xiàn)互妓。
public void setChecked(boolean checked) {
if (mChecked != checked) {
mChecked = checked;
refreshDrawableState();
// 此處省略其他無關(guān)源代碼...
}
}
// 該方法同樣調(diào)用了 refreshDrawableState() 方法,且在這個(gè)類中沒有重寫 refreshDrawableState() 方法坤塞,說明接下來的代碼流程會(huì)與上述流程一樣冯勉。
// 但是這個(gè)類重寫了 drawableStateChanged() 方法和 onCreateDrawableState(int extraSpace) 方法。
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
// 除了調(diào)用 super 的方法摹芙,還更新了自己持有的 mButtonDrawable 的狀態(tài)
if (mButtonDrawable != null) {
int[] myDrawableState = getDrawableState();
// Set the state of the Drawable
mButtonDrawable.setState(myDrawableState);
invalidate();
}
}
// 用數(shù)組保存了要自定義的狀態(tài)的 resource ID灼狰,這里自定義了 *checked* 狀態(tài)
private static final int[] CHECKED_STATE_SET = {
R.attr.state_checked
};
@Override
protected int[] onCreateDrawableState(int extraSpace) {
// 調(diào)用 super 的方法時(shí),extraSpace 參數(shù)加了 1浮禾,
// 實(shí)際上這個(gè) 1 就是 CHECKED_STATE_SET.length,即自定義的狀態(tài)的個(gè)數(shù)
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (isChecked()) {
// 如果當(dāng)前狀態(tài)是 checked盈电,則把 super 返回的 drawableState 數(shù)組與 CHECKED_STATE_SET 數(shù)組合并蝴簇,
// 合并的結(jié)果是在 super 返回的 drawableState 數(shù)組的基礎(chǔ)上,往數(shù)組后面追加了 CHECKED_STATE_SET 數(shù)組的內(nèi)容匆帚。
// 最后將數(shù)組返回熬词。
mergeDrawableStates(drawableState, CHECKED_STATE_SET);
}
return drawableState;
}
至此,就完成了對(duì) checked 狀態(tài)的自定義卷扮,并能通過 setChecked(boolean checked)
方法來改變 checked 狀態(tài)荡澎。總結(jié)一下自定義狀態(tài)需要做的幾件事:
- 提供一個(gè)改變 View 狀態(tài)的方法晤锹,并在狀態(tài)改變時(shí)調(diào)用
refreshDrawableState()
方法摩幔。 - 在
drawableStateChanged()
方法中,調(diào)用自己維護(hù)的 Drawable 的setState
方法鞭铆,傳入getDrawableState()
返回的值或衡,從而更新 Drawable 的狀態(tài)。 - 定義一個(gè) int 數(shù)組车遂,存放自定義的狀態(tài)封断。
- 在
onCreateDrawableState(int extraSpace)
方法中,調(diào)用super.onCreateDrawableState(int)
舶担,傳入 extraSpace 加上上述 int 數(shù)組的長(zhǎng)度坡疼,并將 super 返回的結(jié)果與上述 int 數(shù)組用mergeDrawableStates()
方法合并,最終返回合并后的結(jié)果衣陶。
實(shí)踐
看完原理和系統(tǒng)控件的例子柄瑰,我們也可以來自定義View的狀態(tài)了闸氮。假設(shè)我們要實(shí)現(xiàn)這樣一個(gè)需求:有一個(gè) ListView,它的每個(gè) Item 左側(cè)有一個(gè) CheckBox 可對(duì)整個(gè)列表進(jìn)行多選操作教沾。
這種情況可以使用自定義狀態(tài)來完成蒲跨,Item 是否被 checked 將影響 Drawable 的表現(xiàn),以下以 Item 的最外層 View 為 LinearLayout 為例授翻,自定義一個(gè) CheckableLinearLayout或悲。
public class CheckableLinearLayout extends LinearLayout implements Checkable {
private boolean mIsChecked = false;
private Drawable mCheckboxDrawable;
private static final int[] CHECKED_STATE_SET = {
android.R.attr.state_checked
};
public CheckableLinearLayout(Context context) {
super(context);
init();
}
public CheckableLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public CheckableLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mCheckboxDrawable = getResources().getDrawable(R.drawable.qmui_s_dialog_check_mark);
// 恢復(fù) ViewGroup 的 draw 功能(默認(rèn)關(guān)閉),使 onDraw 方法會(huì)被調(diào)用
setWillNotDraw(false);
}
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
// 將 getDrawableState 返回的狀態(tài)數(shù)組設(shè)置給 mCheckboxDrawable堪唐,并觸發(fā)重繪
if (mCheckboxDrawable != null) {
int[] drawableState = getDrawableState();
mCheckboxDrawable.setState(drawableState);
invalidate();
}
}
@Override
protected int[] onCreateDrawableState(int extraSpace) {
// 調(diào)用 super 時(shí)參數(shù)加上狀態(tài)集的長(zhǎng)度
final int[] drawableState = super.onCreateDrawableState(extraSpace + CHECKED_STATE_SET.length);
if (isChecked()) {
// 被 checked 狀態(tài)下巡语,在 super 返回的數(shù)組上追加自己的狀態(tài)集合
mergeDrawableStates(drawableState, CHECKED_STATE_SET);
}
return drawableState;
}
@Override
public void setChecked(boolean checked) {
if (mIsChecked != checked) {
mIsChecked = checked;
// checked 狀態(tài)改變時(shí)調(diào)用 refreshDrawableState()
refreshDrawableState();
}
}
@Override
public boolean isChecked() {
return mIsChecked;
}
@Override
public void toggle() {
setChecked(!isChecked());
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 將 mCheckboxDrawable 畫到 Canvas 上
if (mCheckboxDrawable != null) {
int left = QMUIDisplayHelper.dpToPx(5);
mCheckboxDrawable.setBounds(left, getPaddingTop(),
left + mCheckboxDrawable.getIntrinsicWidth(),
getPaddingTop() + mCheckboxDrawable.getIntrinsicHeight());
mCheckboxDrawable.draw(canvas);
}
}
}
以下是 dialog_check_mark.xml
文件的內(nèi)容,設(shè)置了 normal 情況和 checked 情況下的不同表現(xiàn)羔杨。
<!-- dialog_check_mark.xml -->
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/checkbox_checked" android:state_checked="true" />
<item android:drawable="@drawable/checkbox_normal" />
</selector>
到此捌臊,就完成了對(duì) LinearLayout 加上 Checked 狀態(tài)管理的功能杨蛋,在被調(diào)用 setCheck(boolean checked) 方法時(shí)兜材,Drawable 的表現(xiàn)會(huì)隨之改變。
總結(jié)
我們從 Button 被 pressed 時(shí)的源碼入手逞力,分析了 Button(View)和 Drawable 如何關(guān)聯(lián)起來曙寡,狀態(tài)改變時(shí)如何通知 Drawable 改變。接著分析了系統(tǒng)控件 CompoundButton 的狀態(tài)管理寇荧。最后自定義了一個(gè)包含 Checked 狀態(tài)的 LinearLayout举庶。