android使用代码生成LayerDrawable的方法、源码分析和注意事项

时间:2023-01-07 00:33:36

前言

在实现Button、TextView等控件的点击效果时,为了有更好的UI体验,一般我们会把控件的背景设置上阴影。传统的做法是美工提供两张具有阴影效果的nine patch图,然后在drawable资源文件夹下添加具有不同状态的selector文件,并使控件的background属性引用此drawable资源。这种做法没有问题,不过由于依赖多张图片,因而缺乏灵活性且会明显增大apk文件。


android使用代码生成LayerDrawable的方法、源码分析和注意事项
图1.使用代码生成的具有“阴影”效果的控件

暂时不讲怎么生成的具有阴影效果的图片,首先分析一下如何通过xml定义LayerDrawable

xml构造LayerDrawable

在android中,每一种在xml文件中定义的图片,均可以使用java代码生成,其中LayerDrawable对应的xml文件的根元素为<layer-list>。

使用xml定义layer-list


android使用代码生成LayerDrawable的方法、源码分析和注意事项
图2.使用xml定义layer-list背景图片

如图2,三个Button均使用layer-list作为背景,具体为:

Button1:

1.底部drawable没有设置padding
2.顶部drawable没有设置padding
3.顶部drawable设置inset为5px

android:left="5px"
android:top="5px"
android:bottom="5px"
android:right="5px"

Button1背景的完整xml:

<?xml version= "1.0" encoding ="utf-8"?> <layer-list xmlns:android="http://schemas.android.com/apk/res/android" > <item > <!-- 底部绿色的drawable,其drawable类型为ShapeDrawable --> <shape android:shape="rectangle" > <solid android:color="#ff00ff00" /> <corners android:radius="3dp" /> </shape> </item > <item  <!-- 顶部红色的drawable,其相对底部绿色drawableinset值均为5px,体现在UI上即为红色圆角矩形在绿色圆角矩形以内,两者边框相距5px--> android:left="5px" android:top="5px" android:bottom="5px" android:right="5px" > <shape android:shape="rectangle" > <solid android:color="#ffff0000" /> <corners android:radius="3dp" /> </shape> </item > </layer-list>

Button2:

1.底部drawable没有设置padding
2.顶部drawable设置inset均为5px

android:left="5px"
android:top="5px"
android:bottom="5px"
android:right="5px"

3.顶部drawable设置padding均为50dp

<padding
    android:bottom="50dp"
    android:left="50dp"
    android:right="50dp"
    android:top="50dp" />

Button2背景的完整xml:

<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
    <item >
        <shape android:shape="rectangle" >
            <solid android:color="#ff00ff00" />
            <corners android:radius="3dp" />
        </shape>
    </item >
    <item  android:left="5px" android:top="5px" android:bottom="5px" android:right="5px" >
        <shape android:shape="rectangle" >
            <solid android:color="#ffff0000" />
            <corners android:radius="3dp" />
            <!-- 顶部红色的drawable的padding值,它决定了控件的内容的padding值,如果控件还有padding,则最终padding取两者相加-->
            <padding  android:bottom="50dp" android:left="50dp" android:right="50dp" android:top="50dp" />
        </shape>
    </item >
</layer-list>

Button3:

1.底部drawable设置padding均为20dp
2.顶部drawable没有设置inset
3.顶部drawable设置padding均为50dp

<padding
    android:bottom="50dp"
    android:left="50dp"
    android:right="50dp"
    android:top="50dp" />

Button3背景的完整xml:

<?xml version= "1.0" encoding ="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
    <item>
        <shape android:shape="rectangle" >
            <solid android:color="#ff00ff00" />
            <corners android:radius="3dp" />
<!-- 底部绿色的drawable的padding值,它决定了下一个drawable相对此drawable的padding值-->
            <padding  android:bottom="20dp" android:left="20dp" android:right="20dp" android:top="20dp" />
        </shape>
    </item>
    <item>
        <shape android:shape="rectangle" >
            <solid android:color="#ffff0000" />
            <corners android:radius="3dp" />
            <padding  android:bottom="50dp" android:left="50dp" android:right="50dp" android:top="50dp" />
        </shape>
    </item>
</layer-list>

应用

开篇提到的生成具有阴影效果的button背景图,应该已经有思路了,即,首先定义多个分别具有两个item的layer-list作为背景,每一个layer-list中先声明的item应该具有inset属性,同时这两个item的颜色应该不同;然后定义一个StateListDrawable(等同于在xml中定义selector),这个StateListDrawable引用多种状态(如按下、正常)的LayerDrawable;最后将此StateListDrawable设置为控件的背景。

首先我介绍一下使用xml文件生成“阴影”背景效果图片:

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

    <item >
        <shape android:shape="rectangle" >
            <solid android:color="#ffbbbbbb" />

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

        </shape>
    </item >

    <item  android:bottom="1px" android:right="1px" >
        <shape android:shape="rectangle" >
            <solid android:color="#ffdddddd" />

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

            <padding  android:bottom="10dp" android:left="10dp" android:right="10dp" android:top="10dp" />
        </shape>
    </item >

</layer-list>

解析:
1. shape元素生成ShapeDrawable对象,不过需要注意的是,xml中虽然指明生成”rectangle”类型的对象,但如果想要在java中生成的rectangle具有圆角,那么java中对应的shape应该是RoundRectShape。
2. solid元素指明背景颜色,且paint的style为fill。
3. 第二个元素android:bottom等代表的是LayerDrawable中第二个drawable相对于第一个drawable的inset,对应的java代码为:

layerDrawable.setLayerInset(1, 0, 0, 1, 1);

源码为:

/** Specify modifiers to the bounds for the drawable[index]. left += l top += t; right -= r; bottom -= b; */
    public void setLayerInset(int index, int l, int t, int r, int b) {
        ChildDrawable childDrawable = mLayerState.mChildren[index];
        childDrawable.mInsetL = l;
        childDrawable.mInsetT = t;
        childDrawable.mInsetR = r;
        childDrawable.mInsetB = b;
    }

可以看出setLayerInset()函数的作用就是将某层(层数从0开始计数)相对于上一层进行向里偏移。当然如果传入的数值为负数,就是向外偏移了,不过这时上层就遮挡住下层了,失去了使用layer的意义了。

4.padding的作用同样非常重要:
(1)当在最上层使用padding时,它指明的是最上层的drawable边缘与内容之间的padding;
(2)当在非最上层使用padding时,它指明当前层与上层之间的padding。

Java代码生成

private void setLayerBg(View view){

        int radius0 = 10;
        float[] outerR = new float[] { radius0, radius0, radius0, radius0, radius0, radius0, radius0, radius0 };
        RoundRectShape roundRectShape0 = new RoundRectShape(outerR, null, null);

        int radius1 = 10;
        float[] outerR1 = new float[] { radius1, radius1, radius1, radius1, radius1, radius1, radius1, radius1 };
        RoundRectShape roundRectShape1 = new RoundRectShape(outerR1, null, null);

        ShapeDrawable shapeDrawableBg = new ShapeDrawable();

        shapeDrawableBg.setPadding(0, 0, 0, 0);
        shapeDrawableBg.setShape(roundRectShape0);

        shapeDrawableBg.getPaint().setStyle(Paint.Style.FILL);
        shapeDrawableBg.getPaint().setColor(0xffbbbbbb);


        ShapeDrawable shapeDrawableFg = new ShapeDrawable();

        shapeDrawableFg.setPadding(23, 23, 23, 23);
        shapeDrawableFg.setShape(roundRectShape1);

        shapeDrawableFg.getPaint().setStyle(Paint.Style.FILL);
        shapeDrawableFg.getPaint().setColor(0xffdddddd);

        Drawable[] layers = {shapeDrawableBg, shapeDrawableFg};
        LayerDrawable layerDrawable = new LayerDrawable(layers);
        layerDrawable.setLayerInset(1, 0, 0, 1, 1);

        view.setBackgroundDrawable(layerDrawable);

    }

注释我就不写了,具体的解释见上面的解析。

LayerDrawable和StateListDrawable相结合使用

当我们遇到可点击的控件时,需要给此控件自定义几个不同状态的background,比如按下效果、普通状态效果,这时就需要用到StateListDrawable。

采用LayerDrawable生成的图片具有的只是静态属性,当将不同状态的LayerDrawable添加到一个StateListDrawable中,这样控件不同状态时均选择是否具有阴影效果。

代码如下:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        text  = (TextView)this.findViewById(R.id.text);
        text.setBackgroundDrawable(getStateListDrawable());
        text.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                Toast.makeText(getApplicationContext(), "onclick", Toast.LENGTH_SHORT).show();
            }
        });
   }

    private Drawable getStateListDrawable(){

        StateListDrawable stateListDrawable = new StateListDrawable();

        int[] stateHighlighted = new int[]{android.R.attr.state_pressed};
        Drawable highlightedDrawable = getLayerDrawable(0xffcccccc);
        stateListDrawable.addState(stateHighlighted, highlightedDrawable);

        int[] stateNormal = new int[]{};
        Drawable normalDrawable = getLayerDrawable(0xffdddddd);
        stateListDrawable.addState(stateNormal, normalDrawable);

        return stateListDrawable;
    }

    private Drawable getLayerDrawable(int foregroundColor){

        int radius0 = 10;
        float[] outerR = new float[] { radius0, radius0, radius0, radius0, radius0, radius0, radius0, radius0 };
        RoundRectShape roundRectShape0 = new RoundRectShape(outerR, null, null);

        int radius1 = 10;
        float[] outerR1 = new float[] { radius1, radius1, radius1, radius1, radius1, radius1, radius1, radius1 };
        RoundRectShape roundRectShape1 = new RoundRectShape(outerR1, null, null);

        ShapeDrawable shapeDrawableBg = new ShapeDrawable();
        shapeDrawableBg.setPadding(0, 0, 0, 0);
        shapeDrawableBg.setShape(roundRectShape0);
        shapeDrawableBg.getPaint().setStyle(Paint.Style.FILL);
        shapeDrawableBg.getPaint().setColor(0xffbbbbbb);

        ShapeDrawable shapeDrawableFg = new ShapeDrawable();
        shapeDrawableFg.setPadding(23, 23, 23, 23);
        shapeDrawableFg.setShape(roundRectShape1);
        shapeDrawableFg.getPaint().setStyle(Paint.Style.FILL);
        shapeDrawableFg.getPaint().setColor(foregroundColor);

        Drawable[] layers = {shapeDrawableBg, shapeDrawableFg};
        LayerDrawable layerDrawable = new LayerDrawable(layers);
        layerDrawable.setLayerInset(1, 0, 0, 1, 1);

        return layerDrawable;
    }

需要注意的是:当给View、TextView、ImageView、ViewGroup等类型的默认没有按下事件的控件添加StateListDrawable时,控件需要设置上click事件,否则按下效果不起作用

备注:

这里只是通过两幅颜色单一的drawable错位简单的生成“阴影效果”,后续可以通过shader等效果,生成逐渐淡出的“阴影”效果。