-
在HarmonyOS应用开发中,尤其是涉及复杂模块化或多任务流设计的应用,启动应用内的不同UIAbility组件是核心场景。然而,开发者在实际操作中常因对启动模式、生命周期、参数传递等机制理解不透彻,导致应用出现白屏、重复创建、无法跳转指定页面、冷启动性能低下等问题。本技术总结旨在系统性剖析典型问题,并提供清晰的解决方案技术难点总结1.1 问题说明在开发和测试应用内UIAbility启动时,主要面临以下几种典型问题:1. 启动缓慢与白屏:应用冷启动耗时过长,导致用户在启动阶段长时间看到白屏或启动图标,影响用户体验。使用性能分析工具(如Profiler)分析启动轨迹时,可能发现耗时主要集中在“UI Ability Launching”或“UI Ability OnForeground”阶段。2. 指定页面跳转失败:从EntryAbility启动另一个FuncAbility时,期望直接跳转到特定页面,但实际效果却始终显示默认首页,或出现页面路由错误。3. 重复创建多个实例:在某些文档或任务管理类的业务场景中,期望重复打开同一份文档时复用同一个UIAbility实例,但实际每次startAbility都会创建一个全新的实例,导致最近任务列表中出现多个相同的任务,占用额外内存并可能导致数据不一致4. 期望进入后台后启动新实例变为Home界面:期望应用在后台运行时,再次启动能展示另一个UIAbility(如FuncAbility)作为主界面,但启动后仍然是原来的UIAbility(EntryAbility)界面。5. 分屏状态丢失:在平板或2in1设备的分屏状态下,从一个UIAbility启动另一个UIAbility,新启动的窗口未继承原有窗口的分屏模式,变成了全屏显示,破坏了用户的多任务操作体验。6. 状态栏样式异常:应用启用了沉浸式状态栏,但当应用切换到后台(如进入多任务视图)时,状态栏颜色未恢复为系统默认样式,在多任务界面显示异常。1.2 原因分析以上问题的根源可以归纳为以下几个方面:1.启动性能低下:在onCreate、onWindowStageCreate、onForeground等生命周期回调中,同步执行了过重的初始化逻辑或耗时操作(如大量文件I/O、复杂计算、同步网络请求),阻塞了主线程,延迟了页面加载和首帧渲染。2.生命周期回调与页面加载时机错位:· 冷启动时,错误地在onCreate中进行页面路由跳转:onCreate时期WindowStage尚未创建,因此UIContext或Router对象不可用,此时调用router.pushUrl等操作无效或会导致异常。· 热启动时,未能正确处理onNewWant回调:当UIAbility实例已存在并被再次启动时,系统不会调用onCreate和onWindowStageCreate,而是直接触发onNewWant。若开发者仅在冷启动路径中处理跳转逻辑,热启动时就会失效。3.启动模式配置不当:· launchType配置为默认的 “singleton” (单实例模式),会导致所有启动请求都复用同一个UIAbility实例,无法满足“每个文档一个实例”的需求。· 未根据需要正确配置“specified” (指定实例模式) 或 “multiton” (多实例模式)。4.实例启动与页面加载逻辑耦合:在单个UIAbility中通过修改全局变量来动态指定启动页面的方案简陋且不可靠。应用进入后台时(如触发onBackground),试图通过在该回调内再次启动一个新UIAbility并销毁自己的方式切换主界面,逻辑混乱,不符合系统调度预期,且易引发生命周期错乱。5.窗口属性继承问题:使用startAbility()启动新的UIAbility实例时,默认不会自动继承调用方窗口的显示模式(如分屏、自由窗口等)。若未在Want参数或启动选项中明确指定窗口属性,新窗口会以默认全屏模式启动。6.状态栏生命周期管理缺失:应用在onWindowStageCreate等回调中设置了沉浸式状态栏,但没有在窗口状态变化(如从前台切换到后台BACKGROUND或INACTIVE)时,相应地恢复系统默认状态栏样式。1.3 解决思路整体的优化和处理思路遵循以下原则:1.性能优化异步化:确保所有生命周期回调,尤其是onCreate、onForeground,只执行轻量级、必要的初始化逻辑。耗时任务必须使用TaskPool、Worker或Promise/async/await异步执行,或延迟到应用启动后的空闲时段处理。2.生命周期与职责分离:· 冷启动路径:在onCreate中解析Want参数并保存;在onWindowStageCreate中,根据已解析的参数,通过1.windowStage.loadContent()加载不同的页面。· 热启动路径:在onNewWant中,再次解析Want参数,并通过已获取的UIContext.getRouter()进行页面跳转。3.精准选用启动模式:· 全局唯一功能:使用singleton。· 每次启动都独立:使用multiton。· 需要根据业务标识(如文件路径)复用实例:使用specified,并配合AbilityStage.onAcceptWant实现自定义匹配逻辑。4.使用标准启动与页面跳转机制:通过Want参数的parameters字段,传递目标页面路由信息。在目标UIAbility中,严格按照冷启动和热启动两条路径来处理该参数并跳转至指定页面。5.明确指定窗口状态:在跨UIAbility启动时,若需保持特定窗口模式(如分屏),应通过StartOptions或windowMode等参数(根据具体API)进行显式设置。6.完整监听和管理窗口状态:在设置沉浸式等自定义UI样式后,需监听窗口生命周期事件(‘inactive’, ‘background’等),并在应用转入后台时,主动调用相关API恢复系统默认样式。1.4 解决方案方案一:优化启动性能在onCreate中异步执行耗时任务:import { UIAbility, AbilityConstant, Want } from '@kit.AbilityKit';import { hilog } from '@kit.PerformanceAnalysisKit';import { taskpool } from '@kit.ArkTS';export default class EntryAbility extends UIAbility { onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) { hilog.info(0x0000, 'testTag', 'onCreate'); // 将同步的耗时任务改为异步 this.asyncInitializeData(); // 如果需要更彻底的并发,可使用taskPool // taskpool.execute(new taskpool.Task(this.heavyCompute.bind(this), ...args)); } private async asyncInitializeData() { // 模拟异步初始化 await new Promise(resolve => setTimeout(resolve, 100)); // 延迟加载,非阻塞主线程 // ... 初始化数据 }}方案二:正确实现冷/热启动指定页面跳转目标UIAbility (FuncAbility) 的正确实现:import { AbilityConstant, Want, UIAbility } from '@kit.AbilityKit';import { hilog } from '@kit.PerformanceAnalysisKit';import { window, UIContext, Router } from '@kit.ArkUI';import { BusinessError } from '@kit.BasicServicesKit';const TAG: string = '[FuncAbility]';const DOMAIN_NUMBER: number = 0xFF00;export default class FuncAbility extends UIAbility { funcAbilityWant: Want | undefined = undefined; // 存储从want中解析的参数 uiContext: UIContext | undefined = undefined; //【冷启动路径】 onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { hilog.info(DOMAIN_NUMBER, TAG, 'Cold Start onCreate'); // 1. 在onCreate中仅解析和保存参数 this.funcAbilityWant = want; // 可以存储参数到AppStorage等全局状态管理,也可以在onWindowStageCreate中使用 // 例如:let targetPage = want.parameters?.router; // ... } onWindowStageCreate(windowStage: window.WindowStage): void { hilog.info(DOMAIN_NUMBER, TAG, 'onWindowStageCreate'); this.uiContext = windowStage.getUIContext(); // 2. 在onWindowStageCreate中根据保存的参数加载指定页面 let targetPageUrl = 'pages/Index'; // 默认页面 if (this.funcAbilityWant?.parameters?.router) { // 根据调用方传递的参数决定加载哪个页面 switch (this.funcAbilityWant.parameters.router) { case 'funcA': targetPageUrl = 'pages/Page_FuncA'; break; case 'funcB': targetPageUrl = 'pages/Page_FuncB'; break; } } windowStage.loadContent(targetPageUrl, (err) => { if (err.code) { hilog.error(DOMAIN_NUMBER, TAG, `Failed to load content. Code: ${err.code}`); return; } hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded in loading the content.'); }); } //【热启动路径】 onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void { hilog.info(DOMAIN_NUMBER, TAG, 'Hot Start onNewWant'); // 解析新的参数 if (want?.parameters?.router && want.parameters.router === 'funcA') { // 确保uiContext已获取 (通常在冷启动的onWindowStageCreate中赋值) if (this.uiContext) { let router: Router = this.uiContext.getRouter(); router.pushUrl({ url: 'pages/Page_FuncA' }) .catch((err: BusinessError) => { hilog.error(DOMAIN_NUMBER, TAG, `Failed to push url. Code: ${err.code}`); }); } } }}方案三:使用指定实例模式(specified)实现文档级实例复用配置module.json5{ "module": { "abilities": [{ "name": "DocAbility", "srcEntry": "./ets/docability/DocAbility.ets", "launchType": "specified", // 关键配置 // ... }]}}调用方启动时传入唯一标识 (如文件路径)import { common, Want } from '@kit.AbilityKit';let context: common.UIAbilityContext = ...; // 获取UIAbilityContextlet want: Want = { bundleName: 'com.example.myapp', abilityName: 'DocAbility', parameters: { instanceKey: '/documents/note1.txt' // 唯一标识,如文件路径 }};context.startAbility(want);实现AbilityStage.onAcceptWant()进行实例匹配import { AbilityStage, Want } from '@kit.AbilityKit';export default class MyAbilityStage extends AbilityStage { onAcceptWant(want: Want): string { if (want.abilityName === 'DocAbility') { const key = want.parameters?.instanceKey; if (key) { return `DocAbilityInstance_${key}`; // 返回匹配Key } } return ''; }}系统会用onAcceptWant返回的字符串去匹配已存在的UIAbility实例。匹配则热启动 (onNewWant),不匹配则冷启动新实例 (onCreate)方案四:解决分屏状态丢失在启动新UIAbility时,通过Want或StartOptions指定窗口模式。虽然文档片段未直接给出相关API示例,但思路是:需要在启动前,获取当前窗口的模式信息。构造Want时,在parameters或StartOptions中携带windowMode等信息。目标UIAbility在onWindowStageCreate中,可以从want.parameters解析并尝试设置相应窗口模式(具体API需查阅最新文档如WindowMode相关)。方案四:解决分屏状态丢失在启动新UIAbility时,通过Want或StartOptions指定窗口模式。虽然文档片段未直接给出相关API示例,但思路是:需要在启动前,获取当前窗口的模式信息。构造Want时,在parameters或StartOptions中携带windowMode等信息。目标UIAbility在onWindowStageCreate中,可以从want.parameters解析并尝试设置相应窗口模式(具体API需查阅最新文档如WindowMode相关)。1.5 结果展示通过实施上述解决方案,能够:显著提升启动性能:将耗时操作从主线程生命周期回调中剥离后,冷启动耗时可从秒级降至毫秒级,有效减少白屏时间,提升用户体验。实现精准页面导航:无论是冷启动还是热启动,都能根据调用方传递的参数,准确跳转到指定的页面,功能逻辑清晰可靠。实现精细化的实例管理:利用specified模式,能够完美支持文档类应用的业务需求——新建文档创建新实例,打开已有文档复用旧实例,既保证了数据隔离,又优化了内存使用和用户体验。保障跨UIAbility交互的体验一致性:通过显式传递和管理窗口状态,确保了在分屏等多窗口场景下,应用内各UIAbility的视觉和交互行为保持一致。建立标准化开发范式:为团队提供了明确的、符合HarmonyOS框架设计理念的UIAbility启动与通信的最佳实践,包括生命周期管理、参数传递、性能优化等,极大降低了后续开发同类功能的认知成本和调试时间,提升了代码的可维护性和复用性。
-
一、项目概述1.1 功能特性● 基于HarmonyOS 4.0+ API实现● 动态投票按钮与进度条动画● 实时投票数据可视化● 流畅的点击反馈动画● 支持多人参与投票统计● 完整的投票结果展示二、架构设计2.1 核心组件结构投票动效系统├── VoteAnimation.ets (主组件)├── VoteButton.ets (投票按钮)├── ProgressBar.ets (进度条)├── VoteResult.ets (投票结果)└── VoteManager.ets (投票管理)2.2 数据模型定义// VoteModel.ets// 投票选项export interface VoteOption {id: string;text: string;color: ResourceColor;count: number;percentage: number;selected: boolean;}// 投票数据export interface VoteData {id: string;question: string;options: VoteOption[];totalVotes: number;selectedOptionId?: string;endTime?: number; // 结束时间戳maxVotes?: number; // 最大投票数}// 动画配置export interface VoteAnimationConfig {buttonScale: number; // 按钮点击缩放比例animationDuration: number; // 动画时长progressAnimationDuration: number; // 进度条动画时长rippleEffect: boolean; // 涟漪效果showPercentage: boolean; // 显示百分比showVoteCount: boolean; // 显示投票数autoUpdate: boolean; // 自动更新投票数据}// 默认配置export class VoteDefaultConfig {static readonly DEFAULT_CONFIG: VoteAnimationConfig = {buttonScale: 0.95,animationDuration: 300,progressAnimationDuration: 800,rippleEffect: true,showPercentage: true,showVoteCount: true,autoUpdate: true};}这里定义了投票系统的核心数据模型。VoteOption接口包含投票选项的所有属性,包括颜色、票数和百分比。VoteData接口管理整个投票数据。VoteAnimationConfig定义动画相关的配置参数。三、核心实现3.1 投票按钮组件// VoteButton.ets@Componentexport struct VoteButton {@Prop option: VoteOption;@Prop isSelected: boolean = false;@Prop isAnimating: boolean = false;@Prop config: VoteAnimationConfig = VoteDefaultConfig.DEFAULT_CONFIG;@State private scale: number = 1;@State private rippleScale: number = 0;@State private rippleOpacity: number = 0;private animationController: animation.Animator = new animation.Animator();private rippleController: animation.Animator = new animation.Animator();// 点击动画private animateClick(): void {this.animationController.stop();this.animationController.update({ duration: this.config.animationDuration, curve: animation.Curve.EaseOut }); this.animationController.onFrame((progress: number) => { this.scale = 1 - (1 - this.config.buttonScale) * progress; }); this.animationController.onFinish(() => { this.scale = 1; }); this.animationController.play(); // 涟漪效果 if (this.config.rippleEffect) { this.animateRipple(); }}// 涟漪动画private animateRipple(): void {this.rippleController.stop();this.rippleScale = 0; this.rippleOpacity = 0.6; this.rippleController.update({ duration: 600, curve: animation.Curve.EaseOut }); this.rippleController.onFrame((progress: number) => { this.rippleScale = 1 + progress * 2; this.rippleOpacity = 0.6 * (1 - progress); }); this.rippleController.onFinish(() => { this.rippleScale = 0; this.rippleOpacity = 0; }); this.rippleController.play();}VoteButton组件实现投票按钮,包含点击缩放动画和涟漪效果。animateClick方法处理按钮点击动画,animateRipple方法实现涟漪扩散效果。// 构建按钮内容@Builderprivate buildButtonContent() {Row({ space: 8 }) {// 选项文本Text(this.option.text).fontSize(18).fontColor(this.isSelected ? Color.White : this.option.color).fontWeight(FontWeight.Bold).layoutWeight(1).textAlign(TextAlign.Start) // 投票数 if (this.config.showVoteCount) { Text(this.option.count.toString()) .fontSize(16) .fontColor(this.isSelected ? Color.White : '#666666') } // 百分比 if (this.config.showPercentage && this.option.percentage > 0) { Text(`${this.option.percentage}%`) .fontSize(14) .fontColor(this.isSelected ? Color.White : this.option.color) .opacity(0.8) } } .padding({ left: 20, right: 20 })}// 构建涟漪效果@Builderprivate buildRippleEffect() {if (!this.config.rippleEffect || this.rippleScale === 0) return;Circle() .width(100) .height(100) .fill(Color.White) .opacity(this.rippleOpacity) .scale({ x: this.rippleScale, y: this.rippleScale }) .position({ x: '50%', y: '50%' })}buildButtonContent方法构建按钮文本内容,包括选项文本、投票数和百分比。buildRippleEffect方法构建涟漪效果,使用白色圆形扩散动画。build() {Stack({ alignContent: Alignment.Center }) {// 按钮背景Row().width(‘100%’).height(60).backgroundColor(this.isSelected ? this.option.color : Color.Transparent).border({width: 2,color: this.option.color,style: BorderStyle.Solid}).borderRadius(30).shadow(this.isSelected ? {radius: 10,color: this.option.color,offsetX: 0,offsetY: 4} : null) // 按钮内容 this.buildButtonContent() // 涟漪效果 this.buildRippleEffect() } .width('100%') .height(60) .scale({ x: this.scale, y: this.scale }) .animation({ duration: this.config.animationDuration, curve: animation.Curve.EaseInOut }) .onClick(() => { if (!this.isAnimating) { this.animateClick(); } })}}build方法创建按钮完整布局,包含背景、内容和涟漪效果。选中状态时显示阴影效果,点击时触发动画。3.2 进度条组件// ProgressBar.ets@Componentexport struct ProgressBar {@Prop option: VoteOption;@Prop isSelected: boolean = false;@Prop config: VoteAnimationConfig = VoteDefaultConfig.DEFAULT_CONFIG;@State private progress: number = 0;@State private width: number = 0;private animationController: animation.Animator = new animation.Animator();aboutToAppear() {this.animateProgress();}aboutToUpdate() {this.animateProgress();}ProgressBar组件显示投票进度条,包含动态宽度动画。@State装饰器管理进度和宽度状态。// 进度条动画private animateProgress(): void {this.animationController.stop();const targetWidth = this.option.percentage; this.animationController.update({ duration: this.config.progressAnimationDuration, curve: animation.Curve.EaseOut }); this.animationController.onFrame((progress: number) => { this.progress = targetWidth * progress; this.width = this.progress; }); this.animationController.play();}// 构建进度条@Builderprivate buildProgressBar() {Row({ space: 0 }) {// 进度条Row().width(${this.width}%).height(‘100%’).backgroundColor(this.option.color).borderRadius(20).animation({duration: this.config.progressAnimationDuration,curve: animation.Curve.EaseOut}) // 剩余部分 Row() .layoutWeight(1) .height('100%') .backgroundColor('#F0F0F0') .borderRadius({ topRight: 20, bottomRight: 20 }) } .width('100%') .height(40) .borderRadius(20) .clip(true)}animateProgress方法执行进度条动画,平滑过渡到目标百分比。buildProgressBar方法构建进度条,使用两个Row分别显示已投票部分和剩余部分。// 构建进度信息@Builderprivate buildProgressInfo() {Row({ space: 12 }) {// 选项文本Text(this.option.text).fontSize(16).fontColor(‘#333333’).fontWeight(this.isSelected ? FontWeight.Bold : FontWeight.Normal) // 百分比 if (this.config.showPercentage) { Text(`${this.option.percentage}%`) .fontSize(14) .fontColor(this.option.color) .fontWeight(FontWeight.Bold) } // 投票数 if (this.config.showVoteCount) { Text(`${this.option.count}票`) .fontSize(12) .fontColor('#666666') .layoutWeight(1) .textAlign(TextAlign.End) } } .width('100%') .margin({ top: 8 })}build() {Column({ space: 4 }) {this.buildProgressBar()this.buildProgressInfo()}.width(‘100%’)}}buildProgressInfo方法构建进度条下方的文本信息,包括选项名称、百分比和票数。build方法组合进度条和文本信息。3.3 主组件 - 投票动效// VoteAnimation.ets@Entry@Componentexport struct VoteAnimation {@State private voteData: VoteData = {id: ‘vote_1’,question: ‘大家冬天喜欢早起还是晚起呀’,options: [{id: ‘option_1’,text: ‘早起’,color: ‘#FF6B6B’,count: 4,percentage: 22,selected: false},{id: ‘option_2’,text: ‘晚起’,color: ‘#4D94FF’,count: 14,percentage: 78,selected: true}],totalVotes: 18,selectedOptionId: ‘option_2’};@State private isVoting: boolean = false;@State private showResult: boolean = false;@State private isAnimating: boolean = false;private config: VoteAnimationConfig = {…VoteDefaultConfig.DEFAULT_CONFIG,buttonScale: 0.95,progressAnimationDuration: 1000,rippleEffect: true};// 模拟投票数据private mockVotes = [{ optionId: ‘option_1’, userId: ‘user_1’, timestamp: Date.now() - 3600000 },{ optionId: ‘option_2’, userId: ‘user_2’, timestamp: Date.now() - 3500000 },{ optionId: ‘option_2’, userId: ‘user_3’, timestamp: Date.now() - 3400000 },{ optionId: ‘option_2’, userId: ‘user_4’, timestamp: Date.now() - 3300000 },{ optionId: ‘option_1’, userId: ‘user_5’, timestamp: Date.now() - 3200000 },{ optionId: ‘option_2’, userId: ‘user_6’, timestamp: Date.now() - 3100000 },{ optionId: ‘option_2’, userId: ‘user_7’, timestamp: Date.now() - 3000000 },{ optionId: ‘option_2’, userId: ‘user_8’, timestamp: Date.now() - 2900000 },{ optionId: ‘option_2’, userId: ‘user_9’, timestamp: Date.now() - 2800000 },{ optionId: ‘option_1’, userId: ‘user_10’, timestamp: Date.now() - 2700000 },{ optionId: ‘option_2’, userId: ‘user_11’, timestamp: Date.now() - 2600000 },{ optionId: ‘option_2’, userId: ‘user_12’, timestamp: Date.now() - 2500000 },{ optionId: ‘option_2’, userId: ‘user_13’, timestamp: Date.now() - 2400000 },{ optionId: ‘option_1’, userId: ‘user_14’, timestamp: Date.now() - 2300000 },{ optionId: ‘option_2’, userId: ‘user_15’, timestamp: Date.now() - 2200000 },{ optionId: ‘option_2’, userId: ‘user_16’, timestamp: Date.now() - 2100000 },{ optionId: ‘option_2’, userId: ‘user_17’, timestamp: Date.now() - 2000000 },{ optionId: ‘option_2’, userId: ‘user_18’, timestamp: Date.now() - 1900000 }];VoteAnimation是主入口组件,管理投票数据和状态。voteData包含初始投票数据,mockVotes模拟18个用户的投票数据。// 处理投票private handleVote(optionId: string): void {if (this.isVoting || this.isAnimating) return;this.isVoting = true; this.isAnimating = true; // 更新选择状态 this.voteData.options = this.voteData.options.map(option => ({ ...option, selected: option.id === optionId })); this.voteData.selectedOptionId = optionId; // 模拟投票 this.simulateVote(optionId); // 显示结果 setTimeout(() => { this.showResult = true; this.isAnimating = false; }, 800);}// 模拟投票过程private simulateVote(optionId: string): void {// 添加新投票const newVote = {optionId,userId: user_${this.voteData.totalVotes + 1},timestamp: Date.now()};this.mockVotes.push(newVote); // 重新计算统计数据 this.calculateVoteStats(); // 模拟实时更新 if (this.config.autoUpdate) { this.simulateRealTimeUpdates(); }}// 计算投票统计private calculateVoteStats(): void {const option1Votes = this.mockVotes.filter(v => v.optionId === ‘option_1’).length;const option2Votes = this.mockVotes.filter(v => v.optionId === ‘option_2’).length;const totalVotes = option1Votes + option2Votes;const option1Percentage = Math.round((option1Votes / totalVotes) * 100); const option2Percentage = 100 - option1Percentage; this.voteData = { ...this.voteData, options: [ { ...this.voteData.options[0], count: option1Votes, percentage: option1Percentage }, { ...this.voteData.options[1], count: option2Votes, percentage: option2Percentage } ], totalVotes };}handleVote方法处理投票操作,更新选择状态并模拟投票过程。simulateVote方法添加新投票并重新计算统计数据。calculateVoteStats方法计算每个选项的票数和百分比。// 模拟实时更新private simulateRealTimeUpdates(): void {// 随机添加一些延迟的投票const updateInterval = setInterval(() => {if (this.voteData.totalVotes >= 30) {clearInterval(updateInterval);return;} // 随机选择一个选项 const randomOption = Math.random() > 0.5 ? 'option_1' : 'option_2'; const newVote = { optionId: randomOption, userId: `user_${this.voteData.totalVotes + 1}`, timestamp: Date.now() }; this.mockVotes.push(newVote); this.calculateVoteStats(); }, 2000); // 每2秒更新一次 // 10秒后停止更新 setTimeout(() => { clearInterval(updateInterval); }, 10000);}// 重置投票private resetVote(): void {this.isVoting = false;this.showResult = false;this.isAnimating = false;// 重置为初始数据 this.voteData = { ...this.voteData, options: this.voteData.options.map(option => ({ ...option, selected: false })), selectedOptionId: undefined };}simulateRealTimeUpdates方法模拟实时投票更新,随机添加新的投票数据。resetVote方法重置投票状态,恢复到初始状态。// 构建投票按钮区域@Builderprivate buildVoteButtons() {Column({ space: 16 }) {// 标题Text(‘大家冬天喜欢早起还是晚起呀’).fontSize(20).fontColor(Color.Black).fontWeight(FontWeight.Bold).textAlign(TextAlign.Center).margin({ bottom: 20 }) // PK文字 Row({ space: 0 }) { VoteButton({ option: this.voteData.options[0], isSelected: this.voteData.options[0].selected, isAnimating: this.isAnimating, config: this.config }) .onClick(() => this.handleVote('option_1')) // PK标志 Text('PK') .fontSize(14) .fontColor('#FFFFFF') .backgroundColor('#FFD700') .padding({ left: 8, right: 8, top: 4, bottom: 4 }) .borderRadius(12) .margin({ left: 8, right: 8 }) VoteButton({ option: this.voteData.options[1], isSelected: this.voteData.options[1].selected, isAnimating: this.isAnimating, config: this.config }) .onClick(() => this.handleVote('option_2')) } .width('100%') .justifyContent(FlexAlign.Center) // 提示文字 if (!this.showResult) { Text('点击上方按钮,选择你的观点') .fontSize(14) .fontColor('#666666') .textAlign(TextAlign.Center) .margin({ top: 20 }) .opacity(0.8) } } .width('100%') .padding(24) .backgroundColor(Color.White) .borderRadius(16) .shadow({ radius: 10, color: '#00000010', offsetX: 0, offsetY: 4 })}buildVoteButtons方法构建投票按钮区域,包含问题标题、两个投票按钮和中间的PK标志,以及操作提示。// 构建投票结果@Builderprivate buildVoteResult() {Column({ space: 16 }) {// 标题Text(‘大家冬天喜欢早起还是晚起呀’).fontSize(20).fontColor(Color.Black).fontWeight(FontWeight.Bold).textAlign(TextAlign.Center).margin({ bottom: 20 }) // 进度条 Column({ space: 20 }) { ForEach(this.voteData.options, (option: VoteOption) => { ProgressBar({ option: option, isSelected: option.selected, config: this.config }) }) } // 投票结果信息 Row({ space: 8 }) { if (this.voteData.selectedOptionId) { const selectedOption = this.voteData.options.find( opt => opt.id === this.voteData.selectedOptionId ); if (selectedOption) { Text(`已选择"${selectedOption.text}"`) .fontSize(16) .fontColor(selectedOption.color) .fontWeight(FontWeight.Medium) } } Text(`${this.voteData.totalVotes}人参与`) .fontSize(14) .fontColor('#666666') .layoutWeight(1) .textAlign(TextAlign.End) } .width('100%') .padding({ top: 8, bottom: 8 }) // 重置按钮 Button('重新投票') .width('100%') .height(44) .backgroundColor('#F0F0F0') .fontColor('#333333') .fontSize(16) .borderRadius(22) .onClick(() => this.resetVote()) } .width('100%') .padding(24) .backgroundColor(Color.White) .borderRadius(16) .shadow({ radius: 10, color: '#00000010', offsetX: 0, offsetY: 4 })}buildVoteResult方法构建投票结果展示区域,包含进度条、选择信息和参与人数,以及重置按钮。// 构建源码链接@Builderprivate buildSourceLink() {Row({ space: 8 }) {Text(‘源码网页’).fontSize(14).fontColor(‘#4D94FF’).fontWeight(FontWeight.Medium) Image($r('app.media.external_link')) .width(16) .height(16) .fillColor('#4D94FF') } .padding({ left: 12, right: 12, top: 8, bottom: 8 }) .backgroundColor('#F0F8FF') .borderRadius(20) .onClick(() => { // 打开源码网页 console.log('打开源码网页'); })}build() {Column({ space: 20 }) {// 状态栏(模拟)Row({ space: 0 }) {Text(‘09:51’).fontSize(16).fontColor(Color.Black).fontWeight(FontWeight.Medium) Row({ space: 4 }) { Image($r('app.media.signal')) .width(16) .height(16) .fillColor(Color.Black) Text('98%') .fontSize(14) .fontColor(Color.Black) } .layoutWeight(1) .justifyContent(FlexAlign.End) } .width('100%') .padding({ left: 16, right: 16, top: 8, bottom: 8 }) // 标题栏 Row({ space: 0 }) { Text('投票动效实现案例') .fontSize(20) .fontColor(Color.Black) .fontWeight(FontWeight.Bold) this.buildSourceLink() .margin({ left: 8 }) } .width('100%') .padding({ left: 16, right: 16, top: 8, bottom: 20 }) // 投票内容 Column({ space: 20 }) { if (this.showResult) { this.buildVoteResult() } else { this.buildVoteButtons() } } .width('100%') .padding(16) .layoutWeight(1) // 底部说明 Text('基于HarmonyOS 4.0+ API实现') .fontSize(12) .fontColor('#999999') .textAlign(TextAlign.Center) .margin({ bottom: 20 }) } .width('100%') .height('100%') .backgroundColor('#F5F5F5')}}build方法创建完整的投票界面,包含状态栏、标题栏、投票内容区域和底部说明。根据showResult状态显示投票按钮或投票结果。四、高级特性4.1 实时投票更新动画// RealTimeVote.ets@Componentexport struct RealTimeVote {@State private voteCounts: Map<string, number> = new Map();@State private voteHistory: number[] = [];@State private isUpdating: boolean = false;private animationController: animation.Animator = new animation.Animator();// 添加实时投票addRealTimeVote(optionId: string): void {const currentCount = this.voteCounts.get(optionId) || 0;this.voteCounts.set(optionId, currentCount + 1);this.animateVoteUpdate(); this.updateVoteHistory();}// 投票更新动画private animateVoteUpdate(): void {this.isUpdating = true;this.animationController.stop(); this.animationController.update({ duration: 500, curve: animation.Curve.Spring }); this.animationController.onFinish(() => { this.isUpdating = false; }); this.animationController.play();}// 更新投票历史private updateVoteHistory(): void {const total = Array.from(this.voteCounts.values()).reduce((a, b) => a + b, 0);this.voteHistory.push(total);if (this.voteHistory.length > 10) { this.voteHistory.shift(); }}// 构建投票趋势图@Builderprivate buildVoteTrend() {if (this.voteHistory.length < 2) return;const maxVotes = Math.max(...this.voteHistory); const canvasWidth = 300; const canvasHeight = 100; const pointSpacing = canvasWidth / (this.voteHistory.length - 1); Canvas() .width(canvasWidth) .height(canvasHeight) .onReady((context: CanvasRenderingContext2D) => { context.clearRect(0, 0, canvasWidth, canvasHeight); // 绘制趋势线 context.beginPath(); context.strokeStyle = '#4D94FF'; context.lineWidth = 2; this.voteHistory.forEach((count, index) => { const x = index * pointSpacing; const y = canvasHeight - (count / maxVotes) * canvasHeight; if (index === 0) { context.moveTo(x, y); } else { context.lineTo(x, y); } }); context.stroke(); // 绘制数据点 this.voteHistory.forEach((count, index) => { const x = index * pointSpacing; const y = canvasHeight - (count / maxVotes) * canvasHeight; context.beginPath(); context.arc(x, y, 4, 0, Math.PI * 2); context.fillStyle = '#4D94FF'; context.fill(); }); })}}RealTimeVote组件实现实时投票更新和趋势图展示。addRealTimeVote方法添加新投票并触发动画,buildVoteTrend方法绘制投票趋势图。4.2 投票分享组件// VoteShare.ets@Componentexport struct VoteShare {@Prop voteData: VoteData;@Prop onShare?: (platform: string) => void;@State private showSharePanel: boolean = false;// 构建分享面板@Builderprivate buildSharePanel() {if (!this.showSharePanel) return;Column({ space: 16 }) { Text('分享投票结果') .fontSize(18) .fontColor(Color.Black) .fontWeight(FontWeight.Bold) Row({ space: 20 }) { // 微信分享 Column({ space: 8 }) { Circle() .width(60) .height(60) .fill('#07C160') .overlay( Image($r('app.media.wechat')) .width(32) .height(32) .fillColor(Color.White) ) Text('微信') .fontSize(12) .fontColor('#333333') } .onClick(() => { this.shareToWechat(); this.showSharePanel = false; }) // 微博分享 Column({ space: 8 }) { Circle() .width(60) .height(60) .fill('#E6162D') .overlay( Image($r('app.media.weibo')) .width(32) .height(32) .fillColor(Color.White) ) Text('微博') .fontSize(12) .fontColor('#333333') } .onClick(() => { this.shareToWeibo(); this.showSharePanel = false; }) // 复制链接 Column({ space: 8 }) { Circle() .width(60) .height(60) .fill('#4D94FF') .overlay( Image($r('app.media.link')) .width(32) .height(32) .fillColor(Color.White) ) Text('复制链接') .fontSize(12) .fontColor('#333333') } .onClick(() => { this.copyLink(); this.showSharePanel = false; }) } Button('取消') .width('100%') .backgroundColor('#F0F0F0') .fontColor('#333333') .onClick(() => { this.showSharePanel = false; }) } .width('80%') .padding(24) .backgroundColor(Color.White) .borderRadius(16) .shadow({ radius: 20, color: '#00000040', offsetX: 0, offsetY: 4 }) .position({ x: '10%', y: '30%' }) .zIndex(1000)}// 构建投票结果卡片@BuilderbuildVoteCard() {Column({ space: 12 }) {Text(this.voteData.question).fontSize(16).fontColor(Color.Black).fontWeight(FontWeight.Bold).textAlign(TextAlign.Center) ForEach(this.voteData.options, (option: VoteOption) => { Row({ space: 8 }) { Text(option.text) .fontSize(14) .fontColor('#333333') .layoutWeight(1) Text(`${option.percentage}%`) .fontSize(14) .fontColor(option.color) .fontWeight(FontWeight.Bold) Text(`${option.count}票`) .fontSize(12) .fontColor('#666666') } }) Text(`共${this.voteData.totalVotes}人参与`) .fontSize(12) .fontColor('#999999') .textAlign(TextAlign.Center) .margin({ top: 8 }) } .padding(16) .backgroundColor(Color.White) .borderRadius(12) .shadow({ radius: 8, color: '#00000020', offsetX: 0, offsetY: 2 })}build() {Stack({ alignContent: Alignment.BottomEnd }) {// 分享按钮Button(‘分享结果’).backgroundColor(‘#4D94FF’).fontColor(Color.White).padding({ left: 16, right: 16 }).borderRadius(20).onClick(() => {this.showSharePanel = true;}) // 分享面板 this.buildSharePanel() }}}VoteShare组件实现投票结果分享功能,包含分享面板和投票结果卡片。支持分享到微信、微博和复制链接。五、最佳实践5.1 性能优化建议动画优化:使用硬件加速的transform属性数据更新:避免不必要的状态更新图片资源:使用合适的图标尺寸事件节流:对频繁触发的事件进行节流性能优化包括:1)使用transform实现动画获得硬件加速;2)精确控制状态更新范围;3)优化图标资源大小;4)对点击等事件进行节流处理。5.2 用户体验优化即时反馈:点击后立即提供视觉反馈平滑过渡:所有状态变化都有动画过渡明确指引:清晰的用户操作指引错误处理:投票失败时的友好提示用户体验优化包括:1)点击按钮立即显示动画反馈;2)状态变化使用平滑动画过渡;3)提供清晰的操作指引;4)处理网络错误等异常情况。5.3 可访问性// 为屏幕阅读器提供支持.accessibilityLabel(‘投票组件’).accessibilityHint(‘点击按钮选择您的观点,查看投票结果’).accessibilityRole(AccessibilityRole.Button).accessibilityState({selected: this.option.selected,disabled: this.isAnimating})可访问性支持为视障用户提供语音反馈,描述组件功能、操作提示和当前状态。六、总结6.1 核心特性本投票动效实现案例提供了完整的投票体验,包含流畅的动画效果、实时的数据更新和美观的视觉设计,符合现代移动应用的用户体验标准。6.2 使用场景投票组件适用于多种场景:社交应用的用户兴趣调查、内容平台的用户偏好收集、教育应用的课堂互动、企业内部的决策意见征集、活动策划的参与者偏好调查等。
-
一、项目概述 1.1 功能特性 基于HarmonyOS 4.0+ API实现使用Swiper组件作为Stepper容器支持手势滑动和按钮切换步骤自定义步骤指示器和进度条流畅的页面切换动画完整的步骤状态管理二、架构设计 2.1 核心组件结构 Swiper Stepper系统├── SwiperStepper.ets (主组件)├── StepperIndicator.ets (步骤指示器)├── StepperContent.ets (步骤内容)├── StepperNavigation.ets (导航控制)└── StepperManager.ets (状态管理)2.2 数据模型定义 // SwiperStepperModel.ets// 步骤数据模型export interface StepperItem { id: string; title: string; subtitle?: string; description?: string; icon?: Resource; status: 'pending' | 'active' | 'completed' | 'error' | 'disabled'; content: CustomBuilder; // 步骤内容构建器 validate?: () => boolean; // 验证函数}// Swiper Stepper配置export interface SwiperStepperConfig { indicatorType: 'dots' | 'numbers' | 'progress' | 'custom'; // 指示器类型 indicatorPosition: 'top' | 'bottom' | 'left' | 'right'; // 指示器位置 showNavigation: boolean; // 显示导航按钮 enableSwipe: boolean; // 启用手势滑动 loop: boolean; // 循环切换 autoPlay: boolean; // 自动播放(演示用) animationDuration: number; // 动画时长 validateSteps: boolean; // 验证步骤 showStepNumbers: boolean; // 显示步骤编号}// 默认配置export class SwiperStepperDefaultConfig { static readonly DEFAULT_CONFIG: SwiperStepperConfig = { indicatorType: 'dots', indicatorPosition: 'top', showNavigation: true, enableSwipe: true, loop: false, autoPlay: false, animationDuration: 300, validateSteps: true, showStepNumbers: true };}这里定义了基于Swiper的Stepper组件数据模型。StepperItem接口包含步骤内容和验证函数。SwiperStepperConfig接口针对Swiper特性进行配置,如指示器类型、位置、循环切换等。三、核心实现 3.1 主组件 - SwiperStepper // SwiperStepper.ets@Componentexport struct SwiperStepper { // 步骤数据 @Prop items: StepperItem[] = []; // 配置 @Prop config: SwiperStepperConfig = SwiperStepperDefaultConfig.DEFAULT_CONFIG; // 事件回调 @Prop onStepChange?: (currentStep: number, previousStep: number) => void; @Prop onStepComplete?: (stepIndex: number) => void; @Prop onStepError?: (stepIndex: number) => void; @State private currentIndex: number = 0; @State private completedSteps: number[] = []; @State private errorSteps: number[] = []; private swiperController: SwiperController = new SwiperController(); private autoPlayTimer: number = 0; aboutToAppear() { if (this.config.autoPlay) { this.startAutoPlay(); } }SwiperStepper是主组件,使用SwiperController控制页面切换。@State装饰器管理当前索引和步骤状态。支持自动播放功能用于演示。// 开始自动播放 private startAutoPlay(): void { this.stopAutoPlay(); this.autoPlayTimer = setInterval(() => { if (this.currentIndex < this.items.length - 1 || this.config.loop) { this.next(); } else { this.stopAutoPlay(); } }, 3000); } // 停止自动播放 private stopAutoPlay(): void { if (this.autoPlayTimer) { clearInterval(this.autoPlayTimer); this.autoPlayTimer = 0; } } // 下一步 next(): void { if (this.currentIndex < this.items.length - 1) { // 验证当前步骤 if (this.config.validateSteps && !this.validateCurrentStep()) { return; } const previousIndex = this.currentIndex; this.currentIndex++; // 标记当前步骤为完成 this.completeStep(previousIndex); // 切换到下一步 this.swiperController.showNext(); this.onStepChange?.(this.currentIndex, previousIndex); } } // 上一步 previous(): void { if (this.currentIndex > 0) { const previousIndex = this.currentIndex; this.currentIndex--; this.swiperController.showPrevious(); this.onStepChange?.(this.currentIndex, previousIndex); } }startAutoPlay和stopAutoPlay方法控制自动播放。next方法包含步骤验证逻辑,验证通过后才切换到下一步。previous方法直接切换到上一步。// 跳转到指定步骤 goToStep(index: number): boolean { if (index < 0 || index >= this.items.length) return false; // 验证是否可以跳转 if (this.config.validateSteps && !this.canGoToStep(index)) { return false; } const previousIndex = this.currentIndex; this.currentIndex = index; this.swiperController.showIndex(index); this.onStepChange?.(this.currentIndex, previousIndex); return true; } // 验证是否可以跳转到指定步骤 private canGoToStep(targetIndex: number): boolean { // 只能跳转到已完成的步骤或相邻步骤 if (targetIndex > this.currentIndex) { for (let i = this.currentIndex + 1; i < targetIndex; i++) { if (!this.completedSteps.includes(i)) { return false; } } } return true; } // 验证当前步骤 private validateCurrentStep(): boolean { const item = this.items[this.currentIndex]; if (item.validate) { const isValid = item.validate(); if (!isValid) { this.markStepAsError(this.currentIndex); return false; } } return true; }goToStep方法支持直接跳转到指定步骤,包含跳转验证逻辑。canGoToStep方法确保只能跳转到已完成的步骤或相邻步骤。validateCurrentStep方法执行步骤验证。// 完成步骤 private completeStep(index: number): void { if (!this.completedSteps.includes(index)) { this.completedSteps = [...this.completedSteps, index]; this.onStepComplete?.(index); } } // 标记步骤为错误 private markStepAsError(index: number): void { if (!this.errorSteps.includes(index)) { this.errorSteps = [...this.errorSteps, index]; this.onStepError?.(index); } } // Swiper变化回调 private onSwiperChange(index: number): void { if (index !== this.currentIndex) { const previousIndex = this.currentIndex; this.currentIndex = index; this.onStepChange?.(this.currentIndex, previousIndex); } }completeStep和markStepAsError方法管理步骤状态。onSwiperChange方法处理Swiper页面切换事件,同步当前索引。// 构建步骤指示器 @Builder private buildStepperIndicator() { if (this.config.indicatorType === 'dots') { this.buildDotsIndicator(); } else if (this.config.indicatorType === 'numbers') { this.buildNumbersIndicator(); } else if (this.config.indicatorType === 'progress') { this.buildProgressIndicator(); } } // 构建圆点指示器 @Builder private buildDotsIndicator() { Row({ space: 8 }) { ForEach(this.items, (_, index: number) => { Circle() .width(8) .height(8) .fill(index === this.currentIndex ? '#4D94FF' : this.completedSteps.includes(index) ? '#96CEB4' : this.errorSteps.includes(index) ? '#FF6B6B' : '#E0E0E0') .animation({ duration: this.config.animationDuration, curve: animation.Curve.EaseInOut }) }) } .padding(12) .backgroundColor('#FFFFFF') .borderRadius(20) .shadow({ radius: 2, color: '#00000010', offsetX: 0, offsetY: 2 }) }buildStepperIndicator方法根据配置构建不同类型的指示器。buildDotsIndicator构建圆点指示器,不同状态使用不同颜色。// 构建数字指示器 @Builder private buildNumbersIndicator() { Row({ space: 4 }) { ForEach(this.items, (item, index: number) => { Column({ space: 2 }) { Text((index + 1).toString()) .fontSize(14) .fontColor(index === this.currentIndex ? '#FFFFFF' : this.completedSteps.includes(index) ? '#96CEB4' : this.errorSteps.includes(index) ? '#FF6B6B' : '#666666') .backgroundColor(index === this.currentIndex ? '#4D94FF' : this.completedSteps.includes(index) ? '#E8F7F6' : this.errorSteps.includes(index) ? '#FFE8E8' : '#F5F5F5') .padding({ left: 8, right: 8, top: 4, bottom: 4 }) .borderRadius(12) if (this.config.showStepNumbers && item.title) { Text(item.title) .fontSize(10) .fontColor('#666666') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) } } .width(60) }) } .padding(8) } // 构建进度条指示器 @Builder private buildProgressIndicator() { const progress = (this.currentIndex + 1) / this.items.length * 100; Column({ space: 8 }) { Row({ space: 4 }) { Text('进度') .fontSize(12) .fontColor('#666666') Text(`${Math.round(progress)}%`) .fontSize(12) .fontColor('#4D94FF') .fontWeight(FontWeight.Bold) } Row() .width('100%') .height(4) .backgroundColor('#E0E0E0') .borderRadius(2) .overlay( Row() .width(`${progress}%`) .height('100%') .backgroundColor('#4D94FF') .borderRadius(2) .animation({ duration: this.config.animationDuration, curve: animation.Curve.EaseInOut }) ) } .padding(12) .backgroundColor('#FFFFFF') .borderRadius(8) }buildNumbersIndicator构建数字指示器,显示步骤编号和标题。buildProgressIndicator构建进度条指示器,显示整体进度百分比。// 构建导航控制 @Builder private buildNavigation() { if (!this.config.showNavigation) return; Row({ space: 12 }) { Button('上一步') .layoutWeight(1) .backgroundColor(this.currentIndex > 0 ? '#4D94FF' : '#CCCCCC') .enabled(this.currentIndex > 0) .onClick(() => this.previous()) Button(this.currentIndex === this.items.length - 1 ? '完成' : '下一步') .layoutWeight(1) .backgroundColor(this.currentIndex === this.items.length - 1 ? '#96CEB4' : '#4D94FF') .onClick(() => { if (this.currentIndex === this.items.length - 1) { this.completeStep(this.currentIndex); console.log('流程完成!'); } else { this.next(); } }) } .padding(16) .backgroundColor('#FFFFFF') }buildNavigation方法构建导航按钮,根据当前步骤显示不同的按钮文本和状态。最后一步显示"完成"按钮。// 构建步骤内容 @Builder private buildStepContent(item: StepperItem, index: number) { Column({ space: 16 }) { // 步骤标题 Column({ space: 8 }) { Text(item.title) .fontSize(20) .fontColor(Color.Black) .fontWeight(FontWeight.Bold) if (item.subtitle) { Text(item.subtitle) .fontSize(14) .fontColor('#666666') } if (item.description) { Text(item.description) .fontSize(12) .fontColor('#999999') .margin({ top: 8 }) } } .width('100%') .padding(16) .backgroundColor('#F8F8F8') .borderRadius(12) // 步骤内容 item.content() } .width('100%') .height('100%') .padding(16) }buildStepContent方法构建步骤内容区域,包含标题、副标题、描述和自定义内容构建器。build() { Column() { // 步骤指示器(顶部) if (this.config.indicatorPosition === 'top') { this.buildStepperIndicator() .margin({ top: 16, bottom: 8 }) } // Swiper内容区域 Swiper(this.swiperController) { ForEach(this.items, (item: StepperItem, index: number) => { SwiperItem() { this.buildStepContent(item, index) } }) } .index(this.currentIndex) .autoPlay(this.config.autoPlay) .interval(3000) .duration(this.config.animationDuration) .loop(this.config.loop) .vertical(false) .indicator(false) // 使用自定义指示器 .enableSwipe(this.config.enableSwipe) .onChange((index: number) => { this.onSwiperChange(index); }) .layoutWeight(1) // 步骤指示器(底部) if (this.config.indicatorPosition === 'bottom') { this.buildStepperIndicator() .margin({ top: 8, bottom: 16 }) } // 导航控制 this.buildNavigation() } .width('100%') .height('100%') .backgroundColor('#F5F5F5') }}build方法创建完整的Stepper布局,根据配置显示顶部或底部指示器。Swiper组件作为步骤内容容器,支持所有Swiper原生功能。3.2 使用示例 // SwiperStepperDemo.ets@Entry@Componentexport struct SwiperStepperDemo { @State private currentStep: number = 0; // 自定义配置 private customConfig: SwiperStepperConfig = { ...SwiperStepperDefaultConfig.DEFAULT_CONFIG, indicatorType: 'numbers', indicatorPosition: 'top', enableSwipe: true, validateSteps: true }; // 步骤数据 @State private stepperItems: StepperItem[] = [ { id: '1', title: '基本信息', subtitle: '填写您的基本信息', description: '请确保信息准确无误', status: 'active', content: this.buildBasicInfoContent(), validate: () => this.validateBasicInfo() }, { id: '2', title: '详细资料', subtitle: '完善您的详细资料', description: '这些信息将用于个性化推荐', status: 'pending', content: this.buildDetailInfoContent(), validate: () => this.validateDetailInfo() }, { id: '3', title: '偏好设置', subtitle: '设置您的个人偏好', description: '根据您的喜好定制体验', status: 'pending', content: this.buildPreferenceContent(), validate: () => true }, { id: '4', title: '确认信息', subtitle: '确认所有信息正确', description: '请仔细核对以下信息', status: 'pending', content: this.buildConfirmationContent(), validate: () => true } ];SwiperStepperDemo是演示入口组件,定义自定义配置和步骤数据。每个步骤包含内容构建器和验证函数。// 构建基本信息内容 @Builder buildBasicInfoContent() { Column({ space: 16 }) { TextInput({ placeholder: '请输入姓名' }) .width('100%') .padding(12) .backgroundColor(Color.White) .borderRadius(8) .onChange((value: string) => { this.basicInfo.name = value; }) TextInput({ placeholder: '请输入邮箱' }) .width('100%') .padding(12) .backgroundColor(Color.White) .borderRadius(8) .onChange((value: string) => { this.basicInfo.email = value; }) TextInput({ placeholder: '请输入电话' }) .width('100%') .padding(12) .backgroundColor(Color.White) .borderRadius(8) .onChange((value: string) => { this.basicInfo.phone = value; }) Text('所有字段均为必填项') .fontSize(12) .fontColor('#666666') .alignSelf(ItemAlign.Start) } } // 验证基本信息 private validateBasicInfo(): boolean { const { name, email, phone } = this.basicInfo; if (!name || !email || !phone) { promptAction.showToast({ message: '请填写所有必填字段', duration: 2000 }); return false; } if (!this.isValidEmail(email)) { promptAction.showToast({ message: '请输入有效的邮箱地址', duration: 2000 }); return false; } return true; }buildBasicInfoContent方法构建基本信息表单内容。validateBasicInfo方法验证表单数据,显示提示信息。// 构建详细资料内容 @Builder buildDetailInfoContent() { Column({ space: 16 }) { Text('选择您的职业') .fontSize(16) .fontColor(Color.Black) .alignSelf(ItemAlign.Start) Radio({ value: 'student', group: 'occupation' }) .checked(true) .onChange((value: boolean) => { if (value) this.detailInfo.occupation = 'student'; }) Text('学生') Radio({ value: 'engineer', group: 'occupation' }) .onChange((value: boolean) => { if (value) this.detailInfo.occupation = 'engineer'; }) Text('工程师') Radio({ value: 'designer', group: 'occupation' }) .onChange((value: boolean) => { if (value) this.detailInfo.occupation = 'designer'; }) Text('设计师') Radio({ value: 'other', group: 'occupation' }) .onChange((value: boolean) => { if (value) this.detailInfo.occupation = 'other'; }) Text('其他') } } // 构建偏好设置内容 @Builder buildPreferenceContent() { Column({ space: 16 }) { Text('选择您感兴趣的领域(可多选)') .fontSize(16) .fontColor(Color.Black) .alignSelf(ItemAlign.Start) Checkbox({ name: 'tech', position: CheckboxPosition.Left }) .onChange((value: boolean) => { this.updatePreference('tech', value); }) Text('科技') Checkbox({ name: 'art', position: CheckboxPosition.Left }) .onChange((value: boolean) => { this.updatePreference('art', value); }) Text('艺术') Checkbox({ name: 'sports', position: CheckboxPosition.Left }) .onChange((value: boolean) => { this.updatePreference('sports', value); }) Text('体育') Checkbox({ name: 'music', position: CheckboxPosition.Left }) .onChange((value: boolean) => { this.updatePreference('music', value); }) Text('音乐') } }buildDetailInfoContent和buildPreferenceContent方法构建其他步骤的内容,包含单选和多选控件。// 构建确认信息内容 @Builder buildConfirmationContent() { Column({ space: 16 }) { Text('请确认以下信息:') .fontSize(18) .fontColor(Color.Black) .fontWeight(FontWeight.Bold) .alignSelf(ItemAlign.Start) // 基本信息 Column({ space: 8 }) { Text('基本信息') .fontSize(16) .fontColor('#4D94FF') .fontWeight(FontWeight.Medium) .alignSelf(ItemAlign.Start) Text(`姓名:${this.basicInfo.name}`) .fontSize(14) .fontColor('#666666') .alignSelf(ItemAlign.Start) Text(`邮箱:${this.basicInfo.email}`) .fontSize(14) .fontColor('#666666') .alignSelf(ItemAlign.Start) Text(`电话:${this.basicInfo.phone}`) .fontSize(14) .fontColor('#666666') .alignSelf(ItemAlign.Start) } .width('100%') .padding(12) .backgroundColor('#F8F8F8') .borderRadius(8) // 详细资料 Column({ space: 8 }) { Text('详细资料') .fontSize(16) .fontColor('#4D94FF') .fontWeight(FontWeight.Medium) .alignSelf(ItemAlign.Start) Text(`职业:${this.getOccupationText(this.detailInfo.occupation)}`) .fontSize(14) .fontColor('#666666') .alignSelf(ItemAlign.Start) } .width('100%') .padding(12) .backgroundColor('#F8F8F8') .borderRadius(8) // 偏好设置 Column({ space: 8 }) { Text('偏好设置') .fontSize(16) .fontColor('#4D94FF') .fontWeight(FontWeight.Medium) .alignSelf(ItemAlign.Start) Text(`兴趣领域:${this.preferences.join(', ')}`) .fontSize(14) .fontColor('#666666') .alignSelf(ItemAlign.Start) } .width('100%') .padding(12) .backgroundColor('#F8F8F8') .borderRadius(8) Checkbox({ name: 'agree', position: CheckboxPosition.Left }) Text('我确认以上信息正确无误') .fontSize(14) .fontColor('#666666') } }buildConfirmationContent方法构建确认页面,汇总显示所有步骤填写的信息。// 步骤变化回调 private onStepChange(currentStep: number, previousStep: number): void { console.log(`步骤从 ${previousStep + 1} 切换到 ${currentStep + 1}`); this.currentStep = currentStep; } // 步骤完成回调 private onStepComplete(stepIndex: number): void { console.log(`步骤 ${stepIndex + 1} 完成`); this.stepperItems[stepIndex].status = 'completed'; if (stepIndex < this.stepperItems.length - 1) { this.stepperItems[stepIndex + 1].status = 'active'; } } build() { Column({ space: 20 }) { // 标题 Text('Swiper Stepper演示') .fontSize(24) .fontColor(Color.Black) .fontWeight(FontWeight.Bold) .margin({ top: 40, bottom: 20 }) // Swiper Stepper组件 SwiperStepper({ items: this.stepperItems, config: this.customConfig, onStepChange: this.onStepChange.bind(this), onStepComplete: this.onStepComplete.bind(this) }) .width('100%') .height('70%') // 额外控制面板 this.buildExtraControls() } .width('100%') .height('100%') .padding(20) .backgroundColor('#F5F5F5') } // 构建额外控制面板 @Builder private buildExtraControls() { Column({ space: 12 }) { Text('快速导航') .fontSize(16) .fontColor(Color.Black) .fontWeight(FontWeight.Medium) .alignSelf(ItemAlign.Start) Row({ space: 8 }) { ForEach(this.stepperItems, (item, index) => { Button(`步骤${index + 1}`) .layoutWeight(1) .height(32) .fontSize(12) .backgroundColor(this.currentStep === index ? '#4D94FF' : '#FFFFFF') .fontColor(this.currentStep === index ? Color.White : '#666666') .onClick(() => { // 这里应该调用SwiperStepper的goToStep方法 console.log(`跳转到步骤 ${index + 1}`); }) }) } } .width('100%') .padding(16) .backgroundColor(Color.White) .borderRadius(12) .shadow({ radius: 2, color: '#00000010', offsetX: 0, offsetY: 2 }) }}build方法创建完整演示界面,包含标题、SwiperStepper组件和额外控制面板。onStepChange和onStepComplete方法处理步骤状态变化。四、高级特性 4.1 自定义指示器 // CustomIndicator.ets@Componentexport struct CustomIndicator { @Prop items: StepperItem[]; @Prop currentIndex: number; @Prop completedSteps: number[]; @Prop errorSteps: number[]; @Builder buildCustomIndicator() { Row({ space: 4 }) { ForEach(this.items, (item, index) => { Column({ space: 2 }) { // 自定义图标 Stack({ alignContent: Alignment.Center }) { if (this.completedSteps.includes(index)) { Image($r('app.media.check_circle_fill')) .width(24) .height(24) .fillColor('#96CEB4') } else if (this.errorSteps.includes(index)) { Image($r('app.media.error_fill')) .width(24) .height(24) .fillColor('#FF6B6B') } else { Circle() .width(20) .height(20) .fill(index === this.currentIndex ? '#4D94FF' : '#E0E0E0') if (index === this.currentIndex) { Circle() .width(8) .height(8) .fill(Color.White) } } } // 连接线 if (index < this.items.length - 1) { Row() .width(20) .height(2) .backgroundColor(this.completedSteps.includes(index) ? '#96CEB4' : '#E0E0E0') } } }) } .padding(12) .backgroundColor('#FFFFFF') .borderRadius(20) }}CustomIndicator组件实现完全自定义的指示器,使用图标和连接线创建独特的视觉效果。4.2 响应式Swiper Stepper // ResponsiveSwiperStepper.ets@Componentexport struct ResponsiveSwiperStepper { @State private screenWidth: number = 0; @State private screenHeight: number = 0; aboutToAppear() { this.updateScreenSize(); window.getWindowClass().on('windowSizeChange', () => { this.updateScreenSize(); }); } private updateScreenSize(): void { const windowClass = window.getWindowClass(); this.screenWidth = windowClass.getWindowWidth(); this.screenHeight = windowClass.getWindowHeight(); } private get responsiveConfig(): SwiperStepperConfig { const isMobile = this.screenWidth < 600; return { ...SwiperStepperDefaultConfig.DEFAULT_CONFIG, indicatorType: isMobile ? 'dots' : 'numbers', indicatorPosition: isMobile ? 'bottom' : 'top' }; } build() { SwiperStepper({ items: this.stepperItems, config: this.responsiveConfig }) .width('100%') .height(this.screenWidth < 600 ? '60%' : '70%') }}ResponsiveSwiperStepper组件实现响应式设计,根据屏幕尺寸自动调整指示器类型和位置。五、最佳实践 5.1 性能优化建议 懒加载内容:使用Swiper的lazyLoad属性延迟加载不可见内容图片优化:使用合适尺寸的图标资源事件节流:对手势事件进行节流处理内存管理:及时清理定时器和事件监听器性能优化包括:1)使用Swiper的懒加载功能;2)优化图标资源尺寸;3)手势事件节流避免过度渲染;4)组件销毁时清理资源。用户体验优化包括:1)使用颜色区分步骤状态;2)利用Swiper的流畅动画;3)提供清晰的操作指引;4)验证失败时给出友好提示。5.3 可访问性 // 为屏幕阅读器提供支持.accessibilityLabel('Swiper步进器').accessibilityHint('使用左右滑动或导航按钮切换步骤').accessibilityRole(AccessibilityRole.Adjustable).accessibilityState({ valueNow: this.currentIndex + 1, valueMin: 1, valueMax: this.items.length})可访问性支持为视障用户提供语音反馈,描述组件功能、操作提示和当前步骤信息。六、总结 基于Swiper的Stepper组件充分利用了Swiper的流畅切换特性,提供了完整的步骤管理功能,支持多种指示器样式和响应式设计。
-
鸿蒙音量检测常见问题与解决方案1.1 问题说明:清晰呈现问题场景与具体表现问题场景在鸿蒙应用开发中,开发者需要实时检测和监控设备音量变化,常见于以下场景:音频录制应用:需要实时显示录音音量大小语音识别应用:需要根据环境音量调整识别灵敏度多媒体播放器:需要同步显示当前播放音量游戏应用:需要根据音量调整游戏音效1.2解决方案:落地解决思路,给出可执行、可复用的具体方案1. 权限配置// module.json5{ "module": { "requestPermissions": [ { "name": "ohos.permission.MICROPHONE", "reason": "$string:microphone_permission_reason", "usedScene": { "abilities": [ "MainAbility" ], "when": "always" } } ] }}2. 核心音量检测管理器// VolumeDetector.tsimport audio from '@ohos.multimedia.audio';import { BusinessError } from '@ohos.base';import common from '@ohos.app.ability.common';export class VolumeDetector { private audioManager: audio.AudioManager | null = null; private volumeChangeListener: audio.AudioVolumeGroupChangeCallback | null = null; private isDetecting: boolean = false; private context: common.UIAbilityContext; constructor(context: common.UIAbilityContext) { this.context = context; } /** * 初始化音频管理器 */ public async init(): Promise<void> { try { // 获取音频管理器实例 this.audioManager = audio.getAudioManager(); // 检查权限 await this.checkAndRequestPermission(); console.log('[VolumeDetector] 音频管理器初始化成功'); } catch (error) { console.error('[VolumeDetector] 初始化失败:', error); throw error; } } /** * 检查和请求权限 */ private async checkAndRequestPermission(): Promise<void> { try { const permissions: Array<Permissions> = ['ohos.permission.MICROPHONE']; const grantStatus = await abilityAccessCtrl.createAt(this.context).verifyAccessToken( permissions ); if (grantStatus.authResults[0] === -1) { // 权限未授予,发起请求 await this.requestPermission(); } } catch (error) { console.error('[VolumeDetector] 权限检查失败:', error); } } /** * 请求权限 */ private async requestPermission(): Promise<void> { // 实现权限请求逻辑 // 这里可以展示自定义的权限请求弹窗 } /** * 开始音量检测 * @param volumeType 音频流类型 * @param callback 音量变化回调 */ public startDetection( volumeType: audio.AudioVolumeType = audio.AudioVolumeType.MEDIA, callback: (currentVolume: number, maxVolume: number) => void ): void { if (!this.audioManager || this.isDetecting) { return; } try { this.isDetecting = true; // 设置音量变化监听 this.volumeChangeListener = (volumeEvent: audio.VolumeEvent): void => { if (volumeEvent.volumeType === volumeType) { const current = volumeEvent.volume; const max = this.getMaxVolume(volumeType); callback(current, max); } }; // 注册监听器 this.audioManager.on('volumeChange', this.volumeChangeListener); // 获取当前音量作为初始值 const currentVolume = this.audioManager.getVolume(volumeType); const maxVolume = this.getMaxVolume(volumeType); callback(currentVolume, maxVolume); console.log('[VolumeDetector] 音量检测已启动'); } catch (error) { console.error('[VolumeDetector] 启动检测失败:', error); this.isDetecting = false; } } /** * 获取最大音量 */ private getMaxVolume(volumeType: audio.AudioVolumeType): number { try { if (this.audioManager) { const volumeGroupManager = this.audioManager.getVolumeManager(); return volumeGroupManager.getMaxVolume(volumeType); } return 15; // 默认最大值 } catch (error) { console.error('[VolumeDetector] 获取最大音量失败:', error); return 15; } } /** * 停止音量检测 */ public stopDetection(): void { if (!this.audioManager || !this.isDetecting) { return; } try { if (this.volumeChangeListener) { this.audioManager.off('volumeChange', this.volumeChangeListener); this.volumeChangeListener = null; } this.isDetecting = false; console.log('[VolumeDetector] 音量检测已停止'); } catch (error) { console.error('[VolumeDetector] 停止检测失败:', error); } } /** * 设置特定音量 */ public setVolume( volumeType: audio.AudioVolumeType, volume: number ): Promise<void> { return new Promise((resolve, reject) => { if (!this.audioManager) { reject(new Error('音频管理器未初始化')); return; } try { this.audioManager.setVolume(volumeType, volume); console.log(`[VolumeDetector] 音量已设置为: ${volume}`); resolve(); } catch (error) { console.error('[VolumeDetector] 设置音量失败:', error); reject(error); } }); } /** * 释放资源 */ public release(): void { this.stopDetection(); this.audioManager = null; console.log('[VolumeDetector] 资源已释放'); }}// 导出音量类型常量export const VolumeType = audio.AudioVolumeType;3. 使用示例// MainAbility.tsimport { VolumeDetector, VolumeType } from './VolumeDetector';import common from '@ohos.app.ability.common';export default class MainAbility extends Ability { private volumeDetector: VolumeDetector | null = null; onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { console.log('[MainAbility] onCreate'); // 初始化音量检测器 this.volumeDetector = new VolumeDetector(this.context); this.initVolumeDetection(); } private async initVolumeDetection(): Promise<void> { try { if (this.volumeDetector) { await this.volumeDetector.init(); // 开始检测媒体音量 this.volumeDetector.startDetection(VolumeType.MEDIA, (currentVolume: number, maxVolume: number) => { console.log(`当前音量: ${currentVolume}/${maxVolume}`); this.updateVolumeUI(currentVolume, maxVolume); } ); } } catch (error) { console.error('[MainAbility] 音量检测初始化失败:', error); } } private updateVolumeUI(current: number, max: number): void { // 更新UI显示 const percentage = (current / max) * 100; console.log(`音量百分比: ${percentage.toFixed(1)}%`); // 这里可以更新UI组件 } onDestroy(): void { console.log('[MainAbility] onDestroy'); // 释放资源 if (this.volumeDetector) { this.volumeDetector.release(); this.volumeDetector = null; } }}4. 音量可视化组件// VolumeVisualizer.ets@Componentexport struct VolumeVisualizer { @Link currentVolume: number; @Link maxVolume: number; build() { Column() { // 音量数值显示 Text(`${this.currentVolume}/${this.maxVolume}`) .fontSize(20) .fontColor(Color.White) // 音量条 Row() { // 当前音量 Column() { Blank() } .width(`${(this.currentVolume / this.maxVolume) * 100}%`) .height(30) .backgroundColor(Color.Blue) // 剩余部分 Column() { Blank() } .backgroundColor(Color.Gray) } .width('100%') .height(30) .borderRadius(15) .overflow(Overflow.Hidden) // 音量等级指示器 Row({ space: 5 }) { ForEach(Array.from({ length: this.maxVolume }, (_, i) => i + 1), (item: number) => { Column() { Blank() } .width(10) .height(item * 5) .backgroundColor(item <= this.currentVolume ? Color.Green : Color.Gray) .borderRadius(2) } ) } .margin({ top: 20 }) } .padding(20) .backgroundColor(0x33000000) .borderRadius(10) }} 1.5 结果展示:开发效率提升以及为后续同类问题提供参考效率提升指标开发时间减少:从平均8小时缩短到2小时代码复用率:达到85%以上维护成本:降低70%问题解决速度:从平均4小时缩短到30分钟
-
鸿蒙语音转文字案例1.1 问题说明:清晰呈现问题场景与具体表现在鸿蒙(HarmonyOS)应用开发中,经常遇到需要将用户的语音输入实时、准确地转换为文本的场景,例如:语音助手: 用户通过语音发送指令。智能输入: 在聊天、笔记应用中,用户通过语音快速输入长文本。语音搜索: 在内容平台中,用户通过语音输入搜索关键词。无障碍服务: 为视觉或操作不便的用户提供语音交互支持1.2 解决方案:落地解决思路,给出可执行、可复用的具体方案以下提供一个基于AsrManager核心类的简化示例代码和集成步骤。步骤一:申请必要权限在module.json5中配置{ "module": { "requestPermissions": [ { "name": "ohos.permission.MICROPHONE" }, { "name": "ohos.permission.INTERNET" // 如果使用云端引擎 } ] }}步骤二:创建核心管理类AsrManager.ets(简化版)// AsrManager.etsimport audio from '@ohos.multimedia.audio';import { AsrCloudEngine } from './AsrCloudEngine'; // 假设的云端引擎适配器import { AsrLocalEngine } from './AsrLocalEngine'; // 假设的本地引擎适配器export enum AsrEngineType { LOCAL, CLOUD, AUTO}export interface AsrResult { text: string; isFinal: boolean; // 是否为最终结果(true),或中间临时结果(false)}export class AsrManager { private audioCapturer: audio.AudioCapturer | undefined; private currentEngine: IAsrEngine | undefined; // IAsrEngine是定义的引擎接口 private onResultCallback: (result: AsrResult) => void = () => {}; // 初始化并选择引擎 async init(engineType: AsrEngineType = AsrEngineType.AUTO, config?: any): Promise<void> { // 1. 根据策略选择引擎 let targetEngineType = engineType; if (engineType === AsrEngineType.AUTO) { // 简单策略:有网且非隐私模式则用云端,否则用本地 // 实际策略可以更复杂 targetEngineType = await this.isNetworkGood() ? AsrEngineType.CLOUD : AsrEngineType.LOCAL; } // 2. 实例化引擎 switch (targetEngineType) { case AsrEngineType.CLOUD: this.currentEngine = new AsrCloudEngine(config?.cloudConfig); break; case AsrEngineType.LOCAL: default: this.currentEngine = new AsrLocalEngine(config?.localConfig); break; } // 3. 初始化音频采集器(配置参数需与引擎要求匹配) await this.initAudioCapturer(); console.info(`ASR Manager initialized with engine: ${AsrEngineType[targetEngineType]}`); } private async initAudioCapturer(): Promise<void> { // 配置音频参数:采样率、声道、格式等 const audioStreamInfo: audio.AudioStreamInfo = { samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_16000, channels: audio.AudioChannel.MONO, sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW }; const audioCapturerInfo: audio.AudioCapturerInfo = { source: audio.SourceType.SOURCE_TYPE_MIC, capturerFlags: 0 // 默认标志 }; this.audioCapturer = await audio.createAudioCapturer(audioCapturerInfo, audioStreamInfo); } // 开始识别 async start(callback: (result: AsrResult) => void): Promise<void> { if (!this.currentEngine || !this.audioCapturer) { throw new Error('ASR Manager not initialized.'); } this.onResultCallback = callback; // 1. 启动引擎 await this.currentEngine.start((engineResult: AsrResult) => { // 接收引擎返回的结果 this.onResultCallback(engineResult); }); // 2. 开始录音并持续向引擎输送数据 await this.audioCapturer.start(); const bufferSize = await this.audioCapturer.getBufferSize(); const audioBuffer = await this.audioCapturer.read(bufferSize, true); // 这里应设置一个循环或使用事件监听,持续读取音频数据 // 并将audioBuffer.data发送给 this.currentEngine.feedAudioData(audioBuffer.data) // 此处为简化示例,实际需要更复杂的异步流处理 console.info('ASR recording started.'); } // 停止识别 async stop(): Promise<void> { await this.audioCapturer?.stop(); await this.currentEngine?.stop(); this.onResultCallback = () => {}; console.info('ASR stopped.'); } // 释放资源 async release(): Promise<void> { await this.stop(); await this.audioCapturer?.release(); this.currentEngine = undefined; } private async isNetworkGood(): Promise<boolean> { // 实现网络状态检查,此处返回true return true; }}步骤三:在UI页面中调用// Index.ets 页面示例import { AsrManager, AsrEngineType } from '../utils/AsrManager';import promptAction from '@ohos.promptAction';@Entry@Componentstruct Index { private asrManager: AsrManager = new AsrManager(); @State recognizedText: string = ''; async aboutToAppear() { // 初始化,选择云端引擎,并传入自定义热词 try { await this.asrManager.init(AsrEngineType.CLOUD, { cloudConfig: { apiKey: 'YOUR_CLOUD_API_KEY', language: 'zh-CN', hotWords: ['鸿蒙', 'HarmonyOS', 'ArkTS'] } }); } catch (error) { promptAction.showToast({ message: `初始化失败: ${error.message}` }); } } // 开始录音按钮事件 async startRecording() { this.recognizedText = '正在聆听...'; try { await this.asrManager.start((result) => { // 实时更新UI,isFinal为true时可做最终处理(如发送) this.recognizedText = result.text; if (result.isFinal) { promptAction.showToast({ message: '识别完成!' }); // 这里可以将最终文本发送出去 } }); } catch (error) { promptAction.showToast({ message: `启动失败: ${error.message}` }); } } // 停止录音按钮事件 async stopRecording() { try { await this.asrManager.stop(); } catch (error) { promptAction.showToast({ message: `停止失败: ${error.message}` }); } } aboutToDisappear() { this.asrManager.release(); } build() { Column() { Text(this.recognizedText) .fontSize(20) .margin(20) .height(100) Button('开始说话') .onClick(() => this.startRecording()) .margin(10) Button('停止') .onClick(() => this.stopRecording()) .margin(10) } .width('100%') .height('100%') }}1.3 结果展示:开发效率提升以及为后续同类问题提供参考开发效率显著提升:标准化集成: 新功能语音识别需求的集成时间从原来的“天”级别缩短到“小时”级别。开发者只需关注AsrManager的init、start、stop三个核心方法,无需深入音频和网络细节。降低决策成本: 通过AUTO模式或简单配置,开发者无需纠结于本地与云端的选择,框架内置了合理的降级策略。代码复用率高: AsrManager及引擎适配器可在全团队乃至全公司范围内复用,形成技术资产。
-
一、项目概述1.1 功能特性● 基于HarmonyOS 4.0+ API实现● 下拉手势触发二楼页面展开● 流畅的物理动画效果● 支持自定义二楼内容● 多种触发模式和动画曲线● 高性能手势识别和渲染二、架构设计2.1 核心组件结构下拉二楼系统├── SecondFloorContainer.ets (主容器)├── PullToRefresh.ets (下拉刷新组件)├── SecondFloorContent.ets (二楼内容)├── PhysicsAnimation.ets (物理动画)└── GestureRecognizer.ets (手势识别)2.2 数据模型定义// SecondFloorModel.ets// 二楼配置export interface SecondFloorConfig {triggerThreshold: number; // 触发阈值(px)maxPullDistance: number; // 最大下拉距离(px)animationDuration: number; // 动画时长(ms)animationCurve: animation.Curve; // 动画曲线enablePhysics: boolean; // 启用物理效果damping: number; // 阻尼系数stiffness: number; // 刚度系数enableHaptic: boolean; // 启用触觉反馈backgroundColor: ResourceColor; // 背景颜色blurBackground: boolean; // 模糊背景enableOverScroll: boolean; // 启用越界滚动}// 二楼状态export interface SecondFloorState {isActive: boolean; // 是否激活isExpanded: boolean; // 是否展开progress: number; // 进度(0-1)pullDistance: number; // 下拉距离velocity: number; // 速度lastUpdateTime: number; // 最后更新时间}// 默认配置export class SecondFloorDefaultConfig {static readonly DEFAULT_CONFIG: SecondFloorConfig = {triggerThreshold: 150,maxPullDistance: 400,animationDuration: 400,animationCurve: animation.Curve.EaseOut,enablePhysics: true,damping: 15,stiffness: 200,enableHaptic: true,backgroundColor: ‘#1A1A2E’,blurBackground: true,enableOverScroll: true};}这里定义了下拉二楼系统的核心数据模型。SecondFloorConfig接口包含所有可配置参数,如触发阈值、动画参数、物理效果等。SecondFloorState接口管理二楼的各种状态。SecondFloorDefaultConfig提供默认配置值。三、核心实现3.1 手势识别器// GestureRecognizer.etsexport class GestureRecognizer {private startY: number = 0;private currentY: number = 0;private startTime: number = 0;private isTracking: boolean = false;private isVertical: boolean = false;// 手势回调private onGestureStart?: (y: number) => void;private onGestureMove?: (y: number, deltaY: number, velocity: number) => void;private onGestureEnd?: (y: number, velocity: number, shouldTrigger: boolean) => void;// 设置回调setCallbacks(onStart: (y: number) => void,onMove: (y: number, deltaY: number, velocity: number) => void,onEnd: (y: number, velocity: number, shouldTrigger: boolean) => void): void {this.onGestureStart = onStart;this.onGestureMove = onMove;this.onGestureEnd = onEnd;}GestureRecognizer类负责识别下拉手势。它跟踪触摸起始位置、当前位置和时间,计算移动距离和速度。通过回调函数将手势事件传递给上层组件。// 触摸开始handleTouchStart(event: TouchEvent): void {if (event.touches.length !== 1) return;const touch = event.touches[0]; this.startY = touch.y; this.currentY = touch.y; this.startTime = Date.now(); this.isTracking = true; this.isVertical = false; this.onGestureStart?.(touch.y);}// 触摸移动handleTouchMove(event: TouchEvent): void {if (!this.isTracking || event.touches.length !== 1) return;const touch = event.touches[0]; const deltaY = touch.y - this.currentY; this.currentY = touch.y; // 判断是否为垂直滑动 if (!this.isVertical) { const deltaX = Math.abs(touch.x - this.startY); this.isVertical = deltaY > deltaX && deltaY > 10; } if (this.isVertical) { event.stopPropagation(); const currentTime = Date.now(); const deltaTime = currentTime - this.startTime; const velocity = deltaY / Math.max(deltaTime, 1); this.onGestureMove?.(touch.y, deltaY, velocity); }}handleTouchStart方法记录触摸起始位置和时间。handleTouchMove方法跟踪触摸移动,判断是否为垂直滑动,计算移动速度和距离,并通过回调通知上层组件。// 触摸结束handleTouchEnd(event: TouchEvent): void {if (!this.isTracking) return;this.isTracking = false; const currentTime = Date.now(); const deltaTime = currentTime - this.startTime; const totalDeltaY = this.currentY - this.startY; const velocity = totalDeltaY / Math.max(deltaTime, 1); // 判断是否应该触发二楼 const shouldTrigger = this.isVertical && totalDeltaY > 0 && Math.abs(velocity) > 0.5; this.onGestureEnd?.(this.currentY, velocity, shouldTrigger);}// 重置状态reset(): void {this.isTracking = false;this.isVertical = false;this.startY = 0;this.currentY = 0;}}handleTouchEnd方法处理触摸结束事件,计算最终速度和移动距离,判断是否应该触发二楼展开。reset方法重置手势识别器状态。3.2 物理动画引擎// PhysicsAnimation.etsexport class PhysicsAnimation {private animationController: animation.Animator = new animation.Animator();private currentValue: number = 0;private targetValue: number = 0;private velocity: number = 0;private damping: number = 15;private stiffness: number = 200;private lastTime: number = 0;// 动画回调private onUpdate?: (value: number, velocity: number) => void;private onComplete?: (value: number) => void;// 设置参数setParameters(damping: number, stiffness: number): void {this.damping = damping;this.stiffness = stiffness;}// 设置回调setCallbacks(onUpdate: (value: number, velocity: number) => void,onComplete: (value: number) => void): void {this.onUpdate = onUpdate;this.onComplete = onComplete;}PhysicsAnimation类实现基于物理的动画效果,使用弹簧模型计算平滑的动画轨迹。通过阻尼系数控制动画衰减,刚度系数控制回弹力度。// 开始动画start(fromValue: number, toValue: number, initialVelocity: number = 0): void {this.currentValue = fromValue;this.targetValue = toValue;this.velocity = initialVelocity;this.lastTime = Date.now();this.animationController.stop(); this.animationController.update({ duration: 0, // 持续动画 curve: animation.Curve.Linear }); this.animationController.onFrame(() => { this.updatePhysics(); }); this.animationController.play();}// 更新物理计算private updatePhysics(): void {const currentTime = Date.now();const deltaTime = Math.min(currentTime - this.lastTime, 50) / 1000; // 转换为秒this.lastTime = currentTime;if (deltaTime <= 0) return; // 弹簧物理计算 const displacement = this.targetValue - this.currentValue; const springForce = this.stiffness * displacement; const dampingForce = this.damping * this.velocity; const acceleration = (springForce - dampingForce) / 1; // 质量设为1 this.velocity += acceleration * deltaTime; this.currentValue += this.velocity * deltaTime; // 检查是否完成 const isAtRest = Math.abs(this.velocity) < 0.1 && Math.abs(displacement) < 0.1; this.onUpdate?.(this.currentValue, this.velocity); if (isAtRest) { this.stop(); this.onComplete?.(this.currentValue); }}// 停止动画stop(): void {this.animationController.stop();}// 获取当前值getCurrentValue(): number {return this.currentValue;}}start方法开始物理动画,设置起始值、目标值和初始速度。updatePhysics方法基于弹簧模型计算动画的物理特性,每帧更新位置和速度。当速度足够小且接近目标值时停止动画。3.3 下拉刷新组件// PullToRefresh.ets@Componentexport struct PullToRefresh {@Prop config: SecondFloorConfig = SecondFloorDefaultConfig.DEFAULT_CONFIG;@Prop onRefresh?: () => void;@Prop onSecondFloorTrigger?: () => void;@State private pullDistance: number = 0;@State private progress: number = 0;@State private isRefreshing: boolean = false;@State private isSecondFloorTriggered: boolean = false;private gestureRecognizer: GestureRecognizer = new GestureRecognizer();private physicsAnimation: PhysicsAnimation = new PhysicsAnimation();private vibration: vibrator.Vibrator | null = null;aboutToAppear() {this.setupGestureCallbacks();this.physicsAnimation.setParameters(this.config.damping, this.config.stiffness);if (this.config.enableHaptic) { this.vibration = vibrator.createVibrator(); }}PullToRefresh组件是下拉二楼系统的核心组件。@State装饰器管理下拉距离、进度、刷新状态等。gestureRecognizer处理手势识别,physicsAnimation处理物理动画。private setupGestureCallbacks(): void {this.gestureRecognizer.setCallbacks(// 手势开始(y: number) => {this.physicsAnimation.stop();}, // 手势移动 (y: number, deltaY: number, velocity: number) => { if (this.isRefreshing || this.isSecondFloorTriggered) return; // 计算下拉距离 let newPullDistance = this.pullDistance + deltaY; if (!this.config.enableOverScroll && newPullDistance < 0) { newPullDistance = 0; } this.pullDistance = newPullDistance; this.progress = Math.min(newPullDistance / this.config.triggerThreshold, 1); // 触发触觉反馈 if (this.progress >= 1 && this.config.enableHaptic) { this.triggerHapticFeedback(); } }, // 手势结束 (y: number, velocity: number, shouldTrigger: boolean) => { if (this.isRefreshing || this.isSecondFloorTriggered) return; if (shouldTrigger && this.pullDistance >= this.config.triggerThreshold) { // 触发二楼 this.triggerSecondFloor(); } else { // 回弹动画 this.animateRebound(velocity); } } );}setupGestureCallbacks方法设置手势识别的回调函数。手势移动时更新下拉距离和进度,达到阈值时触发触觉反馈。手势结束时判断是否触发二楼或执行回弹动画。// 触发二楼private triggerSecondFloor(): void {this.isSecondFloorTriggered = true;// 执行展开动画 this.physicsAnimation.setCallbacks( (value: number) => { this.pullDistance = value; }, () => { this.onSecondFloorTrigger?.(); } ); this.physicsAnimation.start( this.pullDistance, this.config.maxPullDistance, 2 // 初始速度 ); // 触觉反馈 if (this.config.enableHaptic) { this.triggerHapticFeedback('medium'); }}// 回弹动画private animateRebound(initialVelocity: number = 0): void {this.physicsAnimation.setCallbacks((value: number) => {this.pullDistance = value;this.progress = value / this.config.triggerThreshold;},() => {this.pullDistance = 0;this.progress = 0;});this.physicsAnimation.start(this.pullDistance, 0, initialVelocity);}// 触觉反馈private triggerHapticFeedback(type: ‘light’ | ‘medium’ | ‘heavy’ = ‘light’): void {if (!this.vibration) return;const duration = type === 'light' ? 10 : type === 'medium' ? 20 : 30; try { this.vibration.vibrate(duration); } catch (error) { console.warn('触觉反馈不可用:', error); }}triggerSecondFloor方法触发二楼展开,执行展开动画并调用回调函数。animateRebound方法执行回弹动画,将下拉距离恢复为0。triggerHapticFeedback方法提供不同强度的触觉反馈。// 构建下拉指示器@Builderprivate buildPullIndicator() {if (this.pullDistance <= 0) return;Column({ space: 8 }) { // 进度圆环 Stack({ alignContent: Alignment.Center }) { Circle() .width(30) .height(30) .stroke('#FFFFFF40') .strokeWidth(2) .fill(Color.Transparent) Circle() .width(30) .height(30) .stroke('#4D94FF') .strokeWidth(2) .fill(Color.Transparent) .strokeDashArray([Math.PI * 30 * this.progress, Math.PI * 30]) .rotation({ angle: -90 }) // 箭头图标 if (this.progress < 1) { Image($r('app.media.arrow_down')) .width(16) .height(16) .rotate({ angle: this.progress * 180 }) } else { // 二楼图标 Image($r('app.media.second_floor')) .width(16) .height(16) } } // 提示文本 Text(this.progress < 1 ? '下拉刷新' : '释放进入二楼') .fontSize(12) .fontColor('#FFFFFF') .opacity(this.progress * 0.8) } .position({ x: '50%', y: 20 }) .translate({ y: -this.pullDistance / 2 }) .opacity(Math.min(this.pullDistance / 50, 1))}buildPullIndicator方法构建下拉指示器,显示进度圆环、箭头图标和提示文本。进度圆环使用strokeDashArray实现动态进度效果,箭头根据进度旋转,达到阈值时切换为二楼图标。build() {Stack({ alignContent: Alignment.TopStart }) {// 内容区域Column().width(‘100%’).height(‘100%’).translate({ y: this.pullDistance }).clip(true){// 这里放置主要内容this.buildMainContent()} // 下拉指示器 this.buildPullIndicator() } .width('100%') .height('100%') .onTouch((event: TouchEvent) => { if (event.type === TouchType.Down) { this.gestureRecognizer.handleTouchStart(event); } else if (event.type === TouchType.Move) { this.gestureRecognizer.handleTouchMove(event); } else if (event.type === TouchType.Up) { this.gestureRecognizer.handleTouchEnd(event); } })}}build方法创建组件布局,主要内容区域根据下拉距离平移,下拉指示器固定在顶部。绑定触摸事件处理函数,将事件传递给手势识别器。3.4 二楼内容组件// SecondFloorContent.ets@Componentexport struct SecondFloorContent {@Prop config: SecondFloorConfig = SecondFloorDefaultConfig.DEFAULT_CONFIG;@Prop isExpanded: boolean = false;@Prop onClose?: () => void;@State private contentHeight: number = 0;@State private scrollOffset: number = 0;@State private isScrolling: boolean = false;private scroller: Scroller = new Scroller();private closeThreshold: number = 100;// 构建二楼头部@Builderprivate buildSecondFloorHeader() {Column({ space: 12 }) {// 标题栏Row({ space: 8 }) {Text(‘二楼’).fontSize(20).fontColor(Color.White).fontWeight(FontWeight.Bold).layoutWeight(1) Button('关闭') .fontSize(14) .backgroundColor('#FFFFFF20') .onClick(() => { this.onClose?.(); }) } // 搜索栏 Row({ space: 8 }) { TextInput({ placeholder: '搜索二楼内容...' }) .layoutWeight(1) .backgroundColor('#FFFFFF10') .borderRadius(20) .padding(12) .fontColor(Color.White) Button('搜索') .backgroundColor('#4D94FF') .borderRadius(20) } } .width('100%') .padding(20) .backgroundColor(this.config.backgroundColor)}SecondFloorContent组件构建二楼页面的内容。buildSecondFloorHeader方法创建二楼头部,包含标题栏、关闭按钮和搜索栏。// 构建功能网格@Builderprivate buildFeatureGrid() {const features = [{ icon: $r(‘app.media.quick_pay’), title: ‘快捷支付’, color: ‘#FF6B6B’ },{ icon: $r(‘app.media.scan’), title: ‘扫一扫’, color: ‘#4ECDC4’ },{ icon: $r(‘app.media.transport’), title: ‘出行’, color: ‘#45B7D1’ },{ icon: $r(‘app.media.food’), title: ‘美食’, color: ‘#96CEB4’ },{ icon: $r(‘app.media.shopping’), title: ‘购物’, color: ‘#FFEAA7’ },{ icon: $r(‘app.media.entertainment’), title: ‘娱乐’, color: ‘#DDA0DD’ },{ icon: $r(‘app.media.health’), title: ‘健康’, color: ‘#98D8C8’ },{ icon: $r(‘app.media.more’), title: ‘更多’, color: ‘#F7DC6F’ }];Grid() { ForEach(features, (feature, index) => { GridItem() { Column({ space: 8 }) { Circle() .width(50) .height(50) .fill(feature.color) .overlay( Image(feature.icon) .width(24) .height(24) ) Text(feature.title) .fontSize(12) .fontColor(Color.White) .textAlign(TextAlign.Center) } .width('100%') .padding(8) .onClick(() => { console.log(`点击功能: ${feature.title}`); }) } }) } .columnsTemplate('1fr 1fr 1fr 1fr') .rowsTemplate('1fr 1fr') .columnsGap(12) .rowsGap(12) .padding(20)}buildFeatureGrid方法构建功能网格,显示8个常用功能入口,每个功能包含图标和标题,使用Grid布局实现2行4列的排列。// 构建推荐内容@Builderprivate buildRecommendations() {Column({ space: 16 }) {Text(‘推荐内容’).fontSize(18).fontColor(Color.White).fontWeight(FontWeight.Medium).alignSelf(ItemAlign.Start).margin({ left: 20 }) Scroll(this.scroller) { Column({ space: 12 }) { ForEach([1, 2, 3, 4, 5], (index) => { this.buildRecommendationItem(index) }) } .padding(20) } .onScroll((xOffset: number, yOffset: number) => { this.scrollOffset = yOffset; this.isScrolling = yOffset > 0; }) .onScrollEnd(() => { this.isScrolling = false; }) }}// 构建推荐项@Builderprivate buildRecommendationItem(index: number): void {Row({ space: 12 }) {Image($r(‘app.media.recommend_’ + index)).width(80).height(60).borderRadius(8).objectFit(ImageFit.Cover) Column({ space: 4 }) { Text(`推荐内容 ${index}`) .fontSize(16) .fontColor(Color.White) .fontWeight(FontWeight.Medium) .alignSelf(ItemAlign.Start) Text('这里是推荐内容的描述信息') .fontSize(12) .fontColor('#CCCCCC') .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis }) } .layoutWeight(1) .alignItems(HorizontalAlign.Start) } .width('100%') .padding(12) .backgroundColor('#FFFFFF10') .borderRadius(12) .onClick(() => { console.log(`点击推荐项: ${index}`); })}buildRecommendations方法构建推荐内容区域,包含标题和可滚动的推荐项列表。buildRecommendationItem方法构建单个推荐项,包含图片、标题和描述。// 构建关闭手势区域@Builderprivate buildCloseGestureArea() {if (!this.isExpanded || this.isScrolling) return;Column() .width('100%') .height(30) .backgroundColor(Color.Transparent) .gesture( PanGesture({ fingers: 1, direction: PanDirection.Down }) .onActionStart(() => { // 手势开始 }) .onActionUpdate((event: GestureEvent) => { if (event.offsetY > this.closeThreshold) { this.onClose?.(); } }) )}build() {Column().width(‘100%’).height(‘100%’).backgroundColor(this.config.backgroundColor).blur(this.config.blurBackground ? 5 : 0).opacity(this.isExpanded ? 1 : 0).scale({ x: this.isExpanded ? 1 : 0.95, y: this.isExpanded ? 1 : 0.95 }).animation({duration: this.config.animationDuration,curve: this.config.animationCurve}){// 头部this.buildSecondFloorHeader() // 功能网格 this.buildFeatureGrid() // 推荐内容 this.buildRecommendations() .layoutWeight(1) // 关闭手势区域 this.buildCloseGestureArea() }}}buildCloseGestureArea方法构建关闭手势区域,支持向下滑动手势关闭二楼。build方法创建二楼内容的完整布局,应用动画效果,包含头部、功能网格、推荐内容和关闭手势区域。3.5 主容器组件// SecondFloorContainer.ets@Entry@Componentexport struct SecondFloorContainer {@State private secondFloorState: SecondFloorState = {isActive: false,isExpanded: false,progress: 0,pullDistance: 0,velocity: 0,lastUpdateTime: 0};@State private config: SecondFloorConfig = SecondFloorDefaultConfig.DEFAULT_CONFIG;// 主页面内容@Builderprivate buildMainContent() {Column({ space: 20 }) {// 头部Row({ space: 8 }) {Text(‘首页’).fontSize(24).fontColor(Color.Black).fontWeight(FontWeight.Bold).layoutWeight(1) Button('设置') .fontSize(14) .backgroundColor('#4D94FF') } .padding(20) .backgroundColor(Color.White) // 内容列表 Scroll() { Column({ space: 12 }) { ForEach([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], (index) => { this.buildContentItem(index) }) } .padding(20) } .scrollBar(BarState.Off) }}SecondFloorContainer是主入口组件,管理二楼的所有状态。buildMainContent方法构建主页面的内容,包含头部和可滚动的内容列表。// 构建内容项@Builderprivate buildContentItem(index: number): void {Row({ space: 12 }) {Circle().width(50).height(50).fill(‘#4D94FF’).overlay(Text(index.toString()).fontSize(18).fontColor(Color.White).fontWeight(FontWeight.Bold)) Column({ space: 4 }) { Text(`内容项 ${index}`) .fontSize(16) .fontColor(Color.Black) .fontWeight(FontWeight.Medium) .alignSelf(ItemAlign.Start) Text('这里是内容项的详细描述信息') .fontSize(12) .fontColor('#666666') .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis }) } .layoutWeight(1) .alignItems(HorizontalAlign.Start) } .width('100%') .padding(16) .backgroundColor(Color.White) .borderRadius(12) .shadow({ radius: 2, color: '#00000010', offsetX: 0, offsetY: 2 })}// 处理二楼触发private handleSecondFloorTrigger(): void {this.secondFloorState.isExpanded = true;// 动画完成后激活二楼 setTimeout(() => { this.secondFloorState.isActive = true; }, this.config.animationDuration);}// 处理二楼关闭private handleSecondFloorClose(): void {this.secondFloorState.isActive = false;// 动画完成后重置状态 setTimeout(() => { this.secondFloorState.isExpanded = false; }, this.config.animationDuration);}buildContentItem方法构建主页面的内容项。handleSecondFloorTrigger方法处理二楼触发,设置展开状态。handleSecondFloorClose方法处理二楼关闭,重置所有状态。build() {Stack({ alignContent: Alignment.TopStart }) {// 主页面Column().width(‘100%’).height(‘100%’).backgroundColor(‘#F5F5F5’){this.buildMainContent()} // 下拉刷新和二楼触发 PullToRefresh({ config: this.config, onRefresh: () => { console.log('触发刷新'); }, onSecondFloorTrigger: this.handleSecondFloorTrigger.bind(this) }) // 二楼内容 if (this.secondFloorState.isExpanded) { SecondFloorContent({ config: this.config, isExpanded: this.secondFloorState.isActive, onClose: this.handleSecondFloorClose.bind(this) }) .width('100%') .height('100%') .position({ x: 0, y: 0 }) .zIndex(1000) } } .width('100%') .height('100%')}}build方法创建完整的应用布局,使用Stack布局叠加主页面、下拉刷新组件和二楼内容。二楼内容在展开时显示在最上层,支持完整的交互流程。四、高级特性4.1 自定义动画曲线// CustomCurves.etsexport class CustomCurves {// 弹性曲线static readonly ELASTIC_OUT: animation.Curve = {curve: (t: number): number => {return Math.sin(-13.0 * (t + 1.0) * Math.PI / 2) * Math.pow(2.0, -10.0 * t) + 1.0;}};// 弹跳曲线static readonly BOUNCE_OUT: animation.Curve = {curve: (t: number): number => {if (t < (1 / 2.75)) {return 7.5625 * t * t;} else if (t < (2 / 2.75)) {return 7.5625 * (t -= (1.5 / 2.75)) * t + 0.75;} else if (t < (2.5 / 2.75)) {return 7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375;} else {return 7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375;}}};// 回弹曲线static readonly SPRING: animation.Curve = {curve: (t: number): number => {return 1 - Math.cos(t * Math.PI * 4) * Math.exp(-t * 6);}};}CustomCurves类定义自定义动画曲线,包括弹性曲线、弹跳曲线和弹簧曲线,可以创建更生动的动画效果。4.2 性能监控// PerformanceMonitor.etsexport class PerformanceMonitor {private frameCount: number = 0;private lastTime: number = 0;private fps: number = 0;private isMonitoring: boolean = false;// 开始监控startMonitoring(): void {this.isMonitoring = true;this.frameCount = 0;this.lastTime = Date.now();this.monitorLoop();}// 监控循环private monitorLoop(): void {if (!this.isMonitoring) return;this.frameCount++; const currentTime = Date.now(); if (currentTime - this.lastTime >= 1000) { this.fps = Math.round(this.frameCount * 1000 / (currentTime - this.lastTime)); this.frameCount = 0; this.lastTime = currentTime; // 输出性能信息 console.log(`FPS: ${this.fps}`); // 性能警告 if (this.fps < 50) { console.warn('性能下降,建议优化'); } } requestAnimationFrame(() => { this.monitorLoop(); });}// 停止监控stopMonitoring(): void {this.isMonitoring = false;}// 获取当前FPSgetFPS(): number {return this.fps;}}PerformanceMonitor类监控应用性能,计算帧率并在性能下降时发出警告,帮助开发者优化动画性能。五、最佳实践5.1 性能优化建议动画优化:使用transform代替top/left属性手势节流:对高频手势事件进行节流处理内存管理:及时清理不再使用的动画资源图片优化:使用合适尺寸的图片资源性能优化包括:1)优先使用transform实现动画以获得硬件加速;2)手势事件进行节流避免过度渲染;3)动画完成后及时清理资源;4)优化图片尺寸减少内存占用。5.2 用户体验优化视觉反馈:提供清晰的手势操作反馈动画流畅:确保所有动画达到60fps错误处理:网络异常时的友好提示操作引导:首次使用的操作指引用户体验优化包括:1)清晰的手势反馈和状态提示;2)保证动画流畅性;3)网络异常等情况的友好处理;4)首次使用时的操作指引。5.3 可访问性// 为屏幕阅读器提供支持.accessibilityLabel(‘下拉二楼容器’).accessibilityHint(‘下拉页面可以触发二楼功能,释放后展开二楼内容’).accessibilityRole(AccessibilityRole.Adjustable).accessibilityState({expanded: this.secondFloorState.isExpanded,disabled: false})可访问性支持为视障用户提供语音反馈,描述组件功能、操作提示和当前状态。六、总结本实现方案提供了完整的首页下拉进入二楼效果,包含流畅的手势识别、物理动画、可定制的二楼内容和完整的交互流程,通过HarmonyOS最新API实现了高性能的下拉二楼体验。下拉二楼效果适用于多种场景:电商应用的快捷功能和促销入口、社交应用的快速发布和消息中心、新闻应用的专题内容推荐、工具应用的快捷工具面板、娱乐应用的快速播放和内容发现等。
-
一、项目概述1.1 功能特性● 基于HarmonyOS 4.0+ API实现● 支持多张头像拼接组合● 多种布局模式(圆形、网格、重叠、螺旋)● 动态添加/删除头像● 平滑的动画过渡效果● 支持头像裁剪和边框定制二、架构设计2.1 核心组件结构头像拼接系统├── AvatarComposer.ets (主组件)├── AvatarLayout.ets (布局管理器)├── AvatarItem.ets (头像项)├── AvatarCanvas.ets (Canvas渲染)└── AvatarAnimation.ets (动画管理)2.2 数据模型定义// AvatarModel.ets// 头像数据模型export interface AvatarData {id: string;image: Resource; // 头像资源name?: string; // 用户姓名color?: ResourceColor; // 背景色borderColor?: ResourceColor; // 边框颜色borderWidth?: number; // 边框宽度size: number; // 头像尺寸x: number; // X坐标y: number; // Y坐标zIndex: number; // Z轴层级rotation: number; // 旋转角度scale: number; // 缩放比例opacity: number; // 透明度}// 布局配置export interface LayoutConfig {type: ‘circle’ | ‘grid’ | ‘overlap’ | ‘spiral’; // 布局类型containerWidth: number; // 容器宽度containerHeight: number; // 容器高度avatarSize: number; // 基础头像尺寸spacing: number; // 头像间距maxAvatars: number; // 最大头像数量animationDuration: number; // 动画时长enableRotation: boolean; // 是否启用旋转enableScale: boolean; // 是否启用缩放}// 默认配置export class AvatarDefaultConfig {static readonly DEFAULT_LAYOUT_CONFIG: LayoutConfig = {type: ‘circle’,containerWidth: 300,containerHeight: 300,avatarSize: 60,spacing: 10,maxAvatars: 12,animationDuration: 500,enableRotation: true,enableScale: true};}这里定义了头像拼接系统的核心数据模型。AvatarData接口包含每个头像的所有属性,包括位置、样式和变换参数。LayoutConfig接口定义布局的配置参数,支持多种布局类型和动画效果。AvatarDefaultConfig提供默认配置值。三、核心实现3.1 头像项组件// AvatarItem.ets@Componentexport struct AvatarItem {@Prop avatar: AvatarData;@Prop layoutConfig: LayoutConfig;@Prop isDragging: boolean = false;@Prop onAvatarClick?: (avatar: AvatarData) => void;@Prop onAvatarLongPress?: (avatar: AvatarData) => void;@State private scale: number = 1;@State private rotation: number = 0;@State private glowEffect: boolean = false;private animationController: animation.Animator = new animation.Animator();aboutToAppear() {this.rotation = this.avatar.rotation;this.scale = this.avatar.scale;}AvatarItem组件是单个头像的展示单元。@Prop装饰器接收头像数据、布局配置和交互状态。@State装饰器管理动画相关的状态变量。animationController用于控制头像的动画效果。// 点击处理private onAvatarClickHandler(): void {// 点击动画this.animateClick();// 触发回调 this.onAvatarClick?.(this.avatar);}// 长按处理private onAvatarLongPressHandler(): void {// 长按动画this.animateLongPress();// 触发回调 this.onAvatarLongPress?.(this.avatar);}// 点击动画private animateClick(): void {this.animationController.stop();this.animationController.update({ duration: 200, curve: animation.Curve.EaseOut }); this.animationController.onFrame((progress: number) => { this.scale = 1 + 0.1 * Math.sin(progress * Math.PI); }); this.animationController.play();}// 长按动画private animateLongPress(): void {this.glowEffect = true;this.animationController.stop(); this.animationController.update({ duration: 300, curve: animation.Curve.EaseInOut }); this.animationController.onFrame((progress: number) => { this.rotation = 360 * progress; this.scale = 1 + 0.2 * progress; }); this.animationController.onFinish(() => { this.glowEffect = false; this.rotation = this.avatar.rotation; this.scale = this.avatar.scale; }); this.animationController.play();}onAvatarClickHandler和onAvatarLongPressHandler处理头像的交互事件。animateClick实现点击时的缩放动画,animateLongPress实现长按时的旋转和缩放动画,并添加发光效果。// 构建头像内容@Builderprivate buildAvatarContent() {Stack({ alignContent: Alignment.Center }) {// 背景圆形Circle().width(this.avatar.size).height(this.avatar.size).fill(this.avatar.color || ‘#4D94FF’).shadow(this.glowEffect ? {radius: 15,color: this.avatar.borderColor || ‘#4D94FF’,offsetX: 0,offsetY: 0} : null) // 头像图片 Image(this.avatar.image) .width(this.avatar.size - (this.avatar.borderWidth || 0) * 2) .height(this.avatar.size - (this.avatar.borderWidth || 0) * 2) .borderRadius(this.avatar.size / 2) .objectFit(ImageFit.Cover) .interpolation(ImageInterpolation.High) // 高质量插值 // 边框 if (this.avatar.borderWidth && this.avatar.borderWidth > 0) { Circle() .width(this.avatar.size) .height(this.avatar.size) .stroke(this.avatar.borderColor || '#FFFFFF') .strokeWidth(this.avatar.borderWidth) .fill(Color.Transparent) } // 用户姓名标签 if (this.avatar.name && this.isDragging) { Text(this.avatar.name) .fontSize(12) .fontColor(Color.White) .backgroundColor('#00000080') .padding({ left: 4, right: 4, top: 2, bottom: 2 }) .borderRadius(4) .position({ x: 0, y: this.avatar.size / 2 + 5 }) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) } }}buildAvatarContent方法构建头像的完整视觉表现。使用Stack布局叠加背景圆形、头像图片、边框和姓名标签。背景圆形支持发光效果,头像图片使用高质量插值,边框可定制,姓名标签在拖拽时显示。build() {Column().width(this.avatar.size).height(this.avatar.size).position({ x: this.avatar.x, y: this.avatar.y }).scale({ x: this.scale, y: this.scale }).rotate({ angle: this.rotation }).opacity(this.avatar.opacity).zIndex(this.avatar.zIndex).animation({duration: this.layoutConfig.animationDuration,curve: animation.Curve.EaseInOut}).onClick(() => this.onAvatarClickHandler()).gesture(LongPressGesture({ repeat: false }).onAction(() => this.onAvatarLongPressHandler())){this.buildAvatarContent()}}}build方法创建头像容器,应用位置、缩放、旋转、透明度和层级等变换效果。使用animation属性实现平滑的过渡动画。绑定点击和长按手势事件处理器。3.2 布局管理器// AvatarLayout.etsexport class AvatarLayout {private config: LayoutConfig;constructor(config: LayoutConfig) {this.config = config;}// 计算布局calculateLayout(avatars: AvatarData[]): AvatarData[] {const sortedAvatars = […avatars].sort((a, b) => a.zIndex - b.zIndex);switch (this.config.type) { case 'circle': return this.calculateCircleLayout(sortedAvatars); case 'grid': return this.calculateGridLayout(sortedAvatars); case 'overlap': return this.calculateOverlapLayout(sortedAvatars); case 'spiral': return this.calculateSpiralLayout(sortedAvatars); default: return sortedAvatars; }}AvatarLayout类负责计算不同布局模式下头像的位置。calculateLayout方法根据配置的布局类型调用对应的布局计算方法,首先对头像按zIndex排序确保正确的层级关系。// 圆形布局private calculateCircleLayout(avatars: AvatarData[]): AvatarData[] {const centerX = this.config.containerWidth / 2;const centerY = this.config.containerHeight / 2;const radius = Math.min(centerX, centerY) - this.config.avatarSize / 2;return avatars.map((avatar, index) => { const angle = (index / avatars.length) * Math.PI * 2; const x = centerX + Math.cos(angle) * radius - avatar.size / 2; const y = centerY + Math.sin(angle) * radius - avatar.size / 2; return { ...avatar, x, y, rotation: this.config.enableRotation ? angle * 180 / Math.PI : 0, scale: this.config.enableScale ? 1 + Math.sin(angle) * 0.1 : 1 }; });}// 网格布局private calculateGridLayout(avatars: AvatarData[]): AvatarData[] {const cols = Math.ceil(Math.sqrt(avatars.length));const rows = Math.ceil(avatars.length / cols);const cellWidth = this.config.containerWidth / cols;const cellHeight = this.config.containerHeight / rows;return avatars.map((avatar, index) => { const col = index % cols; const row = Math.floor(index / cols); const x = col * cellWidth + (cellWidth - avatar.size) / 2; const y = row * cellHeight + (cellHeight - avatar.size) / 2; return { ...avatar, x, y, rotation: 0, scale: 1 }; });}calculateCircleLayout方法实现圆形布局,将头像均匀分布在圆周上,支持根据角度调整旋转和缩放。calculateGridLayout方法实现网格布局,将头像排列在等分的网格中,确保均匀分布。// 重叠布局private calculateOverlapLayout(avatars: AvatarData[]): AvatarData[] {const centerX = this.config.containerWidth / 2;const centerY = this.config.containerHeight / 2;const maxOffset = this.config.avatarSize * 0.3;return avatars.map((avatar, index) => { const angle = (index / avatars.length) * Math.PI * 2; const offsetX = Math.cos(angle) * maxOffset; const offsetY = Math.sin(angle) * maxOffset; const x = centerX + offsetX - avatar.size / 2; const y = centerY + offsetY - avatar.size / 2; return { ...avatar, x, y, rotation: this.config.enableRotation ? index * 15 : 0, scale: this.config.enableScale ? 1 - index * 0.05 : 1, zIndex: avatars.length - index }; });}// 螺旋布局private calculateSpiralLayout(avatars: AvatarData[]): AvatarData[] {const centerX = this.config.containerWidth / 2;const centerY = this.config.containerHeight / 2;const maxRadius = Math.min(centerX, centerY) - this.config.avatarSize / 2;return avatars.map((avatar, index) => { const spiralProgress = index / Math.max(avatars.length - 1, 1); const angle = spiralProgress * Math.PI * 6; // 3圈螺旋 const radius = spiralProgress * maxRadius; const x = centerX + Math.cos(angle) * radius - avatar.size / 2; const y = centerY + Math.sin(angle) * radius - avatar.size / 2; return { ...avatar, x, y, rotation: this.config.enableRotation ? angle * 180 / Math.PI : 0, scale: this.config.enableScale ? 0.7 + spiralProgress * 0.3 : 1 }; });}// 添加新头像时的布局动画calculateEntryAnimation(avatar: AvatarData, index: number): AvatarData {const centerX = this.config.containerWidth / 2;const centerY = this.config.containerHeight / 2;return { ...avatar, x: centerX - avatar.size / 2, y: centerY - avatar.size / 2, scale: 0, opacity: 0, rotation: 360 };}}calculateOverlapLayout方法实现重叠布局,头像从中心向外轻微偏移,后添加的头像层级更高。calculateSpiralLayout方法实现螺旋布局,头像沿螺旋线排列。calculateEntryAnimation方法计算新头像的入场动画起始状态,从中心缩放进入。3.3 Canvas组合渲染// AvatarCanvas.ets@Componentexport struct AvatarCanvas {private canvasRef: CanvasRenderingContext2D | null = null;private canvasWidth: number = 300;private canvasHeight: number = 300;@Prop avatars: AvatarData[] = [];@Prop layoutConfig: LayoutConfig = AvatarDefaultConfig.DEFAULT_LAYOUT_CONFIG;@Prop onCanvasReady?: (context: CanvasRenderingContext2D) => void;// Canvas就绪回调private onCanvasReadyCallback(context: CanvasRenderingContext2D): void {this.canvasRef = context;const canvas = this.canvasRef.canvas;this.canvasWidth = canvas.width;this.canvasHeight = canvas.height;this.onCanvasReady?.(context); this.render();}AvatarCanvas组件使用Canvas 2D API渲染头像组合效果。canvasRef存储Canvas上下文,onCanvasReadyCallback在Canvas就绪时初始化并开始渲染。// 渲染头像组合private render(): void {if (!this.canvasRef) return;const ctx = this.canvasRef; // 清除画布 ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight); // 绘制背景 this.drawBackground(ctx); // 绘制所有头像 for (const avatar of this.avatars) { this.drawAvatar(ctx, avatar); } // 绘制组合效果 this.drawCompositeEffect(ctx);}// 绘制背景private drawBackground(ctx: CanvasRenderingContext2D): void {// 创建径向渐变背景const gradient = ctx.createRadialGradient(this.canvasWidth / 2,this.canvasHeight / 2,0,this.canvasWidth / 2,this.canvasHeight / 2,Math.max(this.canvasWidth, this.canvasHeight) / 2);gradient.addColorStop(0, '#1A1A2E'); gradient.addColorStop(1, '#16213E'); ctx.fillStyle = gradient; ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight);}render方法执行完整的绘制流程,包括清除画布、绘制背景、绘制头像和组合效果。drawBackground方法创建径向渐变背景,从中心向外颜色变深。// 绘制单个头像private drawAvatar(ctx: CanvasRenderingContext2D, avatar: AvatarData): void {ctx.save();// 应用变换 ctx.translate(avatar.x + avatar.size / 2, avatar.y + avatar.size / 2); ctx.rotate(avatar.rotation * Math.PI / 180); ctx.scale(avatar.scale, avatar.scale); ctx.globalAlpha = avatar.opacity; // 绘制头像背景 ctx.beginPath(); ctx.arc(0, 0, avatar.size / 2, 0, Math.PI * 2); if (avatar.color) { ctx.fillStyle = avatar.color.toString(); ctx.fill(); } // 绘制头像图片(简化实现) // 在实际应用中,这里需要将Resource转换为ImageBitmap this.drawAvatarImage(ctx, avatar); // 绘制边框 if (avatar.borderWidth && avatar.borderWidth > 0) { ctx.beginPath(); ctx.arc(0, 0, avatar.size / 2, 0, Math.PI * 2); ctx.lineWidth = avatar.borderWidth; ctx.strokeStyle = avatar.borderColor?.toString() || '#FFFFFF'; ctx.stroke(); } ctx.restore();}// 绘制头像图片(简化实现)private drawAvatarImage(ctx: CanvasRenderingContext2D, avatar: AvatarData): void {// 在实际实现中,这里需要将Resource转换为ImageBitmap并绘制// 这里使用占位符圆形代替ctx.beginPath();ctx.arc(0, 0, avatar.size / 2 - (avatar.borderWidth || 0), 0, Math.PI * 2);// 创建头像内渐变 const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, avatar.size / 2); gradient.addColorStop(0, '#4D94FF'); gradient.addColorStop(1, '#1A5FB4'); ctx.fillStyle = gradient; ctx.fill(); // 绘制用户首字母 if (avatar.name) { const firstChar = avatar.name.charAt(0).toUpperCase(); ctx.fillStyle = '#FFFFFF'; ctx.font = `${avatar.size / 3}px sans-serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(firstChar, 0, 0); }}drawAvatar方法绘制单个头像,应用变换矩阵实现位置、旋转、缩放和透明度效果。drawAvatarImage方法简化实现头像图片绘制,实际应用中需要将Resource转换为ImageBitmap。// 绘制组合效果private drawCompositeEffect(ctx: CanvasRenderingContext2D): void {if (this.avatars.length < 2) return;// 绘制连接线 this.drawConnectionLines(ctx); // 绘制中心聚合效果 this.drawCenterEffect(ctx);}// 绘制连接线private drawConnectionLines(ctx: CanvasRenderingContext2D): void {const centerX = this.canvasWidth / 2;const centerY = this.canvasHeight / 2;for (const avatar of this.avatars) { const avatarCenterX = avatar.x + avatar.size / 2; const avatarCenterY = avatar.y + avatar.size / 2; ctx.beginPath(); ctx.moveTo(centerX, centerY); ctx.lineTo(avatarCenterX, avatarCenterY); // 创建渐变线条 const gradient = ctx.createLinearGradient( centerX, centerY, avatarCenterX, avatarCenterY ); gradient.addColorStop(0, '#4D94FF00'); gradient.addColorStop(0.5, '#4D94FF80'); gradient.addColorStop(1, '#4D94FF00'); ctx.strokeStyle = gradient; ctx.lineWidth = 1; ctx.stroke(); }}// 绘制中心聚合效果private drawCenterEffect(ctx: CanvasRenderingContext2D): void {const centerX = this.canvasWidth / 2;const centerY = this.canvasHeight / 2;// 绘制中心发光点 ctx.beginPath(); ctx.arc(centerX, centerY, 10, 0, Math.PI * 2); const gradient = ctx.createRadialGradient( centerX, centerY, 0, centerX, centerY, 30 ); gradient.addColorStop(0, '#4D94FFFF'); gradient.addColorStop(1, '#4D94FF00'); ctx.fillStyle = gradient; ctx.fill();}drawCompositeEffect方法绘制头像之间的组合效果。drawConnectionLines绘制从中心到每个头像的连接线,使用渐变线条创造流动感。drawCenterEffect绘制中心发光点,增强视觉焦点。aboutToUpdate() {// 数据更新时重新渲染this.render();}build() {Canvas(this.canvasRef).width(this.layoutConfig.containerWidth).height(this.layoutConfig.containerHeight).backgroundColor(‘#1A1A2E’).onReady((context: CanvasRenderingContext2D) => {this.onCanvasReadyCallback(context);})}}aboutToUpdate生命周期在数据更新时触发重新渲染。build方法创建Canvas组件,设置尺寸和背景色,绑定onReady回调。3.4 主组件 - 头像拼接器// AvatarComposer.ets@Entry@Componentexport struct AvatarComposer {@State private avatars: AvatarData[] = [];@State private selectedAvatar: AvatarData | null = null;@State private isEditing: boolean = false;@State private showCanvas: boolean = false;@State private layoutConfig: LayoutConfig = {…AvatarDefaultConfig.DEFAULT_LAYOUT_CONFIG,containerWidth: 300,containerHeight: 300};private avatarLayout: AvatarLayout = new AvatarLayout(this.layoutConfig);private animationController: animation.Animator = new animation.Animator();// 示例头像数据private sampleAvatars: AvatarData[] = [{id: ‘1’,image: $r(‘app.media.avatar1’),name: ‘张三’,color: ‘#FF6B6B’,borderColor: ‘#FFFFFF’,borderWidth: 2,size: 60,x: 0, y: 0, zIndex: 1, rotation: 0, scale: 1, opacity: 1},{id: ‘2’,image: $r(‘app.media.avatar2’),name: ‘李四’,color: ‘#4ECDC4’,borderColor: ‘#FFFFFF’,borderWidth: 2,size: 60,x: 0, y: 0, zIndex: 2, rotation: 0, scale: 1, opacity: 1},// 更多示例头像…];AvatarComposer是主入口组件,管理所有头像数据和布局状态。@State装饰器管理头像数组、选中状态、编辑模式等。avatarLayout实例处理布局计算,sampleAvatars提供示例数据。aboutToAppear() {// 初始化示例数据this.addSampleAvatars();}// 添加示例头像private addSampleAvatars(): void {this.sampleAvatars.forEach((avatar, index) => {setTimeout(() => {this.addAvatar(avatar);}, index * 200);});}// 添加新头像private addAvatar(avatarData: AvatarData): void {if (this.avatars.length >= this.layoutConfig.maxAvatars) {console.warn(‘已达到最大头像数量’);return;}const newAvatar = { ...avatarData, zIndex: this.avatars.length + 1 }; // 计算入场动画起始状态 const entryAvatar = this.avatarLayout.calculateEntryAnimation(newAvatar, this.avatars.length); this.avatars = [...this.avatars, entryAvatar]; // 执行入场动画 setTimeout(() => { this.updateLayout(); }, 50);}aboutToAppear生命周期初始化示例头像。addSampleAvatars方法延迟添加示例头像,创造逐个入场的效果。addAvatar方法添加新头像,计算入场动画起始状态,然后更新布局。// 删除头像private removeAvatar(avatarId: string): void {const avatarIndex = this.avatars.findIndex(avatar => avatar.id === avatarId);if (avatarIndex === -1) return;// 执行退场动画 this.avatars[avatarIndex].opacity = 0; this.avatars[avatarIndex].scale = 0; setTimeout(() => { this.avatars = this.avatars.filter(avatar => avatar.id !== avatarId); this.updateLayout(); }, this.layoutConfig.animationDuration);}// 更新布局private updateLayout(): void {const newLayout = this.avatarLayout.calculateLayout(this.avatars);animateTo({ duration: this.layoutConfig.animationDuration, curve: animation.Curve.EaseInOut }, () => { this.avatars = newLayout.map((avatar, index) => ({ ...avatar, opacity: 1, scale: avatar.scale })); });}removeAvatar方法删除指定头像,先执行退场动画(透明度变为0,缩放为0),然后从数组中移除。updateLayout方法重新计算布局,使用animateTo实现平滑的布局过渡动画。// 切换布局模式private switchLayoutType(type: LayoutConfig[‘type’]): void {this.layoutConfig.type = type;this.updateLayout();}// 头像点击处理private onAvatarClick(avatar: AvatarData): void {if (this.isEditing) {this.selectedAvatar = avatar;} else {// 非编辑模式下的点击效果this.animateAvatarFocus(avatar);}}// 头像长按处理private onAvatarLongPress(avatar: AvatarData): void {this.isEditing = true;this.selectedAvatar = avatar;}// 头像聚焦动画private animateAvatarFocus(avatar: AvatarData): void {const originalZIndex = avatar.zIndex;// 临时提升层级 avatar.zIndex = 1000; this.animationController.stop(); this.animationController.update({ duration: 300, curve: animation.Curve.EaseInOut }); this.animationController.onFrame((progress: number) => { avatar.scale = 1 + 0.2 * Math.sin(progress * Math.PI); }); this.animationController.onFinish(() => { avatar.zIndex = originalZIndex; avatar.scale = 1; }); this.animationController.play();}switchLayoutType方法切换布局模式。onAvatarClick处理头像点击,编辑模式下选中头像,非编辑模式下执行聚焦动画。onAvatarLongPress进入编辑模式并选中头像。animateAvatarFocus实现头像聚焦动画,临时提升层级并执行缩放效果。// 构建头像容器@Builderprivate buildAvatarContainer() {Stack().width(this.layoutConfig.containerWidth).height(this.layoutConfig.containerHeight).backgroundColor(‘#FFFFFF08’).border({ width: 1, color: ‘#FFFFFF20’, style: BorderStyle.Dashed }).borderRadius(12).overlay(// 头像项Stack() {ForEach(this.avatars, (avatar: AvatarData) => {AvatarItem({avatar: avatar,layoutConfig: this.layoutConfig,isDragging: this.isEditing,onAvatarClick: this.onAvatarClick.bind(this),onAvatarLongPress: this.onAvatarLongPress.bind(this)})})}).overlay(// 空状态提示this.avatars.length === 0 ?Text(‘点击下方按钮添加头像’).fontSize(16).fontColor(‘#FFFFFF60’).textAlign(TextAlign.Center) : null)}buildAvatarContainer方法构建头像显示容器,使用Stack布局叠加头像项和空状态提示。容器有半透明背景和虚线边框,编辑模式下显示拖拽状态。// 构建控制面板@Builderprivate buildControlPanel() {Column({ space: 12 }) {// 布局模式选择Text(‘布局模式’).fontSize(16).fontColor(Color.White).fontWeight(FontWeight.Medium).margin({ bottom: 8 }) Row({ space: 8 }) { ForEach(['circle', 'grid', 'overlap', 'spiral'] as LayoutConfig['type'][], (type: LayoutConfig['type']) => { Button(this.getLayoutTypeText(type)) .layoutWeight(1) .height(32) .fontSize(12) .backgroundColor(this.layoutConfig.type === type ? '#4D94FF' : '#FFFFFF20') .onClick(() => this.switchLayoutType(type)) } ) } // 编辑控制 Row({ space: 8 }) { Button(this.isEditing ? '完成编辑' : '开始编辑') .layoutWeight(1) .backgroundColor(this.isEditing ? '#96CEB4' : '#4D94FF') .onClick(() => { this.isEditing = !this.isEditing; if (!this.isEditing) this.selectedAvatar = null; }) Button('清空所有') .layoutWeight(1) .backgroundColor('#FF6B6B') .onClick(() => { this.avatars = []; }) } // 添加头像按钮 if (this.avatars.length < this.layoutConfig.maxAvatars) { Button('添加头像') .width('100%') .backgroundColor('#FFEAA7') .fontColor('#333333') .onClick(() => { // 随机选择一个示例头像 const randomAvatar = this.sampleAvatars[ Math.floor(Math.random() * this.sampleAvatars.length) ]; this.addAvatar({ ...randomAvatar, id: Date.now().toString() }); }) } } .width('100%') .padding(16) .backgroundColor('#FFFFFF10') .borderRadius(12)}buildControlPanel方法构建控制面板,包含布局模式选择按钮、编辑控制按钮和添加头像按钮。布局模式按钮高亮显示当前选中的模式,编辑按钮切换编辑状态,清空按钮重置所有头像。// 构建Canvas导出面板@Builderprivate buildCanvasPanel() {Column({ space: 12 }) {Text(‘导出效果’).fontSize(16).fontColor(Color.White).fontWeight(FontWeight.Medium) Button('生成组合头像') .width('100%') .backgroundColor('#DDA0DD') .onClick(() => { this.showCanvas = true; }) if (this.showCanvas) { AvatarCanvas({ avatars: this.avatars, layoutConfig: this.layoutConfig }) .width(300) .height(300) .margin({ top: 12 }) Button('保存图片') .width('100%') .margin({ top: 12 }) .onClick(() => { // 在实际应用中,这里应该实现Canvas保存为图片的功能 console.log('保存组合头像图片'); }) } } .width('100%') .padding(16) .backgroundColor('#FFFFFF10') .borderRadius(12) .margin({ top: 20 })}buildCanvasPanel方法构建Canvas导出面板,可以生成和保存组合头像图片。点击"生成组合头像"显示Canvas渲染效果,"保存图片"按钮在实际应用中应实现图片保存功能。// 获取布局模式文本private getLayoutTypeText(type: LayoutConfig[‘type’]): string {switch (type) {case ‘circle’: return ‘圆形’;case ‘grid’: return ‘网格’;case ‘overlap’: return ‘重叠’;case ‘spiral’: return ‘螺旋’;default: return ‘未知’;}}build() {Column({ space: 20 }) {// 标题Text(‘头像拼接特效’).fontSize(24).fontColor(Color.White).fontWeight(FontWeight.Bold).margin({ top: 40, bottom: 20 }) // 头像容器 this.buildAvatarContainer() // 控制面板 this.buildControlPanel() // Canvas导出面板 this.buildCanvasPanel() // 使用说明 Text('使用说明:长按头像进入编辑模式,点击布局模式切换排列方式,点击添加头像增加新成员。') .fontSize(12) .fontColor('#FFFFFF80') .textAlign(TextAlign.Center) .margin({ top: 20, left: 20, right: 20 }) .multilineTextAlignment(TextAlign.Center) } .width('100%') .height('100%') .justifyContent(FlexAlign.Start) .padding(20) .backgroundImage('/resources/base/media/avatar_bg.png') .backgroundImageSize(ImageSize.Cover)}}build方法构建完整的应用界面,包含标题、头像容器、控制面板、Canvas导出面板和使用说明。整个界面使用深色主题背景,布局清晰直观。四、高级特性4.1 拖拽排序// DragManager.etsexport class AvatarDragManager {private draggingAvatar: AvatarData | null = null;private startX: number = 0;private startY: number = 0;private originalX: number = 0;private originalY: number = 0;// 开始拖拽startDrag(avatar: AvatarData, startX: number, startY: number): void {this.draggingAvatar = avatar;this.startX = startX;this.startY = startY;this.originalX = avatar.x;this.originalY = avatar.y;}// 更新拖拽位置updateDrag(x: number, y: number): void {if (!this.draggingAvatar) return;const deltaX = x - this.startX; const deltaY = y - this.startY; this.draggingAvatar.x = this.originalX + deltaX; this.draggingAvatar.y = this.originalY + deltaY;}// 结束拖拽endDrag(): AvatarData | null {const avatar = this.draggingAvatar;this.draggingAvatar = null;return avatar;}// 交换位置swapAvatars(avatar1: AvatarData, avatar2: AvatarData): void {const tempX = avatar1.x;const tempY = avatar1.y;avatar1.x = avatar2.x; avatar1.y = avatar2.y; avatar2.x = tempX; avatar2.y = tempY;}}AvatarDragManager类实现头像拖拽排序功能。startDrag记录拖拽起始状态,updateDrag更新头像位置,endDrag结束拖拽,swapAvatars交换两个头像的位置。4.2 头像裁剪和美化// AvatarEditor.ets@Componentexport struct AvatarEditor {@Prop avatar: AvatarData;@Prop onSave: (avatar: AvatarData) => void;@Prop onCancel: () => void;@State private cropRect: { x: number; y: number; width: number; height: number } = { x: 0, y: 0, width: 100, height: 100 };@State private selectedBorderColor: ResourceColor = ‘#FFFFFF’;@State private selectedBackgroundColor: ResourceColor = ‘#4D94FF’;// 应用裁剪private applyCrop(): void {// 在实际应用中,这里应该实现图像裁剪功能console.log(‘应用头像裁剪’);}// 应用边框private applyBorder(): void {this.avatar.borderColor = this.selectedBorderColor;this.avatar.borderWidth = 2;}// 应用背景色private applyBackground(): void {this.avatar.color = this.selectedBackgroundColor;}build() {Column({ space: 20 }) {Text(‘头像编辑’).fontSize(20).fontColor(Color.White).fontWeight(FontWeight.Bold) // 裁剪预览 Stack() .width(200) .height(200) .backgroundColor('#FFFFFF20') .overlay( Image(this.avatar.image) .width('100%') .height('100%') .objectFit(ImageFit.Cover) ) // 边框颜色选择 Text('边框颜色') .fontSize(16) .fontColor(Color.White) .alignSelf(ItemAlign.Start) Row({ space: 8 }) { ForEach(['#FFFFFF', '#4D94FF', '#FF6B6B', '#4ECDC4', '#FFEAA7'], (color: string) => { Circle() .width(24) .height(24) .fill(color) .border({ width: this.selectedBorderColor === color ? 2 : 0, color: Color.White }) .onClick(() => { this.selectedBorderColor = color; }) } ) } // 背景颜色选择 Text('背景颜色') .fontSize(16) .fontColor(Color.White) .alignSelf(ItemAlign.Start) Row({ space: 8 }) { ForEach(['#4D94FF', '#1A1A2E', '#FF6B6B', '#96CEB4', '#DDA0DD'], (color: string) => { Circle() .width(24) .height(24) .fill(color) .border({ width: this.selectedBackgroundColor === color ? 2 : 0, color: Color.White }) .onClick(() => { this.selectedBackgroundColor = color; }) } ) } // 操作按钮 Row({ space: 12 }) { Button('取消') .layoutWeight(1) .backgroundColor('#FF6B6B') .onClick(() => this.onCancel()) Button('保存') .layoutWeight(1) .backgroundColor('#96CEB4') .onClick(() => { this.applyCrop(); this.applyBorder(); this.applyBackground(); this.onSave(this.avatar); }) } } .width('100%') .padding(20) .backgroundColor('#1A1A2E')}}AvatarEditor组件提供头像编辑功能,包括裁剪、边框颜色选择和背景颜色选择。用户可以选择不同的边框和背景颜色,预览效果后保存修改。五、最佳实践5.1 性能优化建议头像图片优化:使用合适尺寸的图片,避免内存浪费动画性能:使用硬件加速的transform属性内存管理:及时清理不再使用的头像资源渲染优化:对不可见区域的头像进行懒渲染性能优化包括:1)头像图片尺寸适配显示需求;2)优先使用transform实现动画;3)移除头像时清理相关资源;4)大数据量时实现虚拟滚动。5.2 用户体验优化视觉反馈:拖拽时提供明确的视觉提示动画过渡:所有状态变化都有平滑动画错误处理:超出限制时的友好提示操作引导:清晰的操作指引用户体验优化包括:1)拖拽时显示位置提示;2)状态变化使用动画过渡;3)超出最大头像数量时显示提示;4)提供明确的操作指引。5.3 可访问性// 为屏幕阅读器提供支持.accessibilityLabel(‘头像拼接编辑器’).accessibilityHint(‘长按头像进入编辑模式,拖拽调整位置’).accessibilityRole(AccessibilityRole.Adjustable).accessibilityState({disabled: this.isEditing,selected: this.selectedAvatar !== null})可访问性支持为视障用户提供语音反馈,描述组件功能、操作提示和当前状态。六、总结本实现方案提供了完整的头像拼接特效,支持多种布局模式、丰富的交互效果和导出功能,通过HarmonyOS最新API实现了高性能的头像组合效果。头像拼接技术适用于多种场景:团队介绍页面、社交应用的群组头像、活动宣传的参与者展示、个人创意作品、教育应用的班级展示等。
-
1.1 问题说明问题场景在鸿蒙应用开发中,PDF文件预览功能存在以下问题:兼容性问题:不同格式的PDF文件在部分设备上显示异常性能瓶颈:大体积PDF文件加载缓慢,内存占用过高代码复用性差:各项目自行实现,代码重复且维护困难1.2 解决方案方案一:基于PDF.js的优化方案// PDF预览组件核心代码import { PDFDocument } from '@ohos/pdf-parser';@Componentexport struct PdfPreviewComponent { @State currentPage: number = 1; @State totalPages: number = 0; @State pageImages: Array<ImageBitmap> = []; private pdfDoc: PDFDocument | null = null; // 配置参数 @Prop options: PdfOptions = { enableCache: true, maxCacheSize: 100, // MB renderQuality: 'high', enableAnnotations: true }; // 初始化PDF文档 async aboutToAppear() { await this.loadPdfDocument(); } // 加载PDF文档 async loadPdfDocument(filePath: string) { try { this.pdfDoc = await PDFDocument.load(filePath); this.totalPages = this.pdfDoc.getPageCount(); await this.renderPage(1); // 渲染第一页 this.preloadAdjacentPages(); // 预加载相邻页面 } catch (error) { console.error('PDF加载失败:', error); } } // 渲染单页PDF async renderPage(pageNum: number) { if (!this.pdfDoc || pageNum < 1 || pageNum > this.totalPages) { return; } const page = await this.pdfDoc.getPage(pageNum); const viewport = page.getViewport({ scale: 2.0 }); // 创建Canvas渲染 const canvas = new OffscreenCanvas(viewport.width, viewport.height); const context = canvas.getContext('2d'); const renderContext = { canvasContext: context, viewport: viewport }; await page.render(renderContext).promise; // 转换为ImageBitmap const imageBitmap = await createImageBitmap(canvas); // 更新状态(带缓存检查) if (!this.options.enableCache || !this.pageImages[pageNum - 1]) { this.pageImages[pageNum - 1] = imageBitmap; } this.currentPage = pageNum; } // 预加载相邻页面 async preloadAdjacentPages() { const pagesToPreload = [this.currentPage + 1, this.currentPage + 2]; for (const pageNum of pagesToPreload) { if (pageNum <= this.totalPages && !this.pageImages[pageNum - 1]) { setTimeout(() => this.renderPage(pageNum), 100); } } } // 构建UI build() { Column() { // 工具栏 ToolbarComponent({ currentPage: this.currentPage, totalPages: this.totalPages, onPageChange: (page) => this.onPageChange(page), onSearch: (text) => this.searchText(text) }) // 页面显示区域 Scroll() { Column() { if (this.pageImages[this.currentPage - 1]) { Image(this.pageImages[this.currentPage - 1]) .width('100%') .height('auto') .objectFit(ImageFit.Contain) } else { LoadingComponent() } } .padding(10) } .height('80%') // 页面缩略图导航 if (this.options.showThumbnails) { ThumbnailBarComponent({ pageImages: this.pageImages, currentPage: this.currentPage, onThumbnailClick: (page) => this.jumpToPage(page) }) } } } // 页面跳转 private onPageChange(pageNum: number) { if (pageNum >= 1 && pageNum <= this.totalPages) { this.renderPage(pageNum); } } // 文本搜索 private async searchText(text: string) { if (!this.pdfDoc) return; const searchResults = []; for (let i = 1; i <= this.totalPages; i++) { const page = await this.pdfDoc.getPage(i); const textContent = await page.getTextContent(); textContent.items.forEach((item, index) => { if (item.str.includes(text)) { searchResults.push({ page: i, index: index, text: item.str }); } }); } return searchResults; }}方案二:缓存管理策略// PDF缓存管理器export class PdfCacheManager { private static instance: PdfCacheManager; private cache: Map<string, CacheEntry> = new Map(); private maxSize: number = 100 * 1024 * 1024; // 100MB private currentSize: number = 0; static getInstance(): PdfCacheManager { if (!PdfCacheManager.instance) { PdfCacheManager.instance = new PdfCacheManager(); } return PdfCacheManager.instance; } // 缓存PDF页面 async cachePage(pdfId: string, pageNum: number, data: ImageBitmap): Promise<void> { const cacheKey = `${pdfId}_${pageNum}`; const size = await this.calculateSize(data); // 检查缓存空间 if (this.currentSize + size > this.maxSize) { this.clearOldCache(); } this.cache.set(cacheKey, { data: data, timestamp: Date.now(), size: size }); this.currentSize += size; } // 获取缓存页面 getPage(pdfId: string, pageNum: number): ImageBitmap | null { const cacheKey = `${pdfId}_${pageNum}`; const entry = this.cache.get(cacheKey); if (entry) { entry.timestamp = Date.now(); // 更新访问时间 return entry.data; } return null; } // 清理旧缓存 private clearOldCache(): void { const entries = Array.from(this.cache.entries()) .sort((a, b) => a[1].timestamp - b[1].timestamp); let clearedSize = 0; const targetSize = this.maxSize * 0.3; // 清理30%的空间 for (const [key, entry] of entries) { if (clearedSize >= targetSize) break; this.cache.delete(key); clearedSize += entry.size; this.currentSize -= entry.size; } } private async calculateSize(image: ImageBitmap): Promise<number> { // 估算图片大小 return image.width * image.height * 4; // RGBA 4字节 }}方案三:PDF配置类// 配置接口export interface PdfOptions { // 显示配置 defaultScale?: number; maxScale?: number; minScale?: number; // 功能配置 enableSearch?: boolean; enableAnnotations?: boolean; enableBookmarks?: boolean; enablePrint?: boolean; // 性能配置 enableCache?: boolean; maxCacheSize?: number; preloadPages?: number; renderQuality?: 'low' | 'medium' | 'high'; // UI配置 showToolbar?: boolean; showThumbnails?: boolean; showPageNumbers?: boolean; theme?: 'light' | 'dark';}// 默认配置export const DefaultPdfOptions: PdfOptions = { defaultScale: 1.0, maxScale: 3.0, minScale: 0.5, enableSearch: true, enableAnnotations: false, enableBookmarks: true, enablePrint: true, enableCache: true, maxCacheSize: 100, // MB preloadPages: 2, renderQuality: 'medium', showToolbar: true, showThumbnails: true, showPageNumbers: true, theme: 'light'};方案四:集成使用示例// 主页面使用示例@Entry@Componentstruct MainPage { @State pdfPath: string = '/data/storage/el1/base/files/sample.pdf'; build() { Column() { // 使用PDF预览组件 PdfPreviewComponent({ filePath: this.pdfPath, options: { enableSearch: true, enableBookmarks: true, showThumbnails: true, enableCache: true, maxCacheSize: 150, theme: 'dark' } }) // 操作按钮 Row() { Button('打开PDF') .onClick(() => this.openFilePicker()) Button('搜索文本') .onClick(() => this.showSearchDialog()) Button('添加书签') .onClick(() => this.addBookmark()) } .padding(10) .justifyContent(FlexAlign.SpaceAround) } } private async openFilePicker() { // 文件选择逻辑 const filePicker = new FilePicker(); const result = await filePicker.pick({ type: ['pdf'], multiple: false }); if (result && result.length > 0) { this.pdfPath = result[0].path; } }}1.3 结果展示可复用资产核心组件PdfPreviewComponent:主预览组件PdfCacheManager:缓存管理器PdfSearchService:搜索服务工具类库pdf-utils.ets:工具函数集合pdf-types.d.ts:类型定义pdf-constants.ets:常量定义配置文件pdf-config.json:默认配置theme-config.json:主题配置
-
鸿蒙Canvas实战案例优化方案1.1 问题说明:清晰呈现问题场景与具体表现问题场景在鸿蒙应用开发中,开发者在使用Canvas进行图形绘制时经常遇到以下问题:具体表现:性能瓶颈:复杂图形绘制时帧率下降明显,页面卡顿代码冗余:每个图形绘制都需要重复编写基础代码维护困难:绘制逻辑分散,难以统一管理1.2 解决方案:落地解决思路,给出可执行、可复用的具体方案方案一:Canvas渲染引擎核心类// CanvasEngine.ets - 核心渲染引擎import { CanvasRenderingContext2D } from '@ohos.graphics';export class CanvasEngine { private context: CanvasRenderingContext2D; private elements: CanvasElement[] = []; private dirtyRects: Rect[] = []; private animationFrameId: number = 0; private lastRenderTime: number = 0; // 资源池 private gradientCache: Map<string, CanvasGradient> = new Map(); private patternCache: Map<string, CanvasPattern> = new Map(); constructor(canvasId: string) { this.initCanvas(canvasId); } // 初始化Canvas private async initCanvas(canvasId: string): Promise<void> { const canvas = await this.getCanvas(canvasId); this.context = canvas.getContext('2d') as CanvasRenderingContext2D; this.startRenderLoop(); } // 添加图形元素 addElement(element: CanvasElement): void { this.elements.push(element); this.markDirty(element.getBounds()); } // 标记脏矩形 markDirty(rect: Rect): void { this.dirtyRects.push(rect); } // 智能渲染循环 private startRenderLoop(): void { const render = (timestamp: number) => { if (this.dirtyRects.length > 0 || this.elements.some(el => el.needsRedraw())) { this.renderFrame(); } this.animationFrameId = requestAnimationFrame(render); }; this.animationFrameId = requestAnimationFrame(render); } // 帧渲染 private renderFrame(): void { // 合并脏矩形 const mergedRect = this.mergeDirtyRects(); // 设置裁剪区域 this.context.save(); this.context.beginPath(); this.context.rect(mergedRect.x, mergedRect.y, mergedRect.width, mergedRect.height); this.context.clip(); // 清除脏矩形区域 this.context.clearRect(mergedRect.x, mergedRect.y, mergedRect.width, mergedRect.height); // 只渲染在可视区域内的元素 const visibleElements = this.elements.filter(el => this.isElementVisible(el, mergedRect)); // 按z-index排序 visibleElements.sort((a, b) => a.zIndex - b.zIndex); // 批量渲染 visibleElements.forEach(element => { this.context.save(); element.render(this.context); this.context.restore(); }); this.context.restore(); this.dirtyRects = []; } // 合并脏矩形优化 private mergeDirtyRects(): Rect { if (this.dirtyRects.length === 0) { return { x: 0, y: 0, width: 0, height: 0 }; } let minX = Infinity, minY = Infinity; let maxX = -Infinity, maxY = -Infinity; this.dirtyRects.forEach(rect => { minX = Math.min(minX, rect.x); minY = Math.min(minY, rect.y); maxX = Math.max(maxX, rect.x + rect.width); maxY = Math.max(maxY, rect.y + rect.height); }); return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }; }}方案二:图形元素基类与具体实现// CanvasElement.ets - 图形元素基类export abstract class CanvasElement { x: number = 0; y: number = 0; width: number = 0; height: number = 0; zIndex: number = 0; visible: boolean = true; protected needsRedrawFlag: boolean = true; abstract render(ctx: CanvasRenderingContext2D): void; abstract getBounds(): Rect; abstract hitTest(x: number, y: number): boolean; needsRedraw(): boolean { return this.needsRedrawFlag; } markForRedraw(): void { this.needsRedrawFlag = true; } updatePosition(x: number, y: number): void { const oldBounds = this.getBounds(); this.x = x; this.y = y; this.markDirtyWithHistory(oldBounds); } protected markDirtyWithHistory(oldBounds: Rect): void { const newBounds = this.getBounds(); // 标记新旧区域都需要重绘 CanvasEngine.getInstance().markDirty(oldBounds); CanvasEngine.getInstance().markDirty(newBounds); }}// RectangleElement.ets - 矩形元素export class RectangleElement extends CanvasElement { fillColor: string = '#000000'; strokeColor: string = '#000000'; strokeWidth: number = 1; cornerRadius: number = 0; constructor(config: Partial<RectangleElement>) { super(); Object.assign(this, config); } render(ctx: CanvasRenderingContext2D): void { if (!this.visible) return; ctx.save(); if (this.cornerRadius > 0) { // 圆角矩形 this.drawRoundedRect(ctx); } else { // 普通矩形 ctx.beginPath(); ctx.rect(this.x, this.y, this.width, this.height); } if (this.fillColor) { ctx.fillStyle = this.fillColor; ctx.fill(); } if (this.strokeColor && this.strokeWidth > 0) { ctx.strokeStyle = this.strokeColor; ctx.lineWidth = this.strokeWidth; ctx.stroke(); } ctx.restore(); this.needsRedrawFlag = false; } private drawRoundedRect(ctx: CanvasRenderingContext2D): void { const r = this.cornerRadius; const x = this.x; const y = this.y; const w = this.width; const h = this.height; ctx.beginPath(); ctx.moveTo(x + r, y); ctx.lineTo(x + w - r, y); ctx.arcTo(x + w, y, x + w, y + r, r); ctx.lineTo(x + w, y + h - r); ctx.arcTo(x + w, y + h, x + w - r, y + h, r); ctx.lineTo(x + r, y + h); ctx.arcTo(x, y + h, x, y + h - r, r); ctx.lineTo(x, y + r); ctx.arcTo(x, y, x + r, y, r); ctx.closePath(); } getBounds(): Rect { const padding = this.strokeWidth / 2; return { x: this.x - padding, y: this.y - padding, width: this.width + this.strokeWidth, height: this.height + this.strokeWidth }; } hitTest(x: number, y: number): boolean { if (!this.visible) return false; if (this.cornerRadius > 0) { // 圆角矩形碰撞检测 return this.hitTestRoundedRect(x, y); } return x >= this.x && x <= this.x + this.width && y >= this.y && y <= this.y + this.height; }}方案三:动画系统实现// AnimationSystem.ets - 动画管理系统export class AnimationSystem { private static instance: AnimationSystem; private animations: Animation[] = []; private isRunning: boolean = false; static getInstance(): AnimationSystem { if (!AnimationSystem.instance) { AnimationSystem.instance = new AnimationSystem(); } return AnimationSystem.instance; } // 添加动画 addAnimation(animation: Animation): void { this.animations.push(animation); if (!this.isRunning) { this.startAnimationLoop(); } } // 动画循环 private startAnimationLoop(): void { this.isRunning = true; const animate = (timestamp: number) => { let hasActiveAnimations = false; for (let i = this.animations.length - 1; i >= 0; i--) { const animation = this.animations[i]; if (animation.update(timestamp)) { hasActiveAnimations = true; } else { // 动画完成 animation.onComplete?.(); this.animations.splice(i, 1); } } if (hasActiveAnimations) { requestAnimationFrame(animate); } else { this.isRunning = false; } }; requestAnimationFrame(animate); }}// 动画基类export abstract class Animation { protected startTime: number = 0; protected duration: number = 1000; protected delay: number = 0; protected easing: (t: number) => number = Easing.linear; public onComplete?: () => void; protected target: CanvasElement; constructor(target: CanvasElement, duration: number = 1000) { this.target = target; this.duration = duration; } update(timestamp: number): boolean { if (this.startTime === 0) { this.startTime = timestamp + this.delay; return true; } const elapsed = timestamp - this.startTime; if (elapsed < 0) return true; const progress = Math.min(elapsed / this.duration, 1); const easedProgress = this.easing(progress); this.applyAnimation(easedProgress); if (progress >= 1) { this.onAnimationComplete(); return false; } return true; } protected abstract applyAnimation(progress: number): void; protected abstract onAnimationComplete(): void;}// 移动动画export class MoveAnimation extends Animation { private startX: number; private startY: number; private endX: number; private endY: number; constructor( target: CanvasElement, endX: number, endY: number, duration: number = 1000 ) { super(target, duration); this.startX = target.x; this.startY = target.y; this.endX = endX; this.endY = endY; } protected applyAnimation(progress: number): void { const currentX = this.startX + (this.endX - this.startX) * progress; const currentY = this.startY + (this.endY - this.startY) * progress; this.target.updatePosition(currentX, currentY); } protected onAnimationComplete(): void { this.target.updatePosition(this.endX, this.endY); }}方案四:事件处理优化// EventManager.ets - 高效事件管理器export class EventManager { private elements: CanvasElement[] = []; private quadTree: QuadTree; private hoveredElement: CanvasElement | null = null; constructor(width: number, height: number) { this.quadTree = new QuadTree(0, 0, width, height, 4); } // 注册元素 registerElement(element: CanvasElement): void { this.elements.push(element); this.quadTree.insert({ x: element.x, y: element.y, width: element.width, height: element.height, element: element }); } // 处理点击事件 handleClick(x: number, y: number): CanvasElement | null { // 使用四叉树快速查找 const candidates = this.quadTree.retrieve(x, y); // 按z-index逆序检查(后添加的先检查) candidates.sort((a, b) => b.element.zIndex - a.element.zIndex); for (const candidate of candidates) { if (candidate.element.hitTest(x, y) && candidate.element.visible) { return candidate.element; } } return null; } // 处理悬停事件 handleHover(x: number, y: number): void { const hovered = this.handleClick(x, y); if (this.hoveredElement && this.hoveredElement !== hovered) { // 触发离开事件 this.triggerEvent('mouseleave', this.hoveredElement); } if (hovered && hovered !== this.hoveredElement) { // 触发进入事件 this.triggerEvent('mouseenter', hovered); } this.hoveredElement = hovered; }}方案五:实战使用示例// ExamplePage.ets - 使用示例@Entry@Componentstruct CanvasExamplePage { private canvasEngine: CanvasEngine; private animationSystem: AnimationSystem; aboutToAppear(): void { // 初始化引擎 this.canvasEngine = new CanvasEngine('myCanvas'); this.animationSystem = AnimationSystem.getInstance(); // 创建图形元素 const rect1 = new RectangleElement({ x: 50, y: 50, width: 100, height: 100, fillColor: '#FF6B6B', cornerRadius: 10, zIndex: 1 }); const rect2 = new RectangleElement({ x: 200, y: 200, width: 150, height: 80, fillColor: '#4ECDC4', strokeColor: '#333', strokeWidth: 2, zIndex: 2 }); // 添加到引擎 this.canvasEngine.addElement(rect1); this.canvasEngine.addElement(rect2); // 添加动画 const animation = new MoveAnimation(rect1, 300, 300, 2000); animation.easing = Easing.easeInOutCubic; animation.onComplete = () => { console.log('Animation completed!'); }; this.animationSystem.addAnimation(animation); } build() { Column() { // Canvas组件 Canvas(this.canvasEngine.getContext()) .width('100%') .height('80%') .backgroundColor('#f0f0f0') .onClick((event: ClickEvent) => { this.handleCanvasClick(event); }) .onTouch((event: TouchEvent) => { this.handleCanvasTouch(event); }) // 控制按钮 Row({ space: 20 }) { Button('添加矩形') .onClick(() => this.addRandomRectangle()) Button('清除所有') .onClick(() => this.clearAll()) } .padding(20) } } private addRandomRectangle(): void { const rect = new RectangleElement({ x: Math.random() * 300, y: Math.random() * 500, width: 50 + Math.random() * 100, height: 50 + Math.random() * 100, fillColor: this.getRandomColor(), cornerRadius: Math.random() * 20 }); this.canvasEngine.addElement(rect); // 添加动画 const animation = new MoveAnimation( rect, rect.x + 100, rect.y + 50, 1500 ); this.animationSystem.addAnimation(animation); }}1.5 结果展示CanvasEngine- 核心渲染引擎CanvasElement- 图形元素基类RectangleElement- 矩形组件CircleElement- 圆形组件TextElement- 文本组件AnimationSystem- 动画系统EventManager- 事件管理器ResourcePool- 资源池管理
-
一、项目概述1.1 功能特性● 基于HarmonyOS 4.0+ API实现● 音频频谱可视化水波纹效果● 实时音频分析处理● 可自定义的水波纹参数● 高性能Canvas渲染● 支持麦克风音频输入二、架构设计2.1 核心组件结构水波纹特效系统├── AudioWaveEffect.ets (主组件)├── WaveCanvas.ets (Canvas渲染)├── AudioAnalyzer.ets (音频分析)├── WaveGenerator.ets (波纹生成)└── EffectManager.ets (特效管理)2.2 数据模型定义// WaveEffectModel.ets// 水波纹配置export interface WaveConfig {waveCount: number; // 波纹数量waveSpeed: number; // 波纹速度waveWidth: number; // 波纹宽度waveHeight: number; // 波纹高度waveOpacity: number; // 波纹透明度colorPalette: string[]; // 颜色调色板gradientEnabled: boolean; // 是否启用渐变audioSensitivity: number; // 音频敏感度responseType: ‘frequency’ | ‘amplitude’ | ‘both’; // 响应类型}// 音频分析结果export interface AudioAnalysis {frequencies: Float32Array; // 频率数据amplitudes: Float32Array; // 振幅数据volume: number; // 总体音量frequencyBands: number[]; // 频带数据timestamp: number; // 时间戳}// 波纹点数据export interface WavePoint {x: number;y: number;radius: number;opacity: number;color: string;speed: number;life: number; // 生命周期maxLife: number;frequency: number; // 关联的频率}这里定义了水波纹特效系统的核心数据模型。WaveConfig接口包含所有可配置的水波纹参数,如波纹数量、速度、宽度、高度等。AudioAnalysis接口存储音频分析结果,包括频率、振幅、音量等数据。WavePoint接口表示单个波纹点的状态,用于Canvas渲染。三、核心实现3.1 音频分析器// AudioAnalyzer.etsexport class AudioAnalyzer {private audioContext: audio.AudioRenderer | null = null;private analyserNode: audio.AudioCapturer | null = null;private frequencyData: Float32Array = new Float32Array(1024);private timeDomainData: Float32Array = new Float32Array(1024);private isAnalyzing: boolean = false;private animationFrameId: number = 0;private sampleRate: number = 44100;private fftSize: number = 2048;// 分析回调函数private onAnalysisCallback: ((analysis: AudioAnalysis) => void) | null = null;// 初始化音频分析器async initialize(): Promise<boolean> {try {// 获取音频权限const permissionGranted = await this.requestAudioPermission();if (!permissionGranted) {console.error(‘音频权限被拒绝’);return false;} // 创建音频上下文 const audioStreamInfo: audio.AudioStreamInfo = { samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100, channels: audio.AudioChannel.STEREO, sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_F32LE, encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW }; this.audioContext = audio.createAudioRenderer(audioStreamInfo); // 创建音频捕获器 const audioCapturerInfo: audio.AudioCapturerInfo = { source: audio.AudioSourceType.AUDIO_SOURCE_TYPE_MIC, capturerFlags: 0 }; this.analyserNode = audio.createAudioCapturer(audioCapturerInfo); await this.audioContext.start(); await this.analyserNode.start(); return true; } catch (error) { console.error('音频分析器初始化失败:', error); return false; }}AudioAnalyzer类负责音频采集和分析。initialize方法初始化音频系统,请求麦克风权限,创建AudioRenderer和AudioCapturer。audioStreamInfo定义音频流参数(采样率、声道、格式),audioCapturerInfo配置音频捕获参数(源为麦克风)。// 开始音频分析startAnalysis(callback: (analysis: AudioAnalysis) => void): void {if (this.isAnalyzing) return;this.onAnalysisCallback = callback; this.isAnalyzing = true; // 开始分析循环 this.analysisLoop();}// 音频分析循环private analysisLoop(): void {if (!this.isAnalyzing) return;this.processAudioData(); this.animationFrameId = requestAnimationFrame(() => { this.analysisLoop(); });}// 处理音频数据private async processAudioData(): Promise<void> {if (!this.analyserNode || !this.onAnalysisCallback) return;try { // 读取音频数据 const buffer = await this.analyserNode.read(this.fftSize); if (buffer && buffer.byteLength > 0) { // 转换为Float32数组 const dataArray = new Float32Array(buffer); // 计算频率数据 this.calculateFrequencyData(dataArray); // 计算振幅数据 this.calculateAmplitudeData(dataArray); // 计算总体音量 const volume = this.calculateVolume(dataArray); // 计算频带数据 const frequencyBands = this.calculateFrequencyBands(this.frequencyData); // 回调分析结果 this.onAnalysisCallback({ frequencies: this.frequencyData, amplitudes: this.timeDomainData, volume: volume, frequencyBands: frequencyBands, timestamp: Date.now() }); } } catch (error) { console.warn('音频数据处理失败:', error); }}startAnalysis方法启动音频分析循环,注册回调函数。analysisLoop通过requestAnimationFrame实现循环分析。processAudioData方法读取音频数据,计算频率、振幅、音量和频带数据,然后通过回调函数传递分析结果。// 计算频率数据private calculateFrequencyData(data: Float32Array): void {// 这里实现FFT(快速傅里叶变换)计算频率// 简化实现:直接使用原始数据for (let i = 0; i < Math.min(data.length, this.frequencyData.length); i++) {this.frequencyData[i] = Math.abs(data[i]) * 100;}}// 计算振幅数据private calculateAmplitudeData(data: Float32Array): void {for (let i = 0; i < Math.min(data.length, this.timeDomainData.length); i++) {this.timeDomainData[i] = data[i];}}// 计算总体音量private calculateVolume(data: Float32Array): number {let sum = 0;for (let i = 0; i < data.length; i++) {sum += data[i] * data[i];}const rms = Math.sqrt(sum / data.length);return Math.min(rms * 100, 1.0);}// 计算频带数据private calculateFrequencyBands(frequencyData: Float32Array): number[] {const bands = [0, 0, 0, 0, 0, 0, 0]; // 7个频带// 将频率数据分组到不同频带 const bandSize = Math.floor(frequencyData.length / bands.length); for (let i = 0; i < bands.length; i++) { let sum = 0; const start = i * bandSize; const end = Math.min((i + 1) * bandSize, frequencyData.length); for (let j = start; j < end; j++) { sum += frequencyData[j]; } bands[i] = sum / (end - start); } return bands;}calculateFrequencyData方法计算频率数据(简化实现,实际应使用FFT)。calculateAmplitudeData方法计算振幅数据。calculateVolume方法计算RMS(均方根)值作为总体音量。calculateFrequencyBands方法将频率数据分组到7个频带,用于控制不同颜色的波纹。// 停止音频分析stopAnalysis(): void {this.isAnalyzing = false;if (this.animationFrameId) { cancelAnimationFrame(this.animationFrameId); this.animationFrameId = 0; } if (this.analyserNode) { this.analyserNode.stop(); } if (this.audioContext) { this.audioContext.stop(); } this.onAnalysisCallback = null;}// 请求音频权限private async requestAudioPermission(): Promise<boolean> {try {const permissions: Array<Permissions> = [‘ohos.permission.MICROPHONE’]; const grantStatus = await abilityAccessCtrl.requestPermissionsFromUser( globalThis.abilityContext, permissions ); return grantStatus.authResults.every(result => result === 0); } catch (error) { console.error('权限请求失败:', error); return false; }}}stopAnalysis方法停止音频分析,清理资源。requestAudioPermission方法请求麦克风权限,使用abilityAccessCtrl API检查并请求权限,确保应用有权访问麦克风。3.2 水波纹生成器// WaveGenerator.etsexport class WaveGenerator {private waves: WavePoint[] = [];private config: WaveConfig;private canvasWidth: number = 0;private canvasHeight: number = 0;private centerX: number = 0;private centerY: number = 0;private time: number = 0;constructor(config: WaveConfig) {this.config = config;this.initializeWaves();}// 初始化波纹private initializeWaves(): void {this.waves = [];for (let i = 0; i < this.config.waveCount; i++) { this.waves.push(this.createWavePoint(i)); }}// 创建波纹点private createWavePoint(index: number): WavePoint {const angle = (index / this.config.waveCount) * Math.PI * 2;const distance = 20 + Math.random() * 50;return { x: this.centerX + Math.cos(angle) * distance, y: this.centerY + Math.sin(angle) * distance, radius: 0, opacity: 0, color: this.getColorByIndex(index), speed: 1 + Math.random() * this.config.waveSpeed, life: 0, maxLife: 100 + Math.random() * 200, frequency: index % 7 // 关联到7个频带 };}WaveGenerator类负责生成和管理水波纹点。constructor初始化配置和波纹数组。initializeWaves方法创建指定数量的波纹点。createWavePoint方法创建单个波纹点,根据索引计算位置、颜色和属性,使波纹点均匀分布在圆周上。// 根据索引获取颜色private getColorByIndex(index: number): string {if (this.config.colorPalette && this.config.colorPalette.length > 0) {return this.config.colorPalette[index % this.config.colorPalette.length];}// 默认颜色调色板 const defaultColors = [ '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8' ]; return defaultColors[index % defaultColors.length];}// 更新波纹update(deltaTime: number, audioAnalysis: AudioAnalysis | null): void {this.time += deltaTime;// 更新现有波纹 for (let i = this.waves.length - 1; i >= 0; i--) { const wave = this.waves[i]; // 更新生命周期 wave.life += deltaTime * wave.speed; if (wave.life > wave.maxLife) { // 重新生成波纹 this.waves[i] = this.createWavePoint(i); continue; } // 计算生命周期进度 const lifeProgress = wave.life / wave.maxLife; // 更新半径(从小到大再到小) wave.radius = this.calculateRadius(lifeProgress); // 更新透明度 wave.opacity = this.calculateOpacity(lifeProgress); // 音频响应 if (audioAnalysis) { this.applyAudioEffect(wave, audioAnalysis); } // 添加波动效果 this.applyWaveMotion(wave); }}getColorByIndex方法根据索引从调色板获取颜色,支持自定义颜色数组。update方法更新所有波纹点的状态,包括生命周期、半径、透明度和音频响应。生命周期结束后重新生成波纹点。// 计算半径private calculateRadius(lifeProgress: number): number {// 使用正弦函数创建平滑的半径变化const progress = lifeProgress * Math.PI;return Math.sin(progress) * this.config.waveHeight;}// 计算透明度private calculateOpacity(lifeProgress: number): number {// 透明度在生命周期中间最高const progress = lifeProgress * Math.PI;return Math.sin(progress) * this.config.waveOpacity;}// 应用音频效果private applyAudioEffect(wave: WavePoint, analysis: AudioAnalysis): void {if (this.config.responseType === ‘frequency’ || this.config.responseType === ‘both’) {// 频率响应const frequencyValue = analysis.frequencyBands[wave.frequency % analysis.frequencyBands.length] || 0;wave.radius += frequencyValue * this.config.audioSensitivity * 10;}if (this.config.responseType === 'amplitude' || this.config.responseType === 'both') { // 振幅响应 wave.opacity += analysis.volume * this.config.audioSensitivity * 0.5; wave.opacity = Math.min(wave.opacity, 1); }}// 应用波动效果private applyWaveMotion(wave: WavePoint): void {const time = this.time * 0.001;const waveSpeed = 0.5;// 添加轻微的波动效果 wave.x += Math.sin(time + wave.frequency) * waveSpeed; wave.y += Math.cos(time + wave.frequency) * waveSpeed;}// 获取所有波纹getWaves(): WavePoint[] {return this.waves;}// 设置画布尺寸setCanvasSize(width: number, height: number): void {this.canvasWidth = width;this.canvasHeight = height;this.centerX = width / 2;this.centerY = height / 2;// 重新初始化波纹 this.initializeWaves();}}calculateRadius和calculateOpacity方法使用正弦函数计算平滑变化的半径和透明度。applyAudioEffect方法根据音频分析数据调整波纹,频率影响半径,振幅影响透明度。applyWaveMotion方法添加轻微的波动效果,使波纹更生动。setCanvasSize方法更新画布尺寸并重新初始化波纹。3.3 Canvas水波纹渲染器// WaveCanvas.ets@Componentexport struct WaveCanvas {private canvasRef: CanvasRenderingContext2D | null = null;private canvasWidth: number = 300;private canvasHeight: number = 300;private animationId: number = 0;private lastTime: number = 0;@State private isAnimating: boolean = false;@State private waves: WavePoint[] = [];@Prop config: WaveConfig = defaultConfig;@Prop audioAnalysis: AudioAnalysis | null = null;@Prop onCanvasReady?: (context: CanvasRenderingContext2D) => void;private waveGenerator: WaveGenerator = new WaveGenerator(this.config);aboutToAppear() {this.waveGenerator.setCanvasSize(this.canvasWidth, this.canvasHeight);}WaveCanvas组件使用Canvas 2D API渲染水波纹效果。canvasRef存储Canvas上下文,animationId用于控制动画循环。@State装饰器管理组件状态,@Prop接收配置和音频数据。waveGenerator实例在aboutToAppear中初始化。// Canvas就绪回调private onCanvasReadyCallback(context: CanvasRenderingContext2D): void {this.canvasRef = context;// 获取画布尺寸 const canvas = this.canvasRef.canvas; this.canvasWidth = canvas.width; this.canvasHeight = canvas.height; this.waveGenerator.setCanvasSize(this.canvasWidth, this.canvasHeight); // 开始动画 this.startAnimation(); // 通知父组件 this.onCanvasReady?.(context);}// 开始动画private startAnimation(): void {if (this.isAnimating) return;this.isAnimating = true; this.lastTime = performance.now(); this.animationLoop();}// 动画循环private animationLoop(): void {if (!this.isAnimating || !this.canvasRef) return;const currentTime = performance.now(); const deltaTime = currentTime - this.lastTime; this.lastTime = currentTime; // 更新波纹 this.waveGenerator.update(deltaTime, this.audioAnalysis); this.waves = this.waveGenerator.getWaves(); // 渲染 this.render(); this.animationId = requestAnimationFrame(() => { this.animationLoop(); });}onCanvasReadyCallback在Canvas就绪时调用,获取Canvas上下文和尺寸,启动动画循环。startAnimation方法设置动画状态,记录起始时间。animationLoop方法计算时间差,更新波纹状态,调用render方法绘制,然后通过requestAnimationFrame继续循环。// 渲染水波纹private render(): void {if (!this.canvasRef) return;const ctx = this.canvasRef; // 清除画布 ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight); // 绘制背景渐变 this.drawBackground(ctx); // 绘制所有波纹 for (const wave of this.waves) { this.drawWave(ctx, wave); } // 绘制中心点 this.drawCenterPoint(ctx);}// 绘制背景private drawBackground(ctx: CanvasRenderingContext2D): void {if (!this.config.gradientEnabled) {// 纯色背景ctx.fillStyle = ‘#1A1A2E’;ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight);return;}// 创建径向渐变背景 const gradient = ctx.createRadialGradient( this.canvasWidth / 2, this.canvasHeight / 2, 0, this.canvasWidth / 2, this.canvasHeight / 2, Math.max(this.canvasWidth, this.canvasHeight) / 2 ); gradient.addColorStop(0, '#0F3460'); gradient.addColorStop(0.5, '#1A1A2E'); gradient.addColorStop(1, '#16213E'); ctx.fillStyle = gradient; ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight);}render方法执行完整的绘制流程:清除画布、绘制背景、绘制所有波纹、绘制中心点。drawBackground方法绘制背景,支持纯色和径向渐变两种模式,径向渐变从中心向外颜色变深,创造深度感。// 绘制单个波纹private drawWave(ctx: CanvasRenderingContext2D, wave: WavePoint): void {ctx.beginPath();// 设置样式 ctx.globalAlpha = wave.opacity; if (this.config.gradientEnabled) { // 创建径向渐变 const gradient = ctx.createRadialGradient( wave.x, wave.y, 0, wave.x, wave.y, wave.radius ); gradient.addColorStop(0, wave.color); gradient.addColorStop(0.7, wave.color + '80'); // 80是透明度 gradient.addColorStop(1, wave.color + '00'); // 00是完全透明 ctx.fillStyle = gradient; } else { ctx.fillStyle = wave.color; } // 绘制圆形波纹 ctx.arc(wave.x, wave.y, wave.radius, 0, Math.PI * 2); ctx.fill(); // 重置透明度 ctx.globalAlpha = 1; // 添加发光效果 this.drawGlowEffect(ctx, wave);}// 绘制发光效果private drawGlowEffect(ctx: CanvasRenderingContext2D, wave: WavePoint): void {ctx.beginPath();ctx.arc(wave.x, wave.y, wave.radius, 0, Math.PI * 2);// 设置发光样式 ctx.shadowColor = wave.color; ctx.shadowBlur = 15; ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 0; // 绘制发光 ctx.strokeStyle = wave.color + '40'; ctx.lineWidth = 2; ctx.stroke(); // 重置阴影 ctx.shadowColor = 'transparent'; ctx.shadowBlur = 0;}drawWave方法绘制单个波纹点,支持渐变和纯色两种模式。渐变模式创建从中心向外透明度渐变的径向渐变。drawGlowEffect方法添加发光效果,通过Canvas的shadow属性实现,使波纹看起来更明亮、更有立体感。// 绘制中心点private drawCenterPoint(ctx: CanvasRenderingContext2D): void {const centerX = this.canvasWidth / 2;const centerY = this.canvasHeight / 2;// 绘制中心圆形 ctx.beginPath(); ctx.arc(centerX, centerY, 20, 0, Math.PI * 2); // 创建中心渐变 const gradient = ctx.createRadialGradient( centerX, centerY, 0, centerX, centerY, 30 ); gradient.addColorStop(0, '#FFFFFF'); gradient.addColorStop(1, '#4D94FF'); ctx.fillStyle = gradient; ctx.fill(); // 添加中心发光 ctx.beginPath(); ctx.arc(centerX, centerY, 25, 0, Math.PI * 2); ctx.strokeStyle = '#4D94FF80'; ctx.lineWidth = 2; ctx.stroke(); // 绘制音乐图标 this.drawMusicIcon(ctx, centerX, centerY);}// 绘制音乐图标private drawMusicIcon(ctx: CanvasRenderingContext2D, x: number, y: number): void {ctx.fillStyle = ‘#FFFFFF’;// 绘制音符 ctx.beginPath(); // 第一个音符 ctx.arc(x - 5, y - 8, 3, 0, Math.PI * 2); ctx.fill(); // 音符杆 ctx.fillRect(x - 5, y - 11, 2, 10); // 第二个音符 ctx.beginPath(); ctx.arc(x + 5, y - 5, 3, 0, Math.PI * 2); ctx.fill(); // 音符杆 ctx.fillRect(x + 5, y - 8, 2, 8);}drawCenterPoint方法绘制中心区域,包括渐变圆形和音乐图标。中心圆形使用从白到蓝的径向渐变,周围添加发光边框。drawMusicIcon方法绘制简化的音乐符号,增强听歌识曲的主题感。// 停止动画private stopAnimation(): void {this.isAnimating = false;if (this.animationId) { cancelAnimationFrame(this.animationId); this.animationId = 0; }}aboutToDisappear() {this.stopAnimation();}build() {Canvas(this.canvasRef).width(‘100%’).height(‘100%’).backgroundColor(‘#1A1A2E’).onReady((context: CanvasRenderingContext2D) => {this.onCanvasReadyCallback(context);})}}stopAnimation方法停止动画循环,清理资源。aboutToDisappear生命周期中确保动画停止。build方法创建Canvas组件,设置尺寸和背景色,onReady回调在Canvas就绪时初始化渲染。3.4 主组件 - 听歌识曲水波纹特效// AudioWaveEffect.ets@Entry@Componentexport struct AudioWaveEffect {@State private isListening: boolean = false;@State private isAnalyzing: boolean = false;@State private currentAudioAnalysis: AudioAnalysis | null = null;@State private songInfo: SongInfo | null = null;@State private recognitionProgress: number = 0;@State private recognitionStatus: ‘idle’ | ‘listening’ | ‘recognizing’ | ‘complete’ | ‘error’ = ‘idle’;// 水波纹配置@State private waveConfig: WaveConfig = {waveCount: 50,waveSpeed: 2,waveWidth: 3,waveHeight: 100,waveOpacity: 0.7,colorPalette: [‘#FF6B6B’, ‘#4ECDC4’, ‘#45B7D1’, ‘#96CEB4’,‘#FFEAA7’, ‘#DDA0DD’, ‘#98D8C8’, ‘#F7DC6F’],gradientEnabled: true,audioSensitivity: 1.5,responseType: ‘both’};private audioAnalyzer: AudioAnalyzer = new AudioAnalyzer();private recognitionTimer: number = 0;private canvasContext: CanvasRenderingContext2D | null = null;aboutToAppear() {this.initializeAudio();}AudioWaveEffect是主入口组件,管理整个听歌识曲应用的状态。@State装饰器管理监听状态、音频分析数据、歌曲信息和识别进度。waveConfig包含水波纹的可配置参数。audioAnalyzer实例处理音频分析。// 初始化音频private async initializeAudio(): Promise<void> {try {const initialized = await this.audioAnalyzer.initialize(); if (initialized) { console.log('音频系统初始化成功'); } else { console.error('音频系统初始化失败'); this.recognitionStatus = 'error'; } } catch (error) { console.error('初始化失败:', error); this.recognitionStatus = 'error'; }}// 开始听歌识曲private async startListening(): Promise<void> {if (this.isListening) return;this.isListening = true; this.recognitionStatus = 'listening'; this.recognitionProgress = 0; this.songInfo = null; // 开始音频分析 this.audioAnalyzer.startAnalysis((analysis: AudioAnalysis) => { this.currentAudioAnalysis = analysis; // 更新识别进度 this.updateRecognitionProgress(); }); // 模拟识别过程 this.startRecognitionSimulation();}initializeAudio方法初始化音频分析器。startListening方法开始听歌识曲流程,设置状态,启动音频分析,注册分析回调函数更新currentAudioAnalysis,然后开始模拟识别过程。// 更新识别进度private updateRecognitionProgress(): void {if (!this.isListening || this.recognitionStatus !== ‘listening’) return;// 根据分析到的音频数据更新进度 if (this.currentAudioAnalysis) { const volume = this.currentAudioAnalysis.volume; // 音量越大,进度增长越快 this.recognitionProgress = Math.min( this.recognitionProgress + volume * 0.5, 100 ); // 进度达到100%时开始识别 if (this.recognitionProgress >= 100 && this.recognitionStatus === 'listening') { this.startRecognition(); } }}// 开始识别private startRecognition(): void {this.recognitionStatus = ‘recognizing’;this.recognitionProgress = 100;// 模拟识别过程 setTimeout(() => { this.completeRecognition(); }, 2000);}// 模拟识别过程private startRecognitionSimulation(): void {// 5秒后自动停止(模拟超时)this.recognitionTimer = setTimeout(() => {if (this.recognitionStatus === ‘listening’) {this.stopListening();this.showNoSongDetected();}}, 5000) as unknown as number;}updateRecognitionProgress方法根据音频音量更新识别进度,音量越大进度增长越快。startRecognition方法在进度达到100%时开始识别,模拟2秒识别过程。startRecognitionSimulation方法设置5秒超时,如果没有识别到歌曲则停止。// 完成识别private completeRecognition(): void {this.recognitionStatus = ‘complete’;// 模拟识别结果 this.songInfo = { id: '123456', title: '夜空中最亮的星', artist: '逃跑计划', album: '世界', coverUrl: 'https://example.com/cover.jpg', duration: 280, matchConfidence: 92 }; // 停止音频分析 this.stopAudioAnalysis(); // 显示结果3秒后重置 setTimeout(() => { this.resetRecognition(); }, 3000);}// 停止听歌识曲private stopListening(): void {this.isListening = false;this.recognitionStatus = ‘idle’;this.stopAudioAnalysis(); if (this.recognitionTimer) { clearTimeout(this.recognitionTimer); this.recognitionTimer = 0; }}// 停止音频分析private stopAudioAnalysis(): void {this.audioAnalyzer.stopAnalysis();this.currentAudioAnalysis = null;}completeRecognition方法完成识别,设置模拟的歌曲信息,停止音频分析,3秒后重置状态。stopListening方法停止听歌识曲,清理所有状态和定时器。stopAudioAnalysis方法停止音频分析器。// 重置识别private resetRecognition(): void {this.isListening = false;this.recognitionStatus = ‘idle’;this.recognitionProgress = 0;this.songInfo = null;this.currentAudioAnalysis = null;}// 显示未检测到歌曲private showNoSongDetected(): void {// 在实际应用中,这里应该显示提示信息console.log(‘未检测到歌曲,请重试’);}// Canvas就绪回调private onCanvasReady(context: CanvasRenderingContext2D): void {this.canvasContext = context;}aboutToDisappear() {this.stopListening();}resetRecognition方法重置所有识别状态。showNoSongDetected方法处理未检测到歌曲的情况。onCanvasReady回调保存Canvas上下文。aboutToDisappear生命周期确保停止所有活动。// 构建控制面板@Builderprivate buildControlPanel() {Column({ space: 12 }) {// 状态显示Row() {Circle().width(12).height(12).fill(this.getStatusColor()) Text(this.getStatusText()) .fontSize(16) .fontColor(Color.White) .margin({ left: 8 }) } // 进度条 if (this.recognitionStatus === 'listening' || this.recognitionStatus === 'recognizing') { Row() .width('100%') .height(4) .backgroundColor('#FFFFFF40') .borderRadius(2) .overlay( Row() .width(`${this.recognitionProgress}%`) .height('100%') .backgroundColor('#4D94FF') .borderRadius(2) .animation({ duration: 300, curve: animation.Curve.EaseInOut }) ) .margin({ top: 8, bottom: 8 }) } // 控制按钮 Button(this.isListening ? '停止识别' : '开始听歌识曲') .width('80%') .height(40) .backgroundColor(this.isListening ? '#FF6B6B' : '#4D94FF') .onClick(() => { if (this.isListening) { this.stopListening(); } else { this.startListening(); } }) } .width('100%') .padding(16) .backgroundColor('#FFFFFF10') .borderRadius(12) .margin({ bottom: 20 })}buildControlPanel方法构建控制面板,包括状态指示器、进度条和控制按钮。状态指示器根据当前状态显示不同颜色。进度条显示识别进度。按钮在开始/停止识别之间切换。// 构建歌曲信息@Builderprivate buildSongInfo() {if (!this.songInfo) return;Column({ space: 8 }) { // 专辑封面 if (this.songInfo.coverUrl) { Image(this.songInfo.coverUrl) .width(120) .height(120) .borderRadius(8) .objectFit(ImageFit.Cover) } else { Circle() .width(120) .height(120) .fill('#4D94FF') } // 歌曲信息 Text(this.songInfo.title) .fontSize(20) .fontColor(Color.White) .fontWeight(FontWeight.Bold) .textAlign(TextAlign.Center) Text(this.songInfo.artist) .fontSize(16) .fontColor('#CCCCCC') .textAlign(TextAlign.Center) Text(`专辑:${this.songInfo.album}`) .fontSize(14) .fontColor('#999999') .textAlign(TextAlign.Center) // 匹配度 Row({ space: 8 }) { Text('匹配度:') .fontSize(14) .fontColor('#CCCCCC') Text(`${this.songInfo.matchConfidence}%`) .fontSize(16) .fontColor('#4D94FF') .fontWeight(FontWeight.Medium) } .margin({ top: 8 }) } .width('100%') .padding(20) .backgroundColor('#FFFFFF10') .borderRadius(12)}// 获取状态颜色private getStatusColor(): ResourceColor {switch (this.recognitionStatus) {case ‘listening’:return ‘#4D94FF’;case ‘recognizing’:return ‘#FFEAA7’;case ‘complete’:return ‘#96CEB4’;case ‘error’:return ‘#FF6B6B’;default:return ‘#999999’;}}// 获取状态文本private getStatusText(): string {switch (this.recognitionStatus) {case ‘idle’:return ‘准备识别’;case ‘listening’:return ‘正在听歌…’;case ‘recognizing’:return ‘识别中…’;case ‘complete’:return ‘识别完成’;case ‘error’:return ‘识别失败’;default:return ‘未知状态’;}}buildSongInfo方法构建识别到的歌曲信息显示,包括专辑封面、歌曲标题、艺术家、专辑名称和匹配度。getStatusColor和getStatusText方法根据识别状态返回对应的颜色和文本。// 构建配置面板@Builderprivate buildConfigPanel() {Column({ space: 12 }) {Text(‘水波纹设置’).fontSize(18).fontColor(Color.White).fontWeight(FontWeight.Medium).margin({ bottom: 8 }) // 波纹数量 Row({ space: 8 }) { Text('波纹数量') .fontSize(14) .fontColor('#CCCCCC') .layoutWeight(1) Slider({ value: this.waveConfig.waveCount, min: 20, max: 100, step: 5, style: SliderStyle.OutSet }) .layoutWeight(2) .onChange((value: number) => { this.waveConfig.waveCount = value; }) } // 波纹速度 Row({ space: 8 }) { Text('波纹速度') .fontSize(14) .fontColor('#CCCCCC') .layoutWeight(1) Slider({ value: this.waveConfig.waveSpeed, min: 0.5, max: 5, step: 0.5 }) .layoutWeight(2) .onChange((value: number) => { this.waveConfig.waveSpeed = value; }) } // 音频敏感度 Row({ space: 8 }) { Text('音频敏感度') .fontSize(14) .fontColor('#CCCCCC') .layoutWeight(1) Slider({ value: this.waveConfig.audioSensitivity, min: 0.5, max: 3, step: 0.1 }) .layoutWeight(2) .onChange((value: number) => { this.waveConfig.audioSensitivity = value; }) } } .width('100%') .padding(16) .backgroundColor('#FFFFFF10') .borderRadius(12)}buildConfigPanel方法构建水波纹配置面板,包含滑块控件调节波纹数量、速度和音频敏感度。每个滑块控件都绑定到waveConfig的对应属性,实现实时配置更新。build() {Column() {// 标题Text(‘听歌识曲’).fontSize(24).fontColor(Color.White).fontWeight(FontWeight.Bold).margin({ top: 40, bottom: 20 }) // 水波纹Canvas Column() .width(300) .height(300) .borderRadius(150) .clip(true) .shadow({ radius: 20, color: '#4D94FF40' }) .overlay( WaveCanvas({ config: this.waveConfig, audioAnalysis: this.currentAudioAnalysis, onCanvasReady: this.onCanvasReady.bind(this) }) ) .margin({ bottom: 30 }) // 歌曲信息或控制面板 if (this.songInfo) { this.buildSongInfo() } else { this.buildControlPanel() } // 配置面板 this.buildConfigPanel() .margin({ top: 20 }) // 使用说明 Text('使用说明:点击"开始听歌识曲"按钮,将手机靠近音乐源,系统将自动识别歌曲。') .fontSize(12) .fontColor('#999999') .textAlign(TextAlign.Center) .margin({ top: 20, left: 20, right: 20 }) .multilineTextAlignment(TextAlign.Center) } .width('100%') .height('100%') .justifyContent(FlexAlign.Start) .padding(20) .backgroundImage('/resources/base/media/wave_background.png') .backgroundImageSize(ImageSize.Cover)}}build方法构建完整的应用界面,包含标题、水波纹Canvas、控制面板/歌曲信息、配置面板和使用说明。水波纹Canvas使用圆形裁剪和阴影效果。根据是否识别到歌曲显示不同的内容区域。整个界面使用深色主题背景。四、高级特性4.1 音频频谱可视化// SpectrumVisualizer.ets@Componentexport struct SpectrumVisualizer {private ctx: CanvasRenderingContext2D | null = null;private audioAnalysis: AudioAnalysis | null = null;private barCount: number = 64;private barWidth: number = 0;private barSpacing: number = 2;// 绘制频谱private drawSpectrum(): void {if (!this.ctx || !this.audioAnalysis) return;const canvas = this.ctx.canvas; const width = canvas.width; const height = canvas.height; const frequencies = this.audioAnalysis.frequencies; this.ctx.clearRect(0, 0, width, height); // 计算每个柱子的宽度 this.barWidth = (width - (this.barCount - 1) * this.barSpacing) / this.barCount; for (let i = 0; i < this.barCount; i++) { const frequencyIndex = Math.floor(i * frequencies.length / this.barCount); const frequencyValue = frequencies[frequencyIndex] || 0; // 计算柱子的高度 const barHeight = Math.min(frequencyValue * height * 2, height * 0.8); // 计算颜色 const hue = (i / this.barCount) * 240; // 蓝色到紫色 const color = `hsl(${hue}, 100%, 60%)`; // 绘制柱子 const x = i * (this.barWidth + this.barSpacing); const y = height - barHeight; this.ctx.fillStyle = color; this.ctx.fillRect(x, y, this.barWidth, barHeight); // 添加渐变效果 const gradient = this.ctx.createLinearGradient(x, y, x, y + barHeight); gradient.addColorStop(0, color + 'FF'); gradient.addColorStop(1, color + '00'); this.ctx.fillStyle = gradient; this.ctx.fillRect(x, y, this.barWidth, barHeight); }}}SpectrumVisualizer组件实现音频频谱可视化,将频率数据转换为柱状图。每个柱子的高度对应特定频率的强度,颜色在蓝色到紫色之间渐变,从下到上添加透明度渐变,创造发光效果。4.2 3D水波纹效果// Wave3DEffect.ets@Componentexport struct Wave3DEffect {private ctx: WebGLRenderingContext | null = null;private waveProgram: WebGLProgram | null = null;private timeUniform: WebGLUniformLocation | null = null;private audioUniform: WebGLUniformLocation | null = null;private time: number = 0;// 初始化WebGLprivate initWebGL(): void {if (!this.ctx) return;const gl = this.ctx; // 顶点着色器 const vertexShaderSource = ` attribute vec2 aPosition; attribute vec2 aTexCoord; varying vec2 vTexCoord; void main() { gl_Position = vec4(aPosition, 0.0, 1.0); vTexCoord = aTexCoord; } `; // 片元着色器 const fragmentShaderSource = ` precision mediump float; uniform float uTime; uniform float uAudioData[8]; varying vec2 vTexCoord; void main() { vec2 uv = vTexCoord * 2.0 - 1.0; float dist = length(uv); // 基础波纹 float wave = sin(dist * 10.0 - uTime * 2.0) * 0.5 + 0.5; // 音频影响 float audioEffect = 0.0; for (int i = 0; i < 8; i++) { float freq = uAudioData[i]; audioEffect += sin(dist * (5.0 + float(i)) - uTime * (1.0 + freq)) * freq; } // 组合效果 wave = wave * 0.7 + audioEffect * 0.3; // 颜色 vec3 color = vec3( 0.5 + 0.5 * sin(wave + uTime), 0.5 + 0.5 * sin(wave + uTime + 2.0), 0.5 + 0.5 * sin(wave + uTime + 4.0) ); // 透明度 float alpha = 1.0 - smoothstep(0.8, 1.0, dist); gl_FragColor = vec4(color, alpha); } `; // 创建着色器程序 this.waveProgram = this.createProgram(gl, vertexShaderSource, fragmentShaderSource); if (this.waveProgram) { this.timeUniform = gl.getUniformLocation(this.waveProgram, 'uTime'); this.audioUniform = gl.getUniformLocation(this.waveProgram, 'uAudioData'); }}}Wave3DEffect组件使用WebGL实现3D水波纹效果。顶点着色器处理顶点位置,片元着色器计算每个像素的颜色。音频数据通过uniform数组传入着色器,影响波纹的形状和颜色。通过正弦函数组合创建动态的3D波纹效果。五、最佳实践5.1 性能优化建议Canvas优化:使用离屏Canvas预渲染静态元素音频处理优化:使用Web Worker进行FFT计算内存管理:及时清理不再使用的纹理和缓冲区帧率控制:根据设备性能动态调整渲染质量性能优化包括:1)离屏Canvas缓存静态背景减少重绘;2)Web Worker线程处理耗时的FFT计算避免阻塞UI;3)WebGL资源及时清理防止内存泄漏;4)根据设备帧率动态调整波纹数量和渲染质量。5.2 用户体验优化实时反馈:音频输入立即反映到视觉效果触觉反馈:识别成功时提供震动反馈视觉引导:清晰的识别状态指示错误处理:网络异常时的友好提示用户体验优化包括:1)低延迟的音频可视化反馈;2)识别成功时通过vibrator API提供震动反馈;3)清晰的状态指示和进度显示;4)网络错误、权限拒绝等情况的友好提示。5.3 可访问性// 为屏幕阅读器提供支持.accessibilityLabel(‘听歌识曲水波纹特效’).accessibilityHint(‘点击按钮开始识别周围播放的音乐’).accessibilityRole(AccessibilityRole.Button).accessibilityState({disabled: this.isAnalyzing,selected: this.isListening})可访问性支持为视障用户提供语音反馈,描述组件功能、操作提示和当前状态,确保所有用户都能使用听歌识曲功能。六、总结本实现方案提供了完整的听歌识曲水波纹特效,包含音频采集、分析、可视化、识别流程和丰富的视觉效果,通过HarmonyOS最新API实现了高性能的音频处理和图形渲染。
-
一、项目概述1.1 功能特性● 基于HarmonyOS 4.0+ API实现● Grid和List组件内拖拽排序● 平滑的交互动画效果● 支持多种拖拽手势● 跨组件拖拽支持● 高性能渲染优化二、架构设计2.1 核心组件结构拖拽排序系统├── DragManager.ets (拖拽管理器)├── DraggableGrid.ets (可拖拽网格)├── DraggableList.ets (可拖拽列表)├── DragItem.ets (可拖拽项)└── SortAnimation.ets (排序动画)2.2 数据模型定义// DragSortModel.ets// 可拖拽项数据模型export interface DraggableItem {id: string;title: string;icon?: Resource;color: ResourceColor;order: number; // 排序序号type?: ‘grid’ | ‘list’; // 所属容器类型disabled?: boolean; // 是否禁用拖拽}// 拖拽配置export interface DragSortConfig {animationDuration: number; // 动画时长(ms)dragScale: number; // 拖拽时缩放比例dragOpacity: number; // 拖拽时透明度vibrationEnabled: boolean; // 是否启用震动反馈crossDragEnabled: boolean; // 是否允许跨容器拖拽placeholderColor: ResourceColor; // 占位符颜色autoScrollEnabled: boolean; // 是否启用自动滚动scrollThreshold: number; // 自动滚动阈值scrollSpeed: number; // 自动滚动速度}// 默认配置export class DragDefaultConfig {static readonly DEFAULT_CONFIG: DragSortConfig = {animationDuration: 300,dragScale: 1.1,dragOpacity: 0.8,vibrationEnabled: true,crossDragEnabled: false,placeholderColor: ‘#F0F0F0’,autoScrollEnabled: true,scrollThreshold: 50,scrollSpeed: 20};}这里定义了拖拽排序系统的数据模型和配置。DraggableItem接口定义每个可拖拽项的数据结构,包含ID、标题、图标、颜色、排序序号和是否禁用等属性。DragSortConfig接口定义拖拽行为的配置参数,如动画时长、拖拽缩放比例、震动反馈等。DragDefaultConfig提供默认配置值,方便快速使用。三、核心实现3.1 拖拽管理器// DragManager.etsexport class DragManager {private static instance: DragManager;private draggingItem: DraggableItem | null = null;private dragStartPosition: { x: number; y: number } = { x: 0, y: 0 };private currentIndex: number = -1;private targetIndex: number = -1;private containerType: ‘grid’ | ‘list’ | null = null;private listeners: Map<string, (event: DragEvent) => void> = new Map();private vibration: vibrator.Vibrator | null = null;// 单例模式static getInstance(): DragManager {if (!DragManager.instance) {DragManager.instance = new DragManager();}return DragManager.instance;}DragManager是拖拽系统的核心管理器,采用单例模式确保全局唯一实例。它维护当前拖拽状态,包括拖拽中的项、起始位置、当前索引、目标索引、容器类型等。listeners用于存储事件监听器,vibration用于触觉反馈。// 开始拖拽startDrag(item: DraggableItem, index: number, startX: number, startY: number, type: ‘grid’ | ‘list’): void {this.draggingItem = item;this.currentIndex = index;this.targetIndex = index;this.dragStartPosition = { x: startX, y: startY };this.containerType = type;// 震动反馈 if (this.vibrationEnabled()) { this.vibrate(10); } this.notifyListeners('dragStart', { item, index, type });}// 更新拖拽位置updateDragPosition(x: number, y: number, hoverIndex: number = -1): void {if (!this.draggingItem) return;this.targetIndex = hoverIndex; this.notifyListeners('dragMove', { x, y, hoverIndex, item: this.draggingItem });}startDrag方法开始拖拽操作,记录拖拽项、起始位置和容器类型,并触发震动反馈。updateDragPosition方法更新拖拽位置和目标索引,通知所有监听器拖拽移动事件。// 结束拖拽endDrag(): DraggableItem | null {if (!this.draggingItem) return null;const droppedItem = this.draggingItem; const fromIndex = this.currentIndex; const toIndex = this.targetIndex; this.notifyListeners('dragEnd', { item: droppedItem, fromIndex, toIndex, type: this.containerType }); // 重置状态 this.reset(); return droppedItem;}// 取消拖拽cancelDrag(): void {this.notifyListeners(‘dragCancel’, {item: this.draggingItem,index: this.currentIndex});this.reset();}private reset(): void {this.draggingItem = null;this.currentIndex = -1;this.targetIndex = -1;this.dragStartPosition = { x: 0, y: 0 };this.containerType = null;}endDrag方法结束拖拽操作,返回拖拽的项,通知监听器拖拽结束事件,然后重置拖拽状态。cancelDrag方法取消拖拽操作,通知监听器拖拽取消事件。reset方法清理所有拖拽状态。// 震动反馈private vibrate(duration: number): void {try {this.vibration = vibrator.createVibrator();this.vibration.vibrate(duration);} catch (error) {console.warn(‘Vibration not available:’, error);}}private vibrationEnabled(): boolean {// 从配置中获取震动设置return true;}// 事件监听addEventListener(event: string, callback: (event: DragEvent) => void): void {this.listeners.set(event, callback);}removeEventListener(event: string): void {this.listeners.delete(event);}private notifyListeners(eventType: string, data: any): void {const callback = this.listeners.get(eventType);if (callback) {callback({ type: eventType, data });}}// 获取当前拖拽状态getDragState(): {isDragging: boolean;draggingItem: DraggableItem | null;fromIndex: number;toIndex: number;} {return {isDragging: this.draggingItem !== null,draggingItem: this.draggingItem,fromIndex: this.currentIndex,toIndex: this.targetIndex};}}vibrate方法提供震动触觉反馈,增强拖拽交互体验。addEventListener和removeEventListener管理事件监听。getDragState方法返回当前拖拽状态,供其他组件查询。notifyListeners方法通知所有注册的监听器特定事件。3.2 可拖拽列表项// DragItem.ets@Componentexport struct DragItem {@Prop item: DraggableItem;@Prop index: number = 0;@Prop containerType: ‘grid’ | ‘list’ = ‘list’;@Prop config: DragSortConfig = DragDefaultConfig.DEFAULT_CONFIG;@State private isDragging: boolean = false;@State private scale: number = 1;@State private opacity: number = 1;@State private translateX: number = 0;@State private translateY: number = 0;@State private zIndex: number = 0;private dragManager: DragManager = DragManager.getInstance();private longPressTimer: number = 0;private isLongPress: boolean = false;private startX: number = 0;private startY: number = 0;DragItem组件是可拖拽的基础项,包含拖拽所需的所有状态:是否正在拖拽、缩放比例、透明度、平移位置和Z轴层级。dragManager是拖拽管理器实例,longPressTimer用于长按检测,isLongPress标记长按状态,startX/Y记录触摸起始位置。aboutToAppear() {// 监听拖拽事件this.dragManager.addEventListener(‘dragStart’, (event) => {this.handleDragEvent(event);});this.dragManager.addEventListener('dragMove', (event) => { this.handleDragEvent(event); }); this.dragManager.addEventListener('dragEnd', (event) => { this.handleDragEvent(event); });}private handleDragEvent(event: DragEvent): void {switch (event.type) {case ‘dragStart’:this.onDragStart(event.data);break;case ‘dragMove’:this.onDragMove(event.data);break;case ‘dragEnd’:this.onDragEnd(event.data);break;}}在aboutToAppear生命周期中注册拖拽事件监听器,当拖拽管理器触发事件时调用对应的处理方法。handleDragEvent方法根据事件类型分发到不同的处理函数。// 长按开始拖拽private onLongPress(event: GestureEvent): void {if (this.item.disabled) return;this.isLongPress = true; this.isDragging = true; this.scale = this.config.dragScale; this.opacity = this.config.dragOpacity; this.zIndex = 1000; // 确保在最上层 // 开始拖拽 this.dragManager.startDrag( this.item, this.index, this.startX, this.startY, this.containerType );}// 触摸开始private onTouchStart(event: TouchEvent): void {if (this.item.disabled) return;const touch = event.touches[0]; this.startX = touch.x; this.startY = touch.y; // 开始长按计时 this.longPressTimer = setTimeout(() => { this.onLongPress({} as GestureEvent); }, 500) as unknown as number;}onLongPress方法处理长按手势,设置拖拽状态(缩放、透明度、Z轴层级),然后通知拖拽管理器开始拖拽。onTouchStart方法记录触摸起始位置,并启动500ms定时器检测长按。// 触摸移动private onTouchMove(event: TouchEvent): void {if (!this.isDragging || !this.isLongPress) return;const touch = event.touches[0]; const deltaX = touch.x - this.startX; const deltaY = touch.y - this.startY; // 更新位置 this.translateX = deltaX; this.translateY = deltaY; // 通知拖拽管理器 this.dragManager.updateDragPosition(touch.x, touch.y, this.index);}// 触摸结束private onTouchEnd(): void {// 清除长按定时器if (this.longPressTimer) {clearTimeout(this.longPressTimer);this.longPressTimer = 0;}if (this.isDragging && this.isLongPress) { // 结束拖拽 this.dragManager.endDrag(); this.resetState(); } this.isLongPress = false;}onTouchMove方法在拖拽过程中更新项的位置,计算相对于起始位置的偏移量,并通知拖拽管理器当前拖拽位置。onTouchEnd方法清理长按定时器,如果正在拖拽则结束拖拽,然后重置组件状态。private resetState(): void {this.isDragging = false;animateTo({duration: this.config.animationDuration,curve: animation.Curve.EaseOut}, () => {this.scale = 1;this.opacity = 1;this.translateX = 0;this.translateY = 0;this.zIndex = 0;});}// 拖拽事件处理private onDragStart(data: any): void {if (data.item.id === this.item.id) {// 自己开始拖拽,已处理return;}// 其他项开始拖拽,降低透明度 if (this.containerType === data.type) { this.opacity = 0.6; }}private onDragMove(data: any): void {if (data.hoverIndex === this.index && this.containerType === data.item.type) {// 拖拽到当前项上方this.scale = 0.95;} else {this.scale = 1;}}private onDragEnd(data: any): void {// 重置所有状态animateTo({duration: this.config.animationDuration,curve: animation.Curve.EaseOut}, () => {this.scale = 1;this.opacity = 1;});}resetState方法重置组件状态,使用animateTo实现平滑的动画恢复。onDragStart、onDragMove、onDragEnd方法处理拖拽管理器发出的事件:当其他项开始拖拽时降低透明度;当拖拽到当前项上方时轻微缩放;拖拽结束时恢复所有状态。build() {Column().width(this.containerType === ‘grid’ ? ‘100%’ : ‘100%’).height(this.containerType === ‘grid’ ? 100 : 60).backgroundColor(this.item.color).borderRadius(8).scale({ x: this.scale, y: this.scale }).opacity(this.opacity).translate({ x: this.translateX, y: this.translateY }).zIndex(this.zIndex).shadow({ radius: this.isDragging ? 10 : 0, color: Color.Gray }).animation({duration: this.config.animationDuration,curve: animation.Curve.EaseInOut}).gesture(// 长按手势LongPressGesture({ repeat: false }).onAction(() => {if (!this.item.disabled) {this.onLongPress({} as GestureEvent);}})).onTouch((event: TouchEvent) => {if (event.type === TouchType.Down) {this.onTouchStart(event);} else if (event.type === TouchType.Move) {this.onTouchMove(event);} else if (event.type === TouchType.Up) {this.onTouchEnd();}}){// 内容Row({ space: 8 }) {if (this.item.icon) {Image(this.item.icon).width(24).height(24)} Text(this.item.title) .fontSize(16) .fontColor(Color.White) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) } .padding(12) .justifyContent(FlexAlign.Center) }}}build方法构建可拖拽项的外观和交互。应用缩放、透明度、平移、Z轴层级和阴影等视觉效果。通过gesture添加长按手势识别,onTouch处理触摸事件实现拖拽。内部使用Row布局显示图标和标题。3.3 可拖拽网格组件// DraggableGrid.ets@Componentexport struct DraggableGrid {// 数据源@State items: DraggableItem[] = [];// 配置@Prop config: DragSortConfig = DragDefaultConfig.DEFAULT_CONFIG;@Prop columns: number = 3; // 网格列数@Prop columnGap: Length = 8; // 列间距@Prop rowGap: Length = 8; // 行间距// 事件回调@Prop onOrderChange?: (items: DraggableItem[]) => void;@Prop onDragStart?: (item: DraggableItem, index: number) => void;@Prop onDragEnd?: (fromIndex: number, toIndex: number) => void;@State private placeholderIndex: number = -1;@State private isDragging: boolean = false;@State private draggingIndex: number = -1;@State private autoScrollDirection: ‘up’ | ‘down’ | null = null;private dragManager: DragManager = DragManager.getInstance();private gridRef: Grid | null = null;private autoScrollTimer: number = 0;private scrollContainer: Scroller | null = null;aboutToAppear() {this.setupEventListeners();}DraggableGrid组件实现可拖拽排序的网格布局。@State装饰器管理内部状态,@Prop接收配置和回调。placeholderIndex标记拖拽占位符位置,isDragging标记拖拽状态,draggingIndex记录正在拖拽的项索引,autoScrollDirection控制自动滚动方向。private setupEventListeners(): void {this.dragManager.addEventListener(‘dragStart’, (event) => {this.handleDragStart(event.data);});this.dragManager.addEventListener('dragMove', (event) => { this.handleDragMove(event.data); }); this.dragManager.addEventListener('dragEnd', (event) => { this.handleDragEnd(event.data); }); this.dragManager.addEventListener('dragCancel', () => { this.handleDragCancel(); });}private handleDragStart(data: any): void {if (data.type !== ‘grid’) return;this.isDragging = true; this.draggingIndex = data.index; this.placeholderIndex = data.index; this.onDragStart?.(data.item, data.index); // 开始自动滚动检测 if (this.config.autoScrollEnabled) { this.startAutoScrollDetection(); }}setupEventListeners方法注册拖拽事件监听。handleDragStart方法处理拖拽开始事件,记录拖拽索引和占位符位置,触发onDragStart回调,如果启用了自动滚动则开始检测。private handleDragMove(data: any): void {if (data.type !== ‘grid’) return;// 计算悬停的网格索引 const hoverIndex = this.calculateHoverIndex(data.x, data.y); if (hoverIndex !== -1 && hoverIndex !== this.placeholderIndex) { // 更新占位符位置 this.placeholderIndex = hoverIndex; // 自动滚动 if (this.config.autoScrollEnabled) { this.checkAutoScroll(data.y); } }}// 计算悬停的网格索引private calculateHoverIndex(x: number, y: number): number {// 获取网格容器位置// 这里需要实际获取容器的位置信息// 简化实现:根据相对位置计算const itemWidth = 100; // 假设的项宽度 const itemHeight = 100; // 假设的项高度 const relativeX = x; // 实际需要减去容器偏移 const relativeY = y; // 实际需要减去容器偏移 const col = Math.floor(relativeX / (itemWidth + Number(this.columnGap))); const row = Math.floor(relativeY / (itemHeight + Number(this.rowGap))); const index = row * this.columns + col; if (index >= 0 && index < this.items.length && index !== this.draggingIndex) { return index; } return -1;}handleDragMove方法处理拖拽移动事件,计算当前拖拽位置对应的网格索引,更新占位符位置。calculateHoverIndex方法根据坐标计算悬停的网格索引,考虑网格列数、项尺寸和间距,返回有效的索引或-1。private handleDragEnd(data: any): void {if (data.type !== ‘grid’) return;const fromIndex = data.fromIndex; const toIndex = this.placeholderIndex; if (fromIndex !== toIndex && toIndex !== -1) { // 重新排序 this.reorderItems(fromIndex, toIndex); this.onOrderChange?.(this.items); this.onDragEnd?.(fromIndex, toIndex); } // 重置状态 this.resetState();}private handleDragCancel(): void {this.resetState();}// 重新排序private reorderItems(fromIndex: number, toIndex: number): void {const items = […this.items];const [draggedItem] = items.splice(fromIndex, 1);items.splice(toIndex, 0, draggedItem);// 更新order属性 items.forEach((item, index) => { item.order = index; }); this.items = items;}handleDragEnd方法处理拖拽结束事件,如果源索引和目标索引不同则重新排序数组,更新项的order属性,触发onOrderChange和onDragEnd回调,然后重置状态。reorderItems方法实现数组元素的移动,从原位置删除元素,插入到新位置。private resetState(): void {this.isDragging = false;this.draggingIndex = -1;this.placeholderIndex = -1;this.autoScrollDirection = null;// 停止自动滚动 this.stopAutoScroll();}// 自动滚动相关private startAutoScrollDetection(): void {// 实现自动滚动检测}private checkAutoScroll(y: number): void {if (!this.scrollContainer) return;const scrollTop = this.scrollContainer.currentOffset().yOffset; const containerHeight = 400; // 容器高度,实际需要获取 const threshold = this.config.scrollThreshold; if (y < scrollTop + threshold) { // 向上滚动 this.autoScrollDirection = 'up'; this.startAutoScroll('up'); } else if (y > scrollTop + containerHeight - threshold) { // 向下滚动 this.autoScrollDirection = 'down'; this.startAutoScroll('down'); } else { this.autoScrollDirection = null; this.stopAutoScroll(); }}private startAutoScroll(direction: ‘up’ | ‘down’): void {this.stopAutoScroll();this.autoScrollTimer = setInterval(() => { if (this.scrollContainer) { const currentOffset = this.scrollContainer.currentOffset().yOffset; const newOffset = direction === 'up' ? currentOffset - this.config.scrollSpeed : currentOffset + this.config.scrollSpeed; this.scrollContainer.scrollTo({ yOffset: newOffset }); } }, 16) as unknown as number; // 约60fps}private stopAutoScroll(): void {if (this.autoScrollTimer) {clearInterval(this.autoScrollTimer);this.autoScrollTimer = 0;}}resetState方法重置所有拖拽状态,停止自动滚动。checkAutoScroll方法检测是否需要自动滚动,当拖拽位置接近容器边界时触发。startAutoScroll方法启动自动滚动定时器,以60fps频率滚动容器。stopAutoScroll方法停止自动滚动。// 构建占位符@Builderprivate buildPlaceholder(index: number) {if (index !== this.placeholderIndex || !this.isDragging) {return;}Column() .width('100%') .height(100) .backgroundColor(this.config.placeholderColor) .borderRadius(8) .border({ width: 2, color: '#4D94FF', style: BorderStyle.Dashed }) .opacity(0.5)}// 构建网格项@Builderprivate buildGridItem(item: DraggableItem, index: number) {Stack() {// 占位符if (index === this.placeholderIndex && this.isDragging && index !== this.draggingIndex) {this.buildPlaceholder(index)} // 实际项 DragItem({ item: item, index: index, containerType: 'grid', config: this.config }) .opacity(this.isDragging && index === this.placeholderIndex ? 0 : 1) }}buildPlaceholder方法构建拖拽占位符,显示为半透明的虚线边框矩形。buildGridItem方法构建每个网格项,使用Stack布局叠加占位符和实际项,当项处于占位符位置时降低透明度。build() {Column() {// 网格容器Scroll(this.scrollContainer) {Grid() {ForEach(this.items, (item: DraggableItem, index: number) => {GridItem() {this.buildGridItem(item, index)}})}.columnsTemplate('1fr '.repeat(this.columns)).rowsTemplate('100px '.repeat(Math.ceil(this.items.length / this.columns))).columnsGap(this.columnGap).rowsGap(this.rowGap).width(‘100%’)}.width(‘100%’).height(400).onReachStart(() => {// 滚动到顶部}).onReachEnd(() => {// 滚动到底部})}}}build方法构建完整的可拖拽网格组件。使用Scroll包裹Grid实现滚动,Grid通过columnsTemplate定义列模板,rowsTemplate定义行模板。ForEach遍历items数组创建GridItem,每个GridItem包含buildGridItem构建的内容。3.4 可拖拽列表组件// DraggableList.ets@Componentexport struct DraggableList {@State items: DraggableItem[] = [];@Prop config: DragSortConfig = DragDefaultConfig.DEFAULT_CONFIG;@Prop itemHeight: number = 60; // 列表项高度@Prop onOrderChange?: (items: DraggableItem[]) => void;@State private placeholderIndex: number = -1;@State private isDragging: boolean = false;@State private draggingIndex: number = -1;@State private listOffsets: number[] = []; // 每个项的位置偏移private dragManager: DragManager = DragManager.getInstance();private listRef: List | null = null;private scroller: Scroller = new Scroller();DraggableList组件实现可拖拽排序的列表布局。itemHeight定义列表项高度,listOffsets数组缓存每个项的位置信息以提高性能。scroller用于控制列表滚动。aboutToAppear() {this.setupEventListeners();this.calculateItemOffsets();}aboutToUpdate() {this.calculateItemOffsets();}// 计算每个项的位置偏移private calculateItemOffsets(): void {this.listOffsets = [];let offset = 0;for (let i = 0; i < this.items.length; i++) { this.listOffsets[i] = offset; offset += this.itemHeight; if (i < this.items.length - 1) { offset += 8; // 项间距 } }}在aboutToAppear和aboutToUpdate生命周期中计算每个列表项的垂直偏移位置,缓存到listOffsets数组中,避免在拖拽过程中重复计算,提高性能。private setupEventListeners(): void {this.dragManager.addEventListener(‘dragStart’, (event) => {this.handleDragStart(event.data);});this.dragManager.addEventListener('dragMove', (event) => { this.handleDragMove(event.data); }); this.dragManager.addEventListener('dragEnd', (event) => { this.handleDragEnd(event.data); });}private handleDragStart(data: any): void {if (data.type !== ‘list’) return;this.isDragging = true; this.draggingIndex = data.index; this.placeholderIndex = data.index;}private handleDragMove(data: any): void {if (data.type !== ‘list’) return;// 计算当前拖拽位置对应的列表索引 const hoverIndex = this.calculateListHoverIndex(data.y); if (hoverIndex !== -1 && hoverIndex !== this.placeholderIndex) { this.placeholderIndex = hoverIndex; }}// 计算列表悬停索引private calculateListHoverIndex(y: number): number {// 获取列表容器的位置// 简化实现:根据y坐标计算const scrollTop = this.scroller.currentOffset().yOffset; const relativeY = y + scrollTop; // 实际需要考虑容器偏移 for (let i = 0; i < this.listOffsets.length; i++) { const itemTop = this.listOffsets[i]; const itemBottom = itemTop + this.itemHeight; if (relativeY >= itemTop && relativeY <= itemBottom) { // 判断是在项的上半部分还是下半部分 const itemCenter = itemTop + this.itemHeight / 2; if (relativeY < itemCenter) { return i; // 在上半部分,插入到前面 } else { return Math.min(i + 1, this.items.length); // 在下半部分,插入到后面 } } } return -1;}handleDragStart和handleDragMove方法与网格组件类似,处理拖拽开始和移动事件。calculateListHoverIndex方法根据y坐标计算悬停的列表索引,考虑项的高度和位置,判断拖拽位置在项的上半部分还是下半部分,决定插入到前面还是后面。private handleDragEnd(data: any): void {if (data.type !== ‘list’) return;const fromIndex = data.fromIndex; const toIndex = this.placeholderIndex; if (fromIndex !== toIndex && toIndex !== -1) { // 重新排序 this.reorderItems(fromIndex, toIndex); this.onOrderChange?.(this.items); } this.resetState();}private reorderItems(fromIndex: number, toIndex: number): void {const items = […this.items];const [draggedItem] = items.splice(fromIndex, 1);items.splice(toIndex, 0, draggedItem);// 更新order属性 items.forEach((item, index) => { item.order = index; }); this.items = items;}private resetState(): void {this.isDragging = false;this.draggingIndex = -1;this.placeholderIndex = -1;}handleDragEnd方法处理拖拽结束,重新排序数组。reorderItems方法实现列表项的重排序逻辑,与网格组件类似。resetState方法重置拖拽状态。// 构建列表项@Builderprivate buildListItem(item: DraggableItem, index: number) {Column() {// 占位符if (index === this.placeholderIndex && this.isDragging && index !== this.draggingIndex) {Column().width(‘100%’).height(this.itemHeight).backgroundColor(this.config.placeholderColor).borderRadius(4).border({ width: 2, color: ‘#4D94FF’, style: BorderStyle.Dashed }).margin({ bottom: 8 })} // 实际项 DragItem({ item: item, index: index, containerType: 'list', config: this.config }) .width('100%') .height(this.itemHeight) .margin({ bottom: 8 }) .opacity(this.isDragging && index === this.placeholderIndex ? 0 : 1) }}build() {Column() {List({ space: 8, scroller: this.scroller }) {ForEach(this.items, (item: DraggableItem, index: number) => {ListItem() {this.buildListItem(item, index)}})}.width(‘100%’).height(400)}}}buildListItem方法构建每个列表项,包含占位符和实际的可拖拽项。build方法使用List组件创建可滚动列表,ForEach遍历items数组创建ListItem,每个列表项高度固定为itemHeight。四、使用示例4.1 基本使用// DragSortDemo.ets@Entry@Componentexport struct DragSortDemo {@State private gridItems: DraggableItem[] = [{ id: ‘1’, title: ‘项目1’, color: ‘#FF6B6B’, order: 0 },{ id: ‘2’, title: ‘项目2’, color: ‘#4ECDC4’, order: 1 },{ id: ‘3’, title: ‘项目3’, color: ‘#45B7D1’, order: 2 },{ id: ‘4’, title: ‘项目4’, color: ‘#96CEB4’, order: 3 },{ id: ‘5’, title: ‘项目5’, color: ‘#FFEAA7’, order: 4 },{ id: ‘6’, title: ‘项目6’, color: ‘#DDA0DD’, order: 5 }];@State private listItems: DraggableItem[] = [{ id: ‘a’, title: ‘任务A’, color: ‘#FF6B6B’, order: 0 },{ id: ‘b’, title: ‘任务B’, color: ‘#4ECDC4’, order: 1 },{ id: ‘c’, title: ‘任务C’, color: ‘#45B7D1’, order: 2 },{ id: ‘d’, title: ‘任务D’, color: ‘#96CEB4’, order: 3 },{ id: ‘e’, title: ‘任务E’, color: ‘#FFEAA7’, order: 4 }];private customConfig: DragSortConfig = {…DragDefaultConfig.DEFAULT_CONFIG,animationDuration: 250,dragScale: 1.05,vibrationEnabled: true};// 网格排序回调private handleGridOrderChange(items: DraggableItem[]): void {console.log(‘网格新顺序:’, items.map(item => item.title));this.gridItems = items;}// 列表排序回调private handleListOrderChange(items: DraggableItem[]): void {console.log(‘列表新顺序:’, items.map(item => item.title));this.listItems = items;}DragSortDemo是演示入口组件,定义了两组示例数据:gridItems用于网格拖拽,listItems用于列表拖拽。customConfig自定义拖拽配置,缩短动画时长,减小缩放比例。handleGridOrderChange和handleListOrderChange是排序完成后的回调函数,更新数据并打印新顺序。build() {Column({ space: 20 }) {// 标题Text(‘拖拽排序演示’).fontSize(24).fontWeight(FontWeight.Bold).fontColor(Color.Black).margin({ top: 30, bottom: 20 }) // 网格拖拽区域 Text('网格拖拽排序') .fontSize(18) .fontWeight(FontWeight.Medium) .margin({ bottom: 10 }) .alignSelf(ItemAlign.Start) DraggableGrid({ items: this.gridItems, config: this.customConfig, columns: 3, columnGap: 12, rowGap: 12, onOrderChange: this.handleGridOrderChange.bind(this) }) .width('100%') .height(300) .backgroundColor('#F5F5F5') .borderRadius(12) .padding(12) // 列表拖拽区域 Text('列表拖拽排序') .fontSize(18) .fontWeight(FontWeight.Medium) .margin({ top: 20, bottom: 10 }) .alignSelf(ItemAlign.Start) DraggableList({ items: this.listItems, config: this.customConfig, itemHeight: 70, onOrderChange: this.handleListOrderChange.bind(this) }) .width('100%') .height(300) .backgroundColor('#F5F5F5') .borderRadius(12) .padding(12) // 操作按钮 Row({ space: 20 }) { Button('重置网格') .onClick(() => { this.resetGrid(); }) Button('重置列表') .onClick(() => { this.resetList(); }) } .margin({ top: 20 }) } .width('100%') .height('100%') .padding(20) .backgroundColor(Color.White)}build方法构建完整的演示界面,包含标题、可拖拽网格区域、可拖拽列表区域和重置按钮。DraggableGrid和DraggableList组件接收对应的数据和配置,设置样式和回调函数。// 重置网格private resetGrid(): void {this.gridItems = this.gridItems.sort((a, b) => Number(a.id.replace(‘项目’, ‘’)) - Number(b.id.replace(‘项目’, ‘’))).map((item, index) => ({ …item, order: index }));}// 重置列表private resetList(): void {this.listItems = this.listItems.sort((a, b) => a.id.localeCompare(b.id)).map((item, index) => ({ …item, order: index }));}}resetGrid和resetList方法重置对应的数据为初始顺序。resetGrid按数字ID排序,resetList按字母ID排序,并重新设置order属性。五、高级特性5.1 跨容器拖拽// CrossDragManager.etsexport class CrossDragManager extends DragManager {private sourceContainer: ‘grid’ | ‘list’ | null = null;private targetContainer: ‘grid’ | ‘list’ | null = null;private dragPreview: Component | null = null;// 开始跨容器拖拽startCrossDrag(item: DraggableItem, sourceType: ‘grid’ | ‘list’,startX: number, startY: number): void {this.sourceContainer = sourceType;this.targetContainer = null;// 创建拖拽预览 this.createDragPreview(item, startX, startY); super.startDrag(item, -1, startX, startY, sourceType);}// 创建拖拽预览private createDragPreview(item: DraggableItem, x: number, y: number): void {// 在实际实现中,这里应该创建一个全局的拖拽预览组件// 简化实现:通过状态管理在其他组件中显示预览}// 更新拖拽目标updateDragTarget(x: number, y: number, targetType: ‘grid’ | ‘list’ | null,targetIndex: number = -1): void {this.targetContainer = targetType;// 更新拖拽管理器 this.updateDragPosition(x, y, targetIndex); // 更新预览位置 this.updateDragPreview(x, y);}// 完成跨容器拖拽completeCrossDrag(): { sourceItem: DraggableItem, targetType: ‘grid’ | ‘list’, targetIndex: number } | null {if (!this.draggingItem || !this.targetContainer) {this.cancelDrag();return null;}const result = { sourceItem: this.draggingItem, targetType: this.targetContainer, targetIndex: this.targetIndex }; this.endDrag(); this.cleanupDragPreview(); this.sourceContainer = null; this.targetContainer = null; return result;}private cleanupDragPreview(): void {// 清理拖拽预览}}CrossDragManager扩展DragManager,支持跨容器拖拽功能。sourceContainer记录拖拽源容器类型,targetContainer记录目标容器类型。startCrossDrag方法开始跨容器拖拽,创建拖拽预览。updateDragTarget方法更新拖拽目标和预览位置。completeCrossDrag方法完成跨容器拖拽,返回拖拽结果。5.2 动画优化// SortAnimation.ets@Componentexport struct SortAnimation {@Prop fromIndex: number = -1;@Prop toIndex: number = -1;@Prop itemCount: number = 0;@Prop animationType: ‘slide’ | ‘fade’ | ‘scale’ = ‘slide’;@Prop duration: number = 300;@State private animationProgress: number = 0;@State private animatingItems: Set<number> = new Set();private animationController: animation.Animator = new animation.Animator();aboutToAppear() {if (this.fromIndex !== -1 && this.toIndex !== -1) {this.startAnimation();}}private startAnimation(): void {// 确定需要动画的项const start = Math.min(this.fromIndex, this.toIndex);const end = Math.max(this.fromIndex, this.toIndex);for (let i = start; i <= end; i++) { this.animatingItems.add(i); } // 启动动画 this.animationController.update({ duration: this.duration, curve: animation.Curve.EaseInOut }); this.animationController.onFrame((progress: number) => { this.animationProgress = progress; }); this.animationController.play();}// 获取项的位置偏移getItemOffset(index: number): { x: number; y: number } {if (!this.animatingItems.has(index)) {return { x: 0, y: 0 };}const direction = this.fromIndex < this.toIndex ? 1 : -1; const isMovingRight = direction === 1; let offset = 0; if (index === this.fromIndex) { // 被拖拽的项 const totalDistance = Math.abs(this.toIndex - this.fromIndex); offset = totalDistance * 100 * this.animationProgress * direction; } else if ((isMovingRight && index > this.fromIndex && index <= this.toIndex) || (!isMovingRight && index < this.fromIndex && index >= this.toIndex)) { // 被挤开的项 offset = -100 * this.animationProgress * direction; } return { x: offset, y: 0 };}}SortAnimation组件实现排序过程中的动画效果。animationProgress记录动画进度,animatingItems记录需要动画的项索引。startAnimation方法确定动画范围,启动Animator控制器。getItemOffset方法计算每个项在动画中的位置偏移,被拖拽的项移动完整距离,被挤开的项反向移动。六、最佳实践6.1 性能优化建议避免不必要的重绘:使用@State和@Prop合理管理状态节流事件处理:对高频事件进行节流对象池复用:复用拖拽预览等临时对象虚拟滚动:大数据量时使用虚拟滚动性能优化包括:1)合理使用状态装饰器避免整个组件树重绘;2)对拖拽移动事件进行节流,减少计算频率;3)复用拖拽预览等临时对象,减少内存分配;4)大数据量时实现虚拟滚动,只渲染可见区域。6.2 用户体验优化视觉反馈:拖拽时提供明确的视觉反馈触觉反馈:使用震动增强交互感边界处理:容器边界的自动滚动错误恢复:拖拽失败时的优雅恢复用户体验优化包括:1)拖拽时显示缩放、阴影、透明度变化等视觉反馈;2)开始拖拽时提供震动触觉反馈;3)拖拽到容器边界时自动滚动;4)拖拽失败时平滑恢复到原位置。6.3 可访问性// 为屏幕阅读器提供支持.accessibilityLabel(可拖拽项:${item.title}).accessibilityHint(‘长按并拖拽以重新排序’).accessibilityRole(AccessibilityRole.Button).accessibilityState({selected: this.isDragging,disabled: this.item.disabled})可访问性支持通过accessibility属性为辅助技术提供信息。accessibilityLabel描述项的内容,accessibilityHint提供操作提示,accessibilityRole设置为Button表示可交互,accessibilityState提供当前状态(是否选中、是否禁用)。七、总结本实现方案提供了完整的Grid和List内拖拽排序功能,支持长按拖拽、平滑动画、自动滚动、震动反馈等特性,通过合理的架构设计确保了良好的性能和用户体验。
-
1.1 问题说明在HarmonyOS多模块应用开发中,开发者常需将共享功能模块(如通用UI组件、工具类库)封装为HSP(Harmony Share Package)进行复用。然而,实际开发过程中,团队频繁遇到以下问题:1. 签名一致性要求:HSP必须与主应用具有相同的bundleName和签名,导致在调试阶段需要反复安装HSP包2. 多模块集成困难:当多个业务模块同时依赖同一个HSP时,容易出现签名冲突、bundleName不一致的错误3. 开发效率低下:每次修改HSP代码后,都需要重新安装HSP包,调试周期延长4. 版本管理混乱:HSP的版本管理与主应用不一致,导致依赖关系难以追踪具体案例:某电商应用团队在开发商品展示组件库时,将其封装为HSP。在集成到订单模块、商品详情模块和购物车模块时,频繁遇到"签名不匹配"和"bundleName冲突"错误,导致每天平均浪费2小时在包安装和调试上,开发进度严重滞后。1.2 原因分析此问题的根本原因在于HSP的设计定位与实际开发需求不匹配:1. HSP设计局限:· HSP是为应用间共享设计的,而非模块间代码复用· HSP必须通过安装才能使用,与库模块"编译时链接"的预期不符· HSP强制要求与主应用相同的bundleName和签名2. HarmonyOS包管理机制:· HarmonyOS要求所有已安装包必须有唯一bundleName和有效签名· HSP的安装机制导致每次修改都需要重新安装,违背"修改即生效"的开发体验· HSP的bundleName与主应用绑定,无法灵活支持多应用共享3. 开发流程不匹配:· HSP的设计初衷是用于应用间的共享,而非模块间的代码复用· 开发者期望的"类似npm包"的开发体验与HSP的安装式设计存在根本冲突· 早期项目错误地将HSP用于模块化开发,导致技术债积累4. 技术认知偏差:· 开发团队对HAR(Harmony Archive)这种更适合库模块的格式缺乏了解· 未在项目初期规划好模块化方案,导致后期重构成本高1.3 解决思路解决该问题需要将HSP转换为HAR,实现更符合库模块需求的开发体验。整体解决框架如下:HSP模块转换为HAR模块│├── 配置调整:修改核心配置文件,移除非必要项│├── 结构优化:删除HSP特有文件,调整项目结构│├── 构建系统适配:更新构建配置,支持HAR格式│└── 接口导出机制:设置入口文件,暴露可共享API优化方向:1. 精简配置:移除HSP特有配置,保留HAR必需配置2. 结构规范化:遵循HAR项目标准结构,消除冗余3. 接口标准化:提供清晰、统一的导出接口4. 构建优化:配置适合库模块的构建流程5. 开发体验提升:实现"修改即生效"的开发体验1.4 解决方案4.1 模块配置调整步骤1:修改module.json5文件{ "module": { "name": "product-components", "type": "har", // 关键变更:HSP类型改为har "deviceTypes": [ "default", "tablet", "2in1" ] // 删除以下HSP特有属性: // - deliveryWithInstall // - pages}}注意:deviceTypes保留default表示兼容所有设备类型4.2 项目结构优化步骤2:删除页面路由配置删除路径:resource\base\profile\main_pages.json4.3 构建系统适配步骤3:修改hvigorfile.ts文件// hvigorfile.tsimport { harTasks } from '@ohos/hvigor-ohos-plugin';export default { system: harTasks, /* Hvigor内置插件,不可修改 */ plugins: [] /* 自定义插件扩展 */}4.4 依赖管理调整步骤4:修改oh-package.json5文件{ "name": "@company/product-components","version": "1.0.0","description": "Product display components library","main": "Index.ets", // 关键配置:指定导出入口文件"dependencies": { // 依赖项配置} // 删除packageType配置}4.5 项目级配置调整步骤5:修改build-profile.json5文件{ "modules": [ { "name": "entry", "srcPath": "./entry", "targets": [ { "deviceTypes": ["phone", "tablet", "2in1"], "modules": ["entry"] } ] }, { "name": "library", // HSP模块名 "srcPath": "./library", "targets": [] // 删除HSP配置下的targets } ]}4.6 接口导出配置步骤6:创建并配置导出入口文件// Index.ets (模块根目录)// 导出ArkUI组件export { default as ProductCard } from './src/main/ets/components/ProductCard';export { default as ShoppingCart } from './src/main/ets/components/ShoppingCart';// 导出工具函数export * from './src/main/ets/utils/StringUtils';export * from './src/main/ets/utils/NetworkUtils';// 导出业务接口export { ProductService } from './src/main/ets/services/ProductService';4.7 完整转换流程1. 准备工作:· 备份原始HSP模块· 确认无未提交的代码变更· 评估模块中是否包含HSP特有功能2. 执行转换:· 按顺序执行上述5个步骤· 每完成一步进行验证· 重点关注配置文件的语法正确性3. 验证与测试:· 创建测试HAP模块,依赖转换后的HAR· 验证导出接口是否可正常访问· 测试组件渲染和功能是否正常· 检查构建是否成功4. 问题处理:· 遇到构建错误时,检查配置文件语法· 接口无法导入时,验证Index.ets导出路径· 性能问题时,优化导出接口设计1.5 结果展示5.1 转换效果验证成功转换的标志:· 项目构建成功无错误· 其他模块可以正常依赖并使用该HAR· 导出的ArkUI组件在IDE中正确显示代码提示· 应用运行时组件渲染正常,功能完整使用示例// 在其他HAP模块中使用import { ProductCard, ProductService } from '@company/product-components';@Entry@Componentstruct ProductPage { @State products: Array<any> = []; aboutToAppear() { // 调用导出的服务 ProductService.getProducts().then(data => { this.products = data; }); } build() { Column() { // 使用导出的组件 ForEach(this.products, item => { ProductCard({ product: item }) }) } }}5.2 同类问题参考本方案为HarmonyOS开发者提供了HSP转HAR的标准流程,可作为以下场景的参考:场景参考价值通用UI组件库建设提供了标准的HAR封装流程业务能力中心化抽取为能力模块提供复用方案基础设施封装网络、存储、日志等模块的复用第三方SDK集成简化SDK集成和版本管理最佳实践建议:1. 设计先行:在项目初期规划模块边界,避免后期大规模重构2. 渐进式转换:大型模块可分批次导出接口,降低风险3. 版本管理:为HAR模块实施语义化版本控制4. 文档配套:提供完善的API文档和使用示例5. 自动化验证:建立CI流程确保导出接口的稳定性通过本方案,开发团队不仅解决了当前的模块复用问题,更为构建高质量的HarmonyOS应用生态系统奠定了基础。转换后,团队节省调试时间,组件复用率显著提高,为后续的模块化开发提供了可复用的标准流程。
-
1.1 问题说明在HarmonyOS应用开发过程中,开发者经常面临模块化和代码复用的需求。当需要将一个HAP(Harmony Ability Package)模块开发为可共享的库,以便其他模块或应用能够使用其中的ArkUI组件或接口时,会遇到技术障碍。具体表现为:HAP模块无法被其他模块直接引用其导出的接口或组件,导致代码复用困难,开发效率降低,项目结构混乱。某电商项目团队在开发过程中遇到此问题:他们开发了一个通用的商品展示组件库,最初以HAP形式构建,但在尝试将其作为二方库集成到其他业务模块时,发现无法正确导出和使用这些组件,阻碍了项目进度和团队协作效率。1.2 原因分析此问题的根本原因在于HAP与HAR(Harmony Archive)的设计定位和功能差异:1. 架构设计差异:· HAP是可独立安装运行的应用模块,主要面向终端用户· HAR是静态共享包,专为代码和资源共享设计,不可独立安装2. 功能限制:· HAP不支持导出接口或ArkUI组件给其他模块使用· HAP包含Ability组件和页面路由系统,与库模块的定位不符3. 配置冲突:· HAP特有的配置项(如mainElement、pages、abilities等)在库模块中无意义· 构建系统对HAP和HAR的处理流程不同,HAP配置无法满足库模块的构建需求4. 依赖关系:· HAP设计为应用的最顶层模块,而非依赖项· 模块间的依赖关系要求库模块必须使用HAR格式1.3 解决思路解决该问题需要通过系统性转换,将HAP模块重构为HAR模块。整体思路如下:HAP模块转换为HAR模块│├── 配置调整:修改核心配置文件,移除非必要项│├── 结构优化:删除HAP特有文件,调整项目结构│├── 构建系统适配:更新构建配置,支持HAR格式│└── 接口导出机制:设置入口文件,暴露可共享API优化方向:1. 精简配置:移除HAP特有配置,保留HAR必需配置2. 结构规范化:遵循HAR项目标准结构3. 接口标准化:提供清晰的导出接口,提高复用性4. 构建优化:配置适合库模块的构建流程5. 兼容性保障:确保转换后保持原有功能完整性1.4 解决方案4.1 模块配置调整步骤1:修改module.json5文件{ "module": { "name": "your_har_name", "type": "har", // 关键变更:HAP类型改为har "deviceTypes": [ "phone", "tablet", "2in1" ] // 删除以下HAP特有属性: // - mainElement // - deliveryWithInstall // - installationFree // - pages // - extensionAbilities (API version 13及以下) // - abilities (API version 13及以下)}}注意事项:· API version 13及以下版本必须删除abilities标签· 移除所有Ability相关配置,HAR不支持UIAbility和ExtensionAbility· 保留必要的依赖项配置在dependencies中4.2 项目结构优化步骤2:删除页面路由配置删除路径:src\main\resource\base\profile\main_pages.json步骤3:修改构建配置文件// hvigorfile.tsimport { harTasks } from '@ohos/hvigor-ohos-plugin';export default { system: harTasks, /* Hvigor内置插件,不可修改 */ plugins: [] /* 可扩展自定义插件 */}步骤4:修改项目级构建配置// build-profile.json5{ "app": { "signingConfigs": [], "products": [ { "name": "default", "signingConfig": "default", "compatibleSdkVersion": "5.0.0(12)", "runtimeOS": "HarmonyOS" } ]}, "modules": [ { "name": "entry", "srcPath": "./entry", "targets": [ /* 保留其他模块的targets */ ] } // 删除已转换为HAR模块的targets配置 ]}4.3 接口导出配置步骤5:创建并配置导出入口文件// Index.ets (模块根目录)// 导出ArkUI组件export { default as ProductCard } from './src/main/ets/components/ProductCard';export { default as ShoppingCart } from './src/main/ets/components/ShoppingCart';// 导出工具函数export * from './src/main/ets/utils/StringUtils';export * from './src/main/ets/utils/NetworkUtils';// 导出业务接口export { ProductService } from './src/main/ets/services/ProductService';// oh-package.json5{ "name": "@company/product-components","version": "1.0.0","description": "Product display components library","main": "Index.ets", // 关键配置:指定导出入口文件"dependencies": { // 依赖项配置}}4.4 完整转换流程1. 准备工作:· 备份原始HAP模块· 确认无未提交的代码变更· 评估模块中Ability组件的迁移方案2. 执行转换:· 按顺序执行上述5个步骤· 每完成一步进行验证· 重点关注配置文件的语法正确性3. 验证与测试:· 创建测试HAP模块,依赖转换后的HAR· 验证导出接口是否可正常访问· 测试组件渲染和功能是否正常· 检查构建是否成功4. 问题处理:· 遇到构建错误时,检查配置文件语法· 组件无法导入时,验证Index.ets导出路径· 性能问题时,优化导出接口设计1.5 结果展示5.1 转换效果验证成功转换的标志:· 项目构建成功无错误· 其他模块可以正常依赖并使用该HAR· 导出的ArkUI组件在IDE中正确显示代码提示· 应用运行时组件渲染正常,功能完整使用示例:// 在其他HAP模块中使用import { ProductCard, ProductService } from '@company/product-components';@Entry@Componentstruct ProductPage { @State products: Array<any> = []; aboutToAppear() { // 调用导出的服务 ProductService.getProducts().then(data => { this.products = data; }); } build() { Column() { // 使用导出的组件 ForEach(this.products, item => { ProductCard({ product: item }) }) } }}5.2 同类问题参考本方案为HarmonyOS开发者提供了HAP转HAR的标准流程,可作为以下场景的参考:· 通用UI组件库建设· 业务能力中心化抽取· 基础设施(网络、存储、日志等)封装· 第三方SDK的二次封装适配最佳实践建议:1. 设计先行:在项目初期就规划好模块边界,避免后期大规模重构2. 渐进式转换:大型模块可分批次导出接口,降低风险3. 版本管理:为HAR模块实施语义化版本控制4. 文档配套:提供完善的API文档和使用示例5. 自动化验证:建立CI流程确保导出接口的稳定性通过本方案,开发团队不仅解决了当前的模块复用问题,更为构建高质量的HarmonyOS应用生态系统奠定了基础,使技术资产得到有效沉淀和复用。
-
一、项目概述1.1 功能特性● 基于HarmonyOS 4.0+ API实现● 使用Swiper的customContentTransition和rotate属性● 支持3D立方体旋转效果● 支持自动轮播和手动滑动● 响应式设计,适配不同屏幕● 高性能动画渲染二、架构设计2.1 核心组件结构3D立方体轮播组件├── CubeSwiper.ets (主容器)├── CubeItem.ets (立方体面)├── CubeTransition.ets (自定义过渡动画)└── CubeManager.ets (状态管理)2.2 数据模型定义// CubeDataModel.ets// 轮播项数据模型export interface CubeItemData {id: string;title: string;subtitle?: string;color: ResourceColor;image?: Resource;backgroundColor?: ResourceColor;rotation?: number; // 初始旋转角度zIndex?: number; // Z轴位置}// 轮播配置export interface CubeSwiperConfig {autoPlay: boolean; // 自动播放autoPlayInterval: number; // 播放间隔(ms)effect3D: boolean; // 启用3D效果perspective: number; // 透视距离cubeSize: number; // 立方体尺寸rotateAngle: number; // 旋转角度animationDuration: number; // 动画时长showIndicator: boolean; // 显示指示器indicatorType: ‘dot’ | ‘number’ | ‘progress’; // 指示器类型loop: boolean; // 循环播放gestureEnabled: boolean; // 启用手势}// 默认配置export class CubeDefaultConfig {static readonly DEFAULT_CONFIG: CubeSwiperConfig = {autoPlay: true,autoPlayInterval: 3000,effect3D: true,perspective: 1000,cubeSize: 300,rotateAngle: 20,animationDuration: 500,showIndicator: true,indicatorType: ‘dot’,loop: true,gestureEnabled: true};}这里定义了轮播组件的数据模型和配置接口,使用TypeScript接口确保类型安全。CubeItemData定义了每个轮播项的数据结构,包含标题、颜色、图片等属性。CubeSwiperConfig定义了轮播器的配置项,如自动播放、3D效果、动画时长等,便于统一管理和自定义。CubeDefaultConfig提供了默认配置,方便快速使用。三、核心实现3.1 3D立方体旋转轮播组件// CubeSwiper.ets@Componentexport struct CubeSwiper {// 数据源@Prop data: CubeItemData[] = [];// 配置参数@Prop config: CubeSwiperConfig = CubeDefaultConfig.DEFAULT_CONFIG;// 当前激活索引@State currentIndex: number = 0;// 旋转角度@State rotationX: number = 0;@State rotationY: number = 0;// Swiper控制器private swiperController: SwiperController = new SwiperController();// 动画控制器private animationController: animation.Animator = new animation.Animator();// 自动播放定时器private autoPlayTimer: number = 0;aboutToAppear() {if (this.config.autoPlay) {this.startAutoPlay();}}aboutToDisappear() {this.stopAutoPlay();}// 开始自动播放private startAutoPlay(): void {this.stopAutoPlay();this.autoPlayTimer = setInterval(() => { if (this.currentIndex < this.data.length - 1 || this.config.loop) { this.next(); } }, this.config.autoPlayInterval);}这是CubeSwiper组件的初始部分,定义了核心状态变量和生命周期方法。@Prop装饰器用于接收外部传入的数据和配置,@State装饰器用于管理组件内部状态。在aboutToAppear生命周期中启动自动播放,aboutToDisappear中清理定时器以防止内存泄漏。startAutoPlay方法控制自动轮播逻辑。// 停止自动播放private stopAutoPlay(): void {if (this.autoPlayTimer) {clearInterval(this.autoPlayTimer);this.autoPlayTimer = 0;}}// 下一页next(): void {if (this.currentIndex < this.data.length - 1 || this.config.loop) {const nextIndex = (this.currentIndex + 1) % this.data.length;this.swipeTo(nextIndex);}}// 上一页previous(): void {if (this.currentIndex > 0 || this.config.loop) {const prevIndex = (this.currentIndex - 1 + this.data.length) % this.data.length;this.swipeTo(prevIndex);}}// 跳转到指定页swipeTo(index: number): void {if (index >= 0 && index < this.data.length && index !== this.currentIndex) {this.animateTransition(index);this.currentIndex = index;this.swiperController?.showNext();}}这部分实现了轮播导航控制逻辑。next和previous方法处理向前/向后翻页,考虑了循环播放的情况。swipeTo方法确保目标索引有效后才执行跳转。stopAutoPlay方法清理定时器资源,防止内存泄漏。// 执行过渡动画private animateTransition(toIndex: number): void {const direction = toIndex > this.currentIndex ? 1 : -1;this.animationController.stop(); this.animationController.update({ duration: this.config.animationDuration, curve: animation.Curve.EaseInOut }); this.animationController.onFrame((progress: number) => { // 计算旋转角度 const rotateY = direction * 90 * progress; this.rotationY = rotateY; // 透视效果 if (this.config.effect3D) { const perspective = this.config.perspective + progress * 200; // 更新透视 } }); this.animationController.play();}animateTransition方法实现3D过渡动画的核心逻辑。通过animation.Animator控制器创建流畅的动画效果,计算旋转角度实现3D旋转。direction变量确定旋转方向(向前/向后),progress是0-1的动画进度值,用于计算中间状态。// 构建立方体容器@Builderprivate buildCubeContainer() {Column() {// 立方体容器Stack({ alignContent: Alignment.Center }) {// 3D变换容器Column().width(this.config.cubeSize).height(this.config.cubeSize).rotate({x: this.rotationX,y: this.rotationY,z: 0}).perspective(this.config.effect3D ? this.config.perspective : 0).transform3d({ perspective: 1 }){this.buildCubeFaces()}}.width(‘100%’).height(this.config.cubeSize).clip(true)}}buildCubeContainer方法构建3D立方体的外层容器。使用Stack布局居中立方体,Column作为3D变换容器,应用rotate实现3D旋转,perspective属性创建透视效果,transform3d启用3D变换。clip(true)确保内容不溢出容器。// 构建立方体六个面@Builderprivate buildCubeFaces() {// 前CubeFace({data: this.data[this.getValidIndex(this.currentIndex)],position: ‘front’,config: this.config})// 后 CubeFace({ data: this.data[this.getValidIndex(this.currentIndex + 1)], position: 'back', config: this.config }) // 左 CubeFace({ data: this.data[this.getValidIndex(this.currentIndex - 1)], position: 'left', config: this.config }) // 右 CubeFace({ data: this.data[this.getValidIndex(this.currentIndex + 2)], position: 'right', config: this.config }) // 上 CubeFace({ data: this.data[this.getValidIndex(this.currentIndex - 2)], position: 'top', config: this.config }) // 下 CubeFace({ data: this.data[this.getValidIndex(this.currentIndex + 3)], position: 'bottom', config: this.config })}buildCubeFaces方法构建立方体的六个面。每个面通过CubeFace组件渲染,传入对应位置的数据。getValidIndex方法确保索引不越界,支持循环轮播。六个面分别代表前、后、左、右、上、下六个方向。// 获取有效索引private getValidIndex(index: number): number {if (this.config.loop) {return (index + this.data.length) % this.data.length;}return Math.max(0, Math.min(index, this.data.length - 1));}// 构建指示器@Builderprivate buildIndicator() {if (!this.config.showIndicator) {return;}Column() { if (this.config.indicatorType === 'dot') { Row({ space: 8 }) { ForEach(this.data, (_, index: number) => { Circle() .width(8) .height(8) .fill(index === this.currentIndex ? Color.Blue : Color.Gray) .opacity(index === this.currentIndex ? 1 : 0.5) .animation({ duration: 300, curve: animation.Curve.EaseInOut }) }) } } else if (this.config.indicatorType === 'number') { Text(`${this.currentIndex + 1}/${this.data.length}`) .fontSize(16) .fontColor(Color.White) .padding(8) .backgroundColor(Color.Black) .borderRadius(12) } else if (this.config.indicatorType === 'progress') { Row() .width(200) .height(4) .backgroundColor('#FFFFFF40') .borderRadius(2) .overlay( Row() .width(`${(this.currentIndex + 1) / this.data.length * 100}%`) .height('100%') .backgroundColor(Color.Blue) .borderRadius(2) .animation({ duration: 300, curve: animation.Curve.EaseInOut }) ) } } .width('100%') .margin({ top: 20 }) .justifyContent(FlexAlign.Center)}这部分实现页面指示器。支持三种类型:圆点指示器、数字指示器和进度条指示器。getValidIndex方法处理索引边界,支持循环模式。指示器会根据当前激活项高亮显示,并使用动画实现平滑过渡效果。build() {Column() {// 轮播区域this.buildCubeContainer() // 指示器 this.buildIndicator() // 控制按钮 if (!this.config.autoPlay) { Row({ space: 20 }) { Button('上一页') .onClick(() => this.previous()) Button('下一页') .onClick(() => this.next()) } .margin({ top: 20 }) } } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) .onClick(() => { // 点击暂停/恢复自动播放 if (this.config.autoPlay) { if (this.autoPlayTimer) { this.stopAutoPlay(); } else { this.startAutoPlay(); } } })}}build方法是组件的入口,构建完整的界面布局。包含立方体容器、指示器、控制按钮三部分。当autoPlay为false时显示手动控制按钮。点击容器区域可以暂停/恢复自动播放,增强用户交互体验。3.2 立方体面组件// CubeFace.ets@Componentexport struct CubeFace {@Prop data: CubeItemData;@Prop position: ‘front’ | ‘back’ | ‘left’ | ‘right’ | ‘top’ | ‘bottom’;@Prop config: CubeSwiperConfig;@State private scale: number = 1;@State private opacity: number = 1;aboutToAppear() {this.applyPosition();}// 应用位置变换private applyPosition(): void {const halfSize = this.config.cubeSize / 2;switch (this.position) { case 'front': this.transform({ translateZ: halfSize }); break; case 'back': this.transform({ rotateY: 180, translateZ: halfSize }); break; case 'left': this.transform({ rotateY: -90, translateX: -halfSize }); break; case 'right': this.transform({ rotateY: 90, translateX: halfSize }); break; case 'top': this.transform({ rotateX: 90, translateY: -halfSize }); break; case 'bottom': this.transform({ rotateX: -90, translateY: halfSize }); break; }}CubeFace组件表示立方体的一个面。applyPosition方法根据面在立方体上的位置(前、后、左、右、上、下)应用不同的3D变换。每个面先旋转到正确方向,然后平移到立方体边缘位置,translateZ控制前后距离,translateX/Y控制左右/上下距离。// 构建面内容@Builderprivate buildFaceContent() {Column({ space: 10 }) {// 背景色if (this.data.backgroundColor) {Column().width(‘100%’).height(‘100%’).backgroundColor(this.data.backgroundColor)} // 图片 if (this.data.image) { Image(this.data.image) .width('80%') .height('60%') .objectFit(ImageFit.Contain) .borderRadius(8) } // 标题 Text(this.data.title) .fontSize(20) .fontColor(Color.White) .fontWeight(FontWeight.Bold) .textAlign(TextAlign.Center) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) // 副标题 if (this.data.subtitle) { Text(this.data.subtitle) .fontSize(14) .fontColor(Color.White) .opacity(0.8) .textAlign(TextAlign.Center) .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis }) } } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) .padding(20) .backgroundColor(this.data.color)}build() {Column().width(this.config.cubeSize).height(this.config.cubeSize).backgroundColor(this.data.color).borderRadius(12).border({ width: 2, color: Color.White, style: BorderStyle.Solid }).opacity(this.opacity).scale({ x: this.scale, y: this.scale, z: this.scale }){this.buildFaceContent()}}}buildFaceContent方法构建每个面的具体内容,包括背景、图片、标题和副标题。build方法创建面的容器,设置尺寸、背景色、边框和圆角。opacity和scale控制面的显示效果,可以在动画中改变这些值实现渐变和缩放效果。3.3 自定义过渡动画// CubeTransition.ets// 自定义Swiper过渡动画@Componentexport struct CubeTransition {@Prop index: number = 0;@Prop currentIndex: number = 0;@Prop config: CubeSwiperConfig;// 自定义transition属性@Builder@CustomContentTransition(“cubeTransition”)buildCustomTransition(progress: number) {const isActive = this.index === this.currentIndex;const direction = this.index > this.currentIndex ? 1 : -1;// 计算旋转角度 const rotateY = direction * 90 * progress; const rotateX = 0; // 计算缩放 const scale = 1 - Math.abs(progress) * 0.2; // 计算透明度 const opacity = 1 - Math.abs(progress) * 0.5; // 计算Z轴位置 const translateZ = this.config.cubeSize * progress; Column() .rotate({ x: rotateX, y: rotateY, z: 0 }) .scale({ x: scale, y: scale, z: scale }) .opacity(opacity) .translate({ z: translateZ })}}CubeTransition组件实现自定义过渡动画,通过@CustomContentTransition装饰器定义动画效果。progress参数是动画进度(0-1),根据progress计算当前项的旋转角度、缩放比例、透明度和Z轴位置。rotateY实现水平旋转,scale实现缩放效果,opacity实现淡入淡出,translateZ实现深度感。3.4 使用Swiper实现轮播// SwiperCube.ets@Componentexport struct SwiperCube {@Prop data: CubeItemData[] = [];@Prop config: CubeSwiperConfig = CubeDefaultConfig.DEFAULT_CONFIG;@State currentIndex: number = 0;private swiperController: SwiperController = new SwiperController();// 自定义过渡效果@Builderprivate buildCubeItem(item: CubeItemData, index: number) {Column().width(‘100%’).height(‘100%’).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center).customContentTransition(“cubeTransition”, (progress: number) => {return this.buildCubeTransition(index, progress);}){CubeFace({data: item,position: ‘front’,config: this.config}).width(this.config.cubeSize).height(this.config.cubeSize)}}SwiperCube组件使用HarmonyOS的Swiper组件实现轮播效果。buildCubeItem方法为每个轮播项应用自定义过渡动画,customContentTransition属性指定动画效果名称和动画构建函数。progress参数由Swiper组件自动传入,表示当前项的动画进度。// 自定义过渡动画构建器@Builderprivate buildCubeTransition(index: number, progress: number) {const direction = index > this.currentIndex ? 1 : -1;const isActive = index === this.currentIndex;// 计算3D变换 const rotateY = direction * 90 * progress; const scale = 0.8 + Math.abs(1 - Math.abs(progress)) * 0.2; const opacity = 1 - Math.abs(progress) * 0.5; const translateZ = -Math.abs(progress) * 200; Column() .rotate({ x: 0, y: rotateY, z: 0 }) .scale({ x: scale, y: scale, z: scale }) .opacity(opacity) .translate({ z: translateZ }) .perspective(this.config.perspective)}build() {Column() {Swiper() {ForEach(this.data, (item: CubeItemData, index: number) => {SwiperItem() {this.buildCubeItem(item, index)}})}.loop(this.config.loop).indicator(this.config.showIndicator).duration(this.config.animationDuration).onChange((index: number) => {this.currentIndex = index;}).customContentTransition((progress: number) => {return this.buildCustomContentTransition(progress);}).controller(this.swiperController) // 使用rotate属性增强3D效果 .rotate({ x: this.config.rotateAngle, y: 0, z: 0 }) }}}buildCubeTransition方法计算每个轮播项的3D变换参数。rotateY使项水平旋转,scale在动画过程中从0.8缩放到1再回到0.8,opacity实现淡入淡出,translateZ创造深度感。Swiper组件的loop属性控制循环播放,indicator控制指示器显示,duration设置动画时长,onChange监听页面变化,rotate属性为整个Swiper添加3D倾斜效果。四、使用示例4.1 基本使用// CubeSwiperDemo.ets@Entry@Componentexport struct CubeSwiperDemo {// 轮播数据private cubeData: CubeItemData[] = [{id: ‘1’,title: ‘HarmonyOS 4.0’,subtitle: ‘下一代分布式操作系统’,color: ‘#FF6B6B’,backgroundColor: ‘#FFE8E8’,image: $r(‘app.media.harmonyos’)},{id: ‘2’,title: ‘ArkUI框架’,subtitle: ‘声明式UI开发’,color: ‘#4ECDC4’,backgroundColor: ‘#E8F7F6’,image: $r(‘app.media.arkui’)},{id: ‘3’,title: ‘3D图形’,subtitle: ‘强大的3D渲染能力’,color: ‘#45B7D1’,backgroundColor: ‘#E8F2F7’,image: $r(‘app.media.graphic’)},{id: ‘4’,title: ‘动画系统’,subtitle: ‘流畅的动画效果’,color: ‘#96CEB4’,backgroundColor: ‘#EFF7F2’,image: $r(‘app.media.animation’)},{id: ‘5’,title: ‘手势交互’,subtitle: ‘丰富的交互体验’,color: ‘#FFEAA7’,backgroundColor: ‘#FFF8E1’,image: $r(‘app.media.gesture’)}];// 自定义配置private customConfig: CubeSwiperConfig = {…CubeDefaultConfig.DEFAULT_CONFIG,cubeSize: 280,perspective: 1200,rotateAngle: 15,animationDuration: 600,indicatorType: ‘progress’,autoPlayInterval: 4000};这里定义了示例数据,包含5个轮播项,每个项有标题、副标题、颜色和图片。customConfig是自定义配置,覆盖了默认配置的一些参数,如立方体大小设为280px,透视距离1200px,旋转角度15度,动画时长600ms,指示器类型为进度条,自动播放间隔4秒。build() {Column() {// 标题Text(‘3D立方体旋转轮播’).fontSize(24).fontWeight(FontWeight.Bold).fontColor(Color.Black).margin({ top: 30, bottom: 20 }) // 3D立方体轮播 CubeSwiper({ data: this.cubeData, config: this.customConfig }) .width('100%') .height(400) .backgroundColor('#F5F5F5') .borderRadius(20) .padding(20) // 配置面板 this.buildConfigPanel() } .width('100%') .height('100%') .padding(20) .backgroundColor(Color.White)}@Builderprivate buildConfigPanel() {Column({ space: 12 }) {Text(‘配置选项’).fontSize(18).fontWeight(FontWeight.Medium).margin({ top: 20, bottom: 10 }) // 这里可以添加配置控件 // 如:开关、滑块等 }}}build方法构建完整的演示界面,包含标题、3D立方体轮播组件和配置面板。CubeSwiper组件接收数据和配置参数,设置宽高、背景色、圆角和内边距。buildConfigPanel方法预留了配置控件的位置,可以添加交互控件来动态调整轮播参数。4.2 响应式立方体轮播// ResponsiveCubeSwiper.ets@Componentexport struct ResponsiveCubeSwiper {@State private screenWidth: number = 0;@State private screenHeight: number = 0;aboutToAppear() {this.updateScreenSize();// 监听屏幕尺寸变化 window.getWindowClass().on('windowSizeChange', () => { this.updateScreenSize(); });}private updateScreenSize(): void {const windowClass = window.getWindowClass();this.screenWidth = windowClass.getWindowWidth();this.screenHeight = windowClass.getWindowHeight();}// 响应式配置private get responsiveConfig(): CubeSwiperConfig {const isMobile = this.screenWidth < 600;const isTablet = this.screenWidth >= 600 && this.screenWidth < 1024;const baseConfig = CubeDefaultConfig.DEFAULT_CONFIG; if (isMobile) { return { ...baseConfig, cubeSize: 200, perspective: 800, animationDuration: 400 }; } else if (isTablet) { return { ...baseConfig, cubeSize: 250, perspective: 1000, animationDuration: 500 }; } else { return { ...baseConfig, cubeSize: 300, perspective: 1200, animationDuration: 600 }; }}ResponsiveCubeSwiper组件实现响应式设计。在aboutToAppear生命周期中获取初始屏幕尺寸并监听窗口变化。updateScreenSize方法更新屏幕宽高状态。responsiveConfig计算属性根据屏幕宽度返回不同的配置:手机(<600px)使用较小立方体和较快动画,平板(600-1024px)使用中等尺寸,桌面(≥1024px)使用较大立方体和较慢动画。build() {Column() {CubeSwiper({data: this.cubeData,config: this.responsiveConfig}).width(‘100%’).height(this.screenHeight * 0.6)}}}build方法创建响应式轮播组件,高度设为屏幕高度的60%,宽度100%自适应。config属性使用响应式配置,确保在不同设备上都有良好的显示效果。五、高级特性5.1 手势交互增强// CubeGesture.ets@Componentexport struct CubeGesture {@State private rotateX: number = 0;@State private rotateY: number = 0;@State private scale: number = 1;// 手势处理器private handlePan(event: GestureEvent) {const { offsetX, offsetY } = event;// 根据手势更新旋转角度 this.rotateY += offsetX * 0.5; this.rotateX -= offsetY * 0.5; // 限制旋转范围 this.rotateX = Math.max(-60, Math.min(60, this.rotateX)); this.rotateY = Math.max(-60, Math.min(60, this.rotateY));}private handlePinch(event: GestureEvent) {const { scale } = event;this.scale = Math.max(0.5, Math.min(2, this.scale * scale));}CubeGesture组件增强手势交互。handlePan处理拖拽手势,offsetX和offsetY是手势位移量,转换为旋转角度。限制旋转范围在±60度内防止过度旋转。handlePinch处理捏合手势,根据缩放比例调整立方体大小,限制在0.5-2倍范围内。build() {Column().rotate({ x: this.rotateX, y: this.rotateY, z: 0 }).scale({ x: this.scale, y: this.scale, z: this.scale }).perspective(1000).gesture(PanGesture({ fingers: 1 }).onActionUpdate(this.handlePan.bind(this)).onActionEnd(() => {// 手势结束,恢复原状animateTo({ duration: 300 }, () => {this.rotateX = 0;this.rotateY = 0;});})).gesture(PinchGesture().onActionUpdate(this.handlePinch.bind(this)))}}build方法应用3D变换和手势识别。rotate应用旋转,scale应用缩放,perspective设置透视。PanGesture识别单指拖拽,onActionUpdate实时更新旋转,onActionEnd在手势结束后通过animateTo平滑恢复原始角度。PinchGesture识别双指捏合,实时更新缩放比例。5.2 性能优化// CubePerformance.ets@Componentexport struct CubePerformance {// 使用@ObjectLink避免不必要的重绘@ObjectLink config: CubeSwiperConfig;// 使用memoization优化@State private cachedTransforms: Map<number, Transform3D> = new Map();// 获取缓存的变换private getCachedTransform(index: number, progress: number): Transform3D {const key = index * 1000 + Math.floor(progress * 100);if (!this.cachedTransforms.has(key)) { this.cachedTransforms.set(key, this.calculateTransform(index, progress)); } return this.cachedTransforms.get(key)!;}CubePerformance组件实现性能优化。@ObjectLink装饰器避免不必要的重绘。cachedTransforms缓存已计算的变换矩阵,避免重复计算。getCachedTransform方法通过组合index和progress生成唯一键,检查缓存中是否存在对应的变换,不存在则计算并缓存。// 计算变换private calculateTransform(index: number, progress: number): Transform3D {const direction = index > this.currentIndex ? 1 : -1;const rotateY = direction * 90 * progress;const scale = 0.8 + Math.abs(1 - Math.abs(progress)) * 0.2;return { rotate: { x: 0, y: rotateY, z: 0 }, scale: { x: scale, y: scale, z: scale }, translate: { z: -Math.abs(progress) * 200 } };}// 清理缓存private clearCache() {this.cachedTransforms.clear();}}calculateTransform方法计算3D变换参数,返回包含旋转、缩放、平移的对象。clearCache方法清理变换缓存,在数据变化或组件销毁时调用,防止内存泄漏。六、最佳实践6.1 性能优化建议减少重绘:使用@ObjectLink和@Prop优化数据传递动画优化:使用硬件加速的动画内存管理:及时清理缓存和动画资源按需渲染:只渲染可见区域的内容性能优化建议包括:1)通过@ObjectLink避免不必要的数据传递和组件重绘;2)确保动画使用GPU加速以获得流畅的60fps帧率;3)及时清理动画控制器、定时器和缓存数据;4)实现虚拟滚动,只渲染视口内的内容。6.2 用户体验优化平滑动画:保持60fps的动画帧率手势反馈:提供自然的手势交互加载优化:使用骨架屏预加载错误处理:网络异常时的降级处理用户体验优化包括:1)通过requestAnimationFrame和硬件加速确保动画流畅;2)实现拖拽、滑动、捏合等自然手势交互;3)使用骨架屏在数据加载期间提供视觉连续性;4)处理网络错误、加载超时等异常情况。6.3 可访问性// 为屏幕阅读器提供支持.accessibilityDescription(‘3D立方体轮播,当前第currentIndex+1页,共{currentIndex + 1}页,共currentIndex+1页,共{data.length}页’).accessibilityLabel(‘立方体轮播’).accessibilityRole(AccessibilityRole.Adjustable).accessibilityValue({min: 1,max: this.data.length,now: this.currentIndex + 1})可访问性支持通过accessibility属性实现,为视障用户提供语音反馈。accessibilityDescription描述组件功能,accessibilityLabel标识组件,accessibilityRole设为Adjustable表示可调节值,accessibilityValue提供当前值、最小值和最大值。七、总结7.1 核心特性本实现方案充分利用HarmonyOS的最新API特性,特别是Swiper组件的customContentTransition和rotate属性,创建了流畅的3D立方体旋转轮播效果。通过合理的架构设计和性能优化,确保了良好的视觉效果和性能表现。
-
鸿蒙数据加密应用指导1.1 问题说明:清晰呈现问题场景与具体表现问题场景在鸿蒙应用开发中,开发者经常需要对敏感数据进行加密存储和传输,但存在以下具体问题:具体表现:混淆使用加密算法:开发者不确定在不同场景下应使用AES、RSA还是HMAC等加密算法密钥管理不规范:将加密密钥硬编码在代码中,或使用不安全的密钥生成方式API调用复杂:鸿蒙加密API分散在多个模块中,调用方式不统一性能与安全失衡:过度加密影响应用性能,或加密强度不足导致安全风险缺少完整示例:缺乏端到端的加密解密完整实现参考1.2 解决方案:落地解决思路,给出可执行、可复用的具体方案方案一:通用数据加密工具类(完整实现)// CryptoManager.ets - 鸿蒙统一加密管理工具import cryptoFramework from '@ohos.security.cryptoFramework';import huks from '@ohos.security.huks';import util from '@ohos.util';/** * 鸿蒙数据加密管理类 * 支持:AES-GCM, RSA-OAEP, HMAC-SHA256 */export class CryptoManager { private static instance: CryptoManager = null; private keyAlias: string = 'app_secure_key'; private keySize: number = 256; // AES-256 // 单例模式 public static getInstance(): CryptoManager { if (!this.instance) { this.instance = new CryptoManager(); } return this.instance; } /** * 1. AES-GCM 对称加密(推荐用于本地数据) */ public async encryptWithAES(plainText: string): Promise<EncryptResult> { try { // 生成或获取密钥 const key = await this.generateOrGetAESKey(); // 创建加密器 const cipher = cryptoFramework.createCipher( cryptoFramework.SymKeyGenerator.AES_GCM ); // 初始化加密器 await cipher.init(cryptoFramework.CryptoMode.ENCRYPT_MODE, key, null); // 生成GCM模式的IV const iv = this.generateRandomIV(12); // 12字节IV cipher.setCipherSpec(cryptoFramework.CipherSpecIV.IV, iv); // 执行加密 const dataBlob: cryptoFramework.DataBlob = { data: new Uint8Array(util.encodeUtf8(plainText)) }; const encryptedData = await cipher.doFinal(dataBlob); // 返回:密文 + IV + Tag return { cipherText: this.uint8ArrayToBase64(encryptedData.data), iv: this.uint8ArrayToBase64(iv), algorithm: 'AES-GCM-256' }; } catch (error) { console.error(`AES加密失败: ${error.code}, ${error.message}`); throw new Error(`加密失败: ${error.message}`); } } /** * 2. RSA 非对称加密(用于传输密钥或小数据) */ public async encryptWithRSA(plainText: string, publicKey: string): Promise<string> { try { const rsaGenerator = cryptoFramework.createAsyKeyGenerator( cryptoFramework.AsyKeyGenerator.RSA_CRYPTO_ALGORITHM ); // 转换公钥 const keyBlob: cryptoFramework.DataBlob = { data: new Uint8Array(util.encodeUtf8(publicKey)) }; const publicKeyObj = await rsaGenerator.convertKey(keyBlob, null, true); // 创建加密器 const cipher = cryptoFramework.createCipher( cryptoFramework.CryptoMode.ENCRYPT_MODE, publicKeyObj, { alg: cryptoFramework.AsyAlgorithm.RSA_OAEP, md: cryptoFramework.MessageDigest.SHA256, mgf1Md: cryptoFramework.MessageDigest.SHA256 } ); const dataBlob: cryptoFramework.DataBlob = { data: new Uint8Array(util.encodeUtf8(plainText)) }; const encryptedData = await cipher.doFinal(dataBlob); return this.uint8ArrayToBase64(encryptedData.data); } catch (error) { console.error(`RSA加密失败: ${error.message}`); throw error; } } /** * 3. HMAC 数据完整性验证 */ public async generateHMAC(data: string): Promise<string> { const mac = cryptoFramework.createMac(cryptoFramework.MAC_HMAC_SHA256); const key = await this.generateHMACKey(); await mac.init(key); const dataBlob: cryptoFramework.DataBlob = { data: new Uint8Array(util.encodeUtf8(data)) }; const hmacResult = await mac.doFinal(dataBlob); return this.uint8ArrayToHex(hmacResult.data); } // 密钥管理方法 private async generateOrGetAESKey(): Promise<cryptoFramework.SymKey> { const keyGen = cryptoFramework.createSymKeyGenerator( cryptoFramework.SymKeyGenerator.AES_GCM ); // 尝试从HUKS获取现有密钥 try { const key = await this.getKeyFromHUKS(); return key; } catch { // 生成新密钥 const key = await keyGen.generateSymKey(this.keySize); // 存储到HUKS await this.saveKeyToHUKS(key); return key; } } // HUKS密钥存储 private async saveKeyToHUKS(key: cryptoFramework.SymKey): Promise<void> { const properties: huks.HuksOptions = { properties: [ { tag: huks.HuksTag.HUKS_TAG_ALGORITHM, value: huks.HuksKeyAlg.HUKS_ALG_AES }, { tag: huks.HuksTag.HUKS_TAG_KEY_SIZE, value: this.keySize }, { tag: huks.HuksTag.HUKS_TAG_PURPOSE, value: huks.HuksKeyPurpose.HUKS_KEY_PURPOSE_ENCRYPT | huks.HuksKeyPurpose.HUKS_KEY_PURPOSE_DECRYPT }, { tag: huks.HuksTag.HUKS_TAG_BLOCK_MODE, value: huks.HuksCipherMode.HUKS_MODE_GCM } ] }; await huks.generateKeyItem(this.keyAlias, properties); } // 工具方法 private generateRandomIV(length: number): Uint8Array { const iv = new Uint8Array(length); for (let i = 0; i < length; i++) { iv[i] = Math.floor(Math.random() * 256); } return iv; } private uint8ArrayToBase64(uint8Array: Uint8Array): string { return util.base64EncodeToString(uint8Array); } private uint8ArrayToHex(uint8Array: Uint8Array): string { return Array.from(uint8Array) .map(b => b.toString(16).padStart(2, '0')) .join(''); }}// 类型定义interface EncryptResult { cipherText: string; iv?: string; algorithm: string; tag?: string;}方案二:场景化使用示例// UserDataService.ets - 用户数据服务示例import { CryptoManager } from './CryptoManager';export class UserDataService { private cryptoManager: CryptoManager; constructor() { this.cryptoManager = CryptoManager.getInstance(); } /** * 场景1:加密存储用户敏感信息 */ public async saveUserCredentials(username: string, password: string): Promise<void> { // 组合敏感数据 const sensitiveData = JSON.stringify({ username, password, timestamp: Date.now() }); // 使用AES-GCM加密 const encrypted = await this.cryptoManager.encryptWithAES(sensitiveData); // 生成HMAC验证数据完整性 const hmac = await this.cryptoManager.generateHMAC(encrypted.cipherText); // 存储到Preferences const preferences = await dataPreferences.getPreferences(this.context, 'user_credentials'); await preferences.put('encrypted_data', encrypted.cipherText); await preferences.put('encryption_iv', encrypted.iv); await preferences.put('data_hmac', hmac); await preferences.flush(); } /** * 场景2:加密网络传输数据 */ public async prepareSecureRequest(payload: object): Promise<SecureRequest> { const payloadStr = JSON.stringify(payload); // 生成随机会话密钥 const sessionKey = this.generateSessionKey(); // 使用会话密钥加密数据 const encryptedData = await this.cryptoManager.encryptWithAES(payloadStr); // 使用服务器公钥加密会话密钥 const serverPublicKey = await this.getServerPublicKey(); const encryptedSessionKey = await this.cryptoManager.encryptWithRSA( sessionKey, serverPublicKey ); return { encryptedData: encryptedData.cipherText, encryptedKey: encryptedSessionKey, iv: encryptedData.iv, timestamp: Date.now(), signature: await this.cryptoManager.generateHMAC(encryptedData.cipherText) }; }}// 配置文件加密示例export class ConfigEncryptor { /** * 轻量级配置加密 - 使用固定密钥派生 */ public static async encryptConfig(config: object): Promise<string> { const configStr = JSON.stringify(config); const keyMaterial = 'your_app_salt_' + DeviceInfo.deviceId; // 使用PBKDF2派生密钥 const pbkdf2 = cryptoFramework.createMac(cryptoFramework.MAC_PBKDF2); const params: cryptoFramework.PBKDF2Spec = { iterations: 10000, keyLength: 256, md: cryptoFramework.MessageDigest.SHA256, salt: new Uint8Array(util.encodeUtf8('static_salt')) }; const symKeyGenerator = cryptoFramework.createSymKeyGenerator( cryptoFramework.SymKeyGenerator.AES_CBC ); const key = await symKeyGenerator.generateSymKey(256); return await this.simpleEncrypt(configStr, key); }} 方案三:密钥安全管理方案// KeyManager.ets - 高级密钥管理import huks from '@ohos.security.huks';export class KeyManager { private static readonly KEY_ALIAS_PREFIX = 'com.example.app.key.'; /** * 分层密钥管理 */ public static async getKeyForLevel(securityLevel: SecurityLevel): Promise<string> { switch (securityLevel) { case SecurityLevel.LOW: // 应用级密钥,存储在HUKS return await this.getAppLevelKey(); case SecurityLevel.MEDIUM: // 用户级密钥,需要生物特征验证 return await this.getUserLevelKey(); case SecurityLevel.HIGH: // 硬件级密钥,使用安全元件 return await this.getHardwareLevelKey(); default: throw new Error('Unsupported security level'); } } /** * 使用生物特征解锁密钥 */ private static async getUserLevelKey(): Promise<string> { const authParams: huks.HuksOptions = { properties: [ { tag: huks.HuksTag.HUKS_TAG_USER_AUTH_TYPE, value: huks.HuksUserAuthType.HUKS_USER_AUTH_TYPE_FACE }, { tag: huks.HuksTag.HUKS_TAG_AUTH_TIMEOUT, value: 0 // 不超时 } ] }; // 检查是否已授权 const authResult = await huks.initSession('user_key_alias', authParams); if (authResult.errorCode !== 0) { throw new Error('生物特征验证失败'); } return await this.exportKey('user_key_alias'); } /** * 密钥轮换策略 */ public static async rotateKeyIfNeeded(keyAlias: string): Promise<void> { const keyInfo = await huks.getKeyItem(keyAlias, []); const createTime = keyInfo.properties.find( p => p.tag === huks.HuksTag.HUKS_TAG_CREATION_DATETIME )?.value; // 如果密钥创建时间超过90天,执行轮换 if (createTime && (Date.now() - createTime) > 90 * 24 * 60 * 60 * 1000) { await this.rotateKey(keyAlias); } }}操作步骤:1.初始化加密环境# 在项目中添加安全模块依赖# module.json5"requestPermissions": [ { "name": "ohos.permission.USE_BIOMETRIC" }]2.配置gradle依赖// build.gradledependencies { implementation 'io.openharmony.tpc.thirdlib:openssl:1.1.1'}3.使用示例// 在页面中使用import { CryptoManager } from '../utils/CryptoManager';@Entry@Componentstruct SecurePage { private cryptoManager = CryptoManager.getInstance(); async saveSecureData() { const encrypted = await this.cryptoManager.encryptWithAES('敏感数据'); // 存储encrypted.cipherText和encrypted.iv } async verifyDataIntegrity(data: string) { const hmac = await this.cryptoManager.generateHMAC(data); // 与存储的HMAC比较验证 }}1.3 结果展示:消除常见安全漏洞(硬编码密钥、弱IV等)合规性:符合OWASP Mobile Top 10安全标准加密工具库:CryptoManager.ets密钥管理方案:KeyManager.ets场景化示例:5种常见加密场景测试用例:包含边界条件测试
上滑加载中
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签