diff --git a/zh-cn/third-party-cases/Readme-CN.md b/zh-cn/third-party-cases/Readme-CN.md index d578a8e555b6cdc12346e2dfc59121f7019e526e..08e39e036dbbea1637eff05d97a44ed8649d9880 100644 --- a/zh-cn/third-party-cases/Readme-CN.md +++ b/zh-cn/third-party-cases/Readme-CN.md @@ -26,6 +26,9 @@ - [常见弹窗的使用](diverse-dialogues.md) - [折叠展开动效](collapse-and-expand.md) - [列表上拉加载更多内容](list-pullup-loading-data.md) +- [如何删除多选框选项](delete-checkboxgroup-items.md) +- [像素单位转换](pixel-format-transfer.md) + ### 装饰器 - [控制页面刷新范围](overall-and-part-refresh.md) - [如何监听多层状态变化](observed-and-objectlink.md) @@ -38,8 +41,12 @@ - [如何创建悬浮窗](float-window.md) - [保持屏幕常亮](keep-screen-on.md) +### 数据管理 +- [用户首选项的基本使用](preferences-data-process.md) + ### 媒体 - [常见图片编辑](image-edit.md) +- [图片格式转换](image-format-transfer.md) ### 一次开发,多端部署 - [Navigation如何实现多场景UI适配](multi-device-app-dev.md) diff --git a/zh-cn/third-party-cases/delete-checkboxgroup-items.md b/zh-cn/third-party-cases/delete-checkboxgroup-items.md new file mode 100644 index 0000000000000000000000000000000000000000..1d971c9a810396f4fbeda23d187b4cc3b0350a9b --- /dev/null +++ b/zh-cn/third-party-cases/delete-checkboxgroup-items.md @@ -0,0 +1,357 @@ +# 如何删除多选框选项 +## 场景说明 +通常情况下,我们使用多选框都会伴随对选项的操作,比较常见的操作是选中后删除,比如删除购物车的商品、删除账单、删除图片等等。但是,当前OpenHarmony针对多选框组件并没有提供直接的删除其选项的方法,需要开发者自己来实现。本例提供了一种实现方案,供开发者参考。 + +## 效果呈现 +本例最总效果如下: + +![delete-checkboxitem](figures/delete-checkboxitem.gif) + + +## 运行环境 +本例基于以下环境开发,开发者也可以基于其他适配的版本进行开发: + +- IDE: DevEco Studio 4.0 Beta1 +- SDK: Ohos_sdk_public 4.0.7.5 (API Version 10 Beta1) + + +## 实现思路 +本例中涉及的关键特性及实现方案如下: +- 多选框:使用CheckboxGroup组件构建多选框标题,使用Checkbox组件构建多选框选项。 +- 删除多选框选项:通过CheckboxGroup的onChange回调获取到各个选项的选中状态,在删除操作中,将选中的选项从选项列表中删除。 +- 删除时弹出确认框:使用promptAction模块调用showDialog方法弹出对话框,通过回调获取到用户点击的是取消按钮还是确定按钮,如果是确定按钮则执行删除操作。 + +## 开发步骤 +1. 搭建UI布局。 + 整体纵向布局,那就采用Column组件;全选框用CheckboxGroup组件,然后每个选项都包括一个选择框(Checkbox组件)和一个文本(Text组件),且为横向布局,那我们可以把它们放在Flex组件中;最后是一个Button组件。这样整体布局就有了,具体代码如下: + ```ts + @Component + struct CheckboxDemo{ + build(){ + Column(){ + Flex({}){ + CheckboxGroup({}) + Text('水果清单') + } + Flex({}){ + Checkbox({}) + Text('苹果') + } + Flex({}){ + Checkbox({}) + Text('菠萝') + } + Flex({}){ + Checkbox({}) + Text('柚子') + } + Button('删除') + } + } + } + ``` + 框架搭好了,看下效果: + + ![checkbox-before-improve](figures/checkbox-before-improve.PNG) + 可以看到主选框和选项对齐了,接下来我们来调整下样式。 + 先给CheckboxGroup取个名字:fruit_list,然后为各个Checkbox添加相同的group名称,这样就可以将Checkbox挂靠到CheckboxGroup下,剩下的就是给各个组件添加margin、fontSize等通用属性了,不清楚各个组件有哪些属性的请自行查阅组件参考,具体代码如下: + ```ts + @Component + struct CheckboxDemo{ + build(){ + Column(){ + Flex({justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center}){ + // 将CheckboxGroup命名为'fruit_list' + CheckboxGroup({group: 'fruit_list'}) + .selectedColor('#007DFF') + Text('水果清单') + .margin({right:20}) + .fontSize(14) + .lineHeight(20) + .fontColor('#182431') + .fontWeight(FontWeight.Bold) + } + Flex({ justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center}){ + // 通过group参数将Checkbox挂到CheckboxGroup下 + Checkbox({name:'苹果',group:'fruit_list'}) + .selectedColor('#007DFF') + Text('苹果') + .margin({right:20}) + .fontSize(14) + .lineHeight(20) + .fontColor('#182431') + .fontWeight(500) + } + Flex({ justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center}){ + Checkbox({name:'菠萝',group:'fruit_list'}) + .selectedColor('#007DFF') + Text('菠萝') + .margin({right:20}) + .fontSize(14) + .lineHeight(20) + .fontColor('#182431') + .fontWeight(500) + } + Flex({ justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center}){ + Checkbox({name:'柚子',group:'fruit_list'}) + .selectedColor('#007DFF') + Text('柚子') + .margin({right:20}) + .fontSize(14) + .lineHeight(20) + .fontColor('#182431') + .fontWeight(500) + } + Button('删除') + .margin({top:20,left:35}) + .fontSize(15) + .padding({top:5,bottom:5,left:15,right:15}) + } + } + } + ``` + 我们再来看下效果,发现多选框的布局已经正常了: + + ![checkbox-after-improve](figures/checkbox-after-improve.PNG) +2. 简化代码。 + 当前代码重复性很高,包含Checkbox的三个Flex的结构完全一样,我们可以简化一下。把重复的结构通过@Builder抽取出来作为一个自定义组件,用到的地方进行引用即可;每个Flex呈现的内容不同,那就将不同的内容作为参数传入。具体代码如下: + ```ts + @Component + struct CheckboxDemo{ + // flexNameList存储checkbox选项的文本内容;使用@State修饰后,flexNameList发生变化,UI会同步刷新 + @State flexNameList:string[] = ['苹果','菠萝','柚子'] + @Builder + // 将重复内容封装成flexItem,通过参数checkboxName为Text组件传入显示文本,通过groupName绑定CheckboxGroup + flexItem(checkboxName:string,groupName:string){ + Flex({ justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) { + Checkbox({ name:checkboxName, group:groupName}) + .selectedColor('#007DFF') + Text(checkboxName) + .margin({right:20}) + .fontSize(14) + .lineHeight(20) + .fontColor('#182431') + .fontWeight(500) + }.margin({ left: 36 }) + } + + build(){ + Column(){ + Flex({justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center}){ + CheckboxGroup({group: 'fruit_list'}) + .selectedColor('#007DFF') + Text('水果清单') + .margin({right:20}) + .fontSize(14) + .lineHeight(20) + .fontColor('#182431') + .fontWeight(FontWeight.Bold) + } + // 通过ForEach遍历flexNameList循环渲染生成UI + ForEach(this.flexNameList,(item:any)=>{ + // 引用封装的flexItem模板 + this.flexItem(item,'fruit_list') + }) + + Button('删除') + .margin({top:20,left:35}) + .fontSize(15) + .padding({top:5,bottom:5,left:15,right:15}) + }.alignItems(HorizontalAlign.Center) + } + } + ``` +3. 添加删除逻辑。 + 本例中是通过以下方式实现删除操作的:将选中的水果项从flexNameList中删除(为方便展示,本文直接将数据存储在数组中,实际开发需要对接数据库),由于flexNameList被@State修饰,所以其发生变化时会重新执行Build(),从而完成UI刷新,展示删除后的选项。 + 要实现上述逻辑,首先需要获取到被选中的水果项。这里可以通过CheckboxGroup的onChange回调获取,当选项的状态发生变化时,会触发回调并返回各选项的选中状态。 + 具体代码如下: + ```ts + @Component + struct CheckboxDemo{ + ... + // itemStatus用来存储各选项的选中状态 + private itemStatus: CheckboxGroupResult + ... + build(){ + Column(){ + Flex({justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center}){ + CheckboxGroup({group: 'fruit_list'}) + .selectedColor('#007DFF') + // 选项状态发生变化时触发onChange回调,各选项的选中状态储存在itemName中并返回,通过itemStatus.name可以获取到被选中的选项列表 + .onChange((itemName: CheckboxGroupResult) => { + this.itemStatus = itemName + }) + ... + }.alignItems(HorizontalAlign.Center) + } + } + ``` + 将各选项的选中状态存储到itemStatus后,我们就可以通过Button触发删除操作了。当点击删除按钮时,触发删除操作,所以给Button添加onClick事件,并添加删除逻辑。代码如下: + ```ts + @Component + struct CheckboxDemo{ + ...... + // itemStatus用来存储各选项的选中状态 + private itemStatus: CheckboxGroupResult + ...... + build(){ + Column(){ + Flex({justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center}){ + CheckboxGroup({group: 'fruit_list'}) + .selectedColor('#007DFF') + // 选项状态发生变化时触发onChange回调,各选项的选中状态储存在itemName中并返回,通过itemStatus.name可以获取到被选中的选项列表 + .onChange((itemName: CheckboxGroupResult) => { + this.itemStatus = itemName + }) + ...... + Button('删除') + .margin({top:20,left:35}) + .fontSize(15) + .padding({top:5,bottom:5,left:15,right:15}) + // 点击触发删除操作 + .onClick(()=>{ + // 被选中的项存储在this.itemStatus.name列表中 + for(let i of this.itemStatus.name){ + // 从flexNameList中删除被选中的项,刷新UI + this.flexNameList.splice(this.flexNameList.indexOf(i),1) + } + }) + }.alignItems(HorizontalAlign.Center) + } + } + ``` +4. 添加删除确认框。 + 使用promptAction模块调用showDialog方法弹出对话框,然后将删除操作绑定到对话框的确定按钮,具体代码如下: + ```ts + // 导入promptAction模块 + import promptAction from '@ohos.promptAction'; + @Component + struct CheckboxDemo{ + ...... + // itemStatus用来存储各选项的选中状态 + private itemStatus: CheckboxGroupResult + ...... + build(){ + Column(){ + Flex({justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center}){ + CheckboxGroup({group: 'fruit_list'}) + .selectedColor('#007DFF') + // 选项状态发生变化时触发onChange回调,各选项的选中状态储存在itemName中并返回,通过itemStatus.name可以获取到被选中的选项列表 + .onChange((itemName: CheckboxGroupResult) => { + this.itemStatus = itemName + }) + ...... + Button('删除') + .margin({top:20,left:35}) + .fontSize(15) + .padding({top:5,bottom:5,left:15,right:15}) + // 点击触发删除操作 + .onClick(()=>{ + // 调用对话框 + promptAction.showDialog({ + title:'', + message:'确定删除吗?', + buttons:[ + {text:'取消', color:'#000000'}, + {text:'确定', color:'#000000'} + ] + }) + // 用户选择通过data回传,当data.index为1时,用户选择确定,当data.index为0时,用户选择取消 + .then(data=>{ + // 当用户选择确定时,进行删除操作 + if(data.index===1){ + // 被选中的项存储在this.itemStatus.name列表中 + for(let i of this.itemStatus.name){ + // 从flexNameList中删除被选中的项,刷新UI + this.flexNameList.splice(this.flexNameList.indexOf(i),1) + } + } + }) + }) + }.alignItems(HorizontalAlign.Center) + } + } + ``` +## 完整代码 +本例完整代码如下: +```ts +import promptAction from '@ohos.promptAction'; + +@Entry +@Component +struct CheckboxDemo{ + @State flexNameList:string[] = ['苹果','菠萝','柚子'] + private itemStatus:CheckboxGroupResult + @Builder + flexItem(checkboxName:string,groupName:string){ + Flex({ justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) { + Checkbox({ name:checkboxName, group:groupName}) + .selectedColor('#007DFF') + Text(checkboxName) + .margin({right:20}) + .fontSize(14) + .lineHeight(20) + .fontColor('#182431') + .fontWeight(500) + }.margin({ left: 36 }) + } + + build() { + Column() { + if (this.flexNameList.length != 0){ + Flex({ justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) { + CheckboxGroup({ group: 'fruit_list' }) + .selectedColor('#007DFF') + .onChange((itemName: CheckboxGroupResult) => { + this.itemStatus = itemName + }) + Text('水果清单') + .margin({right:20}) + .fontSize(14) + .lineHeight(20) + .fontColor('#182431') + .fontWeight(FontWeight.Bold) + }.margin({top:150}) + + ForEach(this.flexNameList,(item:any)=>{ + this.flexItem(item,'fruit_list') + }) + + Button("删除") + .margin({top:20,left:35}) + .fontSize(15) + .padding({top:5,bottom:5,left:15,right:15}) + .onClick(()=>{ + promptAction.showDialog({ + title:'', + message:'确定删除吗?', + buttons:[ + {text:'取消', color:'#000000'}, + {text:'确定', color:'#000000'} + ] + }) + .then(data=>{ + if(data.index===1){ + for(let i of this.itemStatus.name){ + this.flexNameList.splice(this.flexNameList.indexOf(i),1) + } + } + }) + }) + }else{ + + } + }.alignItems(HorizontalAlign.Center) + } +} +``` +## 参考 + +- [ChecckboxGroup](../application-dev/reference/arkui-ts/ts-basic-components-checkboxgroup.md) +- [Checkbox](../application-dev/reference/arkui-ts/ts-basic-components-checkbox.md) +- [Flex](../application-dev/reference/arkui-ts/ts-container-flex.md) +- [Button](../application-dev/reference/arkui-ts/ts-basic-components-button.md) +- [ohos.promptAction (弹窗)](../application-dev/reference/apis/js-apis-promptAction.md) +- [ForEach循环渲染](../application-dev/quick-start/arkts-rendering-control.md) +- [@State状态管理](../application-dev/quick-start/arkts-state-mgmt-page-level.md) +- [@Builder动态构建UI元素](../application-dev/quick-start/arkts-dynamic-ui-elememt-building.md) \ No newline at end of file diff --git a/zh-cn/third-party-cases/figures/Pixel-Convertion.gif b/zh-cn/third-party-cases/figures/Pixel-Convertion.gif new file mode 100644 index 0000000000000000000000000000000000000000..4f957a440498db78cd117acd058bbcac5a83ffe8 Binary files /dev/null and b/zh-cn/third-party-cases/figures/Pixel-Convertion.gif differ diff --git a/zh-cn/third-party-cases/figures/checkbox-after-improve.PNG b/zh-cn/third-party-cases/figures/checkbox-after-improve.PNG new file mode 100644 index 0000000000000000000000000000000000000000..ad8c650486e67d3dc3fcd451822e98e3366ca6a8 Binary files /dev/null and b/zh-cn/third-party-cases/figures/checkbox-after-improve.PNG differ diff --git a/zh-cn/third-party-cases/figures/checkbox-before-improve.PNG b/zh-cn/third-party-cases/figures/checkbox-before-improve.PNG new file mode 100644 index 0000000000000000000000000000000000000000..d4032a06d341becb01bf12f9216271ddb45b36d0 Binary files /dev/null and b/zh-cn/third-party-cases/figures/checkbox-before-improve.PNG differ diff --git a/zh-cn/third-party-cases/figures/delete-checkboxitem.gif b/zh-cn/third-party-cases/figures/delete-checkboxitem.gif new file mode 100644 index 0000000000000000000000000000000000000000..042f9bcdffd51ff88a4489551ea4e477c74ab0c2 Binary files /dev/null and b/zh-cn/third-party-cases/figures/delete-checkboxitem.gif differ diff --git a/zh-cn/third-party-cases/figures/preference-storage.gif b/zh-cn/third-party-cases/figures/preference-storage.gif new file mode 100644 index 0000000000000000000000000000000000000000..a2fc4adfa609ed5de0fbe24c4164adeed5aca9ba Binary files /dev/null and b/zh-cn/third-party-cases/figures/preference-storage.gif differ diff --git a/zh-cn/third-party-cases/image-format-transfer.md b/zh-cn/third-party-cases/image-format-transfer.md new file mode 100644 index 0000000000000000000000000000000000000000..ee89b7f9eb9d5252d70eacf7948d1cc1d14976f9 --- /dev/null +++ b/zh-cn/third-party-cases/image-format-transfer.md @@ -0,0 +1,225 @@ +# 如何转换图片格式 + +## 场景说明 +当我们获取到图片或者视频的缩略图后,返回的是pixelMap,此时有开发者会有疑问如何将pixelMap转换成jpeg等其他格式的图片,其实使用image类中的packing方法就可以将pixelMap重新打包成新的格式(当前只支持jpeg,webp格式),再使用文件管理就可以将图片存入到应用的沙箱路径。本例即为大家介绍如何完成图片格式转换。 + +## 运行环境 +本例基于以下环境开发,开发者也可以基于其他适配的版本进行开发: + +- IDE: DevEco Studio 4.0 Beta1 +- SDK: Ohos_sdk_public 4.0.7.5 (API Version 10 Beta1) + +## 效果呈现 +本例最终实现效果为:将工程资源文件中png格式的图片转换为jpg格式,并保存在设备中。由于本例不涉及UI讲解,所以不在此提供UI效果。 + +## 实现思路 +本例中完成图片格式转换包含三个关键步骤,相关步骤及实现方案如下: +- 获取到要转换图片的PixelMap数据:使用image的createPixelMap方法获取到图片的PixelMap数据。 +- 将图片的PixelMap重新打包转换为其他格式:使用packing方法进行打包,打包时可以设置格式、压缩质量等。 +- 将重新打包好的图片保存到应用目录:使用图库选择器photoViewPicker的相关功能以及file读写操作完成图片的保存。 + +## 开发步骤 +由于本例重点讲解图片格式的转换,所以开发步骤会着重讲解相关实现,不相关的内容不做介绍,全量代码可参考完整代码章节。 +1. 获取要转换图片的PixelMap数据。 + + 先通过上下文context获取到资源管理器resourceManager,然后通过资源管理器获取到图片数据,然后获取图片的ArrayBuffer,最后通过ArrayBuffer创建imageSource,获取到pixelMap,完成图片解码。 + + 具体代码如下: + ```ts + import common from '@ohos.app.ability.common'; + + @Entry + @Component + struct Index { + ... + context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext + ... + + async getPixelMap(){ + // 获取resourceManager资源管理 + const resourceMgr = this.context.resourceManager + // 获取rawfile文件夹下imagetransfer.PNG的ArrayBuffer + const fileData = await resourceMgr.getMediaContent($r('app.media.imagetransfer')) + const buffer = fileData.buffer + // 创建imageSource + const imageSource = image.createImageSource(buffer) + // 获取PixelMap + const pixelMap = await imageSource.createPixelMap() + return pixelMap + } + ... + } + ``` +2. 将图片的PixelMap重新打包转换为其他格式。 + + 先通过createImagePacker构建ImagePacker实例,再通过该实例调用packing方法进行打包,打包时传入获取到的PixelMap数据及重新打包的图片格式等相关配置信息。 + 具体代码如下: + ```ts + ... + @State src:PixelMap = undefined + ... + // 页面加载前将获取到的图片PixelMap数据赋值给状态变量src + async aboutToAppear() { + this.src = await this.getPixelMap() + } + ... + // 创建ImagePacker实例 + let imagePackerApi = image.createImagePacker(); + let options = { + // 设置重新打包的图片格式 + format: 'image/jpeg', + quality: 98 + }; + // 打包时传入图片的PixelMap:src和图片打包选项:option,异步获取打包后的数据data + imagePackerApi.packing(this.src, options).then((data) => { + console.log('Succeeded in packing the image.'); + }).catch(error => { + console.log('Failed to pack the image..'); + .... + }) + ``` +3. 将重新打包好的图片保存到应用目录。 + + 使用图库选择器photoViewPicker保存文件,保存时可以在保存界面选择保存路径并设定文件名。此时保存的是空文件,然后再使用file将重新打包的图片数据写入保存的文件中,保存完成后我们便可以在保存路径下找到转换格式后的图片文件了。 + 具体代码如下: + ```ts + ... + // 打包时传入图片的pixelmap:src和图片打包选项:option,异步获取打包后的数据data + imagePackerApi.packing(this.src, options).then((data) => { + // 创建文件管理器保存选项实例 + let photoSaveOptions = new picker.PhotoSaveOptions(); + // 保存文件名(可选) + photoSaveOptions.newFileNames = ["imageTransfer.jpg"]; + let photoViewPicker = new picker.PhotoViewPicker(); + + // 保存时传入保存的文件名:photoSaveOptions + photoViewPicker.save(photoSaveOptions) + .then((photoSaveResult) => { + setTimeout(() => { + // 获取到保存文件的URI,后续进行文件读取等操作 + this.uri = photoSaveResult[0]; + + fs.open(this.uri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE).then((file) => { + // 将图片打包数据data写入保存的文件 + fs.write(file.fd, data).then((number) => { + console.info("foo imagetest: write data to file succeed and size is:" + number); + }).catch((err) => { + console.info("foo imagetest: write data to file failed with error:" + err); + }); + // 完成文件写入后,关闭文件 + fs.close(file, (err) => { + if (err) { + console.info("close file failed with error message: " + err.message + ", error code: " + err.code); + } else { + console.info("close file success"); + } + }); + }).catch((err) => { + console.info("foo open file failed with error message: " + err.message + ", error code: " + err.code); + }); + + }, 200) + + }) + .catch((err) => { + console.error('PhotoViewPicker.save failed with err: ' + err); + }) + }) + ... + ``` +## 完整代码 +本例完整代码如下: +```ts +import image from '@ohos.multimedia.image'; +import fs from '@ohos.file.fs'; +import common from '@ohos.app.ability.common'; +import picker from '@ohos.file.picker'; + +@Entry +@Component +struct Index { + @State src:PixelMap = undefined + context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext + private uri = null + // 页面加载前将获取到的图片PixelMap数据赋值给状态变量src + async aboutToAppear() { + this.src = await this.getPixelMap() + } + + async getPixelMap(){ + // 获取resourceManager资源管理 + const resourceMgr = this.context.resourceManager + // 获取rawfile文件夹下httpimage.PNG的ArrayBuffer + const fileData = await resourceMgr.getMediaContent($r('app.media.contact6')) + const buffer = fileData.buffer + // 创建imageSource + const imageSource = image.createImageSource(buffer) + // 创建PixelMap + const pixelMap = await imageSource.createPixelMap() + return pixelMap + console.log('pixelMap ' + JSON.stringify(this.src.getPixelBytesNumber())) + } + + build() { + Row() { + Column() { + Button('转换图片格式:png->jpeg') + .onClick(() => { + // 创建ImagePacker实例 + let imagePackerApi = image.createImagePacker(); + // 设置重新打包的图片格式,及图片压缩质量 + let options = { + format: 'image/jpeg', + quality: 98 + }; + // 打包时传入图片的pixelmap:src和图片打包选项:option,异步获取打包后的数据data + imagePackerApi.packing(this.src, options).then((data) => { + // 创建文件管理器保存选项实例 + let photoSaveOptions = new picker.PhotoSaveOptions(); + // 保存文件名(可选) + photoSaveOptions.newFileNames = ["imageTransfer.jpg"]; + let photoViewPicker = new picker.PhotoViewPicker(); + // 保存时传入保存的文件名:photoSaveOptions + photoViewPicker.save(photoSaveOptions) + .then((photoSaveResult) => { + console.log('foo start') + setTimeout(() => { + // 获取到图片的URI后进行文件读取等操作 + this.uri = photoSaveResult[0]; + fs.open(this.uri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE).then((file) => { + // 将图片打包数据data写入保存的文件 + fs.write(file.fd, data).then((number) => { + console.info("foo imagetest: write data to file succeed and size is:" + number); + }).catch((err) => { + console.info("foo imagetest: write data to file failed with error:" + err); + }); + // 完成文件写入后,关闭文件 + fs.close(file, (err) => { + if (err) { + console.info("close file failed with error message: " + err.message + ", error code: " + err.code); + } else { + console.info("close file success"); + } + }); + }).catch((err) => { + console.info("foo open file failed with error message: " + err.message + ", error code: " + err.code); + }); + }, 200) + }) + .catch((err) => { + console.error('PhotoViewPicker.save failed with err: ' + err); + }) + }) + }) + } + .width('100%') + } + .height('100%') + } +} +``` + +## 参考 +- [@ohos.multimedia.image (图片处理)](../application-dev/reference/apis/js-apis-image.md) +- [@ohos.file.fs (文件管理)](../application-dev/reference/apis/js-apis-file-fs.md) +- [@ohos.file.picker (选择器)](../application-dev/reference/apis/js-apis-file-picker.md) \ No newline at end of file diff --git a/zh-cn/third-party-cases/pixel-format-transfer.md b/zh-cn/third-party-cases/pixel-format-transfer.md new file mode 100644 index 0000000000000000000000000000000000000000..b1432593161531cd3ecafd5c62a7c09f2279fe64 --- /dev/null +++ b/zh-cn/third-party-cases/pixel-format-transfer.md @@ -0,0 +1,814 @@ +# 像素转换(ArkTs) + +在日常应用页面布局设计时,开发者需要知道每个组件的样式及位置,这时就需要了解下长度单位及相互转换方法,ArkUI 开发框架提供了4种像素单位供开发者使用,分别是: px 、 vp 、 fp 和 lpx ,框架采用 vp 为基准数据单位,本篇就简单为大家介绍下像素单位的基本知识与像素单位转换API的使用。通过像素转换案例,向开发者讲解了如何使用像素单位设置组件的尺寸、字体的大小以及不同像素单位之间的转换方法。 + +## 效果呈现 +本例最终效果如下: + +![Pixel-Convertion](figures/Pixel-Convertion.gif) + + + +## 运行环境 +本例基于以下环境开发,开发者也可以基于其他适配的版本进行开发: +- IDE: DevEco Studio 3.1 Release +- SDK: Ohos_sdk_public 3.2.12.5(API Version 9 Release) +## 实现思路 +本篇案例主要功能包括:①像素单位基本知识介绍;②像素单位转换相关API的使用。 +* 构建入口页面:该页面包含两个button组件,通过点击按钮事件,实现到详细页面的跳转 + +* 像素单位介绍页面: + * 构建IntroducitonViewModel.ets + + 创建自定义接口IntroductionItem,根据IntroductionItem接口参数,创建对象数组INTRODUCE_LIST,向对象数组INTRODUCE_LIST中填充像素单位介绍页面所需参数内容。 + + * 构建IntroducitonPage.ets + + 通过ForEach循环渲染上一步骤中对象数组中的每个Item;通过if判断组件的显隐,同时添加样式,完成像素介绍页面。 + +* 像素转换页面: + * 构建ConvertionViewModel.ets + + 创建自定义接口ConversionItem,根据ConversionItem接口参数,创建对象数组ConversionViewModel,向对象数组ConversionViewModel中填充像素转换页面所需参数内容。 + + * 构建ConvertionPage.ets + + 通过ForEach循环渲染上一步构建的ConversionViewModel的每个子菜单Item,同时添加样式,构建像素介绍页面。 + + + +## 开发步骤 +1. 构建入口页面:该页面包含两个button按钮,通过点击按钮事件,实现到详细页的跳转。 + 具体代码如下: + ```ts + // entry/src/main/ets/pages/IndexPage.ets + + import router from '@ohos.router'; + @Entry + @Component + struct IndexPage { + // 定义jumpPage方法,实现路由跳转 + jumpPage(url: string) { + router.pushUrl({ url }) + } + + build() { + Column({ space: 24 }) { + Button('像素介绍') + .height('40vp') + .width('100%') + .backgroundColor($r('app.color.blue_background')) + // 点击时回调jumpPage方法,跳转到pages/IntroductionPage页面 + .onClick(() => this.jumpPage('pages/IntroductionPage')) + + Button('像素转换') + .height('40vp') + .width('100%') + .backgroundColor($r('app.color.blue_background')) + // 点击时回调jumpPage方法,跳转到pages/ConversionPage页面 + .onClick(() => this.jumpPage('pages/ConversionPage')) + } + .backgroundColor($r('app.color.page_background')) + .justifyContent(FlexAlign.Center) + .padding(24) + .width('100%') + .height('100%') + } + } + + ``` + +2. 像素单位介绍页面创建。 + 此页面主要系统介绍像素单位的概念,包含px、vp、lpx以及fp,并在页面中 为Text组件的宽度属性设置不同的像素单位(如px、vp、lpx),fp像素单位则设置为Text组件的字体大小。 + + * 从效果图看,此页面由4个功能相同的菜单组成,我们先构建功能菜单。 + 创建IntroducitonViewModel.ets定义每个子功能菜单Item。 + 具体代码如下: + + ```ts + // entry/src/main/ets/viewmodel/IntroducitonViewModel.ets + + // 创建自定义接口,定义每个Item中的内容 + interface IntroductionItem { + name: string; + title: string; + subTitle: string; + value: string; + smallFontSize: number; + largeFontSize: number; + } + + // 根据自定义接口IntroductionItem,填充内容 + const INTRODUCE_LIST: IntroductionItem[] = [ + { + name: 'px', + title: '屏幕物理像素单位。', + subTitle: null, + value: '200px', + smallFontSize: 0, + largeFontSize: 0 + }, + { + name: 'vp', + title:'屏幕密度相关像素,根据屏幕像素密度转换为屏幕物理像素。', + value:'200vp', + subTitle:'像素密度为160dpi的设备上1vp=1px,1vp对应的物理屏幕像素=(屏幕像素密度/160)px', + smallFontSize: 0, + largeFontSize: 0 + }, + { + name: 'lpx', + title:'视窗逻辑像素单位,lpx单位为实际屏幕宽度与逻辑宽度(通过designWidth配置)的比值。', + subTitle: null, + value: '200lpx', + smallFontSize: 0, + largeFontSize: 0 + }, + { + name: 'fp', + title:'字体像素,与vp类似,随系统字体大小设置变化。', + subTitle:'默认情况下与vp相同,即1vp=1fp,如果用户手动调整了系统字体,scale为缩放比例,设置后的字体大小(单位fp) = 设置前的字体大小 * scale', + value: '', + smallFontSize: 14, + largeFontSize: 24 + } + ]; + + // 定义类IntroductionViewModel,获取像素介绍页面的数据 + class IntroductionViewModel { + getIntroductionList() { + let introductionItems = INTRODUCE_LIST; + return introductionItems; + } + } + + let introductionViewModel = new IntroductionViewModel(); + export default introductionViewModel as IntroductionViewModel; + + ``` + * 渲染像素单位介绍页面,通过ForEach循环渲染上一步构建的IntroductionViewModel的每个子菜单Item;通过if判断组件的显隐,为显示的组件,添加样式,构建像素介绍页面。 + 具体代码如下: + + ```ts + // entry/src/main/ets/pages/IntroducitonPages.ets + import IntroductionViewModel from '../viewmodel/IntroductionViewModel'; + + interface IntroductionItem { + name: string; + title: Resource; + subTitle: Resource; + value: string; + smallFontSize: number; + largeFontSize: number; + } + + @Extend(Text) function titleTextStyle() { + .fontColor($r('app.color.title_font')) + .fontFamily('HarmonyHeiTi_Medium') + .fontWeight(500) + } + + @Entry + @Component + struct IntroductionPage { + build() { + Column() { + Navigation() { + List({ space: 12 }) { + //通过ForEach循环渲染Item,构建像素介绍页面 + ForEach(IntroductionViewModel.getIntroductionList(), (item: IntroductionItem) => { + //渲染每个Item + ListItem() { + Column() { + Text(item.name) + .titleTextStyle() + .fontSize('16fp') + Text(item.title) + .titleTextStyle() + .fontSize('14fp') + .fontFamily('HarmonyHeiTi') + .lineHeight('20fp') + .margin({ top: '8vp'}) + .fontWeight(400) + // subTitle非空,添加Text组件,显示subTitle内容,同时添加样式;不存在则不显示 + if (item.subTitle) { + Text(item.subTitle) + .titleTextStyle() + .opacity(0.6) + .lineHeight('16fp') + .fontSize('12fp') + .fontFamily($r('app.string.HarmonyHeiTi')) + .margin({ top: '20vp' }) + .fontWeight(400) + } + + // value非空,添加Text组件且通过宽度属性设置不同的像素单位 + if (item.value.length > 0) { + Text(item.value) + .titleTextStyle() + .fontColor($r('app.color.item_background')) + .fontSize('16fp') + .textAlign(TextAlign.Center) + .backgroundColor($r('app.color.blue_background')) + .height('28vp') + .width(item.value) + .borderRadius('4vp') + .margin({ top: '12vp' }) + // value为空,添加两个text组件,使用fp像素单位设置为Text组件的字体大小 + } else { + Column() { + Text($r('app.string.font_desc', item.smallFontSize)) + .titleTextStyle() + .fontSize(item.smallFontSize) + Text($r('app.string.font_desc', item.largeFontSize)) + .titleTextStyle() + .fontSize(item.largeFontSize) + .margin({ top: '6vp' }) + } + .alignItems(HorizontalAlign.Start) + .backgroundColor($r('app.color.font_background')) + .width('100%') + .borderRadius('12vp') + .padding('12vp') + .margin({ top: '12vp' }) + } + } + .alignItems(HorizontalAlign.Start) + .width('100%') + .padding('12vp') + .borderRadius('24vp') + .backgroundColor('#FFFFFF') + } + .padding({ + left: '12vp', + right: '12vp' + }) + }) + } + .width('100%') + .height('100%') + } + .titleMode(NavigationTitleMode.Mini) + .title('像素介绍') + } + .backgroundColor($r('app.color.page_background')) + .width('100%') + .height('100%') + } + } + ``` + +3. 像素转换页面创建。 + 此页面主要是通过使用像素转换API,实现不同像素单位之间的相互转换功能。 + + * 从效果图看,此页面由3个功能相同的菜单组成,我们先构建功能菜单。 + 创建ConversionViewModel.ets定义每个子功能菜单Item。 + 具体代码如下: + + ```ts + // entry/src/main/ets/viewmodel/ConversionViewModel.ets + + // 创建自定义接口,定义每个Item中的内容 + interface ConversionItem { + title: string; + subTitle: string; + value: number; + conversionTitle: string; + conversionSubTitle: string; + conversionValue: number; + notice: string; + } + + // 定义类ConversionViewModel,获取像素转换页面的数据 + class ConversionViewModel { + getConversionList() { + let conversionItems = CONVERSION_LIST; + return conversionItems; + } + } + + // 根据自定义接口ConversionItem,填充内容 + export const CONVERSION_LIST: ConversionItem[] = [ + { + title: 'vp > px', + subTitle: `vp2px(60)`, + value: vp2px(60), + conversionTitle: 'px > vp', + conversionSubTitle: `px2vp(60)`, + conversionValue: px2vp(60), + notice: null + }, + { + title: 'fp > px', + subTitle: `fp2px(60)`, + value: fp2px(60), + conversionTitle: 'px > fp', + conversionSubTitle: `px2fp(60})`, + conversionValue: px2fp(60), + notice: null + }, + { + title: 'lpx > px', + subTitle: `lpx2px(60)`, + value: lpx2px(60), + conversionTitle: 'px > lpx', + conversionSubTitle: `px2lpx(60)`, + conversionValue: px2lpx(60), + notice: 'lpx与px之间的转换,需要根据实际情况设置designWidth' + } + ]; + + let conversionViewModel = new ConversionViewModel(); + export default conversionViewModel as ConversionViewModel; + + ``` + * 渲染像素单位介绍页面,通过ForEach循环渲染上一步构建的ConversionViewModel的每个子菜单Item,同时添加样式,构建像素介绍页面。 + 具体代码如下: + + ```ts + // entry/src/main/ets/pages/ConversionPage.ets + + import ConversionViewModel from '../viewmodel/ConversionViewModel'; + + interface ConversionItem { + title: string; + subTitle: string; + value: number; + conversionTitle: string; + conversionSubTitle: string; + conversionValue: number; + notice: string; + } + + @Extend(Text) function descTextStyle() { + .fontColor($r('app.color.title_font')) + .fontSize('14fp') + .fontFamily($r('app.string.HarmonyHeiTi')) + .lineHeight('20fp') + .fontWeight(400) + .margin({ top: '8vp' }) + } + + @Extend(Text) function titleTextStyle() { + .fontColor($r('app.color.title_font')) + .fontSize('16fp') + .fontFamily($r('app.string.HarmonyHeiTi_Medium')) + .fontWeight(500) + } + + @Styles function blueStyle() { + .backgroundColor($r('app.color.blue_background')) + .height('28vp') + .borderRadius('4vp') + .margin({ top: '4vp' }) + } + + @Entry + @Component + struct ConversionPage { + build() { + Column() { + Navigation() { + List({ space: 12 }) { + //通过ForEach循环渲染Item,构建像素转换页面 + ForEach(ConversionViewModel.getConversionList(), (item: ConversionItem) => { + //渲染每个Item + ListItem() { + Column() { + Text(item.title) + .titleTextStyle() + .margin({ top: '6vp' }) + Text(item.subTitle) + .descTextStyle() + .opacity(0.6) + Row() + .blueStyle() + // 为宽度属性设置不同的像素单位 + .width(item.value) + Text(item.conversionTitle) + .titleTextStyle() + .margin({ top: '18vp' }) + Text(item.conversionSubTitle) + .descTextStyle() + .opacity(0.6) + Row() + .blueStyle() + // 为宽度属性设置不同的像素单位 + .width(item.conversionValue) + if (item.notice) { + Text(item.notice) + .descTextStyle() + .fontColor($r('app.color.notice_font')) + } + } + .alignItems(HorizontalAlign.Start) + .width('100%') + .padding('12vp') + .borderRadius('24vp') + .backgroundColor('#FFFFFF') + } + .padding({left: '12vp',right: '12vp'}) + }) + } + .width('100%') + .height('100%') + } + .titleMode(NavigationTitleMode.Mini) + .title('像素转换') + } + .backgroundColor($r('app.color.page_background')) + .width('100%') + .height('100%') + } + } + + ``` + + +## 完整代码 +本例完整代码如下: + +* 应用主页面:entry/src/main/ets/pages/IndexPage.ets。 + + ```ts + import router from '@ohos.router'; + @Entry + @Component + struct IndexPage { + // 定义jumpPage方法,实现路由跳转 + jumpPage(url: string) { + router.pushUrl({ url }) + } + + build() { + Column({ space: 24 }) { + Button('像素介绍') + .height('40vp') + .width('100%') + .backgroundColor($r('app.color.blue_background')) + // 点击时回调jumpPage方法,跳转到pages/IntroductionPage页面 + .onClick(() => this.jumpPage('pages/IntroductionPage')) + + Button('像素转换') + .height('40vp') + .width('100%') + .backgroundColor($r('app.color.blue_background')) + // 点击时回调jumpPage方法,跳转到pages/ConversionPage页面 + .onClick(() => this.jumpPage('pages/ConversionPage')) + } + .backgroundColor($r('app.color.page_background')) + .justifyContent(FlexAlign.Center) + .padding(24) + .width('100%') + .height('100%') + } + } + ``` + + + +* 像素介绍ViewModel:entry/src/main/ets/viewmodel/IntroducitonViewModel.ets。 + + ```ts + // 创建自定义接口,定义每个Item中的内容 + interface IntroductionItem { + name: string; + title: string; + subTitle: string; + value: string; + smallFontSize: number; + largeFontSize: number; + } + + // 根据自定义接口IntroductionItem,填充内容 + const INTRODUCE_LIST: IntroductionItem[] = [ + { + name: 'px', + title: '屏幕物理像素单位。', + subTitle: null, + value: Constants.PIXEL_WIDTH + 'px', + smallFontSize: 0, + largeFontSize: 0 + }, + { + name: 'vp', + title:'屏幕密度相关像素,根据屏幕像素密度转换为屏幕物理像素。', + value: Constants.PIXEL_WIDTH + 'vp', + subTitle:'像素密度为160dpi的设备上1vp=1px,1vp对应的物理屏幕像素=(屏幕像素密度/160)px', + smallFontSize: 0, + largeFontSize: 0 + }, + { + name: 'lpx', + title:'视窗逻辑像素单位,lpx单位为实际屏幕宽度与逻辑宽度(通过designWidth配置)的比值。', + subTitle: null, + value: Constants.PIXEL_WIDTH + 'lpx', + smallFontSize: 0, + largeFontSize: 0 + }, + { + name: 'fp', + title:'字体像素,与vp类似,随系统字体大小设置变化。', + subTitle:'默认情况下与vp相同,即1vp=1fp,如果用户手动调整了系统字体,scale为缩放比例,设置后的字体大小(单位fp) = 设置前的字体大小 * scale', + value: '', + smallFontSize: Constants.SMALL_FONT_SIZE, + largeFontSize: Constants.LARGE_FONT_SIZE + } + ]; + + // 定义类IntroductionViewModel,获取像素介绍页面的数据 + class IntroductionViewModel { + getIntroductionList() { + let introductionItems = INTRODUCE_LIST; + return introductionItems; + } + } + + let introductionViewModel = new IntroductionViewModel(); + export default introductionViewModel as IntroductionViewModel; + ``` + + + +* 像素介绍页面:entry/src/main/ets/pages/IntroducitonPages.ets。 + + ```ts + import IntroductionViewModel from '../viewmodel/IntroductionViewModel'; + + interface IntroductionItem { + name: string; + title: Resource; + subTitle: Resource; + value: string; + smallFontSize: number; + largeFontSize: number; + } + + @Extend(Text) function titleTextStyle() { + .fontColor($r('app.color.title_font')) + .fontFamily('HarmonyHeiTi_Medium') + .fontWeight(500) + } + + @Entry + @Component + struct IntroductionPage { + build() { + Column() { + Navigation() { + List({ space: 12 }) { + //通过ForEach循环渲染Item,构建像素介绍页面 + ForEach(IntroductionViewModel.getIntroductionList(), (item: IntroductionItem) => { + //渲染每个Item + ListItem() { + Column() { + Text(item.name) + .titleTextStyle() + .fontSize('16fp') + Text(item.title) + .titleTextStyle() + .fontSize('14fp') + .fontFamily('HarmonyHeiTi') + .lineHeight('20fp') + .margin({ top: '8vp'}) + .fontWeight(400) + // subTitle非空,添加Text组件,显示subTitle内容,同时添加样式;不存在则不显示 + if (item.subTitle) { + Text(item.subTitle) + .titleTextStyle() + .opacity(0.6) + .lineHeight('16fp') + .fontSize('12fp') + .fontFamily($r('app.string.HarmonyHeiTi')) + .margin({ top: '20vp' }) + .fontWeight(400) + } + + // value非空,添加Text组件且通过宽度属性设置不同的像素单位 + if (item.value.length > 0) { + Text(item.value) + .titleTextStyle() + .fontColor($r('app.color.item_background')) + .fontSize('16fp') + .textAlign(TextAlign.Center) + .backgroundColor($r('app.color.blue_background')) + .height('28vp') + .width(item.value) + .borderRadius('4vp') + .margin({ top: '12vp' }) + // value为空,添加两个text组件,使用fp像素单位设置为Text组件的字体大小 + } else { + Column() { + Text($r('app.string.font_desc', item.smallFontSize)) + .titleTextStyle() + .fontSize(item.smallFontSize) + Text($r('app.string.font_desc', item.largeFontSize)) + .titleTextStyle() + .fontSize(item.largeFontSize) + .margin({ top: '6vp' }) + } + .alignItems(HorizontalAlign.Start) + .backgroundColor($r('app.color.font_background')) + .width('100%') + .borderRadius('12vp') + .padding('12vp') + .margin({ top: '12vp' }) + } + } + .alignItems(HorizontalAlign.Start) + .width('100%') + .padding('12vp') + .borderRadius('24vp') + .backgroundColor('#FFFFFF') + } + .padding({ + left: '12vp', + right: '12vp' + }) + }) + } + .width('100%') + .height('100%') + } + .titleMode(NavigationTitleMode.Mini) + .title('像素介绍') + } + .backgroundColor($r('app.color.page_background')) + .width('100%') + .height('100%') + } + } + ``` + + + +* 像素转换ViewModel:entry/src/main/ets/viewmodel/ConversionViewModel.ets。 + + ```ts + // 创建自定义接口,定义每个Item中的内容 + interface ConversionItem { + title: string; + subTitle: string; + value: number; + conversionTitle: string; + conversionSubTitle: string; + conversionValue: number; + notice: string; + } + + // 定义类ConversionViewModel,获取像素转换页面的数据 + class ConversionViewModel { + getConversionList() { + let conversionItems = CONVERSION_LIST; + return conversionItems; + } + } + + // 根据自定义接口ConversionItem,填充内容 + export const CONVERSION_LIST: ConversionItem[] = [ + { + title: 'vp > px', + subTitle: `vp2px(60)`, + value: vp2px(60), + conversionTitle: 'px > vp', + conversionSubTitle: `px2vp(60)`, + conversionValue: px2vp(60), + notice: null + }, + { + title: 'fp > px', + subTitle: `fp2px(60)`, + value: fp2px(60), + conversionTitle: 'px > fp', + conversionSubTitle: `px2fp(60})`, + conversionValue: px2fp(60), + notice: null + }, + { + title: 'lpx > px', + subTitle: `lpx2px(60)`, + value: lpx2px(60), + conversionTitle: 'px > lpx', + conversionSubTitle: `px2lpx(60)`, + conversionValue: px2lpx(60), + notice: 'lpx与px之间的转换,需要根据实际情况设置designWidth' + } + ]; + + let conversionViewModel = new ConversionViewModel(); + export default conversionViewModel as ConversionViewModel; + ``` + + + +* 像素转换页面:entry/src/main/ets/pages/ConversionPage.ets。 + + ```ts + import ConversionViewModel from '../viewmodel/ConversionViewModel'; + + interface ConversionItem { + title: string; + subTitle: string; + value: number; + conversionTitle: string; + conversionSubTitle: string; + conversionValue: number; + notice: string; + } + + @Extend(Text) function descTextStyle() { + .fontColor($r('app.color.title_font')) + .fontSize('14fp') + .fontFamily($r('app.string.HarmonyHeiTi')) + .lineHeight('20fp') + .fontWeight(400) + .margin({ top: '8vp' }) + } + + @Extend(Text) function titleTextStyle() { + .fontColor($r('app.color.title_font')) + .fontSize('16fp') + .fontFamily($r('app.string.HarmonyHeiTi_Medium')) + .fontWeight(500) + } + + @Styles function blueStyle() { + .backgroundColor($r('app.color.blue_background')) + .height('28vp') + .borderRadius('4vp') + .margin({ top: '4vp' }) + } + + @Entry + @Component + struct ConversionPage { + build() { + Column() { + Navigation() { + List({ space: 12 }) { + //通过ForEach循环渲染Item,构建像素转换页面 + ForEach(ConversionViewModel.getConversionList(), (item: ConversionItem) => { + //渲染每个Item + ListItem() { + Column() { + Text(item.title) + .titleTextStyle() + .margin({ top: '6vp' }) + Text(item.subTitle) + .descTextStyle() + .opacity(0.6) + Row() + .blueStyle() + // 为宽度属性设置不同的像素单位 + .width(item.value) + Text(item.conversionTitle) + .titleTextStyle() + .margin({ top: '18vp' }) + Text(item.conversionSubTitle) + .descTextStyle() + .opacity(0.6) + Row() + .blueStyle() + // 为宽度属性设置不同的像素单位 + .width(item.conversionValue) + if (item.notice) { + Text(item.notice) + .descTextStyle() + .fontColor($r('app.color.notice_font')) + } + } + .alignItems(HorizontalAlign.Start) + .width('100%') + .padding('12vp') + .borderRadius('24vp') + .backgroundColor('#FFFFFF') + } + .padding({left: '12vp',right: '12vp'}) + }) + } + .width('100%') + .height('100%') + } + .titleMode(NavigationTitleMode.Mini) + .title('像素转换') + } + .backgroundColor($r('app.color.page_background')) + .width('100%') + .height('100%') + } + } + ``` + + + + ## 参考 + +[像素单位](../application-dev/reference/arkui-ts/ts-pixel-units.md) + +[List](../application-dev/reference/arkui-ts/ts-container-list.md) + +[Column](../application-dev/reference/arkui-ts/ts-container-column.md) + +[Text](../application-dev/reference/arkui-ts/ts-basic-components-text.md) + +[Navigation](../application-dev/reference/arkui-ts/ts-basic-components-navigation.md) \ No newline at end of file diff --git a/zh-cn/third-party-cases/preferences-data-process.md b/zh-cn/third-party-cases/preferences-data-process.md new file mode 100644 index 0000000000000000000000000000000000000000..b3da7d94ba35ab671d0cd7657e2571a7152e3994 --- /dev/null +++ b/zh-cn/third-party-cases/preferences-data-process.md @@ -0,0 +1,351 @@ +# 首选项的使用 + +## 场景说明 +用户首选项为应用提供Key-Value键值型的数据处理能力,支持应用持久化轻量级数据,并对其修改和查询。当用户希望有一个全局唯一存储的地方,可以采用用户首选项来进行存储。Preferences会将该数据缓存在内存中,当用户读取的时候,能够快速从内存中获取数据。Preferences会随着存放的数据量越多而导致应用占用的内存越大,因此,Preferences不适合存放过多的数据,适用的场景一般为应用保存用户的个性化设置(屏幕亮度,是否开启夜间模式)等。 +本例以一个小示例为大家介绍如何使用用户首选项对数据进行存储、获取、删除。 + +## 效果呈现 +本例最终效果如下: + +![preference-storage](figures/preference-storage.gif) + +## 运行环境 +本例基于以下环境开发,开发者也可以基于其他适配的版本进行开发: + +- IDE: DevEco Studio 4.0 Beta1 +- SDK: Ohos_sdk_public 4.0.7.5 (API Version 10 Beta1) + +## 实现思路 +本例以设置屏幕亮度为例演示如何使用用户首选项管理数据,主要特性及实现方式如下: +- 当用户在文本框输入数据后,点击保存数据,用户首选项将数据缓存在内存中:通过dataPreferences类的getPreferences方法获取用户首选项实例,然后通过该实例调用put方法将数据写入内存。 +- 当用户点击读取数据时,用户首选项将数据从内存中读取出来并显示在输入框中:通过用户首选项实例调用get方法获取到保存的数据,显示在输入框中。 +- 当用户点击删除数据时,用户首选项将数据从内存中删除,用户无法继续读取数据:通过用户首选项实例调用delete方法删除保存的数据。 + +> ![icon-note.gif](../device-dev/public_sys-resources/icon-note.gif) **说明:** +> 用户首选项的使用需要注意以下几点: +> - Key键为string类型,要求非空且长度不超过80个字节。 +> - 如果Value值为string类型,请使用UTF-8编码格式,可以为空,不为空时长度不超过8192个字节。 +> - 内存会随着存储数据量的增大而增大,所以存储的数据量应该是轻量级的,建议存储的数据不超过一万条,否则会在内存方面产生较大的开销。 + +## 开发步骤 +由于本例重点讲解用户首选项的数据管理操作,所以开发步骤会着重讲解如何通过用户首选项完成数据的存储、读取和删除,全量代码可参考完整代码章节。 +1. 首先自定义一个用户首选项类,根据业务封装相关方法方便后续调用。 + 其中包含数据处理的方法,用于完成数据的存储、读取和删除操作。用户首选项接口的使用方式主要在这部分呈现,需要重点关注。 + 具体代码如下: + ```ts + import dataPreferences from '@ohos.data.preferences'; + import promptAction from '@ohos.promptAction'; + import ScreenBrightness from '../common/bean/Brightness'; + + let context = getContext(this); + let preference: dataPreferences.Preferences = null; + + // 自定义用户首选项类 + class PreferenceModel { + private brightness:ScreenBrightness + + // 创建用户首选项实例preference + async getPreferencesFromStorage() { + try { + preference = await dataPreferences.getPreferences(context, 'setting.db'); + } catch (err) { + Logger.error('[PreferenceModel]', `Failed to get preferences, Cause: ${err}`); + } + } + + // 删除数据,调用dataPreferences的deletePreferences接口 + async deletePreferences() { + try { + await dataPreferences.deletePreferences(context, 'setting.db'); + } catch(err) { + Logger.error('[PreferenceModel]', `Failed to delete preferences, Cause: ${err}`); + }; + preference = null; + this.showToastMessage($r('app.string.delete_success_msg')); + } + + // 保存数据 + async putPreference(screenBrightness:ScreenBrightness) { + if (preference === null) { + await this.getPreferencesFromStorage(); + } + // 将用户输入的亮度数据保存到preference,调用用户首选项实例的put接口 + try { + await preference.put('screenBrightness', JSON.stringify(screenBrightness)); + } catch (err) { + Logger.error('[PreferenceModel]', `Failed to put value, Cause: ${err}`); + } + // 使用flush方法将preferences实例的数据存储到持久化文件,调用用户首选项实例的flush接口 + await preference.flush(); + } + + // 获取数据,调用用户首选项实例的get接口 + async getPreference() { + let screenBrightness = ''; + if (preference === null) { + await this.getPreferencesFromStorage(); + } + try { + screenBrightness = await preference.get('screenBrightness', ''); + } catch (err) { + Logger.error('[PreferenceModel]', `Failed to get value, Cause: ${err}`); + } + // 如果判断数据为空则提示用户先输入数据 + if (screenBrightness === '') { + this.showToastMessage($r('app.string.data_is_null_msg')); + return; + } + this.showToastMessage($r('app.string.read_success_msg')); + return JSON.parse(screenBrightness); + } + + // 校验用户输入是否为空 + checkData(screenBrightness:ScreenBrightness) { + if (screenBrightness.brightSwitch === '' || screenBrightness.defaultValue === '') { + this.showToastMessage($r('app.string.fruit_input_null_msg')); + return true; + } + return false; + } + + // 点击保存按钮保存数据 + writeData(screenBrightness:ScreenBrightness) { + // Check whether the data is null. + let isDataNull = this.checkData(screenBrightness); + if (isDataNull) { + return; + } + // The data is inserted into the preferences database if it is not empty. + this.putPreference(screenBrightness); + this.showToastMessage($r('app.string.write_success_msg')); + } + + // 消息弹框 + showToastMessage(message: Resource) { + promptAction.showToast({ + message: message, + duration: 3000 + }); + }; + } + ``` +2. UI中主要包含两大部分:文本和输入框,按钮。将这两部分分别抽取为子组件,在主页中进行调用。具体代码如下: + 文本和输入框子组件: + ```ts + import ScreenBrightness from '../common/bean/Brightness'; + + @Component + export default struct TextItemComponent { + private textResource: Resource; + private placeholderResource: Resource; + private marginBottom: string; + private marginTop: string; + private textInputType: InputType; + private textFlag: number; + @Link screenBrightness: ScreenBrightness; + private textInputCallBack: (value: string) => void; + + aboutToAppear() { + if (this.textFlag === 0) { + this.marginTop = '8%'; + this.marginBottom = '4%'; + this.textInputType = InputType.Normal; + } else { + this.marginBottom = '321vp'; + this.textInputType = InputType.Number; + } + } + + build() { + Column() { + // 文本 + Text(this.textResource) + .fontSize(25) + .height('3.2%') + .width('100%') + .fontColor("#182431") + .letterSpacing('1.58') + .fontWeight(500) + .margin({ + bottom: '2%', + left: '7%', + top: this.marginTop + }) + // 输入框 + TextInput({ + placeholder: this.placeholderResource, + text: this.textFlag === 0 ? (this.screenBrightness.brightSwitch) : (this.screenBrightness.defaultValue) + }) + .placeholderFont({ size: 20, weight: 500 }) + .placeholderColor("#BDC1C4") + .caretColor(Color.Blue) + .type(this.textInputType) + .height('7%') + .width('93%') + .margin({ bottom: this.marginBottom }) + .fontSize(20) + .fontColor("#182431") + .fontWeight(500) + .backgroundColor("#FFFFFF") + .onChange((value: string) => { + this.textInputCallBack(value); + }) + } + } + } + ``` + 按钮子组件: + ```ts + import PreferenceModel from '../model/PreferenceModel'; + import ButtonItemData from '../common/bean/ButtonItemData'; + import ScreenBrightness from '../common/bean/Brightness'; + + @Component + export default struct ButtonComponent { + private buttonItemValues: Array = this.getButtonItemValues(); + @Link screenBrightness: ScreenBrightness; + + build() { + Column() { + ForEach(this.buttonItemValues, (item) => { + Button(item.text, { type: ButtonType.Capsule, stateEffect: true }) + .backgroundColor("#E8A027") + .width('87%') + .height('6%') + .fontWeight(500) + .fontSize(20) + .margin({ bottom: '24vp' }) + .onClick(() => { + item.clickMethod(); + }) + }, item => JSON.stringify(item)) + } + } + + // 在foreach中渲染Button组件时传入不同按钮的参数 + getButtonItemValues() { + let values: Array = [ + new ButtonItemData( + '保存数据', + () => { + // 调用保存方法 + PreferenceModel.writeData(this.screenBrightness); + } + ), + new ButtonItemData( + '读取数据', + () => { + // 调用读取方法 + PreferenceModel.getPreference().then(resultData => { + this.screenBrightness = resultData; + console.info('dbdata is '+JSON.stringify(resultData)) + }); + } + ), + new ButtonItemData( + '删除数据', + () => { + // 调用删除方法 + PreferenceModel.deletePreferences(); + // 数据删除后将相关内容置为空 + this.screenBrightness.brightSwitch = ''; + this.screenBrightness.defaultValue = '' + } + ) + ]; + return values; + } + } + ``` +3. 构建首页UI。 + 在页面生命周期的aboutToAppear中调用自定义首选项类的getPreference方法获取到保存的数据,这样如果用户之前有保存数据的话,进入应用中就可以显示之前保存的数据。 + 具体代码如下: + ```ts + import PreferenceModel from '../model/PreferenceModel'; + import ButtonComponent from '../view/ButtonComponent'; + import TextItemComponent from '../view/TextItemComponent'; + import ScreenBrightness from '../common/bean/Brightness'; + + @Entry + @Component + struct Setting { + @State screenBrightness: ScreenBrightness = new ScreenBrightness('', ''); + + build() { + Column() { + // 亮度调节文本及文本框 + TextItemComponent({ + textResource: $r('app.string.brightness_text'), + placeholderResource: $r('app.string.brightness_placeholder'), + textFlag: 0, + screenBrightness: $screenBrightness, + textInputCallBack: (value) => { + this.screenBrightness.brightSwitch = value; + } + }) + + // 设定值文本及文本框 + TextItemComponent({ + textResource: $r('app.string.defaultValue_text'), + placeholderResource: $r('app.string.defaultValue_placeholder'), + textFlag: 1, + screenBrightness: $screenBrightness, + textInputCallBack: (value) => { + this.screenBrightness.defaultValue = value; + } + }) + + // 按钮 + ButtonComponent({ screenBrightness: $screenBrightness }) + } + .width('100%') + .height('100%') + .backgroundColor("#F1F3F5") + } + + async aboutToAppear() { + await PreferenceModel.getPreferencesFromStorage(); + // 获取到之前保存的数据,显示在输入框中 + PreferenceModel.getPreference().then(resultData => { + this.screenBrightness = resultData; + }); + } + } + ``` + +## 完整代码 +由于开发步骤中已经展示了大部分完整代码,此处补充前文中未呈现的两个数据类: +亮度数据类: +```ts +export default class ScreenBrightness { + // 亮度调节 + brightSwitch: string; + // 设定值 + defaultValue: string; + + constructor(brightSwitch: string, defaultValue: string) { + this.brightSwitch = brightSwitch; + this.defaultValue = defaultValue; + } +} +``` +按钮数据类: +```ts +export default class ButtonItemData { + + // 按钮文本 + text: string; + + // 按钮点击事件触发的方法 + clickMethod: () => void; + + constructor(text: string, clickMethod: () => void) { + this.text = text; + this.clickMethod = clickMethod; + } +} +``` + +## 参考 +- [@ohos.data.preferences (用户首选项)](../application-dev/reference/apis/js-apis-data-preferences.md) +- [通过用户首选项实现数据持久化](../application-dev/database/data-persistence-by-preferences.md) \ No newline at end of file