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

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

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

最少的情况:

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

四个时比较特殊:

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

5 至 6 个时:

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

7 至 9 个时:

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

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

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

最终效果:

代码在这里贴一下:

<template>
    <canvas
        :style="{height: canvasSize + 'px', width: canvasSize + 'px'}"
        :canvas-id="canvasId"
        :id="canvasId"
    >
    </canvas>
</template>
<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 组件地址