diff --git a/zh-cn/third-party-cases/Readme-CN.md b/zh-cn/third-party-cases/Readme-CN.md index 08e39e036dbbea1637eff05d97a44ed8649d9880..6068f46ec97541506e4f61b0b6d1033c069c77f7 100644 --- a/zh-cn/third-party-cases/Readme-CN.md +++ b/zh-cn/third-party-cases/Readme-CN.md @@ -7,6 +7,7 @@ ## 案例目录 ### ArkUI + - [如何按字母分组展示联系人](how-to-group-contacts-with-alphabet.md) - [如何实现列表项的新增和删除](how-to-add-delete-listitems.md) - [如何通过显示动画实现书籍翻页动效](book-flip-animation.md) @@ -28,29 +29,35 @@ - [列表上拉加载更多内容](list-pullup-loading-data.md) - [如何删除多选框选项](delete-checkboxgroup-items.md) - [像素单位转换](pixel-format-transfer.md) +- [应用配色随系统深浅模式自动切换](theme-follow-system.md) +- [界面组件在软键盘弹出时重绘避免遮挡](ui-refresh-for-keyboard.md) ### 装饰器 + - [控制页面刷新范围](overall-and-part-refresh.md) - [如何监听多层状态变化](observed-and-objectlink.md) ### 网络管理 + - [如何请求并加载网络图片](how-to-load-images-from-internet.md) ### 窗口管理 + - [如何实现沉浸模式](immersion-mode.md) - [如何创建悬浮窗](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) - - - +- [Navigation如何实现多场景UI适配](multi-device-app-dev.md) + + diff --git a/zh-cn/third-party-cases/figures/color-mode-directory.png b/zh-cn/third-party-cases/figures/color-mode-directory.png new file mode 100644 index 0000000000000000000000000000000000000000..6fcb01de7e791c752fab5d76e2cb97f5989a36da Binary files /dev/null and b/zh-cn/third-party-cases/figures/color-mode-directory.png differ diff --git a/zh-cn/third-party-cases/figures/keyboard.png b/zh-cn/third-party-cases/figures/keyboard.png new file mode 100644 index 0000000000000000000000000000000000000000..94df2cf5648f3b588478b7bd6baba4e4a1bce6dd Binary files /dev/null and b/zh-cn/third-party-cases/figures/keyboard.png differ diff --git a/zh-cn/third-party-cases/figures/theme-follow-system.png b/zh-cn/third-party-cases/figures/theme-follow-system.png new file mode 100644 index 0000000000000000000000000000000000000000..785ca2d8084ec2d30b602e6bbcc342856db77f8d Binary files /dev/null and b/zh-cn/third-party-cases/figures/theme-follow-system.png differ diff --git a/zh-cn/third-party-cases/theme-follow-system.md b/zh-cn/third-party-cases/theme-follow-system.md new file mode 100644 index 0000000000000000000000000000000000000000..b5c0350027a91f908f2ee352e4d5d2fa75d6ad6d --- /dev/null +++ b/zh-cn/third-party-cases/theme-follow-system.md @@ -0,0 +1,292 @@ +# 应用配色随系统深浅模式自动切换 + +## 场景说明 + +应用深浅配色模式是一种常见的系统外观选项,环境变暗时切换到深色模式,可以减轻眼睛疲劳和节省设备电量。如何让应用外观能够自动适应系统的深浅模式?本文将介绍三种实用的方法。 + +## 效果呈现 + +![theme follow system](figures/theme-follow-system.png) + +## 运行环境 + +本例基于以下环境开发,开发者也可基于其他适配的版本进行开发: + +- IDE: DevEco Studio 3.1.1 Release +- SDK: Ohos_sdk_public 3.2.12.5 (API Version 9 Release) + +## 实现思路 + +本文介绍三种不同的方法,让应用能够自如地适配系统的深浅模式: + +- **利用系统颜色资源**:这种方法最简单,只需要在应用中引用系统提供的颜色资源,例如`ohos_id_color_background`或`ohos_id_color_primary`等,就可以让应用自动跟随系统的颜色设置变化。 +- **使用限定词目录**:这种方法稍微复杂一些,需要在应用中创建不同的限定词目录,例如`resources/dark`或`resources/light`等,来表示不同的应用场景,然后在每个目录下定义不同资源,来适配不同的模式。最后,在应用中只需要引用这些资源的名称,而不需要指定具体的目录,系统会根据当前的模式自动选择合适的资源。 +- **订阅系统环境变量变化**:这种方法最灵活,但也最复杂。它需要在应用中监听系统环境变量的变化,并在变化发生时调用相应的函数来处理。这样,可以在函数中实现任何想要的逻辑,来改变应用外观。 + +## 开发步骤 + +新建一个Empty Ability工程,创建一个仅含"Hello World"页面的新应用。 + +**方式一:系统资源** + +为了方便不同的开发者快速构建应用,OpenHarmony提供了一套系统资源供开发者直接使用。如果在UI组件中指定颜色属性为系统颜色ID,那么应用就可以自动适配系统深/浅模式,无需额外的工作。 + +开发者可以使用`“$r('sys.type.resource_id')”`语法来引用系统资源。其中,sys表示这是一个系统资源,type表示资源的类型,可以是“color”、“string”、“media”等,resource_id表示资源的ID,例如“ohos_id_color_background”、“ohos_id_color_primary”等。 + +可以查看官方指南中应用UX设计中关于资源的介绍,了解OpenHarmony支持的所有系统资源ID以及它们在不同模式和设备中的具体取值。 + +完整代码: + +```typescript +@Entry +@Component +struct Index { + @State message: string = 'Hello World' + + build() { + Row() { + Column() { + Text(this.message) + .fontSize(50) + .fontWeight(FontWeight.Bold) + // 引入系统资源颜色设置文字颜色 + .fontColor($r('sys.color.ohos_id_color_text_primary')) + } + .width('100%') + } + // 引入系统资源颜色设置应用背景颜色 + .backgroundColor($r('sys.color.ohos_id_color_sub_background')) + .height('100%') + } +} +``` + +**方式二:限定词目录** + +为了让应用能够适配系统的深浅模式,我们可以创建一个特殊的Color Mode类型资源目录`resources/dark`,来存放适用于深色模式的资源。当系统处于深色模式时,应用就会优先读取这个目录下的资源。如果没有找到对应的资源,应用会使用默认的`resources/base`目录下的资源。 + +除了Color Mode类型之外,还有很多其他类型的限定词,它们可以根据不同的设备和环境来提供不同的资源。例如,可以使用移动国家码和移动网络码来区分不同地区的运营商,使用语言和文字来区分不同语言和方言,使用横竖屏来区分不同方向的布局,使用设备类型来区分不同大小和形状的设备,使用屏幕密度来区分不同分辨率和像素密度的屏幕等。每种限定词都有一套固定的格式和命名规则,需要遵循这些规则来创建合法的资源目录。同时,每种限定词也有一个相对的优先级顺序,系统会根据这个顺序在多个匹配的资源目录中选择最佳的一个。如果想了解更多关于限定词的信息,可以查看[资源分类](../application-dev/quick-start/resource-categories-and-access.md)。 + +![color-mode](figures/color-mode-directory.png) + +我们为新增的resources/dark/element目录,创建color.json文件。新建颜色ID并赋予具体色值,bg_color为黑色,txt_color为白色。 + +```json +{ + "color": [ + { + "name": "bg_color", + "value": "#000000" + }, + { + "name": "txt_color", + "value": "#c6c6c6" + } + ] +} +``` + +在resources/base/element/color.json,同样创建这两个颜色ID并赋予不同的色值,bg_color为白色,txt_color为黑色。 + +```typescript +{ + "color": [ + { + "name": "start_window_background", + "value": "#FFFFFF" + }, + { + "name": "bg_color", + "value": "#f1f3f5" + }, + { + "name": "txt_color", + "value": "#121212" + } + ] +} +``` + +设置组件属性为我们创建的颜色ID,此时,我们的应用在深色模式下取用ID在dark目录下对应的色值,其他情况下会使用base目录下对应的色值。 + +完整代码: + +```typescript +@Entry +@Component +struct Index { + @State message: string = 'Hello World' + + build() { + Row() { + Column() { + Text(this.message) + .fontSize(50) + .fontWeight(FontWeight.Bold) + .fontColor($r('app.color.txt_color')) + } + .width('100%') + } + .backgroundColor($r('app.color.bg_color')) + .height('100%') + } +} +``` + +**方式三:订阅系统环境变量** + +系统配置更新时会调用onConfigurationUpdate,我们可以获取系统配置信息中ColorMode(系统深浅模式)属性,再使用AppStorage存储系统深浅模式状态, 使用@StorageProp建立AppStorage和自定义组件的单向数据同步,通过@Watch监听状态变量变化并创建回调方法,在状态变量改变时修改颜色变量值,从而实现应用配色随系统深浅模式自动切换。 + +1. 在AbilityStage的onCreate()生命周期中获取当前的颜色模式并保存到AppStorage。 + +```typescript + onCreate(want, launchParam) { + AppStorage.SetOrCreate('currentColorMode', this.context.config.colorMode); + } +``` + +2. 在AbilityStage的onConfigurationUpdate()生命周期中获取最新变更的颜色模式并刷新到AppStorage。 + +```typescript + onConfigurationUpdate(newConfig: Configuration): void { + AppStorage.SetOrCreate('currentColorMode', newConfig.colorMode); + } +``` + +3. 在UI Page中通过@StorageProp + @Watch方式获取当前最新深浅模式并监听设备深浅模式变化。 + +```typescript + @StorageProp('currentColorMode') @Watch('onColorModeChange') currentMode: number + +// 深浅模式枚举值如下 +// export enum ColorMode { +// COLOR_MODE_NOT_SET = -1, +// COLOR_MODE_DARK = 0, +// COLOR_MODE_LIGHT = 1 +// } +``` + +4. 创建UI状态变量,设置组件颜色属性为该状态变量。 + +```typescript + @State color_txt: string = '' + @State color_bg: string = '' + + build() { + Row() { + Column() { + Text(this.message) + .fontSize(50) + .fontWeight(FontWeight.Bold) + .fontColor(this.color_txt) + } + .width('100%') + } + .height('100%') + .backgroundColor(this.color_bg) + } +``` + +5. 在@Watch回调函数中执行自定义的适配逻辑。 + +```javascript + onColorModeChange(): void { + if (this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT) { + this.color_txt = '#121212' + this.color_bg = '#f1f3f5' + } else { + this.color_txt = '#c6c6c6' + this.color_bg = '#000000' + } + } +``` + +6. @Watch装饰器装饰的方法,在第一次初始化的时候不会被调用。只有在后续状态改变时,才会调用@Watch回调方法。所以在第一次初始化时需要根据currentMode的值做一次逻辑适配,具体内容与onColorModeChange保持一致。 + +```typescript + aboutToAppear() { + if (this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT) { + this.color_txt = '#121212' + this.color_bg = '#f1f3f5' + } else { + this.color_txt = '#c6c6c6' + this.color_bg = '#000000' + } + } +``` + +在本例中,我们在监听到深浅模式变化后对两个UI状态变量进行控制,从而刷新对应的UI效果。切换深色模式后,文字和背景颜色将会变化。实际开发过程中,应用可根据自己的场景进行扩展,执行自己的自定义适配逻辑。 + +完整代码: + +```typescript +// index.ets +import ConfigurationConstant from '@ohos.app.ability.ConfigurationConstant'; +@Entry +@Component +struct Index { + @State message: string = 'Hello World' + @State color_txt: string = '' + @State color_bg: string = '' + @StorageProp('currentColorMode') @Watch('onColorModeChange') currentMode: number = 1; + + build() { + Row() { + Column() { + Text(this.message) + .fontSize(50) + .fontWeight(FontWeight.Bold) + .fontColor(this.color_txt) + } + .width('100%') + } + .height('100%') + .backgroundColor(this.color_bg) + } + + aboutToAppear() { + if (this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT) { + this.color_txt = '#121212' + this.color_bg = '#f1f3f5' + } else { + this.color_txt = '#c6c6c6' + this.color_bg = '#000000' + } + } + + onColorModeChange(): void { + if (this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT) { + this.color_txt = '#121212' + this.color_bg = '#f1f3f5' + } else { + this.color_txt = '#c6c6c6' + this.color_bg = '#000000' + } + } +} +``` + + + +```typescript +// EntryAbility.ts +import UIAbility from '@ohos.app.ability.UIAbility'; +import window from '@ohos.window'; +import { Configuration } from '@ohos.app.ability.Configuration'; + +export default class EntryAbility extends UIAbility { + onCreate(want, launchParam) { + AppStorage.SetOrCreate('currentColorMode', this.context.config.colorMode); + } + + onWindowStageCreate(windowStage: window.WindowStage) { + windowStage.loadContent('pages/Index', (err, data) => { + }); + } + + onConfigurationUpdate(newConfig: Configuration): void { + AppStorage.SetOrCreate('currentColorMode', newConfig.colorMode); + } +} + +``` diff --git a/zh-cn/third-party-cases/ui-refresh-for-keyboard.md b/zh-cn/third-party-cases/ui-refresh-for-keyboard.md new file mode 100644 index 0000000000000000000000000000000000000000..8f3a26c580af0a7397f1b5eb010bc817dd71d92b --- /dev/null +++ b/zh-cn/third-party-cases/ui-refresh-for-keyboard.md @@ -0,0 +1,134 @@ +# 界面组件在软键盘弹出时重绘避免遮挡 + +## 场景说明 + +在应用开发中,我们经常会遇到这样的场景:当我们点击一个输入框时,系统会自动弹出软键盘,但是软键盘却会遮挡住输入框或者其他重要的控件。为了解决这个问题,现有的常用策略会在软键盘弹出时,根据输入框的位置,把整个页面向上移动一段距离,从而让输入框能够完全显示在屏幕上。然而,这种方法并不完美,其他控件比如确认按钮仍有可能被软键盘挡住,使得用户无法完成操作。本案例将针对此问题提供一种简单而有效的解决方案。 + +## 效果呈现 + +![ui-refresh-keyboard](figures/keyboard.png) + +## 运行环境 + +本例基于以下环境开发,开发者也可基于其他适配的版本进行开发: + +- IDE: DevEco Studio 3.1.1 Release +- SDK: Ohos_sdk_public 3.2.12.5 (API Version 9 Release) + +## 实现思路 + +要想解决页面遮挡的问题,我们首先要了解它的原因:当我们点击输入框时,系统会弹出软键盘,这时候软键盘会占用一部分屏幕空间,导致显示区域和软键盘区域重叠。软键盘区域属于系统规避区域。除了软键盘区域之外,还有一些其他的系统规避区域,比如状态栏、刘海屏和手势区域等。如果想让应用能够适应这些系统规避区域的变化,我们需要监听这些区域的变化事件并且根据这些区域的尺寸,动态地调整应用的显示区域,重新绘制页面。 + +## 开发步骤 + +1. 创建一个简单的输入验证码界面,它包含了文本框、输入框和确认按钮。这个界面的问题是,当我们点击输入框时,系统会弹出软键盘,但是软键盘却会挡住确认按钮,使得我们无法完成操作。 + + ```typescript + @Entry + @Component + struct Index { + @State screenHeight: number = 0; + build() { + Row() { + Column() { + Text('请输入短信验证码') + .fontSize(30) + .margin({ + bottom:'50' + }) + TextInput() + .width('70%') + .height('150px') + .margin({ + bottom: '30' + }) + Button('确定') + .width('70%') + .margin('20px') + } + .width('100%') + } + .width('100%') + .height('100%') + } + } + ``` + +2. 创建一个变量`screenHeight`,并把它赋值为当前显示区域的高度。这样,我们就可以在后面根据需要来修改这个变量的值,从而改变显示区域的高度。 + + ```typescript + @State screenHeight: number = 0; + + window.getLastWindow(getContext(this)).then(currentWindow =>{ + let property = currentWindow.getWindowProperties(); + let avoidArea = currentWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_KEYBOARD); + // 初始化显示区域高度 + this.screenHeight = px2vp(property.windowRect.height - avoidArea.bottomRect.height); + }) + }) + ``` + +3. 监听系统规避区域的变化事件,判断是否是由于软键盘的弹出或收起导致的。如果是,则就获取软键盘的高度。然后,将`screenHeight`的值设置为原始高度减去软键盘的高度。这样,显示区域就会缩小,避免和软键盘重叠。 + + ```typescript + window.getLastWindow(getContext(this)).then(currentWindow =>{ + let property = currentWindow.getWindowProperties(); + currentWindow.on('avoidAreaChange', async data => { + if (data.type !== window.AvoidAreaType.TYPE_KEYBOARD) { + return; + } + this.screenHeight = px2vp(property.windowRect.height - data.area.bottomRect.height); + }) + }) + ``` + +完整代码 + +```typescript +import window from '@ohos.window'; + +@Entry +@Component +struct Index { + @State screenHeight: number = 0; + + aboutToAppear() { + window.getLastWindow(getContext(this)).then(currentWindow =>{ + let property = currentWindow.getWindowProperties(); + let avoidArea = currentWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_KEYBOARD); + // 初始化显示区域高度 + this.screenHeight = px2vp(property.windowRect.height - avoidArea.bottomRect.height); + // 监视软键盘的弹出和收起 + currentWindow.on('avoidAreaChange', async data => { + if (data.type !== window.AvoidAreaType.TYPE_KEYBOARD) { + return; + } + this.screenHeight = px2vp(property.windowRect.height - data.area.bottomRect.height); + }) + }) + } + + build() { + Row() { + Column() { + Text('请输入短信验证码') + .fontSize(30) + .margin({ + bottom:'50' + }) + TextInput() + .width('70%') + .height('150px') + .margin({ + bottom: '30' + }) + Button('确定') + .width('70%') + .margin('20px') + } + .width('100%') + } + .width('100%').height(this.screenHeight) + } +} +```