你是否曾抱怨過產(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一起使用時可以看出效果。比如:
因為我們這里定義的填充色是透明的所有只有邊框色,讓了如果你需要Button有點擊反應(yīng)的同時還能有圓角的樣式,也是可以通過Shape來實現(xiàn)的,只需要修改填充色(solid)和對應(yīng)的state即可。效果如下:
當(dāng)然我們也可以在原有的基礎(chǔ)上給邊框加上對應(yīng)狀態(tài)下不同的顏色
- 對于此種方式定義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背景色,大部分手機是深灰色)。我們來看一下效果是什么樣的:
我們發(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());
}
});
效果是這樣的:
沒有任何問題,但是如果我們將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>
效果就變成了這樣:
我們發(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());
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);
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);