本文的示例代碼主要是基于selectorInjection進(jìn)行編寫的,如果想了解更多請查看它的詳細(xì)demo。
本文固定連接:https://github.com/tianzhijiexian/Android-Best-Practices
本文推薦的庫:https://github.com/tianzhijiexian/SelectorInjection
一、需求背景
任何一個(gè)項(xiàng)目中都需要按鈕的點(diǎn)擊效果
目前ui界面都走扁平化趨勢,按鈕的selector多為顏色
按鈕形狀各異,按壓效果不同,產(chǎn)生的selector文件過多
selector文件目前是xml配置的方式進(jìn)行定義的,不直觀,無法預(yù)覽
對于單個(gè)頁面才用到的selector文件,目前也只能放入到drawable中,無法內(nèi)聚
如果有新的按壓樣式出現(xiàn),書寫一個(gè)新的selector成本較高
如果要支持5.0的水波紋,改動成本較大
二、需求
- 我想要在多數(shù)情況下能復(fù)用selector,而不是每次都新建一個(gè)
- 對于不同顏色、不同樣式的按鈕,希望用最小的代碼量和文件數(shù)來滿足需求
- 我希望按鈕能自己能計(jì)算出自己被按下后的顏色,無需手動指定
- 我希望在5.0以下系統(tǒng)用顏色,5.0以上用水波紋
- selector應(yīng)該能支持imageview,textview,button這樣的基礎(chǔ)控件
三、實(shí)現(xiàn)
在初期定義好會用到的顏色
即使是一個(gè)再小的應(yīng)用,我也強(qiáng)烈要求設(shè)計(jì)師給定一個(gè)調(diào)色板,以后百分之九十以上的顏色都應(yīng)該從這里取。這樣既可以減少設(shè)計(jì)的選擇負(fù)擔(dān),又可以方便程序在早期做好復(fù)用工作,節(jié)約以后大改的時(shí)間。
[圖片上傳失敗...(image-5574bf-1529568793529)]
關(guān)于配色可以參考:http://www.materialpalette.com/,它會幫助你很快搭配出應(yīng)用的主體顏色。
[圖片上傳失敗...(image-5cd603-1529568793529)]
用半透明遮罩來減少selector文件
假設(shè)我們應(yīng)用里面就一種按鈕樣式,對應(yīng)5種不同的顏色。那在一種樣式的情況下產(chǎn)生的selector文件就是5個(gè)。
如果應(yīng)用支持hdpi,xhdpi,xxhdpi,那么為了實(shí)現(xiàn)一個(gè)按壓效果而建立的文件數(shù)就很龐大了。
文件數(shù) = 支持的分辨率個(gè)數(shù) x 按鈕樣式總數(shù) x 顏色數(shù)
如果還要支持水波紋,文件數(shù)就更多了。
而實(shí)際中,我們可能有矩形、圓角矩形和圓形等按鈕等樣式,我們也要同時(shí)支持hdpi,xhdpi,xxhdpi,xxxhdpi,況且一個(gè)應(yīng)用里面最少的顏色也得要五種。因此,我們需要找到一種辦法來減少文件數(shù)。
因?yàn)閟upport包支持了svg圖形,所以可以推薦設(shè)計(jì)對于icon用svg,簡單輕量。在減少selector文件方面,我先給出一個(gè)用半透明遮罩+顏色組合的方案。
就上面的圓形按鈕來說,我先定義一個(gè)圓形的selector,正常情況下是完全透明的,按下后透明度變小。
<pre class="prettyprint linenums prettyprinted" data-anchor-id="u5v1" style="padding: 9.5px; font-family: Monaco, Menlo, Consolas, "Courier New", monospace; font-size: 14px; color: rgb(51, 51, 51); border-radius: 4px; display: block; margin: 0px 0px 20px; line-height: 20px; word-break: break-all; word-wrap: break-word; white-space: pre-wrap; background: none 0px 0px repeat scroll rgba(102, 128, 153, 0.05); border: 0px solid rgba(0, 0, 0, 0.15); box-shadow: rgba(255, 255, 255, 0.1) 0px 1px 2px inset, rgba(102, 128, 153, 0.05) 45px 0px 0px inset, rgba(102, 128, 153, 0.05) 0px 1px 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="oval">
<solid android:color="#21000000" />
</shape>
</item>
<item>
<shape android:shape="oval">
<solid android:color="#00000000" />
</shape>
</item>
</selector>
</pre>
然后將這個(gè)selector設(shè)置到ImageButton的src種,最后一步是把下面這張圖設(shè)置為按鈕的背景。
<pre class="prettyprint linenums prettyprinted" data-anchor-id="p98t" style="padding: 9.5px; font-family: Monaco, Menlo, Consolas, "Courier New", monospace; font-size: 14px; color: rgb(51, 51, 51); border-radius: 4px; display: block; margin: 0px 0px 20px; line-height: 20px; word-break: break-all; word-wrap: break-word; white-space: pre-wrap; background: none 0px 0px repeat scroll rgba(102, 128, 153, 0.05); border: 0px solid rgba(0, 0, 0, 0.15); box-shadow: rgba(255, 255, 255, 0.1) 0px 1px 2px inset, rgba(102, 128, 153, 0.05) 45px 0px 0px inset, rgba(102, 128, 153, 0.05) 0px 1px 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">
<ImageButton
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@drawable/normal_bg_selector" // 正常情況透明,按下后半透明的selector
android:background="@drawable/blue_btn_icon" // 圓形藍(lán)色圖片
/>
</pre>
且因?yàn)檫@個(gè)selector是圓形的,下回遇到圓形的按鈕時(shí)就可以直接復(fù)用了。如果你掌握了這個(gè)方法,你完全可以隨機(jī)應(yīng)變,讓背景變成selector,src變成圖片,矩形按鈕也是同理。
通過SelectorView來產(chǎn)生按壓效果
上面講到的做法算是一個(gè)投機(jī)取巧的方案,如果它不能滿足你的需求,我再給出一個(gè)方案。SelectorInjection這個(gè)庫中有多個(gè)實(shí)現(xiàn)selector的view可供選擇,我選擇了最簡單的SelectorTextview。
<pre class="prettyprint linenums prettyprinted" data-anchor-id="u2xf" style="padding: 9.5px; font-family: Monaco, Menlo, Consolas, "Courier New", monospace; font-size: 14px; color: rgb(51, 51, 51); border-radius: 4px; display: block; margin: 0px 0px 20px; line-height: 20px; word-break: break-all; word-wrap: break-word; white-space: pre-wrap; background: none 0px 0px repeat scroll rgba(102, 128, 153, 0.05); border: 0px solid rgba(0, 0, 0, 0.15); box-shadow: rgba(255, 255, 255, 0.1) 0px 1px 2px inset, rgba(102, 128, 153, 0.05) 45px 0px 0px inset, rgba(102, 128, 153, 0.05) 0px 1px 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">
<kale.ui.view.SelectorTextView
android:layout_width="200dp"
android:layout_height="100dp"
app:normalColor="#03a9f4" // 正常情況的顏色
app:normalDrawable="@drawable/btn_rectangle_shape" // 形狀
/>
</pre>
通過設(shè)置normalColor
就可以讓這個(gè)textview有了按壓效果。
[圖片上傳失敗...(image-94dc74-1529568793529)]
原理如下:
<pre class="prettyprint linenums prettyprinted" data-anchor-id="fpwl" style="padding: 9.5px; font-family: Monaco, Menlo, Consolas, "Courier New", monospace; font-size: 14px; color: rgb(51, 51, 51); border-radius: 4px; display: block; margin: 0px 0px 20px; line-height: 20px; word-break: break-all; word-wrap: break-word; white-space: pre-wrap; background: none 0px 0px repeat scroll rgba(102, 128, 153, 0.05); border: 0px solid rgba(0, 0, 0, 0.15); box-shadow: rgba(255, 255, 255, 0.1) 0px 1px 2px inset, rgba(102, 128, 153, 0.05) 45px 0px 0px inset, rgba(102, 128, 153, 0.05) 0px 1px 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">
/**
* Make a dark color to press effect
*/
protected int getPressedColor(int normalColor) {
int alpha = 255;
int r = (normalColor >> 16) & 0xFF;
int g = (normalColor >> 8) & 0xFF;
int b = (normalColor >> 1) & 0xFF;
r = (r - 50 < 0) ? 0 : r - 50;
g = (g - 50 < 0) ? 0 : g - 50;
b = (b - 50 < 0) ? 0 : b - 50;
return Color.argb(alpha, r, g, b);
}
</pre>
你完全可以去源碼里面復(fù)寫這個(gè)方法,更改其算法。如果不希望用自動計(jì)算的結(jié)果,而是手動指定按壓效果,你可以用pressedColor
屬性來手動指定按下的顏色。
自動根據(jù)系統(tǒng)版本來顯示水波紋
ripple是5.0才有的效果,如果按照傳統(tǒng)方式的話,我們必須在drawable-v21下建立一個(gè)selector來實(shí)現(xiàn)系統(tǒng)區(qū)分,這樣selector的文件數(shù)目立刻就要乘以二了。所幸的是SelectorTextView會自己做系統(tǒng)區(qū)分,判別是否要顯示水波紋。
如果想關(guān)閉此功能,直接指定showRipple=false
即可。
<pre class="prettyprint linenums prettyprinted" data-anchor-id="ptq3" style="padding: 9.5px; font-family: Monaco, Menlo, Consolas, "Courier New", monospace; font-size: 14px; color: rgb(51, 51, 51); border-radius: 4px; display: block; margin: 0px 0px 20px; line-height: 20px; word-break: break-all; word-wrap: break-word; white-space: pre-wrap; background: none 0px 0px repeat scroll rgba(102, 128, 153, 0.05); border: 0px solid rgba(0, 0, 0, 0.15); box-shadow: rgba(255, 255, 255, 0.1) 0px 1px 2px inset, rgba(102, 128, 153, 0.05) 45px 0px 0px inset, rgba(102, 128, 153, 0.05) 0px 1px 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">
<kale.ui.view.SelectorTextView
android:layout_width="200dp"
android:layout_height="100dp"
app:normalColor="#03a9f4"
app:normalDrawable="@drawable/btn_rectangle_shape"
app:showRipple="false" // 關(guān)閉水波紋
/>
</pre>
利用layer-list和shape的組合實(shí)現(xiàn)復(fù)雜需求
如果我的按鈕很復(fù)雜,有陰影,但背景大部分都是純色的,該如何處理呢?
selectorInjector專門為這樣的情況提供了解決方案,現(xiàn)在就來實(shí)現(xiàn)一個(gè)圓形有陰影的按鈕。
先找到一個(gè)有陰影的按鈕形狀:
然后定義一個(gè)layer-list:
<pre class="prettyprint linenums prettyprinted" data-anchor-id="33jl" style="padding: 9.5px; font-family: Monaco, Menlo, Consolas, "Courier New", monospace; font-size: 14px; color: rgb(51, 51, 51); border-radius: 4px; display: block; margin: 0px 0px 20px; line-height: 20px; word-break: break-all; word-wrap: break-word; white-space: pre-wrap; background: none 0px 0px repeat scroll rgba(102, 128, 153, 0.05); border: 0px solid rgba(0, 0, 0, 0.15); box-shadow: rgba(255, 255, 255, 0.1) 0px 1px 2px inset, rgba(102, 128, 153, 0.05) 45px 0px 0px inset, rgba(102, 128, 153, 0.05) 0px 1px 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:drawable="@drawable/btn_oval_shadow_mask"/> // 帶陰影的按鈕形狀
<item android:id="@android:id/background" > // 以后會被用來填充的背景圖層
<shape android:shape="oval" >
<solid android:color="@android:color/transparent" /> // 以后會給這里上色
</shape>
</item>
</layer-list>
</pre>
需要注意的是,你需要給要填充顏色的item一個(gè)固定id,即@android:id/background
,接下來把這個(gè)layer-list設(shè)置到selectorView的背景中即可:
<pre class="prettyprint linenums prettyprinted" data-anchor-id="wzf2" style="padding: 9.5px; font-family: Monaco, Menlo, Consolas, "Courier New", monospace; font-size: 14px; color: rgb(51, 51, 51); border-radius: 4px; display: block; margin: 0px 0px 20px; line-height: 20px; word-break: break-all; word-wrap: break-word; white-space: pre-wrap; background: none 0px 0px repeat scroll rgba(102, 128, 153, 0.05); border: 0px solid rgba(0, 0, 0, 0.15); box-shadow: rgba(255, 255, 255, 0.1) 0px 1px 2px inset, rgba(102, 128, 153, 0.05) 45px 0px 0px inset, rgba(102, 128, 153, 0.05) 0px 1px 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">
<kale.ui.view.SelectorTextView
android:layout_width="100dp"
android:layout_height="100dp"
app:normalColor="#03a9f4"
app:normalDrawable="@drawable/btn_oval_shadow_bg_layerlist"
/>
</pre>
效果:
這樣的設(shè)計(jì)思路其實(shí)就是把變化和不變的分開,android的默認(rèn)圖層中也會有這樣的樣例。通過這樣的分離,可以讓我們復(fù)用現(xiàn)有的相似圖形,達(dá)到節(jié)約文件和代碼的目的。
多種控件的支持
如果引入了selectorInject這個(gè)庫,你能得到SelectorButton
,SelectorTextView
和SelectorImageButton
三個(gè)控件的支持。
一般情況下SelectorTextView
就可以實(shí)現(xiàn)大多數(shù)效果;在5.0上要顯示按鈕邊框陰影時(shí)就用SelectorButton
;如果圖片中需要src,那就選擇SelectorImageButton
。
注意:
如果你要將自定義view的屬性放入style中定義,需要用"包名:屬性名"
做name
的值,比如:
<pre class="prettyprint linenums prettyprinted" data-anchor-id="dt0a" style="padding: 9.5px; font-family: Monaco, Menlo, Consolas, "Courier New", monospace; font-size: 14px; color: rgb(51, 51, 51); border-radius: 4px; display: block; margin: 0px 0px 20px; line-height: 20px; word-break: break-all; word-wrap: break-word; white-space: pre-wrap; background: none 0px 0px repeat scroll rgba(102, 128, 153, 0.05); border: 0px solid rgba(0, 0, 0, 0.15); box-shadow: rgba(255, 255, 255, 0.1) 0px 1px 2px inset, rgba(102, 128, 153, 0.05) 45px 0px 0px inset, rgba(102, 128, 153, 0.05) 0px 1px 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">
<style name="Button.Oval.Red">
<item name="kale.ui.view:normalColor">@color/red</item>
</style>
</pre>
還有更多
除了支持用顏色做按壓效果之外,其實(shí)還有很多的屬性值得我們?nèi)ネ诰颉?.0.3版本的所有屬性如下:
<pre class="prettyprint linenums prettyprinted" data-anchor-id="ov28" style="padding: 9.5px; font-family: Monaco, Menlo, Consolas, "Courier New", monospace; font-size: 14px; color: rgb(51, 51, 51); border-radius: 4px; display: block; margin: 0px 0px 20px; line-height: 20px; word-break: break-all; word-wrap: break-word; white-space: pre-wrap; background: none 0px 0px repeat scroll rgba(102, 128, 153, 0.05); border: 0px solid rgba(0, 0, 0, 0.15); box-shadow: rgba(255, 255, 255, 0.1) 0px 1px 2px inset, rgba(102, 128, 153, 0.05) 45px 0px 0px inset, rgba(102, 128, 153, 0.05) 0px 1px 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">
``
<attr name="normalColor" format="color" />
``
<attr name="pressedColor" format="color" />
``
<attr name="checkedColor" format="color" />
``
<attr name="normalDrawable" format="reference" />
``
<attr name="pressedDrawable" format="reference" />
``
<attr name="checkedDrawable" format="reference" />
``
<attr name="normalStrokeColor" format="color" />
<attr name="normalStrokeWidth" format="dimension" />
``
<attr name="pressedStrokeColor" format="color" />
<attr name="pressedStrokeWidth" format="dimension" />
``
<attr name="checkedStrokeColor" format="color" />
<attr name="checkedStrokeWidth" format="dimension" />
``
<attr name="isSmart" format="boolean" />
``
<attr name="isSrc" format="boolean" />
``
<attr name="showRipple" format="boolean" />
</pre>
四、分析
本文大部分篇幅是在敘述如何通過變與不變分離的原則來節(jié)約文件的思路。其實(shí)無論是編碼還是做控件,都應(yīng)采用這種思路。那么,難道說android原本的selector的設(shè)計(jì)不合理么?非也,它也是變和不變分離的設(shè)計(jì)思想。
控件的樣子和背景分離,至于背景是用什么drawable實(shí)現(xiàn)都允許。那么為什么android本身不把按壓效果作為view的屬性呢?我們打開selector文件一看就知道了。
<pre class="prettyprint linenums prettyprinted" data-anchor-id="mo63" style="padding: 9.5px; font-family: Monaco, Menlo, Consolas, "Courier New", monospace; font-size: 14px; color: rgb(51, 51, 51); border-radius: 4px; display: block; margin: 0px 0px 20px; line-height: 20px; word-break: break-all; word-wrap: break-word; white-space: pre-wrap; background: none 0px 0px repeat scroll rgba(102, 128, 153, 0.05); border: 0px solid rgba(0, 0, 0, 0.15); box-shadow: rgba(255, 255, 255, 0.1) 0px 1px 2px inset, rgba(102, 128, 153, 0.05) 45px 0px 0px inset, rgba(102, 128, 153, 0.05) 0px 1px 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">
<?xml version="1.0" encoding="utf-8" ?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
``
<item android:drawable="@drawable/pic1" />
``
<item android:state_window_focused="false"
android:drawable="@drawable/pic1" />
``
<item android:state_focused="true"
android:state_pressed="true"
android:drawable= "@drawable/pic2" />
``
<item android:state_focused="false"
android:state_pressed="true"
android:drawable="@drawable/pic3" />
``
<item android:state_selected="true"
android:drawable="@drawable/pic4" />
``
<item android:state_focused="true"
android:drawable="@drawable/pic5" />
</selector>
</pre>
selector的組合有很多很多,把這些屬性放在view的attr中是不現(xiàn)實(shí)的,而且這些東西都是背景樣式,是view的不同狀態(tài),是同一類東西,所以將其放入一個(gè)文件進(jìn)行配置是較為合理的做法。
話又說回來,為啥selectorInjector這個(gè)庫要把selector變成attr了呢?因?yàn)樵谌粘P枨笾校覀冇貌坏剿械臓顟B(tài),通常情況我們認(rèn)為按壓的樣子就是獲得焦點(diǎn)的樣子(做TV或者做鍵盤交互的就不能這么認(rèn)為了),而控件又可以自動計(jì)算出按下后的樣子,所以就產(chǎn)生了這樣一個(gè)庫。
五、尾聲
現(xiàn)在的設(shè)計(jì)風(fēng)格都是走扁平化路線,這個(gè)對于開發(fā)者來說算是一件好事,用顏色做背景比圖片來說更簡單且效率高,占用內(nèi)存也少。在實(shí)際使用中,我會先定義三種類型的shape,一個(gè)是圓形,一個(gè)是矩形,一個(gè)是圓角矩形,然后配合調(diào)色板來實(shí)現(xiàn)設(shè)計(jì)的需求。得益于這些前期的工作,讓我少寫了很多多余文件,提升了后期的開發(fā)速度,以后適配5.0貌似就只需要一行代碼了。
參考文章
http://www.cnblogs.com/tianzhijiexian/p/4505190.html
http://android.blog.51cto.com/268543/564581/