HarmonyOS 鸿蒙Next实现图片编辑裁剪

HarmonyOS 鸿蒙Next实现图片编辑裁剪

最近开发一款鸿蒙应用,需要用到图片裁剪,系统的图片编辑功能目前没有对外开放API调用,所以需要自己整一套裁剪的功能。由于刚接触这个ArkTS语言,探索的过程还是挺费劲。

废话不多说,先直接看效果:

支持自由裁剪和固定比例裁剪。

官方也提供了图片编辑功能的实现:

图片编辑-HarmonyOS NEXT-Codelabs-华为开发者联盟 (huawei.com)

官方效果:

这种方式功能比较简单,裁剪是手动点击的,大小是固定的,并不能满足我的需求。

于是向官方求助,给了一个实现的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[0]);

this.touchPosition = {

x: event.touches[0].screenX,

y: event.touches[0].screenY

}

} else if (event.type === TouchType.Move) {

let moveX = event.changedTouches[0].screenX - this.touchPosition.x;

let moveY = event.changedTouches[0].screenY - this.touchPosition.y;

this.touchPosition = {

x: event.changedTouches[0].screenX,

y: event.changedTouches[0].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,

[picker.PickerMediaType.PHOTO, picker.PickerMediaType.VIDEO], 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 = PhotoSelectResult.photoUris

let file = fs.openSync(photouri[0], 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)

}

}

实现的效果:

这一效果大致可以满足我的需求,但我在测试的时候发现裁剪框滑动的时候有问题,裁剪框和图片位置没做处理,有大神可以自己处理的,可以使用这个去修改。

以我的性格就是继续探索新的方案。

经过不断查找,终于找到了一个满足需求的demo,并且功能很齐全,给大家看看效果:

但这个demo项目部分使用的代码是TS语言,NEXT版需要改造才能用。

相关数据

微信怎么打拼音带声调
365bet官网提现说维护

微信怎么打拼音带声调

⏳ 07-14 💖 24
微信单笔转账最高限额是多少?
365软件下载

微信单笔转账最高限额是多少?

⏳ 07-07 💖 714
狗宝是什么东西 中药狗黄金有什么功效与作用
365bet官网提现说维护

狗宝是什么东西 中药狗黄金有什么功效与作用

⏳ 07-10 💖 803