本文原创发布在华为开发者社区。
介绍
本实例主要展示了图片应用场景相关demo。主要包括了图片预览、图片编辑美化、场景变化前后对比、图片切割九宫格、两张图片拼接、AI抠图、图片加水印等场景示例。
图片预览
使用说明
点击图片,进入图片预览界面。可对图片进行缩放、拖拽等操作。
效果预览
实现思路
1、给图片组件的scale、width、height、offset等属性绑定相关响应式变量
Image(item)
.width(`calc(100% * ${this.activeImage.scale})`)
.height(`calc(100% * ${this.activeImage.scale})`)
.objectFit(ImageFit.Contain)
.draggable(false)
.scale(this.active === i ? { x: this.activeImage.scale, y: this.activeImage.scale } : null)
.offset(this.active === i ? { x: this.activeImage.offsetX, y: this.activeImage.offsetY } : null)
.onComplete(e => {
if (e?.loadingStatus) {
// 记录每张图片的大小
this.imageListSize[i] = {
width: px2vp(Number(e.contentWidth)),
height: px2vp(Number(e.contentHeight))
}
// 记录当前选中图片的大小,方便后面手势操作计算
if (this.active === i) {
this.activeImage.width = this.imageListSize[i].width
this.activeImage.height = this.imageListSize[i].height
}
}
})
2、监听相关手势操作并执行相关操作逻辑,如:二指缩放、单指滑动、双击等
// 双指操作
PinchGesture({ fingers: 2 })
.onActionStart((e) => {
this.defaultScale = this.activeImage.scale
})
.onActionUpdate((e) => {
let scale = e.scale * this.defaultScale
// 计算缩放比例及相应偏移距离
if (scale <= 4 && scale >= 1) {
this.activeImage.offsetX = this.activeImage.offsetX / (this.activeImage.scale - 1) * (scale - 1) || 0
this.activeImage.offsetY = this.activeImage.offsetY / (this.activeImage.scale - 1) * (scale - 1) || 0
this.activeImage.offsetStartX = this.activeImage.offsetX
this.activeImage.offsetStartY = this.activeImage.offsetY
this.activeImage.scale = scale
}
})
// 单指滑动
PanGesture()
.onActionStart(e => {
// 记录起始位置
this.activeImage.dragOffsetX = e.fingerList[0].globalX
this.activeImage.dragOffsetY = e.fingerList[0].globalY
})
.onActionUpdate((e) => {
if (this.activeImage.scale === 1) {
return
}
if(!e.fingerList[0]){
return
}
// 计算移动距离
let offsetX = e.fingerList[0].globalX - this.activeImage.dragOffsetX +
this.activeImage.offsetStartX
let offsetY = e.fingerList[0].globalY - this.activeImage.dragOffsetY +
this.activeImage.offsetStartY
if (this.activeImage.width * this.activeImage.scale > this.containerWidth &&
(this.activeImage.width * this.activeImage.scale - this.containerWidth) / 2 >=
Math.abs(offsetX)) {
this.activeImage.offsetX = offsetX
}
if (this.activeImage.height * this.activeImage.scale >
this.containerHeight &&
(this.activeImage.height * this.activeImage.scale - this.containerHeight) / 2 >=
Math.abs(offsetY)) {
this.activeImage.offsetY = offsetY
}
if ((this.activeImage.width * this.activeImage.scale - this.containerWidth) / 2 < Math.abs(offsetX)) {
this.disabledSwipe = false
}
})
.onActionEnd((e) => {
// 记录当前偏移位置,作为下次操作的其实位置
this.activeImage.offsetStartX = this.activeImage.offsetX
this.activeImage.offsetStartY = this.activeImage.offsetY
})
.onActionCancel(() => {
// 记录当前偏移位置,作为下次操作的其实位置
this.activeImage.offsetStartX = this.activeImage.offsetX
this.activeImage.offsetStartY = this.activeImage.offsetY
}),
//双击手势
TapGesture({ count: 2 })
.onAction(() => {
// 缩小
if (this.activeImage.scale > 1) {
this.activeImage.scale = 1
this.activeImage.offsetX = 0
this.activeImage.offsetY = 0
this.activeImage.offsetStartX = 0
this.activeImage.offsetStartY = 0
this.disabledSwipe = false
} else {
// 放大
this.activeImage.scale = 2
this.disabledSwipe = true
}
}),
//单击手势
TapGesture({ count: 1 })
.onAction(() => {
this.closePreviewFn()
}),
)
)
图片编辑美化
使用说明
可对图片编辑,包含裁剪、旋转、色域调节(本章只介绍亮度、透明度、饱和度)、滤镜等功能。
效果预览
实现思路
1、通过pixelMap的crop方法来裁剪图片
export async function cropImage(pixelMap: image.PixelMap, x = 0, y = 0, width = 300, height = 300) {
// x:裁剪起始点横坐标
// y:裁剪起始点纵坐标
// height:裁剪高度,方向为从上往下
// width:裁剪宽度,方向为从左到右
await pixelMap.crop({ x, y, size: { height, width } })
}
2、通过pixelMap的rotate方法来旋转图片
export async function rotateImage(pixelMap: PixelMap, rotateAngle = 90) {
await pixelMap.rotate(rotateAngle);
}
3、通过对每个像素点的rgb值转换来改变亮度、透明度
export function execColorInfo(bufferArray: ArrayBuffer, last: number, cur: number, hsvIndex: number) {
if (!bufferArray) {
return;
}
const newBufferArr = bufferArray;
let colorInfo = new Uint8Array(newBufferArr);
for (let i = 0; i < colorInfo?.length; i += CommonConstants.PIXEL_STEP) {
// rgb转换成hsv
const hsv = rgb2hsv(colorInfo[i + RGBIndex.RED], colorInfo[i + RGBIndex.GREEN], colorInfo[i + RGBIndex.BLUE]);
let rate = cur / last;
hsv[hsvIndex] *= rate;
// hsv转换成rgb
const rgb = hsv2rgb(hsv[HSVIndex.HUE], hsv[HSVIndex.SATURATION], hsv[HSVIndex.VALUE]);
colorInfo[i + RGBIndex.RED] = rgb[RGBIndex.RED];
colorInfo[i + RGBIndex.GREEN] = rgb[RGBIndex.GREEN];
colorInfo[i + RGBIndex.BLUE] = rgb[RGBIndex.BLUE];
}
return newBufferArr;
}
4、通过effectKit来添加滤镜
export async function pinkColorFilter(pixelMap: PixelMap) {
const pinkColorMatrix: Array<number> = [
1, 1, 0, 0, 0,
0, 1, 0, 0, 0,
0, 0, 1, 0, 0,
0, 0, 0, 1, 0
]
const pixelMapFiltered = await effectKit.createEffect(pixelMap).setColorMatrix(pinkColorMatrix).getEffectPixelMap();
return pixelMapFiltered;
}
场景变化前后对比
使用说明
通过拖拽移动按钮,来展示两张图片的不同
效果预览
实现思路
1.创建三个Stack组件,用来展示装修前后对比图,第一个和第三个Stack分别存放装修前的图片和装修后的图片,zIndex设置为1。第二个Stack存放按钮的图片,zIndex设置为2,这样按钮的图片就会覆盖在两张装修图片之上。
Row() {
/**
* 创建两个Stack组件,用来展示装修前后对比图,分别存放装修前的图片和装修后的图片,zIndex设置为1。
* 中间Column存放按钮的图片,zIndex设置为2,这样按钮的图片就会覆盖在两张装修图片之上。
*/
Stack() {
...
}
.zIndex(CONFIGURATION.Z_INDEX1)
.width(this.leftImageWidth)
.clip(true)
.alignContent(Alignment.TopStart)
Column() {
Image($r("app.media.drag_to_switch_pictures_drag_button"))
.width(30)
.height(160)
.draggable(false)
...
}
.width(2)
.zIndex(CONFIGURATION.Z_INDEX2)
Stack() {
...
}
.zIndex(CONFIGURATION.Z_INDEX1)
.clip(true)
.width(this.rightImageWidth)
.alignContent(Alignment.TopEnd)
}
2.将Image组件放在Row容器里,将Row容器的宽度设置为状态变量,再利用clip属性对于Row容器进行裁剪。
Row() {
Image($r("app.media.drag_to_switch_pictures_before_decoration"))
.width(320)
.height(160)
.draggable(false)
}
.width(this.leftImageWidth)
.zIndex(CONFIGURATION.Z_INDEX1)
.clip(true)
.borderRadius({
topLeft: 10,
bottomLeft: 10
})
3.右边的Image组件与左边同样的操作,但是新增了一个direction属性,使元素从右至左进行布局,为的是让Row从左侧开始裁剪。
Row() {
Image($r("app.media.drag_to_switch_pictures_after_decoration"))
.width(320)
.height(160)
.draggable(false)
}
.width(this.rightImageWidth)
.clip(true)
.zIndex(CONFIGURATION.Z_INDEX1)
.direction(Direction.Rtl)
.borderRadius({
topRight: 10,
bottomRight: 10
})
4.中间的Image组件通过手势事件中的滑动手势对Image组件滑动进行监听,对左右Image组件的宽度进行计算从而重新布局渲染。
Image($r("app.media.drag_to_switch_pictures_drag_button"))
.width(30)
.height(160)
.draggable(false)
.gesture(
PanGesture({ fingers: CONFIGURATION.PAN_GESTURE_FINGERS, distance: CONFIGURATION.PAN_GESTURE_DISTANCE })
.onActionStart(() => {
this.dragRefOffset = CONFIGURATION.INIT_VALUE;
})
.onActionUpdate((event: GestureEvent) => {
// 通过监听GestureEvent事件,实时监听图标拖动距离
this.dragRefOffset = event.offsetX;
this.leftImageWidth = this.imageWidth + this.dragRefOffset;
this.rightImageWidth = CONFIGURATION.IMAGE_FULL_SIZE - this.leftImageWidth;
if (this.leftImageWidth >=
CONFIGURATION.LEFT_IMAGE_RIGHT_LIMIT_SIZE) { // 当leftImageWidth大于等于310vp时,设置左右Image为固定值,实现停止滑动效果。
this.leftImageWidth = CONFIGURATION.LEFT_IMAGE_RIGHT_LIMIT_SIZE;
this.rightImageWidth = CONFIGURATION.RIGHT_IMAGE_RIGHT_LIMIT_SIZE;
} else if (this.leftImageWidth <=
CONFIGURATION.LEFT_IMAGE_LEFT_LIMIT_SIZE) { // 当leftImageWidth小于等于30vp时,设置左右Image为固定值,实现停止滑动效果。
this.leftImageWidth = CONFIGURATION.LEFT_IMAGE_LEFT_LIMIT_SIZE;
this.rightImageWidth = CONFIGURATION.RIGHT_IMAGE_LEFT_LIMIT_SIZE;
}
})
.onActionEnd(() => {
if (this.leftImageWidth <= CONFIGURATION.LEFT_IMAGE_LEFT_LIMIT_SIZE) {
this.leftImageWidth = CONFIGURATION.LEFT_IMAGE_LEFT_LIMIT_SIZE;
this.rightImageWidth = CONFIGURATION.RIGHT_IMAGE_LEFT_LIMIT_SIZE;
this.imageWidth = CONFIGURATION.LEFT_IMAGE_LEFT_LIMIT_SIZE;
} else if (this.leftImageWidth >= CONFIGURATION.LEFT_IMAGE_RIGHT_LIMIT_SIZE) {
this.leftImageWidth = CONFIGURATION.LEFT_IMAGE_RIGHT_LIMIT_SIZE;
this.rightImageWidth = CONFIGURATION.RIGHT_IMAGE_RIGHT_LIMIT_SIZE;
this.imageWidth = CONFIGURATION.LEFT_IMAGE_RIGHT_LIMIT_SIZE;
} else {
this.leftImageWidth = this.imageWidth + this.dragRefOffset; // 滑动结束时leftImageWidth等于左边原有Width+拖动距离。
this.rightImageWidth =
CONFIGURATION.IMAGE_FULL_SIZE - this.leftImageWidth; // 滑动结束时rightImageWidth等于340-leftImageWidth。
this.imageWidth = this.leftImageWidth; // 滑动结束时ImageWidth等于leftImageWidth。
}
})
)
图片切割九宫格
使用说明
通过图库选择一张图片,将其切割成九宫格展示,然后可保存到图库中
效果预览
实现思路
根据图片宽高信息以及要分割的行数列数,计算出每张图片的左上角起始位置的坐标及宽高,根据这些信息获取对应的pixelMap
export async function splitImage(l = 3, c = 3): Promise<SplitImageModel> {
// 选择图片
let uris = await selectImages(1)
let originUri = ''
// 存储切割图片
let imagePixels: image.PixelMap[] = []
if (uris && uris.length) {
originUri = uris[0]
// 创建图像编码ImagePacker对象
let imagePickerApi = image.createImagePacker();
// 以只读方式打开指定下标图片
let file: fileIo.File = await fileIo.open(uris[0], fileIo.OpenMode.READ_ONLY)
let fd: number = file.fd;
// 获取图片源
let imageSource = image.createImageSource(fd);
// 图片信息
let imageInfo = await imageSource.getImageInfo();
// 图片高度除以3,就是把图片切为3份
let height = imageInfo.size.height / l;
let width = imageInfo.size.width / c;
// 切换为 3x3 张图片
for (let i = 0; i < l; i++) {
for (let j = 0; j < c; j++) {
// 设置解码参数DecodingOptions,解码获取pixelMap图片对象
let decodingOptions: image.DecodingOptions = {
desiredRegion: {
size: {
height: height, // 切开图片高度
width: width // 切开图片宽度
},
x: j * width, // 切开x起始位置
y: i * height // 切开y起始位置
}
}
// 根据参数重新九宫格图片
let img: image.PixelMap = await imageSource.createPixelMap(decodingOptions);
// 把生成新图片放到内存里
imagePixels.push(img);
}
}
imagePickerApi.release();
fileIo.closeSync(fd);
}
return { originUri, splitImages: imagePixels }
}
两张图片拼接
使用说明
通过图库选择一张图片,可横向拼接成一张图,也可竖向拼接成一张图。然后保存到图库
实现效果
实现思路
获取要拼接图片的信息,计算出拼接后图片的大小等信息,根据这些信息创建出一个pixelMap。然后将要拼接的图片的pixelMap写入创建的空的pixelMap即可
export async function joinImages(images: Array<string>, isH = true) {
try {
if (images.length < 2) {
return;
}
获取图片信息
...
根据要拼接图片创建拼接后的pixelMap
const combineColor = new ArrayBuffer(combineOpts.size.width * combineOpts.size.height * 4);
const newPixelMap = await image.createPixelMap(combineColor, combineOpts);
for (let x = 0; x < images.length; x++) {
const singleOpts = x === 0 ? singleOpts1 : singleOpts2
let singleColor = new ArrayBuffer(singleOpts.desiredSize!.width * singleOpts.desiredSize!.height * 4);
let imageSource = x === 0 ? imageSource1 : imageSource2
let singleWidth = x === 0 ? singleWidth1 : singleWidth2
let singleHeight = x === 0 ? singleHeight1 : singleHeight2
//读取小图
const singlePixelMap = await imageSource.createPixelMap(singleOpts);
await singlePixelMap.readPixelsToBuffer(singleColor);
//写入大图
let area: image.PositionArea = {
pixels: singleColor,
offset: 0,
stride: singleWidth * 4,
region: {
size: { height: singleHeight, width: singleWidth },
x: isH ? x === 0 ? 0 : singleWidth1 : 0,
y: isH ? 0 : x === 0 ? 0 : singleHeight1
}
}
await newPixelMap.writePixels(area);
}
return newPixelMap;
} catch (err) {
hilog.error(0x0000, 'JOIN_IMAGES', 'PictureJoinTogether join error: ' + JSON.stringify(err))
}
return
}
AI抠图
使用说明
长按需要被抠图的元素并拖拽
效果预览
实现思路
将Image接口的enableAnalyzer属性设为true
Image(this.imagePixelMap)
.enableAnalyzer(true)
.width('100%')
图片加水印
使用说明
从图库中选取图片,点击添加水印按钮。即可添加上水印
效果预览
实现思路
1.根据canvas容器实际大小以及图片的实际大小,将选择的图片绘制到canvas中
// 记录图片实际大小
let imageSource: image.ImageSource = image.createImageSource(imageInfo);
imageSource.getImageInfo((err, value) => {
if (err) {
return;
}
this.hValue = Math.round(value.size.height * 1);
this.wValue = Math.round(value.size.width * 1);
let defaultSize: image.Size = {
height: this.hValue,
width: this.wValue
};
let opts: image.DecodingOptions = {
editable: true,
desiredSize: defaultSize
};
imageSource.createPixelMap(opts, (err, pixelMap) => {
if (err) {
return
}
// 将图片绘制canvas上
let rect = this.getComponentRect("imageContainer") as Record<string, number>
this.imageScale = (rect.right - rect.left) / this.wValue
this.imageHeight = this.hValue * this.imageScale
this.context.transform(this.imageScale, 0, 0, this.imageScale, 0, 0)
this.pixelMap = pixelMap
this.context.drawImage(this.pixelMap, 0, 0)
})
})
2.在画布上绘制水印内容
this.context.beginPath()
this.context.font = `宋体 ${100 / this.imageScale}px}`
this.context.textBaseline = "top"
this.context.fillStyle = "#80b2bec3"
this.context.rotate(Math.PI / 180 * 30)
this.context.fillText("水印水印水印水印", 100 / this.imageScale, 100 / this.imageScale)
this.context.rotate(-Math.PI / 180 * 30)
this.context.closePath()
3.根据图片实际大小将加水印的canvas重新绘制一遍,然后将绘制后的pixelMap保存到土库中
let imageInfo = await this.pixelMap.getImageInfo()
let offCanvas = new OffscreenCanvas(px2vp(imageInfo.size.width), px2vp(imageInfo.size.height))
let offContext = offCanvas.getContext("2d")
let contextPixelMap = this.context.getPixelMap(0, 0, this.context.width, this.context.height)
offContext.drawImage(contextPixelMap, 0, 0, offCanvas.width, offCanvas.height)
savePixelMapToGalleryBySaveButton(this,
offContext.getPixelMap(0, 0, offCanvas.width, offCanvas.height));
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。