一個工具類搞定drawable下扎堆的selector

你是否曾抱怨過產(chǎn)品經(jīng)理,為什么一個app里面按鈕正常/按下狀態(tài)顏色不統(tǒng)一起來?
你是否曾埋怨過UI,為什么不同地方輸入框的顏色、圓角和聚焦字體顏色不一樣?
你是否曾因為為了避免少些一個selector的.xml文件而手動的去控制TextView在選中/非選中狀態(tài)下的顏色?
你是否曾因為drawable目錄下selector,shape文件太多,第二次要用卻忘了以前有沒有定義過又找不到而苦惱?

今天,我便是為解決此問題而來

  • 傳統(tǒng)方式對于Button背景色不同狀態(tài)下以XML文件的方式的定義

通常狀態(tài)下我們對于Button的按壓狀態(tài)下和非按壓狀態(tài)下我們需要兩種不同的背景
一般都是通過xml文件來書寫Selector方式來實現(xiàn)的:

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/blue_bg_dark" android:state_pressed="true"/>
    <item android:drawable="@drawable/blue_bg" android:state_pressed="false"/>
</selector>

當(dāng)然了其實后面一個item不用寫state_pressed="false"也無妨(而且正確的寫法,selector最后一個item就應(yīng)該不寫state),這是以圖片的方式來定義不同狀態(tài)下的背景,當(dāng)然你也可是換成顏色的方式。

不過這兩種方式有個共同的缺點,就是你無法去定義item的形狀。圖片是什么形狀,就是什么形狀。通常設(shè)計給我們的按鈕、輸入框通常都會是一個圓角的矩形,這個時候我們就需要用到Shape。這里就以EditText的圓角背景為例,通常我們會這么定義:

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_window_focused="true">
        <shape android:shape="rectangle">
            <corners android:radius="10dp"/>
            <stroke android:width="1dp" android:color="@color/red"/>
            <solid android:color="@color/transparent"/>
        </shape>
    </item>

    <item android:state_focused="false">
        <shape android:shape="rectangle">
            <corners android:radius="10dp"/>
            <stroke android:width="1dp" android:color="@color/gray"/>
            <solid android:color="@color/transparent"/>
        </shape>
    </item>
</selector>

這樣便是定義了一個圓角半徑為10dp的矩形背景框,當(dāng)EditText獲得焦點的時候邊框會呈現(xiàn)紅色,沒有獲取焦點時會呈現(xiàn)灰色,需要兩個或以上各EditText一起使用時可以看出效果。比如:


1.gif

因為我們這里定義的填充色是透明的所有只有邊框色,讓了如果你需要Button有點擊反應(yīng)的同時還能有圓角的樣式,也是可以通過Shape來實現(xiàn)的,只需要修改填充色(solid)和對應(yīng)的state即可。效果如下:


2.gif

當(dāng)然我們也可以在原有的基礎(chǔ)上給邊框加上對應(yīng)狀態(tài)下不同的顏色


3.gif
  • 對于此種方式定義Selector的注意點

這里我要說的注意點只有一點,就是前面所提到的Selector的最后一個item一定要加上一個沒有寫state的item,這個item就是默認(rèn)狀態(tài)下的item。不過對于這里的默認(rèn),大多數(shù)人存在誤區(qū),很多人可能認(rèn)為對于state_pressed默認(rèn)狀態(tài)就是false,state_selected默認(rèn)狀態(tài)也是false,我們只需要寫一個state_pressed=true或者state_selected=true的item再寫一個默認(rèn)狀態(tài)下的item即可。

我要告訴大家的是:爾等錯了~~

至于為什么錯了,我們就以一個按鈕(默認(rèn):淺藍(lán)色,按下去:深藍(lán)色)為例。
按照大部分人以往的認(rèn)知,應(yīng)該是這么寫

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@android:color/holo_blue_dark" android:state_pressed="true"/>
    <item android:drawable="@android:color/holo_blue_light"/>
</selector>

沒有任何問題,效果也如我們預(yù)期的一樣(大家都知道的,圖就不貼了)。
那么,為什么我會說大家錯了呢?如果我們將上面的代碼片段改成下面這樣呢:

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@android:color/holo_blue_dark"       android:state_pressed="false"/>
    <item android:drawable="@android:color/holo_blue_light"/>
</selector>

我們只是把第一個item的state_pressed由true改為了false。如果找大家之前的想法“最后一個默認(rèn)的item對于state_pressed值為false”,那么照理來講那么對于這個按鈕默認(rèn)狀態(tài)下(沒有按下去)是深藍(lán)色(selector對于重復(fù)state只會取第一個有效值,后面的無效,大家可以自己驗證),按下去我們是沒有設(shè)置的(照理來講應(yīng)該是透明的,或者系統(tǒng)默認(rèn)的button背景色,大部分手機是深灰色)。我們來看一下效果是什么樣的:


4.gif

我們發(fā)現(xiàn):沒有按下去的時候確實是深藍(lán)色,按下去之后卻成了淺藍(lán)色。

這是不是就印證了我所說的“爾等錯了”(容我嘚瑟一下,哈哈~)

正解應(yīng)該是最后一個默認(rèn)item的state包含前面所有item的state值的相反值。意思就是前面有個state_pressed=true的最后一個item就默認(rèn)加上了一個state_pressed=false,前面有個state_focused=false的最后一個item就默認(rèn)加上了一個state_focused=true的item。(我已經(jīng)多次驗證過了,如果誰有更好的理解,歡迎指出!)

  • 默認(rèn)item出現(xiàn)的位置

我們先來把之前的代碼修改一下:

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@android:color/holo_orange_dark" android:state_selected="true"/>
    <item android:drawable="@android:color/holo_blue_dark" android:state_pressed="true"/>
    <item android:drawable="@android:color/holo_blue_light"/>
</selector>

在給按鈕加個監(jiān)聽:

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        button.setSelected(!button2.isSelected());
    }
});

效果是這樣的:


5.gif

沒有任何問題,但是如果我們將selector的默認(rèn)item放到最前面:

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@android:color/holo_blue_light"/>
    <item android:drawable="@android:color/holo_orange_dark" android:state_selected="true"/>
    <item android:drawable="@android:color/holo_blue_dark" android:state_pressed="true"/>
</selector>

效果就變成了這樣:


6.gif

我們發(fā)現(xiàn)不論是默認(rèn)狀態(tài)、點擊狀態(tài)、還是選中狀態(tài)全都是一樣的,顯示的都是默認(rèn)item。

這里就要注意另一個很重要的一點了:范圍大的item必須寫在范圍小的item的后面,默認(rèn)item必須寫在selector的最后一行。其實根據(jù)這一點,我們又會發(fā)現(xiàn)我們前面對于默認(rèn)item的state的總結(jié)是有誤的,如果默認(rèn)item只是有與其他item的state相反的state那么,當(dāng)把它放在前面的時候應(yīng)該是不會影響其他的item的,可想而知默認(rèn)item應(yīng)該是對于每個state不論true還是false都存在,因為selector對于重復(fù)的state只取第一個有效,這也就解釋了為什么把默認(rèn)item放在最前面,后面的item不起作用了。

  • Selector代碼實現(xiàn)

    • 廢話了那么多,終于到了重點了。在Android SDK中Selector對應(yīng)的Java類是android.graphics.drawable.StateListDrawable,對于Selector下的每一個item我們使用addState的方式來添加,比如我們想要添加一個state_pressed=true時背景色為藍(lán)色的item,我們可以采用這段代碼
    StateListDrawable selector = new StateListDrawable();
    selector.addState(new int[]{android.R.attr.state_pressed}, new ColorDrawable(Color.BLACK));
    

    如果想要添加一個state_enabled=false的item,我們可以這樣:

    selector.addState(new int[]{-android.R.attr.state_enabled}, new ColorDrawable(Color.GRAY));
    

    之后再給對應(yīng)的view setBackground即可。
    需要注意的是默認(rèn)item的state我們寫:new int[]{}

    • 對于Shape對應(yīng)的Java類是android.graphics.drawable.GradientDrawable,如果想要實現(xiàn)一個我一開始給大家展示的EditText的那種邊框,我們可以采用以下這段代碼:
    StateListDrawable selector = new StateListDrawable();
    
    GradientDrawable focusedShape = getItemShape(GradientDrawable.RECTANGLE,
        20, Color.TRANSPARENT, 1, Color.RED);
    selector.addState(new int[]{android.R.attr.state_focused}, focusedShape);
    
    GradientDrawable defaultShape = getItemShape(GradientDrawable.RECTANGLE,
            20, Color.TRANSPARENT, 1, Color.GRAY);
    selector.addState(new int[]{android.R.attr.state_focused}, defaultShape);
    
    private GradientDrawable getItemShape(int shape, int cornerRadius,
            int solidColor, int strokeWidth, int strokeColor) {
        GradientDrawable drawable = new GradientDrawable();
        drawable.setShape(shape);
        drawable.setStroke(strokeWidth, strokeColor);
        drawable.setCornerRadius(cornerRadius);
        drawable.setColor(solidColor);
        return drawable;
    }
    
    • 知道了Selector與Shape對應(yīng)的Java類,以及item的規(guī)則(前面廢話了那么多,就是為了現(xiàn)在服務(wù))之后,開始完成我們的工具類(我們就以Selector套Shape為例,其他的關(guān)于文字顏色,以及使用圖片作為背景會在后文源碼中公布):平時我們用到的Selector無非就是enabled,pressed,selected,focused和默認(rèn)這幾種狀態(tài)。對于Shape而言,還有個形狀。好的,我們就定義一個類ShapeSelector
    public static final class ShapeSelector {
    
        @IntDef({GradientDrawable.RECTANGLE, GradientDrawable.OVAL,
                GradientDrawable.LINE, GradientDrawable.RING})
        private @interface Shape {}
    
        private int mShape;               //the shape of background
        private int mDefaultBgColor;      //default background color
        private int mDisabledBgColor;     //state_enabled = false
        private int mPressedBgColor;      //state_pressed = true
        private int mSelectedBgColor;     //state_selected = true
        private int mFocusedBgColor;      //state_focused = true
        private int mStrokeWidth;         //stroke width in pixel
        private int mDefaultStrokeColor;  //default stroke color
        private int mDisabledStrokeColor; //state_enabled = false
        private int mPressedStrokeColor;  //state_pressed = true
        private int mSelectedStrokeColor; //state_selected = true
        private int mFocusedStrokeColor;  //state_focused = true
        private int mCornerRadius;        //corner radius
    
        private boolean hasSetDisabledBgColor = false;
        private boolean hasSetPressedBgColor = false;
        private boolean hasSetSelectedBgColor = false;
        private boolean hasSetFocusedBgColor = false;
    
        private boolean hasSetDisabledStrokeColor = false;
        private boolean hasSetPressedStrokeColor = false;
        private boolean hasSetSelectedStrokeColor = false;
        private boolean hasSetFocusedStrokeColor = false;
    
        public ShapeSelector() {
            //initialize default values
            mShape = GradientDrawable.RECTANGLE;
            mDefaultBgColor = Color.TRANSPARENT;
            mDisabledBgColor = Color.TRANSPARENT;
            mPressedBgColor = Color.TRANSPARENT;
            mSelectedBgColor = Color.TRANSPARENT;
            mFocusedBgColor = Color.TRANSPARENT;
            mStrokeWidth = 0;
            mDefaultStrokeColor = Color.TRANSPARENT;
            mDisabledStrokeColor = Color.TRANSPARENT;
            mPressedStrokeColor = Color.TRANSPARENT;
            mSelectedStrokeColor = Color.TRANSPARENT;
            mFocusedStrokeColor = Color.TRANSPARENT;
            mCornerRadius = 0;
        }
    
        public ShapeSelector setShape(@Shape int shape) {
            mShape = shape;
            return this;
        }
    
        public ShapeSelector setDefaultBgColor(@ColorInt int color) {
            mDefaultBgColor = color;
            if (!hasSetDisabledBgColor)
                mDisabledBgColor = color;
            if (!hasSetPressedBgColor)
                mPressedBgColor = color;
            if (!hasSetSelectedBgColor)
                mSelectedBgColor = color;
            if (!hasSetFocusedBgColor)
                mFocusedBgColor = color;
            return this;
        }
    
        public ShapeSelector setDisabledBgColor(@ColorInt int color) {
            mDisabledBgColor = color;
            hasSetDisabledBgColor = true;
            return this;
        }
    
        public ShapeSelector setPressedBgColor(@ColorInt int color) {
            mPressedBgColor = color;
            hasSetPressedBgColor = true;
            return this;
        }
    
        public ShapeSelector setSelectedBgColor(@ColorInt int color) {
            mSelectedBgColor = color;
            hasSetSelectedBgColor = true;
            return this;
        }
    
        public ShapeSelector setFocusedBgColor(@ColorInt int color) {
            mFocusedBgColor = color;
            hasSetPressedBgColor = true;
            return this;
        }
    
        public ShapeSelector setStrokeWidth(@Dimension int width) {
            mStrokeWidth = width;
            return this;
        }
    
        public ShapeSelector setDefaultStrokeColor(@ColorInt int color) {
            mDefaultStrokeColor = color;
            if (!hasSetDisabledStrokeColor)
                mDisabledStrokeColor = color;
            if (!hasSetPressedStrokeColor)
                mPressedStrokeColor = color;
            if (!hasSetSelectedStrokeColor)
                mSelectedStrokeColor = color;
            if (!hasSetFocusedStrokeColor)
                mFocusedStrokeColor = color;
            return this;
        }
    
        public ShapeSelector setDisabledStrokeColor(@ColorInt int color) {
            mDisabledStrokeColor = color;
            hasSetDisabledStrokeColor = true;
            return this;
        }
    
        public ShapeSelector setPressedStrokeColor(@ColorInt int color) {
            mPressedStrokeColor = color;
            hasSetPressedStrokeColor = true;
            return this;
        }
    
        public ShapeSelector setSelectedStrokeColor(@ColorInt int color) {
            mSelectedStrokeColor = color;
            hasSetSelectedStrokeColor = true;
            return this;
        }
    
        public ShapeSelector setFocusedStrokeColor(@ColorInt int color) {
            mFocusedStrokeColor = color;
            hasSetFocusedStrokeColor = true;
            return this;
        }
    
        public ShapeSelector setCornerRadius(@Dimension int radius) {
            mCornerRadius = radius;
            return this;
        }
    
        public StateListDrawable create() {
            StateListDrawable selector = new StateListDrawable();
    
            //enabled = false
            if (hasSetDisabledBgColor || hasSetDisabledStrokeColor) {
                GradientDrawable disabledShape = getItemShape(mShape, mCornerRadius,
                        mDisabledBgColor, mStrokeWidth, mDisabledStrokeColor);
                selector.addState(new int[]{-android.R.attr.state_enabled}, disabledShape);
            }
    
            //pressed = true
            if (hasSetPressedBgColor || hasSetPressedStrokeColor) {
                GradientDrawable pressedShape = getItemShape(mShape, mCornerRadius,
                        mPressedBgColor, mStrokeWidth, mPressedStrokeColor);
                selector.addState(new int[]{android.R.attr.state_pressed}, pressedShape);
            }
    
            //selected = true
            if (hasSetSelectedBgColor || hasSetSelectedStrokeColor) {
                GradientDrawable selectedShape = getItemShape(mShape, mCornerRadius,
                        mSelectedBgColor, mStrokeWidth, mSelectedStrokeColor);
                selector.addState(new int[]{android.R.attr.state_selected}, selectedShape);
            }
    
            //focused = true
            if (hasSetFocusedBgColor || hasSetFocusedStrokeColor) {
                GradientDrawable focusedShape = getItemShape(mShape, mCornerRadius,
                        mFocusedBgColor, mStrokeWidth, mFocusedStrokeColor);
                selector.addState(new int[]{android.R.attr.state_focused}, focusedShape);
            }
    
            //default
            GradientDrawable defaultShape = getItemShape(mShape, mCornerRadius,
                    mDefaultBgColor, mStrokeWidth, mDefaultStrokeColor);
            selector.addState(new int[]{}, defaultShape);
    
            return selector;
        }
    
        private GradientDrawable getItemShape(int shape, int cornerRadius,
                int solidColor, int strokeWidth, int strokeColor) {
            GradientDrawable drawable = new GradientDrawable();
            drawable.setShape(shape);
            drawable.setStroke(strokeWidth, strokeColor);
            drawable.setCornerRadius(cornerRadius);
            drawable.setColor(solidColor);
            return drawable;
        }
    }
    

    我們采用Builder模式方便對ShapeSelector設(shè)置各種狀態(tài)下的屬性,最后調(diào)用create方法直接生成StateListDrawable對象設(shè)置給對應(yīng)的View即可。

  • 接下來我們來看一下效果

    et1.setBackground(SelectorFactory.newShapeSelector()
            .setDefaultStrokeColor(Color.GRAY)
            .setFocusedStrokeColor(Color.YELLOW)
            .setStrokeWidth(2)
            .create());
    et2.setBackground(SelectorFactory.newShapeSelector()
            .setDefaultStrokeColor(Color.GRAY)
            .setFocusedStrokeColor(Color.YELLOW)
            .setStrokeWidth(2)
            .setCornerRadius(20)
            .create());
    et3.setBackground(SelectorFactory.newShapeSelector()
            .setDefaultStrokeColor(Color.GRAY)
            .setFocusedStrokeColor(Color.RED)
            .setStrokeWidth(2)
            .create());

    et1.setHintTextColor(SelectorFactory.newColorSelector()
            .setDefaultColor(Color.GRAY)
            .setFocusedColor(Color.BLACK)
            .create());
    et2.setHintTextColor(SelectorFactory.newColorSelector()
            .setDefaultColor(Color.GRAY)
            .setFocusedColor(Color.BLACK)
            .create());
    et3.setHintTextColor(SelectorFactory.newColorSelector()
            .setDefaultColor(Color.GRAY)
            .setFocusedColor(Color.BLACK)
            .create());

    et1.setTextColor(SelectorFactory.newColorSelector()
            .setDefaultColor(Color.BLACK)
            .setFocusedColor(Color.YELLOW)
            .create());
    et2.setTextColor(SelectorFactory.newColorSelector()
            .setDefaultColor(Color.BLACK)
            .setFocusedColor(Color.YELLOW)
            .create());
    et3.setTextColor(SelectorFactory.newColorSelector()
            .setDefaultColor(Color.BLACK)
            .setFocusedColor(Color.RED)
            .create());
7.gif
    btn1.setBackground(SelectorFactory.newGeneralSelector()
            .setDefaultDrawable(ContextCompat.getDrawable(this, R.mipmap.blue_primary))
            .setPressedDrawable(this, R.mipmap.blue_primary_dark)
            .create());
    btn2.setBackground(SelectorFactory.newShapeSelector()
            .setDefaultBgColor(ContextCompat.getColor(this, android.R.color.holo_blue_light))
            .setPressedBgColor(ContextCompat.getColor(this, android.R.color.holo_blue_dark))
            .create());
    btn3.setBackground(SelectorFactory.newShapeSelector()
            .setDefaultBgColor(ContextCompat.getColor(this, android.R.color.holo_blue_light))
            .setPressedBgColor(ContextCompat.getColor(this, android.R.color.holo_blue_dark))
            .setSelectedBgColor(ContextCompat.getColor(this, android.R.color.holo_blue_dark))
            .setCornerRadius(20)
            .create());
    btn3.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            btn3.setSelected(!btn3.isSelected());
        }
    });
    btn4.setBackground(SelectorFactory.newShapeSelector()
            .setDefaultBgColor(ContextCompat.getColor(this, android.R.color.holo_blue_light))
            .setPressedBgColor(ContextCompat.getColor(this, android.R.color.holo_blue_dark))
            .setDisabledBgColor(Color.GRAY)
            .create());
    btn4.setEnabled(false);
8.gif
    tv1.setBackground(SelectorFactory.newShapeSelector()
            .setDefaultStrokeColor(Color.GRAY)
            .setStrokeWidth(1)
            .setCornerRadius(20)
            .create());
    tv2.setTextColor(SelectorFactory.newColorSelector()
            .setDefaultColor(Color.BLACK)
            .setPressedColor(Color.YELLOW)
            .create());
    tv3.setTextColor(SelectorFactory.newColorSelector()
            .setDefaultColor(Color.BLACK)
            .setSelectedColor(Color.YELLOW)
            .create());
    tv3.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            tv3.setSelected(!tv3.isSelected());
        }
    });
    tv4.setTextColor(SelectorFactory.newColorSelector()
            .setDefaultColor(Color.BLACK)
            .setSelectedColor(Color.YELLOW)
            .setDisabledColor(Color.GRAY)
            .create());
    tv4.setEnabled(false);
9.gif
  • 使用捷徑
    有人可能會說:雖然是可以不用再定義很多的xml文件了,但是一個頁面有很多的view,我不是要對每個view都是挨個設(shè)置一下背景嘛,這豈不更麻煩。
    ——簡單,我們自定義一個View繼承一下原有的View,再自定義一些屬性,然后在構(gòu)造方法里面拿到這些屬性,用我們的工具類給他設(shè)置上不就行了嘛,這樣是需要再寫布局的時候?qū)懸幌聦傩跃托辛恕?/p>

  • Github

  • CSDN

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容