1. 程式人生 > >selector的使用及執行流程

selector的使用及執行流程

selector是Android中的背景選擇器。一個selector使用幾個不同的drawable來表示相同的圖形,根據物件的狀態來決定使用哪一個drawable。比如,一個按鈕可以有不同的狀態,預設狀態、被按下的狀態。

官方文件:
https://developer.android.com/guide/topics/resources/drawable-resource.html

一、selector的使用
selector的使用語法如下。
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"
    android:constantSize=["true" | "false"]
    android:dither=["true" | "false"]
    android:variablePadding=["true" | "false"] >
    <item
        android:drawable="@[package:]drawable/drawable_resource"
        android:state_pressed=["true" | "false"]
        android:state_focused=["true" | "false"]
        android:state_hovered=["true" | "false"]
        android:state_selected=["true" | "false"]
        android:state_checkable=["true" | "false"]
        android:state_checked=["true" | "false"]
        android:state_enabled=["true" | "false"]
        android:state_activated=["true" | "false"]
        android:state_window_focused=["true" | "false"] />
</selector>

其中會使用到的所有的狀態如下。
android:state_enabled:是否可用狀態(適用於所有View)。
android:state_pressed:被點選狀態(如按鈕被觸控或點選)。
android:state_focused:獲取到焦點狀態(如文字輸入框獲取到焦點)。
android:state_selected:被選中狀態(使用view.setSelected()可觸發)。
android:state_checkable:是否可勾選狀態(適用於可勾選的控制元件,如CheckBox)。
android:state_checked:是否被勾選狀態。
android:state_hovered:游標懸停狀態。4.0版本以上支援。
android:state_activated
:被啟用狀態(使用view.setActivated()可觸發)。3.0版本以上支援。
android:state_window_focused:當前應用程式視窗是否在前臺的狀態(如通知欄被拉下或對話方塊彈出時,當前視窗失去焦點)。

典型案例,為按鈕新增selector,按鈕擁有預設狀態和點選態。在res/drawable/路徑下新增button_bg.xml。
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true" android:drawable="@drawable/button_pressed" /> <!-- pressed -->
    <item android:drawable="@drawable/button_normal" /> <!-- default -->
</selector>

將上面定義的selector新增到Button中。
<Button
    android:layout_height="wrap_content"
    android:layout_width="wrap_content"
    android:background="@drawable/button_bg" />

button.setBackgroundDrawable(context.getResources.getDrawable(R.drawable.button_bg));

注意:selector中從上到下第一個能夠匹配當前狀態的item會被使用。如果一個item沒有定義任何狀態,那麼可以被任何一個狀態所匹配。所以按鈕的預設狀態總是放到最後。

二、selector的執行流程
當我們定義好一個selector的xml檔案,使用setBackgroundDrawable()方法傳入該selector,系統內部會建立一個StateListDrawable型別的物件。StateListDrawable類由Drawable類派生而來。

那麼,View是如何根據不同的狀態來顯示對應的背景圖的呢。

當View的狀態發生改變時,會執行它的refreshDrawableState()方法來重新整理背景圖的Drawable物件。比如,在View的setSelected()、setActivated()等方法中,都能看到當修改完狀態後,會執行refreshDrawableState()方法。
public void refreshDrawableState() {
    ......
    drawableStateChanged();

    ViewParent parent = mParent;
    if (parent != null) {
        parent.childDrawableStateChanged(this);
    }
}

refreshDrawableState()方法內部,主要是呼叫了drawableStateChanged()。從方法中可以看到,其內部會呼叫Drawable類的setState()方法來完成。
protected void drawableStateChanged() {
    // 獲取View當前狀態的資源id陣列
    final int[] state = getDrawableState();

    // 背景Drawable物件
    final Drawable bg = mBackground;
    if (bg != null && bg.isStateful()) {
        // 根據View當前狀態,更新對應狀態下的Drawable物件
        bg.setState(state);
    }

    ......
}

接下來進入Drawable類的setState()方法。當狀態值發生改變時,會回撥onStateChange()方法。
public boolean setState(final int[] stateSet) {
    if (!Arrays.equals(mStateSet, stateSet)) {
        mStateSet = stateSet;
        return onStateChange(stateSet);
    }
    return false;
}

Drawable類中的onStateChange()方法只有一行return false,StateListDrawable類對onStateChange()進行了重寫。
我們進入StateListDrawable類的onStateChange()方法。
@Override
protected boolean onStateChange(int[] stateSet) {
    final boolean changed = super.onStateChange(stateSet);

    int idx = mStateListState.indexOfStateSet(stateSet);
    if (DEBUG) android.util.Log.i(TAG, "onStateChange " + this + " states "
            + Arrays.toString(stateSet) + " found " + idx);
    if (idx < 0) {
        idx = mStateListState.indexOfStateSet(StateSet.WILD_CARD);
    }

    return selectDrawable(idx) || changed;
}
在方法內部,呼叫indexOfStateSet()方法,根據傳入的狀態,從mStateListState物件中,找到第一個能夠匹配該狀態的索引(匹配演算法的實現,在android.util.StateSet類中的stateSetMatches(int[] stateSpec, int[] stateSet)方法)。最後,呼叫selectDrawable()方法。

selectDrawable()方法在StateListDrawable類的直屬父類DrawableContainer類中。在selectDrawable()方法中根據索引獲取對應的Drawable物件,賦值給成員變數mCurrDrawable,最後呼叫invalidateSelf()方法。
public boolean selectDrawable(int idx) {
    ......

    if (idx >= 0 && idx < mDrawableContainerState.mNumChildren) {
        final Drawable d = mDrawableContainerState.getChild(idx);
        mCurrDrawable = d;
    }

    ......

    invalidateSelf();

    return true;
}

invalidateSelf()方法在父類Drawable類中。在方法內部,獲取Callback物件,呼叫Callback的invalidateDrawable()方法,引數傳入this即當前的Drawable物件。
public void invalidateSelf() {
    final Callback callback = getCallback();
    if (callback != null) {
        callback.invalidateDrawable(this);
    }
}

接下來需要弄明白的是上面方法中的Callback物件是誰。在Drawable類中找到getCallback()方法,方法中返回的是成員變數mCallback。mCallback通過setCallback()方法賦值。
private WeakReference<Callback> mCallback = null;

public Callback getCallback() {
    if (mCallback != null) {
        return mCallback.get();
    }
    return null;
}

public final void setCallback(Callback cb) {
    mCallback = new WeakReference<Callback>(cb);
}

其實View類實現了Callback介面。我們再次回到最初的方法setBackgroundDrawable()中。
public class View implements Drawable.Callback {
    ......

    public void setBackgroundDrawable(Drawable background) {
        ......

        if (background != null) {
            ......

            background.setCallback(this);

            mBackground = background;

            ......
        }
    }

    ......
}
關鍵程式碼:background.setCallback(this),也就是說View將自己賦值給了Drawable的成員變數mCallback。再結合上面invalidateSelf()方法中的關鍵程式碼:callback.invalidateDrawable(this),我們可以知道這裡的callback就是View物件。好了,接著invalidateSelf()方法往下走,執行View類的invalidateDrawable()方法。

View類的invalidateDrawable()方法如下。
public void invalidateDrawable(@NonNull Drawable drawable) {
    if (verifyDrawable(drawable)) {
        final Rect dirty = drawable.getDirtyBounds();
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;

        invalidate(dirty.left + scrollX, dirty.top + scrollY,
                dirty.right + scrollX, dirty.bottom + scrollY);
        rebuildOutline();
    }
}
View在invalidateDrawable()方法中,呼叫了invalidate()方法重新繪製。至此,View的背景drawable就發生了改變。