HarmonyOS 鸿蒙Next 有没有自定义相机的demo
HarmonyOS 鸿蒙Next 有没有自定义相机的demo
有没有自定义相机的demo
这边提供一个完整的demo,包括图片裁剪,相机拍照,已经相册选图功能。
本项目基于OpenHarmony三方库 [ImageKnife] 进行场景开发使用:
支持不同类型的本地与网络图片展示。
支持磁拍照展示与图库照片选择展示。
支持进行图片变换: 支持图像像素源图片变换效果。
需要权限
1、ohos.permission.INTERNET
2、ohos.permission.MEDIA_LOCATION
3、ohos.permission.READ_MEDIA
4、ohos.permission.WRITE_MEDIA
下载安装
ohpm install [@ohos](/user/ohos)/imageknife
moudle.json 权限代码
"requestPermissions": [
{
"name": "ohos.permission.INTERNET",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "always"
}
},
{
"name": "ohos.permission.MEDIA_LOCATION",
"reason": "$string:app_permission_MEDIA_LOCATION",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "always"
}
},
{
"name": "ohos.permission.READ_MEDIA",
"reason": "$string:app_permission_READ_MEDIA",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "always"
}
},
{
"name": "ohos.permission.WRITE_MEDIA",
"reason": "$string:app_permission_WRITE_MEDIA",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "inuse"
}
}
]
entryability页面代码:
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
import { ImageKnife } from '[@ohos](/user/ohos)/imageknife'
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(0x1314, 'ImageDemo', '%{public}s', 'Ability onCreate');
AppStorage.setOrCreate('bundleName',this.context.abilityInfo.bundleName)
}
onDestroy(): void {
hilog.info(0x1314, 'ImageDemo', '%{public}s', 'Ability onDestroy');
}
onWindowStageCreate(windowStage: window.WindowStage): void {
// Main window is created, set main page for this ability
hilog.info(0x1314, 'ImageDemo', '%{public}s', 'Ability onWindowStageCreate');
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(0x1314, 'ImageDemo', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
return;
}
hilog.info(0x1314, 'ImageDemo', 'Succeeded in loading the content.');
});
// 初始化全局ImageKnife
ImageKnife.with(this.context);
}
onWindowStageDestroy(): void {
// Main window is destroyed, release UI related resources
hilog.info(0x1314, 'ImageDemo', '%{public}s', 'Ability onWindowStageDestroy');
}
onForeground(): void {
// Ability has brought to foreground
hilog.info(0x1314, 'ImageDemo', '%{public}s', 'Ability onForeground');
}
onBackground(): void {
// Ability has back to background
hilog.info(0x1314, 'ImageDemo', '%{public}s', 'Ability onBackground');
}
}
ets下common文件夹下Constants代码1:
import { HashMap } from '@kit.ArkTS'
import { Tabar } from '../model/Tabar'
export const localImageResource: HashMap<number, ResourceStr> = new HashMap()
localImageResource.set(0, $r('app.media.jpgSample'))
localImageResource.set(1, $r('app.media.pngSample'))
localImageResource.set(2, $r('app.media.svgSample'))
localImageResource.set(3, $r('app.media.gifSample'))
localImageResource.set(4, $r('app.media.bmpSample'))
localImageResource.set(5, $r('app.media.webpSample'))
export const netImageResource: HashMap<number, ResourceStr> = new HashMap()
netImageResource.set(0, 'https://img-home.csdnimg.cn/images/20240327093300.jpg')
netImageResource.set(1, 'https://img-blog.csdnimg.cn/20191215043500229.png')
netImageResource.set(2, 'https://img13.360buyimg.com/n1/jfs/t1/220646/38/10395/30916/61d6e061E1a6d91c8/c0a9a67e726dd7a4.jpg.dpg')
netImageResource.set(3, 'https://gd-hbimg.huaban.com/e0a25a7cab0d7c2431978726971d61720732728a315ae-57EskW_fw658')
netImageResource.set(4, 'https://img-blog.csdn.net/20140514114029140')
netImageResource.set(5, 'https://hbimg.huabanimg.com/95a6d37a39aa0b70d48fa18dc7df8309e2e0e8e85571e-x4hhks_fw658/format/webp')
export const BUNDLE_NAME: string = 'com.harmonyos.image'
export const TAG: string = 'ImageDemo';
export const tabars: Array<Tabar> = [
{
name: '裁剪',
selectN: $r('app.media.ic_pc_cut'),
selectY: $r('app.media.ic_pc_cut_select')
},
{
name: '亮度',
selectN: $r('app.media.ic_public_brightness'),
selectY: $r('app.media.ic_public_brightness_filled')
},
{
name: '对比度',
selectN: $r('app.media.contrast_ratio'),
selectY: $r('app.media.contrast_ratio_selected')
},
{
name: '旋转',
selectN: $r('app.media.ic_switch'),
selectY: $r('app.media.ic_switch_selected')
}
]
export class Constants {
/**
* Title row width.
*/
static readonly TITLE_ROW_WEIGHT: string = '50%';
/**
* Layout full screen.
*/
static readonly LAYOUT_FULL_SCREEN: string = '100%';
/**
* Edit page height.
*/
static readonly EDIT_PAGE_HEIGHT: string = '26%';
/**
* Image show height.
*/
static readonly IMAGE_SHOW_HEIGHT: string = '68%';
/**
* Slider width.
*/
static readonly SLIDER_WIDTH: string = '80%';
/**
* Loading width and height.
*/
static readonly LOADING_WH: string = '30%';
/**
* Clock wise.
*/
static readonly CLOCK_WISE: number = 90;
/**
* Anti clock.
*/
static readonly ANTI_CLOCK: number = -90;
/**
* Tab menu width.
*/
static readonly TAB_MENU_WIDTH: number = 18;
/**
* Navigation height.
*/
static readonly NAVIGATION_HEIGHT: number = 56;
/**
* Adjust slider value.
*/
static readonly ADJUST_SLIDER_VALUE: number[] = [100, 100, 100];
/**
* Slider min.
*/
static readonly SLIDER_MIN: number = 1;
/**
* Slider step.
*/
static readonly SLIDER_MAX: number = 100;
/**
* Slider step.
*/
static readonly SLIDER_STEP: number = 1;
/**
* Pixel step.
*/
static readonly PIXEL_STEP: number = 4;
/**
* Decimal two.
*/
static readonly DECIMAL_TWO: number = 2;
/**
* Color level max.
*/
static readonly COLOR_LEVEL_MAX: number = 255;
/**
* Convert int.
*/
static readonly CONVERT_INT: number = 100;
/**
* Angle 60.
*/
static readonly ANGLE_60: number = 60;
/**
* Angle 120.
*/
static readonly ANGLE_120: number = 120;
/**
* Angle 240.
*/
static readonly ANGLE_240: number = 240;
/**
* Angle 300.
*/
static readonly ANGLE_360: number = 360;
/**
* Angle 360.
*/
static readonly MOD_2: number = 2;
/**
* Average height and width.
*/
static readonly AVERAGE_WEIGHT_WIDTH: number = 2;
/**
* Crop rate 4:3.
*/
static readonly CROP_RATE_4_3: number = 0.75;
/**
* Crop rate 16:9.
*/
static readonly CROP_RATE_9_16: number = 9 / 16;
/**
* Encode quality.
*/
static readonly ENCODE_QUALITY: number = 100;
/**
* Title space.
*/
static readonly TITLE_SPACE: number = 0;
/**
* Slider mode click.
*/
static readonly SLIDER_CLICK_MODE: number = 3;
/**
* Encode format.
*/
static readonly ENCODE_FORMAT: string = 'image/jpeg';
/**
* Encode file permission.
*/
static readonly ENCODE_FILE_PERMISSION: string = 'rw';
/**
* Brightness worker file.
*/
static readonly BRIGHTNESS_WORKER_FILE = 'entry/ets/workers/AdjustBrightnessWork.ts';
/**
* Brightness worker file.
*/
static readonly SATURATION_WORKER_FILE = 'entry/ets/workers/AdjustSaturationWork.ts';
/**
* Image name.
*/
static readonly IMAGE_PREFIX = 'image';
/**
* Image format.
*/
static readonly IMAGE_FORMAT = '.jpg';
/**
* Rawfile name.
*/
static readonly RAW_FILE_NAME = 'low.jpg';
/**
* Cache dir file name.
*/
static readonly RAW_FILE_NAME_TEST = 'low_test.jpg';
/**
* Free ratio.
*/
static readonly DEFAULT_RATIO: number = -1;
/**
* Ratio 1:1.
*/
static readonly RATIO_1_1: number = 1;
/**
* Ratio 16:9.
*/
static readonly RATIO_16_9: number[] = [16, 9];
/**
* Ratio 9:16.
*/
static readonly RATIO_9_16: number[] = [9, 16];
/**
* Ratio 4:3.
*/
static readonly RATIO_4_3: number[] = [4, 3];
/**
* Ratio 3:4.
*/
static readonly RATIO_3_4: number[] = [3, 4];
/**
* Ratio 3:2.
*/
static readonly RATIO_3_2: number[] = [3, 2];
/**
* Ratio 2:3.
*/
static readonly RATIO_2_3: number[] = [2, 3];
/**
* Screen display margin.
*/
static readonly SCREEN_DISPLAY_MARGIN: number = 15;
/**
* Double.
*/
static readonly DOUBLE: number = 2;
/**
* Edit screen can use scope.
*/
static readonly EDIT_SCREEN_USAGE: number = 0.68;
/**
* Title height.
*/
static readonly TITLE_HEIGHT: number = 56;
/**
* Screen manager key.
*/
static readonly APP_KEY_SCREEN_MANAGER: string = 'app_key_screen_manager';
/**
* Whether full screen.
*/
static readonly IS_FULL_SCREEN_KEY: string = 'isFullScreen';
/**
* Device type.
*/
static readonly DEFAULT_DEVICE_TYPE: string = 'phone';
/**
* Status bar color.
*/
static readonly STATUS_BAR_BACKGROUND_COLOR: string = '#F1F3F5';
/**
* Status bar text color.
*/
static readonly STATUS_BAR_CONTENT_COLOR: string = '#000000';
/**
* Navigation height.
*/
static readonly TOP_BAR_SIZE: number = 56;
/**
* Tool bar size.
*/
static readonly TOOL_BAR_SIZE: number = 72;
/**
* Time out.
*/
static readonly TIMEOUT: number = 50;
/**
* Rect line width inner.
*/
static readonly DEFAULT_LINE_WIDTH: number = 0.4;
/**
* Rect line width outer.
*/
static readonly DEFAULT_LINE_RECT_WIDTH: number = 0.8;
/**
* Rect Button line width.
*/
static readonly DEFAULT_BUTTON_WIDTH: number = 2.3;
/**
* Rect Button line padding.
*/
static readonly DEFAULT_BUTTON_PADDING: number = 1;
/**
* Rect Button line length.
*/
static readonly DEFAULT_BUTTON_LENGTH: number = 20;
/**
* Rect line color.
*/
static readonly DEFAULT_LINE_COLOR: string = '#80FFFFFF';
/**
* Rect Button line color outer.
*/
static readonly DEFAULT_RECT_LINE_COLOR: string = '#FFFFFFFF';
/**
* Rect Button line color.
*/
static readonly DEFAULT_BUTTON_COLOR: string = 'white';
/**
* Mask style.
*/
static readonly DEFAULT_MASK_STYLE: string = 'rgba(0, 0, 0, 0.3)';
/**
* Equality threshold.
*/
static readonly EQUALITY_THRESHOLD = 0.0001;
/**
* Min side length.
*/
static readonly DEFAULT_MIN_SIDE_LENGTH: number = 90;
/**
* Touch move identification range.
*/
static readonly DEFAULT_TOUCH_BOUND: number = 20;
/**
* Base scale value.
*/
static readonly BASE_SCALE_VALUE: number = 1.0;
/**
* Default image ratio.
*/
static readonly DEFAULT_IMAGE_RATIO: number = 1.0;
/**
* Rect min side length.
*/
static readonly DEFAULT_MIN_SIDE_LENGTH_EDIT: number = 32;
/**
* Default margin length.
*/
static readonly DEFAULT_MARGIN_LENGTH: number = 20;
/**
* Time out.
*/
static readonly DEFAULT_TIMEOUT_MILLISECOND_1000: number = 1000;
/**
* Default split fraction.
*/
static readonly DEFAULT_SPLIT_FRACTION: number = 3;
}
ets文件夹下common文件夹下BrightNess页面代码:
import {
BrightnessFilterTransformation,
ImageKnifeComponent,
ScaleType,
ImageKnifeData,
ImageKnifeGlobal,
RequestOption
} from '[@ohos](/user/ohos)/imageknife';
import { BusinessError } from '@kit.BasicServicesKit';
import PictureUtil from '../utils/PictureUtil';
/**
* 亮度调节
*/
[@Component](/user/Component)
export struct BrightNess {
private ImageKnife = ImageKnifeGlobal.getInstance().getImageKnife();
private mUrl = $r('app.media.pngSample');
[@State](/user/State) mBrightnessPixelMap?: PixelMap = undefined;
[@State](/user/State) [@Watch](/user/Watch)('brightnessPixelMap') brightNessValue: number = 0;
[@State](/user/State) showUI: boolean = false
textTimerController: TextTimerController = new TextTimerController()
[@State](/user/State) format: string = 'mm:ss.SS'
async aboutToAppear(): Promise<void> {
this.mBrightnessPixelMap = await PictureUtil.getResourcePixelMap($r('app.media.pngSample'))
}
build() {
Column() {
Stack() {
ImageKnifeComponent({
imageKnifeOption: {
loadSrc: this.mBrightnessPixelMap,
mainScaleType: ScaleType.FIT_CENTER,
}
})
.width('100%')
.height(400)
Text(this.brightNessValue.toFixed(0))
.fontSize(48)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
.visibility(this.showUI ? Visibility.Visible : Visibility.Hidden)
TextTimer({ isCountDown: true, count: 1500, controller: this.textTimerController })
.format(this.format)
.fontColor(Color.Black)
.fontSize(50)
.onTimer((utc: number, elapsedTime: number) => {
console.info('textTimer notCountDown utc is:' + utc + ', elapsedTime: ' + elapsedTime)
if (elapsedTime >= 1500) {
this.showUI = false;
}
})
.visibility(Visibility.Hidden)
}
Slider({
value: this.brightNessValue,
min: -100,
max: 100,
style: SliderStyle.OutSet,
})
.blockColor(Color.White)
.trackColor(Color.Gray)
.selectedColor(Color.Gray)
.onChange((value: number, mode: SliderChangeMode) => {
this.brightNessValue = value
this.showUI = true;
if (mode === SliderChangeMode.End) {
this.textTimerController.reset()
this.textTimerController.start()
}
console.info('value:' + value + 'mode:' + mode.toString())
})
Text('参考"ets/component/BrightNess.ets"')
}
.height('100%')
.width('100%')
.padding(15)
.justifyContent(FlexAlign.SpaceAround)
}
/**
*亮度b
*/
brightnessPixelMap() {
let brightness = this.brightNessValue / 100;
let imageKnifeOption = new RequestOption();
new BrightnessFilterTransformation(brightness);
imageKnifeOption.load(this.mUrl)
.addListener({
callback: (err: BusinessError | string, data: ImageKnifeData) => {
this.mBrightnessPixelMap = data.drawPixelMap?.imagePixelMap as PixelMap;
return false;
}
})
.setImageViewSize({ width: vp2px(200), height: vp2px(200) })
.skipMemoryCache(true)
.enableGPU()
.brightnessFilter(brightness)
this.ImageKnife?.call(imageKnifeOption);
}
}
ets文件夹下common文件夹下 Contrast页面代码:
import {
ContrastFilterTransformation,
ImageKnifeComponent,
ScaleType,
ImageKnifeData,
ImageKnifeGlobal,
RequestOption
} from '[@ohos](/user/ohos)/imageknife';
import { BusinessError } from '@kit.BasicServicesKit';
import PictureUtil from '../utils/PictureUtil';
/**
* 对比度调整
*/
[@Component](/user/Component)
export struct Contrast {
private ImageKnife = ImageKnifeGlobal.getInstance().getImageKnife();
private mUrl = $r('app.media.pngSample');
[@State](/user/State) mBrightnessPixelMap?: PixelMap = undefined;
[@State](/user/State) [@Watch](/user/Watch)('contrastPixelMap') contrastValue: number = 0;
[@State](/user/State) showUI: boolean = false
textTimerController: TextTimerController = new TextTimerController()
[@State](/user/State) format: string = 'mm:ss.SS'
async aboutToAppear(): Promise<void> {
this.mBrightnessPixelMap = await PictureUtil.getResourcePixelMap($r('app.media.pngSample'))
}
build() {
Column() {
Stack() {
ImageKnifeComponent({
imageKnifeOption: {
loadSrc: this.mBrightnessPixelMap,
mainScaleType: ScaleType.FIT_CENTER,
}
})
.width('100%')
.height(400)
Text(this.contrastValue.toFixed(0))
.fontSize(48)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
.visibility(this.showUI ? Visibility.Visible : Visibility.Hidden)
TextTimer({ isCountDown: true, count: 1500, controller: this.textTimerController })
.format(this.format)
.fontColor(Color.Black)
.fontSize(50)
.onTimer((utc: number, elapsedTime: number) => {
console.info('textTimer notCountDown utc is:' + utc + ', elapsedTime: ' + elapsedTime)
if (elapsedTime >= 1500) {
this.showUI = false;
}
})
.visibility(Visibility.Hidden)
}
Slider({
value: this.contrastValue,
min: -100,
max: 100,
style: SliderStyle.OutSet,
})
.blockColor(Color.White)
.trackColor(Color.Gray)
.selectedColor(Color.Gray)
.onChange((value: number, mode: SliderChangeMode) => {
this.contrastValue = value
this.showUI = true;
if (mode === SliderChangeMode.End) {
this.textTimerController.reset()
this.textTimerController.start()
}
console.info('value:' + value + 'mode:' + mode.toString())
})
Text('参考"ets/component/Contrast.ets"')
}
.height('100%')
.width('100%')
.padding(15)
.justifyContent(FlexAlign.SpaceAround)
}
/**
*对比度
*/
contrastPixelMap() {
let imageKnifeOption = new RequestOption();
let transformation = new ContrastFilterTransformation(this.contrastValue);
imageKnifeOption.load(this.mUrl)
.addListener({
callback: (err: BusinessError | string, data: ImageKnifeData) => {
this.mBrightnessPixelMap = data.drawPixelMap?.imagePixelMap as PixelMap;
return false;
}
})
.setImageViewSize({ width: vp2px(200), height: vp2px(200) })
.skipMemoryCache(true)
.enableGPU()
.contrastFilter(this.contrastValue)
this.ImageKnife?.call(imageKnifeOption);
}
}
CropImageComponent组件代码:
import {
CropOptions,
FileUtils,
ImageKnifeComponent,
ImageKnifeGlobal,
Options,
PixelMapCrop,
ScaleType
} from '[@ohos](/user/ohos)/imageknife';
import { Constants, TAG } from '../common/Constants';
import { common } from '@kit.AbilityKit';
import { resourceManager } from '@kit.LocalizationKit';
import { BusinessError } from '@kit.BasicServicesKit';
import Pictures from '../utils/PictureUtil';
import { image } from '@kit.ImageKit';
/**
* 图片裁剪
*/
[@Component](/user/Component)
export struct CropImageComponent {
private settings: RenderingContextSettings = new RenderingContextSettings(true)
private canvasContext: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
[@State](/user/State) cropImage: ResourceStr = $r('app.media.pngSample')
[@State](/user/State) options1: Options = new Options();
[@State](/user/State) cropTap: boolean = false;
[@State](/user/State) _rotate: number = 0;
[@State](/user/State) _scale: number = 1;
[@State](/user/State) cropOptions: CropOptions = {
src: this.cropImage,
size: {
width: 300,
height: 300
}
};
private context: common.UIAbilityContext = ImageKnifeGlobal.getInstance().getHapContext() as common.UIAbilityContext;
/**
* 初始化一个默认图片显示
* [@returns](/user/returns)
*/
async aboutToAppear(): Promise<void> {
const resourceMgr = this.context.resourceManager as resourceManager.ResourceManager;
let imageBuffer = await resourceMgr.getMediaContent($r("app.media.pngSample").id);
let arrayBuffer = FileUtils.getInstance().uint8ArrayToBuffer(imageBuffer);
this.setOptions(arrayBuffer)
}
build() {
Scroll() {
Column({ space: 10 }) {
PixelMapCrop({ options: $options1, cropTap: this.cropTap })
Canvas(this.canvasContext)
.width(this.cropOptions.size.width)
.height(this.cropOptions.size.height)
.rotate({
z: 1,
centerX: this.cropOptions.size.width / 2,
centerY: this.cropOptions.size.height / 2,
angle: this._rotate
})
.scale({ x: this._scale, y: this._scale, z: 1.0 })
.gesture(GestureGroup(GestureMode.Parallel,
RotationGesture({ fingers: 2 }).onActionUpdate((event?: GestureEvent) => {
if (event != undefined) {
this._rotate = event.angle;
}
}), PinchGesture({ fingers: 2 }).onActionUpdate((event?: GestureEvent) => {
if (event != undefined) {
this._scale = event.scale;
}
})))
Row() {
Row() {
Button('点击裁剪图片').onClick(() => {
this.cropTap = !this.cropTap;
})
.width('49%')
.type(ButtonType.Normal)
Button('图库选图').onClick(async (event: ClickEvent) => {
let uri = await Pictures.getImageFromGallery()
if (uri) {
Pictures.getPixelMap(uri).then(async pixelMap => {
if (pixelMap) {
let packOpts : image.PackingOption = { format:"image/png", quality:100 };
const imagePackerApi = image.createImagePacker();
imagePackerApi.packing(pixelMap, packOpts).then( (data : ArrayBuffer) => {
this.setOptions(data)
}).catch((error : BusinessError) => {
console.error('Failed to pack the image. And the error is: ' + error);
})
}
})
}
})
.width('49%')
.type(ButtonType.Normal)
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
Text('参考"ets/component/CropImage.ets"')
}
.width('100%')
}
.height('100%')
.padding(15)
}
setOptions(arrayBuffer: ArrayBuffer) {
try {
let optionx = new Options();
optionx.setWidth(800).setHeight(800)
.setCropFunction((err: BusinessError | string, pixelmap: PixelMap | null, sx: number, sy: number) => {
console.log('PMC setCropFunction callback')
if (err) {
console.error('PMC crop err =' + err)
} else {
this.cropOptions.size.width = sx * px2vp(1);
this.cropOptions.size.height = sy * px2vp(1);
if (pixelmap != null) {
this.cropOptions.src = pixelmap;
this.canvasContext.drawImage(pixelmap, 0, 0, this.cropOptions.size.width, this.cropOptions.size.height)
}
}
})
optionx.loadBuffer(arrayBuffer, () => {
this.options1 = optionx;
})
} catch (err) {
console.log(err)
}
}
}
TransformRotate 组件代码:
import {
ImageKnifeComponent,
ScaleType,
ImageKnifeData,
ImageKnifeGlobal,
RequestOption,
RotateImageTransformation
} from '[@ohos](/user/ohos)/imageknife';
import { BusinessError } from '@kit.BasicServicesKit';
import PictureUtil from '../utils/PictureUtil';
/**
* 图片旋转
*/
[@Component](/user/Component)
export struct TransformRotate {
private ImageKnife = ImageKnifeGlobal.getInstance().getImageKnife();
private mUrl = $r('app.media.pngSample');
[@State](/user/State) mRotatePixelMap?: PixelMap = undefined;
[@State](/user/State) [@Watch](/user/Watch)('transformRotate') mRotate: number = 0;
[@State](/user/State) showUI: boolean = false
textTimerController: TextTimerController = new TextTimerController()
[@State](/user/State) format: string = 'mm:ss.SS'
[@State](/user/State) savePath: string = ''
async aboutToAppear(): Promise<void> {
this.mRotatePixelMap = await PictureUtil.getResourcePixelMap($r('app.media.pngSample'))
}
build() {
Scroll() {
Column() {
Stack() {
ImageKnifeComponent({
imageKnifeOption: {
loadSrc: this.mRotatePixelMap,
mainScaleType: ScaleType.FIT_CENTER,
}
})
.width('100%')
.height(400)
Text(this.mRotate.toFixed(0))
.fontSize(48)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
.visibility(this.showUI ? Visibility.Visible : Visibility.Hidden)
TextTimer({ isCountDown: true, count: 1500, controller: this.textTimerController })
.format(this.format)
.fontColor(Color.Black)
.fontSize(50)
.onTimer((utc: number, elapsedTime: number) => {
console.info('textTimer notCountDown utc is:' + utc + ', elapsedTime: ' + elapsedTime)
if (elapsedTime >= 1500) {
this.showUI = false;
}
})
.visibility(Visibility.Hidden)
}
Column() {
Text('固定角度')
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceEvenly }) {
Button('左转45°')
.controlSize(ControlSize.NORMAL)
.buttonStyle(ButtonStyleMode.EMPHASIZED)
.type(ButtonType.Normal)
.width('30%')
.margin({ top: 5, bottom: 5 })
.borderRadius(5)
.onClick(() => {
this.mRotate = this.mRotate >= 45 ? this.mRotate - 45 : this.mRotate + 360 - 45;
})
Button('左转90°')
.controlSize(ControlSize.NORMAL)
.buttonStyle(ButtonStyleMode.EMPHASIZED)
.type(ButtonType.Normal)
.width('30%')
.margin({ top: 5, bottom: 5 })
.borderRadius(5)
.onClick(() => {
this.mRotate = this.mRotate >= 90 ? this.mRotate - 90 : this.mRotate + 360 - 90;
})
Button('旋转180°')
.controlSize(ControlSize.NORMAL)
.buttonStyle(ButtonStyleMode.EMPHASIZED)
.type(ButtonType.Normal)
.width('30%')
.margin({ top: 5, bottom: 5 })
.borderRadius(5)
.onClick(() => {
this.mRotate = this.mRotate + 180 >= 360 ? this.mRotate - 180 : this.mRotate + 180;
})
Button('右转45°')
.controlSize(ControlSize.NORMAL)
.buttonStyle(ButtonStyleMode.EMPHASIZED)
.type(ButtonType.Normal)
.width('30%')
.margin({ top: 5, bottom: 5 })
.borderRadius(5)
.onClick(() => {
this.mRotate = this.mRotate + 45 >= 360 ? this.mRotate + 45 - 360 : this.mRotate + 45;
})
Button('右转90°')
.controlSize(ControlSize.NORMAL)
.buttonStyle(ButtonStyleMode.EMPHASIZED)
.type(ButtonType.Normal)
.width('30%')
.margin({ top: 5, bottom: 5 })
.borderRadius(5)
.onClick(() => {
this.mRotate = this.mRotate + 90 >= 360 ? this.mRotate + 90 - 360 : this.mRotate + 90;
})
}
}
.borderWidth(1)
.borderColor('#E5E5E5')
Slider({
value: this.mRotate,
min: 0,
max: 360,
style: SliderStyle.OutSet,
})
.blockColor(Color.White)
.trackColor(Color.Gray)
.selectedColor(Color.Gray)
.margin({ top: 15 })
.onChange((value: number, mode: SliderChangeMode) => {
this.mRotate = value
this.showUI = true;
if (mode === SliderChangeMode.End) {
this.textTimerController.reset()
this.textTimerController.start()
}
console.info('value:' + value + 'mode:' + mode.toString())
})
Button('保存')
.onClick(async (event: ClickEvent) => {
if (this.mRotatePixelMap) {
this.savePath = await PictureUtil.savePixelMap(this.mRotatePixelMap)
}
})
.width('100%')
.type(ButtonType.Normal)
.borderRadius(5)
.margin({ top: 15 })
if (this.savePath) {
Text('保存后沙箱路径:' + this.savePath).margin({ top: 15 })
}
Text('参考"ets/component/TransformRotate.ets"').margin({ top: 15 })
}
.width('100%')
.padding(15)
.justifyContent(FlexAlign.SpaceAround)
}
.height('100%')
.scrollBar(BarState.Off)
}
/**
* 旋转
*/
transformRotate() {
let imageKnifeOption = new RequestOption();
let transformation = new RotateImageTransformation(this.mRotate);
imageKnifeOption.load(this.mUrl)
.addListener({
callback: (err: BusinessError | string, data: ImageKnifeData) => {
this.mRotatePixelMap = data.drawPixelMap?.imagePixelMap as PixelMap;
return false;
}
})
.setImageViewSize({ width: vp2px(200), height: vp2px(200) })
.skipMemoryCache(true)
.rotateImage(this.mRotate)
this.ImageKnife?.call(imageKnifeOption);
}
}
model文件夹下pages代码:
export class Pages {
names: string = ""
values: NavPathStack | null = null
}
ets文件夹下model文件夹下tabar代码:
export class Tabar {
name: string = '';
selectN: ResourceStr = '';
selectY: ResourceStr = '';
}
pages文件夹下CapabilityList代码:
import { Tabar } from '../model/Tabar'
import { tabars } from '../common/Constants';
import { CropImageComponent } from '../component/CropImage';
import { BrightNess } from '../component/BrightNess';
import { Contrast } from '../component/Contrast';
import { TransformRotate } from '../component/TransformRotate';
[@Entry](/user/Entry)
[@Component](/user/Component)
struct CapabilityList {
[@State](/user/State) fontColor: string = '#182431'
[@State](/user/State) selectedFontColor: string = '#007DFF'
[@State](/user/State) currentIndex: number = 0
private controller: TabsController = new TabsController()
build() {
Column() {
Tabs({ barPosition: BarPosition.End, controller: this.controller }) {
TabContent() {
CropImageComponent()
}
.tabBar(this.tabBuilder(0, tabars[0]))
.backgroundColor(Color.White)
TabContent() {
BrightNess()
}
.tabBar(this.tabBuilder(1, tabars[1]))
.backgroundColor(Color.White)
TabContent() {
Contrast()
}
.tabBar(this.tabBuilder(2, tabars[2]))
.backgroundColor(Color.White)
TabContent() {
TransformRotate()
}
.tabBar(this.tabBuilder(3, tabars[3]))
.backgroundColor(Color.White)
}
.onChange((index: number) => {
this.currentIndex = index
})
.backgroundColor('#F1F3F5')
.scrollable(false)
}
}
[@Builder](/user/Builder)
tabBuilder(index: number, tabar: Tabar) {
Column() {
Image(this.currentIndex === index ? tabar.selectY : tabar.selectN)
.width(24)
.height(24)
.margin({ bottom: 4 })
.objectFit(ImageFit.Contain)
Text(tabar.name)
.fontColor(this.currentIndex === index ? this.selectedFontColor : this.fontColor)
.fontSize(10)
.fontWeight(500)
.lineHeight(14)
}.width('100%').height('100%').justifyContent(FlexAlign.Center)
}
}
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 8px; right: 8px; font-size: 14px;">复制</button>
HarmonyOS 鸿蒙Next 自定义相机 Demo
HarmonyOS 鸿蒙Next确实支持自定义相机开发,并且有相关示例代码可供参考。以下是关于自定义相机Demo的简要说明:
HarmonyOS提供了CameraKit API,允许开发者自定义相机功能。基于CameraKit,开发者可以通过AVRecorder实现录像功能。示例代码中展示了如何通过CameraKit自定义相机,并通过AVRecorder进行录像。该示例包括了相机输入、预览输出流和录像输出流的创建与配置。
要运行此Demo,您需要确保开发环境满足以下条件:
- 支持的设备:华为手机
- HarmonyOS系统:HarmonyOS NEXT Developer Beta1及以上
- DevEco Studio版本:DevEco Studio NEXT Developer Beta1及以上
- HarmonyOS SDK版本:HarmonyOS NEXT Developer Beta1 SDK及以上
您可以在HarmonyOS开发者官方文档中找到更详细的指导信息和示例代码。如果问题依旧没法解决请联系官网客服,官网地址是:https://www.itying.com/category-93-b0.html。