CircleImageView 解析与定制

来源:互联网 发布:秒杀java实现代码 编辑:程序博客网 时间:2024/06/01 21:26

CircleImageView 是一个非常轻量的实现圆形头像的类。GitHub 上 Start 数达到了 7k+,质量非常高。使用方法非常简单,具体参见 GitHub 文档。

本文通过源码进行分析,学习其实现原理,做简单的定制。

注:CircleImageView 中,fillColor 相关设置由于已经被废弃,下文中将不展示所有 fillColor 相关内容。

源码执行顺序

在本例子里,是在 xml 里设置了一张 Drawable 图片。这种情况下我们按照执行的方法顺序来看一下代码。加入 Log 将执行顺序打印出来,插入 Log 的方法参考博客:给每一个函数加一行LOG。

public void setImageDrawable(Drawable drawable) {private void initializeBitmap() {private Bitmap getBitmapFromDrawable(Drawable drawable) {private void setup() {public void setAdjustViewBounds(boolean adjustViewBounds) {public CircleImageView(Context context, AttributeSet attrs, int defStyle) {private void init() {private void setup() {public CircleImageView(Context context, AttributeSet attrs) {protected void onSizeChanged(int w, int h, int oldw, int oldh) {private void setup() {private RectF calculateBounds() {private void applyColorFilter() {private void updateShaderMatrix() {protected void onDraw(Canvas canvas) {

源码解析

先从父类看起:查看 ImageView 的构造方法,首先执行了 initImageView()

private void initImageView() {    mMatrix = new Matrix();    // 默认缩放方式设置为 FIT_CENTER    mScaleType = ScaleType.FIT_CENTER;    // 省略}

实际上 CircleImageView 是不支持 FIT_CENTER 方式的,所以后续在 init() 中强制更改为 CENTER_CROP。

接着由于 xml 里设置了 src 所以会执行 setImageDrawable(Drawable drawable),由于 CircleImageView 重写了此方法,所以执行的是 CircleImageView 中的方法:

@Overridepublic void setImageDrawable(Drawable drawable) {    super.setImageDrawable(drawable);    initializeBitmap();}

可以看到在一系列 setImageXxxxx 方法中,调用了 initializeBitmap() 方法:

private void initializeBitmap() {    if (mDisableCircularTransformation) {        mBitmap = null;    } else {        // 获取 Bitmap        mBitmap = getBitmapFromDrawable(getDrawable());    }    setup();}

获取 Bitmap:

private Bitmap getBitmapFromDrawable(Drawable drawable) {    // 没有图片直接返回空    if (drawable == null) {        return null;    }    // BitmapDrawable 直接返回相应的 Bigmap    if (drawable instanceof BitmapDrawable) {        return ((BitmapDrawable) drawable).getBitmap();    }    try {        Bitmap bitmap;        if (drawable instanceof ColorDrawable) {            // 创建 2dp 大小的 bitmap            bitmap = Bitmap.createBitmap(COLORDRAWABLE_DIMENSION,             COLORDRAWABLE_DIMENSION, BITMAP_CONFIG);        } else {            // 其他类型的 Drawable,创建对应大小的 bitmap            // 由于此处可能会出现 getIntrinsicWidth 为 -1,需要捕获异常            bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),             drawable.getIntrinsicHeight(), BITMAP_CONFIG);        }        // 创建指定的画布        Canvas canvas = new Canvas(bitmap);        // 指定该图片需要绘制的区域,这个区域就是 draw 方法调用时绘制的区域        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());        // 将图片绘制到画布上        drawable.draw(canvas);        return bitmap;    } catch (Exception e) {        e.printStackTrace();        return null;    }}

然后进行设置属性:

private void setup() {    if (!mReady) {        // 延迟设置属性        mSetupPending = true;        return;    }    // 省略}

实际上此时 View 还未初始化,所以此处暂不设置 CircleImageView 的大小参数。

接下来回到父类查看,将设置 adjustViewBounds 参数,由于 XML 定义里的 android:adjustViewBounds="true" 会将这个 ImageView 的 scaleType 设为 fitCenter。而 CircleImageView 不支持除 CENTER_CROP 之外的缩放属性,所以,CircleImageView 中重写了该方法检查此属性,如果设置了 adjustViewBounds 则会抛出异常:

@Overridepublic void setAdjustViewBounds(boolean adjustViewBounds) {    // 不支持 adjustViewBounds 属性,因为此属性会设置 scaleType 为 fitCenter    if (adjustViewBounds) {        throw new IllegalArgumentException("adjustViewBounds not supported.");    }}

执行完 ImageView 的构造方法后,执行 CircleImageView 构造方法:

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);    a.recycle();    init();}

可以看到,在获取了设置的属性即环形边缘的宽度、颜色以及是否覆盖图片后,执行 init() 方法来真正地设置 CircleImageView 的属性。

private void init() {    // 设置 scaleType 为 CENTER_CROP    super.setScaleType(SCALE_TYPE);    // View 已经准备好可以设置属性了    mReady = true;    // 此时根据标志位开始设置属性    if (mSetupPending) {        setup();        mSetupPending = false;    }}

此时执行 setup() 方法,View 大小为 0,无需设置属性。

private void setup() {    if (!mReady) {        mSetupPending = true;        return;    }    // View 大小为 0    if (getWidth() == 0 && getHeight() == 0) {        return;    }    // 省略}

在系统布局完后,会回调 onSizeChanged() 方法:

@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {    super.onSizeChanged(w, h, oldw, oldh);    setup();}

核心方法的参数计算与设置

这时再调用 setup(),View 就有大小了,开始设置相应的属性,也就是把一张图片设置为圆形头像的方法的各个参数的计算与设置。

private void setup() {    if (!mReady) {        mSetupPending = true;        return;    }    // View 大小为 0    if (getWidth() == 0 && getHeight() == 0) {        return;    }    // 无可设置 Bitmap    if (mBitmap == null) {        invalidate();        return;    }    // BitmapShader 的作用在于:指定绘制来源为图片,将一个变换矩阵设置给 Bitmap 画笔    mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);    // Bitmap 画笔设置抗锯齿    mBitmapPaint.setAntiAlias(true);    // 设置 BitmapShader,绘制时就是指定的图片内容,后面还会设置它的矩阵属性对图片作变换    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(calculateBounds());    // 计算环形的半径,环形半径为环形中心到环形的中间的长度    // 所以方形区域 - 环形宽度 / 2 就是环形的半径了    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);    // 设置 Bitmap 画笔带的蒙层(滤镜)颜色    applyColorFilter();    // 图片显示时需要进行缩放、平移,通过矩阵来变换    updateShaderMatrix();    invalidate();}

我们分别来看看 calculateBounds() applyColorFilter() updateShaderMatrix() 方法:

calculateBounds() 就是获取 View 当中可以绘制的最大的方形区域,并且这个区域定位于 View 的中心,使圆形头像是居中显示的。

private RectF calculateBounds() {    // 获取 View 可以绘制的宽度与高度,可以看到支持 padding 属性的设置    int availableWidth  = getWidth() - getPaddingLeft() - getPaddingRight();    int availableHeight = getHeight() - getPaddingTop() - getPaddingBottom();    // 由于是圆形头像,所以从可绘制的区域中找到一个最大的方形区域    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);}

applyColorFilter() 比较简单无需说明。

private void applyColorFilter() {    if (mBitmapPaint != null) {        mBitmapPaint.setColorFilter(mColorFilter);    }}

主要变换操作就是通过 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;        // 缩放之后需要在 x 轴上进行平移,以得到图片正中心的图案        dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f;    } else {        // 如果图片宽高比小于控件的宽高比,缩放比例由宽度决定        scale = mDrawableRect.width() / (float) mBitmapWidth;        // 缩放之后需要在 y 轴上进行平移,以得到图片正中心的图案        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);}

setup() 中,将执行 invalidate() 回调 onDraw() 将控件绘制出来。

@Overrideprotected void onDraw(Canvas canvas) {    if (mDisableCircularTransformation) {        super.onDraw(canvas);        return;    }    if (mBitmap == null) {        return;    }    // 绘制圆形图片    canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(),     mDrawableRadius, mBitmapPaint);    if (mBorderWidth > 0) {        // 绘制环形边缘        canvas.drawCircle(mBorderRect.centerX(), mBorderRect.centerY(),         mBorderRadius, mBorderPaint);    }}

以上就是 CircleImageView 控件的整个流程了。可以看到确实是很轻的,而其实现方式是优雅且高性能的。因为需要计算的参数一开始就计算好了,会进行多次的绘制方法使用了最简单的内容,无须进行计算,直接应用即可。

关于该实现方式的性能分析可以参考:Android-解析自定义view之圆形头像的各类方案

自定义修改

现在有一个需求,这个圆形头像需要随着滑动渐渐透明消失。我们知道设置一个图片的透明度的方法为:mImageView.setImageAlpha(int alpha)。那直接对创建的 CircleImageView 调用这个方法来设置行不行呢?毕竟它也是继承 ImageView 的嘛。

答案当然是不可以,原因就是它的 onDraw 已经与父类不一样了,ImageView 中,透明度生效是直接修改 mDrawable.setAlpha(),然后在 onDrawmDrawable.draw(canvas)。而 CircleImageView 中是使用了自定义的 Paint 进行绘制的。

因此,可以参考 CircleImageView 中的 setColorFilter 方法:

public void setColorFilter(ColorFilter cf) {    if (cf == mColorFilter) {        return;    }    mColorFilter = cf;    applyColorFilter();    invalidate();}

通过 applyColorFilter 方法(见前文) mBitmapPaint.setColorFilter(mColorFilter) 设置了 mBitmapPaint 的 ColorFilter 颜色,再 invalidate() 触发绘制即可生效。所以增加设置透明度的方法,增加一个 alpha 变量,然后方法写成如下即可:

@Overridepublic void setImageAlpha(int alpha) {    if (mAlpha != alpha) {        mAlpha = alpha;        mBitmapPaint.setAlpha(mAlpha);        invalidate();    }}

最后,既然提供了修改的方法,也应该提供查询的方法:

@Overridepublic int getImageAlpha() {    return mAlpha;}

这样子,我们就给圆形头像加上了透明度的设置方法。

参考链接:

  1. CircleImageView
  2. 给每一个函数加一行LOG
  3. Android-解析自定义view之圆形头像的各类方案
原创粉丝点击