简化View的背景定义

在Android开发过程中,设置View的背景是一种常见的需求,通常我们会使用xml文件来View定义。但是这种方式定义起来太繁琐,为了简化这一流程,我们通过JAVA代码来定义View的背景。

背景

在Android开发过程中,设置View的背景是一种常见的需求,通常我们会使用xml文件来View定义。

比如定义一个圆角矩形的背景 rectange_white_6_bg.xml:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">

    <solid android:color="@color/white"/>

    <stroke  android:color="@color/x1" android:width="1px"/>

    <corners  android:radius="6dp"/>

</shape>

然后在View的background属性中引用即可:

android:background="@drawable/rectange_white_6_bg.xml"

这似乎也没什么问题,也就一个文件,简单几行代码而已,但是如果需要背景随着View的状态变化呢?比如这样的需求: 按钮正常显示时为白色,按下时为灰色。 这是开发中会经常遇到的需求吧。如果不考虑背景的形状,只是考虑颜色,一个文件也可以实现:

<selector xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:state_pressed="false" android:color="@color/normal_color"/>

    <item android:state_pressed="true" android:color="@color/pressed_color"/>

</selector>

但是如果需要考虑背景形状呢?那就不一样了,实现它需要定义3个背景资源才可实现:

button_normal_bg.xml:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">

    <solid android:color="@color/white"> </solid>

    <stroke  android:color="@color/x1" android:width="1px"></stroke>

    <corners  android:radius="6dp"/>

</shape>

button_pressed_bg.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">

    <solid android:color="@color/gray"> </solid>

    <stroke  android:color="@color/x1" android:width="1px"></stroke>

    <corners  android:radius="6dp"/>

</shape>

button_bg.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/button_normal_bg"/>

    <item android:state_pressed="true" android:drawable="@drawable/button_pressed_bg"/>

</selector>

卧槽,简直不能忍,就为了实现一个背景,需要定义3个文件,那定义个5,6种按钮就十几个文件了,你可能会说复用,我只想说呵呵,UI的眼睛贼细,差一个像素都得重新定义。而随着版本的不断迭代,UI人员的流动,UI标准的变化,定义的这种资源文件会越来越多。

解决方案

既然使用xml定义会带来这种问题,那么可以尝试用JAVA代码来实现,我们知道,View的背景实际上就是一个Drawable, 而Android提供了多个Drawable供开发者使用,下面我们就通过Drawable来实现View背景的定义。

需求点

  • 可设置背景的颜色(纯色/渐变);
  • 可设置背景的形状;
  • 可设置背景的边框宽度和颜色;
  • 可设置背景的圆角;
  • 可根据状态加载不同的背景;
  • 可设置资源文件为背景;

实现

通过对各种Drawable API的了解,发现GradientDrawable和StateListDrawable能够实现我们的需求:

/**
 * 适用于有边框且背景为纯色 -> 具有相同圆角的场景
 * @param view
 * @param shape
 *      public static final int RECTANGLE = 0;  //矩形
        public static final int OVAL = 1;       // 圆形
        public static final int LINE = 2;       // 线条
        public static final int RING = 3;       // 圆环
 * @param bgColor
 * @param borderColor
 * @param borderWidth
 * @param radius
 */
public static void setShapeBg(View view, int shape, int bgColor, int borderColor,
                              int borderWidth, int radius) {
    GradientDrawable drawable = new GradientDrawable();
    drawable.setShape(shape);
    drawable.setColor(bgColor);
    drawable.setStroke(borderWidth, borderColor);
    drawable.setCornerRadius(radius);
    view.setBackground(drawable);
}

背景颜色,形状,边框颜色,边框宽度,圆角都实现了,但是我们发现圆角是个int值,也就是说,背景的4个角的圆角要么不设置,要么都得设置,这有点局限。随后我们发现GradientDrawable有个setCornerRadii方法接收float[]类型的参数,那实现不同的圆角就靠它了。

/**
 * 适用于有边框且背景为纯色 -> 圆角有差异的场景
 * @param view
 * @param shape
 * @param bgColor
 * @param borderColor
 * @param borderWidth
 * @param radius
 */
public static void setShapeBg(View view, int shape, int bgColor, int borderColor,
                              int borderWidth, float[] radius) {
    GradientDrawable drawable = new GradientDrawable();
    drawable.setShape(shape);
    drawable.setColor(bgColor);
    drawable.setStroke(borderWidth, borderColor);
    drawable.setCornerRadii(radius);
    view.setBackground(drawable);
}

==注意==: 这里的radius必须为8维数组,每2组数代表一个圆角,依次为左上角,右上角,右下角,左下角。例如:左上角和右下角为10的圆角定义为:new float[]{10, 10, 0, 0, 10, 10, 0, 0};

为了满足不同的需求,后面的实现我们会提供固定radius和可变radius两种版本。

我们知道GradientDrawable本身就是渐变的Drawable, 如果设置的颜色为一种颜色,就是纯色,如果为多种颜色,就可实现渐变,这里我们针对渐变单独定义一组方法,可实现渐变的方向和颜色:

/**
 * 适用于渐变背景 -> 具有相同圆角的场景
 * @param view
 * @param shape
 * @param orientation
 * @param bgColor
 * @param radius
 */
public static void setShapeBg(View view, int shape, GradientDrawable.Orientation orientation,
                              int[] bgColor, int borderColor, int borderWidth, int radius) {
    GradientDrawable drawable = new GradientDrawable(orientation, bgColor);
    drawable.setShape(shape);
    drawable.setStroke(borderWidth, borderColor);
    drawable.setCornerRadius(radius);
    view.setBackground(drawable);
}

/**
 * 适用于渐变背景 -> 圆角有差异的场景
 * @param view
 * @param shape
 * @param orientation
 * @param bgColor
 * @param radius
 */
public static void setShapeBg(View view, int shape, GradientDrawable.Orientation orientation,
                              int[] bgColor, int borderColor, int borderWidth, float[] radius) {
    GradientDrawable drawable = new GradientDrawable(orientation, bgColor);
    drawable.setShape(shape);
    drawable.setStroke(borderWidth, borderColor);
    drawable.setCornerRadii(radius);
    view.setBackground(drawable);
}

以上的方法实现的只是一个固定的背景,无法根据View的状态进行动态改变。下面我们通过StateListDrawable来实现这种动态改变的背景。Androidt提供的StateListDrawable其实就对应xml文件中的元素,那么只要添加不同的状态即可实现:

/**
 * 根据View的状态加载背景 -> 具有相同圆角的场景
 * @param view
 * @param state View的状态
 * @param shape
 * @param bgColor
 * @param borderColor
 * @param borderWidth
 * @param radius
 */
public static void setSelectorBg(View view, int state, int shape, int[] bgColor, int[] borderColor,
                              int borderWidth, int radius) {
    if (bgColor == null || bgColor.length < 2) {
        throw new IllegalArgumentException();
    }
    int normalBorderColor = 0;
    int stateBorderColor = 0;
    if (borderColor != null && borderColor.length == 2) {
        normalBorderColor = borderColor[0];
        stateBorderColor = borderColor[1];
    }
    StateListDrawable drawable = new StateListDrawable();
    drawable.addState(new int[] {state}, getDrawable(shape, bgColor[1], stateBorderColor, borderWidth, radius));
    drawable.addState(new int[] {}, getDrawable(shape, bgColor[0], normalBorderColor, borderWidth, radius));
    view.setBackground(drawable);
}

/**
 * 根据View的状态加载背景 -> 圆角不同的场景
 * @param view
 * @param state View的状态
 * @param shape
 * @param bgColor
 * @param borderColor
 * @param borderWidth
 * @param radius
 */
public static void setSelectorBg(View view, int state, int shape, int[] bgColor, int[] borderColor,
                                 int borderWidth, float[] radius) {
    if (bgColor == null || bgColor.length < 2) {
        throw new IllegalArgumentException();
    }
    int normalBorderColor = 0;
    int stateBorderColor = 0;
    if (borderColor != null && borderColor.length == 2) {
        normalBorderColor = borderColor[0];
        stateBorderColor = borderColor[1];
    }
    StateListDrawable drawable = new StateListDrawable();
    drawable.addState(new int[] {state}, getDrawable(shape, bgColor[1], stateBorderColor, borderWidth, radius));
    drawable.addState(new int[] {}, getDrawable(shape, bgColor[0], normalBorderColor, borderWidth, radius));
    view.setBackground(drawable);
}

==注意==:

  1. bgColor[0]是状态值为false时的颜色,也就是默认的颜色,bgColor[0]是状态值为true时的颜色;
  2. int[] {}或int[] {-state}表示默认状态;
  3. state必须使用Android提供的属性:

    public static final int state_above_anchor = 16842922;
    public static final int state_accelerated = 16843547;
    public static final int state_activated = 16843518;
    public static final int state_active = 16842914;
    public static final int state_checkable = 16842911;
    public static final int state_checked = 16842912;
    public static final int state_drag_can_accept = 16843624;
    public static final int state_drag_hovered = 16843625;
    public static final int state_empty = 16842921;
    public static final int state_enabled = 16842910;
    public static final int state_expanded = 16842920;
    public static final int state_first = 16842916;
    public static final int state_focused = 16842908;
    public static final int state_hovered = 16843623;
    public static final int state_last = 16842918;
    public static final int state_long_pressable = 16843324;
    public static final int state_middle = 16842917;
    public static final int state_multiline = 16843597;
    public static final int state_pressed = 16842919;
    public static final int state_selected = 16842913;
    public static final int state_single = 16842915;
    public static final int state_window_focused = 16842909;

上面代码中的getDrawable(…)其实就是从上面的setShapeBg中提取的代码:

/**
 * 获取构造的背景 -> 具有相同圆角的场景
 * @param shape
 * @param bgColor
 * @param borderColor
 * @param borderWidth
 * @param radius
 * @return
 */
public static Drawable getDrawable(int shape, int bgColor, int borderColor,
                                   int borderWidth, int radius) {
    GradientDrawable drawable = new GradientDrawable();
    drawable.setShape(shape);
    drawable.setColor(bgColor);
    drawable.setStroke(borderWidth, borderColor);
    drawable.setCornerRadius(radius);
    return drawable;
}

/**
 * 获取构造的背景 -> 圆角有差异的场景
 * @param shape
 * @param bgColor
 * @param borderColor
 * @param borderWidth
 * @param radius
 * @return
 */
public static Drawable getDrawable(int shape, int bgColor, int borderColor,
                                   int borderWidth, float[] radius) {
    GradientDrawable drawable = new GradientDrawable();
    drawable.setShape(shape);
    drawable.setColor(bgColor);
    drawable.setStroke(borderWidth, borderColor);
    drawable.setCornerRadii(radius);
    return drawable;
}

上面实现的方法都是通过颜色来设置背景的,有时候我们还会使用图片等drawable资源来设置背景。

/**
 * 根据View状态设置不同的背景
 * @param context
 * @param view
 * @param state
 * @param resId 可为图片,drawable资源
 */
public static void setSelectorBg(Context context, View view, int state, int[] resId) {
    if (resId == null || resId.length < 2) {
        throw new IllegalArgumentException();
    }
    if (view.getContext() != null && view.getContext().getResources() != null) {
        StateListDrawable drawable = new StateListDrawable();
        drawable.addState(new int[]{state}, view.getContext().getResources().getDrawable(resId[1]));
        drawable.addState(new int[]{}, view.getContext().getResources().getDrawable(resId[0]));
        view.setBackground(drawable);
    }
}

测试

下面我们写一段代码来测试这几种设置背景的方法:

int r = 15;
int normalBgColor = Color.parseColor("#5df2d6");
int normalBorderColor = Color.parseColor("#00bfa5");
int pressedBgColor = Color.parseColor("#00bfa5");
int pressedBorderColor = Color.parseColor("#008e76");

float[] radius = new float[] {r, r, r, r, 0, 0, 0, 0};
ViewBgUtils.setShapeBg(containerLayout.getChildAt(0), GradientDrawable.RECTANGLE, normalBgColor, 15);
ViewBgUtils.setShapeBg(containerLayout.getChildAt(1), GradientDrawable.RECTANGLE, normalBgColor, radius);
ViewBgUtils.setShapeBg(containerLayout.getChildAt(2), GradientDrawable.RECTANGLE, normalBgColor, normalBorderColor, 4, 15);
ViewBgUtils.setShapeBg(containerLayout.getChildAt(3), GradientDrawable.RECTANGLE, normalBgColor, normalBorderColor, 4, radius);
ViewBgUtils.setSelectorBg(containerLayout.getChildAt(4), android.R.attr.state_pressed, GradientDrawable.RECTANGLE,
        new int[] {normalBgColor, pressedBgColor}, 10);
ViewBgUtils.setSelectorBg(containerLayout.getChildAt(5), android.R.attr.state_pressed, GradientDrawable.RECTANGLE,
        new int[] {normalBgColor, pressedBgColor}, radius);
ViewBgUtils.setSelectorBg(containerLayout.getChildAt(6), android.R.attr.state_pressed, GradientDrawable.RECTANGLE,
        new int[] {normalBgColor, pressedBgColor}, new int[] {normalBorderColor, pressedBorderColor}, 4, 15);
ViewBgUtils.setSelectorBg(containerLayout.getChildAt(7), android.R.attr.state_pressed, GradientDrawable.RECTANGLE,
        new int[] {normalBgColor, pressedBgColor},new int[] {normalBorderColor, pressedBorderColor}, 4, radius);

运行结果:

image

PS: 因为前4个按钮和后四个按钮的显示效果是一样的,只有点击时才有区别,所以放了前4个按钮的截图。

源码下载

ViewBgUtils.java

总结

通过使用这种方法我们只需要设置相应的参数,而不用去定义背景资源文件。大大简化了View背景的定义过程。
PS: 手贱的可以star。。。

,