相信很多人都用过此控件:
Paste_Image.png如何使用就不在这样叙述了,直接使用的观客可以到这里
https://github.com/hdodenhof/CircleImageView
本文从自定义View学习的角度出发剖析作者是如何编写这类型控件的。
自定义View的分类:
基本上自定义View分为三类:直接继承View,拓展系统已有的View(本文剖析属于此类),继承ViewGroup或其他ViewGroup的子类。废话少说,开始分析此类。该控件就这么一个类;所以算相对容易的。
CircleImageView构造函数
Paste_Image.png继承ImageView
基本上重写三个构造函数,其实就是调用三个参数的那个,都是套路呀。。。
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);
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();
}
我们看看Image源码的构造函数:
Paste_Image.png都是同一个套路:
基本一个参数的构造函数是在java代码中new出来的
两个参数的构造函数就是通过xml编写调用的
两个参数其实是调用三个参数的,那么第三个参数就是关于主题的。
看看三个参数的构造函数到底干了些什么
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);
mFillColor = a.getColor(R.styleable.CircleImageView_civ_fill_color, DEFAULT_FILL_COLOR);
a.recycle();
init();
}
就这么些,一句句给大家逐一解答:
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView, defStyle, 0);
此行意思是从xml布局文件中获取自定义view类型的一个类型数组;
其实我们自定义view有些时候会自定义一些属性,就会在一个xml文件上先写好需要定义什么属性,这个属性的类型是什么,供布局的xml调用者使用;
该CircleImageView就自定义了这些属性。看看这里的xml的declare-styleable中的name属性和R.styleable.CircleImageView是一致的。
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);
这四行基本一样就放在一起讲,就是通过刚才定义的类型数组获取各自得到的数值,如果没有得到布局xml文件的数值就会提供一个默认值给它。这四个方法都是两个参数,第二个参数就是默认值,而第一个就是布局xml文件调用者写的值;
这里说过题外话:对于获取dp或sp值的,基本上都是这样书写,原因是安卓通过dp或sp获取到对应的px。在java代码里面获取到的都是px为单位的。而在布局xml是dp或sp:
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,16,getResources().getDisplayMetrics())
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,1,getResources().getDisplayMetrics())
这种写法就是给默认值的数值转换为dp或sp单位的。
a.recycle();
该方法就是获取完xml布局文件的属性都需要将TypeArray回收,方便之后使用。ImageView源码亦都一样,所以官方的源码一定要多看呀。。。
构造函数看完再看它最后调用的一个方法init()
private void init(){
super.setScaleType(SCALE_TYPE);
mReady = true;
if(mSetupPending){
setup();
mSetupPending = false;
}
}
调用父类的setScaleType为CircleImageView设置缩放策略。该CircleImageView只支持ScaleType.CENTER_CROP这种缩放策略:意思是将图片等比例缩放在View上,不会对其进行拉伸填满View;大白话就是图片都会显示在View上,如果图片比例大于View即剪切多余部分显示正中间部分;其他的缩放策略可以看看此介绍,个人觉得说得不错,这里就不描述:
http://blog.csdn.net/buaaroid/article/details/49360779
再看看源码为什么说该控件只支持ScaleType.CENTER_CROP属性:
private static final ScaleType SCALE_TYPE = ScaleType.CENTER_CROP;
super.setScaleType(SCALE_TYPE);
@Override
public void setScaleType(ScaleType scaleType) {
if(scaleType != SCALE_TYPE){
throw new IllegalArgumentException(String.format("ScaleType %s not supported.",scaleType));
}
}
CircleImageView重写了ImageView的setScaleType方法;只要用其他的scaleType即报错
setup()方法
这里有两个变量:mReady和mSetupPending;它们是用来控制setup方法被调用的。
这里需要解释整个CircleImageView的调用流程,之后再将逐一方法进行讲解。
整个CircleImageView的调用流程
- 1、由于是继承ImageView,所以先调用父类的构造函数,当调用ImageView的构造函数是调用了setImageDrawable方法 --->setup方法 mReady为false,mSetupPending为true,这样只能到setup第一个判断
initializeBitmap()方法通过getBitmapFromDrawable()方法获取bitmap
2、再走构造方法 --> init()方法 由于现在 还没有调用onMeasure ,所以通过不了第二个判断 ,现在的宽高是0
3、onMeasure方法被调用,多次被调用,获得宽高
4、获得宽高后,onSizeChange()被调用 先调用父类的,然后再次被调用onMeasure---> 再次调用setup方法
mBorderRect设置矩形区域,mFillRect设置矩形区域;mBorderRadius\mFillRadius设置半径
5、在setup() 调用applyColorFilter() 和updateShaderMatrix()
6、invalidate() 重绘调用onDraw()方法
整个流程大概就是这样。可能这样看不是太清楚,看官们可以在源码上debug看看是不是这样的。毕竟“纸上得来终觉浅”
揭开setup方法的面纱
简单来说setup方法主要确定CircleImageView所占的范围,图片如何在CircleImageView显示。
现在每行代码分析:
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);
BitmapShader就是一个位图着色器,简单来说就是将图片呈现在canvas画布中。
Shader.TileMode.CLAMP该属性是图片最后一个像素进行拉伸处理;一般用此属性居多,另外两个分别是:
REPEAT :横向和纵向的重复渲染器图片,平铺。
MIRROR :横向和纵向的重复渲染器图片,这个和REPEAT重复方式不一样,他是以镜像方式平铺。
mBitmapPaint.setAntiAlias(true);就是抗锯齿
mBitmapPaint.setShader(mBitmapShader);将画笔设置bitmapShader
mBorderPaint.setStyle(Paint.Style.STROKE);
mBorderPaint.setAntiAlias(true);
mBorderPaint.setColor(mBorderColor);
mBorderPaint.setStrokeWidth(mBorderWidth);
CircleImageView分别有三个画笔分别上面代码展示的负责图片mBitmapPaint,负责边界的即这里的mBorderPaint,还有负责填充的mFillPaint;上面四行代码就是给边界画笔作准备,准备画一个空心圆。
mBorderPaint.setStyle(Paint.Style.STROKE);
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);
//初始图片显示区域为mBorderRect(CircleImageView中图片区域的实际大小)
mDrawableRect.set(mBorderRect);
if(!mBorderOverlay && mBorderWidth > 0){
//这里是当存在边界的情况将mDrawableRect的矩形区域缩小,注意这里获取到的mBorderWidth是px单位的非xml中的dp
mDrawableRect.inset(mBorderWidth - 1.0f,mBorderWidth - 1.0f);
}
//取图片显示区域的矩形的半径,该半径是少于或等于边界矩形的半径,因为存在边界即少于,不存在即等于
mDrawableradius = Math.min(mDrawableRect.height()/2.0f,mDrawableRect.width()/2.0f);
setup方法说完再看看里面计算矩形区域与矩阵控制缩放的方法
calculateBounds()计算矩形区域的边界
private RectF calculateBounds(){
//出去内边距获取真正显示图片的区域
int availableWidth = getWidth() - getPaddingLeft() - getPaddingRight();
int availableHeight = getHeight() - getPaddingBottom() - getPaddingTop();
//取长宽之间较小的那个长度
int sideLength = Math.min(availableWidth,availableHeight);
//这里计算的左坐标与上的坐标,其实就是为了左右居中或者上下居中。让图片始终显示在view的中间
float left = getPaddingLeft() + (availableWidth - sideLength) / 2f;
float top = getPaddingTop() + (availableHeight - sideLength) / 2f;
//最后返回一个矩形
return new RectF(left,top,left + sideLength,top + sideLength);
}
updateShaderMatrix()更改着色器矩阵
其实该方法就是用ImageView中CENTER_CROP模式的算法,看看ImageView中CENTER_CROP的代码:
Paste_Image.png再看看CircleImageView的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);
}
看出来了吧,一模一样的,所以源码是很多学习借鉴的地方的
基本上CircleImageView的核心代码就是上面这三个方法了setup、calculateBounds、updateShaderMatrix。
下面随到看看onDraw方法干了些什么
@Override
protected void onDraw(Canvas canvas) {
//是否禁用图片圆形属性。如果为true,则就是普通方形图片
if(mDisableCircularTransformation){
super.onDraw(canvas);
return;
}
if(mBitmap == null){
return;
}
//如果设置了图片底色,绘制图片底色。
if(mFillColor != Color.TRANSPARENT){
canvas.drawCircle(mDrawableRect.centerX(),mDrawableRect.centerY(),mDrawableradius,mFillPaint);
}
//画内部图片区域(我们给mBitmapPaint设置了Shader,给Shader设置了LocalMatrix,通过ShaderMatrix设置了缩放比,及平移操作完成功能);
canvas.drawCircle(mDrawableRect.centerX(),mDrawableRect.centerY(),mDrawableradius,mBitmapPaint);
//如果设置了BorderWidth宽度,绘制;
if(mBorderWidth > 0){
//这里画边框圆
canvas.drawCircle(mBorderRect.centerX(),mBorderRect.centerY(),mBorderRadius,mBorderPaint);
}
}
其实onDraw方法就是根据不同的情况把圆画出来而已。当存在边界宽度的时候就用mBorderRadius为半径,mBorderRect为边界区域,反之亦然。
最后再看看提供给使用者的一些方法,过时的那些我就不说了。
- setImageURI
- setImageResource
- setImageDrawable
- setImageBitmap
这四个方法都是把图片加载到view上的,所以都需要调用initializeBitmap方法把图片转为bitmap然后调用setup方法。
setBorderColor
由于只是更改边界的颜色,所以只需调用onDraw方法即可,所以调用了invalidate()
setBorderOverlay\setPaddingRelative\setPadding\onSizeChanged
这四个方法都影响到矩形区域边界的计算所以需要调用setup方法重新计算矩形区域
setScaleType\setAdjustViewBounds
这两个方法都是设置view的宽高比的,由于该CircleImageView只支持一种缩放策略:ScaleType.CENTER_CROP所以作者把这两个方法禁用了。
最后可能会问为什么没有重写onLayout和onMeasure方法
原因是onLayout一般是继承ViewGroup或它的子类,才会重写该方法。
至于onMeasure是因为该CircleImageView是对ImageView功能的扩展,对于宽高的测量就交给ImageView去负责。它只需要通过测量宽高后去较小的为半径作圆。
网友评论