自定义CircleImageView的实现

首先来说说我最近遇到的奇葩事!

上了一周的火,患了牙周炎,吃药一直不好,不吃药了,居然好了,哈哈。。。

其次,就来说说为啥想写这篇博客?

好吧,因为面试的时候被问到了,问得自己哑口无言的,那么,为了增加自己的知识储备量,我需要动手实践一下啦。。。为了下次被问到的时候不再尴尬。。。也为了自己以后实现这类需求的时候能够信手拈来啊。。。

然后呢?我们就来说说实现思路啦!

其实,这个实现是有个开源项目的,那么,我们也就是一个学习的过程啦,学习过程,当然还是要记录一下的,方便以后自己回顾啊。。。下面我就说说自己依样画葫芦的过程以及过程中遇到的问题。

那么,大概的实现思路是什么呢?其实我有想到俩,第一个,很简单,就是在一个ImageView上面绘制一个遮罩,遮罩设置成和图片一样大小,但是只有中间的最大圆显示出来遮罩下面的东西,其余的地方都绘制成和承载背景一个颜色,那么就能看起来是个圆形图片啦。第二个,我们就只用绘制一层,避免过度绘制,就是把我们图片的承托工具绘制成圆形的,再把图片根据承托背景调整,最终显示,这样绘制出来的效果会比第一个好,既然说了这是按照开源项目依样画葫芦,那么肯定也按照他们的思路,用好的那个办法啦。。。下面说说“copy”过程。。。。

  • 首先,就像很大自定义View一样,新建一个类,我们命名为CircleImageView,并实现前三个构造函数,然后呢,在我们attrs文件里面,添加我们可能会用到的基本属性啦。

    <declare-styleable name="CircleImageView">
       <attr name="border_width" format="dimension"/>
       <attr name="border_color" format="color"/>
       <attr name="border_overlay" format="boolean"/>
    </declare-styleable>
    
  • 其次呢?我们就像自定义View一样啊,定义我们需要用到的一些变量。

    private static final int COLOR_DRAWABLE_DIMENSION = 2;
    private static final int DEFAULT_BORDER_WIDTH = 0;
    private static final int DEFAULT_BORDER_COLOR = Color.BLACK;
    private static final ScaleType SCALE_TYPE = ScaleType.CENTER_CROP;
    private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888;
    private static final boolean DEFAULT_BORDER_OVERLAY = false;
    private final RectF mDrawableRect = new RectF();
    private final RectF mBorderRect = new RectF();
    
    private final Matrix mShaderMatrix = new Matrix();
    private int mBorderWidth = DEFAULT_BORDER_WIDTH;
    private int mBorderColor = DEFAULT_BORDER_COLOR;
    
    private Paint mBitmapPaint = new Paint();
    private Paint mBorderPaint = new Paint();
    
    private Bitmap mBitmap;
    private BitmapShader mBitmapShader;
    private int mBitmapWidth;
    private int mBitmapHeight;
    
    private float mDrawableRadius;
    private float mBorderRadius;
    
    private ColorFilter mColodrFilter;
    
    private boolean mReady;
    private boolean mSetUpPending;
    private boolean mBorderOverlay;
    
  • 然后呢?在构造函数里面获取自定义参数。

    public CircleImageView(Context context) {
        super(context);
        init();
    }
    
    public CircleImageView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }
    
    public CircleImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView, defStyleAttr, 0);
        mBorderWidth = typedArray.getDimensionPixelSize(R.styleable.CircleImageView_border_width, DEFAULT_BORDER_WIDTH);
        mBorderColor = typedArray.getColor(R.styleable.CircleImageView_border_color, DEFAULT_BORDER_COLOR);
        mBorderOverlay = typedArray.getBoolean(R.styleable.CircleImageView_border_overlay, DEFAULT_BORDER_OVERLAY);
    
        typedArray.recycle();
    
        init();
    }
    

    等一下,init函数是什么?干什么用的呢?我们先看下代码。

    private void init(){
        super.setScaleType(SCALE_TYPE);
        mReady = true;
    
        if (mSetUpPending){
            setUp();
            mSetUpPending = false;
        }
    }
    

    仔细研究发现,好像是有个变量初始化,还有个根据变量看是否执行setUp函数的,作用就是保证第一次执行setup函数里下面代码要在构造函数执行完毕时调用,那么,setUp函数又是干什么的呢?一连串的问号???继续看代码。

    private void setUp(){
        if (!mReady){
            mSetUpPending = true;
            return;
        }
    
        if (mBitmap == null){
            return;
        }
    
        // 构建渲染器,用mBitmap来填充绘制区域 ,参数值代表如果图片太小的话 就直接拉伸
        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);
    
        mBitmapHeight = mBitmap.getHeight();
        mBitmapWidth = mBitmap.getWidth();
    
        mBorderRect.set(0, 0, getWidth(), getHeight());
        //计算 圆形带边界部分(外圆)的最小半径,取mBorderRect的宽高减去一个边缘大小的一半的较小值(这个地方我比较纳闷为什么求外圆半径需要先减去一个边缘大小)
        mBorderRadius = Math.min((mBorderRect.height() - mBorderWidth) / 2, (mBorderRect.width() - mBorderWidth) / 2);
         // 初始图片显示区域为mBorderRect(CircleImageView的布局实际大小)
        mDrawableRect.set(mBorderRect);
        if (!mBorderOverlay){
            //通过inset方法  使得图片显示的区域从mBorderRect大小上下左右内移边界的宽度形成区域
            mDrawableRect.inset(mBorderWidth, mBorderWidth);
        }
        //这里计算的是内圆的最小半径,也即去除边界宽度的半径
        mDrawableRadius = Math.min(mDrawableRect.height() / 2, mDrawableRect.width() / 2);
        //设置渲染器的变换矩阵也即是mBitmap用何种缩放形式填充
        updateShaderMatrix();
        //手动触发ondraw()函数 完成最终的绘制
        invalidate();
    }
    

    哦,原来是对代码自定义属性进行取值的,还有就是构建渲染器BitmapShader用Bitmap来填充绘制区域,设置样式和内外圆半径计算等,以及调用updateShaderMatrix()函数和 invalidate()函数;那么,updateShaderMatrix函数又是干什么的呢?一看名字就知道啦,肯定和Shader,Matrix 有关啦,对,他就是拿来计算缩放比例和平移位置,以及设置BitmapShader的Matrix 参数的。下面展示代码:

    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.height() - mBitmapHeight * scale) * 0.5f;
        } else {
            scale = mDrawableRect.width() / (float) mBorderWidth;
            dy = (mDrawableRect.height() - mBorderWidth * scale) * 0.5f;
        }
    
        // 缩放
        mShaderMatrix.setScale(scale, scale);
        //平移
        mShaderMatrix.postTranslate((int) (dx + 0.5f) + mDrawableRect.left, (int)(dy + 0.5f) + mDrawableRect.top);
        //设置变换矩阵
        mBitmapShader.setLocalMatrix(mShaderMatrix);
    }
    
  • 再然后呢?想象一下,ImageView类的自定义控件,肯定是显示图片是主要部分啦,但是显示图片分为很多种类,根据BitMap,资源ID显示,根据Drawable显示,根据URL显示等。目前我们先实现这四种吧。。

     @Override
    public void setImageBitmap(Bitmap bm) {
        super.setImageBitmap(bm);
        mBitmap = bm;
        setUp();
    }
    
    @Override
    public void setImageDrawable(@Nullable Drawable drawable) {
        super.setImageDrawable(drawable);
        mBitmap = getBitmapFromDrawable(drawable);
        setUp();
    }
    
    @Override
    public void setImageResource(@DrawableRes int resId) {
        super.setImageResource(resId);
        mBitmap = getBitmapFromDrawable(getDrawable());
        setUp();
    }
    
    @Override
    public void setImageURI(Uri uri) {
        super.setImageURI(uri);
        mBitmap = getBitmapFromDrawable(getDrawable());
        setup();
    }
    

    看了以上代码以后,又有疑惑了,getBitmapFromDrawable 函数又是什么?呀,这就是个将Drawable转换为Bitmap的函数啦,代码实现如下。。。

    private Bitmap getBitmapFromDrawable(Drawable drawable){
        if (drawable == null){
            return null;
        }
    
        if (drawable instanceof BitmapDrawable){
            return ((BitmapDrawable) drawable).getBitmap();
        }
    
        Bitmap bitmap;
        if (drawable instanceof ColorDrawable){
            bitmap = Bitmap.createBitmap(COLOR_DRAWABLE_DIMENSION, COLOR_DRAWABLE_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;
    }
    
  • 对了,千万记住,别忘了还有最重要的一步哟,那就是重写OnDraw函数啦,那onDraw函数里面到底实现了些什么呢?当然就是绘制啦,show time:

    @Override
    protected void onDraw(Canvas canvas) {
        if (getDrawable() == null){
            return;
        }
    
        canvas.drawCircle(getWidth() / 2, getHeight() / 2, mDrawableRadius, mBitmapPaint);
        if (mBorderWidth != 0){
            canvas.drawCircle(getWidth() / 2, getHeight() / 2, mBorderRadius, mBorderPaint);
        }
    }
    

    做完了以上几个步骤,那么剩下的,就是一些细枝末节的补充工作啦。。完整代码就不贴出了,我主要是介绍一下思路,完整代码Google一下,GitHub上有开源原项目代码哦。。。嘻嘻。。。

说完思路啦,最后呢?我们就来总结总结啦。。。

  • 就如你们看到的一样,自定义的View,不论是ImageView还是TextView,都逃不过这几个步骤啦。
    1. 自定义View的属性
    2. 继承View,重写构造函数,并在构造函数里获取我们的自定义属性
    3. 重写onMeasure方法(由于这个我们需要控制显示大小以及形状,所以是要重写的,一般的自定义View不需要实现)
    4. 重写OnLayout方法(这一次我们没有用到,因为我们的布局很简单,没有强制设置该图片要定位在布局中的某个位置,不然,我们也需要实现的哟)
    5. 重写onDraw方法。。。这一步,一般都要有的,毕竟,我们自定义view就是需要显示我们想要显示的内容啊,那么如何显示呢,肯定是需要先有内容再显示,内容怎么来呢?绘制。。。所以,这一步基本上是必备的哟。。小伙伴们一定记住哟。。。

总结完啦,希望自己每周都能对自己用到的新东西做个总结,坚持学习,坚持实践,每天进步一点点。。。(^__^) 嘻嘻……