前阵子公司要我们用 uniapp 开发应用,经过不断折腾也算是上线了,最近有要求我们给 app 接入腾讯的 IM,IM 有个群聊功能,本来我以为群聊的头像是 IM 为我们生成好的,结果发现要我们自己处理。我发现微信群的头像在群创建后就固定了,不会随着群成员更换头像而改变,我猜测应该是创建的时候用 canvas 或其他方式生成了一张静态图片然后被存了起来。

一开始我在网上找了很多插件,总之不太行,还有很多是后端处理的方案,但我这要前端处理也没有办法,最终试了一圈,还是自己画吧。

先分析一下头像可能出现的情况:

最少的情况:

成群最少三个,三个时三个头像在图中间平铺。

四个时比较特殊:

头像呈现一个“田”字,而不是大多数的三等分。

5 至 6 个时:

一排三个 ,另一排两个或三个,少的那排头像永远居中,上下两排在垂直方式也是居中排列。

7 至 9 个时:

两排三个,剩下一排 1 至 3 个,此时行数已经撑满整个头像区域。

到这里基本已经完成了大半,剩下的就是绘制了。

绘制完成后我将 canvas 转成了 base64,并通过 @loaded 传递了出来,用户可以后续自行处理。

最终效果:

代码在这里贴一下:

1
2
3
4
5
6
7
8
<template>
<canvas
:style="{height: canvasSize + 'px', width: canvasSize + 'px'}"
:canvas-id="canvasId"
:id="canvasId"
>
</canvas>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
<script>		
export default {
props: {
// 不满三个的行显示在上面还是在下面,默认在上面(同微信)
avatarReverse: {
type: Boolean,
default: true
},
// 头像数组
images: {
type: Array,
default: () => ([])
},
// 头像背景色
backgroundColor: {
type: String,
default: '#ffffff'
},
// 边框宽度
borderWidth: {
type: Number,
default: 2
},
// 每个头像的尺寸
avatarSize: {
type: Number,
default: 30
}
},
data() {
// 如果同时渲染多个,会出现 id 重复问题,所以 加上时间戳跟随机数
const now = (+new Date() + Math.random().toFixed(4) * 10000)
return {
canvasId: `__myCanvas${now}`,
avatarArray: [],
colNumber: 3
}
},
computed: {
canvasSize() {
return this.avatarSize * 3 + this.borderWidth * 4
},
avatarSize2() {
return (this.canvasSize - this.borderWidth * 3) / 2
},
pointMap() {
// 这里的 1,2,3 为每行 / 每列的数组长度
return {
1: (this.canvasSize - this.avatarSize) / 2,
2: (this.canvasSize - (this.avatarSize * 2 + this.borderWidth)) / 2,
3: this.borderWidth
}
}
},
created() {
this.init()
},
mounted() {
this.drawAvatar()
},
methods: {
drawAvatar() {
const ctx = uni.createCanvasContext(this.canvasId, this)
ctx.setFillStyle(this.backgroundColor)
ctx.fillRect(0, 0, this.canvasSize, this.canvasSize)

for (let i = 0; i < this.avatarArray.length; i++) {
const item = this.avatarArray[i]
// 按九宫格分的情况
if (this.colNumber === 3) {
const colStart = this.pointMap[this.avatarArray.length]
const rowStart = this.pointMap[item.length]
for (let v = 0; v < item.length; v++) {
ctx.drawImage(
item[v],
rowStart + (this.avatarSize + this.borderWidth) * v, colStart + (this.avatarSize + this
.borderWidth) * i, this.avatarSize, this.avatarSize)
}
}
// 按4宫格分的情况
if (this.colNumber === 2) {
for (let v = 0; v < item.length; v++) {
ctx.drawImage(
item[v],
this.borderWidth + (this.borderWidth + this.avatarSize2) * i,
this.borderWidth + (this.borderWidth + this.avatarSize2) * v,
this.avatarSize2,
this.avatarSize2)
}
}
}
ctx.draw(true, ret => {
// #ifdef APP-PLUS
uni.canvasToTempFilePath({
x: 0, // 起点坐标
y: 0,
width: this.canvasSize, // canvas 宽
height: this.canvasSize, // canvas 高
canvasId: this.canvasId, // canvas id
success: res => {
const savedFilePath = res.tempFilePath // 相对路径
const path = plus.io.convertLocalFileSystemURL(savedFilePath) // 绝对路径
const fileReader = new plus.io.FileReader()
fileReader.readAsDataURL(path)
fileReader.onloadend = (res) => { // 读取文件成功完成的回调函数
this.$emit('loaded', res.target.result)
}
}
})
// #endif
})
},
init() {
const images = this.images.slice(0, 9)
this.avatarArray = this.splitArray(this.avatarReverse ? images.reverse() : images)
},
splitArray(data) {
if (data.length === 4) {
this.colNumber = 2
}
const result = []
for (let i = 0, len = data.length; i < len; i += this.colNumber) {
result.push(data.slice(i, i + this.colNumber))
}
return this.avatarReverse ? result.reverse() : result
}
}
}
</script>

GitHub 组件地址