Android 圆形图片开源项目CircleImageView源码分析
2016-12-09 21:02
363 查看
上一篇文章中,讲了Android圆形图片实现2种方式中的Xfermode方式。
Android 圆形图片 CircleImageView(Xfermode方式)
今天讲解Android圆形图片实现的另一种方式,BitmapShader(着色器,也叫渲染器)和Matrix(矩阵)方式。
讲解的方式是,分析github上优秀的开源项目:
https://github.com/hdodenhof/CircleImageView
废话不多说,先让项目跑起来,看效果:
我们分2部分来讲解:
CircleImageView使用。
CircleImageView源码分析。
主要就2个文件,一个类文件,一个自定义属性文件。
CircleImageView的使用也很简单,把项目中的CircleImageView类和res/values下的attrs.xml文件考到自己项目相应的目录。
或者把它作为lib依赖到项目中。
有4个自定义属性,一个是圆形图片边框的宽度,一个是边框的颜色。另外2个属性,我也还没有弄明白是什么作用,弄明白之后再补上吧。
示例中添加了2个自定义属性,一个是边框的宽度,为2dp;一个是边框的颜色,为黑色。
需要注意的是,使用CircleImageView时,用到了自定义属性。
要使用自定义属性,需要在布局文件的根布局中添加一条语句,Eclipse和Android studio中添加的有一点区别。
Eclipse中添加(com.zcw.circleimageview为包名):
Android studio中添加:
CircleImageView的使用就是这样啦,是不是很简单。
那什么是BitmapShader?
构造函数有一个Bitmap参数,且不能为空。另外2个参数,分别是x轴和y轴上的渲染方式。
所以,调用这个构造函数会产生一个画有一个位图的渲染器(Shader)。
渲染方式是什么?
渲染方式有哪些?
我们先看第二个问题,再看第一个问题,会比较好理解一些。
渲染方式有3种:
CLAMP 拉伸
REPEAT 平铺
MIRROR 镜像
这3中渲染方式,是不是看着好像似曾相识。没错,就是电脑设置壁纸的方式。
渲染方式可以理解为图像在画布上铺开的方式。
比如电脑设置壁纸,壁纸就是图像,显示屏就是画布。
说到这里,3中渲染方式对应的效果,大家自行结合电脑设置壁纸的效果去体会吧。
在CircleImageView图片处理中,我们使用的CLAMP(拉伸方式)。
可能有人会有疑问,如果使用拉伸方式,那图片不会失真吗?
不会,因为我们会用Matrix对图片进行适当的缩放,使图片正好符合我们的大小。
CircleImageView项目中,需要用到Matrix的缩放和平移效果。
用图片生成一个BitmapShader(着色器,也叫渲染器)。
为Bitmapshader设置一个Matrix(矩阵)。
为Paint(画笔)设置Bitmapshader。
用Paint(画笔)画圆。
第1步中,生成一个Bitmapshader(着色器),相当于有了一张图片。
第2步中,Matrix对图片进行了缩放,以适合我们要求的大小;然后进行平移,保证画出来的图像是原来图像的正中心。
第3步中,把Bitmapshader设置给一支画笔,那这种画笔画出来的内容,就是图片的内容。
第4步,指定绘画的形状。
它是从setImageXXX函数开始的,而不是从构造函数开始的。
所以它的流程是:
setImageXXX函数,获取到bitmap图像,进入setup函数。
构造函数,再次进入setup函数,对变量进行初始化。
在setup函数中,进行绘画区域大小的计算(calculateBounds方法)。
在setup函数中,初始化Matrix矩阵,设置缩放和平移。
调用onDraw函数画图。
接下来,我们对这5个主要步骤中的源码进行分析。
在示例中,调用的是setImageDrawable方法。
在setImageDrawable方法的调用链:
setImageDrawable——initializeBitmap——getBitmapFromDrawable——setup。
在getBitmapFromDrawable函数中,拿到图片;然后第一次进入setup函数。
注意,第一次进入setup函数时,并没有进入init函数把mReady变量设置为true。
所以第一次进入setup函数时,mReady = false,把mSetupPending设置为true就退出了。
这一段代码的作用是,当mBorderOverlay为false时,图像的绘画边缘,会比边框的小一点,可以避免边框的色差问题。
mBorderOverlay为false和true的效果如下所示:
有3个构造函数,第一个构造函数,用于在代码中动态添加CircleImageView使用。
第3个构造函数中,获取了自定义属性。
每个构造函数都会调用init方法。
init代码如下:
在代码中,把mReady设置为true,因为第一次进入setup函数,把mSetupPending设置为了true,所有会再次调用setup函数。
这一段代码的作用是,处理padding值,然后从图像中得到一个最大的正方形区域。
calculateBounds的放回值,设置了边框的绘制区域。
图像的绘制区域,要比边框的小一些,在如下代码中进行了设置。
在函数中,这一句代码比较难理解:
其实等价于这一句代码:
这一句代码作用是,比较图片和所绘区域宽缩放比、高缩放比,那个小。取小的,作为矩阵的缩放比。
至于为什么用乘法,而不用除法,我想应该是为了避免出现除数为0的情况。
设置缩放比之后,对矩阵设置了平移
其中(dx + 0.5f)的处理,是四舍五入。
在前面的步骤中,我们指定了画图的内容,指定了画图的区域,指定了合适的缩放和平移。
在onDraw中,我们只要指定画图的形状就行了。
比如我们把onDarw改成这样
我们画出的就是圆角图片了,如下图所示:
项目中一些坐标计算的代码,大家自行去理解吧。
到这里,CircleImageView开源项目就讲解完毕了。
今天就先写到这里,之后可能会更新,对Xfermode实现方式和这种实现方式进行对比。
Android 圆形图片 CircleImageView(Xfermode方式)
今天讲解Android圆形图片实现的另一种方式,BitmapShader(着色器,也叫渲染器)和Matrix(矩阵)方式。
讲解的方式是,分析github上优秀的开源项目:
https://github.com/hdodenhof/CircleImageView
废话不多说,先让项目跑起来,看效果:
我们分2部分来讲解:
CircleImageView使用。
CircleImageView源码分析。
CircleImageView的使用
CircleImageView的结构很简单:主要就2个文件,一个类文件,一个自定义属性文件。
CircleImageView的使用也很简单,把项目中的CircleImageView类和res/values下的attrs.xml文件考到自己项目相应的目录。
或者把它作为lib依赖到项目中。
CircleImageView自定义属性
CircleImageView自定义属性如下:<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="CircleImageView"> <attr name="civ_border_width" format="dimension" /> <attr name="civ_border_color" format="color" /> <attr name="civ_border_overlay" format="boolean" /> <attr name="civ_fill_color" format="color" /> </declare-styleable> </resources>
有4个自定义属性,一个是圆形图片边框的宽度,一个是边框的颜色。另外2个属性,我也还没有弄明白是什么作用,弄明白之后再补上吧。
使用CircleImageView
在布局文件中使用CircleImageView很简单,就和使用ImageView是一样的。代码如下:<de.hdodenhof.circleimageview.CircleImageView android:layout_width="160dp" android:layout_height="160dp" android:layout_centerInParent="true" android:src="@drawable/hugh" app:civ_border_width="2dp" app:civ_border_color="@color/dark" />
示例中添加了2个自定义属性,一个是边框的宽度,为2dp;一个是边框的颜色,为黑色。
需要注意的是,使用CircleImageView时,用到了自定义属性。
要使用自定义属性,需要在布局文件的根布局中添加一条语句,Eclipse和Android studio中添加的有一点区别。
Eclipse中添加(com.zcw.circleimageview为包名):
xmlns:zcw="http://schemas.android.com/apk/res/com.zcw.circleimageview"
Android studio中添加:
xmlns:app="http://schemas.android.com/apk/res-auto"
CircleImageView的使用就是这样啦,是不是很简单。
CircleImageView源码分析
CircleImageView项目采用的方式是BitmapShader(着色器,也叫渲染器)和Matrix(矩阵)方式实现的。那什么是BitmapShader?
BitmapShader(着色器,也叫渲染器)简单介绍
Bitmapshader是Shader的子类,只有一个构造函数,如下:/** * Call this to create a new shader that will draw with a bitmap. * * @param bitmap The bitmap to use inside the shader * @param tileX The tiling mode for x to draw the bitmap in. * @param tileY The tiling mode for y to draw the bitmap in. */ public BitmapShader(@NonNull Bitmap bitmap, TileMode tileX, TileMode tileY) { mBitmap = bitmap; mTileX = tileX; mTileY = tileY; init(nativeCreate(bitmap, tileX.nativeInt, tileY.nativeInt)); }
构造函数有一个Bitmap参数,且不能为空。另外2个参数,分别是x轴和y轴上的渲染方式。
所以,调用这个构造函数会产生一个画有一个位图的渲染器(Shader)。
渲染方式是什么?
渲染方式有哪些?
我们先看第二个问题,再看第一个问题,会比较好理解一些。
渲染方式有3种:
CLAMP 拉伸
REPEAT 平铺
MIRROR 镜像
这3中渲染方式,是不是看着好像似曾相识。没错,就是电脑设置壁纸的方式。
渲染方式可以理解为图像在画布上铺开的方式。
比如电脑设置壁纸,壁纸就是图像,显示屏就是画布。
说到这里,3中渲染方式对应的效果,大家自行结合电脑设置壁纸的效果去体会吧。
在CircleImageView图片处理中,我们使用的CLAMP(拉伸方式)。
可能有人会有疑问,如果使用拉伸方式,那图片不会失真吗?
不会,因为我们会用Matrix对图片进行适当的缩放,使图片正好符合我们的大小。
Matrix(矩阵)简单介绍
矩阵在图像处理中,可以实现图片平移、缩放等效果。CircleImageView项目中,需要用到Matrix的缩放和平移效果。
CircleImageView的实现原理
CircleImageView的实现原理为:用图片生成一个BitmapShader(着色器,也叫渲染器)。
为Bitmapshader设置一个Matrix(矩阵)。
为Paint(画笔)设置Bitmapshader。
用Paint(画笔)画圆。
第1步中,生成一个Bitmapshader(着色器),相当于有了一张图片。
第2步中,Matrix对图片进行了缩放,以适合我们要求的大小;然后进行平移,保证画出来的图像是原来图像的正中心。
第3步中,把Bitmapshader设置给一支画笔,那这种画笔画出来的内容,就是图片的内容。
第4步,指定绘画的形状。
CircleImageView中的主要变量
CircleImageView中的主要变量如下:private final RectF mDrawableRect = new RectF(); // 画图形的区域 private final RectF mBorderRect = new RectF(); // 画边框的区域 private final Matrix mShaderMatrix = new Matrix(); // 矩阵 private final Paint mBitmapPaint = new Paint(); // 画图像的画笔 private final Paint mBorderPaint = new Paint(); // 画边框的画笔 private final Paint mFillPaint = new Paint(); private int mBorderColor = DEFAULT_BORDER_COLOR; // 边框颜色 private int mBorderWidth = DEFAULT_BORDER_WIDTH; // 边框宽度 private int mFillColor = DEFAULT_FILL_COLOR; private Bitmap mBitmap; // 图像 private BitmapShader mBitmapShader; // 着色器 private int mBitmapWidth; // 图像的宽 private int mBitmapHeight; // 图像的高 private float mDrawableRadius; // 所画圆形图像的半径 private float mBorderRadius; // 所画边框的半径
CircleImageView的执行流程
CircleImageView的执行流程中,有一点需要注意的是:它是从setImageXXX函数开始的,而不是从构造函数开始的。
所以它的流程是:
setImageXXX函数,获取到bitmap图像,进入setup函数。
构造函数,再次进入setup函数,对变量进行初始化。
在setup函数中,进行绘画区域大小的计算(calculateBounds方法)。
在setup函数中,初始化Matrix矩阵,设置缩放和平移。
调用onDraw函数画图。
接下来,我们对这5个主要步骤中的源码进行分析。
setImageXXX函数
CircleImageView覆写了4个setImageXXX函数,用于获取图片。@Override public void setImageBitmap(Bitmap bm) { super.setImageBitmap(bm); Log.e("CircleImageView", "setImageBitmap"); initializeBitmap(); } @Override public void setImageDrawable(Drawable drawable) { super.setImageDrawable(drawable); Log.e("CircleImageView", "setImageDrawable"); initializeBitmap(); } @Override public void setImageResource(@DrawableRes int resId) { super.setImageResource(resId); Log.e("CircleImageView", "setImageResource"); initializeBitmap(); } @Override public void setImageURI(Uri uri) { super.setImageURI(uri); Log.e("CircleImageView", "setImageURI"); initializeBitmap(); }
在示例中,调用的是setImageDrawable方法。
在setImageDrawable方法的调用链:
setImageDrawable——initializeBitmap——getBitmapFromDrawable——setup。
在getBitmapFromDrawable函数中,拿到图片;然后第一次进入setup函数。
setup函数
setup函数代码如下:private void setup() { if (!mReady) { mSetupPending = true; return; } if (getWidth() == 0 && getHeight() == 0) { return; } if (mBitmap == null) { invalidate(); return; } mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); mBitmapPaint.setAntiAlias(true); mBitmapPaint.setShader(mBitmapShader); mBorderPaint.setStyle(Paint.Style.STROKE); mBorderPaint.setAntiAlias(true); mBorderPaint.setColor(mBorderColor); mBorderPaint.setStrokeWidth(mBorderWidth); mFillPaint.setStyle(Paint.Style.FILL); mFillPaint.setAntiAlias(true); mFillPaint.setColor(mFillColor); mBitmapHeight = mBitmap.getHeight(); mBitmapWidth = mBitmap.getWidth(); mBorderRect.set(calculateBounds()); mBorderRadius = Math.min((mBorderRect.height() - mBorderWidth) / 2.0f, (mBorderRect.width() - mBorderWidth) / 2.0f); mDrawableRect.set(mBorderRect); if (!mBorderOverlay && mBorderWidth > 0) { mDrawableRect.inset(mBorderWidth - 1.0f, mBorderWidth - 1.0f); } mDrawableRadius = Math.min(mDrawableRect.height() / 2.0f, mDrawableRect.width() / 2.0f); applyColorFilter(); updateShaderMatrix(); invalidate(); }
注意,第一次进入setup函数时,并没有进入init函数把mReady变量设置为true。
所以第一次进入setup函数时,mReady = false,把mSetupPending设置为true就退出了。
这一段代码的作用是,当mBorderOverlay为false时,图像的绘画边缘,会比边框的小一点,可以避免边框的色差问题。
if (!mBorderOverlay && mBorderWidth > 0) { mDrawableRect.inset(mBorderWidth - 1.0f, mBorderWidth - 1.0f); }
mBorderOverlay为false和true的效果如下所示:
进入构造函数
接下来进入构造函数public CircleImageView(Context context) { super(context); init(); } public CircleImageView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CircleImageView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); Log.e("CircleImageView", "构造函数"); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView, defStyle, 0); mBorderWidth = a.getDimensionPixelSize(R.styleable.CircleImageView_civ_border_width, DEFAULT_BORDER_WIDTH); mBorderColor = a.getColor(R.styleable.CircleImageView_civ_border_color, DEFAULT_BORDER_COLOR); mBorderOverlay = a.getBoolean(R.styleable.CircleImageView_civ_border_overlay, DEFAULT_BORDER_OVERLAY); mFillColor = a.getColor(R.styleable.CircleImageView_civ_fill_color, DEFAULT_FILL_COLOR); a.recycle(); init(); }
有3个构造函数,第一个构造函数,用于在代码中动态添加CircleImageView使用。
第3个构造函数中,获取了自定义属性。
每个构造函数都会调用init方法。
init代码如下:
private void init() { super.setScaleType(SCALE_TYPE); mReady = true; if (mSetupPending) { setup(); mSetupPending = false; } }
在代码中,把mReady设置为true,因为第一次进入setup函数,把mSetupPending设置为了true,所有会再次调用setup函数。
再次进入setup函数
再次进入setup函数中,对变量进行了初始化。在setup函数中,计算绘画区域
初始化一些变量之后,调用了calculateBounds,计算绘画区域:private RectF calculateBounds() { int availableWidth = getWidth() - getPaddingLeft() - getPaddingRight(); int availableHeight = getHeight() - getPaddingTop() - getPaddingBottom(); int sideLength = Math.min(availableWidth, availableHeight); float left = getPaddingLeft() + (availableWidth - sideLength) / 2f; float top = getPaddingTop() + (availableHeight - sideLength) / 2f; return new RectF(left, top, left + sideLength, top + sideLength); }
这一段代码的作用是,处理padding值,然后从图像中得到一个最大的正方形区域。
calculateBounds的放回值,设置了边框的绘制区域。
图像的绘制区域,要比边框的小一些,在如下代码中进行了设置。
mBorderRadius = Math.min((mBorderRect.height() - mBorderWidth) / 2.0f, (mBorderRect.width() - mBorderWidth) / 2.0f);
mDrawableRect.set(mBorderRect);
if (!mBorderOverlay && mBorderWidth > 0) { mDrawableRect.inset(mBorderWidth - 1.0f, mBorderWidth - 1.0f); }
mDrawableRadius = Math.min(mDrawableRect.height() / 2.0f, mDrawableRect.width() / 2.0f);
在setup函数中,进行Matrix(矩阵)的初始化
在setup函数中,调用updateShaderMatrix进行矩阵的初始化private void updateShaderMatrix() { float scale; float dx = 0; float dy = 0; mShaderMatrix.set(null); // 计算图片缩放的倍数,取一个比较小的缩放倍数 if (mBitmapWidth * mDrawableRect.height() > mDrawableRect.width() * mBitmapHeight) { scale = mDrawableRect.height() / (float) mBitmapHeight; dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f; } else { scale = mDrawableRect.width() / (float) mBitmapWidth; dy = (mDrawableRect.height() - mBitmapHeight * scale) * 0.5f; } mShaderMatrix.setScale(scale, scale); mShaderMatrix.postTranslate((int) (dx + 0.5f) + mDrawableRect.left, (int) (dy + 0.5f) + mDrawableRect.top); mBitmapShader.setLocalMatrix(mShaderMatrix); }
在函数中,这一句代码比较难理解:
if (mBitmapWidth * mDrawableRect.height() > mDrawableRect.width() * mBitmapHeight)
其实等价于这一句代码:
if (mBitmapWidth / mDrawableRect.width() > mBitmapHeight / mDrawableRect.height())
这一句代码作用是,比较图片和所绘区域宽缩放比、高缩放比,那个小。取小的,作为矩阵的缩放比。
至于为什么用乘法,而不用除法,我想应该是为了避免出现除数为0的情况。
设置缩放比之后,对矩阵设置了平移
mShaderMatrix.postTranslate((int) (dx + 0.5f) + mDrawableRect.left, (int) (dy + 0.5f) + mDrawableRect.top);
其中(dx + 0.5f)的处理,是四舍五入。
在onDraw函数中画图
完成以上设置之后,在onDraw函数中画图,就很简单了。@Override protected void onDraw(Canvas canvas) { if (mDisableCircularTransformation) { super.onDraw(canvas); return; } if (mBitmap == null) { return; } if (mFillColor != Color.TRANSPARENT) { canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mFillPaint); } canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mBitmapPaint); if (mBorderWidth > 0) { canvas.drawCircle(mBorderRect.centerX(), mBorderRect.centerY(), mBorderRadius, mBorderPaint); } }
在前面的步骤中,我们指定了画图的内容,指定了画图的区域,指定了合适的缩放和平移。
在onDraw中,我们只要指定画图的形状就行了。
比如我们把onDarw改成这样
@Override protected void onDraw(Canvas canvas) { if (mDisableCircularTransformation) { super.onDraw(canvas); return; } if (mBitmap == null) { return; } if (mFillColor != Color.TRANSPARENT) { canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mFillPaint); } canvas.drawRoundRect(mDrawableRect, 40, 40, mBitmapPaint); }
我们画出的就是圆角图片了,如下图所示:
项目中一些坐标计算的代码,大家自行去理解吧。
到这里,CircleImageView开源项目就讲解完毕了。
今天就先写到这里,之后可能会更新,对Xfermode实现方式和这种实现方式进行对比。
相关文章推荐
- 使用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的关闭事件