2018年7月

前言

昨天写了一个自定义button留了个小坑,就是如何用代码写一个点击阴影效果。上次说的那种直接改当前颜色的透明度方法其实是不可行的,因为只改变其透明度很难实现阴影效果。所以这里我才用了另外一个方法设置drawable。


使用java代码实现layer-list

我们在编写drawable的xml时有时会用到一个叫做layer-list标签。这个标签可以用于两个或多个shape重叠,进而产生类似底部阴影,或者层叠之类的立体效果。当然,除了这些还有其他效果。它的效果类似与帧布局,后加进来的shape会放置在之前的shape之上。而今天要完成的效果就是将一个带透明度的黑色shape背景放置在原设置的颜色之上产生点击会带阴影的交互效果。
那么如何通过java代码编写layer-list呢?其实很简单,Android自带一个LayerDrawable就是对应layer-list标签的。

    GradientDrawable gd = new GradientDrawable();           
    gd.setCornerRadii(radius);          
    gd.setColor(backgroundColor);
    GradientDrawable gd2 = new GradientDrawable();
    gd2.setCornerRadii(radius);
    gd2.setColor(Color.parseColor("#55000000"));
    Drawable[] layers = {gd, gd2};
    LayerDrawable layerDrawable = new LayerDrawable(layers);

LayerDrawable类的构造函数需要接收一个Drawable数组,数组里的元素顺序越往后的就代表这个Drawable对象在越上层。然后把第二个GradientDrawable对象的颜色改成带有透明度的黑色即可。这个生成出来的LayerDrawable就是一个带有原颜色又带有一层阴影了。最后将这个LayerDrawable对象代替之前的gd2即可。

    sd.addState(new int[] { pressed, window_focused }, layerDrawable);
    sd.addState(new int[] { pressed, -focused }, layerDrawable);          //负号->false
    sd.addState(new int[] { selected }, layerDrawable);
    sd.addState(new int[] { focused }, layerDrawable);
    sd.addState(new int[] {}, gd);

最后

Android的Button控件默认是一个底部带有阴影的点击时z轴明显抬高的样式,如果想去掉这种默认样式的话可以在自己项目的AppTheme中修改buttonStyle。

<item name="android:buttonStyle">@style/Widget.AppCompat.Button.Borderless</item>

这里使用的是原生自带的样式,也可用自定义主题继承该主题来做修改。而上述所引用的样式就是您自己编写的自定义。

前言

Android中的样式是通过设置background来进行修改的,而background所设置的参数是一个drawable对象。在初学Android的时候都会了解到如果你想一个button带有圆角而且带有一些不一样的点击效果,也就是所谓的自定义样式。这时候你就需要通过新建一个drawable的xml文件,通过设置相应的selector、shape去更改button的样式。但是实际情况往往会复杂很多,首先一款app不可能每一个按钮的样式都是相同的,如果按照一个样式我需要编写1到2个xml(譬如为了代码的复用会将shape的xml独立编写等类似情况),那么app当中如果出现了有多个类似样式的button(简单来说可能会出现控件的形状都是一直的,但是颜色却不一致)。按照上述情况的话,理论上我们可能需要重复写很多类似的drawable文件,偏偏Android并没有类似css的background-color属性。说到这,就让我想起了自定义控件,用自定义控件、属性来封装一个上述这种坑爹的button。
(其实在Android较新的support library中有一个新控件叫MaterialButton,Google出的这个控件其实也是跟上述类似的。但考虑到各大Android系统的兼容性问题,还是自己封装一个吧)。


用java代码动态设置selector

首先我用的方法是将drawable对象的设置交给java代码来实现,这里就要先简单理解selector如何被解析成java代码的。以下内容我是看一个csdn文章的。地址如下:
请输入链接描述
首先以一个selector的xml为例:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
 <item
     android:state_pressed="false"
     android:drawable="@drawable/normal">
 </item>
 <item
     android:state_pressed="true"
     android:drawable="@drawable/selected">
 </item>
</selector>

selector文件会被解析成一个叫StateListDrawable类的对象,该类中包含一个StateListState类的对象。它是StateListDrawable的静态内部类。在StateListState对象里面有一个mStateSets的二维数组,StateListState的父类DrawableContainerState里面有一个mDrawables的图片数组。
这里简单理解就是StateSet主要就是用于表示控件的状态,譬如上述代码的state_pressed或者state_fouced等。而mDrawables存放的就是我们编写好的样式譬如@drawable/normal。
首先得到第一个item,然后把item里面的状态放进一个一维数组stateSet,把drawable对应的图片也解析出来,接着就把这个一维数组stateSet放入mStateSets的二维数组的第0项,把drawable图片放入mDrawables数组的第0项,这样这个状态集合与图片资源通过下标进行了对应。接下来的item同理。
上述就是整个selector解析成java代码的过程,而接下来要说的就是通过java代码直接设置这个StateListDrawable对象。
直接上代码:

    StateListDrawable sd = new StateListDrawable();
    int pressed = android.R.attr.state_pressed;
    int window_focused = android.R.attr.state_window_focused;
    int focused = android.R.attr.state_focused;
    int selected = android.R.attr.state_selected;
    sd.addState(new int[] { pressed, window_focused }, gd2);
    sd.addState(new int[] { pressed, -focused }, gd2);
    sd.addState(new int[] { selected }, gd2);
    sd.addState(new int[] { focused }, gd2);
    sd.addState(new int[] {}, gd);

上述的pressed、window_focused等值是Android view中的状态常量。和state_pressed等状态是一个含义的。可参考请输入链接描述
gd和gd2两个对象是GradientDrawable对象下面会讲到。
-focused的意思与state_focused=false等同。
上述代码所完成的效果其实就与xml中设置selector的每个item的效果相同。最终所得出的StateListDrawable对象就和解析selector的效果等同了。设置完成后就可将该StateListDrawable对象利用view的setBackground方法设置view的样式。


利用GradientDrawable代替drawable文件的设置

上面说到的gd和gd2对象是GradientDrawable对象。通常我们在设置selector文件的时候引用的drawable文件都会是shape文件。shape中我们可以设置如圆角、颜色等属性。而在java代码中我们可以通过GradientDrawable来实现这一操作。

GradientDrawable gd = new GradientDrawable();
gd.setCornerRadii(floats); 
gd.setColor(backgroundColor);
GradientDrawable gd2 = new GradientDrawable();
gd2.setCornerRadii(floats);
gd2.setColor(getPressColor());
  • setCornerRadii方法是用于设置圆角的,它的参数是传入一个float类型数组,这里的floats其实是一个数组名即:

    float[] floats = new float[] { rectRadius, rectRadius,rectRadius, rectRadius, rectRadius, rectRadius, 
    rectRadius, rectRadius };

    该数组有8个元素,可以理解为一个长方形的控件4个角每个角需要两个圆角半径去实现一个圆角。

  • setColor方法顾名思义就是设置颜色的。

上述的gd、gd2简单理解就是一个是normal状态、另一个是获得了焦点focused或者被挤压pressed状态。这样设置就相当于完成了两个简单的shape文件。


设置被按压效果的颜色

最后还有一个比较强迫症的问题是,如果每次都需要设置按压的颜色未免有点麻烦,这里我想到的方式是在代码中自己调整颜色的透明度(这个先留一个坑,颜色可能不单单只有透明度需要调整!!!)以下是我通过复制Color类源码写的,Color类内置有分析rgb数值的方法,但是这些方法貌似需要api26才能使用,所以我就直接复制出来使用了。以下是这部分的代码:

 /**
 * 设置按压后的颜色
 * @return
 */
private int getPressColor(){
    float r = ((backgroundColor >> 16) & 0xff) / 255.0f;
    float g = ((backgroundColor >>  8) & 0xff) / 255.0f;
    float b = ((backgroundColor      ) & 0xff) / 255.0f;

    int color=((int) (0.3f * 255.0f + 0.5f) << 24) |
            ((int) (r   * 255.0f + 0.5f) << 16) |
            ((int) (g * 255.0f + 0.5f) <<  8) |
            (int) (b  * 255.0f + 0.5f);
    return color;
}

这里我只是简单的将正常状态的颜色算出它的rgb数组再将透明度进行修改重新生成按压效果的颜色。这个颜色的调整还有待修改,留个小坑。
至此,整个自定义的Button简单版就这样完成了。