最近做即时聊天功能,一个需求是自定义群头像效果仿钉钉和微信的群头像,记录一下实现的过程逻辑,以下便是实现的流程和代码
前言
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、自定义属性类
群组头像的一些自定义属性:
- 背景颜色 backgroundColor
- 宫格文本文字大小 textSize
- 宫格文本文字颜色 textColor
- 头像圆角半径 roundRadius
- 头像形状 shape
- 宫格分割线宽度 dividerWidth
- 宫格分割线颜色 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、群组头像的绘制
- 创建两张canvas画布,用户绘制主图层和裁剪覆盖图层
- 计算每个宫格头像的位置和大小
- 如果有自定义头像链接,使用glide加载头像,头像请求到刷新绘制
- 如果是文本头像,计算文本文字的大小
- 绘制宫格的分割线
- 绘制裁剪覆盖图层,裁剪圆角或圆形
具体实现代码如下所示:
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 的版本