
HarmonyOS开发之利用Canvas实现群头像生成 原创
在鸿蒙应用开发中,头像处理是一个常见的需求,无论是个人头像的裁剪、美化,还是群头像的生成,都可以通过Canvas、Image组件等来实现。本文将介绍如何利用这些组件实现个人头像处理以及群头像生成。
头像处理
调用系统相机拍照,从系统相册选择图片,根据圆形遮罩,裁剪生成头像
-
创建图片选择器:使用picker.PhotoViewPicker创建图片选择器,设置选择模式为图片,数量最大为1,调用photoViewPicker.select方法获取用户选择的图片URI,并将其存储到模型中。
async openPicker() {
try {
const photoSelectOptions = new picker.PhotoSelectOptions();
photoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
photoSelectOptions.maxSelectNumber = 1;
const photoViewPicker = new picker.PhotoViewPicker();
let uris: picker.PhotoSelectResult = await photoViewPicker.select(photoSelectOptions);
if (!uris || uris.photoUris.length === 0) return;
let uri: string = uris.photoUris[0];
this.model.setImage(uri)
.setFrameWidth(1000)
.setFrameRatio(1);
} catch (e) {
console.error(‘openPicker’, JSON.stringify(e));
}
} -
绘制圆形取景框:使用Canvas的arc方法绘制圆形取景框,设置取景框的宽度和宽高比。
Canvas(this.context).width(‘100%’).height(‘100%’)
.backgroundColor(Color.Transparent)
.onReady(() => {
if (this.context == null) {
return
}
let height = this.context.height
let width = this.context.width
this.context.fillStyle= this.model.maskColor;
this.context.fillRect(0, 0, width, height)
let centerX = width / 2;
let centerY = height / 2;
let minDimension = Math.min(width, height);
let frameRadiusInVp = (minDimension - px2vp(this.model.frameWidth)) / 2;
this.context.globalCompositeOperation = ‘destination-out’
this.context.fillStyle = ‘white’
let frameWidthInVp = px2vp(this.model.frameWidth);
let frameHeightInVp = px2vp(this.model.getFrameHeight());
let x = (width - px2vp(this.model.frameWidth)) / 2;
let y = (height - px2vp(this.model.getFrameHeight())) / 2;
this.context.beginPath();
this.context.arc(centerX, centerY, px2vp(this.model.frameWidth/2), 0, 2 * Math.PI);
this.context.fill();
this.context.globalCompositeOperation = ‘source-over’;
this.context.strokeStyle = this.model.strokeColor;
let radius = Math.min(frameWidthInVp, frameHeightInVp) / 2;
this.context.beginPath();
this.context.arc(centerX, centerY, radius, 0, 2 * Math.PI);
this.context.closePath();
this.context.lineWidth = 1;
this.context.stroke();
}) -
添加拖动手势:通过PanGesture和PinchGesture添加拖动和缩放手势,允许用户调整图片位置和大小。
.gesture(
GestureGroup(GestureMode.Parallel,
PanGesture({})
.onActionStart(() => {
this.startOffsetX = this.model.offsetX;
this.startOffsetY = this.model.offsetY;
})
.onActionUpdate((event:GestureEvent) => {
if (event) {
if (this.model.panEnabled) {
let distanceX: number = this.startOffsetX + vp2px(event.offsetX) / this.model.scale;
let distanceY: number = this.startOffsetY + vp2px(event.offsetY) / this.model.scale;
this.model.offsetX = distanceX;
this.model.offsetY = distanceY;
this.updateMatrix()
}
}
})
.onActionEnd(() => {
this.checkImageAdapt();
}),PinchGesture({ fingers: 2 })
.onActionStart(() => {
this.tempScale = this.model.scale
})
.onActionUpdate((event) => {
if (event) {
if (!this.model.zoomEnabled) return;
this.zoomTo(this.tempScale * event.scale);
}
})
.onActionEnd(() => {
this.checkImageAdapt();
})
)
) -
校准图片:在拖拽或缩放动作结束后,检查图片是否填满取景框,若未填满则自动调整图片大小和位置,使其与取景框对齐。
private checkImageAdapt() {
let offsetX = this.model.offsetX;
let offsetY = this.model.offsetY;
let scale = this.model.scale;
let widthScale = this.model.componentWidth / this.model.imageWidth;
let heightScale = this.model.componentHeight / this.model.imageHeight;
let adaptScale = Math.min(widthScale, heightScale);
let showWidth = this.model.imageWidth * adaptScale * this.model.scale;
let showHeight = this.model.imageHeight * adaptScale * this.model.scale;
let imageX = (this.model.componentWidth - showWidth) / 2;
let imageY = (this.model.componentHeight - showHeight) / 2;
let frameX = (this.model.componentWidth - this.model.frameWidth) / 2;
let frameY = (this.model.componentHeight - this.model.getFrameHeight()) / 2;
let showX = imageX + offsetX * scale;
let showY = imageY + offsetY * scale;
if(this.model.frameWidth > showWidth || this.model.getFrameHeight() > showHeight) {
let xScale = this.model.frameWidth / showWidth;
let yScale = this.model.getFrameHeight() / showHeight;
let newScale = Math.max(xScale, yScale);
this.model.scale = this.model.scale * newScale;
showX *= newScale;
showY *= newScale;
}
if(showX > frameX) {
showX = frameX;
} else if(showX + showWidth < frameX + this.model.frameWidth) {
showX = frameX + this.model.frameWidth - showWidth;
}
if(showY > frameY) {
showY = frameY;
} else if(showY + showHeight < frameY + this.model.getFrameHeight()) {
showY = frameY + this.model.getFrameHeight() - showHeight;
}
this.model.offsetX = (showX - imageX) / scale;
this.model.offsetY = (showY - imageY) / scale;
this.updateMatrix();
}
5. 生成新PixelMap数据:根据取景框的位置和大小,使用image.Region获取图片对应的PixelMap数据,生成新的头像图片。
public async crop(format: image.PixelMapFormat) : Promise<image.PixelMap> {
if(!this.src || this.src == ‘’) {
throw new Error(‘Please set src first’);
}
if(this.imageWidth == 0 || this.imageHeight == 0) {
throw new Error(‘The image is not loaded’);
}
let widthScale = this.componentWidth / this.imageWidth;
let heightScale = this.componentHeight / this.imageHeight;
let adaptScale = Math.min(widthScale, heightScale);
let totalScale = adaptScale * this.scale;
let showWidth = this.imageWidth * totalScale;
let showHeight = this.imageHeight * totalScale;
let imageX = (this.componentWidth - showWidth) / 2;
let imageY = (this.componentHeight - showHeight) / 2;
let frameX = (this.componentWidth - this.frameWidth) / 2;
let frameY = (this.componentHeight - this.getFrameHeight()) / 2;
let showX = imageX + this.offsetX * this.scale;
let showY = imageY + this.offsetY * this.scale;
let x = (frameX - showX) / totalScale;
let y = (frameY - showY) / totalScale;
let file = fs.openSync(this.src, fs.OpenMode.READ_ONLY)
let imageSource : image.ImageSource = image.createImageSource(file.fd);
let decodingOptions : image.DecodingOptions = {
editable: true,
desiredPixelFormat: image.PixelMapFormat.BGRA_8888,
}
let pm = await imageSource.createPixelMap(decodingOptions);
let cp = await this.copyPixelMap(pm);
pm.release();
let region: image.Region = { x: x, y: y, size: { width: this.frameWidth / totalScale, height: this.getFrameHeight() / totalScale } };
cp.cropSync(region);
return cp;
}
async copyPixelMap(pm: PixelMap): Promise<PixelMap> {
const imageInfo: image.ImageInfo = await pm.getImageInfo();
const buffer: ArrayBuffer = new ArrayBuffer(pm.getPixelBytesNumber());
await pm.readPixelsToBuffer(buffer);
const opts: image.InitializationOptions = {
editable: true,
pixelFormat: image.PixelMapFormat.RGBA_8888,
size: { height: imageInfo.size.height, width: imageInfo.size.width }
};
return image.createPixelMap(buffer, opts);
}
头像添加身份标识、Lottie动画以及直播头像
-
添加身份标识:通过在头像图片上叠加标签图片来实现身份标识的添加,使用Image组件设置头像和标签的位置、大小等属性。
Row() {
Image(r(‘app.media.app_icon’))
.height(16).width(16)
.borderRadius(8)
.position({ x: 75, y: 75 })
}
.width(100)
.height(100)2. 添加Lottie动画:使用Lottie库加载JSON格式的动画文件,在头像上叠加Lottie动画,实现直播头像的动态效果。
1.下载安裝:ohpm install @ohos/lottie。
2.相对路径加载,项目内entry/src/main/ets文件夹下创建和pages同级的目录common,将需要播放的json文件放在目录common下。path路径加载只支持文件夹下的相对路径,不能使用./或者…/的相对路径,会导致动画加载不出来,例如:path: ‘common/lottie/test.json’。
Button(‘播放’)
.onClick(() => {
if (this.animateItem === null) {
this.animateItem = lottie.loadAnimation({
container: this.mainCanvasRenderingContext,
renderer: ‘canvas’,
loop: 10,
autoplay: true,
name: this.animateName,
contentMode: ‘Contain’,
path: ‘common/lottie/grunt.json’,
})
}
})3. 实现水波纹动画和缩放动画:通过显式动画和关键帧动画实现头像的水波纹和缩放动画效果,增强头像的视觉表现。
build() {
Column({ space: 5 }) {
Stack() {
ForEach(this.scaleList, (item: number, index: number) => {
Column() {
}
.width(100)
.height(100)
.borderRadius(50)
.backgroundColor(Color.Red)
.opacity(this.opacityList[index])
.scale({ x: this.scaleList[index], y: this.scaleList[index] })
.onAppear(() => {
animateTo({ duration: 1000, iterations: -1 }, () => {
this.scaleList[index] = this.cloneScaleList[index] + this.scaleRatio
this.opacityList[index] = this.cloneOpacityList[index] - 0.1
})
})
}, (item: number, index: number) => index.toString())Row(){
Image($r(‘app.media.app_icon’)).width(100).borderRadius(50)
.scale({ x: this.scaleN, y: this.scaleN})
.onAppear(()=>{
this.scaleN = 1.0;
this?.uiContext?.keyframeAnimateTo({ iterations: -1 }, [
{
duration: 1000,
event: () => {
this.scaleN = 0.9;
}
},
{
duration: 1000,
event: () => {
this.scaleN = 1;
}
}
])
})
}
.height(100).width(100).borderRadius(50)
.backgroundColor(Color.Blue)
}
}.height(‘100%’).width(‘100%’).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
}群头像生成
使用canvas实现群头像功能 -
获取单个头像图片:将每个群成员的头像图片转换为ImageBitmap对象。
private img: ImageBitmap = new ImageBitmap(“common/images/imageHeadtwo.png”);2. 绘制头像:使用Canvas的drawImage方法将头像绘制到画布上,根据群成员数量计算每张头像在画布上的位置,确保布局合理。
build() {
Canvas(this.context)
.width(‘200’)
.height(‘200’)
.backgroundColor(‘#F5DC62’)
.onReady(() => {
if (this.imgCount == 3) {
let imageW = (this.canvasW - this.itemGap) / 2;
this.context.drawImage(this.img, (this.canvasW - imageW) / 2, 0, imageW, imageW);
this.context.drawImage(this.img, 0, imageW + this.itemGap, imageW, imageW);
this.context.drawImage(this.img, imageW + this.itemGap, imageW + this.itemGap, imageW, imageW);
} else if (this.imgCount == 4) {
let imageW = (this.canvasW - this.itemGap) / 2;
this.context.drawImage(this.img, 0, 0, imageW, imageW);
this.context.drawImage(this.img, imageW + this.itemGap, 0, imageW, imageW);
this.context.drawImage(this.img, 0, imageW + this.itemGap, imageW, imageW);
this.context.drawImage(this.img, imageW + this.itemGap, imageW + this.itemGap, imageW, imageW);
} else if (this.imgCount == 5) {
let imageW = (this.canvasW - this.itemGap * 2) / 3;
let startX = (this.canvasW - imageW * 2 - this.itemGap) / 2;
this.context.drawImage(this.img, startX, startX, imageW, imageW);
this.context.drawImage(this.img, startX + imageW + this.itemGap, startX, imageW, imageW);
this.context.drawImage(this.img, 0, startX + imageW + this.itemGap, imageW, imageW);
this.context.drawImage(this.img, imageW + this.itemGap, startX + imageW + this.itemGap, imageW, imageW);
this.context.drawImage(this.img, 2 * (imageW + this.itemGap), startX + imageW + this.itemGap, imageW, imageW);
} else if (this.imgCount == 6) {
let imageW = (this.canvasW - this.itemGap * 2) / 3;
let startX = (this.canvasW - imageW * 2 - this.itemGap) / 2;
this.context.drawImage(this.img, 0, startX, imageW, imageW);
this.context.drawImage(this.img, imageW + this.itemGap, startX, imageW, imageW);
this.context.drawImage(this.img, 2 * (imageW + this.itemGap), startX, imageW, imageW);
this.context.drawImage(this.img, 0, startX + imageW + this.itemGap, imageW, imageW);
this.context.drawImage(this.img, imageW + this.itemGap, startX + imageW + this.itemGap, imageW, imageW);
this.context.drawImage(this.img, 2 * (imageW + this.itemGap), startX + imageW + this.itemGap, imageW, imageW);
} else if (this.imgCount == 7) {
let imageW = (this.canvasW - this.itemGap * 2) / 3;
let startX = (this.canvasW - imageW) / 2;
this.context.drawImage(this.img, startX, 0, imageW, imageW);
this.context.drawImage(this.img, 0, imageW + this.itemGap, imageW, imageW);
this.context.drawImage(this.img, imageW + this.itemGap, imageW + this.itemGap, imageW, imageW);
this.context.drawImage(this.img, 2 * (imageW + this.itemGap), imageW + this.itemGap, imageW, imageW);
this.context.drawImage(this.img, 0, 2 * (imageW + this.itemGap), imageW, imageW);
this.context.drawImage(this.img, imageW + this.itemGap, 2 * (imageW + this.itemGap), imageW, imageW);
this.context.drawImage(this.img, 2 * (imageW + this.itemGap), 2 * (imageW + this.itemGap), imageW, imageW);
} else if (this.imgCount == 8) {
let imageW = (this.canvasW - this.itemGap * 2) / 3;
let startX = (this.canvasW - imageW * 2 - this.itemGap) / 2;
this.context.drawImage(this.img, startX, 0, imageW, imageW);
this.context.drawImage(this.img, startX + imageW + this.itemGap, 0, imageW, imageW);
this.context.drawImage(this.img, 0, imageW + this.itemGap, imageW, imageW);
this.context.drawImage(this.img, imageW + this.itemGap, imageW + this.itemGap, imageW, imageW);
this.context.drawImage(this.img, 2 * (imageW + this.itemGap), imageW + this.itemGap, imageW, imageW);
this.context.drawImage(this.img, 0, 2 * (imageW + this.itemGap), imageW, imageW);
this.context.drawImage(this.img, imageW + this.itemGap, 2 * (imageW + this.itemGap), imageW, imageW);
this.context.drawImage(this.img, 2 * (imageW + this.itemGap), 2 * (imageW + this.itemGap), imageW, imageW);
} else if (this.imgCount == 9) {
let imageW = (this.canvasW - this.itemGap * 2) / 3;
this.context.drawImage(this.img, 0, 0, imageW, imageW);
this.context.drawImage(this.img, imageW + this.itemGap, 0, imageW, imageW);
this.context.drawImage(this.img, 2 * (imageW + this.itemGap), 0, imageW, imageW);
this.context.drawImage(this.img, 0, imageW + this.itemGap, imageW, imageW);
this.context.drawImage(this.img, imageW + this.itemGap, imageW + this.itemGap, imageW, imageW);
this.context.drawImage(this.img, 2 * (imageW + this.itemGap), imageW + this.itemGap, imageW, imageW);
this.context.drawImage(this.img, 0, 2 * (imageW + this.itemGap), imageW, imageW);
this.context.drawImage(this.img, imageW + this.itemGap, 2 * (imageW + this.itemGap), imageW, imageW);
this.context.drawImage(this.img, 2 * (imageW + this.itemGap), 2 * (imageW + this.itemGap), imageW, imageW);
}let pixelmap = this.context.getPixelMap(0, 0, this.canvasW, this.canvasW);
this.headImg = pixelmap;
this.context.setPixelMap(pixelmap);
});
}3. 获取PixelMap数据:使用getPixelMap方法获取画布上指定区域的PixelMap数据,生成群头像。
通过以上方法,可以实现个人头像的处理和群头像的生成,为鸿蒙应用中的用户头像功能提供丰富的实现方式。
