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()
,然后在 onDraw
中 mDrawable.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;}
这样子,我们就给圆形头像加上了透明度的设置方法。
参考链接:
- CircleImageView
- 给每一个函数加一行LOG
- Android-解析自定义view之圆形头像的各类方案
- CircleImageView 解析与定制
- CircleImageView解析
- CircleImageView源码解析
- Widget-CircleImageView解析
- CircleImageView
- CircleImageView
- CircleImageView
- circleImageview
- CircleImageView
- CircleImageView的实现与使用
- CircleImageView的实现与使用
- CircleImageView用法及源码解析(雷惊风)
- Glide与圆形图片CircleImageView的问题
- github上的圆形图片框架CircleImageView使用及解析
- 定制解析json
- Glide与CircleImageView加载圆形图片的问题
- CircleImageView分析
- ADROID 架构解析 语言定制
- KSGT
- Spring4笔记--IOC
- 跳转Activity时,加入动画效果
- js数组方法
- 未来的世界一定是尔虞我诈的世界吗
- CircleImageView 解析与定制
- 面试题22—栈的压入、弹出序列
- jQuery加载html文档的几种方法
- 报文格式
- ERROR: SWT folder '' does not exist
- Node安装和使用
- syscon 的使用
- 理解回调机制-java
- 面试题23—从上往下打印二叉树