博主声明:
转载请在开头附加本文链接及作者信息,并标记为转载。本文由博主 威威喵 原创,请多支持与指教。
本文首发于此 博主:威威喵 | 博客主页:https://blog.csdn.net/smile_running
我们来介绍一款开源框架:CircleImageView,相信绝大部分的android开发者都使用过。而且它在github上已经有上万个star,可以说是一款相当热门的开源库。这款开源框架的作用如其名称,将图片设置成圆形图。那么它为什么这么热门呢,当然有它必然的理由。首先,我们看它的用法:
它的用法比较简单、易用,就好比系统控件一样方便。当然,在使用之前,你必须提供它的依赖:
dependencies {
...
implementation 'de.hdodenhof:circleimageview:2.2.0'
}
那么它的显示结果就是这个样子:
所以,这就是CircleImageView的使用方式了,也是github上hdodenhof大神的原文使用教程。因为使用比较简单,我就直接copy了一份过来。本文以简单的使用方式作为铺垫,为了帮助一些没有使用过的读者,所以这里就借花献佛,依照原教程写了一份。
接下来,我将带你走进CircleImageView的源码,带你一起读一下大神的思想。看到源码你会觉得很庆幸,为什么怎么说呢?因为源码就只有一个类和一个xml文件。你没有看错,确实是这样的。我们来看看项目结构:
首先,我们看xml文件,这个文件里自定义了几个属性方法。分别是:
根据上面提供的属性方法,你可以设置CircleImageView的边框宽度、颜色、是否覆盖、背景颜色等。当然,建议大家还是自己去实现一下,体验这些属性的效果区别。
注意:上面的注释部分提及,已经废弃设置背景颜色这个属性方法。
接着,我们来看主要类,也是唯一的一个类。虽然这个类将近500行的代码,不过我们还是来解读一下作者是怎么实现以及作者的思路,这样我们才能一步一步的接近大神们的思想。
当你进入这个类时,你首先会发现这个类并不是继承自View,而是直接继承了ImageView类来实现的。这样做有一个好处就是我们不需要写大量的代码来布局和测量,还能够直接使用ImageView类已经封装好的一些代码,所以大大降低了代码量。
为了更好的理解作者的思路,我们在读源码时可以利用画图、流程图等绘制工具来一步一步的跟进作者的思路,别让自己陷入错误的思路。所以,我花了一点时间画了一张思路图:
这张图大致的列出了这个类的主要的属性设置以及一些重要的方法。当然,还有一些其他的方法,我也会一并讲解。图的右边是一些重要变量属性的设置,例如scaleType(缩放类型)、画笔、着色器、位图等等,还有一些我没写出来的变量。
那么,这些变量的具体如何去设置,它们的属性我就不在此介绍了。因为涉及太多了方面的知识了,建议大家没见过的或者不熟悉的类或者属性,自己去补补。本人也是如此,在读这份源码之前也补了很多基础知识。比如:Bitmap、Bitmap.Config、BitmapShader等。
所以说,读源码是强迫自己学习未知知识的好方式,因为你没这些知识做铺垫的话,将很难读得懂源码。当然,必定还会碰到一些算法问题。所以说,算法是非常重要的,也是程序员的内功。
以上,仅仅是本人提供的一些阅读源码的方法,你也许有更好的方式。
再扯回来,我们看看CircleImageView的构造函数。首先,我们的入手点应该是它的构造函数,看看它究竟干了些什么事情。那么就得看看代码了:
public CircleImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
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);
// Look for deprecated civ_fill_color if civ_circle_background_color is not set
if (a.hasValue(R.styleable.CircleImageView_civ_circle_background_color)) {
mCircleBackgroundColor = a.getColor(R.styleable.CircleImageView_civ_circle_background_color,
DEFAULT_CIRCLE_BACKGROUND_COLOR);
} else if (a.hasValue(R.styleable.CircleImageView_civ_fill_color)) {
mCircleBackgroundColor = a.getColor(R.styleable.CircleImageView_civ_fill_color,
DEFAULT_CIRCLE_BACKGROUND_COLOR);
}
a.recycle();
init();
}
构造函数前部分内容仅仅是获取在布局中设置的属性内容,就是上面提及的xml文件里的属性方法。看到最后,它调用了init()方法,这意味着初始化开始了。我们跟着作者的思路,一步一步的解读。
先看看源码(以下套路都是如此):
private void init() {
super.setScaleType(SCALE_TYPE);
mReady = true;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setOutlineProvider(new OutlineProvider());
}
if (mSetupPending) {
setup();
mSetupPending = false;
}
}
通过初始化,为CircleImageView控件设置了默认的缩放类型,也就是CENTER_CROP模式。并且还不支持其他类型,如果你设置了其他的,那么对不起,我将报异常。代码如下:
private static final ScaleType SCALE_TYPE = ScaleType.CENTER_CROP;
@Override
public void setScaleType(ScaleType scaleType) {
if (scaleType != SCALE_TYPE) {
throw new IllegalArgumentException(String.format("ScaleType %s not supported.", scaleType));
}
}
接着是判断5.0以上的系统,为其设置了轮廓,这个我们先不理它。接下来是整个CircleImageView的重头戏:调用了setup()方法。但是,这里作者添加了一个判断,我们得搞清楚作者的意图是什么。
你有没有发现,加了这个判断,第一次init()时,setup()函数是不会执行的。这么做的原因就是为了初始化Bitmap,保证在setup()方法执行时,Bitmap已经被加载好。
这是因为我们在CircleImageView控件中会设置一个src属性,或者你会调用setBitmap...(...)方法来设置要显示图片。那么为了保证Bitmap的初始化,它做了这样的事情:
private void initializeBitmap() {
if (mDisableCircularTransformation) {
mBitmap = null;
} else {
mBitmap = getBitmapFromDrawable(getDrawable());
}
setup();
}
然后,在initializeBitmap()方法中去调用setup()方法,从而保证了bitmap不为空。而bitmap的获取方式,就是getBitmapFromDrawable(...)方法。我们且来看看:
private Bitmap getBitmapFromDrawable(Drawable drawable) {
if (drawable == null) {
return null;
}
if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable) drawable).getBitmap();
}
try {
Bitmap bitmap;
if (drawable instanceof ColorDrawable) {
bitmap = Bitmap.createBitmap(COLORDRAWABLE_DIMENSION, COLORDRAWABLE_DIMENSION, BITMAP_CONFIG);
} else {
bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), BITMAP_CONFIG);
}
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
以上是获取Bitmap的代码,其实也没什么好说明的。不得不说作者的考虑还是挺全面的,考虑到了多种方式去获取Bitmap的多种类型。这就与我们的setBitmap...(param)的方法里传入的参数息息相关了,这里不是特别难理解。
在初始化完了bitmap之后,就要进入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);
mCircleBackgroundPaint.setStyle(Paint.Style.FILL);
mCircleBackgroundPaint.setAntiAlias(true);
mCircleBackgroundPaint.setColor(mCircleBackgroundColor);
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();
}
乍一看,哇,蛮长滴。不过不慌,因为这是这个类中唯一一个比较长的方法了。在这个方法里,处理了很多事情。
比如:1、判断构造函数执行结果;
2、判断控件的宽、高是否为0;
3、再次判断bitmap是否为空;
4、进行画笔的初始化。
还有一堆堆的处理,我就不依次列举了。可以看到作者做了这么多的空判断,逻辑可谓是真滴严谨,这也是我们该学习的地方。只有严谨的代码,才能茁壮的跑在机器上。
我们看mBorderRect.set(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);
}
如果你觉得上面的代码不好理解,那么完全没有关系。因为我画了一张草图,诠释了上面代码所做的事情:
再解释一下为什么要取得宽、高的一个最小值。你想想啊,我们圆的一周的半径都是相等的,那么矩形的内接圆的半径肯定是以短边为半径的。所以,这就是为什么要取短的一边来重新构成一个矩形的原因了。
好了,我们继续往下看。它把计算好的矩形赋给了border,也就相当于给CircleImageView控件加了一个透明边框。当然,默认是黑色的,你可以对边框设置颜色、宽度。所以,作者提供了一个设置边框宽度的方法。取得这个宽度,进行边框半径的设置,那么只要宽度大于0的话,将在图片上产生一个边框遮罩效果。
再接下来就是图片的半径设置了,其实都是一样的操作。在这里设置这些参数的原因,是为了在onDraw()方法里绘制处理。为了能够动态更新,作者将这些方法抽离到一个setup()函数里,再调用invalidate()进行刷新绘制。
在setup()函数末尾,它还要调用了两个方法。分别是:applyColorFilter()和updateShaderMatrix()方法。第一个很简单,就是应用颜色过滤器,但一般也不会用这个方法,有兴趣的自己去研究。
我们重点看看updateShaderMatrix()方法,这个是为了更新shader矩阵,我们看看源码:
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);
}
在这个方法里,可能会比较难理解。这是CircleImageView中最难的一个方法了。刚刚读的时候,我也不知道它要干什么,看代码是大概知道它在做缩放、平移阵列的操作。但是,我并不懂这样做的作用和意义,所以啊,去查了一下才搞懂了。
它的最重要的作用是:设置BitmapShader的Matrix参数,对图片mBitmap位置用缩放、平移形式填充,目的是用最小的缩放比例,使得图片的某个方向的边的尺寸缩放到图片显示区域(mDrawableRect)一样。做到了图片损失度最小。同时scale保证Bitmap的宽或高和目标区域一致,那么高或宽就需要进行位移,使得Bitmap居中。
所以,一切归根结底还是得扔给onDraw()方法去绘制,不然设置了这么多的属性和写的方法又有何用?接着,本文将结束最后的环节,那就是看看onDraw()的源码了:
@Override
protected void onDraw(Canvas canvas) {
if (mDisableCircularTransformation) {
super.onDraw(canvas);
return;
}
if (mBitmap == null) {
return;
}
if (mCircleBackgroundColor != Color.TRANSPARENT) {
canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mCircleBackgroundPaint);
}
canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mBitmapPaint);
if (mBorderWidth > 0) {
canvas.drawCircle(mBorderRect.centerX(), mBorderRect.centerY(), mBorderRadius, mBorderPaint);
}
}
依然如此,逻辑还是要严谨的,先进行非空判断。onDraw()函数里进行了三个绘制圆的操作,但两个是有条件的。第一个,判断背景颜色不是透明,才进行绘制背景。第二个,绘制圆形图片,这也是必然的,否则之前的一切将毫无意义。第三个,绘制遮罩边框的圆,还是得判断边框的宽度大于0才进行绘制。
那么至此,本文对开源框架:CircleImageView源码的分析已经结束了。虽然有一些函数、属性没有在本文提及,但是还是建议大家能自己去看看源码,这样理解的更加透彻。