Fresco 图片圆角实现原理及 Android 中图片圆角实现方法
2015-12-28 16:13
921 查看
上篇文章 介绍了 Fresco 基础使用和实现图片圆角的方法,可以通过两种方式来实现圆角:BITMAP_ONLY 模式和 OVERLAY_COLOR 模式。本文通过分析 Fresco 源码来介绍这两种方式实现圆角的原理,并总结 Android 中常用的实现图片圆角的方法。
本文重点分析 Fresco 中实现图片圆角的源码,其他部分的源码,将在后续文章中介绍。
Rounded.java
RoundedBitmapDrawable.java
RoundedColorDrawable.java
RoundedCornersDrawable.java
其中 Rounded 是圆角实现类的接口,定义了圆角类实现的方法:
其他三个类实现了 Rounded 接口,来实现两种不同模式的圆角,RoundedCornersDrawable.java 用于实现 OVERLAY_COLOR 模式的圆角,而 RoundedBitmapDrawable.java 和 RoundedCorlorDrawable.java 都是用于实现 BITMAP_ONLY 模式的圆角,两者的区别在于传入的资源类型不同,前者是对 BitmapDrawable 进行圆角处理,而后者是对 ColorDrawable 进行处理。
了解了源码中实现图片圆角的结构,下面开始进入到具体的代码中了解具体的实现过程
进入到 RoundedBitmapDrawable.java 中,首先看它的绘制过程,找到 draw() 方法:
从 draw() 方法可以了解到圆角图片的绘制过程:
更新变换矩阵,用于图片大小缩放适配
判断有没有设置属性,如果没有则直接绘制,如果有则进行下一步
更新 Path,根据属性确定绘制的形状
更新 Paint,将图片资源填充到画笔
绘制图片,绘制边框
本文的重点是了解圆角的实现过程,所以接下来就进入到 updatePath() 和 updatePaint() 中看看 Path 和 Paint 是怎样实现圆角的
从上面的代码中可以大致了解到其主要是根据属性值 (mIsCircle) 来配置 Path,主要使用到 Path 的两个方法:addCircle() 和 addRoundRect(),这两个方法分别实现绘制圆形和绘制矩形,其参数描述如下:
addCircle(float x,float y,float radius,Direction dir)
圆心 x 坐标
圆心 y 坐标
圆的半径
绘制圆的方向,值 Direction.CCW 为逆时针方向,值 Directin.CW 为顺时针方向
addRoundRect(RectF rect,float[] radii,Direction dir)
外接矩形
圆角半径数组,共 8 个值,每两个为一对,顺序为:左上 -> 右上 -> 右下 -> 左下
绘制圆的方向,同上
以上,便完成了 Path 的更新,接下来是对 Paint 的更新
从这里大概清楚了这种方式是通过 BitmapShader 方式来实现图片圆角的。于是这里产生了一个疑问:既然可以直接使用 BitmapShader 来实现圆角,那如果接下来再直接使用 canvas 的 drawCircle() 和 drawRoundRect()也能实现圆形图片和圆角图片,为什么还要多此一举的使用 Path 来绘制圆形和圆角矩形呢? 我对此的理解是:canvas 的drawRoundRect() 没有办法实现四个不同大小的圆角,而通过 Path 的 addRoundRect() 方法是能够实现不同圆角的图片,使用Path是为了满足这个需求。至于为什么不能在 xml 布局文件中设置不同大小的圆角而只能在代码中设置这个问题,依然不明白,期待在后续的分析中能够解决这个问题。
以上,是实现图片圆角过程中主要的步骤,至于后面的绘制边框,没太大的难度,这里就不再叙述。
从上面的代码中可以了解到这种方式的实现原理大致如下:
每次设置属性的时候都更新一下 Path ,也就是根据属性决定 Path 是画圆形还是圆角矩形
将图片按照正常的方式先画出来(调用super.draw())
对Path设置填充模式为 INVERSE_EVEN_ODD,取 Path 未占用的区域(占用的是圆形或者圆角矩形区域)
画出 Path,这样未占用的区域就是指定的背景色了,占用区域就是圆角图片
判断如果是圆形,则再画一个圆形,按照上述填充规则
最后如果有边框就画边框,同样的规则
下面通过一幅示意图简单描述一下这种方式的实现原理:
绘制过程大致是这样,我认为只要理解了 Path 的填充模式,这个原理就很好理解的,关于 Path 的填充模式,这篇文章中有详细介绍,可以参考一下。
以上,通过源码分析的方式理解了 Fresco 中圆角矩形的实现原理,在分析这些代码的时候我查阅了一些资料,包括 Drawable 类,Path 类,Paint 类等一些类的使用方法,然后结合源码,跟着思路慢慢得出结果,并实现了一个简单的自定义 View 来验证结果。效果如下,代码在 GitHub 上
剪裁:从原始图片中剪出一个圆角图片
覆盖:在原始图片上遮盖住圆角多余的部分,剩下的可见部分就是圆角矩形了
基于这两类方法,我大概总结了一些可以实现圆角矩形的方法,如下
可以看到,这个方法中新建了一个指定宽高的 Bitmap 对象,然后创建了一个相同大小的矩形,利用画布绘制时指定圆角大小,这样在画布上就有了一个圆角矩形,然后设置画笔的剪裁方式为 Mode.SRC_IN,将原图添加到画布上,就形成了一个圆角矩形的图片。不推荐使用这种方式来实现图片圆角,因为这种方式会对每一个要实现圆角的图片生成一个新的 Bitmap 对象,将会增加内存消耗,在需要加载大量图片的时候就会很可能引发内存泄漏。
使用 Xfremode 的具体实现就不多叙述,感兴趣的可以参考这篇文章
在使用时,只需要将要实现圆角的图片放在这个自定义Layout内部就行了:
首先在 /res/drawable 目录下新建形状 frame.xml :
接着在使用时将两个 ImageView 放在同一个 ViewGroup 中即可:
首先在 res/drawable/ 目录下创建 形状文件 round_corners.xml:
接着在使用时,只需要对 View 设置 background 属性为这个 形状文件即可:
接着在 java 代码中 findView 并设置 ImageView.setClipToOutline(true);
以上,介绍了 Fresco 中实现圆角图片的两种方式,并总结了一些 Android 中实现圆角图片的方案,文章中用到的代码放在 GitHub 上。水平有限,如有错误或疏漏,还望不吝赐教。
本文重点分析 Fresco 中实现图片圆角的源码,其他部分的源码,将在后续文章中介绍。
Fresco 中圆角实现原理
在 com.facebook.drawee.drawable 包中有如下文件Rounded.java
RoundedBitmapDrawable.java
RoundedColorDrawable.java
RoundedCornersDrawable.java
其中 Rounded 是圆角实现类的接口,定义了圆角类实现的方法:
public interface Rounded { void setCircle(boolean isCircle); void setRadius(float radius); void setRadii(float[] radii); void setBorder(int color, float width); }
其他三个类实现了 Rounded 接口,来实现两种不同模式的圆角,RoundedCornersDrawable.java 用于实现 OVERLAY_COLOR 模式的圆角,而 RoundedBitmapDrawable.java 和 RoundedCorlorDrawable.java 都是用于实现 BITMAP_ONLY 模式的圆角,两者的区别在于传入的资源类型不同,前者是对 BitmapDrawable 进行圆角处理,而后者是对 ColorDrawable 进行处理。
了解了源码中实现图片圆角的结构,下面开始进入到具体的代码中了解具体的实现过程
BITMAP_ONLY 模式
作为默认的实现模式,首先来了解下这种模式的实现过程进入到 RoundedBitmapDrawable.java 中,首先看它的绘制过程,找到 draw() 方法:
@Override public void draw(Canvas canvas) { updateTransform();//更新图片变换矩阵 updateNonzero();//更新 0 值,判断有没有 设置圆形,圆角,边框等属性 if (!mIsNonzero) {//如果没有设置以上属性,则 mIsNonzero 返回 false,直接调用父类的绘制 super.draw(canvas); return; } updatePath();//更新 Path updatePaint();//更新画笔 int saveCount = canvas.save();//保存画布状态 canvas.concat(mInverseTransform);//设置变换矩阵 canvas.drawPath(mPath, mPaint);//绘制 Path if (mBorderWidth != 0) {//绘制边框 mBorderPaint.setStrokeWidth(mBorderWidth); mBorderPaint.setColor(DrawableUtils.multiplyColorAlpha(mBorderColor, mPaint.getAlpha())); canvas.drawPath(mPath, mBorderPaint); } canvas.restoreToCount(saveCount);//合并图像 }
从 draw() 方法可以了解到圆角图片的绘制过程:
更新变换矩阵,用于图片大小缩放适配
判断有没有设置属性,如果没有则直接绘制,如果有则进行下一步
更新 Path,根据属性确定绘制的形状
更新 Paint,将图片资源填充到画笔
绘制图片,绘制边框
本文的重点是了解圆角的实现过程,所以接下来就进入到 updatePath() 和 updatePaint() 中看看 Path 和 Paint 是怎样实现圆角的
private void updatePath() { if (mIsPathDirty) {//大概是说如果有对图片进行设置 mPath.reset();//重置 Path mRootBounds.inset(mBorderWidth/2, mBorderWidth/2);//矩形向内缩进半个边框宽度,避免边框遮挡图片 if (mIsCircle) {//如果设置为圆形图片,则 Path 设置为圆形,否则就设置为矩形 mPath.addCircle( mRootBounds.centerX(), mRootBounds.centerY(), Math.min(mRootBounds.width(), mRootBounds.height())/2, Path.Direction.CW); } else { mPath.addRoundRect(mRootBounds, mCornerRadii, Path.Direction.CW); } mRootBounds.inset(-(mBorderWidth/2), -(mBorderWidth/2));//Path 设置完成,恢复矩形 mPath.setFillType(Path.FillType.WINDING); mIsPathDirty = false; } }
从上面的代码中可以大致了解到其主要是根据属性值 (mIsCircle) 来配置 Path,主要使用到 Path 的两个方法:addCircle() 和 addRoundRect(),这两个方法分别实现绘制圆形和绘制矩形,其参数描述如下:
addCircle(float x,float y,float radius,Direction dir)
圆心 x 坐标
圆心 y 坐标
圆的半径
绘制圆的方向,值 Direction.CCW 为逆时针方向,值 Directin.CW 为顺时针方向
addRoundRect(RectF rect,float[] radii,Direction dir)
外接矩形
圆角半径数组,共 8 个值,每两个为一对,顺序为:左上 -> 右上 -> 右下 -> 左下
绘制圆的方向,同上
以上,便完成了 Path 的更新,接下来是对 Paint 的更新
private void updatePaint() { Bitmap bitmap = getBitmap();//获取需要绘制的 Bitmap if (mLastBitmap == null || mLastBitmap.get() != bitmap) {//防止重复引用 mLastBitmap = new WeakReference<Bitmap>(bitmap);//新建一个弱引用 Bitmap 对象 mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));//设置 Shader mIsShaderTransformDirty = true; } if (mIsShaderTransformDirty) {//设置变换矩阵 mPaint.getShader().setLocalMatrix(mTransform); mIsShaderTransformDirty = false; } }
从这里大概清楚了这种方式是通过 BitmapShader 方式来实现图片圆角的。于是这里产生了一个疑问:既然可以直接使用 BitmapShader 来实现圆角,那如果接下来再直接使用 canvas 的 drawCircle() 和 drawRoundRect()也能实现圆形图片和圆角图片,为什么还要多此一举的使用 Path 来绘制圆形和圆角矩形呢? 我对此的理解是:canvas 的drawRoundRect() 没有办法实现四个不同大小的圆角,而通过 Path 的 addRoundRect() 方法是能够实现不同圆角的图片,使用Path是为了满足这个需求。至于为什么不能在 xml 布局文件中设置不同大小的圆角而只能在代码中设置这个问题,依然不明白,期待在后续的分析中能够解决这个问题。
以上,是实现图片圆角过程中主要的步骤,至于后面的绘制边框,没太大的难度,这里就不再叙述。
OVERLAY_COLOR 模式
这种模式的实现在 RoundedCornersDrawable.java 文件中,可以看到,其实这个类中还存在着另外一种模式:CLIPPING 模式,从官方文档中可以了解到不使用这个模式的原因,这里就不再叙述。直接看使用 OVERLAY_COLOR 模式的代码实现,同样的,首先找到绘制过程 draw() 方法:@Override public void draw(Canvas canvas) { Rect bounds = getBounds(); switch (mType) { case CLIPPING://暂不支持这种方式,跳过 // clip, note: doesn't support anti-aliasing int saveCount = canvas.save(); mPath.setFillType(Path.FillType.EVEN_ODD); canvas.clipPath(mPath); super.draw(canvas); canvas.restoreToCount(saveCount); break; case OVERLAY_COLOR: super.draw(canvas);//首先让父类绘制图像 mPaint.setColor(mOverlayColor);//设置画笔颜色 mPaint.setStyle(Paint.Style.FILL);//设置画笔样式为填充 mPath.setFillType(Path.FillType.INVERSE_EVEN_ODD);//设置 Path 的填充模式 canvas.drawPath(mPath, mPaint);//画遮盖图层 if (mIsCircle) {//如果是圆形,则用 Canvas 画一个圆形 // INVERSE_EVEN_ODD will only draw inverse circle within its bounding box, so we need to // fill the rest manually if the bounds are not square. float paddingH = (bounds.width() - bounds.height() + mBorderWidth) / 2f; float paddingV = (bounds.height() - bounds.width() + mBorderWidth) / 2f; if (paddingH > 0) { canvas.drawRect(bounds.left, bounds.top, bounds.left + paddingH, bounds.bottom, mPaint); canvas.drawRect( bounds.right - paddingH, bounds.top, bounds.right, bounds.bottom, mPaint); } if (paddingV > 0) { canvas.drawRect(bounds.left, bounds.top, bounds.right, bounds.top + paddingV, mPaint); canvas.drawRect( bounds.left, bounds.bottom - paddingV, bounds.right, bounds.bottom, mPaint); } } break; } if (mBorderColor != Color.TRANSPARENT) {//画边框 mPaint.setStyle(Paint.Style.STROKE); mPaint.setColor(mBorderColor); mPaint.setStrokeWidth(mBorderWidth); mPath.setFillType(Path.FillType.EVEN_ODD); canvas.drawPath(mPath, mPaint); } }
从上面的代码中可以了解到这种方式的实现原理大致如下:
每次设置属性的时候都更新一下 Path ,也就是根据属性决定 Path 是画圆形还是圆角矩形
将图片按照正常的方式先画出来(调用super.draw())
对Path设置填充模式为 INVERSE_EVEN_ODD,取 Path 未占用的区域(占用的是圆形或者圆角矩形区域)
画出 Path,这样未占用的区域就是指定的背景色了,占用区域就是圆角图片
判断如果是圆形,则再画一个圆形,按照上述填充规则
最后如果有边框就画边框,同样的规则
下面通过一幅示意图简单描述一下这种方式的实现原理:
绘制过程大致是这样,我认为只要理解了 Path 的填充模式,这个原理就很好理解的,关于 Path 的填充模式,这篇文章中有详细介绍,可以参考一下。
以上,通过源码分析的方式理解了 Fresco 中圆角矩形的实现原理,在分析这些代码的时候我查阅了一些资料,包括 Drawable 类,Path 类,Paint 类等一些类的使用方法,然后结合源码,跟着思路慢慢得出结果,并实现了一个简单的自定义 View 来验证结果。效果如下,代码在 GitHub 上
Android 中实现圆角的方案
在 Android 中有很多方法能够实现圆角矩形。根据现实生活中的经验,要对一张图片实现圆角,无非两种方式,一种是剪出圆角,另一种是遮住圆角。因此,可以简单的将实现圆角的方案分成两类:剪裁:从原始图片中剪出一个圆角图片
覆盖:在原始图片上遮盖住圆角多余的部分,剩下的可见部分就是圆角矩形了
基于这两类方法,我大概总结了一些可以实现圆角矩形的方法,如下
方案一:将原始图片中截取的圆角矩形图片放在一个新建的Bitmap中
这种方式大致是 剪裁 类的方式,主要代码如下:public static Bitmap toRoundCorner(Bitmap bitmap, int pixels) { Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Config.ARGB_8888); Canvas canvas = new Canvas(output); final int color = 0xff424242; final Paint paint = new Paint(); final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); final RectF rectF = new RectF(rect); final float roundPx = pixels; paint.setAntiAlias(true); canvas.drawARGB(0, 0, 0, 0); paint.setColor(color); canvas.drawRoundRect(rectF, roundPx, roundPx, paint); paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN)); canvas.drawBitmap(bitmap, rect, rect, paint); return output; }
可以看到,这个方法中新建了一个指定宽高的 Bitmap 对象,然后创建了一个相同大小的矩形,利用画布绘制时指定圆角大小,这样在画布上就有了一个圆角矩形,然后设置画笔的剪裁方式为 Mode.SRC_IN,将原图添加到画布上,就形成了一个圆角矩形的图片。不推荐使用这种方式来实现图片圆角,因为这种方式会对每一个要实现圆角的图片生成一个新的 Bitmap 对象,将会增加内存消耗,在需要加载大量图片的时候就会很可能引发内存泄漏。
方案二:通过 Xfermode 实现
这种方式是一种 覆盖 类的方式,关于 Xfremode 有一张比较经典的示意图可以很好的解释他是做什么的使用 Xfremode 的具体实现就不多叙述,感兴趣的可以参考这篇文章
方案三:通过对 ViewGroup 进行设置,使包裹在内部的图片呈现圆角矩形
这种方式依然是一种 覆盖 的方式,只不过不是对当前要显示的图片进行覆盖,而是上升到父容器,对父容器进行设置,内部的图片不做任何改变。依然不推荐这种方式,因为通过这种方式实现圆角图片会增加布局的层级,在Android性能优化中有提到过尽量减少布局的层次嵌套,因此这种方式仅作参考,下面是实现代码:public class RoundRelativeLayout extends RelativeLayout { private float radius; private boolean isPathValid; private Path mPath = new Path(); public RoundRelativeLayout(Context context) { super(context); } public RoundRelativeLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public RoundRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RoundRelativeLayout); radius = ta.getDimension(R.styleable.RoundRelativeLayout_radius, 0); ta.recycle(); } @Override protected void dispatchDraw(Canvas canvas) { canvas.clipPath(getRoundRectPath()); super.dispatchDraw(canvas); } @Override public void draw(Canvas canvas) { canvas.clipPath(getRoundRectPath()); super.draw(canvas); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int oldWidth = getMeasuredWidth(); int oldHeight = getMeasuredHeight(); super.onMeasure(widthMeasureSpec, heightMeasureSpec); int newWidth = getMeasuredWidth(); int newHeight = getMeasuredHeight(); if (newWidth != oldWidth || newHeight != oldHeight) { isPathValid = false; } } private Path getRoundRectPath() { if (isPathValid) { return mPath; } mPath.reset(); int width = getWidth(); int height = getHeight(); RectF bounds = new RectF(0, 0, width, height); mPath.addRoundRect(bounds, radius, radius, Path.Direction.CW); isPathValid = true; return mPath; } }
在使用时,只需要将要实现圆角的图片放在这个自定义Layout内部就行了:
<com.tc.view.RoundRelativeLayout android:layout_width="wrap_content" android:layout_height="wrap_content" app:radius="20dp"> <ImageView android:layout_width="80dp" android:layout_height="80dp" android:src="@mipmap/img_test"/> </com.tc.view.RoundRelativeLayout>
方案四:使用形状 Shape 覆盖
上层是一个圆角矩形的圆环形状,覆盖在下层要显示的 ImageView 上,这是一种 覆盖 的方式。作为了解,这种方式也不推荐,因为要使用两个ImageView来显示一张图片,上层圆角矩形圆环形状需要先定制,包括圆角大小,颜色等属性,导致可定制性不强,使用不方便,仅作参考。实现代码如下:首先在 /res/drawable 目录下新建形状 frame.xml :
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <solid android:color="#00ffffff" /> <corners android:radius="12dp" /> <stroke android:width="6dp" android:color="#ffffffff" /> </shape>
接着在使用时将两个 ImageView 放在同一个 ViewGroup 中即可:
<FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content"> <ImageView android:layout_width="match_parent" android:layout_height="match_parent" android:padding="6dp" android:src="@mipmap/img_test"/> <ImageView android:src="@drawable/frame" android:layout_width="match_parent" android:layout_height="match_parent" /> </FrameLayout>
方案五:使用 Android 自带的剪切方法
这种方式仅支持 API 21 及以上版本,具体使用方法如下:首先在 res/drawable/ 目录下创建 形状文件 round_corners.xml:
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <corners android:radius="10dp" /> </shape>
接着在使用时,只需要对 View 设置 background 属性为这个 形状文件即可:
<ImageView android:id="@+id/clip_img" android:layout_width="80dp" android:layout_height="80dp" android:src="@mipmap/img_test" android:background="@drawable/round_corners"/>
接着在 java 代码中 findView 并设置 ImageView.setClipToOutline(true);
ImageView clipImg= (ImageView) findViewById(R.id.clip_img); clipImg.setClipToOutline(true);
方案六: .9.png 实现
通过添加一个.9.png 的背景来实现图片的圆角,关于.9.png 图的制作,可以参考网上的一些资料教程,难度不大,这里只做介绍,感兴趣的朋友可以尝试一下。以上,介绍了 Fresco 中实现圆角图片的两种方式,并总结了一些 Android 中实现圆角图片的方案,文章中用到的代码放在 GitHub 上。水平有限,如有错误或疏漏,还望不吝赐教。
相关文章推荐
- 使用C++实现JNI接口需要注意的事项
- Android IPC进程间通讯机制
- Android Manifest 用法
- [转载]Activity中ConfigChanges属性的用法
- Android之获取手机上的图片和视频缩略图thumbnails
- Android之使用Http协议实现文件上传功能
- Android学习笔记(二九):嵌入浏览器
- android string.xml文件中的整型和string型代替
- i-jetty环境搭配与编译
- android之定时器AlarmManager
- android wifi 无线调试
- Android Native 绘图方法
- Android java 与 javascript互访(相互调用)的方法例子
- android 代码实现控件之间的间距
- android FragmentPagerAdapter的“标准”配置
- Android"解决"onTouch和onClick的冲突问题
- android:installLocation简析
- android searchView的关闭事件
- SourceProvider.getJniDirectories