8.3 案例实战
8.3.1 实现进度条通知
本案例主要介绍如何使用通知能力和基础组件,实现模拟下载文件,发送通知。
8.3.1.1 案例效果截图
8.3.1.2 案例运用到的知识点
- 核心知识点
- 通知:可以通过通知接口发送消息,终端用户可以通过通知栏查看通知内容,也可以点击通知来打开应用。
- 其他知识点
- ArkTS 语言基础
- V2版状态管理:@ComponentV2/@Local/
- Stage模型
- 自定义组件和组件生命周期
- @Builder装饰器:自定义构建函数
- @Extend装饰器:定义扩展组件样式
- if/else:条件渲染
- 内置组件:Column/Row/Text/Image/Button/Progress
- 日志管理类的编写
- 常量与资源分类的访问
- @ohos.promptAction (弹窗)
- MVVM模式
8.3.1.3 代码结构解读
├──entry/src/main/ets // 代码区
│ ├──common
│ │ ├──constants
│ │ │ └──CommonConstants.ets // 公共常量类
│ │ └──utils
│ │ ├──Logger.ets // 日志工具类
│ │ ├──NotificationUtil.ets // 通知工具类
│ │ └──ResourseUtil.ets // 资源文件工具类
│ ├──entryability
│ │ └──EntryAbility.ets // 程序入口类
│ └──pages
│ └──MainPage.ets // 主页面
└──entry/src/main/resources // 资源文件目录
8.3.1.4 公共文件与资源
本案例涉及到共常量类和日志类代码如下。
- 公共常量类(CommonConstants)
// main/ets/common/constants/CommonConstants.ets
export default class CommonConstants {
static readonly FULL_LENGTH: string = '100%'
static readonly TITLE_WIDTH: string = '86.7%'
static readonly CARD_WIDTH: string = '93.3%'
static readonly IMAGE_WEIGHT: number = 1
static readonly CARD_CONTENT_WEIGHT: number = 5
static readonly CARD_CONTENT_WIDTH: string = '70%'
static readonly CARD_IMAGE_WIDTH: string = '30%'
static readonly DOWNLOAD_FILE: string = '1653067.mp4'
static readonly FILE_SIZE: string = '25.01MB'
static readonly FONT_WEIGHT_LAGER: number = 500
static readonly FONT_OPACITY: number = 0.6
static readonly PROGRESS_TOTAL: number = 100
static readonly PROGRESS_SPEED: number = 2
static readonly UPDATE_FREQUENCY: number = 1000
static readonly NOTIFICATION_ID: number = 1000
}
export enum DOWNLOAD_STATUS {
INITIAL,
DOWNLOADING,
PAUSE,
FINISHED
}
- 日志类(Logger)
// main/ets/common/utils/Logger.ets
import { hilog } from '@kit.PerformanceAnalysisKit'
class Logger {
private domain: number
private prefix: string
private format: string = '%{public}s, %{public}s'
constructor(prefix: string = 'MyApp', domain: number = 0xFF00) {
this.prefix = prefix
this.domain = domain
}
debug(...args: string[]): void {
hilog.debug(this.domain, this.prefix, this.format, args)
}
info(...args: string[]): void {
hilog.info(this.domain, this.prefix, this.format, args)
}
warn(...args: string[]): void {
hilog.warn(this.domain, this.prefix, this.format, args)
}
error(...args: string[]): void {
hilog.error(this.domain, this.prefix, this.format, args)
}
}
export default new Logger('DownLoadNotification', 0xFF00)
本案例涉及到的资源文件如下:
- string.json
// main/resources/base/element/string.json
// main/resources/en_US/element/string.json
{
"string": [
{
"name": "module_desc",
"value": "module description"
},
{
"name": "EntryAbility_desc",
"value": "description"
},
{
"name": "EntryAbility_label",
"value": "DownloadNotification"
},
{
"name": "title",
"value": "File DownLoad"
},
{
"name": "button_download",
"value": "DownLoad"
},
{
"name": "button_pause",
"value": "Pause"
},
{
"name": "button_resume",
"value": "Resume"
},
{
"name": "button_cancel",
"value": "Cancel"
},
{
"name": "button_finish",
"value": "Open"
},
{
"name": "notification_title_download",
"value": "Downloading"
},
{
"name": "notification_title_pause",
"value": "Download paused"
},
{
"name": "notification_title_finish",
"value": "Download completed"
},
{
"name": "invalid_button_toast",
"value": "The function is not implemented"
},
{
"name": "invalid_progress_toast",
"value": "The progress bar template is not supported"
}
]
}
// main/resources/zh_CN/element/string.json
{
"string": [
{
"name": "module_desc",
"value": "模块描述"
},
{
"name": "EntryAbility_desc",
"value": "description"
},
{
"name": "EntryAbility_label",
"value": "下载通知"
},
{
"name": "title",
"value": "文件下载"
},
{
"name": "button_download",
"value": "下载"
},
{
"name": "button_pause",
"value": "暂停"
},
{
"name": "button_resume",
"value": "继续"
},
{
"name": "button_cancel",
"value": "取消"
},
{
"name": "button_finish",
"value": "打开"
},
{
"name": "notification_title_download",
"value": "下载中"
},
{
"name": "notification_title_pause",
"value": "已暂停下载"
},
{
"name": "notification_title_finish",
"value": "下载完成"
},
{
"name": "invalid_button_toast",
"value": "功能暂未实现"
},
{
"name": "invalid_progress_toast",
"value": "不支持进度条模板"
}
]
}
- float.json
// main/resources/base/element/float.json
{
"float": [
{
"name": "title_font_size",
"value": "24fp"
},
{
"name": "card_height",
"value": "108vp"
},
{
"name": "name_font_size",
"value": "16fp"
},
{
"name": "name_font_height",
"value": "16vp"
},
{
"name": "normal_font_size",
"value": "14fp"
},
{
"name": "title_margin_top",
"value": "24vp"
},
{
"name": "card_border_radius",
"value": "16vp"
},
{
"name": "card_padding",
"value": "16vp"
},
{
"name": "card_image_length",
"value": "48vp"
},
{
"name": "button_width",
"value": "72vp"
},
{
"name": "button_height",
"value": "28vp"
},
{
"name": "button_border_radius",
"value": "14vp"
},
{
"name": "button_margin",
"value": "8vp"
},
{
"name": "button_font_size",
"value": "12fp"
}
]
}
- color.json
// main/resources/base/element/color.json
{
"color": [
{
"name": "start_window_background",
"value": "#FFFFFF"
},
{
"name": "index_background_color",
"value": "#F1F3F5"
},
{
"name": "button_color",
"value": "#0A59F7"
},
{
"name": "cancel_button_color",
"value": "#0D000000"
}
]
}
8.3.1.5 发送通知
发送通知需要完成以下步骤:
- 导入通知模块,查询系统是否支持进度条模板。
- 获取点击通知拉起应用时,需要的Want信息。
- 构造进度条模板对象,并发布通知。
具体代码如下:
// main/ets/common/utils/NotificationUtil.ets
import { wantAgent, common } from '@kit.AbilityKit'
import { notificationManager } from '@kit.NotificationKit'
import CommonConstants from '../constants/CommonConstants'
import Logger from '../utils/Logger'
/**
* 创建WantAgent对象
* @param bundleName 应用包名
* @param abilityName Ability名称
* @returns 返回一个Promise,解析为WantAgent对象
*/
export function createWantAgent(bundleName: string, abilityName: string): Promise<object> {
// 定义WantAgent的配置信息
let wantAgentInfo = {
wants: [
{
bundleName: bundleName, // 设置目标应用的包名
abilityName: abilityName // 设置目标Ability的名称
}
],
operationType: wantAgent.OperationType.START_ABILITY, // 设置操作类型为启动Ability
requestCode: 0, // 设置请求码
wantAgentFlags: [wantAgent.WantAgentFlags.CONSTANT_FLAG] // 设置WantAgent的标志位
} as wantAgent.WantAgentInfo
// 调用wantAgent.getWantAgent方法创建WantAgent对象
return wantAgent.getWantAgent(wantAgentInfo)
}
/**
* 发布通知
* @param progress 下载进度
* @param title 通知标题
* @param wantAgentObj WantAgent对象,用于通知点击跳转
*/
export function publishNotification(progress: number, title: string, wantAgentObj: object) {
// 定义通知模板
let template: notificationManager.NotificationTemplate = {
name: 'downloadTemplate', // 模板名称必须为downloadTemplate
data: {
title: `${title}`, // 设置通知标题
fileName: `${title}:${CommonConstants.DOWNLOAD_FILE}`, // 设置文件名
progressValue: progress, // 设置当前进度值
progressMaxValue: CommonConstants.PROGRESS_TOTAL, // 设置进度最大值
isProgressIndeterminate: false // 设置进度条是否为不确定状态
}
}
// 定义通知请求
let notificationRequest: notificationManager.NotificationRequest = {
id: CommonConstants.NOTIFICATION_ID, // 设置通知ID
notificationSlotType: notificationManager.SlotType.CONTENT_INFORMATION, // 设置通知槽类型
template: template, // 设置通知模板
content: {
notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, // 设置通知内容类型
normal: {
title: `${title}:${CommonConstants.DOWNLOAD_FILE}`, // 设置通知标题
text: ' ', // 设置通知文本
additionalText: `${progress}%` // 设置附加文本(显示进度百分比)
}
},
wantAgent: wantAgentObj // 设置WantAgent对象
}
// 发布通知
notificationManager.publish(notificationRequest).catch((err: Error) => {
Logger.error(`[ANS] publish failed,message is ${err}`) // 如果发布失败,记录错误日志
})
}
/**
* 打开通知权限
* @param context UIAbilityContext对象
*/
export function openNotificationPermission(context: common.UIAbilityContext) {
// 请求启用通知权限
notificationManager.requestEnableNotification(context).then(() => {
Logger.info('Enable notification success') // 如果成功,记录日志
}).catch((err: Error) => {
Logger.error('Enable notification failed because ' + JSON.stringify(err)) // 如果失败,记录错误日志
})
}
关键代码说明:
createWantAgent****函数:
-
- 用于创建一个
WantAgent对象,该对象定义了点击通知后要执行的操作(如启动某个 Ability)。 - 通过
wantAgent.getWantAgent方法创建WantAgent对象。
- 用于创建一个
publishNotification****函数:
-
- 用于发布一个带有进度条的通知。
- 使用
notificationManager.NotificationTemplate定义通知模板,支持显示下载进度。 - 通过
notificationManager.publish方法发布通知。
openNotificationPermission****函数:
-
- 用于请求用户启用通知权限。
- 通过
notificationManager.requestEnableNotification方法请求权限。
8.3.1.6 模拟下载
文件下载共有四种状态,分别为初始化、下载中、暂停下载、下载完成。主要实现以下功能:
- 初始化状态,点击下载,启动Interval定时器,持续发送通知。
- 下载中,点击暂停,清除定时器,发送一次通知,显示当前进度。
- 暂停下载,点击继续,重新启动定时器,重复步骤一。
- 下载完成,清除定时器。
具体页面代码如下:
// main/ets/pages/MainPage.ets
import { common } from '@kit.AbilityKit' // 导入AbilityKit中的common模块,用于获取UIAbility上下文
import { promptAction } from '@kit.ArkUI' // 导入ArkUI中的promptAction模块,用于显示提示信息
import { notificationManager } from '@kit.NotificationKit' // 导入NotificationKit中的notificationManager模块,用于管理通知
import { createWantAgent, publishNotification, openNotificationPermission } from '../common/utils/NotificationUtil' // 导入自定义的通知工具函数
import { getStringByRes } from '../common/utils/ResourseUtil' // 导入资源工具函数,用于获取字符串资源
import Logger from '../common/utils/Logger' // 导入日志工具
import CommonConstants, { DOWNLOAD_STATUS } from '../common/constants/CommonConstants' // 导入常量定义
@Entry // 标记为入口组件
@ComponentV2 // 标记为组件
struct MainPage {
@Local downloadStatus: number = DOWNLOAD_STATUS.INITIAL // 本地状态变量,表示下载状态,初始值为INITIAL
@Local downloadProgress: number = 0 // 本地状态变量,表示下载进度,初始值为0
private context = getContext(this) as common.UIAbilityContext // 获取当前UIAbility的上下文
private isSupport: boolean = true // 是否支持下载模板通知
private notificationTitle: string = '' // 通知标题
private wantAgentObj: object = new Object() // WantAgent对象,用于通知点击后的跳转
private interval: number = -1 // 定时器ID,用于控制下载进度更新
// 生命周期函数,页面即将显示时调用
aboutToAppear() {
openNotificationPermission(this.context) // 打开通知权限
let bundleName = this.context.abilityInfo.bundleName // 获取当前应用的包名
let abilityName = this.context.abilityInfo.name // 获取当前Ability的名称
createWantAgent(bundleName, abilityName).then(want => { // 创建WantAgent
this.wantAgentObj = want // 将创建的WantAgent对象赋值给wantAgentObj
}).catch((err: Error) => {
Logger.error(`getWantAgent fail, err: ${JSON.stringify(err)}`) // 如果创建失败,记录错误日志
})
notificationManager.isSupportTemplate('downloadTemplate').then(isSupport => { // 检查是否支持下载模板通知
if (!isSupport) {
promptAction.showToast({ // 如果不支持,显示提示信息
message: $r('app.string.invalid_button_toast')
})
}
this.isSupport = isSupport // 更新isSupport状态
})
}
// 返回按钮点击事件处理函数
onBackPress() {
this.cancel() // 调用cancel方法取消下载
}
// 构建UI
build() {
Column() {
Text($r('app.string.title')) // 显示标题
.fontSize($r('app.float.title_font_size'))
.fontWeight(CommonConstants.FONT_WEIGHT_LAGER)
.width(CommonConstants.TITLE_WIDTH)
.textAlign(TextAlign.Start)
.margin({
top: $r('app.float.title_margin_top'),
bottom: $r('app.float.title_margin_top')
})
Row() {
Column() {
Image($r('app.media.ic_image')) // 显示图片
.objectFit(ImageFit.Fill)
.width($r('app.float.card_image_length'))
.height($r('app.float.card_image_length'))
}
.layoutWeight(CommonConstants.IMAGE_WEIGHT)
.height(CommonConstants.FULL_LENGTH)
.alignItems(HorizontalAlign.Start)
Column() {
Row() {
Text(CommonConstants.DOWNLOAD_FILE) // 显示下载文件名
.fontSize($r('app.float.name_font_size'))
.textAlign(TextAlign.Center)
.fontWeight(CommonConstants.FONT_WEIGHT_LAGER)
.lineHeight($r('app.float.name_font_height'))
Text(`${this.downloadProgress}%`) // 显示下载进度百分比
.fontSize($r('app.float.normal_font_size'))
.lineHeight($r('app.float.name_font_height'))
.opacity(CommonConstants.FONT_OPACITY)
}
.justifyContent(FlexAlign.SpaceBetween)
.width(CommonConstants.FULL_LENGTH)
Progress({ // 显示进度条
value: this.downloadProgress,
total: CommonConstants.PROGRESS_TOTAL
})
.width(CommonConstants.FULL_LENGTH)
Row() {
Text(CommonConstants.FILE_SIZE) // 显示文件大小
.fontSize($r('app.float.normal_font_size'))
.lineHeight($r('app.float.name_font_height'))
.opacity(CommonConstants.FONT_OPACITY)
if (this.downloadStatus === DOWNLOAD_STATUS.INITIAL) { // 根据下载状态显示不同的按钮
this.customButton($r('app.string.button_download'), (): Promise<void> => this.start())
} else if (this.downloadStatus === DOWNLOAD_STATUS.DOWNLOADING) {
Row() {
this.cancelButton()
this.customButton($r('app.string.button_pause'), (): Promise<void> => this.pause())
}
} else if (this.downloadStatus === DOWNLOAD_STATUS.PAUSE) {
Row() {
this.cancelButton()
this.customButton($r('app.string.button_resume'), (): Promise<void> => this.resume())
}
} else {
this.customButton($r('app.string.button_finish'), (): void => this.open())
}
}
.width(CommonConstants.FULL_LENGTH)
.justifyContent(FlexAlign.SpaceBetween)
}
.layoutWeight(CommonConstants.CARD_CONTENT_WEIGHT)
.height(CommonConstants.FULL_LENGTH)
.justifyContent(FlexAlign.SpaceBetween)
}
.width(CommonConstants.CARD_WIDTH)
.height($r('app.float.card_height'))
.backgroundColor(Color.White)
.borderRadius($r('app.float.card_border_radius'))
.justifyContent(FlexAlign.SpaceBetween)
.padding($r('app.float.card_padding'))
}
.width(CommonConstants.FULL_LENGTH)
.height(CommonConstants.FULL_LENGTH)
.backgroundColor($r('app.color.index_background_color'))
}
// 下载函数,使用定时器模拟下载进度
download() {
this.interval = setInterval(async () => {
if (this.downloadProgress === CommonConstants.PROGRESS_TOTAL) { // 如果下载完成
this.notificationTitle = await getStringByRes($r('app.string.notification_title_finish'), this) // 获取完成通知标题
this.downloadStatus = DOWNLOAD_STATUS.FINISHED // 更新下载状态为完成
clearInterval(this.interval) // 清除定时器
} else {
this.downloadProgress += CommonConstants.PROGRESS_SPEED // 更新下载进度
}
if (this.isSupport) {
publishNotification(this.downloadProgress, this.notificationTitle, this.wantAgentObj) // 发布通知
}
}, CommonConstants.UPDATE_FREQUENCY) // 定时器间隔时间
}
// 开始下载
async start() {
this.notificationTitle = await getStringByRes($r('app.string.notification_title_download'), this) // 获取下载通知标题
this.downloadStatus = DOWNLOAD_STATUS.DOWNLOADING // 更新下载状态为下载中
this.downloadProgress = 0 // 重置下载进度
this.download() // 调用下载函数
}
// 暂停下载
async pause() {
this.notificationTitle = await getStringByRes($r('app.string.notification_title_pause'), this) // 获取暂停通知标题
clearInterval(this.interval) // 清除定时器
this.downloadStatus = DOWNLOAD_STATUS.PAUSE // 更新下载状态为暂停
if (this.isSupport) {
publishNotification(this.downloadProgress, this.notificationTitle, this.wantAgentObj) // 发布通知
}
}
// 恢复下载
async resume() {
this.notificationTitle = await getStringByRes($r('app.string.notification_title_download'), this) // 获取下载通知标题
this.download() // 调用下载函数
this.downloadStatus = DOWNLOAD_STATUS.DOWNLOADING // 更新下载状态为下载中
}
// 取消下载
async cancel() {
this.downloadProgress = 0 // 重置下载进度
clearInterval(this.interval) // 清除定时器
this.downloadStatus = DOWNLOAD_STATUS.INITIAL // 更新下载状态为初始状态
notificationManager.cancel(CommonConstants.NOTIFICATION_ID) // 取消通知
}
// 打开文件
open() {
promptAction.showToast({ // 显示提示信息
message: $r('app.string.invalid_button_toast')
})
}
// 自定义按钮
@Builder
customButton(textResource: Resource, click: Function = () => {
}) {
Button(textResource)
.backgroundColor($r('app.color.button_color'))
.buttonsStyle()
.onClick(() => {
click()
})
}
// 取消按钮
@Builder
cancelButton() {
Button($r('app.string.button_cancel'))
.buttonsStyle()
.backgroundColor($r('app.color.cancel_button_color'))
.fontColor($r('app.color.button_color'))
.margin({ right: $r('app.float.button_margin') })
.onClick(() => {
this.cancel()
})
}
}
// 扩展Button组件样式
@Extend(Button)
function buttonsStyle() {
.constraintSize({ minWidth: $r('app.float.button_width') })
.height($r('app.float.button_height'))
.borderRadius($r('app.float.button_border_radius'))
.fontSize($r('app.float.button_font_size'))
}
关键代码说明:
- UI部分:
-
- 使用了
Column、Row、Text、Image、Progress等组件来构建页面布局。 - 根据
downloadStatus的不同状态,显示不同的按钮(下载、暂停、继续、取消、完成)。
- 使用了
- 逻辑部分:
-
aboutToAppear:页面显示时初始化通知权限、WantAgent,并检查是否支持下载模板通知。download:使用setInterval模拟下载进度,并根据进度更新通知。start、pause、resume、cancel:分别处理下载的开始、暂停、继续和取消操作。open:处理下载完成后的操作(目前只是显示一个提示)。
- 工具函数:
-
createWantAgent、publishNotification、openNotificationPermission:用于创建WantAgent、发布通知和打开通知权限。getStringByRes:用于获取资源文件中的字符串。
- 样式扩展:
-
buttonsStyle:扩展了Button组件的样式,统一了按钮的宽度、高度、圆角和字体大小。
8.3.1.7 工具函数
该函数是一个通用的工具函数,用于从资源文件中获取字符串资源。通过异步方式获取资源,确保在资源加载完成后再进行后续操作。同时,加入了错误处理机制,增强了代码的健壮性。
import Logger from '../utils/Logger' // 导入日志工具,用于记录错误信息
/**
* 根据资源ID获取字符串资源
* @param resource 资源对象,包含资源ID等信息
* @param component 组件对象,用于获取上下文
* @returns 返回获取到的字符串资源,如果资源无效则返回空字符串
*/
export async function getStringByRes(resource: Resource, component: Object): Promise<string> {
// 检查资源是否有效
if (!resource) {
Logger.error('getStringByRes resource is invalid') // 如果资源无效,记录错误日志
return '' // 返回空字符串
}
// 获取组件上下文中的资源管理器,并调用getStringValue方法获取字符串资源
let string = await getContext(component).resourceManager.getStringValue(resource.id)
return string // 返回获取到的字符串
}
8.3.1.8 代码与视频教程
完整案例代码与视频教程请参见:
代码:Code-08-01.zip。
视频:《实现进度条通知》。
8.3.2 实现验证码登录
本案例基于窗口能力,实现验证码登录的场景,主要完成以下功能:
- 登录页面主窗口实现沉浸式。
- 输入用户名和密码后,拉起验证码校验子窗口。
- 验证码校验成功后,主窗口跳转到应用首页。
8.3.2.1 案例效果截图
8.3.2.2 案例运用到的知识点
- 核心知识点
- 主窗口:应用主窗口用于显示应用界面,会在“任务管理界面”显示。
- 子窗口:应用子窗口用于显示应用的弹窗、悬浮窗等辅助窗口。
- 沉浸式:指对状态栏、导航栏等系统窗口进行控制,减少状态栏导航栏等系统界面的突兀感,从而使用户获得最佳体验的能力。
- 其他知识点
- ArkTS 语言基础
- V2版状态管理:@ComponentV2/@Local/@Param/@Monitor
- 自定义组件和组件生命周期
- @Builder装饰器:自定义构建函数
- @Extend装饰器:定义扩展组件样式
- ForEach:循环渲染
- if/else:条件渲染
- 内置组件:Column/Row/Scroll/Swiper/Grid/Stack/Text/TextInput/Image/Button/Blank/Line/Rect
- 日志管理类的编写
- 常量与资源分类的访问
- 组件导航 (Navigation)
- 页面路由 (@ohos.router)
- MVVM模式
8.3.2.3 代码结构解读
├──entry/src/main/ets // 代码区
│ ├──common
│ │ ├──constants
│ │ │ └──CommonConstants.ets // 公共常量类
│ │ └──utils
│ │ ├──GlobalContext.ets // 全局上下文
│ │ └──Logger.ets // 公共日志类
│ ├──entryability
│ │ └──EntryAbility.ets // 程序入口类
│ ├──model
│ │ └──WindowModel.ets // 应用后端数据管理类
│ ├──pages
│ │ ├──HomePage.ets // 登录之后的首页
│ │ ├──LoginPage.ets // 登录页面
│ │ ├──SuccessPage.ets // 验证码校验成功页面
│ │ └──VerifyPage.ets // 输入验证码页面
│ └──viewmodel
│ ├──GridItem.ets // 首页网格数据实体类
│ ├──WindowViewModel.ets // 应用界面数据管理类
│ └──VerifyItem.ets // 验证码数据实体类
└──entry/src/main/resources // 资源文件目录
8.3.2.4 公共文件与资源
本案例涉及到的公共文件包括公共常量类、全局上下文类、日志类三个部分。
- 公共常量类(CommonConstants)
// main/ets/common/constants/CommonConstants.ets
export default class CommonConstants {
static readonly INPUT_ACCOUNT_LENGTH: number = 11
static readonly INPUT_PASSWORD_LENGTH: number = 8
static readonly COMMON_SPACE: number = 12
static readonly LOGIN_WAIT_TIME: number = 2000
static readonly SUCCESS_PAGE_URL: string = 'pages/SuccessPage'
static readonly HOME_PAGE_URL: string = 'pages/HomePage'
static readonly LOGIN_PAGE_URL: string = 'pages/LoginPage'
static readonly VERIFY_PAGE_URL: string = 'pages/VerifyPage'
static readonly HOME_PAGE_ACTION: string = 'startHomePage'
static readonly SUB_WINDOW_NAME: string = 'subWindow'
static readonly STATUS_BAR_COLOR: string = '#F1F3F5'
static readonly STATUS_BAR_CONTENT_COLOR: string = '#000000'
static readonly SUB_WINDOW_WIDTH_RATIO: number = 0.93
static readonly SUB_WINDOW_ASPECT_RATIO: number = 1.25
static readonly FULL_PARENT: string = '100%'
static readonly BUTTON_WIDTH: string = '86.7%'
static readonly GRID_FOUR_COLUMNS: string = '1fr 1fr 1fr 1fr'
static readonly GRID_TWO_ROWS: string = '1fr 1fr'
static readonly GRID_TWO_COLUMNS: string = '1fr 1fr'
static readonly GRID_THREE_ROWS: string = '1fr 1fr 1fr'
}
- 全局上下文单例模式类(GlobalContext)
// main/ets/common/utils/GlobalContext.ets
export class GlobalContext {
private constructor() {
}
private static instance: GlobalContext
private _objects = new Map<string, Object>()
public static getContext(): GlobalContext {
if (!GlobalContext.instance) {
GlobalContext.instance = new GlobalContext()
}
return GlobalContext.instance
}
getObject(value: string): Object | undefined {
return this._objects.get(value)
}
setObject(key: string, objectClass: Object): void {
this._objects.set(key, objectClass)
}
}
- 日志类(Logger)
import { hilog } from '@kit.PerformanceAnalysisKit'
export class Logger {
private domain: number
private prefix: string
private format: string = '%{public}s, %{public}s'
constructor(prefix: string = 'WindowManager', domain: number = 0xFF00) {
this.prefix = prefix
this.domain = domain
}
debug(...args: string[]): void {
hilog.debug(this.domain, this.prefix, this.format, args)
}
info(...args: string[]): void {
hilog.info(this.domain, this.prefix, this.format, args)
}
warn(...args: string[]): void {
hilog.warn(this.domain, this.prefix, this.format, args)
}
error(...args: string[]): void {
hilog.error(this.domain, this.prefix, this.format, args)
}
}
export default new Logger()
本案例涉及到的资源文件如下:
- string.json
// main/resources/base/element/string.json
// main/resources/en_US/element/string.json
{
"string": [
{
"name": "module_desc",
"value": "module description"
},
{
"name": "EntryAbility_desc",
"value": "description"
},
{
"name": "EntryAbility_label",
"value": "WindowManager"
},
{
"name": "login_page",
"value": "Login page"
},
{
"name": "login_more",
"value": "Log in to your account to use more services"
},
{
"name": "account",
"value": "Account"
},
{
"name": "password",
"value": "Password"
},
{
"name": "verify",
"value": "Login via Verification Code"
},
{
"name": "register_account",
"value": "Registering an Account"
},
{
"name": "forgot_password",
"value": "Forgot Password"
},
{
"name": "message_login",
"value": "Login via SMS"
},
{
"name": "change_one",
"value": "Change One"
},
{
"name": "verify_input_placeholder",
"value": "Please enter the verification code"
},
{
"name": "verify_wrong_hints",
"value": "Incorrect verification code. Please try again"
},
{
"name": "verify_hints",
"value": "To protect your network security, please enter the verification code"
},
{
"name": "verify_ok",
"value": "OK"
},
{
"name": "success",
"value": "Verification success"
},
{
"name": "success_login_hints",
"value": "The login page is automatically displayed in 2 seconds"
},
{
"name": "my_love",
"value": "My favorite"
},
{
"name": "history_record",
"value": "History"
},
{
"name": "message",
"value": "Message"
},
{
"name": "shopping_cart",
"value": "Shopping cart"
},
{
"name": "my_goal",
"value": "My goal"
},
{
"name": "group",
"value": "Group"
},
{
"name": "favorites",
"value": "Favorites"
},
{
"name": "recycle_bin",
"value": "Recycle Bin"
},
{
"name": "home_top",
"value": "Rankings"
},
{
"name": "home_text_top",
"value": "XiaMen Station, we'll see you."
},
{
"name": "home_new",
"value": "New product launch"
},
{
"name": "home_text_new",
"value": "XiaMen Station, we'll see you."
},
{
"name": "home_brand",
"value": "Big-name flash shopping"
},
{
"name": "home_text_brand",
"value": "More Big Names"
},
{
"name": "home_found",
"value": "Discover the good stuff"
},
{
"name": "home_text_found",
"value": "XiaMen Station, we'll see you."
},
{
"name": "home_list",
"value": "List"
},
{
"name": "home_title",
"value": "Home"
}
]
}
// main/resources/zh_CN/element/string.json
{
"string": [
{
"name": "module_desc",
"value": "模块描述"
},
{
"name": "EntryAbility_desc",
"value": "描述"
},
{
"name": "EntryAbility_label",
"value": "窗口管理"
},
{
"name": "login_page",
"value": "登录界面"
},
{
"name": "login_more",
"value": "登录帐号以使用更多服务"
},
{
"name": "account",
"value": "帐号"
},
{
"name": "password",
"value": "密码"
},
{
"name": "verify",
"value": "验证码检测"
},
{
"name": "register_account",
"value": "注册帐号"
},
{
"name": "forgot_password",
"value": "忘记密码"
},
{
"name": "message_login",
"value": "短信验证码登录"
},
{
"name": "change_one",
"value": "换一张"
},
{
"name": "verify_input_placeholder",
"value": "请输入验证码"
},
{
"name": "verify_wrong_hints",
"value": "验证码错误,请重新输入"
},
{
"name": "verify_hints",
"value": "为了保护你的网络安全,请输入验证码"
},
{
"name": "verify_ok",
"value": "确定"
},
{
"name": "success",
"value": "验证成功"
},
{
"name": "success_login_hints",
"value": "2s后自动跳转登录"
},
{
"name": "my_love",
"value": "我的最爱"
},
{
"name": "history_record",
"value": "历史记录"
},
{
"name": "message",
"value": "消息"
},
{
"name": "shopping_cart",
"value": "购物车"
},
{
"name": "my_goal",
"value": "我的目标"
},
{
"name": "group",
"value": "圈子"
},
{
"name": "favorites",
"value": "收藏"
},
{
"name": "recycle_bin",
"value": "回收站"
},
{
"name": "home_top",
"value": "排行榜"
},
{
"name": "home_text_top",
"value": "厦门站,我们不见不散"
},
{
"name": "home_new",
"value": "新品首发"
},
{
"name": "home_text_new",
"value": "厦门站,我们不见不散"
},
{
"name": "home_brand",
"value": "大牌闪购"
},
{
"name": "home_text_brand",
"value": "更多大牌"
},
{
"name": "home_found",
"value": "发现好物"
},
{
"name": "home_text_found",
"value": "厦门站,我们不见不散"
},
{
"name": "home_list",
"value": "列表"
},
{
"name": "home_title",
"value": "首页"
}
]
}
- float.json
// main/resources/base/element/float.json
{
"float": [
{
"name": "logo_image_size",
"value": "78vp"
},
{
"name": "logo_margin_top",
"value": "125vp"
},
{
"name": "logo_margin_bottom",
"value": "9vp"
},
{
"name": "page_title_text_size",
"value": "24fp"
},
{
"name": "normal_text_size",
"value": "16fp"
},
{
"name": "large_text_size",
"value": "21fp"
},
{
"name": "big_text_size",
"value": "16fp"
},
{
"name": "small_text_size",
"value": "14fp"
},
{
"name": "little_text_size",
"value": "12fp"
},
{
"name": "login_more_margin_bottom",
"value": "24vp"
},
{
"name": "login_more_margin_top",
"value": "8vp"
},
{
"name": "login_input_height",
"value": "48vp"
},
{
"name": "forgot_margin_top",
"value": "12vp"
},
{
"name": "forgot_margin",
"value": "12vp"
},
{
"name": "input_padding",
"value": "12vp"
},
{
"name": "line_height",
"value": "0.5vp"
},
{
"name": "line_margin",
"value": "12vp"
},
{
"name": "login_button_height",
"value": "40vp"
},
{
"name": "login_button_margin_top",
"value": "231vp"
},
{
"name": "login_register_margin_bottom",
"value": "40vp"
},
{
"name": "login_button_margin_bottom",
"value": "12vp"
},
{
"name": "login_page_padding_bottom",
"value": "24vp"
},
{
"name": "login_background_opacity",
"value": "0.2"
},
{
"name": "login_padding",
"value": "12vp"
},
{
"name": "verify_padding",
"value": "24vp"
},
{
"name": "verify_image_height",
"value": "48vp"
},
{
"name": "verify_image_width",
"value": "164vp"
},
{
"name": "verify_hints_width",
"value": "88vp"
},
{
"name": "verify_hints_padding",
"value": "40vp"
},
{
"name": "verify_text_input_margin",
"value": "8vp"
},
{
"name": "verify_ok_margin",
"value": "32vp"
},
{
"name": "verify_bottom_padding",
"value": "12vp"
},
{
"name": "verify_auto_jump_bottom_margin",
"value": "24vp"
},
{
"name": "success_image_size",
"value": "56vp"
},
{
"name": "success_image_margin",
"value": "32vp"
},
{
"name": "home_padding",
"value": "12vp"
},
{
"name": "home_tab_titles_margin",
"value": "12vp"
},
{
"name": "home_tab_titles_padding",
"value": "12vp"
},
{
"name": "home_swiper_border_radius",
"value": "16vp"
},
{
"name": "home_swiper_margin",
"value": "24vp"
},
{
"name": "home_grid_columns_gap",
"value": "8vp"
},
{
"name": "home_grid_row_gap",
"value": "12vp"
},
{
"name": "home_grid_padding",
"value": "12vp"
},
{
"name": "home_grid_height",
"value": "124vp"
},
{
"name": "background_text_margin",
"value": "4vp"
},
{
"name": "home_list_padding",
"value": "8vp"
},
{
"name": "background_border_radius",
"value": "24vp"
},
{
"name": "home_text_margin",
"value": "12vp"
},
{
"name": "home_background_border_radius",
"value": "12vp"
},
{
"name": "home_second_grid_height",
"value": "406vp"
},
{
"name": "home_home_cell_size",
"value": "24vp"
},
{
"name": "home_home_cell_margin",
"value": "4vp"
},
{
"name": "home_button_bottom",
"value": "55vp"
}
]
}
- color.json
// main/resources/base/element/color.json
{
"color": [
{
"name": "start_window_background",
"value": "#FFFFFF"
},
{
"name": "background",
"value": "#F1F3F5"
},
{
"name": "title_text_color",
"value": "#182431"
},
{
"name": "login_more_text_color",
"value": "#99182431"
},
{
"name": "placeholder_color",
"value": "#99182431"
},
{
"name": "line_color",
"value": "#0D000000"
},
{
"name": "login_button_color",
"value": "#007DFF"
},
{
"name": "login_button_disable",
"value": "#66007DFF"
},
{
"name": "login_font_disable",
"value": "#FFFFFF"
},
{
"name": "login_blue_text_color",
"value": "#007DFF"
},
{
"name": "verify_wrong_hints_color",
"value": "#FFFA2A2D"
},
{
"name": "home_grid_font_color",
"value": "#99182431"
}
]
}
8.3.2.5 页面沉浸式设置
- EntryAbility
// main/ets/entryability/EntryAbility.ets
import { UIAbility } from '@kit.AbilityKit'
import { window } from '@kit.ArkUI'
import CommonConstants from '../common/constants/CommonConstants'
import Logger from '../common/utils/Logger'
import WindowModel from '../model/WindowModel'
// 定义EntryAbility类,继承自UIAbility
export default class EntryAbility extends UIAbility {
// 当窗口阶段创建时调用
onWindowStageCreate(windowStage: window.WindowStage) {
Logger.info('Ability onWindowStageCreate')
// 获取WindowModel的单例实例
let windowModel = WindowModel.getInstance()
// 设置当前窗口阶段
windowModel.setWindowStage(windowStage)
// 设置主窗口为沉浸式(全屏模式)
windowModel.setMainWindowImmersive()
// 加载指定页面(LoginPage)的内容
windowStage.loadContent(CommonConstants.LOGIN_PAGE_URL, (err) => {
if (err.code) {
Logger.error(`Failed to load the content. Code:${err.code},message:${err.message}`)
return
}
})
}
}
关键代码说明:
- 在EntryAbility里的onWindowStageCreate生命周期函数里,获取WindowModel的单例实例。
- 设置当前窗口阶段。
- 设置主窗口为沉浸式(全屏模式)。
- 加载LoginPage页面。
- WindowModel
// main/ets/model/WindowModel.ets
import { window, display } from '@kit.ArkUI' // 导入ArkUI的window和display模块,用于窗口管理和屏幕信息获取
import CommonConstants from '../common/constants/CommonConstants' // 导入公共常量
import Logger from '../common/utils/Logger' // 导入日志工具
import { GlobalContext } from '../common/utils/GlobalContext' // 导入全局上下文工具
import { BusinessError } from '@kit.BasicServicesKit' // 导入业务错误类型
// 定义WindowModel类,用于管理窗口的创建、配置和销毁
export default class WindowModel {
private windowStage?: window.WindowStage // 当前窗口阶段实例
private subWindowClass?: window.Window // 子窗口实例
// 获取WindowModel的单例实例
static getInstance(): WindowModel {
let instance: WindowModel = GlobalContext.getContext().getObject('windowModel') as WindowModel
if (instance === undefined) { // 如果实例不存在,则创建一个新的实例
instance = new WindowModel()
GlobalContext.getContext().setObject('windowModel', instance) // 将实例存储到全局上下文中
}
return instance
}
// 设置当前窗口阶段
setWindowStage(windowStage: window.WindowStage) {
this.windowStage = windowStage
}
// 创建子窗口
// ...
// 设置主窗口为沉浸式模式
setMainWindowImmersive() {
if (this.windowStage === undefined) { // 如果窗口阶段未定义,记录错误日志并返回
Logger.error('windowStage is undefined.')
return
}
// 获取主窗口实例
this.windowStage.getMainWindow((err, windowClass: window.Window) => {
if (err.code) { // 如果获取失败,记录错误日志并返回
Logger.error(`Failed to obtain the main window. Code:${err.code}, message:${err.message}`)
return
}
let isLayoutFullScreen = true
try {
// 设置窗口布局为全屏模式
let promise = windowClass.setWindowLayoutFullScreen(isLayoutFullScreen)
promise.then(() => {
Logger.info('Succeeded in setting the window layout to full-screen mode.')
}).catch((err: BusinessError) => { // 如果设置失败,记录错误日志
Logger.error(`Failed to set the window layout to full-screen mode. Cause code: ${err.code}, message: ${err.message}`)
})
} catch (exception) { // 如果捕获到异常,记录错误日志
Logger.error(`Failed to set the window layout to full-screen mode. Cause code: ${exception.code}, message: ${exception.message}`)
}
let names: Array<'status' | 'navigation'> = []
try {
// 设置系统栏(状态栏和导航栏)为不可见
let promise = windowClass.setWindowSystemBarEnable(names)
promise.then(() => {
Logger.info('Succeeded in setting the system bar to be invisible.')
}).catch((err: BusinessError) => { // 如果设置失败,记录错误日志
Logger.error(`Failed to set the system bar to be invisible. Cause code: ${err.code}, message: ${err.message}`)
})
} catch (exception) { // 如果捕获到异常,记录错误日志
Logger.error(`Failed to set the system bar to be invisible. Cause code: ${exception.code}, message: ${exception.message}`)
}
})
}
// 销毁子窗口
// ...
}
关键代码说明:
- 本部分代码只实现了EntryAbility需要的功能。
- 实现了定义WindowModel类。
- 实现了获取WindowModel的单例实例的getInstance方法。
- 实现了设置当前窗口阶段setWindowStage方法。
- 实现了设置主窗口为沉浸式模式setMainWindowImmersive方法。
8.3.2.6 登录页面
- LoginPage
// main/ets/pages/LoginPage.ets
import router from '@ohos.router'
import CommonConstants from '../common/constants/CommonConstants'
import Logger from '../common/utils/Logger'
import WindowModel from '../model/WindowModel'
// 定义一个扩展函数,用于设置TextInput的样式
@Extend(TextInput)
function inputStyle() {
.placeholderColor($r('app.color.placeholder_color'))
.backgroundColor($r('app.color.start_window_background'))
.height($r('app.float.login_input_height'))
.fontSize($r('app.float.big_text_size'))
.padding({
left: $r('app.float.input_padding'),
right: $r('app.float.input_padding')
})
}
// 定义一个扩展函数,用于设置Text的样式
@Extend(Text)
function blueTextStyle() {
.fontColor($r('app.color.login_blue_text_color'))
.fontSize($r('app.float.small_text_size'))
.fontWeight(FontWeight.Medium)
.margin({
left: $r('app.float.forgot_margin'),
right: $r('app.float.forgot_margin')
})
}
// 定义登录页面组件
@Entry
@ComponentV2
struct LoginPage {
@Local account: string = '' // 本地状态,存储账号
@Local password: string = '' // 本地状态,存储密码
@Local isShadow: boolean = false // 本地状态,控制阴影显示
private windowModel: WindowModel = WindowModel.getInstance() // 获取窗口模型实例
@Local isHome: boolean = false // 本地状态,控制是否跳转到主页
// 生命周期函数,页面即将显示时调用
aboutToAppear() {
// 监听HOME_PAGE_ACTION事件,当事件触发时设置isHome为true
getContext(this).eventHub.on(CommonConstants.HOME_PAGE_ACTION, () => {
this.isHome = true
})
}
// 构建UI
build() {
Stack({ alignContent: Alignment.Top }) {
Column() {
Image($r('app.media.ic_logo'))
.width($r('app.float.logo_image_size'))
.height($r('app.float.logo_image_size'))
.margin({
top: $r('app.float.logo_margin_top'),
bottom: $r('app.float.logo_margin_bottom')
})
Text($r('app.string.login_page'))
.fontSize($r('app.float.page_title_text_size'))
.fontWeight(FontWeight.Medium)
.fontColor($r('app.color.title_text_color'))
Text($r('app.string.login_more'))
.fontSize($r('app.float.normal_text_size'))
.fontColor($r('app.color.login_more_text_color'))
.margin({
bottom: $r('app.float.login_more_margin_bottom'),
top: $r('app.float.login_more_margin_top')
})
Column() {
TextInput({ placeholder: $r('app.string.account') }) // 账号输入框
.maxLength(CommonConstants.INPUT_ACCOUNT_LENGTH)
.inputStyle()
.onChange((value: string) => { // 监听输入变化
this.account = value // 更新账号状态
})
Line() // 显示分割线
.width(CommonConstants.FULL_PARENT)
.height($r('app.float.line_height'))
.backgroundColor($r('app.color.line_color'))
.margin({
left: $r('app.float.line_margin'),
right: $r('app.float.line_margin')
})
TextInput({ placeholder: $r('app.string.password') }) // 密码输入框
.maxLength(CommonConstants.INPUT_PASSWORD_LENGTH)
.type(InputType.Password)
.inputStyle()
.onChange((value: string) => { // 监听输入变化
this.password = value // 更新密码状态
})
}
.padding({
top: $r('app.float.background_text_margin'),
bottom: $r('app.float.background_text_margin')
})
.width(CommonConstants.FULL_PARENT)
.backgroundColor($r('app.color.start_window_background'))
.borderRadius($r('app.float.background_border_radius'))
Row() {
Text($r('app.string.message_login')).blueTextStyle()
Text($r('app.string.forgot_password')).blueTextStyle()
}
.justifyContent(FlexAlign.SpaceBetween)
.width(CommonConstants.FULL_PARENT)
.margin({ top: $r('app.float.forgot_margin_top') })
Blank() // 空白占位符
Button($r('app.string.verify'), { type: ButtonType.Capsule }) // 显示验证按钮
.width(CommonConstants.BUTTON_WIDTH)
.height($r('app.float.login_button_height'))
.fontSize($r('app.float.normal_text_size'))
.fontWeight(FontWeight.Medium)
.enabled(isLoginClickable(this.account, this.password))
.backgroundColor(isLoginClickable(this.account, this.password) ? $r('app.color.login_button_color') :
$r('app.color.login_button_disable'))
.fontColor(isLoginClickable(this.account, this.password) ? Color.White : $r('app.color.login_font_disable'))
.margin({
top: $r('app.float.login_button_margin_top'),
bottom: $r('app.float.login_button_margin_bottom')
})
.onClick(() => { // 监听按钮点击事件
this.windowModel.createSubWindow() // 创建子窗口
this.isShadow = true // 显示阴影
})
Text($r('app.string.register_account'))
.fontColor($r('app.color.login_blue_text_color'))
.fontSize($r('app.float.normal_text_size'))
.fontWeight(FontWeight.Medium)
.margin({ bottom: $r('app.float.login_register_margin_bottom') })
}
.backgroundColor($r('app.color.background'))
.height(CommonConstants.FULL_PARENT)
.width(CommonConstants.FULL_PARENT)
.padding({
left: $r('app.float.login_padding'),
right: $r('app.float.login_padding'),
bottom: $r('app.float.login_page_padding_bottom')
})
if (this.isShadow) { // 如果isShadow为true,显示阴影
Rect()
.width(CommonConstants.FULL_PARENT)
.height(CommonConstants.FULL_PARENT)
.fillOpacity($r('app.float.login_background_opacity'))
.fill($r('app.color.title_text_color'))
}
}
}
// 监听isHome状态的变化
@Monitor('isHome')
toHome(monitor: IMonitor) {
if (this.isHome) { // 如果isHome为true,跳转到主页
router.replaceUrl({
url: CommonConstants.HOME_PAGE_URL
}).catch((err: Error) => {
Logger.error(`pushUrl failed, message:${err.message}`)
})
}
}
}
// 判断登录按钮是否可点击
function isLoginClickable(account: string, password: string): boolean {
return account !== '' && password !== '' // 当账号和密码都不为空时返回true
}
关键代码说明:
- 生命周期函数aboutToAppear,注册监听了HOME_PAGE_ACTION事件,当事件触发时设置isHome为true。
- this.windowModel.createSubWindow(),创建了子窗口,用于显示验证页面和验证成功页面。
- Rect()在isShadow为true时,显示一个模态窗口的蒙版。
- @Monitor('isHome'),用于监听isHome状态的变化,如果isHome为true,则跳转到HomePage页面。
- EntryAbility
import { window, display } from '@kit.ArkUI'
import CommonConstants from '../common/constants/CommonConstants'
import Logger from '../common/utils/Logger'
import { GlobalContext } from '../common/utils/GlobalContext'
import { BusinessError } from '@kit.BasicServicesKit'
// 定义WindowModel类,用于管理窗口的创建、配置和销毁
export default class WindowModel {
private windowStage?: window.WindowStage // 当前窗口阶段实例
private subWindowClass?: window.Window // 子窗口实例
// 获取WindowModel的单例实例
// ...
// 设置当前窗口阶段
// ...
// 创建子窗口
createSubWindow() {
if (this.windowStage === undefined) { // 如果窗口阶段未定义,记录错误日志并返回
Logger.error('Failed to create the subWindow.')
return
}
// 创建子窗口
this.windowStage.createSubWindow(CommonConstants.SUB_WINDOW_NAME, (err, data: window.Window) => {
if (err.code) { // 如果创建失败,记录错误日志并返回
Logger.error(`Failed to create the window. Code:${err.code}, message:${err.message}`)
return
}
// 获取子窗口实例
this.subWindowClass = data
// 获取屏幕的宽度和高度
let screenWidth = display.getDefaultDisplaySync().width
let screenHeight = display.getDefaultDisplaySync().height
// 根据子窗口的宽高比计算子窗口的宽度和高度
let windowWidth = screenWidth * CommonConstants.SUB_WINDOW_WIDTH_RATIO
let windowHeight = windowWidth / CommonConstants.SUB_WINDOW_ASPECT_RATIO
// 计算子窗口的起始坐标
let moveX = (screenWidth - windowWidth) / 2
let moveY = screenHeight - windowHeight
// 移动子窗口到指定位置
this.subWindowClass.moveWindowTo(moveX, moveY, (err) => {
if (err.code) { // 如果移动失败,记录错误日志并返回
Logger.error(`Failed to move the window. Code:${err.code}, message:${err.message}`)
return
}
})
// 调整子窗口的大小
this.subWindowClass.resize(windowWidth, windowHeight, (err) => {
if (err.code) { // 如果调整大小失败,记录错误日志并返回
Logger.error(`Failed to change the window size. Code:${err.code}, message:${err.message}`)
return
}
})
// 设置子窗口的内容
this.subWindowClass.setUIContent(CommonConstants.VERIFY_PAGE_URL, (err) => {
if (err.code) { // 如果设置内容失败,记录错误日志并返回
Logger.error(`Failed to load the content. Code:${err.code}, message:${err.message}`)
return
}
if (this.subWindowClass === undefined) { // 如果子窗口实例未定义,记录错误日志并返回
Logger.error('subWindowClass is undefined.')
return
}
// 设置子窗口的背景颜色为透明
this.subWindowClass.setWindowBackgroundColor('#00000000')
// 显示子窗口
this.subWindowClass.showWindow((err) => {
if (err.code) { // 如果显示失败,记录错误日志并返回
Logger.error(`Failed to show the window. Code:${err.code}, essage:${err.message}`)
return
}
})
})
})
}
// 设置主窗口为沉浸式模式
// ...
// 销毁子窗口
// ...
}
关键代码说明:
- createSubWindow方法实现了创建子窗口。
- setUIContent设置了子窗口的内容为VerifyPage页面。
8.3.2.7 校验子窗口页面
- 校验页面
// main/ets/pages/VerifyPage.ets
import { router } from '@kit.ArkUI'
import CommonConstants from '../common/constants/CommonConstants'
import Logger from '../common/utils/Logger'
import VerifyItem from '../viewmodel/VerifyItem' // 导入验证项数据模型
import WindowViewModel from '../viewmodel/WindowViewModel' // 导入窗口视图模型
// 定义一个扩展函数,用于设置Text的样式
@Extend(Text)
function promptTextStyle() {
.fontSize($r('app.float.small_text_size'))
.width(CommonConstants.FULL_PARENT)
.padding({
left: $r('app.float.verify_padding'),
right: $r('app.float.verify_padding')
})
}
// 定义验证页面组件
@Entry
@ComponentV2
struct VerifyPage {
@Local isInputWrong: boolean = false
@Local verifyItem: VerifyItem = new VerifyItem($r('app.media.ic_verity_character1'), 'XYZK')
@Local inputText: string = ''
private verifyMap: Map<number, VerifyItem> = new Map()
private imageId: number = 0
// 生命周期函数,页面即将显示时调用
aboutToAppear() {
// 从WindowViewModel获取验证项映射表
this.verifyMap = WindowViewModel.getVerifyMap()
// 更新当前验证项
this.updateVerifyItem()
}
// 更新当前验证项
updateVerifyItem(){
// 根据imageId获取新的验证项
let verifyItemNew: VerifyItem | undefined = this.verifyMap.get(this.imageId)
if (verifyItemNew !== undefined) {
// 更新当前验证项
this.verifyItem = verifyItemNew
}
}
build() {
Column() {
Column() {
Row() {
Image(this.verifyItem.image) // 显示验证图片
.height($r('app.float.verify_image_height'))
.width($r('app.float.verify_image_width'))
Text($r('app.string.change_one'))
.fontColor($r('app.color.login_blue_text_color'))
.fontSize($r('app.float.big_text_size'))
.width($r('app.float.verify_hints_width'))
.textAlign(TextAlign.Center)
.onClick(() => { // 监听点击事件
this.imageId = (this.imageId + 1) % 2 // 切换验证项ID
this.updateVerifyItem() // 更新验证项
})
}
.padding({
left: $r('app.float.verify_padding'),
top: $r('app.float.verify_padding'),
right: $r('app.float.verify_hints_padding')
})
.width(CommonConstants.FULL_PARENT)
.justifyContent(FlexAlign.SpaceBetween)
// 显示输入框
TextInput({ placeholder: $r('app.string.verify_input_placeholder'), text: this.inputText })
.placeholderColor($r('app.color.placeholder_color'))
.fontSize($r('app.float.big_text_size'))
.height($r('app.float.login_button_height'))
.margin({
left: $r('app.float.verify_padding'),
right: $r('app.float.verify_padding'),
top: $r('app.float.verify_text_input_margin'),
bottom: $r('app.float.verify_text_input_margin')
})
.onChange((value: string) => {
this.inputText = value
})
if (this.isInputWrong) { // 如果输入错误
Text($r('app.string.verify_wrong_hints'))
.fontColor($r('app.color.verify_wrong_hints_color'))
.promptTextStyle()
} else { // 如果输入正确
Text($r('app.string.verify_hints'))
.fontColor($r('app.color.placeholder_color'))
.promptTextStyle()
}
Text($r('app.string.verify_ok')) // 显示“确定”按钮
.fontSize($r('app.float.big_text_size'))
.fontColor($r('app.color.login_blue_text_color'))
.margin({
top: $r('app.float.verify_ok_margin'),
bottom: $r('app.float.verify_ok_margin')
})
.onClick(() => { // 监听点击事件
let verifyText: string = this.verifyItem.characters
// 获取验证项的正确字符
if (this.inputText.toLowerCase() === verifyText.toLowerCase()) { // 如果输入正确
router.replaceUrl({ // 跳转到成功页面
url: CommonConstants.SUCCESS_PAGE_URL
}).catch((err: Error) => { // 如果跳转失败,记录错误日志
Logger.error(`pushUrl failed, message:${err.message}`)
})
} else { // 如果输入错误
this.isInputWrong = true // 设置输入错误状态为true
this.inputText = '' // 清空输入内容
this.imageId = (this.imageId + 1) % 2 // 切换验证项ID
this.updateVerifyItem() // 更新验证项
}
})
}
.backgroundColor(Color.White)
.height(CommonConstants.FULL_PARENT)
.borderRadius($r('app.float.verify_ok_margin'))
}
.padding({ bottom: $r('app.float.verify_bottom_padding') })
.height(CommonConstants.FULL_PARENT)
}
}
关键代码说明:
- verifyMap: Map<number, VerifyItem>,定义验证Map。
- 在aboutToAppear里从WindowViewModel获取验证项映射表WindowViewModel.getVerifyMap()。
- updateVerifyItem函数更新验证,
- this.inputText.toLowerCase() === verifyText.toLowerCase() 获取验证项的正确字符,跳转到成功页面SuccessPage。
- 验证项数据模型
export default class VerifyItem {
image: Resource
characters: string
constructor(image: Resource, characters: string) {
this.image = image
this.characters = characters
}
}
- 窗口视图模型
// main/ets/viewmodel/WindowViewModel.ets
// ...
import VerifyItem from './VerifyItem'
export class WindowViewModel {
getVerifyMap(): Map<number, VerifyItem> {
let verifyMap: Map<number, VerifyItem> = new Map()
verifyMap.set(0, new VerifyItem($r('app.media.ic_verity_character1'), 'XYZK'))
verifyMap.set(1, new VerifyItem($r('app.media.ic_verity_character2'), 'LHBR'))
return verifyMap
}
// ...
}
export default new WindowViewModel()
8.3.2.8 校验成功页面
// main/ets/pages/SuccessPage.ets
import CommonConstants from '../common/constants/CommonConstants'
import WindowModel from '../model/WindowModel'
@Entry
@ComponentV2
struct SuccessPage {
// 生命周期函数,页面即将显示时调用
aboutToAppear() {
// 设置一个定时器,延迟执行以下操作
setTimeout(() => {
// 销毁子窗口
WindowModel.getInstance().destroySubWindow()
// 触发HOME_PAGE_ACTION事件
getContext(this).eventHub.emit(CommonConstants.HOME_PAGE_ACTION)
}, CommonConstants.LOGIN_WAIT_TIME)
}
build() {
Column() {
Column() {
Text($r('app.string.success')) // 显示“成功”文本
.fontSize($r('app.float.large_text_size'))
.padding({
left: $r('app.float.verify_padding'),
top: $r('app.float.verify_padding')
})
.width(CommonConstants.FULL_PARENT)
Image($r('app.media.ic_success')) // 显示成功图片
.width($r('app.float.success_image_size'))
.height($r('app.float.success_image_size'))
.margin({
top: $r('app.float.success_image_margin'),
bottom: $r('app.float.success_image_margin')
})
Text($r('app.string.success_login_hints')) // 显示成功登录提示
.fontSize($r('app.float.small_text_size'))
.fontColor($r('app.color.placeholder_color'))
.margin({
bottom: $r('app.float.verify_auto_jump_bottom_margin')
})
}
.backgroundColor(Color.White)
.height(CommonConstants.FULL_PARENT)
.borderRadius($r('app.float.verify_ok_margin'))
}
.padding({ bottom: $r('app.float.verify_bottom_padding') })
.height(CommonConstants.FULL_PARENT)
}
}
关键代码说明:
- 在aboutToAppear里,两秒后自动销毁子窗口并触发打开首页面(HomePage)的事件。
- WindowModel.getInstance().destroySubWindow() 销毁子窗口。
- getContext(this).eventHub.emit(CommonConstants.HOME_PAGE_ACTION),触发HOME_PAGE_ACTION事件。
8.3.2.9 首页面
- HomePage
// main/ets/pages/HomePage.ets
import CommonConstants from '../common/constants/CommonConstants'
import ItemData from '../viewmodel/GridItem'
import WindowViewModel from '../viewmodel/WindowViewModel'
@Entry
@ComponentV2
struct HomePage {
private swiperController: SwiperController = new SwiperController()
// 定义导航栏标题的UI构建函数
@Builder
NavigationTitle() {
Text($r('app.string.home_title'))
.fontWeight(FontWeight.Medium)
.fontSize($r('app.float.page_title_text_size'))
.margin({ top: $r('app.float.home_tab_titles_margin') })
.padding({ left: $r('app.float.home_tab_titles_padding') })
}
build() {
Navigation() { // 使用Navigation组件作为页面容器
Scroll() { // 使用Scroll组件实现页面滚动
Column({ space: CommonConstants.COMMON_SPACE }) {
// 轮播图部分
Swiper(this.swiperController) {
ForEach(WindowViewModel.getSwiperImages(), (img: Resource, index?: number) => {
Image(img).borderRadius($r('app.float.home_swiper_border_radius'))
}, (img: Resource, index?: number) => index + JSON.stringify(img.id))
}
.margin({
top: $r('app.float.home_swiper_margin')
})
.autoPlay(true)
// 第一个Grid部分
Grid() {
ForEach(WindowViewModel.getFirstGridData(), (firstItem: ItemData, index?: number) => {
GridItem() {
Column() {
Image(firstItem.image)
.width($r('app.float.home_home_cell_size'))
.height($r('app.float.home_home_cell_size'))
Text(firstItem.title)
.fontSize($r('app.float.little_text_size'))
.margin({ top: $r('app.float.home_home_cell_margin') })
}
}
}, (firstItem: ItemData, index?: number) => index + JSON.stringify(firstItem))
}
.columnsTemplate(CommonConstants.GRID_FOUR_COLUMNS)
.rowsTemplate(CommonConstants.GRID_TWO_ROWS)
.columnsGap($r('app.float.home_grid_columns_gap'))
.rowsGap($r('app.float.home_grid_row_gap'))
.padding({
top: $r('app.float.home_grid_padding'),
bottom: $r('app.float.home_grid_padding')
})
.height($r('app.float.home_grid_height'))
.backgroundColor(Color.White)
.borderRadius($r('app.float.background_border_radius'))
// 列表标题
Text($r('app.string.home_list'))
.fontSize($r('app.float.normal_text_size'))
.fontWeight(FontWeight.Medium)
.width(CommonConstants.FULL_PARENT)
.margin({ top: $r('app.float.home_text_margin') })
// 第二个Grid部分
Grid() {
ForEach(WindowViewModel.getSecondGridData(), (secondItem: ItemData, index?: number) => {
GridItem() {
Column() {
Text(secondItem.title) // 显示标题
.fontSize($r('app.float.normal_text_size'))
.fontWeight(FontWeight.Medium)
Text(secondItem.others)
.margin({ top: $r('app.float.background_text_margin') })
.fontSize($r('app.float.little_text_size'))
.fontColor($r('app.color.home_grid_font_color'))
}
.alignItems(HorizontalAlign.Start)
}
.padding({
top: $r('app.float.home_list_padding'),
left: $r('app.float.home_list_padding')
})
.borderRadius($r('app.float.home_background_border_radius'))
.align(Alignment.TopStart)
.backgroundImage(secondItem.image)
.backgroundImageSize(ImageSize.Cover)
.width(CommonConstants.FULL_PARENT)
.height(CommonConstants.FULL_PARENT)
}, (secondItem: ItemData, index?: number) => index + JSON.stringify(secondItem))
}
.width(CommonConstants.FULL_PARENT)
.height($r('app.float.home_second_grid_height'))
.columnsTemplate(CommonConstants.GRID_TWO_COLUMNS)
.rowsTemplate(CommonConstants.GRID_THREE_ROWS)
.columnsGap($r('app.float.home_grid_columns_gap'))
.rowsGap($r('app.float.home_grid_row_gap'))
.margin({ bottom: $r('app.float.home_button_bottom') })
}
}
.scrollBar(BarState.Off)
.margin({
left: $r('app.float.home_padding'),
right: $r('app.float.home_padding')
})
}
.title(this.NavigationTitle()) // 设置导航栏标题
.titleMode(NavigationTitleMode.Mini) // 设置导航栏标题模式为Mini
.hideBackButton(true) // 隐藏返回按钮
.backgroundColor($r('app.color.background')) // 设置背景颜色
.mode(NavigationMode.Stack) // 设置导航模式为Stack
}
}
- 构建首页面的VM
// main/ets/viewmodel/WindowViewModel.ets
import GridItem from './GridItem'
// ...
export class WindowViewModel {
// ...
getSwiperImages(): Array<Resource> {
let swiperImages: Resource[] = [
$r('app.media.ic_swiper1'),
$r('app.media.ic_swiper2'),
$r('app.media.ic_swiper3'),
$r('app.media.ic_swiper4')
]
return swiperImages
}
getFirstGridData(): Array<GridItem> {
let firstGridData: GridItem[] = [
new GridItem($r('app.string.my_love'), $r('app.media.ic_love')),
new GridItem($r('app.string.history_record'), $r('app.media.ic_record')),
new GridItem($r('app.string.message'), $r('app.media.ic_message')),
new GridItem($r('app.string.shopping_cart'), $r('app.media.ic_shopping')),
new GridItem($r('app.string.my_goal'), $r('app.media.ic_target')),
new GridItem($r('app.string.group'), $r('app.media.ic_circle')),
new GridItem($r('app.string.favorites'), $r('app.media.ic_favorite')),
new GridItem($r('app.string.recycle_bin'), $r('app.media.ic_recycle'))
]
return firstGridData
}
getSecondGridData(): Array<GridItem> {
let secondGridData: GridItem[] = [
new GridItem($r('app.string.home_top'), $r('app.media.ic_top'), $r('app.string.home_text_top')),
new GridItem($r('app.string.home_new'), $r('app.media.ic_new'), $r('app.string.home_text_new')),
new GridItem($r('app.string.home_brand'), $r('app.media.ic_brand'), $r('app.string.home_text_brand')),
new GridItem($r('app.string.home_found'), $r('app.media.ic_found'), $r('app.string.home_text_found')),
new GridItem($r('app.string.home_top'), $r('app.media.ic_top'), $r('app.string.home_text_top')),
new GridItem($r('app.string.home_new'), $r('app.media.ic_new'), $r('app.string.home_text_new'))
]
return secondGridData
}
}
export default new WindowViewModel()
8.3.2.10 代码与视频教程
完整案例代码与视频教程请参见:
代码:Code-08-02.zip。
视频:《实现验证码登录》。
8.4 本章小节
本章重点介绍了 HarmonyOS 开发中通知与窗口管理的关键知识。在通知领域,从基础概念到复杂操作,涵盖了通知发布、管理的全流程,开发者可依据不同场景灵活选择和处理通知,如进度条通知在文件下载场景的应用。窗口管理部分,清晰区分了系统窗口与应用窗口,详细说明了主窗口和子窗口的设置方式以及沉浸式效果的实现,这些能力为优化用户界面体验提供了有力支持,像验证码登录案例中登录页面的沉浸式设计和子窗口的运用。通过理论与案例结合的方式,开发者能够深入理解并将这些知识运用到实际开发中,打造出功能丰富、体验良好的应用程序,满足用户多样化的需求,提升应用在 HarmonyOS 生态中的竞争力。