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

作者: 721d739b6619 | 来源:发表于2017-07-15 18:17 被阅读80次

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

    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调用者使用;

    Paste_Image.png 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的代码:

    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源码剖析)

        本文链接:https://www.haomeiwen.com/subject/trxahxtx.html