Android系统上绘制圆角和阴影的几种姿势

在实际的开发工程中,对视图增加圆角和阴影效果的绘制是比较常见的需求,Android系统提供了一系列的方法以帮助开发者实现基础的视图圆角和阴影效果,但在面对实际的视觉需求时,想要完美达到视觉设计师的设计要求就难免需要了解一些基础的绘图原理和绘图方法才能达到特殊的设计需求,这里就简单对比和总结了常见的圆角和阴影的绘图方法。

圆角

View的圆角背景实现圆角效果

使用原生提供的ShapeDrawable实现背景。

1
2
3
4
5
6
7
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#ff0000" />
<corners android:topLeftRadius="10dp"
android:topRightRadius="10dp"
android:bottomRightRadius="10dp"
android:bottomLeftRadius="10dp"/>
</shape>

Alt text

使用圆角贴图实现圆角效果

在介绍“贴图”之前先说明在Android绘图相关的两个必备知识,分布是Paint Style和Path。

Paint Style

在用画笔(Paint)的时候有三种Style,选择不同的画笔样式时就可达到不同的画笔效果,分别是

  • Paint.Style.STROKE 只绘制图形轮廓(描边)
  • Paint.Style.FILL 只绘制图形内容
  • Paint.Style.FILL_AND_STROKE 既绘制轮廓也绘制内容
    Alt text
Path

当我们在想要绘制一些形状时,Canvas提供了一些基础形状的绘制方法,如圆形、矩形、椭圆等。你只需要选择相应的绘制方法并设置你想要的绘制参数就能绘制出你想要的简单图形效果。但对于那些复杂一点的图形则没法去绘制,如一个心形、正多边形、五角星等,使用Path不仅能够绘制简单图形,也可以绘制这些比较复杂的图形。Path封装了由直线和曲线(二次,三次贝塞尔曲线等)构成的几何路径。你能用Canvas中的drawPath来把这条路径画出来(同样支持Paint的不同绘制模式),也可以用于剪裁画布和根据路径绘制文字。

如何制作圆角贴图

以左上角贴图实现为例,使用Path约束绘图范围

1
2
3
4
5
6
7
8
9
10
11
12
RectF fakeCornerRectF = sRectF;
fakeCornerRectF.set(0, 0, mCornerRoundRadius * 2, mCornerRoundRadius * 2);
// 绘制左上圆角背景
if (mTopLeftCorner) {
fakeCornerRectF.offsetTo(left - mCornerOffset, top - mCornerOffset);
mCompatibilityModePath.rewind();
mCompatibilityModePath.moveTo(left - mCornerOffset, top - mCornerOffset);
mCompatibilityModePath.lineTo(left + mCornerRoundRadius, top - mCornerOffset);
mCompatibilityModePath.arcTo(fakeCornerRectF, START_TOP, -QUARTER_CIRCLE);
mCompatibilityModePath.close();
canvas.drawPath(mCompatibilityModePath, mBackgroundPaint);
}

Alt text

使用Paint.Style.FILL画笔绘制,将贴图效果绘制在ImageView容器的(0,0)坐标上。即可达到想要的圆角效果

对Bitmap的裁剪实现圆角效果

例如使用android support包里的RoundedBitmapDrawable,创建一个被裁剪圆角的BitmapDrawable。

1
2
RoundedBitmapDrawable drawable = RoundedBitmapDrawableFactory.create(mContext.getResources(), bitmap);
drawable.setCornerRadius(40);

对Canvas画板做裁剪实现圆角效果

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
protected void onDraw(Canvas canvas) {
int left = getPaddingLeft();
int top = getPaddingTop();
int right = canvas.getWidth() - getPaddingRight();
int bottom = getHeight() - getPaddingBottom();

mCanvasRect.set(left, top, right, bottom);
mCanvasPath.reset();
mCanvasPath.addRoundRect(mCanvasRect, mRx, mRy, Path.Direction.CW);
canvas.clipPath(mCanvasPath);
super.onDraw(canvas);
}

阴影

elevation属性和translationZ属性

UI 控件的elevation属性可以设置其高度,呈现在界面中的直观效果就是阴影效果,在 xml 布局文件中,通过 android:elevation 属性设置,在 java 代码中通过 View 类提供的setElevation()方法设置。但是这个属性存在版本兼容问题,是 Android 5.0 引进的 API。所以,当 minSdkVersion 值小于21时,系统会在 xml 的对应使用地方给出一个 lint 提示:

Attribute elevation is only used in API level 21 and higher

当然你也可以选择忽略这个提示,或者使用tools:targetApi属性消除这个提示,这样做的话,在低于5.0版本的系统中将不会出现阴影效果。然而,有一个更好的办法做到兼容,那就是借助ViewCompat这个万能的兼容类,使View 的 elevation 属性兼容至低版本中:

ViewCompat.setElevation(View view, float elevation)

注意:尤其要注意,视图的阴影一定是由有轮廓的视图投射出来的。简单来说,就是需要设置控件的背景,即 android:background 属性。我们可以选择图片作为背景,也可以使用 标签定义一个 drawable 形状。

使用.9图实现阴影效果

说到阴影效果最简单最省力的方法莫过于设置一个.9的背景图啦!这里推荐一个站点,可以在线制作.9阴影图。http://inloop.github.io/shadow4android/

使用模糊画笔绘制阴影效果

Alt text

先看想要做到的阴影效果,想要在红色的轮播banner下方显示一条红色的阴影效果。用已知的阴影方案比如设置视图的Z轴高度或者设置.9阴影背景都无法实现这种效果。

可以将实现上图的局部阴影效果的绘制步骤分解成两层:

  1. 自定义一个ShadowLayout容器,在onDraw方法中重写绘制步骤
  2. 如何绘制阴影效果?使用带有BlurMaskFilter效果的画笔在合适的地方绘制一个椭圆阴影。可以理解成先用一个模糊画笔先画眉,再在合适的位置上绘制想要的图片。画眉效果如下图:

Alt text

这里有个前提,需要关闭当前View的硬件加速功能。setLayerType(LAYER_TYPE_SOFTWARE, null)。具体的实现代码如下所示:

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
@Override
protected void onDraw(Canvas canvas) {
drawShadow(canvas);

super.onDraw(canvas);
}

private void drawShadow(Canvas canvas) {
if (shadowView != null && shadowOn) {
canvas.drawOval(getDrawOvalRect(), getShadowPaint());
}
}

private int getShadowColor() {
if (!runPalette) {
return shadowColor;
}
runPalette = false;
if (shadowView instanceof ImageView) {
if (((ImageView) shadowView).getDrawable() instanceof ColorDrawable) {
shadowColor = getDarkerColor(((ColorDrawable) ((ImageView) shadowView).getDrawable()).getColor());
} else if (((ImageView) shadowView).getDrawable() instanceof BitmapDrawable) {
Bitmap bitmap = ((BitmapDrawable) ((ImageView) shadowView).getDrawable()).getBitmap();
Palette.Swatch mSwatch = Palette.from(bitmap).generate().getDominantSwatch();

if (null != mSwatch) {
int rgb = mSwatch.getRgb();
shadowColor = 0x4C000000 | (Color.red(rgb) << 16) | (Color.green(rgb) << 8) | Color.blue(rgb);
}
} else {
shadowColor = Color.TRANSPARENT;
}
}

return shadowColor;
}

private Paint getShadowPaint() {
if (shadowView != null) {
int rgb = getShadowColor();
shadowPaint.setColor(rgb);
shadowPaint.setMaskFilter(new BlurMaskFilter(paddingBottom, BlurMaskFilter.Blur.NORMAL));
// shadowPaint.setShadowLayer(radius, 0, shadowDimen, rgb);
}

return shadowPaint;
}

参考文档

https://yifeng.studio/2017/02/26/android-elevation-and-shadow/