简单好用的阴影库 ShadowLayout

在开发过程中常会遇见带阴影效果的控件,通过 SDK 提供的 CardViewandroid:elevation 可以实现,也可以通过 .9 图实现。但是使用这两种方法会有一些弊端,比如:不可以控制阴影颜色,如果使用 .9 图片过多,会增加 APK 安装文件的体积。针对以上问题,自己写了一个为控件添加阴影的库 —- ShadowLayout。接下来就 ShadowLayout 展开本文,本文主要分为以下两个部分:

  1. 关于 ShadowLayout 的使用;
  2. 关于 ShadowLayout 的原理。

关于 ShadowLayout 的使用

先来看一张使用 ShadowLayout 库实现的各种阴影的效果图,如下图所示:



如上图所示,通过使用 ShadowLayout 可以控制阴影的颜色、范围、显示边界(上下左右四个边界)、x 轴和 y 轴的偏移量。

添加依赖

Gradle:

1
compile 'com.lijiankun24:shadowlayout:1.0.0'

Maven:

1
2
3
4
5
6
<dependency>
<groupId>com.lijiankun24</groupId>
<artifactId>shadowlayout</artifactId>
<version>1.0.0</version>
<type>pom</type>
</dependency>

如何使用

在 xml 中添加如下布局文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<com.lijiankun24.shadowlayout.ShadowLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="30dp"
app:shadowColor="#66000000"
app:shadowDx="0dp"
app:shadowDy="3dp"
app:shadowRadius="10dp"
app:shadowSide="all">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:contentDescription="@null"
android:src="@mipmap/ic_launcher"/>
</com.lijiankun24.shadowlayout.ShadowLayout>

上面 xml 布局文件实现的效果如下图所示:



如上面 xml 中代码显示的那样,总共有 5 个自定义属性,其含义分别如下:

  • app:shadowColor="#66000000" 控制阴影的颜色,注意:颜色必须带有透明度的值
  • app:shadowDx="0dp" 控制阴影 x 轴的偏移量
  • app:shadowDy="3dp" 控制阴影 y 轴的偏移量
  • app:shadowRadius="10dp" 控制阴影的范围
  • app:shadowSide="all|left|right|top|bottom" 控制阴影显示的边界,共有五个值

关于 ShadowLayout 的原理

ShadowLayout 的原理其实非常简单,大概可以分为以下几步:

  1. 通过自定义属性获取阴影的相关属性,包括:阴影颜色、阴影范围大小、阴影显示边界、阴影 x 轴和 y 轴的偏移量;
  2. onLayout() 方法中获取到阴影应该显示的范围,并设置此 ShadowLayoutPadding 值以给阴影的显示留出空间;
  3. onDraw() 方法中使用 CanvasPaint 的方法绘制阴影。

    绘制阴影最重要的两个方法:

    • Paint.setShadowLayer(float radius, float dx, float dy, int shadowColor) 设置阴影的大小、颜色、x 轴和 y 轴的偏移量
    • canvas.drawRect(RectF rect, Paint paint) 设置阴影显示的位置

ShadowLayout 库中只有一个文件 —- ShadowLayout.javaShadowLayoutRelativeLayout 的子类,其源码如下所示(有较为详细的注释):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
/**
* ShadowLayout.java
* <p>
* Created by lijiankun on 17/8/11.
*/
public class ShadowLayout extends RelativeLayout {
public static final int ALL = 0x1111;
public static final int LEFT = 0x0001;
public static final int TOP = 0x0010;
public static final int RIGHT = 0x0100;
public static final int BOTTOM = 0x1000;
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private RectF mRectF = new RectF();
/**
* 阴影的颜色
*/
private int mShadowColor = Color.TRANSPARENT;
/**
* 阴影的大小范围
*/
private float mShadowRadius = 0;
/**
* 阴影 x 轴的偏移量
*/
private float mShadowDx = 0;
/**
* 阴影 y 轴的偏移量
*/
private float mShadowDy = 0;
/**
* 阴影显示的边界
*/
private int mShadowSide = ALL;
public ShadowLayout(Context context) {
this(context, null);
}
public ShadowLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ShadowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs);
}
/**
* 获取绘制阴影的位置,并为 ShadowLayout 设置 Padding 以为显示阴影留出空间
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
float effect = mShadowRadius + dip2px(5);
float rectLeft = 0;
float rectTop = 0;
float rectRight = this.getWidth();
float rectBottom = this.getHeight();
int paddingLeft = 0;
int paddingTop = 0;
int paddingRight = 0;
int paddingBottom = 0;
if (((mShadowSide & LEFT) == LEFT)) {
rectLeft = effect;
paddingLeft = (int) effect;
}
if (((mShadowSide & TOP) == TOP)) {
rectTop = effect;
paddingTop = (int) effect;
}
if (((mShadowSide & RIGHT) == RIGHT)) {
rectRight = this.getWidth() - effect;
paddingRight = (int) effect;
}
if (((mShadowSide & BOTTOM) == BOTTOM)) {
rectBottom = this.getHeight() - effect;
paddingBottom = (int) effect;
}
if (mShadowDy != 0.0f) {
rectBottom = rectBottom - mShadowDy;
paddingBottom = paddingBottom + (int) mShadowDy;
}
if (mShadowDx != 0.0f) {
rectRight = rectRight - mShadowDx;
paddingRight = paddingRight + (int) mShadowDx;
}
mRectF.left = rectLeft;
mRectF.top = rectTop;
mRectF.right = rectRight;
mRectF.bottom = rectBottom;
this.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
}
/**
* 真正绘制阴影的方法
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(mRectF, mPaint);
}
/**
* 读取设置的阴影的属性
*
* @param attrs 从其中获取设置的值
*/
private void init(AttributeSet attrs) {
setLayerType(View.LAYER_TYPE_SOFTWARE, null); // 关闭硬件加速
this.setWillNotDraw(false); // 调用此方法后,才会执行 onDraw(Canvas) 方法
TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.ShadowLayout);
if (typedArray != null) {
mShadowColor = typedArray.getColor(R.styleable.ShadowLayout_shadowColor,
ContextCompat.getColor(getContext(), android.R.color.black));
mShadowRadius = typedArray.getDimension(R.styleable.ShadowLayout_shadowRadius, dip2px(0));
mShadowDx = typedArray.getDimension(R.styleable.ShadowLayout_shadowDx, dip2px(0));
mShadowDy = typedArray.getDimension(R.styleable.ShadowLayout_shadowDy, dip2px(0));
mShadowSide = typedArray.getInt(R.styleable.ShadowLayout_shadowSide, ALL);
typedArray.recycle();
}
mPaint.setAntiAlias(true);
mPaint.setColor(Color.TRANSPARENT);
mPaint.setShadowLayer(mShadowRadius, mShadowDx, mShadowDy, mShadowColor);
}
/**
* dip2px dp 值转 px 值
*
* @param dpValue dp 值
* @return px 值
*/
private float dip2px(float dpValue) {
DisplayMetrics dm = getContext().getResources().getDisplayMetrics();
float scale = dm.density;
return (dpValue * scale + 0.5F);
}
}


至此,关于 ShadowLayout 库的使用方法和原理至此全部介绍完毕,库在 GitHub 上 ShadowLayout,欢迎 star 和 fork,也欢迎通过下面二维码下载 APK 体验,如果有什么问题欢迎指出。我的工作邮箱:jiankunli24@gmail.com


参考资料:

Android 视图高度和阴影的那点事儿亦枫

ShadowViewHelper天天_byconan