前言
一直想封装一个圆形图片的ImageView,正好这两天看见郭霖推送的文章,且讲的正好是我想学习的,于是马上把他的文章看了一遍(文章地址http://www.wtoutiao.com/p/5f2wsQa.html),自己也重新实现了一遍。效果如下:
挺简单的两个效果,也挺实用,在项目中经常用,一个是方形图片的边框圆角,一个是圆形图片,实现逻辑不是很难,不过中间有些地方有点绕,待会慢慢解释
结构图
这是结构图:
大致要实现的效果就是右边那种,红色的Bitmap是经过处理缩放后的效果
自定义属性
要想自由控制是方形圆角(就叫Round)还是圆形(Circle)效果,所以需要一个自定义属性type,来通知ImageView分别绘制Round还是Circle,这是第一个。同时,圆角半径想要灵活输入,也需要一个自定义属性,故,两个自定义属性需要定义,看下代码:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="borderRadio" format="dimension"/>
<attr name="type">
<enum name="circle" value="0"/>
<enum name="round" value="1"/>
</attr>
<declare-styleable name="CircleImageView">
<attr name="borderRadio" />
<attr name="type" />
</declare-styleable>
</resources>
代码中我们定义了borderRadio和type两个属性,其中type是枚举类型(贫道也是第一次用,额,尴尬。。。),有circle和round两个元素。borderRadio圆角半径, type控制是绘制方形还是圆形的标志,好了自定义属性就看到这里。
自定义ImageView
1 . 首先我们需要定义一个类继承ImageView,实现构造方法
public CircleImageView(Context context) {
this(context, null, 0);
}
public CircleImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CircleImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint = new Paint();
//去锯齿效果
mPaint.setAntiAlias(true);
matrix = new Matrix();
//获取自定义属性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView);
mBorderRadio = typedArray.getDimensionPixelSize(R.styleable.CircleImageView_borderRadio,
dp2px(DFAULT_ROUND_SIZE));
type = typedArray.getInt(R.styleable.CircleImageView_type, CIRCLE); //默认画圆
typedArray.recycle();
}
我实现了三个构造方法,让一个的和两个的调用三个的构造方法,然后再第三个中实现ImageView的初始化方法,一切都是套路~~~。 初始化方法中,主要获取了自定义属性的值。matrix是一个3*3矩阵,用来实现图片的平移和缩放操作。dp2px()是一个工具方法,把dp转化为px,实现不同平台像素适配。代码为:
private int dp2px(int dp){
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,dp , getResources().getDisplayMetrics());
}
这样的效果网上有很多,其实还有别的写法可以实现,就不多说了。
2 . onMeasure测量
方形图片就用系统的测量模式,圆形的需要自己重新实现测量,代码如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if(type == CIRCLE){
circleWidth = Math.min(getMeasuredWidth(), getMeasuredHeight());
mRadius = circleWidth / 2;
setMeasuredDimension(circleWidth, circleWidth);
}
}
3 . onDraw绘制
先上代码:
if(getDrawable() == null){
return;
}
setBitmapShader();
if(type == CIRCLE){
canvas.drawCircle(mRadius, mRadius, mRadius, mPaint);
}else {
canvas.drawRoundRect(recf, mBorderRadio, mBorderRadio, mPaint);
}
首先判空,接下来这个setBitmapShader();什么意思? 好了,客官别急,稍好介绍,然后根据type进行圆形和方形图片的绘制。接下来看setBitmapShader()之前先来了解下BitmapShader。
BitmapShader
BitmapShader是Shader的子类,可以通过Paint.setShader(Shader shader)进行设置、
这里我们只关注BitmapShader,构造方法:
mBitmapShader = new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP);
参数1:bitmap
参数2,参数3:TileMode;
TileMode的取值有三种:
CLAMP 拉伸
REPEAT 重复
MIRROR 镜像
如果大家给电脑屏幕设置屏保的时候,如果图片太小,可以选择重复、拉伸、镜像;
重复:就是横向、纵向不断重复这个bitmap
镜像:横向不断翻转重复,纵向不断翻转重复;
拉伸:这个和电脑屏保的模式应该有些不同,这个拉伸的是图片最后的那一个像素;横向的最后一个横行像素,不断的重复,纵项的那一列像素,不断的重复;
现在大概明白了,BitmapShader通过设置给mPaint,然后用这个mPaint绘图时,就会根据你设置的TileMode,对绘制区域进行着色。
这里需要注意一点:就是BitmapShader是从你的画布的左上角开始绘制的,不在view的右下角绘制个正方形,它不会在你正方形的左上角开始。
好了,到此,我相信大家对BitmapShader有了一定的了解了;当然了,如果你希望对Shader充分的了解,请参考爱歌的神作:
然后了解最后稍微有点复杂的setBitmapShader()
/**
* 设置BitmapShader,渲染图像,使用图像为绘制图形着色
*/
private void setBitmapShader() {
double scale = 1;
float dx = 0, dy = 0;
Bitmap bitmap = ((BitmapDrawable)getDrawable()).getBitmap();
BitmapShader bitmapShader = new BitmapShader(bitmap,
Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
//图片宽高
int bitmapWidth = bitmap.getWidth();
int bitmapHeight = bitmap.getHeight();
//视图宽高
int viewWidth = getWidth();
int viewHeight = getHeight();
if(type == CIRCLE){
int bSize = Math.min(bitmapWidth, bitmapHeight);
scale = circleWidth * 1.0 / bSize;
}else {
scale = Math.max(viewHeight * 1.0f / bitmapHeight, viewWidth * 1.0f / bitmapWidth);
}
if(bitmapWidth * viewHeight > bitmapHeight * viewWidth){
dx = (float) ((viewWidth - bitmapWidth*scale)*0.5f);
}else {
dy = (float) ((viewHeight - bitmapHeight*scale)*0.5f);
}
matrix.setScale((float) scale, (float) scale);
matrix.postTranslate(dx, dy);
mPaint.setShader(bitmapShader);
}
代码中首先将drawable转化为Bitmap,大家参考鸿洋大神这段代码也可以:
/**
* drawable转bitmap
*
* @param drawable
* @return
*/
private Bitmap drawableToBitamp(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, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, w, h);
drawable.draw(canvas);
return bitmap;
}
我只是为了图简便(人懒啊~~~)。接下来,如果type是圆形就获取图片宽高中的小值,然后根据测量宽高获取scale = circleWidth * 1.0 / bSize; bSize取小值是为了让缩放后的图片要大于imageView宽高,否则绘制时,可能出现在平移图片后,部分ImageView空白,拉伸影响图片质量问题。type是方形的差不多也是这样的原因。然后是平移图片的逻辑,其中bitmapWidth * viewHeight > bitmapHeight * viewWidth用来判断是水平平移还是竖直平移,大家可以自行画图测试下(贫道也是想了半天不懂,画图画懂的)
中间黑色是ImageView如果bitmapWidth * viewHeight > bitmapHeight * viewWidth,那么bitmap就代表红色的方块,dx = (float) ((viewWidth - bitmapWidth*scale)*0.5f),就是平移dx,否则是绿色方块,就平移dy。大家不难发现sx,dy计算的都是负值,这两做的效果就是不让中间这块空白,否则拉伸图片。最后就是通过matrix平移和缩放图片。然后将bitmapShader赋给画笔进行相应的绘制。
还有一个recf没有介绍,看下代码
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if(type == ROUND){
recf = new RectF(0, 0 , getWidth(), getHeight());
}
}
很简单在onSizeChanged中实例化RectF并赋给recf,好了客官,最后整个过程就基本结束了。
使用
引入自定义属性的命名空间
xmlns:app="http://schemas.android.com/apk/res-auto"
在布局中引用属性:
<com.chen.demo.view.CircleImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/a1"
app:borderRadio="5dp"
app:type="round"
/>
<com.chen.demo.view.CircleImageView
android:layout_marginTop="20dp"
android:layout_width="200dp"
android:layout_height="200dp"
android:src="@mipmap/a2"
app:borderRadio="5dp"
app:type="circle"
/>
最后
啰嗦了半天,其实这些逻辑还是挺简单的,但是我记得我第一次看自定义圆形图片时没看懂,然后直接copy的,不得不说知识还是靠积累和沉淀的,不是一下就会有很大提高的,大神除外。这篇文章还只是很简单的自定义,还可以为其加上边框等效果,这些我以后整理好了,再重新发布一篇文章,今天就到这里。