2017-7-15(CircleImageView源码剖析)

相信很多人都用过此控件:

2017-7-15(CircleImageView源码剖析)_第1张图片
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源码的构造函数:

2017-7-15(CircleImageView源码剖析)_第2张图片
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调用者使用;

Paste_Image.png
2017-7-15(CircleImageView源码剖析)_第3张图片
Paste_Image.png

该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的代码:

2017-7-15(CircleImageView源码剖析)_第4张图片
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去负责。它只需要通过测量宽高后去较小的为半径作圆。

好了,CircleImageView就剖析到这里,其实这个也是很好学习ImageView的踏脚石,和写好自定义view的宝贵资源。自定义View难其实就难在如何计算坐标值。这个需要通过多写,学习优秀的自定义view和官方的源码。

你可能感兴趣的:(2017-7-15(CircleImageView源码剖析))