Android圆角图片和圆形图片实现总结
转载注明出处:http://www.jianshu.com/p/818e1284158d
1. 概述
Android研发中经常会遇见圆角或者圆形图片的展示,但是系统中ImageView并不能直接支持,需要我们自己做一些处理,来实现圆角图片或者圆形图片,自己最近对这块的实现做了一下总结,看一下几种实现方法。
- 图层叠加,上层覆盖一层蒙版,遮挡图片,让图片展示出圆角或者圆形效果
- 重新绘制
- BitmapShader
- Xfermode
- RoundedBitmapDrawable
- CardView,使用官方控件,自动裁剪,达到圆角或者圆形效果。(5.0以上系统)
第一种方法,很傻,不太优雅,不推荐这种方法,了解这种方法是因为确实看见有人这么做过。推荐使用第二种方法,重新绘制图片,不仅可以绘制出四角圆角图片,还可以绘制出底部/顶部/左边/右边圆角效果,非常方便。如果想要偷懒,可以直接使用RoundedBitmapDrawable来实现圆角照片效果。而第三种方法,使用CardView
控件有很大的局限性,它只在5.0以及以上的系统中有效。
下面就来看一下各种方法的具体情况吧。
2. 图层叠加
图层叠加原理很简单,就是在一张图片上面叠加一层图,覆盖部分,让图片展示成圆角。具体原理可以参考下面这张图。
图-1 图层叠加原理图看一下布局代码,很简单。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">
<FrameLayout
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@drawable/demo_icon_android_logo">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/demo_icon_shader"/>
</FrameLayout>
</RelativeLayout>
运行结果如下:
图-2 图片叠层运行结果图效果还是可以的,但是缺点也很明显,一是要准备两张图片,会增加额外的包体积;二是图片叠加一起,必然导致过度绘制;三是该方法太死板,使用时候要考虑图片大小,控件大小,以及蒙层图圆角锯齿效果,非常不灵活,Java里面不灵活就意味着可用性不强。
所以这种方法不建议使用,大家作为了解就行。提出这种方法是为了让大家了解一下图层,后面重新绘制过程中,有一种方法逻辑跟这个相似,也是绘制图层叠加,生成圆角图片。
3. 重新绘制
对图片的绘制要特别的注意,有时候可能图片太大需要我们压缩,关于图片压缩大家可以查看这片文章压缩图片。重新绘制图片,将图片绘制成圆角,有三种方法。
- 使用BitmapShader
- 使用Xfermode
- 使用RoundedBitmapDrawable
从性能上讲并没有太大的区别,但是从使用灵活性上说,个人推荐使用BitmapShader。
在介绍这三个方法之前,先介绍一下图片的拉伸缩放适配,大多数情况先,要展示图片的控件(ImageView)的长宽和图片的长宽并不是一致的,甚至长宽比都不一致,所以在拿到一张图片时候,大多数情况下需要根据控件的大小对图片进行拉伸缩放处理,有人会问为什么不直接使用ImageView
属性scaleType
去控制拉伸缩放,这是因为当我们将一个Bitmap绘制成圆角后,再去进行拉伸缩放,圆角可能会变形,所以在Bitmap设置到控件之前就需要对Bitmap进行一下拉伸缩放处理,直接看下面代码。
// 图片根据控件大小等比例缩放拉伸
float widthScale = imageViewWidth * 1.0f / bitmap.getWidth();
float heightScale = imageViewHeight * 1.0f / bitmap.getHeight();
// 设置长宽拉伸缩放比
Matrix matrix = new Matrix();
matrix.setScale(widthScale, heightScale);
// 拉伸缩放图片
Bitmap newBt = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
注:拉伸缩放也可以在Canvas.drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint)方法中控制,后面会提及到。
3.1 BitmapShader绘制圆角
所有的绘制圆角的实现,推荐使用这个方法,不仅仅可以帮助我们实现圆角,连部分圆角都可以实现,比如顶部是两个圆角,底部是两个直角的图片。
首先介绍一下BitmapShader
这个类,它作为纹理用于绘制一张图。新图可以是纹理图重复/镜像/边缘像素拉伸而绘制成的新图。这个类构造函数很简单,BitmapShader(Bitmap bitmap, TileMode tileX, TileMode tileY)
,第一个参数是Bitmap,作为纹理图传入,tileX是指在水平方向上的绘制方式,tileY是指在竖直方向上的绘制方式。TileMode
有三种属性,拉伸、重复、镜像。
- TileMode.CLAMP 拉伸绘制,并不是指图片拉伸,而是指图片最后一个像素不断绘制,纹理图水平或者竖直方向最后一个像素不断绘制
- TileMode.REPEAT 重复绘制,在水平或者竖直方向上不断重复绘制纹理图
- TileMode.MIRROR 镜像绘制,水平或者竖直方向不断的绘制翻转纹理图
使用BitmapShader绘制图的时候,是从画布的左上角开始绘制的。我们是使用拉伸的绘制模式,直接来看一下代码,了解处理过程。
/**
* 利用BitmapShader绘制圆角图片
*
* @param bitmap
* 待处理图片
* @param outWidth
* 结果图片宽度,一般为控件的宽度
* @param outHeight
* 结果图片高度,一般为控件的高度
* @param radius
* 圆角半径大小
* @return
* 结果图片
*/
private Bitmap roundBitmapByShader(Bitmap bitmap, int outWidth, int outHeight, int radius) {
if(bitmap == null) {
throw new NullPointerException("Bitmap can't be null");
}
// 初始化缩放比
float widthScale = outWidth * 1.0f / bitmap.getWidth();
float heightScale = outHeight * 1.0f / bitmap.getHeight();
Matrix matrix = new Matrix();
matrix.setScale(widthScale, heightScale);
// 初始化绘制纹理图
BitmapShader bitmapShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
// 根据控件大小对纹理图进行拉伸缩放处理
bitmapShader.setLocalMatrix(matrix);
// 初始化目标bitmap
Bitmap targetBitmap = Bitmap.createBitmap(outWidth, outHeight, Bitmap.Config.ARGB_8888);
// 初始化目标画布
Canvas targetCanvas = new Canvas(targetBitmap);
// 初始化画笔
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setShader(bitmapShader);
// 利用画笔将纹理图绘制到画布上面
targetCanvas.drawRoundRect(new RectF(0, 0, outWidth, outWidth), radius, radius, paint);
return targetBitmap;
}
首先初始化了绘制的纹理图,并对图片进行了拉伸缩放处理,没有用到上面提及的拉伸缩放处理方法是因为BitmapShader本身自带了这个属性,进行设置即可。然后初始化了画布和画笔,将设置画笔绘制的纹理图,画笔在绘制图形时候就不是使用单纯的颜色绘制了。最后在利用画笔在画布上面绘制出圆形图片。
看一下这个方法Canvas.drawRoundRect(RectF rect, float rx, float ry, Paint paint)
,第一个参数是指定画布的绘制区域,从画布的左上角开始计算;第二个第三个参数是圆角的x轴/y轴的绘制半径,一般设置成相同的值;第三个就是画笔。
我们看一下运行效果。
图-3 BitmapShader圆角效果图看起来很简单,但这并不是我推荐使用这种方法的原因,丰富的绘制结果才是,有很多样式可以绘制,讲一个比较简单的,底部两个圆角,顶部两个直角,看一下运行效果图。
图-4 BitmapShader底部圆角效果图已经感觉有点厉害了,先来看代码的实现。
/**
* 利用BitmapShader绘制底部圆角图片
*
* @param bitmap
* 待处理图片
* @param outWidth
* 结果图片宽度,一般为控件的宽度
* @param outHeight
* 结果图片高度,一般为控件的高度
* @param radius
* 圆角半径大小
* @return
* 结果图片
*/
private Bitmap roundBottomBitmapByShader(Bitmap bitmap, int outWidth, int outHeight, int radius) {
if(bitmap == null) {
throw new NullPointerException("Bitmap can't be null");
}
// 初始化缩放比
float widthScale = outWidth * 1.0f / bitmap.getWidth();
float heightScale = outHeight * 1.0f / bitmap.getHeight();
Matrix matrix = new Matrix();
matrix.setScale(widthScale, heightScale);
// 初始化绘制纹理图
BitmapShader bitmapShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
// 根据控件大小对纹理图进行拉伸缩放处理
bitmapShader.setLocalMatrix(matrix);
// 初始化目标bitmap
Bitmap targetBitmap = Bitmap.createBitmap(outWidth, outHeight, Bitmap.Config.ARGB_8888);
// 初始化目标画布
Canvas targetCanvas = new Canvas(targetBitmap);
// 初始化画笔
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setShader(bitmapShader);
// 利用画笔绘制底部圆角
targetCanvas.drawRoundRect(new RectF(0, outHeight - 2 * radius, outWidth, outWidth), radius, radius, paint);
// 利用画笔绘制顶部上面直角部分
targetCanvas.drawRect(new RectF(0, 0, outWidth, outHeight - radius), paint);
return targetBitmap;
}
仔细的同学已经发现了,这部分代码和上面很相似,只不过最后两句有些不同。对,因为后面两句是对图片的绘制,最后输出我们想要的效果。targetCanvas.drawRoundRect()
先在底部绘制出圆角的矩形,然后targetCanvas.drawRect()
绘制出上面的直角。targetCanvas.drawRoundRect()
它在画布上面绘制了图片底部2*radius
高度的部分,绘制出的结果是一个高度为2*radius
的圆角图片,然后再在画布0到outHeight - radius
部分绘制出一个直角矩形,这个直角矩形正好覆盖了之前绘制的圆角图片的上半部分,只露出下面的圆角,此时画布上面呈现出的图像就是刚才运行的效果图,看一下下面的原理图帮助我们理解一下。
是不是有点类似上面提高的图层叠加的原理了,根据各种叠加情况,可以绘制出多种效果,有下面几种(同一个图中圆角半径都相同):
- 四角都是圆角
- 同边圆角,底部圆角/顶部圆角/左边圆角/右边圆角
- 对角线圆角,左上右下圆角/左下右上圆角
- 单个圆角,左上圆角/左下圆角/右上圆角/右下圆角
- 三个圆角,左上非圆角/左下非圆角/右上非圆角/右下非圆角
除了这些绘制效果,我们还可以绘制出圆角半径不同的图,在此不做讨论。上述所有的绘制代码在后面会给出工程地址,或者直接点击这里查看。
3.2 Xfermode绘制圆角
在使用画笔Paint去绘制东西,当绘制多个图层叠加的时候,有16中模式。效果如下图。
图-6 Xfermode类型效果图模式 | 说明 |
---|---|
PorterDuff.Mode.CLEAR | 所有绘制不会绘制到画布上 |
PorterDuff.Mode.SRC | 显示上层绘制图形 |
PorterDuff.Mode.DST | 显示下层绘制图形 |
PorterDuff.Mode.SRC_OVER | 图形叠加,上层盖住下层 |
PorterDuff.Mode.DST_OVER | 图形叠加,下层盖住上层 |
PorterDuff.Mode.SRC_IN | 显示上层交集部分 |
PorterDuff.Mode.DST_IN | 显示下层交集部分 |
PorterDuff.Mode.SRC_OUT | 显示上层非交集部分 |
PorterDuff.Mode.DST_OUT | 显示下层非交集部分 |
PorterDuff.Mode.SRC_ATOP | 显示下层非交集部分和上层交集部分 |
PorterDuff.Mode.DST_ATOP | 显示下层交集部分与上层非交集部分 |
PorterDuff.Mode.XOR | 去除交集部分 |
PorterDuff.Mode.DARKEN | 交集部分颜色加深 |
PorterDuff.Mode.LIGHTEN | 交集部分颜色变亮 |
PorterDuff.Mode.MULTIPLY | 显示交集部分,颜色混合叠加 |
PorterDuff.Mode.SCREEN | 取两图层全部区域,交集部分变为透明色 |
官方demo中主要绘制代码如下:
// mDstB是黄色的圆形图bitmap
// mSrcB是蓝色的矩形图bitmap
canvas.drawBitmap(mDstB, 0, 0, paint);
paint.setXfermode(sModes[i]);
canvas.drawBitmap(mSrcB, 0, 0, paint)
可以看到在两个绘制图形过程中,添加Xfermode绘制模式,能够改变两个图的叠加效果,我们主要关注一下SrcIn
模式,可以看见,用图层叠加的交集去截取mSrcB
图,可以利用这个,想绘制一个圆角的图,然后设置绘制模式,接着绘制一个矩形的图,两者一叠加,正好是用圆角图去截取矩形图,矩形图也就是我们的原图片了。
直接看一下实现代码。
/**
* 利用Xfermode绘制圆角图片
*
* @param bitmap
* 待处理图片
* @param outWidth
* 结果图片宽度,一般为控件的宽度
* @param outHeight
* 结果图片高度,一般为控件的高度
* @param radius
* 圆角半径大小
* @return
* 结果图片
*/
private Bitmap roundBitmapByXfermode(Bitmap bitmap, int outWidth, int outHeight, int radius) {
if(bitmap == null) {
throw new NullPointerException("Bitmap can't be null");
}
// 等比例缩放拉伸
float widthScale = outWidth * 1.0f / bitmap.getWidth();
float heightScale = outHeight * 1.0f / bitmap.getHeight();
Matrix matrix = new Matrix();
matrix.setScale(widthScale, heightScale);
Bitmap newBt = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
// 初始化目标bitmap
Bitmap targetBitmap = Bitmap.createBitmap(outWidth, outHeight, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(targetBitmap);
canvas.drawARGB(0, 0, 0, 0);
Paint paint = new Paint();
paint.setAntiAlias(true);
RectF rectF = new RectF(0f, 0f, (float) outWidth, (float) outHeight);
// 在画布上绘制圆角图
canvas.drawRoundRect(rectF, radius, radius, paint);
// 设置叠加模式
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
// 在画布上绘制原图片
Rect ret = new Rect(0, 0, outWidth, outHeight);
canvas.drawBitmap(newBt, ret, ret, paint);
return targetBitmap;
}
最后展示一下运行的效果图。
图-7 Xfermode圆角效果图.png3.3 RoundedBitmapDrawable绘制圆角
如果你觉得上面两种方法还是好麻烦,肯本不想去搞这些绘制的东西,有没有简单的方法呢?
有!
在V4包中有一个RoundedBitmapDrawable
类,专门帮助实现圆角效果。
用法也很简单,看代码。
/**
* 利用RoundedBitmapDrawable绘制圆角图片
*
* @param bitmap
* 待处理图片
* @param outWidth
* 结果图片宽度,一般为控件的宽度
* @param outHeight
* 结果图片高度,一般为控件的高度
* @param radius
* 圆角半径大小
* @return
* 结果图片
*/
private Drawable roundBitmapByBitmapDrawable(Bitmap bitmap, int outWidth, int outHeight, int radius) {
if(bitmap == null) {
throw new NullPointerException("Bitmap can't be null");
}
// 等比例缩放拉伸
float widthScale = outWidth * 1.0f / bitmap.getWidth();
float heightScale = outHeight * 1.0f / bitmap.getHeight();
Matrix matrix = new Matrix();
matrix.setScale(widthScale, heightScale);
Bitmap newBt = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
// 绘制圆角
RoundedBitmapDrawable dr = RoundedBitmapDrawableFactory.create(getResources(), newBt);
dr.setCornerRadius(radius);
dr.setAntiAlias(true);
return dr;
}
代码就是三行,初始化,然后设置圆角半径dr.setCornerRadius()
,设置边缘平滑dr.setAntiAlias()
(这步可以不需要)。运行效果如下。
其实去查看dr.setCornerRadius()
源代码,会发现它内部就是使用的BitmapShader
来绘制圆角效果。官方做了一层封装,使得我们使用起来更加方便。
public void setCornerRadius(float cornerRadius) {
if (mCornerRadius == cornerRadius) return;
mIsCircular = false;
if (isGreaterThanZero(cornerRadius)) {
mPaint.setShader(mBitmapShader);
} else {
mPaint.setShader(null);
}
mCornerRadius = cornerRadius;
invalidateSelf();
}
4. CardView控件圆角
CardView是官方在V7包中新增的一个控件,继承FrameLayout
布局,拥有圆角和阴影属性的控件,既然是新控件,看一下它的一些属性。
- cardElevation 阴影的大小
- cardMaxElevation 阴影最大高度
- cardBackgroundColor 卡片的背景色
- cardCornerRadius 卡片的圆角大小
- contentPadding 卡片内容于边距的间隔
- contentPaddingBottom
- contentPaddingTop
- contentPaddingLeft
- contentPaddingRight
- contentPaddingStart
- contentPaddingEnd
- cardUseCompatPadding 设置内边距,V21+的版本和之前的版本仍旧具有一样的计算方式
- cardPreventConrerOverlap 在V20和之前的版本中添加内边距,这个属性为了防止内容和边角的
比较关注的是cardCornerRadius
和cardElevation
这两个属性,为了实现圆角图片的效果,将cardElevation
属性也就是阴影效果,设置为0dp,看一下具体的布局文件代码。
<android.support.v7.widget.CardView
android:layout_width="@dimen/round_bitmap_width"
android:layout_height="@dimen/round_bitmap_width"
app:cardCornerRadius="5dp"
app:cardElevation="0dp">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY"
android:src="@drawable/demo_icon_android_logo"/>
</android.support.v7.widget.CardView>
将CardView
作为父布局,容纳ImageView
控件。看一下运行效果。
这个控件用起来非常偷懒,可惜的是CardView
调用了View的setClipToOutline(true)
方法来裁边,5.0一下的系统没有这个API,也就是说,5.0以下的系统不会呈现出圆角。
虽然这个控件不能帮助我们实现圆角图片的效果,但是给我们提供了一个圆角控件,也能帮助我们解决很多需求场景。
5. 圆形图片的实现
了解了圆角照片的实现,圆形照片的实现就很简单了。圆形图片需要的是一个正方形的图片,将圆角半径设置为正方形图片边长一半即可。但是有时候,我们拿到的图片是一个矩形,长宽不一样长!!!我们需要对图片做裁剪处理,取矩形中间部分,让它变成一个正方形图片。
以BitmapShader
为例。
/**
* 利用BitmapShader绘制底部圆角图片
*
* @param bitmap
* 待处理图片
* @param edgeWidth
* 正方形控件大小
* @param radius
* 圆角半径大小
* @return
* 结果图片
*/
private Bitmap circleBitmapByShader(Bitmap bitmap, int edgeWidth, int radius) {
if(bitmap == null) {
throw new NullPointerException("Bitmap can't be null");
}
float btWidth = bitmap.getWidth();
float btHeight = bitmap.getHeight();
// 水平方向开始裁剪的位置
float btWidthCutSite = 0;
// 竖直方向开始裁剪的位置
float btHeightCutSite = 0;
// 裁剪成正方形图片的边长,未拉伸缩放
float squareWidth = 0f;
if(btWidth > btHeight) { // 如果矩形宽度大于高度
btWidthCutSite = (btWidth - btHeight) / 2f;
squareWidth = btHeight;
} else { // 如果矩形宽度不大于高度
btHeightCutSite = (btHeight - btWidth) / 2f;
squareWidth = btWidth;
}
// 设置拉伸缩放比
float scale = edgeWidth * 1.0f / squareWidth;
Matrix matrix = new Matrix();
matrix.setScale(scale, scale);
// 将矩形图片裁剪成正方形并拉伸缩放到控件大小
Bitmap squareBt = Bitmap.createBitmap(bitmap, (int)btWidthCutSite, (int)btHeightCutSite, (int)squareWidth, (int)squareWidth, matrix, true);
// 初始化绘制纹理图
BitmapShader bitmapShader = new BitmapShader(squareBt, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
// 初始化目标bitmap
Bitmap targetBitmap = Bitmap.createBitmap(edgeWidth, edgeWidth, Bitmap.Config.ARGB_8888);
// 初始化目标画布
Canvas targetCanvas = new Canvas(targetBitmap);
// 初始化画笔
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setShader(bitmapShader);
// 利用画笔绘制圆形图
targetCanvas.drawRoundRect(new RectF(0, 0, edgeWidth, edgeWidth), radius, radius, paint);
return targetBitmap;
}
裁剪拉伸的逻辑比较复杂,主要是因为图片长宽和正方形边长各种关系复杂导致。将图片处理成正方形图片后,后面代码和切圆角代码基本类似。
- 如果矩形图片的宽大于高,那么竖直方向上不需要裁剪,以矩形图片高度为裁剪后的正方形的边长,同时计算出水平方向上的裁剪位置
- 如果矩形图片的高不小于宽,那么水平方向上不需要裁剪,以矩形图片宽度为裁剪后的正方形的边长,同时计算出竖直方向上的裁剪位置
- 最后计算出裁剪后的正方形图边长与控件边长的拉伸缩放比例,因为都是正方形,只需要计算一边的缩放比即可。
最后来看一下圆形图片的实现效果。
图-11 BitmapShader圆形效果图看到这里你会发现,其实圆形图片的实现和圆角图片的实现基本是相同的,了解圆角图片的实现方法,圆形图片的实现自然不在话下。
6. 总结
上述介绍了大概五种圆角图片的实现方法,方法没有优劣,在具体的业务场景选择最适合的方法才是最重要的,但是最为一个有追求的程序员,还是建议大家多了解重新绘制里面的几种方法。
最后附上Demo工程的地址:地址链接,里面包含了利用BitmapShader
绘制多种效果的单例类RoundBitmapTransformation
。在写demo过程中,因为使用的图片太大,有出现OOM的异常,如果觉得图片太大,你可以替换掉Demo工程中的图片,其实在实际中对图片操作也很容易引起OOM,关于图片的压缩处理,大家可以参考这篇文章。