上一篇文章中,讲了Android圆形图片实现2种方式中的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.CircleImageViewandroid: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的执行流程中,有一点需要注意的是:
在CircleImageView父类ImageView的构造函数中,会调用setImageXXX函数。
所以它的流程是:
- 父类ImageView的构造函数,调用setImageXXX函数。
- setImageXXX函数,获取到bitmap图像,进入setup函数。setImageXXX函数,获取到bitmap图像,进入setup函数。
- 构造函数,再次进入setup函数,对变量进行初始化。
- 在setup函数中,进行绘画区域大小的计算(calculateBounds方法)。
- 在setup函数中,初始化Matrix矩阵,设置缩放和平移。
- 调用onDraw函数画图。
接下来,我们对这6个主要步骤中的源码进行分析。
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实现方式和这种实现方式进行对比。