android自定义view-打造圆形ImageView(一)
前言:
在许多应用中会遇到用户头像呈现圆形的情况,但因缺少现成的圆形ImageView组件调用,最常见的做法就是自行开发一个圆角ImageView组件,为了这一目的,在此我们着手开发一个圆角ImageView组件以备后用
。为什么标题会有(一)呢,其实打造圆形ImageView,我能想到的有三种方式,
- BitmapShader(渲染器,将画笔用bitmap图形填充)
- Xfermode
- 继承drawable
就我个人而言,在使用多个第三方图片加载框架如Picasso、Universal-Image-Loader和Volley时,默认采用基于ImageView的圆形ImageView更为便捷;即我们目前采用的前两种方案;但相比之下,默认采用基于Drawable的方式最为简单;换言之,默认采用基于Drawable的方式无疑是最为简洁直接的选择
截图:

在本处我们的imageview可以配置为圆形或带圆角的形式。如果需要实现带有圆角的imageview功能,则无需手动配置XML文件并定义drawable资源。直接使用本机提供的功能即可显著简化开发流程。
正文:
首先,任何重写view都需要的几个步骤:
- 继承view
- 自定义属性
- 重写onMeasure方法【可不重写】
- 重写onDraw方法
Step1:继承View
public class RoundImageView extends ImageView
step2:自定义属性:attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="borderRadius" format="dimension" />
<attr name="imageType">
<enum name="circle" value="0" />
<enum name="round" value="1" />
</attr>
<declare-styleable name="RoundImageView">
<attr name="borderRadius" />
<attr name="imageType" />
</declare-styleable>
</resources>
在这一部分
Step3:在构造器中初始化值
public RoundImageView(Context context) {
this(context, null);
}
public RoundImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public RoundImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// 初始化画笔等属性
mMatrix = new Matrix();
mPaint = new Paint();
mPaint.setAntiAlias(true);
// 获取自定义属性值
TypedArray array = context.getTheme().obtainStyledAttributes(attrs, R.styleable.RoundImageView, defStyle, 0);
int count = array.getIndexCount();
for (int i = 0; i < count; i++) {
int attr = array.getIndex(i);
switch (attr) {
case R.styleable.RoundImageView_borderRadius:
// 获取圆角大小
mBorderRadius = array.getDimensionPixelSize(R.styleable.RoundImageView_borderRadius, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, BORDER_RADIUS_DEFAULT, getResources().getDisplayMetrics()));
break;
case R.styleable.RoundImageView_imageType:
// 获取ImageView的类型
type = array.getInt(R.styleable.RoundImageView_imageType, TYPE_CIRCLE);
break;
}
}
// Give back a previously retrieved StyledAttributes, for later re-use.
array.recycle();
}
这一部分代码的作用是为了调整BitmapShader中的LocalMatrix缩放参数。为什么要这样做呢?当图片尺寸超过我们的View区域时,在当前场景下就需要对Bitmap进行相应的缩放处理。为此,在代码中调用了TypedArray数组来获取所需的视图属性值,并在此基础上提出一个问题:在构造函数中已经初始化好了RoundImageView属性集,请问为什么不直接使用它来进行配置?其实可以直接利用这个属性集来进行配置:完成后的TypedArray数组应该及时回收资源以释放内存占用空间
Step4:重写onMeasure方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 如果是圆形,则强制宽高一致,以最小的值为准
if (type == TYPE_CIRCLE) {
mWidth = Math.min(getMeasuredWidth(), getMeasuredHeight());
mRadius = mWidth / 2;
setMeasuredDimension(mWidth, mWidth);
}
}
我们最近在开发一个图形界面应用时,在onMeasure方法内部仅计算了圆形情况下view的宽高比,并非逐一修改其尺寸。这是因为由于圆形物体的特点其宽高尺寸必须相等。因此,在这里我们选择了较小值作为直径来实现这一目标。
Step5:重写onDraw方法
@Override
protected void onDraw(Canvas canvas) {
if (getDrawable() == null) {
return;
}
// 设置渲染器
setShader();
if (type == TYPE_ROUND) {
canvas.drawRoundRect(mRectF, mBorderRadius, mBorderRadius, mPaint);
} else {
canvas.drawCircle(mRadius, mRadius, mRadius, mPaint);
}
}
在onDraw方法中主要涉及两种思路:第一种思路是通过指定type为圆形来绘制圆;第二种思路则是通过指定type为圆角来绘制带有圆角的图片。在开始绘制之前必须设置画笔属性,并在此过程中定义图形渲染器(Shader)。为了实现这一功能,在处理这两种情况时需要先定义好各自的几何属性对象:第一种情况是一个简单的圆形对象;第二种情况则是一个具有圆角矩形属性的对象。在处理这两种不同类型的绘图需求时,请确保所有必要的属性都已经正确配置完毕之后才能开始绘图操作。
private void setShader() {
Drawable drawable = getDrawable();
if (drawable == null) {
return;
}
Bitmap bitmap = drawable2Bitmap(drawable);
mBitmapShader = new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP);
float scale = 1.0f;
if (type == TYPE_ROUND) {
scale = Math.max(getWidth() * 1.0f / bitmap.getWidth(), getHeight() * 1.0f / bitmap.getHeight());
} else if (type == TYPE_CIRCLE) {
// 取小值,如果取大值的话,则不能覆盖view
int bitmapWidth = Math.min(bitmap.getWidth(), getHeight());
scale = mWidth * 1.0f / bitmapWidth;
}
mMatrix.setScale(scale, scale);
mBitmapShader.setLocalMatrix(mMatrix);
mPaint.setShader(mBitmapShader);
}
首先,我们需要配置src的drawable为bitmaps。随后,在设置完后,请您依次选择BitmapShader的不同模式:CLAMP(拉伸)、REPEAT(重复)以及MIRROR(镜像)。值得注意的是,在这里CLAMP与我们通常理解的不同之处在于它仅对宽度和高度方向上的最后一个像素进行拉伸处理。重点在于,在设置shader时,若采用圆角处理,则需借助图形来理解其原理:

我上面代码的分子分母颠倒了,所以取的是大值。如果是圆形呢?

当我们的drawable宽度较小时,则将其缩放至圆形宽度;而如果大于Drawble宽度,则缩减为View的宽度。下一步要做的是变换Shader的Matrix矩阵。
mMatrix.setScale(scale, scale);
mBitmapShader.setLocalMatrix(mMatrix);
mPaint.setShader(mBitmapShader);
首先缩放matrix并随后设置Shader矩阵之后接着将Shader赋值给paint对象之后将在onDraw事件中调用paint进行绘制此外还需要注意如何将drawable对象转换为bitmap对实际上在onDraw事件中直接创建Canvas可能会带来性能问题因此建议可以直接传递到setShader方法中的参数
贴一下完整的代码:
RoundImageView.java:
package com.beyole.view;
import com.beyole.roundimageview.R;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Bitmap.Config;
import android.graphics.RectF;
import android.graphics.Shader.TileMode;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.widget.ImageView;
public class RoundImageView extends ImageView {
// ImageView类型
private int type;
// 圆形图片
private static final int TYPE_CIRCLE = 0;
// 圆角图片
private static final int TYPE_ROUND = 1;
// 默认圆角宽度
private static final int BORDER_RADIUS_DEFAULT = 10;
// 获取圆角宽度
private int mBorderRadius;
// 画笔
private Paint mPaint;
// 半径
private int mRadius;
// 缩放矩阵
private Matrix mMatrix;
// 渲染器,使用图片填充形状
private BitmapShader mBitmapShader;
// 宽度
private int mWidth;
// 圆角范围
private RectF mRectF;
public RoundImageView(Context context) {
this(context, null);
}
public RoundImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public RoundImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// 初始化画笔等属性
mMatrix = new Matrix();
mPaint = new Paint();
mPaint.setAntiAlias(true);
// 获取自定义属性值
TypedArray array = context.getTheme().obtainStyledAttributes(attrs, R.styleable.RoundImageView, defStyle, 0);
int count = array.getIndexCount();
for (int i = 0; i < count; i++) {
int attr = array.getIndex(i);
switch (attr) {
case R.styleable.RoundImageView_borderRadius:
// 获取圆角大小
mBorderRadius = array.getDimensionPixelSize(R.styleable.RoundImageView_borderRadius, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, BORDER_RADIUS_DEFAULT, getResources().getDisplayMetrics()));
break;
case R.styleable.RoundImageView_imageType:
// 获取ImageView的类型
type = array.getInt(R.styleable.RoundImageView_imageType, TYPE_CIRCLE);
break;
}
}
// Give back a previously retrieved StyledAttributes, for later re-use.
array.recycle();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 如果是圆形,则强制宽高一致,以最小的值为准
if (type == TYPE_CIRCLE) {
mWidth = Math.min(getMeasuredWidth(), getMeasuredHeight());
mRadius = mWidth / 2;
setMeasuredDimension(mWidth, mWidth);
}
}
@Override
protected void onDraw(Canvas canvas) {
if (getDrawable() == null) {
return;
}
// 设置渲染器
setShader();
if (type == TYPE_ROUND) {
canvas.drawRoundRect(mRectF, mBorderRadius, mBorderRadius, mPaint);
} else {
canvas.drawCircle(mRadius, mRadius, mRadius, mPaint);
}
}
private void setShader() {
Drawable drawable = getDrawable();
if (drawable == null) {
return;
}
Bitmap bitmap = drawable2Bitmap(drawable);
mBitmapShader = new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP);
float scale = 1.0f;
if (type == TYPE_ROUND) {
scale = Math.max(getWidth() * 1.0f / bitmap.getWidth(), getHeight() * 1.0f / bitmap.getHeight());
} else if (type == TYPE_CIRCLE) {
// 取小值,如果取大值的话,则不能覆盖view
int bitmapWidth = Math.min(bitmap.getWidth(), getHeight());
scale = mWidth * 1.0f / bitmapWidth;
}
mMatrix.setScale(scale, scale);
mBitmapShader.setLocalMatrix(mMatrix);
mPaint.setShader(mBitmapShader);
}
/** * 将Drawable转化为Bitmap
* * @param drawable
* @return
*/
private Bitmap drawable2Bitmap(Drawable drawable) {
if (drawable instanceof BitmapDrawable) {
BitmapDrawable bd = (BitmapDrawable) drawable;
return bd.getBitmap();
}
int w = drawable.getIntrinsicWidth();
int h = drawable.getIntrinsicHeight();
// 创建画布
Bitmap bitmap = Bitmap.createBitmap(w, h, Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, w, h);
drawable.draw(canvas);
return bitmap;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mRectF = new RectF(0, 0, getWidth(), getHeight());
}
/** * 对外公布的设置borderRadius方法
* * @param borderRadius
*/
public void setBorderRadius(int borderRadius) {
int pxValue = dp2px(borderRadius);
if (this.mBorderRadius != pxValue) {
this.mBorderRadius = pxValue;
// 这时候不需要父布局的onLayout,所以只需要调用onDraw即可
invalidate();
}
}
/** * 对外公布的设置形状的方法
* * @param type
*/
public void setType(int type) {
if (this.type != type) {
this.type = type;
if (this.type != TYPE_CIRCLE && this.type != TYPE_ROUND) {
this.type = TYPE_CIRCLE;
}
// 这个时候改变形状了,就需要调用父布局的onLayout,那么此view的onMeasure方法也会被调用
requestLayout();
}
}
/** * dp2px
*/
public int dp2px(int val) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, val, getResources().getDisplayMetrics());
}
}
我在此处也发布了两种方案。一种方案是通过设置type来实现功能定位目标;另一种方案则是调整圆角图片的borderRadius以达到视觉效果优化的目的。我发现这两种方案在处理view时采用了不同的机制:其中一种是invoke invalidate()函数来终止当前布局请求;另一种则是invoke requestLayout()以启动父视图的重新布局流程。区别在于前者相当于直接调用View.onDraw()的方法完成当前布局请求;而后者则是在特定条件下触发parent view重新计算并应用新的布局设定。
Step6:使用
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:beyole="http://schemas.android.com/apk/res/com.beyole.roundimageview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<com.beyole.view.RoundImageView
android:layout_width="200dip"
android:layout_height="200dip"
android:src="@drawable/demo"
beyole:borderRadius="20dip"
beyole:imageType="round" />
<com.beyole.view.RoundImageView
android:layout_width="200dip"
android:layout_height="200dip"
android:src="@drawable/demo"
beyole:borderRadius="20dip"
beyole:imageType="circle" />
</LinearLayout>
后语:
嗯,在这里我们的项目已经圆满结束了。大家有没有觉得不算太难呢?(不敢说自己认为它很简单)
下载地址:<>
题外话:
android交流群:279031247(广告勿入)
微/single Weibo:@冰山智脑
