当前位置 博文首页 > 启舰:自定义控件三部曲之绘图篇(十六)——给控件添加阴影效果

    启舰:自定义控件三部曲之绘图篇(十六)——给控件添加阴影效果

    作者:[db:作者] 时间:2021-06-30 15:33

    前言:要么出击,要么出局,命运女神总会眷顾拼劲全力的一方

    ?

    相关文章:

    《Android自定义控件三部曲文章索引》:http://blog.csdn.net/harvic880925/article/details/50995268

    ?

    这节我们将学到如下内容:

    • 传统地给按钮添加阴影的方法
    • 如何给已有控件添加阴影
    • 如何给图片添加阴影

    一、layerlist给按钮添加阴影效果

    ?

    给控件添加阴影有很多方法,但平常我们给按钮添加阴影最常用的就是使用layerlist多层绘图来添加阴影效果,我们先来看一下给按钮添加的阴影效果图:

    ?

    ?

    从效果图中可以明显看出,按钮的外围多了一圈灰色的阴影效果。
    在开始做阴影效果之前,我们先讲解一下有关layerlist的知识。
    在xml中,我们有常用的几个标签:shape、selector、layerlist;

    • shape标签:以前我们讲过,就是利用代码绘制出背景效果,可以定义填充色、描边、圆角、渐变等。不了解的同学可以参考下:《详解shape标签》?
    • selector标签:用于定义在用户不同的动作状态下,使用不同的背景值。有关selector的知识,博主没有讲过,也不打算再讲了,难度不大,自己搜几个帖子就能学会了。
    • layerlist标签:这个标签的主要作用就是将多个图层按照顺序叠起来,做为一个背景图来显示。

    1、layerlist示例:

    ?

    layerlist标签就是模拟Photoshop中图层的概念,把每一张图层按照顺序叠加起来,做为背景图来显示;
    我们先来看一下简单的例子,我们要显示一下两只蜗牛的图片:

    ?

    ?

    它由三张图片组成:
    一张纯蓝色的背景:(blog1_1.png)

    一只黄蜗牛:(blog1_2.png)

    一只土色蜗牛:(blog1_3.png)

    我们先定义一个layerlist的文件(shade.xml)

    ?

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

    ?

    这里分别将上面的三张图片做为item添加给layer-list;效果图就是一开始演示的那样。layer-list使用起来很简单,只需要把每一层设置为其中的item即可。
    有一点需要注意,layer-list标签的Item中不仅可以设置drawable,也可以设置shape、selector,我们下面一一做下尝试:

    ?

    2、layer-list与shape标签

    ?

    编写控件阴影drawable代码
    上面我们使用使用的是layer-list中item的drawable属性来直接引入图片,其实除了drawable属性,item还有另外几个属性:

    ?

    • android:top 顶部的偏移量
    • android:bottom 底部的偏移量
    • android:left 左边的偏移量
    • android:right 右边的偏移量

    这四个偏移量和控件的margin设置差不多,都是外间距的效果。如果不设置偏移量,前面的图层就完全挡住了后面的图层,从而也看不到后面的图层效果了。
    言归正转,先来看看如何在layer-list中使用shape标签:

    ?

    <?xml version="1.0" encoding="utf-8"?>
    <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
        <item>
            <shape>
                <corners android:radius="25dp"/>
                <solid android:color="#E4E4E4"/>
            </shape>
        </item>
        <item android:left="2dp" android:top="2dp"
              android:bottom="2dp" android:right="2dp">
            <shape>
                <corners android:radius="25dp"/>
                <solid android:color="#FFFFFF"/>
            </shape>
        </item>
    </layer-list>

    上面的代码实现的效果是这样的:

    ?

    ?

    ?

    大家看到类似阴影的效果了吧,不错,这段代码就是实现按钮阴影的代码,我们来仔细看一下
    首先,它使用layer-list将两层shape叠加在一起,底部的shape代码为:

    ?

    <item>
        <shape>
            <corners android:radius="25dp"/>
            <solid android:color="#E4E4E4"/>
        </shape>
    </item>

    底部是一个灰色的矩形,它的四个角被圆角化,并且填充为灰色。
    上层绘制的shape对应的代码为:

    ?

    <item android:left="2dp" android:top="2dp"
          android:bottom="2dp" android:right="2dp">
        <shape>
            <corners android:radius="25dp"/>
            <solid android:color="#FFFFFF"/>
        </shape>
    </item>

    它同样绘制的是一个四个角都被圆角化的矩形,但填充颜色是纯白色。为了露出底层的灰色阴影,我们需要给上层的shape加上边距,这也就是item的 android:left=”2dp” android:top=”2dp” android:bottom=”2dp” android:right=”2dp”这四个属性的作用,相当于margin的作用。
    使用阴影drawable
    在写好layer-list以后,我们需要在按钮控件中使用它:

    ?

    <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:padding="10dp"
            android:layout_margin="10dp"
            android:background="@drawable/layer_shape_list"
            android:text="带阴影的按钮"
            android:textColor="#ff0000"/>

    我们来看下效果:

    ?

    ?

    ?

    从效果图中可以看到,我们虽然实现了带阴影的按钮效果,但是在点击时却没有任何状态变化,这对于按钮是完全不能接受的,所以我们需要给按钮添加上状态变化,这就需要用到selector标签了

    3、layer-list与selector标签

    改造方法一:使用layer-list做根结点
    下面我们对上面shape的代码进行改造,当用户手指按下的时候,将前景色改为黄色,代码为:

    ?

    <?xml version="1.0" encoding="utf-8"?>
    <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
        <item>
            <shape>
                <corners android:radius="25dp"/>
                <solid android:color="#E4E4E4"/>
            </shape>
        </item>
        <item android:left="2dp" android:top="2dp"
              android:bottom="2dp" android:right="2dp">
            <selector>
                <item android:state_pressed="true">
                    <shape>
                        <corners android:radius="25dp"/>
                        <solid android:color="#FFFF00"/>
                    </shape>
                </item>
                <item>
                    <shape>
                        <corners android:radius="25dp"/>
                        <solid android:color="#FFFFFF"/>
                    </shape>
                </item>
            </selector>
        </item>
    </layer-list>

    我们先来看一下效果,然后再来看代码

    ?

    这里明显实现了当用户点击时前景变化的功能。下面我们再来讲解下代码
    首先,这里同样是绘制两层layer,第一层,依然是阴影层,代码没动:

    ?

    ?

    <item>
        <shape>
            <corners android:radius="25dp"/>
            <solid android:color="#E4E4E4"/>
        </shape>
    </item>

    在第一层绘制完成以后,当绘制第二层时就出现问题了:

    <item android:left="2dp" android:top="2dp"
          android:bottom="2dp" android:right="2dp">
        <selector>
            <item android:state_pressed="true">
                <shape>
                    <corners android:radius="25dp"/>
                    <solid android:color="#FFFF00"/>
                </shape>
            </item>
            <item>
                <shape>
                    <corners android:radius="25dp"/>
                    <solid android:color="#FFFFFF"/>
                </shape>
            </item>
        </selector>
    </item>

    第二层中,会对当前用户状态做判断,如果用户当前是按下状态,则绘制:

    <item android:state_pressed="true">
        <shape>
            <corners android:radius="25dp"/>
            <solid android:color="#FFFF00"/>
        </shape>
    </item>

    如果是其它状态,则绘制默认图像:

    <item>
        <shape>
            <corners android:radius="25dp"/>
            <solid android:color="#FFFFFF"/>
        </shape>
    </item>

    所以对于layer-list标签,从这里也可以看出来:它的绘制是逐层绘制的,层与层之间是没有任何影响的,每一层可以单独设置selector标签来响应不同的用户操作状态。
    改造方法二:使用selector做根结点
    上面我们使用layer-list来做根结点来绘制出按钮的不同状态响应的效果,对selector、layer-list使用熟悉的同学,应该还可以想到另一种实现方式,使用selector做为根结点来实现响应不同用户操作。
    我们先直接上代码吧:

    ?

    <?xml version="1.0" encoding="utf-8"?>
    <selector xmlns:android="http://schemas.android.com/apk/res/android">
        <item android:state_pressed="true">
            <layer-list>
                <item>
                    <shape>
                        <corners android:radius="25dp"/>
                        <solid android:color="#E4E4E4"/>
                    </shape>
                </item>
                <item android:left="2dp" android:top="2dp"
                      android:bottom="2dp" android:right="2dp">
                    <shape>
                        <corners android:radius="25dp"/>
                        <solid android:color="#FFFF00"/>
                    </shape>
                </item>
            </layer-list>
        </item>
    
        <item>
            <layer-list>
                <item>
                    <shape>
                        <corners android:radius="25dp"/>
                        <solid android:color="#E4E4E4"/>
                    </shape>
                </item>
                <item android:left="2dp" android:top="2dp"
                      android:bottom="2dp" android:right="2dp">
                    <shape>
                        <corners android:radius="25dp"/>
                        <solid android:color="#FFFFFF"/>
                    </shape>
                </item>
            </layer-list>
        </item>
    
    </selector>

    同样我们先来看一下使用代码与效果,然后再来讲解实现原理:
    使用方法,同样是做为background引入:

    <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:padding="10dp"
            android:layout_margin="10dp"
            android:background="@drawable/selector_layer_list"
            android:text="Selector为根的按钮"
            android:textColor="#ff0000"/>

    效果图如下:

    ?

    很明显,实现了与上面layer-list标签为根同样的效果,我们现在来看一下代码原理:
    代码看起来很长,很唬人,其实原理很简单,它就是根据当前不同的状态,绘制不同的图形,当用户是按压状态时,通过layer-list绘制出一下最上层是黄色,底层是灰色的按钮背景图像:

    ?

    <item android:state_pressed="true">
        <layer-list>
            <item>
                <shape>
                    <corners android:radius="25dp"/>
                    <solid android:color="#E4E4E4"/>
                </shape>
            </item>
            <item android:left="2dp" android:top="2dp"
                  android:bottom="2dp" android:right="2dp">
                <shape>
                    <corners android:radius="25dp"/>
                    <solid android:color="#FFFF00"/>
                </shape>
            </item>
        </layer-list>
    </item>

    然后在其它状态时,绘制一个前景色是白色,背景色是灰色的按钮背景图:

    <item>
        <layer-list>
            <item>
                <shape>
                    <corners android:radius="25dp"/>
                    <solid android:color="#E4E4E4"/>
                </shape>
            </item>
            <item android:left="2dp" android:top="2dp"
                  android:bottom="2dp" android:right="2dp">
                <shape>
                    <corners android:radius="25dp"/>
                    <solid android:color="#FFFFFF"/>
                </shape>
            </item>
        </layer-list>
    </item>

    ?

    这部分代码难度不大,就不再讲了。

    ?

    4、存在问题

    由于使用layer-list标签实现的阴影只能做为background引入,所以如果对你是文字时,它的阴影效果就变成了这样:

    ?

    <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="文字的阴影效果"
            android:layout_margin="10dp"
            android:padding="10dp"
            android:background="@drawable/layer_shape_list"/>

    对应效果图为:

    ?

    ?

    ?

    看起来跟按钮一个样 - _ -!!! 很囧有没有,文字的阴影应该是这样的才对:

    所以我们下面就要开始讲解如何实现文字的阴影效果啦,嘿嘿

    二、Paint.setShadowLayer实现阴影效果

    ?

    上面我们讲了利用layer-list只能实现按钮的阴影效果,对于文字和图片都无法实现阴影效果,除了layer-list,我们只能用自定义控件来实现阴影效果了,Paint中有一个专门用来实现阴影效果的函数setShadowLayer,我们先来看看这个函数实现的阴影效果图:

    ?

    ?

    从效果图中可以看出setShadowLayer函数能够实现:

    • 定制阴影模糊程度
    • 定制阴影偏移距离
    • 清除阴影和显示阴影

    1、setShadowLayer构造函数

    看起来setShadowLayer好像能够完成阴影定制的方方面面,我们先来看看它的构造函数:

    ?

    ?

    ?

    ?

    public void setShadowLayer(float radius, float dx, float dy, int color)

    ?

    它参数的意义如下:

    ?

    • float radius:意思是模糊半径,radius越大越模糊,越小越清晰,但是如果radius设置为0,则阴影消失不见;有关清除阴影的问题,下面我们会专门讲。
    • float dx:阴影的横向偏移距离,正值向右偏移,负值向左偏移
    • float dy:阴影的纵向偏移距离,正值向下偏移,负值向上偏移
    • int color:绘制阴影的画笔颜色,即阴影的颜色(对图片阴影无效)

    ?

    我们这里需要着重讲两个点:一个是模糊半径,另一个是绘制阴影的画笔颜色为什么对图片无效:
    模糊半径的具体意义:
    setShadowLayer使用的是高斯模糊算法,高斯模糊的具体算法是:对于正在处理的每一个像素,取周围若干个像素的RGB值并且平均,然后这个平均值就是模糊处理过的像素,如果对图片中的所有像素都这么处理的话,处理完成的图片就会变得模糊。
    取周围像素的半径就是模糊半径.很容易知道,模糊半径越大,所得平均像素与原始像素相差就越大,也就越模糊
    绘制阴影的画笔颜色为什么对图片无效
    从上面的效果图中可以看出,使用setShadowLayer所产生的阴影,对于文字和绘制的图形的阴影都是使用自定义的阴影画笔颜色来画的,而图片的阴影则是直接产生一张相同的图片,仅对阴影图片的边缘进行模糊。
    大家可能会疑问,会什么对图片的处理是生成一张相同的背景图片呢?这是因为为了给图片添加阴影,如果统一使用某一种颜色来做阴影可能会与图片的颜色相差很大,而且不协调,比如某张图片的色彩非常丰富,而阴影如果使用灰色来做,可能就会显得很突兀,所以为了解决这个问题,针对图片的阴影就不再是统一颜色了,而是复制出这张图片,把复制出的图片的边缘进行模糊,做为阴影;但这样又会引起一个问题,就是如果我们想把图片的阴影做成灰色怎么办?使用setShadowLayer自动生成阴影是没办法了,在下篇我们会具体来讲,如何给图片添加指定颜色的阴影。

    注意:这里有一点需要非常注意的是setShadowLayer只有文字绘制阴影支持硬件加速,其它都不支持硬件加速,所以为了方便起见,我们需要在自定义控件中禁用硬件加速。

    ?

    2、示例一:初步使用setShadowLayer

    多说无益,还是直接来个例子比较实在,本次示例效果图如下:

    ?

    ?

    ?

    这里实现了对文本,图形,Image的阴影效果;具体的代码如下:

    ?

    public class ShadowLayerView extends View {
        private Paint mPaint = new Paint();
        private Bitmap mDogBmp;
        public ShadowLayerView(Context context) {
            super(context);
            init();
        }
    
        public ShadowLayerView(Context context, AttributeSet attrs) {
            super(context, attrs);
            init();
        }
    
        public ShadowLayerView(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
            init();
        }
    
        private void init(){
            setLayerType( LAYER_TYPE_SOFTWARE , null);
            mPaint.setColor(Color.GREEN);
            mPaint.setTextSize(25);
            mPaint.setShadowLayer(1, 10, 10, Color.GRAY);
            mDogBmp = BitmapFactory.decodeResource(getResources(),R.drawable.dog);
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
    
            canvas.drawText("启舰大SB",100,100,mPaint);
    
            canvas.drawCircle(200,200,50,mPaint);
    
            canvas.drawBitmap(mDogBmp,null,new Rect(200,300,200+mDogBmp.getWidth(),300+mDogBmp.getHeight()),mPaint);
        }
    }

    代码看起来很长,其实就是自定义了一个控件,在里面画了点东东;我们分别来看下吧
    首先是初始化,在初始化时设置画笔的颜色

    ?

    private void init(){
        setLayerType( LAYER_TYPE_SOFTWARE , null);
        mPaint.setColor(Color.GREEN);
        mPaint.setTextSize(25);
        mPaint.setShadowLayer(1, 10, 10, Color.GRAY);
        mDogBmp = BitmapFactory.decodeResource(getResources(),R.drawable.dog);
    }

    在初始化的时候,就是先禁用硬件加速,然后设置paint的属性,由于我们需要画图片,所以先把要画的图片加载进来。这里需要注意两个颜色:

    ?

    mPaint.setColor(Color.GREEN);
    mPaint.setShadowLayer(1, 10, 10, Color.GRAY);

    mPaint.setColor指的是设置画笔的颜色是绿色,从效果图中也可以看出来画出来的字体和圆形都是绿色的
    而mPaint.setShadowLayer中设置的 Color.GRAY,指的是阴影的颜色,从效果图中也明显可以看出,字体和阴影的颜色都是灰色的。
    然后就是onDraw的绘图部分了,这里就没什么好讲的了,如果从头看到这里的话,canvas的操作应该很熟练了;

    ?

    3、示例二:setShadowLayer各参数意义

    下面我们就来实现一下这部分开篇时的效果,动态添加setShadowLayer中的各个参数,就可以明显看出来它们的作用:
    在上面的代码上面,我们讲setShadowLayer变成了动态设置,代码如下:

    ?

    ?

    public class ShadowLayerView extends View {
        private Paint mPaint = new Paint();
        private Bitmap mDogBmp;
        private int mRadius = 1,mDx = 10,mDy = 10;
        public ShadowLayerView(Context context) {
            super(context);
            init();
        }
    
        public ShadowLayerView(Context context, AttributeSet attrs) {
            super(context, attrs);
            init();
        }
    
        public ShadowLayerView(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
            init();
        }
    
        private void init(){
            setLayerType( LAYER_TYPE_SOFTWARE , null);
            mPaint.setColor(Color.GREEN);
            mPaint.setTextSize(25);
            mDogBmp = BitmapFactory.decodeResource(getResources(),R.drawable.dog);
        }
    
    
        public void changeRadius() {
            mRadius++;
            postInvalidate();
        }
    
        public void changeDx() {
            mDx+=5;
            postInvalidate();
        }
    
        public void changeDy() {
            mDy+=5;
            postInvalidate();
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
    
            mPaint.setShadowLayer(mRadius, mDx, mDy, Color.GRAY);
    
            canvas.drawText("启舰大SB",100,100,mPaint);
    
            canvas.drawCircle(200,200,50,mPaint);
    
            canvas.drawBitmap(mDogBmp,null,new Rect(200,300,200+mDogBmp.getWidth(),300+mDogBmp.getHeight()),mPaint);
        }
    }

    这段代码难度并不大,只是将 mPaint.setShadowLayer中的各参数写成了变量,并向外暴露了几个接口changeRadius()、changeDx()、changeDy();当外部调用这些接口时,增加对应的变量,并且重绘控件;
    由于每次重绘控件都肯定会调用onDraw方法,所以,我们将mPaint.setShadowLayer的设置放到onDraw方法里来,以确保每次重绘时mPaint.setShadowLayer的设置都会被更新。
    在使用时:

    ?

    public class MyActivity extends Activity implements View.OnClickListener{
        private ShadowLayerView mShadowLayerView;
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);
            mShadowLayerView = (ShadowLayerView)findViewById(R.id.shadowlayerview);
            findViewById(R.id.radius_btn).setOnClickListener(this);
            findViewById(R.id.dx_btn).setOnClickListener(this);
            findViewById(R.id.dy_btn).setOnClickListener(this);
        }
    
        @Override
        public void onClick(View v) {
            switch (v.getId()){
                case R.id.radius_btn:
                    mShadowLayerView.changeRadius();
                    break;
                case R.id.dx_btn:
                    mShadowLayerView.changeDx();;
                    break;
                case R.id.dy_btn:
                    mShadowLayerView.changeDy();
                    break;
            }
        }
    }

    使用代码很简单,就不再讲了。
    效果图如下:

    ?

    ?

    ?

    从效果图中可以明显看到各个参数的区别,但正是通过效果图,我们可以明显得看出两个结论:

    • 1、图片的阴影是不受阴影画笔颜色影响的,它是一张图片的副本;
    • 2、无论是图片还是图形,模糊时,仅模糊边界部分,随着模糊半径的增大,向内、向外延伸;其实很好理解这个问题:由于模糊半径的增大,高斯模糊向周边取值的范围在增大,所以向内、向外延伸的距离就会更大

    4、Paint.clearShadowLayer()清除阴影

    上面我们讲解了使用setShadowLayer添加阴影的问题,下面我们再来看看如何清除阴影的。清除阴影其实有两个方法,可以将setShadowLayer的radius的值设为0,也可以使用专门的清除阴影的函数:

    ?

    ?

    ?

    ?

    //Paint系函数:清除ShadowLayer阴影
    public void clearShadowLayer() 

    将setShadowLayer的radius的值设为0来清除阴影的用法,我这里就不再演示了,大家可以自己试试,我们这里尝试下使用clearShadowLayer() 来清除阴影的用法。
    在上面函数的基础上,我们另外添加一个变量来控制当前是否显示阴影:

    ?

    public class ShadowLayerView extends View {
        …………
        private boolean mSetShadow = true;
        …………
        public void clearShadow(){
            mSetShadow = false;
            postInvalidate();
        }
    
        public void showShadow(){
            mSetShadow = true;
            postInvalidate();
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
    
            if (mSetShadow) {
                mPaint.setShadowLayer(mRadius, mDx, mDy, Color.GRAY);
            }else {
                mPaint.clearShadowLayer();
            }
    
            canvas.drawText("启舰大SB",100,100,mPaint);
    
            canvas.drawCircle(200,200,50,mPaint);
    
            canvas.drawBitmap(mDogBmp,null,new Rect(200,300,200+mDogBmp.getWidth(),300+mDogBmp.getHeight()),mPaint);
        }
    }

    修改的代码很简单,增加一个变量mSetShadow来控制当前是否显示阴影,如果需要显示阴影就调用mPaint.setShadowLayer(mRadius, mDx, mDy, Color.GRAY);设置阴影,如果不需要显示阴影就调用mPaint.clearShadowLayer();来清除阴影;
    对于使用btn调用clearShadow()、showShadow()这两个接口的用法,就不再帖代码了,没啥难度,源码里也有;
    效果图如下:

    ?

    ?

    ?

    在目前的所有例子中,我们的定义控件在xml中使用时,layout_widht、layout_height都统一设置成match_parent或者fill_parent来强制全屏;是时间教大家如何使用wrap_content属性,如何让控件自已计算高度了,下篇我们就来看看这个问题。

    源码在文章底部给出

    三、TextView及其派生类使用ShadowLayer添加阴影效果

    ?

    上面我们通过自定义控件来实现了自定义阴影效果,那么问题来了,如果我需要给已有的控件添加阴影效果,实现下面这样的效果:

    ?

    ?

    ?

    1、XML实现

    ?

    从上面可以看到,TextView,Button,EditView中的文字都具有阴影效果。那是怎么实现的呢?难道我们需要在原生控件的甚而上派生一个类在onDraw里使用setShadowLayer来绘制阴影吗?
    答案当然不是,setShadowLayer是API 1 就已经引入的方法,同样,对于TextView和从TextView派生的类都自然具体XML属性来设置阴影。这几个设置阴影的XML属性如下:

    ?

    ?

    ?

    <TextView
           …………
            android:shadowRadius="3"
            android:shadowDx="5"
            android:shadowDy="5"
            android:shadowColor="@android:color/darker_gray"/>

    这几个属性的意义非常容易理解,直接对应setShadowLayer的几个参数setShadowLayer(float radius, float dx, float dy, int color),但这几个属性只有TextVIew及其派生类才会有,其它类是没有的,TextVIew的派生类如下:

    ?

    ?

    ?

    所以一般我们使用的Button和EditText是可以使用Xml来实现阴影的。