HarmonyOS 鸿蒙Next实现图片编辑裁剪
最近开发一款鸿蒙应用,需要用到图片裁剪,系统的图片编辑功能如今没有对外开放API调用,以是需要本身整一套裁剪的功能。由于刚接触这个ArkTS语言,探索的过程还是挺费劲。废话不多说,先直接看效果:
https://i-blog.csdnimg.cn/direct/a9dd1171633a4a4daa5568d82b4fecbf.jpeg
支持自由裁剪和固定比例裁剪。
官方也提供了图片编辑功能的实现:
图片编辑-HarmonyOS NEXT-Codelabs-华为开发者同盟 (huawei.com)
官方效果:
https://i-blog.csdnimg.cn/direct/449192250e28458e8a4af4d6adfe8233.png
这种方式功能比力简朴,裁剪是手动点击的,大小是固定的,并不能满足我的需求。
于是向官方求助,给了一个实现的demo,大致代码如下:
model/Bean.ets
export interface RectPosition {
x: number;
y: number;
height: number;
width: number;
}
export enum ActionType {
topLeft,
topRight,
bottomLeft,
bottomRight,
move
}
export interface Position {
x: number;
y: number;
}
export interface InitPosition {
x: number;
y: number;
width: number;
height: number;
}
pages/ImageCropPage.ets
import image from '@ohos.multimedia.image';
import { resourceManager } from '@kit.LocalizationKit';
import { RectPosition, ActionType, Position, InitPosition } from '../model/Bean'
const TAG = "IMAGE_CROPPING"
@Entry
@Component
struct ImageCropPage {
@Provide pixelMap: image.PixelMap | undefined = undefined;
@Provide pixelMapBackUp: image.PixelMap | undefined = undefined;
@Provide imageInfo: image.ImageInfo | undefined = undefined;
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private canvasContext: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
private settings2: RenderingContextSettings = new RenderingContextSettings(true);
private canvasContext2: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings2);
private settings3: RenderingContextSettings = new RenderingContextSettings(true);
private canvasContext3: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings3);
private actionType: ActionType = ActionType.move;
private rotateOn: boolean = false
private sw: number = 266; //图片展示框固定宽度
private sh: number = 390; //图片展示框固定高度
@State clipImageWidth: number = 0;
@State clipImageHeight: number = 0;
@State imageArea: RectPosition = {
x: 0,
y: 0,
width: 0,
height: 0
};
private touchPosition: Position = {
x: 0,
y: 0,
};
@State initPosition: InitPosition = {
x: 0,
y: 0,
width: 0,
height: 0,
}
@State isCrop: boolean = false
@State cropImageInfo: image.ImageInfo | undefined = undefined;
@State pixelMapChange: boolean = false
@State @Watch('drawMask') clipRect: RectPosition = {
x: 0,
y: 0,
height: 0,
width: 0
};
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
if (this.isCrop) {
if (this.pixelMapChange) {
Image(this.pixelMap)
.width(this.clipImageWidth)
.height(this.clipImageHeight)
.margin({ top: '10%' })
.objectFit(ImageFit.Fill)
} else {
Image(this.pixelMap)
.width(this.clipImageWidth)
.height(this.clipImageHeight)
.margin({ top: '10%' })
.objectFit(ImageFit.Fill)
}
} else {
Canvas(this.canvasContext)
.width(this.sw)
.height(this.sh)
.onReady(() => {
this.drawImage()
})
.onAreaChange((value: Area, newVal: Area) => {
// 获取图片位置xy
this.initPosition.x = Math.round(newVal.position.x as number)
this.initPosition.y = Math.round(newVal.position.y as number)
})
// 蒙层
Canvas(this.canvasContext3)
.position({
x: this.initPosition.x,
y: this.initPosition.y
})
.width(this.sw)
.height(this.sh)
// 裁剪框
Canvas(this.canvasContext2)
.position({
x: this.clipRect.x,
y: this.clipRect.y
})
.width(this.clipRect.width)
.height(this.clipRect.height)
.onReady(() => {
this.drawClipImage()
})
.onTouch(event => {
if (event.type === TouchType.Down) {
this.isMove(event.target.area, event.touches);
this.touchPosition = {
x: event.touches.screenX,
y: event.touches.screenY
}
} else if (event.type === TouchType.Move) {
let moveX = event.changedTouches.screenX - this.touchPosition.x;
let moveY = event.changedTouches.screenY - this.touchPosition.y;
this.touchPosition = {
x: event.changedTouches.screenX,
y: event.changedTouches.screenY
}
this.moveClipCanvas(moveX, moveY);
}
})
}
Row() {
Image($rawfile('rotate.png'))
.width(40)
.height(40)
.onClick(() => {
this.rotateImage()
})
}
.margin({ top: 50 })
.height('7%')
.width('100%')
.padding(30)
Row() {
Image($rawfile('reset.png'))
.width(40)
.height(40)
.onClick(() => {
this.cancel()
})
Image($rawfile('crop.png'))
.width(40)
.height(40)
.onClick(() => {
this.clipImage()
})
}
.margin({ top: 10 })
.width('100%')
.height('7%')
.padding(30)
.justifyContent(FlexAlign.SpaceBetween)
}
.width('100%')
.height('100%')
.backgroundColor('#000000')
}
// 旋转图片
async rotateImage() {
if (this.rotateOn) {
await this.pixelMap?.rotate(90)
const info = await this.pixelMap?.getImageInfo()
this.cropImageInfo = info
if (this.pixelMapChange) {
this.pixelMapChange = false
} else {
this.pixelMapChange = true
}
}
}
// 取消剪切
cancel() {
this.pixelMap = this.pixelMapBackUp
this.isCrop = false
this.rotateOn = false
}
// 判断操作类型
isMove(area: Area, touch: TouchObject) {
if (touch.x < 60 && touch.y < 60) { // 左上角
this.actionType = ActionType.topLeft
} else if (touch.x < 60 && touch.y > (Number(area.height) - 60)) { // 左下
this.actionType = ActionType.bottomLeft
} else if (touch.x > Number(area.width) - 60 && touch.y < 60) { // 右上
this.actionType = ActionType.topRight
} else if (touch.x > Number(area.width) - 60 && touch.y > (Number(area.height) - 60)) { // 右下
this.actionType = ActionType.bottomRight
} else {
this.actionType = ActionType.move
}
}
// 绘制背景图
async drawImage() {
await this.initData('test.jpg')
if (this.imageInfo != undefined) {
// let width = px2vp(this.imageInfo.size.width);
// let height = px2vp(this.imageInfo.size.height)
// this.canvasContext.drawImage(this.pixelMap, 0, 0, px2vp(this.imageInfo.size.width),
// px2vp(this.imageInfo.size.height));
this.canvasContext.drawImage(this.pixelMap,0,0,this.imageInfo.size.width,this.imageInfo.size.height,0,0,this.sw, this.sh)
this.canvasContext.save();
}
}
// 绘制蒙层
drawMask() {
this.canvasContext3.clearRect(0, 0, this.sw, this.sh);
this.canvasContext3.fillStyle = 'rgba(0,0,0,0.7)';
this.canvasContext3.fillRect(0, 0, this.sw, this.sh);
this.canvasContext3.clearRect(this.clipRect.x - this.initPosition.x, this.clipRect.y - this.initPosition.y,
this.clipRect.width, this.clipRect.height);
}
// 绘制裁剪框
drawClipImage() {
this.canvasContext2.clearRect(0, 0, this.clipRect.width, this.clipRect.height);
this.canvasContext2.lineWidth = 6
this.canvasContext2.strokeStyle = '#ff6be038'
this.canvasContext2.beginPath()
this.canvasContext2.moveTo(0, 20)
this.canvasContext2.lineTo(0, 0);
this.canvasContext2.lineTo(20, 0);
this.canvasContext2.moveTo(this.clipRect.width - 20, 0);
this.canvasContext2.lineTo(this.clipRect.width, 0);
this.canvasContext2.lineTo(this.clipRect.width, 20);
this.canvasContext2.moveTo(0, this.clipRect.height - 20);
this.canvasContext2.lineTo(0, this.clipRect.height);
this.canvasContext2.lineTo(20, this.clipRect.height);
this.canvasContext2.moveTo(this.clipRect.width - 20, this.clipRect.height);
this.canvasContext2.lineTo(this.clipRect.width, this.clipRect.height);
this.canvasContext2.lineTo(this.clipRect.width, this.clipRect.height - 20);
this.canvasContext2.stroke()
this.canvasContext2.beginPath();
this.canvasContext2.lineWidth = 0.5;
let height = Math.round(this.clipRect.height / 3);
for (let index = 0; index <= 3; index++) {
let y = index === 3 ? this.clipRect.height : height * index;
this.canvasContext2.moveTo(0, y);
this.canvasContext2.lineTo(this.clipRect.width, y);
}
let width = Math.round(this.clipRect.width / 3);
for (let index = 0; index <= 3; index++) {
let x = index === 3 ? this.clipRect.width : width * index;
this.canvasContext2.moveTo(x, 0);
this.canvasContext2.lineTo(x, this.clipRect.height);
}
this.canvasContext2.stroke();
}
// 获取pixelMap与imageInfo
async initData(fileName: string) {
const context: Context = getContext(this);
const resourceMgr: resourceManager.ResourceManager = context.resourceManager;
const fileData = await resourceMgr.getRawFileContent(fileName);
const buffer = fileData.buffer;
// const imageSource: image.ImageSource = image.createImageSource(buffer);
const imageSource: image.ImageSource = image.createImageSource(getContext().filesDir + "/picture2.jpg");
const pixelMap: image.PixelMap = await imageSource.createPixelMap()
this.pixelMap = pixelMap
this.pixelMapBackUp = pixelMap
const imageInfo = await pixelMap.getImageInfo()
this.imageInfo = imageInfo
// 裁剪框初始位置
this.initPosition.width = px2vp(Math.round(this.imageInfo.size.width))
this.initPosition.height = px2vp(Math.round(this.imageInfo.size.height))
this.clipRect.height = this.sh
this.clipRect.width = this.sw
this.clipRect.x = this.initPosition.x
this.clipRect.y = this.initPosition.y
}
// 裁剪图片
async clipImage() {
let ratioX = 1;
let ratioY = 1;
if (this.imageInfo != undefined) {
ratioX = this.imageInfo.size.width / this.sw;
ratioY = this.imageInfo.size.height / this.sh;
}
let x = this.clipRect.x - this.initPosition.x;
let y = this.clipRect.y - this.initPosition.y;
console.log('clipImage() x= ' + x + 'y = ' + y + ' height = ' + this.clipRect.height + ' width = ' + this.clipRect.width)
await this.pixelMap?.crop({
x: vp2px(x),
y: vp2px(y),
size: {
height: this.clipRect.height * ratioY,
width: this.clipRect.width * ratioX
}
})
this.cropImageInfo = await this.pixelMap?.getImageInfo();
this.isCrop = true
this.rotateOn = true
this.setClipImageSize();
}
setClipImageSize(){
let maxWidth: number = 300;
let maxHeight: number = 500;
if(this.cropImageInfo!=null){
let aspectRatio: number = this.cropImageInfo.size.height / this.cropImageInfo.size.width;
if(this.cropImageInfo.size.width > px2vp(maxWidth)){ //宽度固定,计算高度
this.clipImageWidth = maxWidth;
if(maxWidth * aspectRatio > maxHeight){
this.clipImageHeight = maxHeight;
}else{
this.clipImageHeight = maxWidth * aspectRatio;
}
}else{
if(this.cropImageInfo.size.height > px2vp(maxHeight)){//高度固定,计算宽度
this.clipImageHeight = maxHeight;
this.clipImageWidth = maxWidth / aspectRatio;
}else{
this.clipImageWidth = px2vp(this.cropImageInfo.size.width);
this.clipImageHeight = px2vp(this.cropImageInfo.size.height);
}
}
}
}
// 裁剪框位置和大小变化 初始位置为图片的初始坐标 移动的坐标
moveClipCanvas(moveX: number, moveY: number) {
let clipRect: RectPosition = {
x: this.clipRect.x,
y: this.clipRect.y,
width: this.clipRect.width,
height: this.clipRect.height
}
switch (this.actionType) {
case ActionType.move:
clipRect.x += moveX;
clipRect.y += moveY;
break;
case ActionType.topLeft:
clipRect.x += moveX;
clipRect.y += moveY;
clipRect.width += -moveX;
clipRect.height += -moveY;
break;
case ActionType.topRight:
clipRect.y += moveY;
clipRect.width += moveX;
clipRect.height += -moveY;
break;
case ActionType.bottomLeft:
clipRect.x += moveX;
clipRect.width += -moveX;
clipRect.height += moveY;
break;
case ActionType.bottomRight:
clipRect.width += moveX;
clipRect.height += moveY;
break;
default:
break;
}
// 偏移坐标小于初始位置
if (clipRect.x < this.initPosition.x) {
clipRect.x = this.initPosition.x;
}
if (clipRect.y < this.initPosition.y) {
clipRect.y = this.initPosition.y;
}
// 横坐标限制位置
if (clipRect.width + clipRect.x > this.sw + this.initPosition.x) {
if (this.actionType === ActionType.move) {
clipRect.x = this.sw + this.initPosition.x - clipRect.width;
} else {
clipRect.width = this.sw + this.initPosition.x - clipRect.x;
}
}
// 纵坐标限制
if (clipRect.height + clipRect.y > this.sh + this.initPosition.y) {
if (this.actionType === ActionType.move) {
clipRect.y = this.sh + this.initPosition.y - clipRect.height;
} else {
clipRect.height = this.sh + this.initPosition.y - clipRect.y;
}
}
this.clipRect = {
x: Math.round(clipRect.x),
y: Math.round(clipRect.y),
width: Math.round(clipRect.width),
height: Math.round(clipRect.height)
};
}
}
pages/Index.ets
import picker from '@ohos.multimedia.cameraPicker'
import camera from '@ohos.multimedia.camera';
import common from '@ohos.app.ability.common';
import { BusinessError } from '@ohos.base';
import fileuri from '@ohos.file.fileuri';
import fs from '@ohos.file.fs';
import photoAccessHelper from '@ohos.file.photoAccessHelper';
import { router } from '@kit.ArkUI';
let context = getContext(this) as common.Context;
class CameraPosition {
cameraPosition: camera.CameraPosition
saveUri: string
constructor(cameraPosition: camera.CameraPosition, saveUri: string) {
this.cameraPosition = cameraPosition
this.saveUri = saveUri
}
}
let pathDir = getContext().filesDir;
console.log('保存路径为'+pathDir)
let filePath = pathDir + '/picture.jpg'
fs.createRandomAccessFileSync(filePath, fs.OpenMode.CREATE);
let uri = fileuri.getUriFromPath(filePath);
async function photo() {
try {
let pickerProfile = new CameraPosition(camera.CameraPosition.CAMERA_POSITION_BACK, uri)
let pickerResult: picker.PickerResult = await picker.pick(context,
, pickerProfile);
console.log("the pick pickerResult is:" + JSON.stringify(pickerResult));
} catch (error) {
let err = error as BusinessError;
console.error(`the pick call failed. error code: ${err.code}`);
}
}
async function picture() {
let PhotoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
PhotoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
PhotoSelectOptions.maxSelectNumber = 1;
PhotoSelectOptions.isSearchSupported = false;
PhotoSelectOptions.isEditSupported = false;
PhotoSelectOptions.isPreviewForSingleSelectionSupported = false;
let photoPicker = new photoAccessHelper.PhotoViewPicker();
photoPicker.select(PhotoSelectOptions).then((PhotoSelectResult: photoAccessHelper.PhotoSelectResult) => {
let photouri: Array<string> = PhotoSelectResult.photoUris
let file = fs.openSync(photouri, fs.OpenMode.READ_ONLY)
let file2 = fs.openSync(pathDir+'/picture2.jpg', fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE)
fs.copyFileSync(file.fd, file2.fd)
fs.closeSync(file);
fs.closeSync(file2);
router.pushUrl({
url: "pages/ImageCropPage"
})
})
}
@Entry
@Component
struct Index {
build() {
Column() {
Button('选择并编辑').onClick(() => {
picture()
})
}
.width('100%')
.height('100%')
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Center)
}
} 实现的效果:
https://i-blog.csdnimg.cn/direct/fc5f1e9ddf9e496d933636909bfccff5.jpeg
这一效果大致可以满足我的需求,但我在测试的时候发现裁剪框滑动的时候有问题,裁剪框和图片位置没做处理惩罚,有大神可以本身处理惩罚的,可以利用这个去修改。
以我的性格就是继续探索新的方案。
经过不停查找,终于找到了一个满足需求的demo,并且功能很齐全,给大家看看效果:
https://i-blog.csdnimg.cn/direct/9ebe7f7f97264dffaeb0504b2ca4952c.png
但这个demo项目部分利用的代码是TS语言,NEXT版需要改造才能用。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]