聊天群组头像要拼成下图样式,最多显示 5 个头像,虽然我觉得两个人已经不是群组了,但是功能上可以删减人,依然保持群组。
截屏2021-01-08 下午2.07.18.png
要将多个人头像拼成一个,最初的设想是自定义 View,在 onDraw 里自己将多个 Bitmap 绘制上去。
首先分析这个设计效果,假设整个图片的宽为 width,高为 height:
未命名.png 大圆半径为 radius,小圆半径是 0.5*radius,并且第一张图从 0°,第二张图在 180°。
未命名2.png 比如 5 个人头像,有一张图被盖住了两边,所以认为是第一张被绘制的图,在 270° 的位置,看设计尺寸,小图变径是 0.4*radius。然后每张图的圆心角度都增加 360°/5。 drawBitmap 需要指定范围 RectF,只要知道小图圆心坐标就行了。而所有小图的圆心都在红色圆上,比如蓝色小圆,圆心在最外面大圆的角度是第一张图所在 270° 加 360°/5(图片数目)*2(自己顺序),也是相对于红色圆的角度。 通过 PathMeasure 的 getLength() 先计算出红色圆的周长,然后通过 getgetPosTan() 算出这个圆心的坐标。
// View 宽 width,高 height,大圆半径 radius,头像小圆半径 bitmapRadius,小图索引为 i,第一张图坐标是 startAngle
Path path = new Path();
// 红色圆,辅助计算头像小图圆心坐标
path.addCircle(width/2f, height/2f, radius - bitmapRadius, Path.Direction.CW);
// 测出红色圆总长度
PathMeasure measure = new PathMeasure();
measure.setPath(path, false);
float length = measure.getLength();
// 头像小圆圆心在红色圆上的角度
float angle = startAngle + i * 360f/bitmapSize;
if (angle > 360) {
angle -= 360;
}
// 计算头像小圆圆心坐标
float[] pos = new float[2];
measure.getPosTan(length * angle / 360f, pos, null);
// 根据坐标和半径圈出头像小图的范围
RectF rectF = new RectF(pos[0] - bitmapRadius, pos[1] - bitmapRadius, pos[0] + bitmapRadius, pos[1] + bitmapRadius);
// 根据这个范围绘制头像
canvas.drawBitmap(bitmaps.get(i), null, rectF, paint);
自定义 View
public class MucAvatar extends ImageView {
private Paint paint;
public MucAvatar(Context context) {
super(context);
}
public MucAvatar(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public MucAvatar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
{
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.parseColor("#E1E0E0"));
}
private List<Bitmap> bitmaps;
private float totalRadius;
// 要绘制的所有头像
public void setBitmap(List<Bitmap> bitmaps) {
if (bitmaps == null || bitmaps.isEmpty()) return;
if (bitmaps.size() == 1) {
// 一张图就直接设置
setImageBitmap(bitmaps.get(0));
} else {
this.bitmaps = bitmaps;
invalidate(); // 绘制
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (bitmaps == null || bitmaps.size() <= 1) return;
int width = getWidth();
int height = getHeight();
// 理论上 View 在 xml 中应该写宽高一样的
totalRadius = Math.min(width, height)/2f;
// 先画上一个圆底
canvas.drawCircle(width/2f, height/2f, totalRadius, paint);
switch (bitmaps.size()) {
// case 1:
// canvas.drawBitmap(bitmaps.get(0), null, new RectF(width/2f-totalRadius,height/2f-totalRadius,width/2f+totalRadius,height/2f+totalRadius), paint);
// break;
case 2:
drawBitmaps(canvas, 0.5f, 0);
break;
case 3:
drawBitmaps(canvas, 0.5f, 270);
break;
case 4:
drawBitmaps(canvas, 9f/20f, 45);
break;
default:
drawBitmaps(canvas, 0.4f, 270);
break;
}
}
/**
* @param radiusScale 是整体半径的几分之几
* @param startAngle 第一张图片,相对于0度的偏移角度
*/
private void drawBitmaps(Canvas canvas, float radiusScale, float startAngle) {
float bitmapRadius = totalRadius * radiusScale;
int bitmapSize = Math.min(bitmaps.size(), 5);
Path path = new Path();
path.addCircle(getWidth()/2f, getHeight()/2f, totalRadius - bitmapRadius, Path.Direction.CW);
PathMeasure measure = new PathMeasure();
measure.setPath(path, false);
float length = measure.getLength();
float[] pos = new float[2];
for (int i = 0; i < bitmapSize; i++) {
float angle = startAngle + i * 360f/bitmapSize;
if (angle > 360) {
angle -= 360;
}
measure.getPosTan(length * angle / 360f, pos, null);
RectF rectF = new RectF(pos[0] - bitmapRadius, pos[1] - bitmapRadius, pos[0] + bitmapRadius, pos[1] + bitmapRadius);
canvas.drawBitmap(bitmaps.get(i), null, rectF, paint);
}
}
}
然后
// 用 glide 拿到 Bitmap
public static void getAvatarBitmap(Fragment fragment, String url, SingleObserver<Bitmap> singleObserver) {
io.reactivex.Single.create(new SingleOnSubscribe<Bitmap>() {
@Override
public void subscribe(@io.reactivex.annotations.NonNull SingleEmitter<Bitmap> emitter) throws Exception {
Bitmap bitmap;
try {
bitmap = Glide.with(fragment)
.asBitmap()
.load(mUrl)
.apply(RequestOptions.circleCropTransform())
.priority(Priority.LOW)
.submit(BaseUtil.dp2px(20), BaseUtil.dp2px(20))
.get();
} catch (Exception e) {
bitmap = BitmapFactory.decodeResource(fragment.getResources(), R.drawable.defalut);
}
emitter.onSuccess(bitmap);
}
}).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(singleObserver);
}
MucAvatar mucAvatar; // xml 中用这个控件
List<Bitmap> bitmaps = new ArrayList<>();
for (int i = 0; i<bitmapSize; i++) {
ImageLoaderHelper.getAvatarBitmap(fragment, url, new SingleObserver<Bitmap>() {
@Override
public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) {
}
@Override
public void onSuccess(@io.reactivex.annotations.NonNull Bitmap bitmap) {
bitmaps.add(bitmap);
if (bimmaps.size()==bitmapSize) {
mucAvatar.setBitmap(bitmaps);
}
}
@Override
public void onError(@io.reactivex.annotations.NonNull Throwable e) {
}
});
}
若在 RecyclerView 中使用,会因复用产生错乱问题,要加 tag,Bitmap 列表取回来后进行对比,这已经不是这个头像拼接本身问题了。
参考文档,这种方式使用起来更优雅,更简洁。
除了 glide 依赖还要添加 annotationProcessor "com.github.bumptech.glide:compiler:$rootProject.glideVersion"
List<MucMember> mucMembers
GlideApp.with(fragment.getContext())
.load(new MucAvatarModel(mucMembers))
.priority(Priority.LOW)
.placeholder(R.drawable.default)
.into(imageView);
在网上搜到的,说 Target 不直接用 ImageView,用普通的 Target,在 onResourceReady 中手动 setImageDrawable,测试的确有用。
public class RecyclerViewItemTarget extends CustomTarget<Drawable> {
private ImageView iv;
private int position;
public RecyclerViewItemTarget(ImageView iv, int position) {
this.iv = iv;
this.position = position;
this.iv.setTag(position); // 复用后,可能被其它position改变这个tag
}
@Override
public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
if ((int)iv.getTag() == position) {
iv.setImageDrawable(resource);
}
}
@Override
public void onLoadCleared(@Nullable Drawable placeholder) {
iv = null;
}
}
后来发现有崩溃,原因是这个 Target 中宽高没设置,想起过去看 Glide3 的 SimpleTarget 了,Glide4 原理也类似,看源码必须把尺寸传进去,不如直接继承 ViewTarget 了,把 View 传进去,框架自己会监听 View 树的变化获取尺寸。
public class RecyclerViewItemTarget extends CustomViewTarget<ImageView, Drawable> {
private ImageView iv;
private int position;
public RecyclerViewItemTarget(ImageView iv, int position) {
super(iv); // 传给框架
this.iv = iv;
this.position = position;
this.iv.setTag(position); // 复用后,可能被其它position改变这个tag
}
@Override
public void onLoadFailed(@Nullable Drawable errorDrawable) {
if ((int)iv.getTag() == position) {
iv.setImageDrawable(errorDrawable);
}
}
@Override
public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
if ((int)iv.getTag() == position) {
iv.setImageDrawable(resource);
}
}
@Override
protected void onResourceCleared(@Nullable Drawable placeholder) {
// 不要设置,不然 RecyclerView item 点击去其它页面时会有个闪烁
}
}
突然变了设计,多张图片有重叠时,每一张图压着别人,也被别人压着,不能像原来那样第一张图被第二张和最后一张都压着。想想其实有很多中实现方法吧。
现在的思路是再搞一张和原来的大圆一模一样的原图,然后将第一张和最后一张以同样的位置在第二个画布上绘制,将两者做个效果,让第一张图被压的那一角跑到最后一张上面去,然后再将这张画布的图片覆盖到原来的上面。
使用 SRC_ATOP 模式,最后一张图先绘作为 DST(黄色),然后绘制第一张图作为 SRC(蓝色),这样第一张图的一角就盖在了最后一张图上面。
16101093058097.png
修改 MucAvatarDataFetcher
private void drawBitmaps(List<Bitmap> bitmaps, Canvas canvas, float radiusScale, float startAngle) {
float bitmapRadius = totalRadius * radiusScale;
int bitmapSize = Math.min(bitmaps.size(), 5);
Path path = new Path();
path.addCircle(width / 2f, height / 2f, totalRadius - bitmapRadius, Path.Direction.CW);
PathMeasure measure = new PathMeasure();
measure.setPath(path, false);
float length = measure.getLength();
float[] pos = new float[2];
RectF rectF0 = null; // 记下第一张图片的位置
for (int i = 0; i < bitmapSize; i++) {
float angle = startAngle + i * 360f / bitmapSize;
if (angle > 360) {
angle -= 360;
}
measure.getPosTan(length * angle / 360f, pos, null);
RectF rectF = new RectF(pos[0] - bitmapRadius, pos[1] - bitmapRadius, pos[0] + bitmapRadius, pos[1] + bitmapRadius);
if (i == 0) {
rectF0 = rectF;
}
if (i == bitmapSize-1) {
// 最后一张图片,生成另一个大的图片
canvas.drawBitmap(getBitmapLast(bitmaps.get(i), bitmaps.get(0), rectF, rectF0), 0,0, paint);
} else {
// 其它图片还是按原来方式绘制
canvas.drawBitmap(bitmaps.get(i), null, rectF, paint);
}
}
}
private Bitmap getBitmapLast(Bitmap bitmapLast, Bitmap bitmap0, RectF rectFLast, RectF rectF0) {
// 新画布
Bitmap output = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(output);
// 先绘制最后一张图
canvas.drawBitmap(bitmapLast, null, rectFLast, paint);
// 模式为 SRC_ATOP
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));
// 绘制第一张图
canvas.drawBitmap(bitmap0, null, rectF0 ,paint);
return output;
}