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

news/2025/2/15 15:46:24/

上一篇文章中,讲了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.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的实现原理为:

  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的执行流程中,有一点需要注意的是:
在CircleImageView父类ImageView的构造函数中,会调用setImageXXX函数。

所以它的流程是:

  1. 父类ImageView的构造函数,调用setImageXXX函数。
  2. setImageXXX函数,获取到bitmap图像,进入setup函数。setImageXXX函数,获取到bitmap图像,进入setup函数。
  3. 构造函数,再次进入setup函数,对变量进行初始化。
  4. 在setup函数中,进行绘画区域大小的计算(calculateBounds方法)。
  5. 在setup函数中,初始化Matrix矩阵,设置缩放和平移。
  6. 调用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实现方式和这种实现方式进行对比。


http://www.ppmy.cn/news/246369.html

相关文章

java简单的图片处理程序

import java.io.File;import java.io.FileNotFoundException;import java.io.IOException;import java.io.RandomAccessFile; public class Demo04 { public static void main(String[] args){ //图片变亮变暗&#xff0c;使用缓冲数组 //注意如果想要实现上面的三个效果&#…

Python+tkinter(界面设计)实现高清大图片图片保存

文章目录 前言基本开发环境分析网页开始工作实现以下是全部代码 作为爱玩电脑的你是不是也需要经常更换一下自己的电脑壁纸呢? 换上一张心仪的图片整个人都舒畅多了。但是在网上有很多心仪的图片想要保存下来&#xff0c;如果一张张的去保存那效率又低&#xff0c;而后面的壁纸…

【博客639】Life of a label in prometheus

prometheus中label的生命周期 前言 Prometheus labels allow you to model your application deployment in the manner best suited to your organisation. As directly supporting every potential configurations would be impossible, we offer relabelling to give you t…

day 49 :121. 买卖股票的最佳时机;122. 买卖股票的最佳时机 II;123. 买卖股票的最佳时机 III

买卖股票 121. 买卖股票的最佳时机&#xff1a;一次买入卖出1. 贪心算法2. 动态规划1. dp数组以及下标名义2. 递归公式3. dp数组如何初始化4. 代码 122. 买卖股票的最佳时机 II:可以多次买入卖出2. 动态规划1. dp数组以及下标名义2. 递归公式3. dp数组如何初始化4. 代码 123. 买…

Web3.0概念

学习web3您需要先掌握 JavaScript node React 后续 我们将学习一门新的语言 叫 Solidity 他是一种只能合约语言开发 我们利用web3将不再依赖后端 而是连接只能合约开发 首先 我们先不用急着写代码 还是要概念为先 首先 我们来对比 WEB1.0到3.0的概念 首先 web1.0 更多处于信…

unity 开发中10个小知识(一)

现在记忆力越来越差&#xff0c;写过很多遍的内容&#xff0c;都有可能需要慢慢才能想起来&#xff0c;这里就记录下在unity开发过程中一些小的知识点 一、获取unity层级和layerMask int ground LayerMask.NameToLayer("Ground"); int groundMask 1<<ground…

Qt/GUI/布局/实现窗口折叠效果/且在操作时父窗口尺寸跟随变动

文章目录 概述无法resize到小尺寸可行方案其他方案 概述 本文旨在&#xff0c;实现如下所示的显示或隐藏 ‘附加选项’ 的效果&#xff0c;以折的不常用信息和操作项&#xff0c;减少普通用户负担&#xff0c;提升用户体验。在某些软件中此类窗口折叠效果&#xff0c;常用 “……

地图实火!断货加印,限时折扣抢购通道开启

&#xff08;关注公众号点击图片三折购买《社交泛娱乐出海作战地图》&#xff09; 实火&#xff01; 融云自制《社交泛娱乐出海作战地图》 “WICC 泛娱乐出海嘉年华”最热单品 关注【融云全球互联网通信云】了解更多 《出海作战地图》线下首发立刻引爆现场&#xff0c;“如…