场景一:调用系统相机拍照,从系统相册选择图片,根据圆形遮罩,裁剪生成个人头像。
场景一实现方案:
1.创建图片选择器photoViewPicker,设置选择模式为图片,数量最大为1,调用photoViewPicker,将选择的图片以uri形式,存储到cropModel里面,并设置取景框宽度和宽高比
2.以画布为中心,使用canvas中API的this.context.arc绘制圆形取景框
3.添加拖动手势.gesture(gesturegroup),panGesture(onActionStart,onActionUpdate,onActionEnd)
4.对取景框对图片校准,若拖动缩放图片过程中,拖拽动作停止时,缩放图片太小;未占满取景框时,则将图片缩放到和取景框一 样宽度,调整图片和取景框在一个中心点上
5.根据cropModel存储的uri路径,获取图片源,根据生成的XY坐标和size,使用api image.Region获取图片对应的PixelMap
各步骤代码实现:
创建photoViewPicker,选择图片,并以uri形式,存储到cropModel。
// 弹出图片选择器方法
async openPicker() {
try {
// 设置图片选择器选项
const photoSelectOptions = new picker.PhotoSelectOptions();
// 限制只能选择一张图片
photoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
photoSelectOptions.maxSelectNumber = 1;
// 创建并实例化图片选择器
const photoViewPicker = new picker.PhotoViewPicker();
// 选择图片并获取图片URI
let uris: picker.PhotoSelectResult = await photoViewPicker.select(photoSelectOptions);
if (!uris || uris.photoUris.length === 0) return;
// 获取选中图片的第一张URI
let uri: string = uris.photoUris[0];
this.model.setImage(uri)
.setFrameWidth(1000)
.setFrameRatio(1);
} catch (e) {
console.error('openPicker', JSON.stringify(e));
}
}
handleFileSelection(event: MyEvent) {
const PhotoSelectOptions = new picker.PhotoSelectOptions();
PhotoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
PhotoSelectOptions.maxSelectNumber = 1;
const photoPicker = new picker.PhotoViewPicker();
photoPicker.select(PhotoSelectOptions)
.then((PhotoSelectResult) => {
if (PhotoSelectResult.photoUris.length === 0) {
console.warn('No image selected.');
return;
}
const srcUri = PhotoSelectResult.photoUris[0];
this.model.setImage(srcUri)
.setFrameWidth(1000)
.setFrameRatio(1);
})
.catch((selectError: object) => {
console.error('Failed to invoke photo picker:', JSON.stringify(selectError));
});
return true;
}
}
使用画布进行圆形取景框的绘制。
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();
// 使用 arc 方法绘制圆形
this.context.arc(centerX, centerY, radius, 0, 2 * Math.PI);
// 关闭路径
this.context.closePath();
// 描绘圆形边框
this.context.lineWidth = 1; // 边框宽度
this.context.stroke();
})
添加拖拽手势,调整获取图片的内容。
.gesture(
GestureGroup(GestureMode.Parallel,
// 拖动手势
PanGesture({})
.onActionStart(() => {
hilog.info(0, "CropView", "Pan gesture start");
this.startOffsetX = this.model.offsetX;
this.startOffsetY = this.model.offsetY;
})
.onActionUpdate((event:GestureEvent) => {
hilog.info(0, "CropView", `Pan gesture update: ${JSON.stringify(event)}`);
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(() => {
hilog.info(0, "CropView", "Pan gesture end");
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;
hilog.info(0, "CropView", `offsetX: ${offsetX}, offsetY: ${offsetY}, scale: ${scale}`)
// 图片适配控件的时候也进行了缩放,计算出这个缩放比例
let widthScale = this.model.componentWidth / this.model.imageWidth;
let heightScale = this.model.componentHeight / this.model.imageHeight;
let adaptScale = Math.min(widthScale, heightScale);
hilog.info(0, "CropView", `Image scale ${adaptScale} while attaching the component[${this.model.componentWidth}, ${this.model.componentHeight}]`)
// 经过两次缩放(适配控件、手势)后,图片的实际显示大小
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;
hilog.info(0, "CropView", `Image left top is (${imageX}, ${imageY})`)
// 取景框的左上角坐标
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;
hilog.info(0, "CropView", `Image show at (${showX}, ${showY})`)
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;
}
// 调整x轴方向位置,使图像填满取景框
if(showX > frameX) {
showX = frameX;
} else if(showX + showWidth < frameX + this.model.frameWidth) {
showX = frameX + this.model.frameWidth - showWidth;
}
// 调整y轴方向位置,使图像填满取景框
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();
}
根据取景框的内缩放图片的位置大小,生成新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,
}
// 创建pixelMap
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());
// TODO 知识点:通过readPixelsToBuffer实现PixelMap的深拷贝,其中readPixelsToBuffer输出为BGRA_8888
await pm.readPixelsToBuffer(buffer);
// TODO 知识点:readPixelsToBuffer输出为BGRA_8888,此处createPixelMap需转为RGBA_8888
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);
}
}
场景二:对头像添加VIP标识或其他标签,添加lottie动画,以及直播头像
1.头像添加身份标识。
实现:主要以头像图片和标签图片叠加实现此效果
Row() {
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动画文件,来实现此效果。
1.lottie是一个适用于HarmonyOS的动画库,它可以解析json格式的动画,并在移动设备上进行本地渲染。
2.下载安裝:ohpm install @ohos/lottie。
3.相对路径加载,项目内entry/src/main/ets文件夹下创建和pages同级的目录common,将需要播放的json文件放在目录common下。
4.path路径加载只支持文件夹下的相对路径,不能使用./或者../的相对路径,会导致动画加载不出来,正确格式:path: 'common/lottie/grunt.json'。
Button('播放')
.onClick(() => {
if (this.animateItem === null) {
this.animateItem = lottie.loadAnimation({
container: this.mainCanvasRenderingContext,
renderer: 'canvas', // canvas 渲染模式
loop: 10,
autoplay: true,
name: this.animateName,
contentMode: 'Contain',
path: 'common/lottie/grunt.json', //路径加载动画只支持entry/src/main/ets 文件夹下的相对路径
})
}
}
3.头像水波纹动画实现:
1.实现水波纹动画,设置2个长度为3数组,sacleList主要控制缩放,每次加0.1的倍数,opacityList控制透明度渐变效果,每次递减0.1倍。
2.使用显式动画 (animateTo)在onAppear中,执行2组数组中设置的缩放和透明度的动画系数,来达到水波纹的缩放和渐变效果。
.scale({ x: this.scaleList[index], y: this.scaleList[index] })
.onAppear(() => {
animateTo({ duration: 1000, iterations: -1 }, () => {
//iterations: -1
//每个圆缩放系数+0.1
this.scaleList[index] = this.cloneScaleList[index] + this.scaleRatio
//每个圆透明度-0.1
this.opacityList[index] = this.cloneOpacityList[index] - 0.1
})
})
头像缩放动画实现
1.头像缩放动画,使用关键帧动画 (keyframeAnimateTo)来实现。
2.第一帧将头像从1.0缩小到0.9,第二帧将头像从0.9放大到1.0,动画播放次数iterations设置为-1,无限次播放。
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 }, [
{
// 第一段关键帧动画时长为1000ms,scale属性做从1到0.9的动画
duration: 1000,
event: () => {
this.scaleN = 0.9;
}
},
{
// 第二段关键帧动画时长为1000ms,scale属性做从0.9到1的动画
duration: 1000,
event: () => {
this.scaleN = 1;
}
}
])
})
}
整体效果代码如下:
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 }, () => {
// iterations: -1
// 每个圆缩放系数+0.1
this.scaleList[index] = this.cloneScaleList[index] + this.scaleRatio
// 每个圆透明度-0.1
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 }, [
{
// 第一段关键帧动画时长为1000ms,scale属性做从1到0.9的动画
duration: 1000,
event: () => {
this.scaleN = 0.9;
}
},
{
// 第二段关键帧动画时长为1000ms,scale属性做从0.9到1的动画
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实现群头像功能
1.使获取单个头像图片,转换成ImageBitmap。
private img:ImageBitmap = new ImageBitmap("common/images/imageHeadtwo.png")
2.通过CanvasRenderingContext2D对象和OffscreenCanvasRenderingContext2D对象直接调用drawImage进行绘制
其中的四个参数,表示以画布左上角坐标起点,前2个表示x,y坐标位置,后2个参数表示在画布上绘制的宽高。
3.头像个数3-9张,并且根据图片的数量,计算每种情况下图片所在画布位置,自动调整布局,确保每个图片都是正方形,且有统一的边框和间距。
4.根据API:getPixelMap,根据画布起始点位置,和自己想要获取画布内容大小,来获取对应的pixelMap数据。
5.拿到pixelMap数据后,可以用image图片来展示。
群成员数量不同,画布上对应的群成员头像所在的坐标不同,这里以3-9张为例代码如下:
属性介绍
//群成员数量
private imgCount:number = 9;
//画布宽度/长度
private canvasW:number = 200;
//头像间距
private itemGap:number = 10;
多情况头像布局
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)
}
实现:以9张头像为例,代码实现如下
Canvas(this.context)
.width('200')
.height('200')
.backgroundColor('#F5DC62')
.onReady(() => {
this.context.drawImage(this.img, 0, 0, 60, 60)
this.context.drawImage(this.img, 70, 0, 60, 60)
this.context.drawImage(this.img, 140, 0, 60, 60)
this.context.drawImage(this.img, 0, 70, 60, 60)
this.context.drawImage(this.img, 70, 70, 60, 60)
this.context.drawImage(this.img, 140, 70, 60, 60)
this.context.drawImage(this.img, 0, 140, 60, 60)
this.context.drawImage(this.img, 70, 140, 60, 60)
this.context.drawImage(this.img, 140, 140, 60, 60)
let pixelmap = this.context.getPixelMap(0, 0, 200, 200)
this.headImg = pixelmap;
this.context.setPixelMap(pixelmap)
})
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。