浅谈开源框架:CircleImageView源码设计

版权声明:【本文是博主原创文章,未经许可,不得转载本文任何内容】 (*^▽^*)欢迎您阅读我的文章,觉得不错请动动小手点个【赞】,若有疑问或出入请回复区指明!原文地址: https://blog.csdn.net/smile_Running/article/details/82526105

【博主声明】欢迎审阅,未经许可,请勿转载,谢谢!

· 使用教程

   今天,我们来介绍一款开源框架:CircleImageView,相信绝大部分的android开发者都使用过。而且它在github上已经有上万个star,可以说是一款相当热门的开源库。这款开源框架的作用如其名称,将图片设置成圆形图。那么它为什么这么热门呢,当然有它必然的理由。首先,我们看它的用法:

<de.hdodenhof.circleimageview.CircleImageView
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/profile_image"
    android:layout_width="96dp"
    android:layout_height="96dp"
    android:src="@drawable/profile"
    app:civ_border_width="2dp"
    app:civ_border_color="#FF000000"/>

  它的用法比较简单、易用,就好比系统控件一样方便。当然,在使用之前,你必须提供它的依赖:

dependencies {
    ...
    implementation 'de.hdodenhof:circleimageview:2.2.0'
}

那么它的显示结果就是这个样子:

所以,这就是CircleImageView的使用方式了,也是github上hdodenhof大神的原文使用教程。因为使用比较简单,我就直接copy了一份过来。

本文以简单的使用方式作为铺垫,为了帮助一些没有使用过的读者,所以这里就借花献佛,依照原教程写了一份。

· 源码分析

   接下来,我将带你走进CircleImageView的源码,带你一起读一下大神的思想。看到源码你会觉得很庆幸,为什么怎么说呢?因为源码就只有一个类和一个xml文件。你没有看错,确实是这样的。我们来看看项目结构:

· 属性文件

   首先,我们看xml文件,这个文件里自定义了几个属性方法。分别是:

<resources>
    <declare-styleable name="CircleImageView">
        <attr name="civ_border_width" format="dimension" />
        <attr name="civ_border_color" format="color" />
        <attr name="civ_border_overlay" format="boolean" />
        <!-- {@deprecated Use civ_circlebackground_color instead.} -->
        <attr name="civ_fill_color" format="color" />
        <attr name="civ_circle_background_color" format="color" />
    </declare-styleable>
</resources>

  根据上面提供的属性方法,你可以设置CircleImageView的边框宽度、颜色、是否覆盖、背景颜色等。当然,建议大家还是自己去实现一下,体验这些属性的效果区别。

注意:上面的注释部分提及,已经废弃设置背景颜色这个属性方法。

· 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()方法,这意味着初始化开始了。我们跟着作者的思路,一步一步的解读。

· 进入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)的方法里传入的参数息息相关了,这里不是特别难理解。

· 进入setup()方法

  在初始化完了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源码的分析已经结束了。虽然有一些函数、属性没有在本文提及,但是还是建议大家能自己去看看源码,这样理解的更加透彻。

©原文链接:https://blog.csdn.net/smile_Running/article/details/82526105

@作者博客:_Xu2WeI

@更多博文:查看作者的更多博文

猜你喜欢

转载自blog.csdn.net/smile_Running/article/details/82526105