github:https://github.com/ai2101039/YLCircleImageView
效果:
变态的需求
GIF
可用属性
/**
* 图片展示方式
* 0 -- 图片顶部开始展示,铺满,如果Y轴铺满时,X轴大,则图片水平居中
* 1 -- 图片中心点与指定区域中心重合
* 2 -- 图片底部开始展示,铺满,如果Y轴铺满时,X轴大,则图片水平居中
* 3 -- 图片完全展示
*/
public static final int TOP = 0;
public static final int CENTER = 1;
public static final int BOTTOM = 2;
public static final int FITXY = 3;
主要逻辑
1、使用Path创建图形路径
2、根据绘制所需占据的矩形RectF,在BitMap中找到相似矩形 Src,然后将BitMap指定区域内容绘制到 RectF中,配合 PorterDuffXfermode 进行挖洞
代码解读
1、构造器里面获取自定义属性的值,并提供默认值
2、initRadius,设置半径,看注释
private void initRadius() {
// 该处便于代码编写 如XML设置 radius = 20,topLeftRadius = 10,最终结果是 10 20 20 20
if (radius != 0) {
topLeftRadius = topLeftRadius == 0 ? radius : topLeftRadius;
...
}
// 如果设置了 radius = 20,topLeftRadius = 10,topLeftRadius_x = 30,
// 最终结果,topLeftRadius_x = 30,topLeftRadius_y = 10,其余 20
topLeftRadius_x = topLeftRadius_x == 0 ? topLeftRadius : topLeftRadius_x;
topLeftRadius_y = topLeftRadius_y == 0 ? topLeftRadius : topLeftRadius_y;
...
}
3、判断是否需要绘制多边形,也就是说,如果使用者不设置如下的属性,那么将等同于普通的 imageView
// 判断是否需要调用绘制函数
circle = borderWidth != 0 || borderSpace != 0 ||
topLeftRadius_x != 0 || topLeftRadius_y != 0 ||
topRightRadius_x != 0 || topRightRadius_y != 0 ||
bottomLeftRadius_x != 0 || bottomLeftRadius_y != 0 ||
bottomRightRadius_x != 0 || bottomRightRadius_y != 0;
4、针对Glide设置
if (circle) {
// 为什么设置这一条,因为Glide中,在into 源码内
// 不同的 ScaleType 会对drawable进行压缩,一旦压缩了,我们在onDraw里面获取图片的大小就没有意义了
setScaleType(ScaleType.MATRIX);
}
onDraw
知识点:
1、绘制边框,参考 https://blog.youkuaiyun.com/qq_26030147/article/details/84976626
在绘制边框时候,线宽是以线为中心,两边扩大,所以会有一半的线宽绘制不出来
所以,我们在绘制描边时候,调用 RectF.inset,调整矩形大小
2、绘制图片(2.3 与 2.5 是最重要的部分)
2.1、图片所要放的矩形 rectF,调整大小为,描边 + 间距
2.2、调用 canvas.saveLayer,得到 layerID
2.3、绘制圆角矩形,看函数 drawPath(canvas, rectF, borderPaint, i);
2.4、设置挖洞模式
2.5、根据 rectF,在BitMap中找到 与 rectF的相似矩形 src,然后返回
2.6、将 BitMap 指定大小的 src 区域的像素,绘制到 rectF,使用画笔 paint,而画笔已经设置了挖洞模式
2.7、paint还原,将 layerID 的图层绘制到 Canvas
@SuppressLint("DrawAllocation")
@Override
protected void onDraw(Canvas canvas) {
Drawable drawable = getDrawable();
// 使用局部变量,降低函数调用次数
int vw = getMeasuredWidth();
int vh = getMeasuredHeight();
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
// 绘制描边
if (borderWidth != 0) {
RectF rectF = new RectF(paddingLeft, paddingTop, vw - paddingRight, vh - paddingBottom);
// 描边会有一半处于框体之外
float i = borderWidth / 2;
// 移动矩形,以便于描边都处于view内
rectF.inset(i, i);
// 绘制描边,半径需要进行偏移 i
drawPath(canvas, rectF, borderPaint, i);
}
if ((null != drawable && circle)) {
RectF rectF = new RectF(paddingLeft, paddingTop, vw - paddingRight, vh - paddingBottom);
// 矩形需要缩小的值
float i = borderWidth + borderSpace;
// 这里解释一下,为什么要减去一个像素,因为像素融合时,由于锯齿的存在和图片像素不高,会导致图片和边框出现1像素的间隙
// 大家可以试一下,去掉这一句,然后用高清图就不会出问题,用非高清图就会出现
i = i > 1 ? i - 1 : 0;
// 矩形偏移
rectF.inset(i, i);
int layerId = canvas.saveLayer(rectF, null, Canvas.ALL_SAVE_FLAG);
// 多边形
drawPath(canvas, rectF, paint, i);
// 设置像素融合模式
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
// drawable转为 bitmap
Bitmap bitmap = drawableToBitmap(drawable);
// 根据图片的大小,控件的大小,图片的展示形式,然后来计算图片的src取值范围
Rect src = getSrc(bitmap, (int) rectF.width(), (int) rectF.height());
// dst取整个控件,也就是表示,我们的图片要占满整个控件
canvas.drawBitmap(bitmap, src, rectF, paint);
paint.setXfermode(null);
canvas.restoreToCount(layerId);
} else {
super.onDraw(canvas);
}
}
2.3、绘制圆角矩形,看函数 drawPath(canvas, rectF, borderPaint, i);
使用 path绘制圆角矩形,而且提供了 四个角,每个角都可以单独设置 X Y的半径,CW是顺时针的意思
/**
* 绘制多边形
*
* @param canvas 画布
* @param rectF 矩形
* @param paint 画笔
* @param offset 半径偏移量
*/
private void drawPath(Canvas canvas, RectF rectF, Paint paint, float offset) {
Path path = new Path();
path.addRoundRect(rectF,
new float[]{
topLeftRadius_x - offset, topLeftRadius_y - offset,
topRightRadius_x - offset, topRightRadius_y - offset,
bottomRightRadius_x - offset, bottomRightRadius_y - offset,
bottomLeftRadius_x - offset, bottomLeftRadius_y - offset}, Path.Direction.CW);
path.close();
canvas.drawPath(path, paint);
}
2.5、根据 rectF,在BitMap中找到 与 rectF的相似矩形 src,然后返回
这部分代码较长,好好看下,先看下示例图
最后一步
根据展示类型,修改图片截取区域。这部分就不解释了,很简单。
/**
* 这里详细说一下,我们的目标就是在 bitmap 中找到一个 和 view 宽高比例相等的 一块矩形
* tempRect,然后截取出来 放到整个view中
* tempRect 总是会存在
*
* @param bitmap bitmap
* @param rw 绘制区域的宽度
* @param rh 绘制区域的高度
* @return 矩形
*/
private Rect getSrc(@NonNull Bitmap bitmap, int rw, int rh) {
// bw bh,bitmap 的宽高
// vw vh,view 的宽高
int bw = bitmap.getWidth();
int bh = bitmap.getHeight();
int left = 0, top = 0, right = 0, bottom = 0;
// 判断 bw/bh 与 vw/vh
int temp1 = bw * rh;
int temp2 = rw * bh;
// 相似矩形的宽高
int[] tempRect = {bw, bh};
if (temp1 == temp2) {
return new Rect(0, 0, bw, bh);
}
// tempRect 的宽度比 bw 小
else if (temp1 > temp2) {
int tempBw = temp2 / rh;
tempRect[0] = tempBw;
}
// tempRect 的宽度比 bw 大
else if (temp1 < temp2) {
int tempBh = temp1 / rw;
tempRect[1] = tempBh;
}
// tempRect 的宽度与 bw 的比值
Boolean compare = bw > tempRect[0];
switch (styleType) {
case TOP:
// 从上往下展示,我们这里的效果是不止从上往下,compare = true,还要居中
left = compare ? (bw - tempRect[0]) / 2 : 0;
top = 0;
right = compare ? (bw + tempRect[0]) / 2 : tempRect[0];
bottom = tempRect[1];
break;
case CENTER:
// 居中
left = compare ? (bw - tempRect[0]) / 2 : 0;
top = compare ? 0 : (bh - tempRect[1]) / 2;
right = compare ? (bw + tempRect[0]) / 2 : tempRect[0];
bottom = compare ? tempRect[1] : (bh + tempRect[1]) / 2;
break;
case BOTTOM:
left = compare ? (bw - tempRect[0]) / 2 : 0;
top = compare ? 0 : bh - tempRect[1];
right = compare ? (bw + tempRect[0]) / 2 : tempRect[0];
bottom = compare ? tempRect[1] : bh;
break;
case FITXY:
left = 0;
top = 0;
right = bw;
bottom = bh;
break;
default:
}
return new Rect(left, top, right, bottom);
}
总结:经过上面的步骤,我们在图片中找到了一块内容,这块内容所在的矩形大小 和 将要绘制到的区域矩形 是相似的。
比如我们找到的区域是 0,0,300,300,目标矩形是 150 X 150,那么就相当于把图片压缩2倍。
这个和 图片大小 300 X 300,ImageView 宽高 150 X 150 ,是一个意思