本文的示例代码主要是基于selectorInjection进行编写的,如果想了解更多请查看它的详细demo。
本文固定连接:https://github.com/tianzhijiexian/Android-Best-Practices
本文推荐的库:https://github.com/tianzhijiexian/SelectorInjection
一、需求背景
任何一个项目中都需要按钮的点击效果
目前ui界面都走扁平化趋势,按钮的selector多为颜色
按钮形状各异,按压效果不同,产生的selector文件过多
selector文件目前是xml配置的方式进行定义的,不直观,无法预览
对于单个页面才用到的selector文件,目前也只能放入到drawable中,无法内聚
如果有新的按压样式出现,书写一个新的selector成本较高
如果要支持5.0的水波纹,改动成本较大
二、需求
- 我想要在多数情况下能复用selector,而不是每次都新建一个
- 对于不同颜色、不同样式的按钮,希望用最小的代码量和文件数来满足需求
- 我希望按钮能自己能计算出自己被按下后的颜色,无需手动指定
- 我希望在5.0以下系统用颜色,5.0以上用水波纹
- selector应该能支持imageview,textview,button这样的基础控件
三、实现
在初期定义好会用到的颜色
即使是一个再小的应用,我也强烈要求设计师给定一个调色板,以后百分之九十以上的颜色都应该从这里取。这样既可以减少设计的选择负担,又可以方便程序在早期做好复用工作,节约以后大改的时间。
[图片上传失败...(image-5574bf-1529568793529)]
关于配色可以参考:http://www.materialpalette.com/,它会帮助你很快搭配出应用的主体颜色。
[图片上传失败...(image-5cd603-1529568793529)]
用半透明遮罩来减少selector文件
假设我们应用里面就一种按钮样式,对应5种不同的颜色。那在一种样式的情况下产生的selector文件就是5个。
如果应用支持hdpi,xhdpi,xxhdpi,那么为了实现一个按压效果而建立的文件数就很庞大了。
文件数 = 支持的分辨率个数 x 按钮样式总数 x 颜色数
如果还要支持水波纹,文件数就更多了。
而实际中,我们可能有矩形、圆角矩形和圆形等按钮等样式,我们也要同时支持hdpi,xhdpi,xxhdpi,xxxhdpi,况且一个应用里面最少的颜色也得要五种。因此,我们需要找到一种办法来减少文件数。
因为support包支持了svg图形,所以可以推荐设计对于icon用svg,简单轻量。在减少selector文件方面,我先给出一个用半透明遮罩+颜色组合的方案。
就上面的圆形按钮来说,我先定义一个圆形的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>
然后将这个selector设置到ImageButton的src种,最后一步是把下面这张图设置为按钮的背景。
<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" // 圆形蓝色图片
/>
</pre>
且因为这个selector是圆形的,下回遇到圆形的按钮时就可以直接复用了。如果你掌握了这个方法,你完全可以随机应变,让背景变成selector,src变成图片,矩形按钮也是同理。
通过SelectorView来产生按压效果
上面讲到的做法算是一个投机取巧的方案,如果它不能满足你的需求,我再给出一个方案。SelectorInjection这个库中有多个实现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>
通过设置normalColor
就可以让这个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>
你完全可以去源码里面复写这个方法,更改其算法。如果不希望用自动计算的结果,而是手动指定按压效果,你可以用pressedColor
属性来手动指定按下的颜色。
自动根据系统版本来显示水波纹
ripple是5.0才有的效果,如果按照传统方式的话,我们必须在drawable-v21下建立一个selector来实现系统区分,这样selector的文件数目立刻就要乘以二了。所幸的是SelectorTextView会自己做系统区分,判别是否要显示水波纹。
如果想关闭此功能,直接指定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" // 关闭水波纹
/>
</pre>
利用layer-list和shape的组合实现复杂需求
如果我的按钮很复杂,有阴影,但背景大部分都是纯色的,该如何处理呢?
selectorInjector专门为这样的情况提供了解决方案,现在就来实现一个圆形有阴影的按钮。
先找到一个有阴影的按钮形状:
然后定义一个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一个固定id,即@android:id/background
,接下来把这个layer-list设置到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>
效果:
这样的设计思路其实就是把变化和不变的分开,android的默认图层中也会有这样的样例。通过这样的分离,可以让我们复用现有的相似图形,达到节约文件和代码的目的。
多种控件的支持
如果引入了selectorInject这个库,你能得到SelectorButton
,SelectorTextView
和SelectorImageButton
三个控件的支持。
一般情况下SelectorTextView
就可以实现大多数效果;在5.0上要显示按钮边框阴影时就用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>
还有更多
除了支持用颜色做按压效果之外,其实还有很多的属性值得我们去挖掘。1.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>
四、分析
本文大部分篇幅是在叙述如何通过变与不变分离的原则来节约文件的思路。其实无论是编码还是做控件,都应采用这种思路。那么,难道说android原本的selector的设计不合理么?非也,它也是变和不变分离的设计思想。
控件的样子和背景分离,至于背景是用什么drawable实现都允许。那么为什么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中是不现实的,而且这些东西都是背景样式,是view的不同状态,是同一类东西,所以将其放入一个文件进行配置是较为合理的做法。
话又说回来,为啥selectorInjector这个库要把selector变成attr了呢?因为在日常需求中,我们用不到所有的状态,通常情况我们认为按压的样子就是获得焦点的样子(做TV或者做键盘交互的就不能这么认为了),而控件又可以自动计算出按下后的样子,所以就产生了这样一个库。
五、尾声
现在的设计风格都是走扁平化路线,这个对于开发者来说算是一件好事,用颜色做背景比图片来说更简单且效率高,占用内存也少。在实际使用中,我会先定义三种类型的shape,一个是圆形,一个是矩形,一个是圆角矩形,然后配合调色板来实现设计的需求。得益于这些前期的工作,让我少写了很多多余文件,提升了后期的开发速度,以后适配5.0貌似就只需要一行代码了。
参考文章
http://www.cnblogs.com/tianzhijiexian/p/4505190.html
http://android.blog.51cto.com/268543/564581/