Android ViewAndroid开发Android学习

Android圆角图片和圆形图片实现总结

2016-11-10  本文已影响1687人  王三的猫阿德

转载注明出处:http://www.jianshu.com/p/818e1284158d

1. 概述

Android研发中经常会遇见圆角或者圆形图片的展示,但是系统中ImageView并不能直接支持,需要我们自己做一些处理,来实现圆角图片或者圆形图片,自己最近对这块的实现做了一下总结,看一下几种实现方法。

第一种方法,很傻,不太优雅,不推荐这种方法,了解这种方法是因为确实看见有人这么做过。推荐使用第二种方法,重新绘制图片,不仅可以绘制出四角圆角图片,还可以绘制出底部/顶部/左边/右边圆角效果,非常方便。如果想要偷懒,可以直接使用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。

在介绍这三个方法之前,先介绍一下图片的拉伸缩放适配,大多数情况先,要展示图片的控件(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有三种属性,拉伸、重复、镜像。

使用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部分绘制出一个直角矩形,这个直角矩形正好覆盖了之前绘制的圆角图片的上半部分,只露出下面的圆角,此时画布上面呈现出的图像就是刚才运行的效果图,看一下下面的原理图帮助我们理解一下。

图-5 Bitmapshader底部圆角原理

是不是有点类似上面提高的图层叠加的原理了,根据各种叠加情况,可以绘制出多种效果,有下面几种(同一个图中圆角半径都相同):

除了这些绘制效果,我们还可以绘制出圆角半径不同的图,在此不做讨论。上述所有的绘制代码在后面会给出工程地址,或者直接点击这里查看

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圆角效果图.png

3.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()(这步可以不需要)。运行效果如下。

图-8 RoundedBitmapDrawable圆角效果图

其实去查看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布局,拥有圆角和阴影属性的控件,既然是新控件,看一下它的一些属性。

比较关注的是cardCornerRadiuscardElevation这两个属性,为了实现圆角图片的效果,将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控件。看一下运行效果。

图-9 CardView圆角效果图-5.0系统

这个控件用起来非常偷懒,可惜的是CardView调用了View的setClipToOutline(true)方法来裁边,5.0一下的系统没有这个API,也就是说,5.0以下的系统不会呈现出圆角。

图-10 CardView圆角效果图-4.4系统

虽然这个控件不能帮助我们实现圆角图片的效果,但是给我们提供了一个圆角控件,也能帮助我们解决很多需求场景。

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,关于图片的压缩处理,大家可以参考这篇文章

上一篇下一篇

猜你喜欢

热点阅读