您的位置:首页 > 移动开发 > Android开发

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源码分析。

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实现方式和这种实现方式进行对比。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息