HarmonyOS开发之利用Canvas实现群头像生成 原创

Datcon
发布于 2025-3-24 17:08
浏览
0收藏

在鸿蒙应用开发中,头像处理是一个常见的需求,无论是个人头像的裁剪、美化,还是群头像的生成,都可以通过Canvas、Image组件等来实现。本文将介绍如何利用这些组件实现个人头像处理以及群头像生成。
头像处理
调用系统相机拍照,从系统相册选择图片,根据圆形遮罩,裁剪生成头像

  1. 创建图片选择器:使用​​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));
    }
    }

  2. 绘制圆形取景框:使用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();
    })

  3. 添加拖动手势:通过​​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();
    })
    )
    )

  4. 校准图片:在拖拽或缩放动作结束后,检查图片是否填满取景框,若未填满则自动调整图片大小和位置,使其与取景框对齐。
    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动画以及直播头像

  1. 添加身份标识:通过在头像图片上叠加标签图片来实现身份标识的添加,使用​​Image​​组件设置头像和标签的位置、大小等属性。
    Row() {
    Image(r(app.media.imageHead)).width(100).height(100).borderRadius(50).borderWidth(2).borderColor(Color.Orange)Image(r('app.media.imageHead')) .width(100).height(100) .borderRadius(50) .borderWidth(2) .borderColor(Color.Orange) 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’。
    HarmonyOS开发之利用Canvas实现群头像生成-鸿蒙开发者社区
    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实现群头像功能

  2. 获取单个头像图片:将每个群成员的头像图片转换为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数据,生成群头像。
    通过以上方法,可以实现个人头像的处理和群头像的生成,为鸿蒙应用中的用户头像功能提供丰富的实现方式。

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
分类
标签
收藏
回复
举报


回复
    相关推荐