前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >群组头像拼接

群组头像拼接

作者头像
三流编程
发布2021-01-13 14:38:34
1.3K0
发布2021-01-13 14:38:34
举报

需求

聊天群组头像要拼成下图样式,最多显示 5 个头像,虽然我觉得两个人已经不是群组了,但是功能上可以删减人,依然保持群组。

截屏2021-01-08 下午2.07.18.png

自定义 View 方式实现

思路

要将多个人头像拼成一个,最初的设想是自定义 View,在 onDraw 里自己将多个 Bitmap 绘制上去。

首先分析这个设计效果,假设整个图片的宽为 width,高为 height:

  • 2 个人头像

未命名.png 大圆半径为 radius,小圆半径是 0.5*radius,并且第一张图从 0°,第二张图在 180°。

  • 5 个人头像,3、4 原理一样

未命名2.png 比如 5 个人头像,有一张图被盖住了两边,所以认为是第一张被绘制的图,在 270° 的位置,看设计尺寸,小图变径是 0.4*radius。然后每张图的圆心角度都增加 360°/5。 drawBitmap 需要指定范围 RectF,只要知道小图圆心坐标就行了。而所有小图的圆心都在红色圆上,比如蓝色小圆,圆心在最外面大圆的角度是第一张图所在 270° 加 360°/5(图片数目)*2(自己顺序),也是相对于红色圆的角度。 通过 PathMeasure 的 getLength() 先计算出红色圆的周长,然后通过 getgetPosTan() 算出这个圆心的坐标。

代码语言:javascript
复制
// 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

代码语言:javascript
复制
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);
        }
    }
}

然后

代码语言:javascript
复制
// 用 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 ModuleLoader 方式

实现自定义 ModuleLoader

参考文档,这种方式使用起来更优雅,更简洁。

除了 glide 依赖还要添加 annotationProcessor "com.github.bumptech.glide:compiler:$rootProject.glideVersion"

使用自定义的 ModuleLoader 加载

代码语言:javascript
复制
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,测试的确有用。

代码语言:javascript
复制
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 树的变化获取尺寸。

代码语言:javascript
复制
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

代码语言:javascript
复制
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;
}
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 需求
  • 自定义 View 方式实现
    • 思路
      • 实现
      • 自定义 Glide ModuleLoader 方式
        • 实现自定义 ModuleLoader
          • 使用自定义的 ModuleLoader 加载
            • 群组图片闪烁问题
            • 显示效果一个压一个
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档