Android 圆形图片开源项目CircleImageView源码分析

来源:互联网 发布:金盾网络验证 编辑:程序博客网 时间:2024/06/06 03:51

上一篇文章中,讲了Android圆形图片实现2种方式中的Xfermode方式。
Android 圆形图片 CircleImageView(Xfermode方式)

今天讲解Android圆形图片实现的另一种方式,BitmapShader(着色器,也叫渲染器)和Matrix(矩阵)方式。
讲解的方式是,分析github上优秀的开源项目:
https://github.com/hdodenhof/CircleImageView

废话不多说,先让项目跑起来,看效果:
这里写图片描述

我们分2部分来讲解:

  1. CircleImageView使用。
  2. 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的实现原理为:

  1. 用图片生成一个BitmapShader(着色器,也叫渲染器)。
  2. 为Bitmapshader设置一个Matrix(矩阵)。
  3. 为Paint(画笔)设置Bitmapshader。
  4. 用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函数开始的,而不是从构造函数开始的。

所以它的流程是:

  1. setImageXXX函数,获取到bitmap图像,进入setup函数。
  2. 构造函数,再次进入setup函数,对变量进行初始化。
  3. 在setup函数中,进行绘画区域大小的计算(calculateBounds方法)。
  4. 在setup函数中,初始化Matrix矩阵,设置缩放和平移。
  5. 调用onDraw函数画图。

接下来,我们对这5个主要步骤中的源码进行分析。

setImageXXX函数

CircleImageView覆写了4个setImageXXX函数,用于获取图片。

@Overridepublic void setImageBitmap(Bitmap bm) {    super.setImageBitmap(bm);    Log.e("CircleImageView", "setImageBitmap");    initializeBitmap();}@Overridepublic void setImageDrawable(Drawable drawable) {    super.setImageDrawable(drawable);    Log.e("CircleImageView", "setImageDrawable");    initializeBitmap();}@Overridepublic void setImageResource(@DrawableRes int resId) {    super.setImageResource(resId);    Log.e("CircleImageView", "setImageResource");    initializeBitmap();}@Overridepublic 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函数中画图,就很简单了。

@Overrideprotected 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改成这样

@Overrideprotected 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实现方式和这种实现方式进行对比。

0 0
原创粉丝点击