Android 仿钉钉群组头像实现

最近做即时聊天功能,一个需求是自定义群头像效果仿钉钉和微信的群头像,记录一下实现的过程逻辑,以下便是实现的流程和代码

前言

Demo 样例运行的环境是 Java 17,效果图如下:

 

1、自定义属性

在res/values目录下面新建attrs.xml文件用于自定义属性,代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CombinedView">
        <!-- 头像的背景色 -->
        <attr name="combined_backgroundColor" format="color" />
        <!-- 头像的形状,圆角方形和圆形 -->
        <attr name="combined_shape" format="enum">
            <enum name="round" value="1" />
            <enum name="circle" value="2" />
        </attr>
        <!-- 圆角方形的圆角半径-->
        <attr name="combined_roundRadius" format="dimension" />
        <!-- 宫格分割线的颜色 -->
        <attr name="combined_dividerColor" format="color" />
        <!-- 宫格分割线的宽度 -->
        <attr name="combined_dividerWidth" format="dimension" />
        <!-- 宫格文本的颜色 -->
        <attr name="combined_textColor" format="color" />
        <!-- 只有一个宫格头像文字的大小,多个宫格时根据该值来计算 -->
        <attr name="combined_textSize" format="dimension" />
    </declare-styleable>

</resources>

2、自定义属性类 

群组头像的一些自定义属性:

  1. 背景颜色  backgroundColor
  2. 宫格文本文字大小  textSize
  3. 宫格文本文字颜色  textColor
  4. 头像圆角半径  roundRadius
  5. 头像形状 shape
  6. 宫格分割线宽度  dividerWidth
  7. 宫格分割线颜色  dividerColor

具体代码如下:

public class CombinedAttrs {
    /**
     * 头像的形状,圆角方形和圆形
     */
    @CombinedShape
    int shape;
    /**
     * 圆角的半径
     */
    float roundRadius;
    /**
     * 宫格分割线的宽度
     */
    float dividerWidth;
    /**
     * 宫格分割线的颜色
     */
    int dividerColor;
    /**
     * 整个头像的背景色
     */
    int backgroundColor;
    /**
     * 文字头像文字最大值
     */
    float textSize;
    /**
     * 文字头像文字的颜色
     */
    int textColor;

    public CombinedAttrs(@CombinedShape int shape, float roundRadius, float dividerWidth,
                         int dividerColor, int backgroundColor, float textSize, int textColor) {
        this.shape = shape;
        this.roundRadius = roundRadius;
        this.dividerWidth = dividerWidth;
        this.dividerColor = dividerColor;
        this.backgroundColor = backgroundColor;
        this.textSize = textSize;
        this.textColor = textColor;
    }
}

 3、头像形状定义

头像分为圆角方形和圆形两种头像,定位的枚举值如下所示:

import androidx.annotation.IntDef;


@IntDef({CombinedShape.ROUND, CombinedShape.CIRCLE})
public @interface CombinedShape {
    /**
     * 圆角方形
     */
    int ROUND = 1;
    /**
     * 圆形
     */
    int CIRCLE = 2;
}

4、宫格的基础信息

主要包含文本头像的文本信息,自定义头像的头像链接

import androidx.annotation.ColorInt;


public class Combined {
    /**
     * 文本框的背景色
     */
    @ColorInt
    private int backgroundColor;
    /**
     * 没有自定义头像的联系人采用文本绘制头像
     */
    private String text;
    /**
     * 自定义头像链接
     */
    private String url;

    public int getBackgroundColor() {
        return backgroundColor;
    }

    public void setBackgroundColor(int backgroundColor) {
        this.backgroundColor = backgroundColor;
    }

    public String getText() {
        return text == null ? "" : text;
    }

    public void setText(String text) {
        this.text = text;
    }

    public String getUrl() {
        return url == null ? "" : url;
    }

    public void setUrl(String url) {
        this.url = url;
    }
}

5、自定义控件

使用自定义控件绘制显示群组头像,自定义控件代码如下所示:

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Rect;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

import androidx.annotation.Nullable;

import com.example.combined.R;

import java.util.ArrayList;
import java.util.List;


public class CombinedView extends View {
    public static final String TAG = "CombinedView";
    /**
     * 自定义属性
     */
    CombinedAttrs mCombinedAttrs;
    CombinedCanvas mCombinedCanvas;
    Bitmap mCacheBitmap;
    Rect srcRect = new Rect();
    Rect dstRect = new Rect();
    List<Combined> combinedList = new ArrayList<>();

    public CombinedView(Context context) {
        this(context, null);
    }

    public CombinedView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CombinedView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        // 读取自定义属性
        TypedArray attributes = context.obtainStyledAttributes(attrs, R.styleable.CombinedView);
        int shape = attributes.getInteger(R.styleable.CombinedView_combined_shape, CombinedShape.ROUND);
        int dividerColor = attributes.getColor(R.styleable.CombinedView_combined_dividerColor, Color.WHITE);
        int backgroundColor = attributes.getColor(R.styleable.CombinedView_combined_backgroundColor, Color.GRAY);
        float roundRadius = attributes.getDimension(R.styleable.CombinedView_combined_roundRadius, 8f);
        float dividerWidth = attributes.getDimension(R.styleable.CombinedView_combined_dividerWidth, 1f);
        float textSize = attributes.getDimension(R.styleable.CombinedView_combined_textSize, 16f);
        int textColor = attributes.getColor(R.styleable.CombinedView_combined_textColor, Color.WHITE);
        mCombinedAttrs = new CombinedAttrs(shape, roundRadius, dividerWidth, dividerColor, backgroundColor, textSize, textColor);
        // 回收资源
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            attributes.close();
        } else {
            attributes.recycle();
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        newCombinedCanvas();
        performDraw();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(Color.TRANSPARENT);
        if (mCacheBitmap != null) {
            // bitmap 的位置大小
            srcRect.left = 0;
            srcRect.top = 0;
            srcRect.right = mCacheBitmap.getWidth();
            srcRect.bottom = mCacheBitmap.getHeight();

            // 目标位置的大小
            dstRect.left = 0;
            dstRect.top = 0;
            dstRect.right = getWidth();
            dstRect.bottom = getHeight();
            canvas.drawBitmap(mCacheBitmap, srcRect, dstRect, null);
        }
    }

    /**
     * 对外开发的调用方法
     *
     * @param list List<Combined> 宫格数据源
     */
    public void drawCombined(List<Combined> list) {
        this.combinedList.clear();
        this.combinedList.addAll(list);
        newCombinedCanvas();
        performDraw();
    }

    private void performDraw() {
        mCombinedCanvas.draw(getContext(), getMeasuredWidth(), getMeasuredHeight(), combinedList, (list, bitmap) -> {
            Log.d(TAG, "onCombinedCanvas: ");
            mCacheBitmap = bitmap;
            postInvalidate();
        });
    }

    /**
     * 初始化
     */
    private void newCombinedCanvas() {
        if (mCombinedCanvas == null) {
            mCombinedCanvas = new CombinedCanvas(mCombinedAttrs);
        }
    }

6、群组头像的绘制

  1. 创建两张canvas画布,用户绘制主图层和裁剪覆盖图层
  2. 计算每个宫格头像的位置和大小
  3. 如果有自定义头像链接,使用glide加载头像,头像请求到刷新绘制
  4. 如果是文本头像,计算文本文字的大小
  5. 绘制宫格的分割线
  6. 绘制裁剪覆盖图层,裁剪圆角或圆形

具体实现代码如下所示:

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.text.TextUtils;
import android.util.Log;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


public class CombinedCanvas {
    public static final String TAG = "CombinedCanvas";
    Context mContext;
    OnCombinedCanvasCallback mCallback;
    /**
     * 画布的宽高
     */
    int width, height;
    /**
     * 画笔
     */
    Paint mPaint;
    /**
     * 裁剪图层的大小范围
     */
    RectF mOverlayRectF;
    /**
     * Bitmap 的大小范围
     */
    Rect srcRect = new Rect();
    /**
     * 图层裁剪模式 PorterDuff.Mode.DST_IN
     */
    PorterDuffXfermode mPorterMode;
    /**
     * 主图层和裁剪图层画布
     */
    Canvas mCanvas, mOverlayCanvas;
    /**
     * 主图层Bitmap和裁剪图层Bitmap
     */
    Bitmap mCanvasBitmap, mOverlayBitmap;
    /**
     * 宫格数据源
     */
    List<Combined> mCombinedList = new ArrayList<>();
    /**
     * 宫格的位置数据源
     */
    List<RectF> mCombinedCells = new ArrayList<>();
    /**
     * 链接头像的缓存数据源
     */
    Map<Integer, Bitmap> mBitmapCache = new HashMap<>();
    /**
     * 自定义属性
     */
    CombinedAttrs mCombinedAttrs;

    public CombinedCanvas(CombinedAttrs attrs) {
        this.mCombinedAttrs = attrs;
        mPaint = new Paint();
        mPaint.setAntiAlias(true);

        mPorterMode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
        mOverlayRectF = new RectF();
    }

    /**
     * 对外提供的绘制调用方法
     *
     * @param context      Context 上下文环境
     * @param width        int 画布宽度
     * @param height       int 画布高度
     * @param combinedList List<Combined> 宫格数据源
     * @param callback     OnCombinedCanvasCallback 头像回调接口
     */
    public void draw(Context context, int width, int height, List<Combined> combinedList, OnCombinedCanvasCallback callback) {
        if (width == 0 || height == 0) {
            Log.w(TAG, "the canvas width and height must be > 0 ");
            return;
        }
        this.mContext = context;
        this.width = width;
        this.height = height;
        this.mCallback = callback;
        this.mCombinedList.clear();
        this.mCombinedList.addAll(combinedList);

        newCombinedCanvas();
        newOverlayCanvas();
        loadCellImage();
    }

    /**
     * 判断加载头像链接
     */
    private void loadCellImage() {
        computeCombinedCellLocation();
        mBitmapCache.clear();
        for (int index = 0; index < mCombinedList.size(); index++) {
            Combined combined = mCombinedList.get(index);
            RectF rectF = mCombinedCells.get(index);
            if (hasURL(combined)) {
                Log.d(TAG, "loadCellImage: ");
                CombinedLoader.getInstance().loadImage(mContext, (int) rectF.width(), (int) rectF.height(), index,
                        combined.getUrl(), (retCode, index1, newBitmap) -> {
                            Log.d(TAG, "loadCellImage index:" + index1 + " draw new bitmap: " + newBitmap);
                            mBitmapCache.put(index1, newBitmap);
                            performDraw();
                        });
            }
        }
        performDraw();
    }

    /**
     * 创建主图层
     */
    private void newCombinedCanvas() {
        if (mCanvas == null) {
            mCanvasBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
            mCanvas = new Canvas(mCanvasBitmap);
        }
        mCanvas.drawColor(mCombinedAttrs.backgroundColor);
    }

    /**
     * 创建裁剪图层
     */
    private void newOverlayCanvas() {
        if (mOverlayCanvas == null) {
            mOverlayBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
            mOverlayCanvas = new Canvas(mOverlayBitmap);
        }
        mPaint.setXfermode(null);
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(mCombinedAttrs.backgroundColor);

        mOverlayRectF.left = 0;
        mOverlayRectF.top = 0;
        mOverlayRectF.right = getWidth();
        mOverlayRectF.bottom = getHeight();

        if (mCombinedAttrs.shape == CombinedShape.ROUND) {
            mOverlayCanvas.drawRoundRect(mOverlayRectF, mCombinedAttrs.roundRadius, mCombinedAttrs.roundRadius, mPaint);
        } else if (mCombinedAttrs.shape == CombinedShape.CIRCLE) {
            float centerX = getWidth() * 1f / 2;
            float centerY = getHeight() * 1f / 2;
            float circleRadius = Math.min(centerX, centerY);
            mOverlayCanvas.drawCircle(centerX, centerY, circleRadius, mPaint);
        }
    }

    /**
     * 绘制头像
     */
    private void performDraw() {
        this.mPaint.setXfermode(null);
        this.mPaint.setAntiAlias(true);
        this.mPaint.setStyle(Paint.Style.STROKE);

        drawCombined();
        drawDividerLine();
        drawOverlay();
        onResultCallback();
    }

    /**
     * 绘制每个宫格的头像
     */
    private void drawCombined() {
        mCanvas.drawColor(mCombinedAttrs.backgroundColor);
        for (int i = 0; i < mCombinedCells.size(); i++) {
            RectF rectF = mCombinedCells.get(i);
            Combined combined = mCombinedList.get(i);
            mPaint.setStyle(Paint.Style.FILL);
            mPaint.setColor(combined.getBackgroundColor());
            mCanvas.drawRect(rectF, mPaint);

            if (hasURL(combined)) {
                Bitmap bitmap = mBitmapCache.get(i);
                if (bitmap != null) {
                    srcRect.left = 0;
                    srcRect.top = 0;
                    srcRect.right = bitmap.getWidth();
                    srcRect.bottom = bitmap.getHeight();
                    mCanvas.drawBitmap(bitmap, srcRect, rectF, null);
                }
            } else {
                drawText(combined, rectF, i);
            }
        }
    }

    /**
     * 绘制宫格的文本头像
     *
     * @param combined Combined 宫格信息
     * @param rectF    RectF 坐位置
     * @param index    int 宫格下标
     */
    private void drawText(Combined combined, RectF rectF, int index) {
        float left = rectF.left;
        float top = rectF.top;
        float textSize = computeTextSize(index);
        String text = getText(combined);

        mPaint.setTextSize(textSize);
        mPaint.setColor(mCombinedAttrs.textColor);

        Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
        float textWidth = mPaint.measureText(text);


        float textTop = fontMetrics.top;
        float textBottom = fontMetrics.bottom;
        float textHeight = textBottom + textTop;

        float x = left + rectF.width() / 2 - textWidth / 2;
        float y = top + rectF.height() / 2 - textHeight / 2;
        mCanvas.drawText(text, x, y, mPaint);
    }

    /**
     * 绘制宫格间的分割线
     */
    private void drawDividerLine() {
        int size = mCombinedList.size();
        float dividerWidth = mCombinedAttrs.dividerWidth;
        float offset = dividerWidth / 2;
        float startX, startY;
        float endX, endY;

        mPaint.setColor(mCombinedAttrs.dividerColor);
        mPaint.setStrokeWidth(dividerWidth);

        if (size == 2) {
            startY = 0;
            startX = getWidth() * 1f / 2 - offset;
            endX = getWidth() * 1f / 2 - offset;
            endY = getHeight();
            mCanvas.drawLine(startX, startY, endX, endY, mPaint);
        } else if (size == 3) {
            startX = getWidth() * 1f / 2 - offset;
            startY = 0;
            endX = getWidth() * 1f / 2 - offset;
            endY = getHeight();
            mCanvas.drawLine(startX, startY, endX, endY, mPaint);

            startX = getWidth() * 1f / 2;
            startY = getHeight() * 1f / 2 - offset;
            endX = getWidth();
            endY = getHeight() * 1f / 2 - offset;
            mCanvas.drawLine(startX, startY, endX, endY, mPaint);
        } else if (size == 4) {
            startX = getWidth() * 1f / 2 - offset;
            startY = 0;
            endX = getWidth() * 1f / 2 - offset;
            endY = getHeight();
            mCanvas.drawLine(startX, startY, endX, endY, mPaint);

            startX = 0;
            startY = getHeight() * 1f / 2 - offset;
            endX = getWidth();
            endY = getHeight() * 1f / 2 - offset;
            mCanvas.drawLine(startX, startY, endX, endY, mPaint);
        } else {
            List<float[]> cellLocations = computeCells(size);
            float stepX = getWidth() * 1f / 3;
            float stepY = getHeight() * 1f / 3;
            if (size == 5) {
                float[] location = cellLocations.get(0);

                startX = location[0] + stepX - offset;
                endX = location[0] + stepX - offset;
                startY = location[1];
                endY = location[1] + stepY;
                mCanvas.drawLine(startX, startY, endX, endY, mPaint);


                location = cellLocations.get(2);
                startX = location[0];
                endX = location[0] + getHeight();
                startY = location[1] - offset;
                endY = location[1] - offset;
                mCanvas.drawLine(startX, startY, endX, endY, mPaint);


                startX = location[0] + stepX - offset;
                endX = location[0] + stepX - offset;
                startY = location[1];
                endY = location[1] + stepY;
                mCanvas.drawLine(startX, startY, endX, endY, mPaint);

                startX = location[0] + stepX * 2 - offset;
                endX = location[0] + stepX * 2 - offset;
                startY = location[1];
                endY = location[1] + stepY;
                mCanvas.drawLine(startX, startY, endX, endY, mPaint);

            } else if (size == 6) {
                float[] loc = cellLocations.get(1);
                startX = loc[0] - offset;
                endX = loc[0] - offset;
                startY = loc[1];
                endY = loc[1] + stepY * 2;
                mCanvas.drawLine(startX, startY, endX, endY, mPaint);

                loc = cellLocations.get(2);
                startX = loc[0] - offset;
                endX = loc[0] - offset;
                startY = loc[1];
                endY = loc[1] + stepY * 2;
                mCanvas.drawLine(startX, startY, endX, endY, mPaint);

                loc = cellLocations.get(3);
                startX = loc[0];
                endX = loc[0] + getWidth();
                startY = loc[1] - offset;
                endY = loc[1] - offset;
                mCanvas.drawLine(startX, startY, endX, endY, mPaint);

            } else if (size == 7) {
                float[] loc = cellLocations.get(2);
                startX = 0;
                endX = getWidth();
                startY = loc[1] - offset;
                endY = loc[1] - offset;
                mCanvas.drawLine(startX, startY, endX, endY, mPaint);

                startX = loc[0] - offset;
                endX = loc[0] - offset;
                startY = loc[1];
                endY = loc[1] + stepY * 2;
                mCanvas.drawLine(startX, startY, endX, endY, mPaint);

                loc = cellLocations.get(3);
                startX = loc[0] - offset;
                endX = loc[0] - offset;
                startY = loc[1];
                endY = loc[1] + stepY * 2;
                mCanvas.drawLine(startX, startY, endX, endY, mPaint);

                loc = cellLocations.get(4);
                startX = loc[0];
                endX = loc[0] + getWidth();
                startY = loc[1] - offset;
                endY = loc[1] - offset;
                mCanvas.drawLine(startX, startY, endX, endY, mPaint);

            } else if (size == 8) {
                float[] loc = cellLocations.get(1);
                startX = loc[0] - offset;
                endX = loc[0] - offset;
                startY = loc[1];
                endY = loc[1] + stepY;
                mCanvas.drawLine(startX, startY, endX, endY, mPaint);

                loc = cellLocations.get(3);
                startX = 0;
                endX = getWidth();
                startY = loc[1] - offset;
                endY = loc[1] - offset;
                mCanvas.drawLine(startX, startY, endX, endY, mPaint);

                startX = loc[0] - offset;
                endX = loc[0] - offset;
                startY = loc[1];
                endY = loc[1] + stepY * 2;
                mCanvas.drawLine(startX, startY, endX, endY, mPaint);

                loc = cellLocations.get(4);
                startX = loc[0] - offset;
                endX = loc[0] - offset;
                startY = loc[1];
                endY = loc[1] + stepY * 2;
                mCanvas.drawLine(startX, startY, endX, endY, mPaint);

                loc = cellLocations.get(5);
                startX = loc[0];
                endX = loc[0] + getWidth();
                startY = loc[1] - offset;
                endY = loc[1] - offset;
                mCanvas.drawLine(startX, startY, endX, endY, mPaint);

            } else if (size == 9) {
                float[] loc = cellLocations.get(1);
                startX = loc[0] - offset;
                endX = loc[0] - offset;
                startY = loc[1];
                endY = loc[1] + getHeight();
                mCanvas.drawLine(startX, startY, endX, endY, mPaint);

                loc = cellLocations.get(2);
                startX = loc[0] - offset;
                endX = loc[0] - offset;
                startY = loc[1];
                endY = loc[1] + getHeight();
                mCanvas.drawLine(startX, startY, endX, endY, mPaint);

                loc = cellLocations.get(3);
                startX = loc[0];
                endX = loc[0] + getWidth();
                startY = loc[1] - offset;
                endY = loc[1] - offset;
                mCanvas.drawLine(startX, startY, endX, endY, mPaint);

                loc = cellLocations.get(6);
                startX = loc[0];
                endX = loc[0] + getWidth();
                startY = loc[1] - offset;
                endY = loc[1] - offset;
                mCanvas.drawLine(startX, startY, endX, endY, mPaint);
            }
        }
    }

    /**
     * 绘制覆盖裁剪图层
     */
    private void drawOverlay() {
        mPaint.setXfermode(mPorterMode);
        mCanvas.drawBitmap(mOverlayBitmap, 0, 0, mPaint);
    }

    /**
     * 计算宫格的起始坐标和位置
     */
    private void computeCombinedCellLocation() {
        int size = mCombinedList.size();
        List<RectF> rectList = new ArrayList<>(size);
        for (int index = 0; index < mCombinedList.size(); index++) {
            RectF rectF = computeCellRectF(index, size);
            rectList.add(rectF);
        }
        mCombinedCells.clear();
        mCombinedCells.addAll(rectList);
    }

    /**
     * 计算每个宫格的坐标位置
     *
     * @param index int
     * @param size  int
     * @return RectF
     */
    private RectF computeCellRectF(int index, int size) {
        RectF rectF = new RectF();
        float left = 0;
        float top = 0;
        float width = getWidth();
        float height = getHeight();

        if (size == 2) {
            width = getWidth() * 1f / 2;
            if (index == 1) {
                left = getWidth() * 1f / 2;
                top = 0;
            }
        } else if (size == 3) {
            width = getWidth() * 1f / 2;
            if (index == 1) {
                height = getHeight() * 1f / 2;
                left = getWidth() * 1f / 2;
                top = 0;
            } else if (index == 2) {
                height = getHeight() * 1f / 2;
                left = getWidth() * 1f / 2;
                top = getHeight() * 1f / 2;
            }
        } else if (size == 4) {
            width = getWidth() * 1f / 2;
            height = getHeight() * 1f / 2;
            if (index == 0) {
                left = 0;
                top = 0;
            } else if (index == 1) {
                left = getWidth() * 1f / 2;
                top = 0;
            } else if (index == 2) {
                left = 0;
                top = getHeight() * 1f / 2;
            } else if (index == 3) {
                left = getWidth() * 1f / 2;
                top = getHeight() * 1f / 2;
            }
        } else if (size >= 5) {
            width = getWidth() * 1f / 3;
            height = getHeight() * 1f / 3;
            float[] location = computeCells(size).get(index);
            left = location[0];
            top = location[1];
        }
        rectF.left = left;
        rectF.top = top;
        rectF.right = left + width;
        rectF.bottom = top + height;
        return rectF;
    }

    /**
     * 计算宫格起始的x,y坐标
     *
     * @param size int
     * @return List<float [ ]>
     */
    private List<float[]> computeCells(int size) {
        List<float[]> locationList = new ArrayList<>();
        float left;
        float top;
        float width = getWidth() * 1f / 3;
        float height = getHeight() * 1f / 3;
        int residue = size % 3;
        int row = size / 3 + (residue > 0 ? 1 : 0);
        float y = computeY(row);
        Log.d(TAG, "computeCells row: " + row);
        for (int i = 0; i < row; i++) {
            top = y + height * i;
            int column = computeColumn(i, residue);
            float x = computeX(i, residue);
            for (int k = 0; k < column; k++) {
                left = x + k * width;
                locationList.add(new float[]{left, top});
            }
        }
        return locationList;
    }

    /**
     * 计算每行的列数
     *
     * @param index   int  行数
     * @param residue int  余数
     * @return int
     */
    private int computeColumn(int index, int residue) {
        if (index == 0 && residue > 0) {
            return residue;
        }
        return 3;
    }

    /**
     * 计算每行起始的y轴的位置
     *
     * @param row int 行数
     * @return float
     */
    private float computeY(int row) {
        float height = getHeight() * 1f / 3;
        return row == 3 ? 0 : height / 2;
    }

    /**
     * 计算每行起始的x轴的位置
     *
     * @param row     行数
     * @param residue 余数
     * @return float
     */
    private float computeX(int row, int residue) {
        if (row == 0) {
            float width = getWidth() * 1f / 3;
            if (residue == 1) {
                return getWidth() * 1f / 2 - width / 2;
            } else if (residue == 2) {
                return width / 2;
            }
        }
        return 0;
    }

    /**
     * 返回画布的宽度
     *
     * @return int
     */
    private int getWidth() {
        return width;
    }

    /**
     * 返回画布的高度
     *
     * @return int
     */
    private int getHeight() {
        return height;
    }

    /**
     * @param combined Combined
     * @return boolean
     */
    private boolean hasURL(Combined combined) {
        return combined != null && !TextUtils.isEmpty(combined.getUrl());
    }

    /**
     * 获取每个宫格的文本
     *
     * @param combined Combined
     * @return String
     */
    private String getText(Combined combined) {
        String text = combined.getText();
        if (TextUtils.isEmpty(text)) {
            text = "unknown";
        }
        Log.d(TAG, "getText: " + text);
        boolean single = mCombinedList.size() == 1;
        return single ? text.substring(0, Math.min(2, text.length())) : text.substring(0, 1);
    }

    /**
     * 计算每个宫格文本的文字大小
     *
     * @param index int
     * @return float
     */
    private float computeTextSize(int index) {
        float baseTextSize = mCombinedAttrs.textSize;
        int size = mCombinedList.size();
        float textSize = baseTextSize;
        if (size == 2) {
            textSize = baseTextSize * 0.8f;
        } else if (size == 3) {
            if (index == 0) {
                textSize = baseTextSize * 0.8f;
            } else {
                textSize = baseTextSize * 0.6f;
            }
        } else if (size == 4) {
            textSize = baseTextSize * 0.6f;
        } else if (size >= 5) {
            textSize = baseTextSize * 0.5f;
        }
        return textSize;
    }

    /**
     * 头像绘制完成,回调刷新UI
     */
    private void onResultCallback() {
        Bitmap bitmap = Bitmap.createBitmap(mCanvasBitmap);
        if (mCallback != null) {
            mCallback.onCombinedCanvas(mCombinedList, bitmap);
        }
    }

    public interface OnCombinedCanvasCallback {
        /**
         * 头像回调
         *
         * @param list   List<Combined>
         * @param bitmap Bitmap
         */
        void onCombinedCanvas(List<Combined> list, Bitmap bitmap);
    }
}

7、Glide加载头像

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.bumptech.glide.Glide;
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import com.bumptech.glide.request.RequestOptions;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition;


public class CombinedLoader {
    public static final String TAG = "CombinedLoader";
    public static final int SUCCESS = 0;
    public static final int FAILED = -1;


    OnCombinedLoadCallback onCombinedLoadCallback;

    private CombinedLoader() {

    }

    public static CombinedLoader getInstance() {
        return new CombinedLoader();
    }

    /**
     * 使用glide加载头像
     *
     * @param context  Context
     * @param width    int width  加载头像的目标宽度
     * @param height   int height  加载头像的目标高度
     * @param index    index    加载头像的序列
     * @param url      String   头像链接
     * @param callback OnCombinedLoadCallback 回调接口
     */
    public void loadImage(Context context, int width, int height, int index, String url, OnCombinedLoadCallback callback) {
        this.onCombinedLoadCallback = callback;
        Glide.with(context)
                .asBitmap()
                .override(width, height)
                .apply(new RequestOptions().transform(new CenterCrop()))
                .load(url)
                .into(new CustomTargetImpl(index));
    }

    class CustomTargetImpl extends CustomTarget<Bitmap> {
        int index;

        public CustomTargetImpl(int index) {
            this.index = index;
        }

        @Override
        public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
            if (onCombinedLoadCallback != null) {
                onCombinedLoadCallback.onCombinedLoad(SUCCESS, index, resource);
            }
        }

        @Override
        public void onLoadCleared(@Nullable Drawable placeholder) {

        }

        @Override
        public void onLoadFailed(@Nullable Drawable errorDrawable) {
            super.onLoadFailed(errorDrawable);
            Log.w(TAG, "onLoadFailed: ");
            if (onCombinedLoadCallback != null) {
                onCombinedLoadCallback.onCombinedLoad(FAILED, index, null);
            }
        }
    }

    public interface OnCombinedLoadCallback {
        void onCombinedLoad(int retCode, int index, Bitmap newBitmap);
    }

8、代码使用样例

layout 布局文件

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/teal_700">

    <Button
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="刷新"
        android:layout_marginStart="8dp"
        android:textAllCaps="false"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.example.widget.combined.CombinedView
        android:id="@+id/combinedView"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_marginStart="20dp"
        android:layout_marginTop="20dp"
        app:combined_backgroundColor="@color/color_combined_background"
        app:combined_dividerColor="@color/color_combined_divider"
        app:combined_dividerWidth="1dp"
        app:combined_roundRadius="5dp"
        app:combined_shape="circle"
        app:combined_textSize="16sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/btn" />

</androidx.constraintlayout.widget.ConstraintLayout>

java 代码

import android.graphics.Color;
import android.os.Bundle;

import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;

import com.example.widget.combined.Combined;
import com.example.widget.combined.CombinedView;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;


public class SampleActivity extends AppCompatActivity {
    CombinedView combinedView;

    private final String[] avatars = new String[]{
            "https://img2.baidu.com/it/u=367273999,4201595611&fm=253&fmt=auto&app=138&f=JPEG?w=535&h=500",
            "https://img2.baidu.com/it/u=2957045803,1622480034&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",
            "https://img0.baidu.com/it/u=1691000662,1326044609&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",
            "https://img2.baidu.com/it/u=473659940,2616707866&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",
            "https://img0.baidu.com/it/u=345359896,661384602&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",
            "https://img0.baidu.com/it/u=2954567999,959431819&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",
            "https://img2.baidu.com/it/u=3377923072,1972706124&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",
            "https://img2.baidu.com/it/u=2826562948,3171725130&fm=253&fmt=auto&app=138&f=JPEG?w=400&h=400",
            "https://img1.baidu.com/it/u=2076707725,3468393586&fm=253&fmt=auto&app=138&f=JPEG?w=508&h=500",
    };

    private final String[] avatarTexts = new String[]{
            "凌云阁",
            "大唐醉",
            "听雨楼",
            "云梦泽",
            "中华楼",
            "水云涧",
            "落日谷",
            "千机阁",
            "万宝楼",
    };


    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sample);

        combinedView = findViewById(R.id.combinedView);
        combinedView.drawCombined(getData());
        findViewById(R.id.btn).setOnClickListener(v -> combinedView.drawCombined(getData()));
    }

    private List<Combined> getData() {
        Random random = new Random();
        int count = random.nextInt(9) + 1;
        List<Combined> list = new ArrayList<>();
        for (int i = 0; i < count; i++) {
            Combined combined = new Combined();
            if (isText()) {
                combined.setText(getAvatarText(i));
            } else {
                combined.setUrl(getUrl(i));
            }
            combined.setBackgroundColor(Color.GRAY);
            list.add(combined);
        }
        return list;
    }

    private String getAvatarText(int index) {
        return avatarTexts[index % avatarTexts.length];
    }

    private String getUrl(int index) {
        return avatars[index % avatars.length];
    }

    private boolean isText() {
        Random random = new Random();
        int value = random.nextInt(10);
        return value % 3 == 0;
    }

最后

附上必要的配置和权限:

app模块 build.gradle文件添加glide引用

implementation 'com.github.bumptech.glide:glide:4.15.1'

AndroidManifest.xml文件声明网络权限

<uses-permission android:name="android.permission.INTERNET" />

以上就是这个的代码实现逻辑和过程。代码链接Demo 工程下载地址,Demo运行环境是Java 17,有需要的小伙伴可能需要在AndroidStudio里面修改一下JDK的版本,或者修改一下工程里面gradle 的版本

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值