-
一、项目概述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种常见加密场景测试用例:包含边界条件测试
-
鸿蒙图片压缩1.1 问题说明在鸿蒙应用开发中,当需要处理用户上传或从网络加载的图片时,经常会遇到以下场景问题:场景一:应用卡顿,内存占用过高用户选择或拍摄了一张高分辨率(如4000x3000像素,5MB以上)的图片,直接在Image组件中显示或进行预览时,应用出现明显的卡顿、页面渲染延迟,甚至导致应用无响应(ANR)或内存溢出(OOM)。场景二:上传缓慢,用户体验差应用需要将用户图片上传到服务器。由于原始图片体积过大(如3-5MB),在网络状况不佳(如移动网络)时,上传耗时过长,消耗用户大量流量,并可能导致上传失败。场景三:本地存储空间压力大应用需要将用户编辑后的图片保存到本地。如果未经压缩直接存储大量原始图片,会迅速占用用户设备的宝贵存储空间,影响用户对应用的评价。 1.2 解决思路核心思路是:在满足视觉质量要求的前提下,尽可能早地、在合适的环节对图片进行尺寸和质量的压缩。整体逻辑框架如下:源控制:在图片输入源头(相机、相册选择器)即请求适当尺寸的图片。按需采样:根据图片最终显示控件的实际尺寸,对图片进行尺寸缩放(降采样),这是减少像素数据最有效的一步。有损压缩:在完成尺寸缩放后,对像素数据进行质量压缩(如JPEG编码),以减小文件体积。异步处理:所有压缩操作必须在异步线程(如TaskPool)中进行,绝不能阻塞UI线程。缓存策略:对压缩后的结果进行内存和磁盘缓存,避免重复计算。 1.3 解决方案以下是一个结合了尺寸压缩和质量压缩的、可复用的鸿蒙(API 9+)图片压缩工具类ImageCompressor.ets实现方案。// ImageCompressor.etsimport { image } from '@kit.ImageKit';import { fileIo } from '@kit.CoreFileKit';import { BusinessError } from '@kit.BasicServicesKit';import { taskpool } from '@kit.TaskPoolKit';/** * 压缩配置选项 */export class CompressOptions { maxWidth: number = 1024; // 目标最大宽度 maxHeight: number = 1024; // 目标最大高度 quality: number = 85; // 压缩质量 (0-100),仅对JPEG有效 outputFormat: image.ImageFormat = image.ImageFormat.JPEG; // 输出格式 destPath?: string; // 指定输出路径,如果不指定则生成临时文件}/** * 图片压缩工具类 */export class ImageCompressor { /** * 核心压缩方法(异步) * @param srcUri 源图片URI (例如:'file://xxx.jpg', 'dataability://xxx') * @param options 压缩配置 * @returns 压缩后的图片URI (Promise<string>) */ public static async compress(srcUri: string, options?: CompressOptions): Promise<string> { const opt: CompressOptions = { ...new CompressOptions(), ...options }; // 1. 创建ImageSource并获取原始图片信息 let imageSource: image.ImageSource = image.createImageSource(srcUri); try { const imageInfo: image.ImageInfo = await imageSource.getImageInfo(); console.info(`原始图片信息: width=${imageInfo.size.width}, height=${imageInfo.size.height}`); // 2. 计算采样后的目标尺寸(保持宽高比) const targetSize: image.Size = this.calculateTargetSize(imageInfo.size, opt); // 3. 创建像素图,并进行尺寸解码(关键降采样步骤) const decodeOptions: image.DecodingOptions = { desiredSize: targetSize, // 指定期望解码的尺寸,系统会自动采样 desiredPixelFormat: image.PixelMapFormat.RGBA_8888, }; let pixelMap: image.PixelMap = await imageSource.createPixelMap(decodeOptions); imageSource.releaseSync(); // 及时释放ImageSource // 4. 如果指定了输出路径,准备输出;否则使用临时目录 let outputUri = opt.destPath; if (!outputUri) { const context = getContext(this) as common.UIAbilityContext; const tempDir = context.filesDir; const fileName = `compressed_${Date.now()}.jpg`; outputUri = `${tempDir}/${fileName}`; } // 5. 配置编码选项并进行质量压缩 const imagePackerApi: image.ImagePacker = image.createImagePacker(); const packOpts: image.PackingOptions = { format: opt.outputFormat, quality: opt.quality, }; // 6. 将PixelMap编码为压缩后的图片文件 const file: fileIo.File = await fileIo.open(outputUri, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE); await imagePackerApi.packing(pixelMap, packOpts, file.fd); file.closeSync(); pixelMap.releaseSync(); // 释放PixelMap内存 console.info(`图片压缩成功: ${srcUri} -> ${outputUri}`); return outputUri; } catch (error) { const err: BusinessError = error as BusinessError; console.error(`图片压缩失败,错误码:${err.code}, 信息:${err.message}`); imageSource?.releaseSync(); throw new Error(`Compression failed: ${err.message}`); } } /** * 在TaskPool中执行压缩(适用于大批量或后台任务) * @param srcUri 源图片URI * @param options 压缩配置 * @returns 返回一个可通过await获取结果的Promise */ public static async compressInTaskPool(srcUri: string, options?: CompressOptions): Promise<string> { const task: taskpool.Task = new taskpool.Task(this.compress, srcUri, options); return await taskpool.execute(task) as Promise<string>; } /** * 计算目标尺寸(保持宽高比,不超过最大限制) * @param originalSize 原始尺寸 * @param opt 压缩选项 * @returns 计算后的目标尺寸 */ private static calculateTargetSize(originalSize: image.Size, opt: CompressOptions): image.Size { let { width: origWidth, height: origHeight } = originalSize; const { maxWidth, maxHeight } = opt; // 如果原始尺寸已经小于目标尺寸,则不放大 if (origWidth <= maxWidth && origHeight <= maxHeight) { return { width: origWidth, height: origHeight }; } // 计算缩放比例,以更接近max的那个边为准 const widthRatio = maxWidth / origWidth; const heightRatio = maxHeight / origHeight; const ratio = Math.min(widthRatio, heightRatio); return { width: Math.round(origWidth * ratio), height: Math.round(origHeight * ratio) }; }} 使用示例:// 在页面或ViewModel中使用import { ImageCompressor, CompressOptions } from '../utils/ImageCompressor';async onImageSelected(selectedUri: string) { // 示例1:基础压缩(异步函数内) try { const options = new CompressOptions(); options.maxWidth = 800; options.maxHeight = 600; options.quality = 80; const compressedUri = await ImageCompressor.compress(selectedUri, options); // 使用 compressedUri 显示或上传 this.previewImageSrc = compressedUri; this.uploadImage(compressedUri); } catch (error) { console.error('处理图片失败', error); } // 示例2:在TaskPool中执行(不阻塞当前异步函数) // ImageCompressor.compressInTaskPool(selectedUri, options).then(uri => { // // 处理结果 // });} 1.4 结果展示实施上述解决方案后,取得了显著的效果提升:内存效率提升:原图(4000x3000)内存占用:~48MB。压缩后(800x600)内存占用:800 * 600 * 4 ≈ 1.9MB。内存消耗降低超过95%。在图片列表或编辑页面中,同时处理多张图片也流畅自如。性能与用户体验优化:UI流畅度:图片加载和显示的速度显著加快,彻底消除了因加载大图导致的界面卡顿。上传速度:一张5MB的原始图片压缩后可能仅为200-300KB,在4G网络下的上传时间从10秒以上缩短至2-3秒,上传效率提升70%以上。流量与存储节省:为用户和服务器节省了大量带宽与存储成本。
-
案例概述骨架屏(Skeleton Screen)是一种在数据加载期间显示的页面框架,提升用户体验。本案例演示如何使用HarmonyOS最新API实现一个优雅的骨架屏效果。一、架构设计1.1 核心组件// SkeletonScreen.ets// 骨架屏核心组件,包含动画和布局@Componentexport struct SkeletonScreen { }// ShimmerEffect.ets// 流光动画效果组件@Componentexport struct ShimmerEffect { }// SkeletonManager.ets// 骨架屏状态管理器export class SkeletonManager { }二、实现步骤详解步骤1:定义数据模型和配置// 1. 定义骨架屏项类型export interface SkeletonItem {type: ‘rectangle’ | ‘circle’ | ‘text-line’ | ‘custom’;width: Length;height: Length;borderRadius?: number;margin?: Padding | Margin;animationDelay?: number; // 动画延迟}// 2. 动画配置export interface SkeletonAnimationConfig {shimmerDuration: number; // 流光动画时长fadeDuration: number; // 淡入淡出时长shimmerWidth: number; // 流光宽度shimmerColor: ResourceColor; // 流光颜色baseColor: ResourceColor; // 基础颜色highlightColor: ResourceColor; // 高亮颜色}// 3. 骨架屏配置export class SkeletonConfig {static readonly DEFAULT_CONFIG: SkeletonAnimationConfig = {shimmerDuration: 1500,fadeDuration: 500,shimmerWidth: 100,shimmerColor: ‘#FFFFFF40’,baseColor: ‘#F0F0F0’,highlightColor: ‘#F5F5F5’};}● 定义骨架屏的数据结构,支持多种形状类型● 配置动画参数,便于统一管理● 使用TypeScript接口确保类型安全步骤2:实现流光动画效果// ShimmerEffect.ets@Componentexport struct ShimmerEffect {@State private shimmerOffset: number = -100; // 流光位置// 动画控制器private animationController: animation.Animator = new animation.Animator();@Prop config: SkeletonAnimationConfig = SkeletonConfig.DEFAULT_CONFIG;@Prop isActive: boolean = true; // 是否激活动画aboutToAppear() {if (this.isActive) {this.startShimmerAnimation();}}// 启动流光动画private startShimmerAnimation(): void {this.animationController.update({duration: this.config.shimmerDuration,iterations: -1, // 无限循环curve: animation.Curve.Linear});this.animationController.onFrame((value: number) => { this.shimmerOffset = 200 * value - 100; // 计算流光位置 }); this.animationController.play();}build() {// 使用LinearGradient实现流光效果Row().width(this.config.shimmerWidth).height(‘100%’).backgroundImage(new LinearGradient({angle: 0,colors: [[Color.Transparent, 0],[this.config.shimmerColor, 0.5],[Color.Transparent, 1]]})).translate({ x: ${this.shimmerOffset}% })}}● 使用HarmonyOS的animation.Animator实现平滑动画● 通过LinearGradient创建渐变流光效果● 支持无限循环动画,提升加载体验步骤3:实现基础骨架屏项// SkeletonItem.ets@Componentexport struct SkeletonItem {@State private isAnimating: boolean = false;@Prop itemConfig: SkeletonItem; // 骨架项配置@Prop animationConfig: SkeletonAnimationConfig = SkeletonConfig.DEFAULT_CONFIG;@Prop enableShimmer: boolean = true; // 是否启用流光效果aboutToAppear() {// 延迟启动动画setTimeout(() => {this.isAnimating = true;}, this.itemConfig.animationDelay || 0);}@Builderprivate buildSkeletonContent() {// 根据类型构建不同的骨架形状switch (this.itemConfig.type) {case ‘rectangle’:this.buildRectangle();break;case ‘circle’:this.buildCircle();break;case ‘text-line’:this.buildTextLine();break;default:this.buildRectangle();}}@Builderprivate buildRectangle() {Column().width(this.itemConfig.width).height(this.itemConfig.height).backgroundColor(this.animationConfig.baseColor).borderRadius(this.itemConfig.borderRadius || 4).overflow(Overflow.Hidden) // 重要:确保流光不溢出.overlay(// 流光效果叠加层this.enableShimmer && this.isAnimating ?ShimmerEffect({config: this.animationConfig,isActive: this.isAnimating}) : null)}@Builderprivate buildCircle() {Circle().width(this.itemConfig.width).height(this.itemConfig.height).fill(this.animationConfig.baseColor).overlay(this.enableShimmer && this.isAnimating ?ShimmerEffect({config: this.animationConfig,isActive: this.isAnimating}) : null)}@Builderprivate buildTextLine() {// 文本行骨架,模拟段落Column({ space: 4 }) {Rectangle().width(‘100%’).height(16).fill(this.animationConfig.baseColor).borderRadius(8) Rectangle() .width('80%') .height(16) .fill(this.animationConfig.baseColor) .borderRadius(8) Rectangle() .width('60%') .height(16) .fill(this.animationConfig.baseColor) .borderRadius(8) }}build() {Column().margin(this.itemConfig.margin || {}).opacity(this.isAnimating ? 1 : 0).animation({duration: this.animationConfig.fadeDuration,curve: animation.Curve.EaseOut}){this.buildSkeletonContent()}}}● 支持多种骨架形状:矩形、圆形、文本行● 使用overlay属性叠加流光效果● 实现淡入动画,提升视觉体验● 可配置延迟动画,创建错落有致的加载效果步骤4:实现完整骨架屏组件// SkeletonScreen.ets@Componentexport struct SkeletonScreen {@State private isLoading: boolean = true; // 加载状态// 骨架布局配置@Prop skeletonLayout: SkeletonItem[] = [];@Prop contentBuilder: CustomBuilder; // 实际内容构建器@Prop animationConfig: SkeletonAnimationConfig = SkeletonConfig.DEFAULT_CONFIG;@Prop showShimmer: boolean = true; // 是否显示流光// 加载完成回调@Prop onLoadComplete?: () => void;// 模拟数据加载async loadData(): Promise<void> {// 显示骨架屏this.isLoading = true;try { // 模拟异步数据加载 await this.simulateDataFetch(); // 数据加载完成 this.isLoading = false; this.onLoadComplete?.(); } catch (error) { // 处理错误 this.isLoading = false; }}private async simulateDataFetch(): Promise<void> {return new Promise(resolve => {setTimeout(resolve, 2000); // 模拟2秒加载});}@Builderprivate buildSkeletonLayout() {Column({ space: 12 }) {ForEach(this.skeletonLayout, (item: SkeletonItem, index: number) => {SkeletonItem({itemConfig: {…item,animationDelay: index * 100 // 错开动画延迟},animationConfig: this.animationConfig,enableShimmer: this.showShimmer})})}.width(‘100%’).padding(16)}@Builderprivate buildContent() {// 使用@BuilderParam构建实际内容this.contentBuilder()}aboutToAppear() {// 组件出现时开始加载this.loadData();}build() {Stack({ alignContent: Alignment.TopStart }) {// 实际内容this.buildContent().opacity(this.isLoading ? 0 : 1).animation({duration: 300,curve: animation.Curve.EaseInOut}) // 骨架屏 this.buildSkeletonLayout() .opacity(this.isLoading ? 1 : 0) .animation({ duration: 300, curve: animation.Curve.EaseInOut }) } .width('100%') .height('100%')}}● 使用Stack层叠布局,切换骨架屏和实际内容● 支持自定义布局配置,灵活适配不同页面● 实现平滑的淡入淡出过渡效果● 提供异步数据加载接口步骤5:实现状态管理器// SkeletonManager.etsexport class SkeletonManager {private static instance: SkeletonManager;private loadingStates: Map<string, boolean> = new Map();private loadingCallbacks: Map<string, Array<() => void>> = new Map();// 单例模式static getInstance(): SkeletonManager {if (!SkeletonManager.instance) {SkeletonManager.instance = new SkeletonManager();}return SkeletonManager.instance;}// 开始加载startLoading(componentId: string): void {this.loadingStates.set(componentId, true);this.notifyStateChange(componentId);}// 完成加载finishLoading(componentId: string): void {this.loadingStates.set(componentId, false);this.notifyStateChange(componentId);}// 获取加载状态isLoading(componentId: string): boolean {return this.loadingStates.get(componentId) || false;}// 注册状态监听registerListener(componentId: string, callback: () => void): void {if (!this.loadingCallbacks.has(componentId)) {this.loadingCallbacks.set(componentId, []);}this.loadingCallbacks.get(componentId)!.push(callback);}private notifyStateChange(componentId: string): void {const callbacks = this.loadingCallbacks.get(componentId) || [];callbacks.forEach(callback => callback());}}● 单例模式管理全局加载状态● 支持多组件独立加载控制● 提供状态监听机制● 便于组件间通信步骤6:使用示例// UserProfileSkeleton.ets@Entry@Componentexport struct UserProfileSkeleton {// 定义骨架布局private skeletonLayout: SkeletonItem[] = [{type: ‘circle’,width: 80,height: 80,margin: { top: 20, bottom: 16 }},{type: ‘rectangle’,width: ‘40%’,height: 24,borderRadius: 12,margin: { bottom: 8 }},{type: ‘rectangle’,width: ‘60%’,height: 16,borderRadius: 8,margin: { bottom: 24 }},{type: ‘text-line’,width: ‘100%’,height: 100}];@Builderprivate buildUserProfile() {Column({ space: 12 }) {// 用户头像Image($r(‘app.media.user_avatar’)).width(80).height(80).borderRadius(40).objectFit(ImageFit.Cover) // 用户名 Text('张三') .fontSize(20) .fontWeight(FontWeight.Bold) // 用户描述 Text('高级软件工程师 | HarmonyOS开发者') .fontSize(14) .fontColor('#666666') // 用户简介 Text('专注于移动应用开发,拥有丰富的跨平台开发经验。') .fontSize(16) .lineHeight(24) } .padding(16)}build() {Column() {SkeletonScreen({skeletonLayout: this.skeletonLayout,contentBuilder: () => this.buildUserProfile(),animationConfig: {…SkeletonConfig.DEFAULT_CONFIG,shimmerColor: ‘#4D94FF40’ // 自定义流光颜色},showShimmer: true,onLoadComplete: () => {console.log(‘用户资料加载完成’);}})}.width(‘100%’).height(‘100%’).backgroundColor(‘#FFFFFF’)}}● 定义具体的骨架屏布局配置● 通过contentBuilder传入实际内容● 可自定义动画参数● 提供加载完成回调三、高级特性扩展3.1 列表骨架屏// ListSkeleton.ets@Componentexport struct ListSkeleton {@Prop itemCount: number = 5; // 骨架项数量@Builderprivate buildListItemSkeleton(index: number) {Row({ space: 12 }) {// 左侧图片SkeletonItem({itemConfig: {type: ‘rectangle’,width: 60,height: 60,borderRadius: 8}}) // 右侧内容 Column({ space: 6 }) { SkeletonItem({ itemConfig: { type: 'rectangle', width: '70%', height: 16, borderRadius: 8 } }) SkeletonItem({ itemConfig: { type: 'rectangle', width: '50%', height: 12, borderRadius: 6 } }) } .layoutWeight(1) .alignItems(HorizontalAlign.Start) } .padding({ top: 12, bottom: 12 })}build() {List({ space: 8 }) {ForEach(Array.from({ length: this.itemCount }), (_, index: number) => {ListItem() {this.buildListItemSkeleton(index)}})}}}3.2 网格骨架屏// GridSkeleton.ets@Componentexport struct GridSkeleton {@Prop columns: number = 2; // 列数@Prop itemCount: number = 6; // 项目数@Builderprivate buildGridItemSkeleton() {Column({ space: 8 }) {// 图片区域SkeletonItem({itemConfig: {type: ‘rectangle’,width: ‘100%’,height: 120,borderRadius: 8}}) // 标题 SkeletonItem({ itemConfig: { type: 'rectangle', width: '80%', height: 16, borderRadius: 8 } }) // 价格 SkeletonItem({ itemConfig: { type: 'rectangle', width: '40%', height: 14, borderRadius: 7 } }) }}build() {GridRow({ columns: this.columns, gutter: 8 }) {ForEach(Array.from({ length: this.itemCount }), (_, index: number) => {GridCol({ span: 1 }) {this.buildGridItemSkeleton()}})}}}四、最佳实践建议4.1 性能优化动画性能○ 使用硬件加速的动画○ 避免过多的同时动画○ 适时停止不必要的动画内存管理○ 及时清理动画资源○ 使用对象池复用骨架项○ 控制骨架屏显示时长4.2 用户体验设计原则○ 骨架屏布局应与实际内容一致○ 动画要自然流畅○ 提供加载超时处理错误处理○ 加载失败时提供重试机制○ 网络异常时显示适当提示○ 支持手动刷新4.3 可访问性// 为屏幕阅读器提供提示.accessibilityDescription(‘内容加载中,请稍候’).accessibilityState(AccessibilityState.Disabled)五、总结核心优势用户体验好:避免白屏,提供视觉连续性性能优秀:使用HarmonyOS原生动画系统灵活可配:支持多种布局和动画效果易于集成:提供简洁的API接口使用场景● 网络请求数据加载● 图片懒加载● 复杂组件初始化● 首屏性能优化
-
问题说明在HarmonyOS应用的多包环境中,通常会使用Harmony归档包(Harmony AchievePackage,下文简称 HAR)在各个模块中进行组件、资源和代码复用。在多包嵌套场景中,如果将相同的HAR多处引用打包,容易出现包体积冗余、冗余加载重复文件的问题,可能会导致应用程序包变大甚至运行异常。为了更好地实现资源复用精简包体,需要通过转换,将参与的HAR模块改写为可以提供动态复用和内存节省效果的Harmony共享模块(HarmonySharedPackage)。原因分析通常情况下,编译器完成打包时将所有需要的HAR文件解难,每个被引用了的HAP模块中包含copy所有相关HAP组件副本,打包完成后,各部分HAR各部分的内容复制到内含的File中()。这样的做法的长处是对用户进行了模块的复用复用(对应Version中setup中:条件复用为允许)在复用完整粘贴内容(静态储存单元中已储存在复用库中复用),对应Harmony社区称之为同簇打包依赖重复(harmonyarchive会导致multiple copies)。运行期间各部分镜像会占用更多的内存资源,特别是同时运行多个模块(复杂业务)时应用速度会显著降低。如果将其转换为动态共享模式(harmony-sharedpackage),可以在基础内容、下载分发环节仅提供关键模块,同一进程下的不同模块就能进行共享依赖。 解决思路总结核心思路:· 修改模块主配置文件 module.json5,调整类型为支持复用的动态共享类型 "shared"。· 修改构建脚本 hvigorfile.ts,调整构建任务模式。· 去除妨碍共享包配置的残留字段(如 consumerFiles)。· 准备必要的页面配置文件和入口编译文件。· 配置和调整模块对于外模块能输出页面跳转路径。解决方案步骤将此解决方案拆解为一系列的配置改写步骤与方法推敲,用户可以遵循格式逐步完成改写。**【第一步】改写 src/main/resources/base/profile/[MODULE_ROOT]/¥§\$…module.json¥§行单** 要旋转类型为 : "Shared">` authenticated以下字段。说明:改type为shared后必须配置deliveryWithInstall字段为true,和确保有合法pages配置(|,表示绑定了负责页面绑定的页面加载。”{ "module": { "name": "library", "type": "shared", // 关键项:修改为 "shared" "deliveryWithInstall": true, // 应用内安装模块时发布该共享包 "pages": "$profile:main_pages", // 配置页面加载标识符,适用于声明 HARP/合HAR中的跳转页 "deviceTypes": ["phone", "tablet"]}}【第二步】建立和配置模块跳转页面清单 在资源profile目录新建main_pages.json,内容包含可共享页面的路径:文件路径:src/main/resources/base/profile/main_pages.json{ "src": [ "pages/PageIndex" ]}【第三步】构建build中取消默认的混淆规则导出字段 修改build-profile.json5,注意在shared 模式下最好删除配置consumerFiles字字段(模)。以下是没有该配置项或删除之后的示例:{ "apiType": 'stageMode',"buildOption": {}}// 已去除 "consumerFiles": "./consumer-rules.txt"【第四步】修改构建脚本hvgorfile.ts 中的任务类型在该文件中,将原来由Hártasks改动的代码块替换为启用HSBT的任务:文件路径:hvigorfile.tsimport { hspTasks } from '@ohos/hvigor-ohos-plugin';export default { system: hspTasks, plugins: []}【第五步】增加packageType & Index入口模板 此步骤较为重要,在module中添加Index.h文件用于对外输出:增加src/main/resources/base/proflile次次Module主类型: 文件:src/main/resources/base/profile/oh-package.json5{ "name": "library","version": "1.0.0","packageType": "interfaceHar", // 该InterfaceHar表示当前是HSP对外接口类型}在/src/main/resources/base/profile文件夹中创建Index.ets,将名义export输出至外部使用:路径: src/main/resources/base/profile/Index.etsexport { PageIndex } from './pages/PageIndex';// 应当包含在src/main/ets/pages的 PageIndex.ets 是实际页面文件第六步:创建实际公开页文件进入ets目录,创建页面文件,支持符合ArkTS语法的页面布局:路径: src/main/ets/pages/PageIndex.ets@Entry@Componentstruct PageIndex { @State message: string = 'hello world' build() { Column() { Text(this.message) .fontSize(50) .fontWeight(FontWeight.Bold) .fontColor(Color.Blue) } .width('100%') .height('100%') .alignItems(HorizontalAlign.Center) }}第七步:最后一步重新编译模块1. 进入模块所在的文件夹。2. 运行`hvigorw`或点击DevEco Studio Build菜单的 `Build` > `Build HSP` 对模块进行编译。编译生成的产物,可在工程的build目录下得到 `library-default-unsigned.hsp` 和 `library.har`。A C A U T I O N确保编码及结构文件大小写精准概率均等。总结以上步骤完成了Har到Hsp的配置转换和技术性修改。若修改过程出现依赖报错或配置未生效,应按步骤验证相关时,其中步骤重点包括modules.json5及hvigorfile interface与publicated中方的修改,并删除consumerFiles段入口,这一点是容易被忽略的关键。若不配置导致不能再接收H页表的调用,请仔细检查public命名页面文件中强制显示跳转正确的键值(Pages 保留文字类名称),保证홍页面统一源自H and/or HSP下跳转到直。文章中提到的核心要点均适合ACS stage project,或HarmonyOS member codebase用于处理多重环境。
-
一、问题说明在HarmonyOS应用开发中,线程间通信是多线程并发场景下的核心需求。主要问题包括:1. ArkTS线程间数据传输限制:Actor内存隔离模型下,线程间无法直接共享内存,必须通过序列化/反序列化传递数据,单次传输限制16MB。2. Native子线程无法回调ArkTS函数:Native侧通过std::thread或pthread创建的子线程无法直接调用只能在UI主线程执行的ArkTS函数。3. 通信效率低下:JS对象序列化/反序列化带来性能开销,大量数据传递时影响应用响应性能。4. 线程数量限制:Worker线程数量限制为64个,过多的线程创建会导致内存和调度开销增大。二、原因分析根据HarmonyOS线程模型和Actor并发模型,问题根源如下:2.1 内存隔离机制· ArkTS线程模型:每个线程拥有独立的ArkTS引擎实例和内存空间,基于Actor模型实现内存隔离。· 线程间通信限制:必须通过消息传递机制进行通信,不能直接访问对方内存空间。2.2 ArkTS线程类型限制· UI主线程:负责UI绘制、事件分发、ArkTS引擎管理。· TaskPool线程(推荐):自动管理生命周期,支持优先级调度和负载均衡。· Worker线程:开发者自行管理生命周期,支持耗时任务和线程间通信。2.3 序列化约束· 对象类型限制:不支持@State、@Prop、@Link、@Observed等装饰器修饰的复杂类型。· 序列化方式:采用Structured Clone算法,通过深拷贝实现对象传输。2.4 Native线程约束· 线程安全:主线程中的napi_env、napi_value、napi_ref不能在子线程直接使用。· 回调限制:ArkTS函数只能在UI主线程调用。三、解决思路基于不同通信场景,提供分层解决方案:3.1 ArkTS线程间通信1. 基本通信:使用TaskPool和Worker的标准通信接口。2. 高效通信:使用@Sendable装饰器实现引用传递,减少序列化开销。3. 内存共享:使用SharedArrayBuffer配合Atomics原子操作。4. 事件通信:使用Emitter实现进程内线程间异步事件通信。3.2 Native与ArkTS线程通信1. 线程安全函数:使用Node-API的napi_create_threadsafe_function机制。2. libuv异步通信:使用uv_async_send方法(备选方案)。3. Native回调:Native侧执行耗时任务后,通过安全机制回调ArkTS函数。3.3 数据传递优化1. 分类传输:根据数据类型选择合适的通信对象。2. 减少数据量:控制单次传输数据大小。3. 异步锁管理:多线程访问共享数据时使用异步锁保证安全。四、解决方案方案一:ArkTS线程间高效通信4.1.1 TaskPool任务与宿主线程通信/** * SharedData.ets * 并发任务处理工具 * 功能: * 1. 定义并发处理函数 processData * 2. 提供任务管理和进度监控 * 3. 支持并发处理大量数据 */import { taskpool } from '@kit.ArkTS';import { hilog } from '@kit.PerformanceAnalysisKit';/** * Params 接口 * 定义处理数据的参数 */interface Params{ data: number[]; // 要处理的数据数组 threshold: number; // 阈值,用于筛选数据}/** * ProgressData 接口 * 定义进度数据的结构 */interface ProgressData{ type: string; // 数据类型,用于标识进度信息 current: number; // 当前处理的索引 total: number; // 总数据量 processed: number; // 已处理并符合条件的数据量}/** * processData 函数 * 并发处理数据,筛选出大于阈值的数据 * @param params 处理参数,包含数据数组和阈值 * @returns 筛选后的结果数组 */@Concurrentexport function processData(params: Params): number[] { const results: number[] = []; // 处理数据并实时发送进度 for (let i = 0; i < params.data.length; i++) { // 筛选出大于阈值的数据 if (params.data[i] > params.threshold) { results.push(params.data[i]); } // 每处理100个数据发送一次进度 if (i % 100 === 0) { taskpool.Task.sendData({ type: 'progress', current: i + 1, total: params.data.length, processed: results.length }); } } return results;}/** * MainPage 类 * 示例类,展示如何使用并发任务处理 */class MainPage { private currentTask: taskpool.Task | null = null; // 当前正在执行的任务 /** * 启动任务 * 1. 生成测试数据 * 2. 创建任务参数 * 3. 创建并配置任务 * 4. 执行任务并处理结果 */ async startProcessing(): Promise<void> { // 生成测试数据 const data = this.generateTestData(10000); // 创建任务参数 const params: Params = { data: data, threshold: 50 }; // 创建任务 const task = new taskpool.Task(processData, params); // 设置数据接收回调,用于处理进度信息 task.onReceiveData((progressData: ProgressData) => { hilog.info(0x0000, 'MainPage', `Progress: ${progressData.current}/${progressData.total}, Found: ${progressData.processed}`); this.updateProgress(progressData.current, progressData.total); }); // 保存当前任务 this.currentTask = task; try { // 执行任务并等待结果 const result: number[] = await taskpool.execute(task) as number[]; hilog.info(0x0000, 'MainPage', `Processing completed, found ${result.length} items`); this.displayResults(result); } catch (error) { hilog.error(0x0000, 'MainPage', `Task failed: ${error.message}`); } } /** * 生成测试数据 * @param count 数据量 * @returns 随机数据数组 */ private generateTestData(count: number): number[] { const data: number[] = []; for (let i = 0; i < count; i++) { data.push(Math.floor(Math.random() * 100)); } return data; } /** * 更新进度显示(示例) * @param current 当前处理的索引 * @param total 总数据量 */ private updateProgress(current: number, total: number): void { // 更新UI进度条 } /** * 显示结果(示例) * @param results 处理后的结果数组 */ private displayResults(results: number[]): void { // 处理结果显示 }}4.1.2 Worker线程即时通信/** * WorkerData.ets * Worker线程处理工具 * 功能: * 1. 定义Worker线程消息处理逻辑 * 2. 提供数据处理功能 * 3. 支持取消处理操作 */import { worker, ThreadWorkerGlobalScope, ErrorEvent } from '@kit.ArkTS';import { hilog } from '@kit.PerformanceAnalysisKit';const workerPort: ThreadWorkerGlobalScope = worker.workerPort;let shouldCancel = false;// 数据负载类型定义interface DataPayload { items: number[];}// 消息类型定义interface WorkerMessage { command: string; payload?: DataPayload;}// 批量结果消息类型定义interface BatchResultMessage { type: string; batchId: number; processedCount: number; completed: boolean;}// 取消消息类型定义interface CancelMessage { type: string; processedCount: number;}// 结果消息类型定义interface ResultMessage { type: string; processedCount: number;}// 错误消息类型定义interface ErrorMessage { type: string; error: string;}/** * 消息事件类型定义 */interface WorkerMessageEvent { data: WorkerMessage;}/** * 结果消息事件类型定义 */interface ResultMessageEvent { data: BatchResultMessage | CancelMessage | ErrorMessage;}/** * 处理Worker线程消息 */workerPort.onmessage = (event: WorkerMessageEvent): void => { const message: WorkerMessage = event.data; switch (message.command) { case 'process': if (message.payload) { processData(message.payload); } else { hilog.error(0x0000, 'DataProcessor', 'Missing payload for process command'); } break; case 'cancel': shouldCancel = true; break; default: hilog.warn(0x0000, 'DataProcessor', `Unknown command: ${message.command}`); }};/** * 数据处理函数 * @param data 要处理的数据 */function processData(data: DataPayload): void { let count = 0; const batchSize = 100; for (let i = 0; i < data.items.length; i++) { // 模拟数据处理 const processed = data.items[i] * 2; count++; // 分批发送结果 if (count % batchSize === 0 || i === data.items.length - 1) { const result: BatchResultMessage = { type: 'batch_result', batchId: Math.floor(i / batchSize), processedCount: count, completed: i === data.items.length - 1 }; workerPort.postMessage(result); } // 检查取消标志 if (shouldCancel) { const cancelledMessage: CancelMessage = { type: 'cancelled', processedCount: count }; workerPort.postMessage(cancelledMessage); shouldCancel = false; // 重置取消标志 return; } }}/** * MainPage 类 * 示例类,展示如何使用Worker线程处理数据 */class MainPage { private workerInstance: worker.ThreadWorker | null = null; /** * 启动Worker处理 * 1. 创建Worker实例 * 2. 设置消息接收回调 * 3. 设置错误处理回调 * 4. 发送处理命令 */ async startWorkerProcessing(): Promise<void> { this.workerInstance = new worker.ThreadWorker('entry/ets/workers/DataProcessor.ets'); // 设置消息接收 this.workerInstance.onmessage = (event: ResultMessageEvent): void => { const data: BatchResultMessage | CancelMessage | ErrorMessage = event.data; switch (data.type) { case 'batch_result': const batchResult = data as BatchResultMessage; this.handleBatchResult(batchResult.batchId, batchResult.processedCount, batchResult.completed); break; case 'cancelled': const cancelMessage = data as CancelMessage; this.handleCancellation(cancelMessage.processedCount); break; case 'error': const errorMessage = data as ErrorMessage; this.handleError(errorMessage.error); break; } }; // 设置错误处理 this.workerInstance.onerror = (err: ErrorEvent): void => { hilog.error(0x0000, 'MainPage', `Worker error: ${err.message}`); }; // 发送处理命令 const data = this.generateLargeDataSet(); const message: WorkerMessage = { command: 'process', payload: { items: data } }; this.workerInstance.postMessage(message); } /** * 生成测试数据 * @returns 测试数据数组 */ private generateLargeDataSet(): number[] { const data: number[] = []; for (let i = 0; i < 10000; i++) { data.push(Math.floor(Math.random() * 1000)); } return data; } /** * 处理批量结果 * @param batchId 批次ID * @param processedCount 已处理数量 * @param completed 是否完成 */ private handleBatchResult(batchId: number, processedCount: number, completed: boolean): void { hilog.info(0x0000, 'MainPage', `Batch ${batchId} completed, processed ${processedCount} items${completed ? ', all done!' : ''}`); } /** * 处理取消操作 * @param processedCount 已处理数量 */ private handleCancellation(processedCount: number): void { hilog.info(0x0000, 'MainPage', `Processing cancelled, processed ${processedCount} items`); } /** * 处理错误 * @param error 错误信息 */ private handleError(error: string): void { hilog.error(0x0000, 'MainPage', `Processing error: ${error}`); } /** * 取消处理 */ cancelProcessing(): void { if (this.workerInstance) { const message: WorkerMessage = { command: 'cancel' }; this.workerInstance.postMessage(message); } } /** * 清理Worker */ cleanupWorker(): void { if (this.workerInstance) { this.workerInstance.terminate(); this.workerInstance = null; } }}方案二:Native侧子线程与UI主线程通信4.2.1 基于线程安全函数机制(推荐方案)// Native侧代码 - ThreadSafeCommunicator.cpp#include <napi/native_api.h>#include <hilog/log.h>#include <thread>#include <vector> // 回调上下文结构 struct ThreadCallbackContext { napi_env env; napi_ref jsCallbackRef; std::vector<int> processedData; int requestId;};class ThreadSafeCommunicator { private: napi_threadsafe_function tsFunction_; bool initialized_; public: ThreadSafeCommunicator() : initialized_(false) {} ~ThreadSafeCommunicator() { cleanup();} // 初始化线程安全函数 napi_status initialize(napi_env env, napi_value jsCallback) { if (initialized_) { return napi_ok; } // 创建线程安全函数 napi_status status = napi_create_threadsafe_function( env, jsCallback, nullptr, napi_value("ThreadSafeCallback"), 0, // 无限队列 1, // 初始线程数 nullptr, nullptr, nullptr, [](napi_env env, napi_value js_callback, void* context, void* data) { // 此回调在主线程执行 ThreadCallbackContext* ctx = static_cast<ThreadCallbackContext*>(data); if (env && js_callback && ctx) { // 准备回调参数 napi_value resultArray; napi_create_array_with_length(env, ctx->processedData.size(), &resultArray); for (size_t i = 0; i < ctx->processedData.size(); i++) { napi_value element; napi_create_int32(env, ctx->processedData[i], &element); napi_set_element(env, resultArray, i, element);}napi_value requestId;napi_create_int32(env, ctx->requestId, &requestId);// 调用JavaScript回调napi_value argv[2];argv[0] = resultArray;argv[1] = requestId;napi_value global;napi_get_global(env, &global);napi_call_function(env, global, js_callback, 2, argv, nullptr);}// 清理资源if (ctx) { napi_delete_reference(ctx->env, ctx->jsCallbackRef); delete ctx;}},&tsFunction_);if (status == napi_ok) { initialized_ = true; OH_LOG_INFO(LOG_APP, "Thread safe function initialized.");}return status;}// 从子线程调用ArkTS函数void callFromWorkerThread(const std::vector<int>& data, int requestId, napi_env env, napi_ref callbackRef) { if (!initialized_) { OH_LOG_ERROR(LOG_APP, "Thread safe function not initialized!"); return; } // 准备上下文数据 auto context = new ThreadCallbackContext(); context->env = env; context->jsCallbackRef = callbackRef; context->processedData = data; context->requestId = requestId; // 获取线程安全函数 napi_acquire_threadsafe_function(tsFunction_, napi_tsfn_blocking); // 调用线程安全函数 napi_call_threadsafe_function(tsFunction_, context, napi_tsfn_nonblocking); // 释放线程安全函数 napi_release_threadsafe_function(tsFunction_, napi_tsfn_release);}// 清理资源void cleanup() { if (initialized_ && tsFunction_) { napi_release_threadsafe_function(tsFunction_, napi_tsfn_release); initialized_ = false; }}};// 导出的Native方法static ThreadSafeCommunicator gCommunicator;// 异步处理函数(在子线程执行)static void processInWorkerThread(int requestId) { OH_LOG_INFO(LOG_APP, "Worker thread started for request %{public}d", requestId); // 模拟耗时处理 std::vector<int> results; for (int i = 0; i < 100; i++) { results.push_back(i * requestId); std::this_thread::sleep_for(std::chrono::milliseconds(10)); } OH_LOG_INFO(LOG_APP, "Worker thread completed for request %{public}d", requestId);}// ArkTS侧代码 - NativeCommunicator.etsimport { hilog } from '@kit.PerformanceAnalysisKit';// Native模块声明declare namespace native { function initializeThreadSafeFunction(callback: (data: number[], requestId: number) => void): void; function startProcessing(requestId: number): void; function cleanup(): void;}class NativeCommunicator { private requestCounter: number = 0; // 初始化通信 initialize(): void { try { // 初始化线程安全函数 native.initializeThreadSafeFunction(this.handleNativeCallback.bind(this)); hilog.info(0x0000, 'NativeCommunicator', 'Thread safe function initialized'); } catch (error) { hilog.error(0x0000, 'NativeCommunicator', `Initialization failed: ${error.message}`); } } // 启动Native侧处理 startNativeProcessing(): void { const requestId = ++this.requestCounter; hilog.info(0x0000, 'NativeCommunicator', `Starting native processing request ${requestId}`); try { native.startProcessing(requestId); } catch (error) { hilog.error(0x0000, 'NativeCommunicator', `Failed to start processing: ${error.message}`); } } // 处理Native回调 private handleNativeCallback(data: number[], requestId: number): void { hilog.info(0x0000, 'NativeCommunicator', `Received callback for request ${requestId}, data length: ${data.length}`); // 更新UI或处理数据 this.updateUIWithData(data, requestId); } // 清理资源 cleanup(): void { try { native.cleanup(); hilog.info(0x0000, 'NativeCommunicator', 'Native communicator cleaned up'); } catch (error) { hilog.error(0x0000, 'NativeCommunicator', `Cleanup failed: ${error.message}`); } } // 更新UI(示例) private updateUIWithData(data: number[], requestId: number): void { // 实现UI更新逻辑 const sum = data.reduce((acc, val) => acc + val, 0); hilog.info(0x0000, 'NativeCommunicator', `Request ${requestId}: Sum = ${sum}, Avg = ${(sum / data.length).toFixed(2)}`); }}方案三:使用Emitter进行线程间通信// 事件管理器 - ThreadEventManager.etsimport { emitter, EmitterEvent } from '@ohos.events.emitter';import { hilog } from '@kit.PerformanceAnalysisKit';export enum ThreadEventType { DATA_PROCESSING_START = 'data_processing_start', DATA_PROCESSING_PROGRESS = 'data_processing_progress', DATA_PROCESSING_COMPLETE = 'data_processing_complete', TASK_CANCELLED = 'task_cancelled', ERROR_OCCURRED = 'error_occurred'}export interface ProgressEventData { taskId: string; current: number; total: number; percentage: number;}export interface CompleteEventData { taskId: string; result: number[]; processingTime: number;}export class ThreadEventManager { private static instance: ThreadEventManager; private eventSubscriptions: Map<string, number[]> = new Map(); static getInstance(): ThreadEventManager { if (!ThreadEventManager.instance) { ThreadEventManager.instance = new ThreadEventManager(); } return ThreadEventManager.instance; } // 订阅事件 subscribe(eventType: ThreadEventType, callback: (eventData: EmitterEvent) => void, priority?: number): number { const innerEvent = { eventId: eventType }; const subscriptionId = emitter.on(innerEvent, callback); // 保存订阅ID if (!this.eventSubscriptions.has(eventType)) { this.eventSubscriptions.set(eventType, []); } this.eventSubscriptions.get(eventType)?.push(subscriptionId); return subscriptionId; } // 发布事件 emit(eventType: ThreadEventType, data: unknown): void { const innerEvent = { eventId: eventType, priority: emitter.EventPriority.HIGH }; const eventData = { data: data }; emitter.emit(innerEvent, eventData); } // 取消订阅 unsubscribe(eventType: ThreadEventType, subscriptionId?: number): void { if (subscriptionId) { emitter.off(subscriptionId); // 从映射中移除 const ids = this.eventSubscriptions.get(eventType); if (ids) { const index = ids.indexOf(subscriptionId); if (index > -1) { ids.splice(index, 1); } } } else { // 取消该事件的所有订阅 const ids = this.eventSubscriptions.get(eventType); if (ids) { ids.forEach(id => emitter.off(id)); this.eventSubscriptions.delete(eventType); } } } // 清理所有订阅 cleanup(): void { this.eventSubscriptions.forEach((ids) => { ids.forEach(id => emitter.off(id)); }); this.eventSubscriptions.clear(); }}// 使用示例 - 数据处理控制器class DataProcessorController { private eventManager: ThreadEventManager; private currentTaskId: string | null = null; constructor() { this.eventManager = ThreadEventManager.getInstance(); this.setupEventListeners(); } // 设置事件监听器 private setupEventListeners(): void { // 监听进度更新 this.eventManager.subscribe( ThreadEventType.DATA_PROCESSING_PROGRESS, this.handleProgressUpdate.bind(this) ); // 监听完成事件 this.eventManager.subscribe( ThreadEventType.DATA_PROCESSING_COMPLETE, this.handleCompletion.bind(this) ); // 监听错误事件 this.eventManager.subscribe( ThreadEventType.ERROR_OCCURRED, this.handleError.bind(this) ); } // 启动数据处理任务 async startDataProcessing(data: number[]): Promise<void> { this.currentTaskId = `task_${Date.now()}`; // 通知任务开始 this.eventManager.emit(ThreadEventType.DATA_PROCESSING_START, { taskId: this.currentTaskId, dataSize: data.length, timestamp: Date.now() }); // 在TaskPool中处理数据 const task = new taskpool.Task(this.processDataInTaskPool.bind(this), { taskId: this.currentTaskId, data: data }); try { const result = await taskpool.execute(task); this.eventManager.emit(ThreadEventType.DATA_PROCESSING_COMPLETE, result); } catch (error) { this.eventManager.emit(ThreadEventType.ERROR_OCCURRED, { taskId: this.currentTaskId, error: error.message }); } } // TaskPool中的处理函数 @Concurrent private processDataInTaskPool(params: { taskId: string, data: number[] }): { taskId: string, result: number[], processingTime: number } { const startTime = Date.now(); const results: number[] = []; const batchSize = 100; for (let i = 0; i < params.data.length; i++) { // 处理数据 const processed = params.data[i] * Math.random(); results.push(processed); // 发送进度更新 if (i % batchSize === 0 || i === params.data.length - 1) { ThreadEventManager.getInstance().emit( ThreadEventType.DATA_PROCESSING_PROGRESS, { taskId: params.taskId, current: i + 1, total: params.data.length, percentage: ((i + 1) / params.data.length * 100).toFixed(1) } ); } } const processingTime = Date.now() - startTime; return { taskId: params.taskId, result: results, processingTime: processingTime }; } // 处理进度更新 private handleProgressUpdate(event: EmitterEvent): void { const progressData = event.data as ProgressEventData; hilog.info(0x0000, 'DataProcessor', `Task ${progressData.taskId}: ${progressData.percentage}% completed`); // 更新UI进度 this.updateProgressUI(progressData); } // 处理完成事件 private handleCompletion(event: EmitterEvent): void { const completeData = event.data as CompleteEventData; hilog.info(0x0000, 'DataProcessor', `Task ${completeData.taskId} completed in ${completeData.processingTime}ms`); // 更新UI显示结果 this.displayResults(completeData); } // 处理错误 private handleError(event: EmitterEvent): void { const errorData = event.data as { taskId: string, error: string }; hilog.error(0x0000, 'DataProcessor', `Task ${errorData.taskId} failed: ${errorData.error}`); // 显示错误信息 this.showError(errorData.error); } // UI更新方法(示例) private updateProgressUI(progress: ProgressEventData): void { // 实现UI进度更新 } private displayResults(results: CompleteEventData): void { // 实现结果显示 } private showError(error: string): void { // 实现错误显示 }}五、总结5.1 核心要点总结线程模型理解:掌握HarmonyOS的Actor内存隔离模型,理解不同线程类型(主线程、TaskPool线程、Worker线程)的特性。通信机制选择:· ArkTS线程间:优先使用TaskPool,需要双向通信时使用Worker。· Native与ArkTS:优先使用线程安全函数机制。五、总结5.1 核心要点总结线程模型理解:掌握HarmonyOS的Actor内存隔离模型,理解不同线程类型(主线程、TaskPool线程、Worker线程)的特性。通信机制选择:· ArkTS线程间:优先使用TaskPool,需要双向通信时使用Worker。· Native与ArkTS:优先使用线程安全函数机制。· 进程内通信:使用Emitter进行事件驱动通信。性能优化关键:· 使用@Sendable装饰器减少序列化开销。· 使用SharedArrayBuffer实现高效内存共享。· 控制单次传输数据量(≤16MB)。5.2 最佳实践建议线程数量管理:· Worker线程数量控制在64个以内。· 优先使用TaskPool,由系统自动管理线程生命周期。· I/O密集型任务使用异步接口,避免线程阻塞。通信对象选择:· 小量数据:使用普通可序列化对象。· 大量数据:使用@Sendable对象或SharedArrayBuffer。· 二进制数据:使用ArrayBuffer。· 资源对象:使用Transferable对象(如文件描述符)。错误处理和资源管理:· 及时清理线程和通信资源。· 处理序列化异常和通信超时。· 使用异步锁保证共享数据安全。5.3 版本兼容性本方案基于HarmonyOS API Version 10+设计,支持ArkTS和Native开发。关键API包括:· @kit.ArkTS:TaskPool、Worker、Sendable5.4 性能监控建议在开发过程中使用:· Trace工具:监控线程间通信耗时。· 内存分析:检查SharedArrayBuffer使用情况。· 性能测试:验证多线程通信对应用性能的影响。通过合理选择和应用这些线程间通信方案,开发者可以在HarmonyOS平台上构建高性能、响应迅速的多线程应用,同时避免常见的线程安全和资源管理问题。· @ohos.events.emitter:事件通信· Node-API:线程安全函数· C++标准库:std::thread、pthread1. 进程内通信:使用Emitter进行事件驱动通信。性能优化关键:· 使用@Sendable装饰器减少序列化开销。· 使用SharedArrayBuffer实现高效内存共享。· 控制单次传输数据量(≤16MB)。5.2 最佳实践建议线程数量管理:· Worker线程数量控制在64个以内。· 优先使用TaskPool,由系统自动管理线程生命周期。· I/O密集型任务使用异步接口,避免线程阻塞。通信对象选择:· 小量数据:使用普通可序列化对象。· 大量数据:使用@Sendable对象或SharedArrayBuffer。· 二进制数据:使用ArrayBuffer。· 资源对象:使用Transferable对象(如文件描述符)。错误处理和资源管理:· 及时清理线程和通信资源。· 处理序列化异常和通信超时。· 使用异步锁保证共享数据安全。5.3 版本兼容性本方案基于HarmonyOS API Version 10+设计,支持ArkTS和Native开发。关键API包括:· @kit.ArkTS:TaskPool、Worker、Sendable· @ohos.events.emitter:事件通信· Node-API:线程安全函数· C++标准库:std::thread、pthread5.4 性能监控建议在开发过程中使用:1. Trace工具:监控线程间通信耗时。2. 内存分析:检查SharedArrayBuffer使用情况。3. 性能测试:验证多线程通信对应用性能的影响。通过合理选择和应用这些线程间通信方案,开发者可以在HarmonyOS平台上构建高性能、响应迅速的多线程应用,同时避免常见的线程安全和资源管理问题。
-
案例概述1.1 问题背景在移动应用开发中,TabBar是常见的底部导航组件。传统的TabBar通常采用平面设计,但为了提升用户体验和视觉吸引力,开发者需要实现以下效果:凸起效果:中间按钮凸起,吸引用户点击凹陷效果:TabBar整体凹陷,增强立体感动态交互:点击动画、悬浮效果状态管理:选中状态、未选中状态区分适配性:兼容不同设备尺寸1.2 解决方案概述本案例提供完整的自定义TabBar解决方案,包含:● 凸起TabBar:中间按钮凸起,带动画效果● 凹陷TabBar:整体凹陷设计,增强立体感● 平滑过渡:Tab切换平滑动画● 状态管理:完整的Tab状态管理● 主题适配:支持深色/浅色模式实现步骤详解步骤1:定义数据模型和配置类// TabBarModels.etsexport interface TabItem {id: string; // Tab唯一标识text: string; // 显示文本icon: Resource; // 图标资源activeIcon: Resource; // 激活图标badge?: number | string; // 角标disabled?: boolean; // 是否禁用accessibilityLabel?: string; // 无障碍标签}export interface TabBarMetrics {height: number; // TabBar高度width: number; // TabBar宽度itemWidth: number; // 每个Tab宽度bulgeHeight: number; // 凸起高度indentDepth: number; // 凹陷深度safeAreaBottom: number; // 底部安全区域}export interface TabAnimationConfig {duration: number; // 动画时长curve: number; // 动画曲线scale: number; // 缩放比例translateY: number; // Y轴偏移}export interface TabStyle {normalColor: ResourceColor; // 正常颜色activeColor: ResourceColor; // 激活颜色backgroundColor: ResourceColor; // 背景色borderColor: ResourceColor; // 边框颜色shadowColor: ResourceColor; // 阴影颜色textSize: number; // 文字大小iconSize: number; // 图标大小borderRadius: number; // 圆角半径}export class TabBarConfig {// 凸起TabBar配置static readonly BULGE_CONFIG = {height: 80, // 总高度bulgeHeight: 20, // 凸起高度bulgeWidth: 60, // 凸起宽度iconSize: 28, // 图标大小textSize: 12, // 文字大小borderRadius: 40, // 圆角半径shadowBlur: 20, // 阴影模糊shadowOffsetY: 4, // 阴影偏移highlightScale: 1.2, // 高亮缩放};// 凹陷TabBar配置static readonly INDENT_CONFIG = {height: 70, // 总高度indentDepth: 8, // 凹陷深度borderWidth: 1, // 边框宽度iconSize: 24, // 图标大小textSize: 11, // 文字大小borderRadius: 20, // 圆角半径innerPadding: 8, // 内边距highlightDepth: 4, // 高亮深度};// 动画配置static readonly ANIMATION_CONFIG = {tabChange: {duration: 300, // Tab切换动画curve: Curve.EaseInOut,},bulgePress: {duration: 200, // 凸起按钮按下curve: Curve.FastOutLinearIn,},indentPress: {duration: 150, // 凹陷按钮按下curve: Curve.EaseOut,},badgeUpdate: {duration: 300, // 角标更新curve: Curve.Spring,},};// 样式配置static readonly STYLE_CONFIG = {light: {normalColor: ‘#666666’, // 正常状态颜色activeColor: ‘#0066FF’, // 激活状态颜色backgroundColor: ‘#FFFFFF’, // 背景色borderColor: ‘#F0F0F0’, // 边框颜色shadowColor: ‘#40000000’, // 阴影颜色bulgeColor: ‘#0066FF’, // 凸起按钮颜色bulgeShadow: ‘#400066FF’, // 凸起按钮阴影},dark: {normalColor: ‘#AAAAAA’,activeColor: ‘#4D94FF’,backgroundColor: ‘#1C1C1E’,borderColor: ‘#2C2C2E’,shadowColor: ‘#40000000’,bulgeColor: ‘#4D94FF’,bulgeShadow: ‘#404D94FF’,},};}步骤1是整个自定义TabBar系统的基础架构设计,我们首先建立了一套完整的数据模型和配置体系。这个步骤的目的是为TabBar的各种状态、样式、动画等提供类型安全的定义和可配置的参数。步骤2:实现TabBar管理器// TabBarManager.etsimport { hilog } from ‘@kit.PerformanceAnalysisKit’;export class TabBarManager {private static instance: TabBarManager;private currentTab: string = ‘’;private previousTab: string = ‘’;private tabs: Map<string, TabItem> = new Map();private tabChangeCallbacks: Array<(tabId: string) => void> = [];private badgeUpdateCallbacks: Array<(tabId: string, badge: number | string) => void> = [];private animationStateCallbacks: Array<(tabId: string, isAnimating: boolean) => void> = [];private theme: ‘light’ | ‘dark’ = ‘light’;private metrics: TabBarMetrics = {height: 0,width: 0,itemWidth: 0,bulgeHeight: 0,indentDepth: 0,safeAreaBottom: 0};private constructor() {this.initialize();}public static getInstance(): TabBarManager {if (!TabBarManager.instance) {TabBarManager.instance = new TabBarManager();}return TabBarManager.instance;}private initialize(): void {this.initThemeListener();hilog.info(0x0000, ‘TabBarManager’, ‘TabBar管理器初始化完成’);}private initThemeListener(): void {// 监听系统主题变化try {const context = getContext(this) as common.UIAbilityContext;// 这里可以添加主题监听逻辑} catch (error) {hilog.error(0x0000, ‘TabBarManager’, '初始化主题监听失败: ’ + JSON.stringify(error));}}// 注册Tabpublic registerTab(tab: TabItem): void {this.tabs.set(tab.id, tab);if (!this.currentTab && this.tabs.size > 0) {this.currentTab = tab.id;}hilog.info(0x0000, ‘TabBarManager’, 注册Tab: ${tab.id} - ${tab.text});}// 切换Tabpublic switchTab(tabId: string, animated: boolean = true): boolean {if (!this.tabs.has(tabId)) {hilog.warn(0x0000, ‘TabBarManager’, Tab不存在: ${tabId});return false;}const tab = this.tabs.get(tabId)!; if (tab.disabled) { hilog.warn(0x0000, 'TabBarManager', `Tab被禁用: ${tabId}`); return false; } if (tabId === this.currentTab) { return false; } this.previousTab = this.currentTab; this.currentTab = tabId; hilog.info(0x0000, 'TabBarManager', `切换Tab: ${this.previousTab} -> ${this.currentTab}, 动画: ${animated}`); // 通知所有监听者 this.tabChangeCallbacks.forEach(callback => { callback(tabId); }); return true;}// 更新角标public updateBadge(tabId: string, badge: number | string): boolean {if (!this.tabs.has(tabId)) {return false;}const tab = this.tabs.get(tabId)!; tab.badge = badge; // 通知角标更新 this.badgeUpdateCallbacks.forEach(callback => { callback(tabId, badge); }); hilog.info(0x0000, 'TabBarManager', `更新角标: ${tabId} = ${badge}`); return true;}// 获取当前Tabpublic getCurrentTab(): string {return this.currentTab;}// 获取Tab信息public getTabInfo(tabId: string): TabItem | undefined {return this.tabs.get(tabId);}// 获取所有Tabpublic getAllTabs(): TabItem[] {return Array.from(this.tabs.values());}// 设置尺寸信息public setMetrics(metrics: TabBarMetrics): void {this.metrics = metrics;}// 获取尺寸信息public getMetrics(): TabBarMetrics {return this.metrics;}// 设置主题public setTheme(theme: ‘light’ | ‘dark’): void {this.theme = theme;hilog.info(0x0000, ‘TabBarManager’, 切换主题: ${theme});}// 获取当前主题public getCurrentTheme(): ‘light’ | ‘dark’ {return this.theme;}// 获取主题样式public getThemeStyle(): TabStyle {const style = TabBarConfig.STYLE_CONFIG[this.theme];return {…style,textSize: 14,iconSize: 24,borderRadius: 20};}// 注册监听器public onTabChange(callback: (tabId: string) => void): void {this.tabChangeCallbacks.push(callback);}public onBadgeUpdate(callback: (tabId: string, badge: number | string) => void): void {this.badgeUpdateCallbacks.push(callback);}public onAnimationStateChange(callback: (tabId: string, isAnimating: boolean) => void): void {this.animationStateCallbacks.push(callback);}// 通知动画状态public notifyAnimationState(tabId: string, isAnimating: boolean): void {this.animationStateCallbacks.forEach(callback => {callback(tabId, isAnimating);});}// 清理public destroy(): void {this.tabs.clear();this.tabChangeCallbacks = [];this.badgeUpdateCallbacks = [];this.animationStateCallbacks = [];hilog.info(0x0000, ‘TabBarManager’, ‘TabBar管理器已销毁’);}}步骤2实现了TabBar的核心管理器,这是一个单例模式的协调者,负责管理所有Tab的状态、协调组件间的通信、处理主题切换等全局功能。步骤3:实现凸起TabBar组件// BulgeTabBar.ets@Componentexport struct BulgeTabBar {private tabManager: TabBarManager = TabBarManager.getInstance();@State tabs: TabItem[] = [];@State currentTab: string = ‘’;@State animationStates: Map<string, boolean> = new Map();@State bulgeAnimationValue: number = 1;@State isThemeDark: boolean = false;@Prop onTabChange?: (tabId: string) => void;@Prop selectedColor?: ResourceColor;@Prop normalColor?: ResourceColor;@Prop backgroundColor?: ResourceColor;@Prop bulgeColor?: ResourceColor;@Prop height: number = TabBarConfig.BULGE_CONFIG.height;@Prop bulgeHeight: number = TabBarConfig.BULGE_CONFIG.bulgeHeight;@Prop showDivider: boolean = true;private bulgeAnimation: AnimationController = new AnimationController({ duration: 200 });aboutToAppear() {this.tabs = this.tabManager.getAllTabs();this.currentTab = this.tabManager.getCurrentTab();this.setupListeners();// 初始化动画状态 this.tabs.forEach(tab => { this.animationStates.set(tab.id, false); });}aboutToDisappear() {this.bulgeAnimation.reset();}private setupListeners(): void {this.tabManager.onTabChange((tabId: string) => {this.currentTab = tabId;});this.tabManager.onBadgeUpdate((tabId: string, badge: number | string) => { const index = this.tabs.findIndex(tab => tab.id === tabId); if (index !== -1) { this.tabs[index].badge = badge; this.tabs = [...this.tabs]; // 触发更新 } });}private onTabClick(tab: TabItem, index: number): void {if (tab.disabled) {return;}// 触发动画 this.triggerTabAnimation(tab.id); // 中间凸起按钮特殊动画 if (index === Math.floor(this.tabs.length / 2)) { this.triggerBulgeAnimation(); } // 切换Tab if (this.tabManager.switchTab(tab.id, true)) { this.onTabChange?.(tab.id); }}private triggerTabAnimation(tabId: string): void {this.animationStates.set(tabId, true);this.animationStates = new Map(this.animationStates);this.tabManager.notifyAnimationState(tabId, true); setTimeout(() => { this.animationStates.set(tabId, false); this.animationStates = new Map(this.animationStates); this.tabManager.notifyAnimationState(tabId, false); }, TabBarConfig.ANIMATION_CONFIG.tabChange.duration);}private triggerBulgeAnimation(): void {this.bulgeAnimation.value = 1;this.bulgeAnimation.play();}@Builderprivate buildTabItem(tab: TabItem, index: number) {const isActive = tab.id === this.currentTab;const isAnimating = this.animationStates.get(tab.id) || false;const isMiddleTab = index === Math.floor(this.tabs.length / 2);const themeStyle = this.tabManager.getThemeStyle();const config = TabBarConfig.BULGE_CONFIG;if (isMiddleTab) { // 中间凸起按钮 this.buildBulgeTab(tab, isActive, isAnimating, themeStyle); } else { // 普通Tab按钮 this.buildNormalTab(tab, isActive, isAnimating, themeStyle); }}@Builderprivate buildNormalTab(tab: TabItem, isActive: boolean, isAnimating: boolean, style: TabStyle) {const config = TabBarConfig.BULGE_CONFIG;const scale = isAnimating ? config.highlightScale : 1;const opacity = isAnimating ? 0.8 : 1;const translateY = isAnimating ? -5 : 0;Column() { // 角标 if (tab.badge !== undefined) { this.buildBadge(tab.badge); } // 图标 Stack({ alignContent: Alignment.Center }) { Image(isActive ? tab.activeIcon : tab.icon) .width(config.iconSize) .height(config.iconSize) .objectFit(ImageFit.Contain) .interpolation(ImageInterpolation.High) } .width(config.iconSize + 20) .height(config.iconSize + 20) .borderRadius((config.iconSize + 20) / 2) .backgroundColor(isActive ? `${style.activeColor}20` : Color.Transparent) // 文本 Text(tab.text) .fontSize(config.textSize) .fontColor(isActive ? style.activeColor : style.normalColor) .fontWeight(isActive ? FontWeight.Medium : FontWeight.Normal) .margin({ top: 4 }) .opacity(isActive ? 1 : 0.8) } .scale({ x: scale, y: scale }) .opacity(opacity) .translate({ y: translateY }) .animation({ duration: TabBarConfig.ANIMATION_CONFIG.tabChange.duration, curve: TabBarConfig.ANIMATION_CONFIG.tabChange.curve }) .width('100%') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) .padding({ top: 8, bottom: 8 }) .onClick(() => this.onTabClick(tab, index))}@Builderprivate buildBulgeTab(tab: TabItem, isActive: boolean, isAnimating: boolean, style: TabStyle) {const config = TabBarConfig.BULGE_CONFIG;const bulgeScale = isAnimating ? 1.1 : 1;const shadowOpacity = isAnimating ? 0.6 : 0.4;const translateY = isAnimating ? -config.bulgeHeight * 0.5 : -config.bulgeHeight * 0.3;Column() { // 凸起背景 Circle() .width(config.iconSize + 40) .height(config.iconSize + 40) .fill(this.bulgeColor || style.bulgeColor) .shadow({ radius: config.shadowBlur, color: style.bulgeShadow, offsetY: config.shadowOffsetY }) .opacity(shadowOpacity) // 图标容器 Circle() .width(config.iconSize + 30) .height(config.iconSize + 30) .fill(this.backgroundColor || style.backgroundColor) .margin({ top: -config.iconSize - 30 }) // 图标 Image(isActive ? tab.activeIcon : tab.icon) .width(config.iconSize + 10) .height(config.iconSize + 10) .objectFit(ImageFit.Contain) .interpolation(ImageInterpolation.High) .margin({ top: -config.iconSize - 25 }) } .scale({ x: bulgeScale, y: bulgeScale }) .translate({ y: translateY }) .animation({ duration: TabBarConfig.ANIMATION_CONFIG.bulgePress.duration, curve: TabBarConfig.ANIMATION_CONFIG.bulgePress.curve }) .width('100%') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) .onClick(() => this.onTabClick(tab, Math.floor(this.tabs.length / 2)))}@Builderprivate buildBadge(badge: number | string) {const isNumber = typeof badge === ‘number’;const showDot = !isNumber && badge === ‘dot’;if (showDot) { // 小红点 Circle() .width(8) .height(8) .fill('#FF3B30') .position({ x: '60%', y: '10%' }) } else { // 数字角标 Text(badge.toString()) .fontSize(10) .fontColor('#FFFFFF') .textAlign(TextAlign.Center) .backgroundColor('#FF3B30') .padding({ left: 6, right: 6, top: 2, bottom: 2 }) .borderRadius(10) .position({ x: '60%', y: '5%' }) .maxLines(1) .minFontSize(8) }}@Builderprivate buildDivider() {if (this.showDivider) {Divider().strokeWidth(0.5).color(‘#F0F0F0’).margin({ left: 16, right: 16 })}}build() {const themeStyle = this.tabManager.getThemeStyle();const config = TabBarConfig.BULGE_CONFIG;const totalHeight = this.height + this.bulgeHeight + this.tabManager.getMetrics().safeAreaBottom;Column() { // 顶部边框 this.buildDivider() // Tab内容 Row() { ForEach(this.tabs, (tab: TabItem, index: number) => { Column() { this.buildTabItem(tab, index) } .layoutWeight(1) .height(this.height) }) } .width('100%') .height(this.height) .backgroundColor(this.backgroundColor || themeStyle.backgroundColor) } .width('100%') .height(totalHeight) .backgroundColor(this.backgroundColor || themeStyle.backgroundColor) .shadow({ radius: config.shadowBlur, color: themeStyle.shadowColor, offsetY: -config.shadowOffsetY }) .position({ x: 0, y: 0 })}}步骤3实现了具有凸起效果的TabBar组件,这是本案例的核心视觉组件之一。该组件的特点是中间按钮会凸起显示,吸引用户注意力,常用于突出重要功能(如发布、拍照等)步骤4:实现凹陷TabBar组件// IndentTabBar.ets@Componentexport struct IndentTabBar {private tabManager: TabBarManager = TabBarManager.getInstance();@State tabs: TabItem[] = [];@State currentTab: string = ‘’;@State animationStates: Map<string, boolean> = new Map();@State pressDepth: Map<string, number> = new Map();@State isThemeDark: boolean = false;@Prop onTabChange?: (tabId: string) => void;@Prop selectedColor?: ResourceColor;@Prop normalColor?: ResourceColor;@Prop backgroundColor?: ResourceColor;@Prop borderColor?: ResourceColor;@Prop height: number = TabBarConfig.INDENT_CONFIG.height;@Prop indentDepth: number = TabBarConfig.INDENT_CONFIG.indentDepth;@Prop showBorder: boolean = true;private longPressTimer: Map<string, number> = new Map();aboutToAppear() {this.tabs = this.tabManager.getAllTabs();this.currentTab = this.tabManager.getCurrentTab();this.setupListeners();// 初始化状态 this.tabs.forEach(tab => { this.animationStates.set(tab.id, false); this.pressDepth.set(tab.id, 0); });}aboutToDisappear() {// 清理定时器this.longPressTimer.forEach(timer => {clearTimeout(timer);});this.longPressTimer.clear();}private setupListeners(): void {this.tabManager.onTabChange((tabId: string) => {this.currentTab = tabId;});this.tabManager.onBadgeUpdate((tabId: string, badge: number | string) => { const index = this.tabs.findIndex(tab => tab.id === tabId); if (index !== -1) { this.tabs[index].badge = badge; this.tabs = [...this.tabs]; } });}private onTabClick(tab: TabItem, index: number): void {if (tab.disabled) {return;}// 触发按下动画 this.triggerPressAnimation(tab.id, index); // 切换Tab if (this.tabManager.switchTab(tab.id, true)) { this.onTabChange?.(tab.id); }}private onTabPressIn(tabId: string, index: number): void {this.pressDepth.set(tabId, TabBarConfig.INDENT_CONFIG.highlightDepth);this.pressDepth = new Map(this.pressDepth);}private onTabPressOut(tabId: string, index: number): void {this.pressDepth.set(tabId, 0);this.pressDepth = new Map(this.pressDepth);}private onTabLongPress(tab: TabItem, index: number): void {if (tab.disabled) {return;}// 长按反馈 this.triggerLongPressAnimation(tab.id); hilog.info(0x0000, 'IndentTabBar', `长按Tab: ${tab.text}`); // 可以在这里触发预览或其他功能}private triggerPressAnimation(tabId: string, index: number): void {this.animationStates.set(tabId, true);this.animationStates = new Map(this.animationStates);this.tabManager.notifyAnimationState(tabId, true); setTimeout(() => { this.animationStates.set(tabId, false); this.animationStates = new Map(this.animationStates); this.tabManager.notifyAnimationState(tabId, false); }, TabBarConfig.ANIMATION_CONFIG.indentPress.duration);}private triggerLongPressAnimation(tabId: string): void {this.pressDepth.set(tabId, TabBarConfig.INDENT_CONFIG.highlightDepth * 2);this.pressDepth = new Map(this.pressDepth);setTimeout(() => { this.pressDepth.set(tabId, 0); this.pressDepth = new Map(this.pressDepth); }, 300);}@Builderprivate buildTabItem(tab: TabItem, index: number) {const isActive = tab.id === this.currentTab;const isAnimating = this.animationStates.get(tab.id) || false;const depth = this.pressDepth.get(tab.id) || 0;const themeStyle = this.tabManager.getThemeStyle();const config = TabBarConfig.INDENT_CONFIG;Column() { // Tab项 Column() { // 角标 if (tab.badge !== undefined) { this.buildBadge(tab.badge); } // 凹陷背景 Column() .width('80%') .height('80%') .backgroundColor(isActive ? `${themeStyle.activeColor}20` : `${themeStyle.normalColor}10`) .borderRadius(config.borderRadius - depth) .margin({ top: depth }) // 图标和文字 Column() { // 图标 Stack({ alignContent: Alignment.Center }) { Image(isActive ? tab.activeIcon : tab.icon) .width(config.iconSize) .height(config.iconSize) .objectFit(ImageFit.Contain) .interpolation(ImageInterpolation.High) .margin({ top: depth * 0.5 }) } .width(config.iconSize + 20) .height(config.iconSize + 20) .borderRadius((config.iconSize + 20) / 2) .backgroundColor(isActive ? `${themeStyle.activeColor}30` : Color.Transparent) // 文字 Text(tab.text) .fontSize(config.textSize) .fontColor(isActive ? themeStyle.activeColor : themeStyle.normalColor) .fontWeight(isActive ? FontWeight.Medium : FontWeight.Normal) .margin({ top: 6 }) .opacity(isActive ? 1 : 0.8) } .margin({ top: -depth }) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) .borderRadius(config.borderRadius) .backgroundColor(themeStyle.backgroundColor) .shadow({ radius: depth > 0 ? 4 : 2, color: '#40000000', offsetY: depth > 0 ? 2 : 1 }) .overlay( Column() .width('100%') .height('100%') .borderRadius(config.borderRadius) .border({ width: 0.5, color: depth > 0 ? `${themeStyle.activeColor}30` : `${themeStyle.borderColor}` }) ) } .scale({ x: isAnimating ? 0.95 : 1, y: isAnimating ? 0.95 : 1 }) .animation({ duration: TabBarConfig.ANIMATION_CONFIG.indentPress.duration, curve: TabBarConfig.ANIMATION_CONFIG.indentPress.curve }) .width('100%') .height('100%') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) .onClick(() => this.onTabClick(tab, index)) .onTouch((event: TouchEvent) => { if (event.type === TouchType.Down) { this.onTabPressIn(tab.id, index); } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) { this.onTabPressOut(tab.id, index); } }) .onClick(() => this.onTabClick(tab, index)) .onLongPress(() => this.onTabLongPress(tab, index))}@Builderprivate buildBadge(badge: number | string) {const isNumber = typeof badge === ‘number’;const showDot = !isNumber && badge === ‘dot’;if (showDot) { // 小红点 Circle() .width(6) .height(6) .fill('#FF3B30') .position({ x: '60%', y: '15%' }) } else { // 数字角标 Text(badge.toString()) .fontSize(9) .fontColor('#FFFFFF') .textAlign(TextAlign.Center) .backgroundColor('#FF3B30') .padding({ left: 4, right: 4, top: 1, bottom: 1 }) .borderRadius(8) .position({ x: '60%', y: '10%' }) .maxLines(1) .minFontSize(7) }}@Builderprivate buildIndentBackground() {const themeStyle = this.tabManager.getThemeStyle();const config = TabBarConfig.INDENT_CONFIG;const totalHeight = this.height + this.indentDepth + this.tabManager.getMetrics().safeAreaBottom;Column() { // 主凹陷区域 Row() .width('100%') .height(this.height) .backgroundColor(themeStyle.backgroundColor) .borderRadius(config.borderRadius) .border({ width: this.showBorder ? config.borderWidth : 0, color: this.borderColor || themeStyle.borderColor }) .shadow({ radius: 8, color: themeStyle.shadowColor, offsetY: 2 }) .padding({ left: 8, right: 8, top: 8, bottom: 8 }) // 底部填充 Column() .width('100%') .height(this.indentDepth) .backgroundColor(themeStyle.backgroundColor) } .width('100%') .height(totalHeight) .backgroundColor(themeStyle.backgroundColor)}build() {const themeStyle = this.tabManager.getThemeStyle();const config = TabBarConfig.INDENT_CONFIG;const totalHeight = this.height + this.indentDepth + this.tabManager.getMetrics().safeAreaBottom;Stack({ alignContent: Alignment.Bottom }) { // 凹陷背景 this.buildIndentBackground() // Tab内容 Row() { ForEach(this.tabs, (tab: TabItem, index: number) => { Column() { this.buildTabItem(tab, index) } .layoutWeight(1) .height(this.height - 16) // 减去内边距 .margin({ left: 4, right: 4, top: 8, bottom: 8 }) }) } .width('100%') .height(this.height) } .width('100%') .height(totalHeight)}}步骤4实现了具有凹陷效果的TabBar组件,这是另一种常见的TabBar设计风格。与凸起效果相反,凹陷TabBar让整个TabBar区域看起来像是嵌入到界面中,营造出精致、现代的视觉感受。步骤5:实现TabBar容器页面// CustomTabBarPage.ets@Entry@Componentexport struct CustomTabBarPage {@State currentPage: number = 0;@State selectedTab: string = ‘home’;@State themeMode: ‘light’ | ‘dark’ = ‘light’;@State showBulgeTabBar: boolean = true;@State badgeCounts: Map<string, number> = new Map([[‘message’, 3],[‘mine’, 99]]);private tabManager: TabBarManager = TabBarManager.getInstance();// 定义Tab数据private tabs: TabItem[] = [{id: ‘home’,text: ‘首页’,icon: $r(‘app.media.ic_home’),activeIcon: $r(‘app.media.ic_home_fill’),accessibilityLabel: ‘首页’},{id: ‘explore’,text: ‘发现’,icon: $r(‘app.media.ic_explore’),activeIcon: $r(‘app.media.ic_explore_fill’),accessibilityLabel: ‘发现’},{id: ‘add’,text: ‘发布’,icon: $r(‘app.media.ic_add’),activeIcon: $r(‘app.media.ic_add_fill’),accessibilityLabel: ‘发布’},{id: ‘message’,text: ‘消息’,icon: $r(‘app.media.ic_message’),activeIcon: $r(‘app.media.ic_message_fill’),badge: 3,accessibilityLabel: ‘消息’},{id: ‘mine’,text: ‘我的’,icon: $r(‘app.media.ic_mine’),activeIcon: $r(‘app.media.ic_mine_fill’),badge: 99,accessibilityLabel: ‘我的’}];aboutToAppear() {// 注册所有Tabthis.tabs.forEach(tab => {this.tabManager.registerTab(tab);});// 设置初始Tab this.tabManager.switchTab('home'); // 监听Tab变化 this.tabManager.onTabChange((tabId: string) => { this.selectedTab = tabId; this.currentPage = this.tabs.findIndex(tab => tab.id === tabId); }); hilog.info(0x0000, 'CustomTabBarPage', 'TabBar页面初始化完成');}aboutToDisappear() {this.tabManager.destroy();}private switchTheme(): void {this.themeMode = this.themeMode === ‘light’ ? ‘dark’ : ‘light’;this.tabManager.setTheme(this.themeMode);}private switchTabBarType(): void {this.showBulgeTabBar = !this.showBulgeTabBar;}private updateBadge(tabId: string): void {const currentBadge = this.badgeCounts.get(tabId) || 0;const newBadge = currentBadge + 1;this.badgeCounts.set(tabId, newBadge);this.tabManager.updateBadge(tabId, newBadge > 99 ? ‘99+’ : newBadge);}private clearBadge(tabId: string): void {this.badgeCounts.set(tabId, 0);this.tabManager.updateBadge(tabId, 0);}@Builderprivate buildControlPanel() {Column({ space: 16 }) {Text(‘自定义TabBar演示’).fontSize(20).fontWeight(FontWeight.Bold).fontColor(this.themeMode === ‘dark’ ? ‘#FFFFFF’ : ‘#000000’).margin({ bottom: 20 }) Row({ space: 12 }) { Button('切换主题') .layoutWeight(1) .height(40) .backgroundColor(this.themeMode === 'dark' ? '#4D94FF' : '#0066FF') .fontColor('#FFFFFF') .onClick(() => this.switchTheme()) Button(this.showBulgeTabBar ? '凹陷效果' : '凸起效果') .layoutWeight(1) .height(40) .backgroundColor('#34C759') .fontColor('#FFFFFF') .onClick(() => this.switchTabBarType()) } Row({ space: 12 }) { Button('消息+1') .layoutWeight(1) .height(40) .backgroundColor('#FF9500') .fontColor('#FFFFFF') .onClick(() => this.updateBadge('message')) Button('清空消息') .layoutWeight(1) .height(40) .backgroundColor('#FF3B30') .fontColor('#FFFFFF') .onClick(() => this.clearBadge('message')) } Row({ space: 12 }) { Button('我的+1') .layoutWeight(1) .height(40) .backgroundColor('#AF52DE') .fontColor('#FFFFFF') .onClick(() => this.updateBadge('mine')) Button('清空我的') .layoutWeight(1) .height(40) .backgroundColor('#FF2D55') .fontColor('#FFFFFF') .onClick(() => this.clearBadge('mine')) } Text(`当前Tab: ${this.selectedTab}`) .fontSize(16) .fontColor(this.themeMode === 'dark' ? '#AAAAAA' : '#666666') .margin({ top: 20 }) Text(`当前主题: ${this.themeMode === 'dark' ? '深色' : '浅色'}`) .fontSize(16) .fontColor(this.themeMode === 'dark' ? '#AAAAAA' : '#666666') Text(`TabBar类型: ${this.showBulgeTabBar ? '凸起效果' : '凹陷效果'}`) .fontSize(16) .fontColor(this.themeMode === 'dark' ? '#AAAAAA' : '#666666') } .width('100%') .padding(20) .backgroundColor(this.themeMode === 'dark' ? '#1C1C1E' : '#F8F8F8') .borderRadius(20) .margin({ left: 20, right: 20, top: 20 })}@Builderprivate buildContent() {TabContent() {// 首页TabContentItem() {Column() {this.buildControlPanel() Column({ space: 12 }) { Text('凸起TabBar特点') .fontSize(18) .fontWeight(FontWeight.Bold) .fontColor(this.themeMode === 'dark' ? '#FFFFFF' : '#000000') Text('• 中间按钮凸起设计,增强视觉层次') Text('• 凸起按钮带动画效果,提升交互反馈') Text('• 支持角标显示,包括数字和小红点') Text('• 平滑的切换动画,优化用户体验') Text('• 自适应主题,支持深色/浅色模式') } .width('100%') .padding(20) .backgroundColor(this.themeMode === 'dark' ? '#2C2C2E' : '#FFFFFF') .borderRadius(12) .margin({ top: 20, left: 20, right: 20 }) } .width('100%') .height('100%') .scrollable(ScrollDirection.Vertical) .scrollBar(BarState.Auto) } .tabBar(this.buildTabBar('首页', 'app.media.ic_home_fill')) // 发现页 TabContentItem() { Column({ space: 20 }) { Text('发现') .fontSize(24) .fontWeight(FontWeight.Bold) .fontColor(this.themeMode === 'dark' ? '#FFFFFF' : '#000000') Text('这是发现页面内容') .fontSize(16) .fontColor(this.themeMode === 'dark' ? '#AAAAAA' : '#666666') } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) } .tabBar(this.buildTabBar('发现', 'app.media.ic_explore_fill')) // 发布页 TabContentItem() { Column({ space: 20 }) { Text('发布') .fontSize(24) .fontWeight(FontWeight.Bold) .fontColor(this.themeMode === 'dark' ? '#FFFFFF' : '#000000') Text('这是发布页面内容') .fontSize(16) .fontColor(this.themeMode === 'dark' ? '#AAAAAA' : '#666666') } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) } .tabBar(this.buildTabBar('发布', 'app.media.ic_add_fill')) // 消息页 TabContentItem() { Column({ space: 20 }) { Text('消息') .fontSize(24) .fontWeight(FontWeight.Bold) .fontColor(this.themeMode === 'dark' ? '#FFFFFF' : '#000000') Text('这是消息页面内容') .fontSize(16) .fontColor(this.themeMode === 'dark' ? '#AAAAAA' : '#666666') } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) } .tabBar(this.buildTabBar('消息', 'app.media.ic_message_fill')) // 我的页 TabContentItem() { Column({ space: 20 }) { Text('我的') .fontSize(24) .fontWeight(FontWeight.Bold) .fontColor(this.themeMode === 'dark' ? '#FFFFFF' : '#000000') Text('这是我的页面内容') .fontSize(16) .fontColor(this.themeMode === 'dark' ? '#AAAAAA' : '#666666') } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) } .tabBar(this.buildTabBar('我的', 'app.media.ic_mine_fill')) } .vertical(false) .scrollable(true) .barPosition(BarPosition.End) .onChange((index: number) => { const tabId = this.tabs[index]?.id; if (tabId) { this.selectedTab = tabId; this.tabManager.switchTab(tabId); } })}@Builderprivate buildTabBar(text: string, icon: Resource) {Row() {Image(icon).width(20).height(20).objectFit(ImageFit.Contain).margin({ right: 6 }) Text(text) .fontSize(14) }}@Builderprivate buildCustomTabBar() {if (this.showBulgeTabBar) {// 凸起TabBarBulgeTabBar({onTabChange: (tabId: string) => {this.selectedTab = tabId;this.currentPage = this.tabs.findIndex(tab => tab.id === tabId);},selectedColor: this.themeMode === ‘dark’ ? ‘#4D94FF’ : ‘#0066FF’,normalColor: this.themeMode === ‘dark’ ? ‘#AAAAAA’ : ‘#666666’,backgroundColor: this.themeMode === ‘dark’ ? ‘#1C1C1E’ : ‘#FFFFFF’,bulgeColor: this.themeMode === ‘dark’ ? ‘#4D94FF’ : ‘#0066FF’,height: 60,bulgeHeight: 20})} else {// 凹陷TabBarIndentTabBar({onTabChange: (tabId: string) => {this.selectedTab = tabId;this.currentPage = this.tabs.findIndex(tab => tab.id === tabId);},selectedColor: this.themeMode === ‘dark’ ? ‘#4D94FF’ : ‘#0066FF’,normalColor: this.themeMode === ‘dark’ ? ‘#AAAAAA’ : ‘#666666’,backgroundColor: this.themeMode === ‘dark’ ? ‘#1C1C1E’ : ‘#FFFFFF’,borderColor: this.themeMode === ‘dark’ ? ‘#2C2C2E’ : ‘#F0F0F0’,height: 60,indentDepth: 10,showBorder: true})}}build() {Column() {// 主内容Column().layoutWeight(1).width(‘100%’).backgroundColor(this.themeMode === ‘dark’ ? ‘#000000’ : ‘#F8F8F8’) // 自定义TabBar this.buildCustomTabBar() } .width('100%') .height('100%') .backgroundColor(this.themeMode === 'dark' ? '#000000' : '#F8F8F8')}}步骤5实现了一个完整的演示页面,将前面所有的组件和功能整合在一起,展示自定义TabBar的实际应用效果。这个页面不仅是一个演示,也是一个功能完备的应用界面模板。技术架构总结核心特点双模式支持:凸起和凹陷两种设计风格完整动画:点击、长按、切换都有平滑动画主题适配:支持深色/浅色模式高度可配置:所有参数均可自定义良好性能:使用状态管理和动画优化使用方式// 使用凸起TabBarBulgeTabBar({onTabChange: (tabId) => console.log(tabId),height: 60,bulgeHeight: 20})// 使用凹陷TabBarIndentTabBar({onTabChange: (tabId) => console.log(tabId),height: 60,indentDepth: 10})这个案例提供了完整的自定义TabBar实现,可以直接集成到您的HarmonyOS应用中,或者作为学习和参考的模板。
-
案例概述1.1 问题背景在移动应用开发中,当用户在输入框中输入内容时,系统软键盘会自动从屏幕底部弹出。在手机等小屏幕设备上,软键盘通常会占据30%-50%的屏幕高度,导致以下问题:输入框被遮挡:用户看不到正在输入的内容操作按钮被遮挡:提交、下一步等关键按钮不可见页面布局混乱:未考虑键盘高度的固定布局会被打乱用户体验差:需要手动滚动或关闭键盘才能看到完整内容1.2 解决方案概述本案例提供完整的软键盘避让解决方案,通过以下核心机制实现:键盘事件监听:实时监听软键盘的显示和隐藏动态布局调整:根据键盘状态自动调整页面布局智能滚动定位:确保当前输入框始终在可视区域内平滑动画过渡:使用动画实现自然的布局变化边界情况处理:处理各种特殊场景和边缘情况实现步骤详解步骤1:定义数据模型和工具类目的:为软键盘避让功能建立完整的数据结构和配置体系,为后续功能实现提供类型安全和配置支持。核心功能说明:键盘信息管理:定义键盘状态的数据结构,包括高度、可见性、动画信息等输入框信息管理:管理页面上所有输入框的位置、状态和验证规则屏幕尺寸计算:精确计算不同设备的可用屏幕空间,考虑安全区域滚动状态管理:支持动画滚动和位置追踪统一配置管理:集中管理所有可调参数,便于维护和优化关键技术点:● 使用TypeScript接口确保类型安全● 考虑不同设备的屏幕差异(刘海屏、圆角屏、虚拟导航栏)● 配置参数分离,便于适配不同需求● 支持动画效果配置,提升用户体验步骤说明:首先定义软键盘避让功能所需的核心数据结构和配置类,这是整个系统的基础。步骤1实现代码:// KeyboardAvoidModels.etsexport interface KeyboardInfo {height: number; // 键盘的实际高度(像素)width: number; // 键盘的宽度(像素)isVisible: boolean; // 键盘当前是否可见appearanceTime: number; // 键盘显示的时间戳(用于性能分析)duration: number; // 键盘动画持续时间(毫秒)curve: number; // 键盘动画曲线类型}export interface InputFieldInfo {id: string; // 输入框的唯一标识符type: string; // 输入框类型(text, password, number, email等)placeholder: string; // 占位符文本isFocused: boolean; // 当前是否获得焦点position: { // 输入框在屏幕上的位置信息x: number; // 左上角X坐标y: number; // 左上角Y坐标width: number; // 宽度height: number; // 高度};required: boolean; // 是否为必填项validation?: (value: string) => boolean; // 自定义验证函数}export interface LayoutMetrics {screenWidth: number; // 屏幕的总宽度screenHeight: number; // 屏幕的总高度safeAreaTop: number; // 安全区域顶部偏移(状态栏、刘海等)safeAreaBottom: number; // 安全区域底部偏移(虚拟导航栏等)statusBarHeight: number; // 状态栏高度navigationBarHeight: number; // 导航栏高度availableHeight: number; // 实际可用的内容高度}export interface ScrollPosition {offsetY: number; // 当前的垂直滚动偏移量targetOffsetY: number; // 目标滚动偏移量(用于动画)isScrolling: boolean; // 是否正在执行滚动动画animationDuration: number; // 动画持续时间}export class KeyboardAvoidConfig {static readonly SCROLL_CONFIG = {duration: 300, // 滚动动画持续时间,300ms提供平滑体验curve: Curve.EaseOut, // 缓出动画曲线,结束时更自然offsetPadding: 20, // 偏移量内边距,确保输入框不被刚好遮挡minOffset: 0, // 最小滚动偏移,不能向上滚动smoothScrolling: true, // 启用平滑滚动,提升用户体验scrollBehavior: ‘smooth’ as const // 滚动行为类型};static readonly KEYBOARD_CONFIG = {defaultHeight: 300, // 默认键盘高度,用于初始化minHeight: 200, // 最小键盘高度,兼容小键盘maxHeight: 400, // 最大键盘高度,兼容大键盘detectInterval: 100, // 检测间隔,平衡性能和实时性animationDuration: 250, // 键盘动画持续时间enableAutoAdjust: true // 启用自动调整};static readonly INPUT_CONFIG = {focusAnimation: true, // 焦点切换动画highlightColor: ‘#0066FF20’, // 高亮颜色(带透明度)borderColor: ‘#E0E0E0’, // 默认边框颜色focusedBorderColor: ‘#0066FF’, // 获得焦点时的边框颜色errorBorderColor: ‘#FF3B30’, // 错误时的边框颜色padding: 16, // 内边距margin: 8 // 外边距};static readonly PERFORMANCE_CONFIG = {throttleDelay: 16, // 节流延迟,约60fps(1000/60≈16.67)debounceDelay: 100, // 防抖延迟,处理连续快速操作enableCache: true, // 启用缓存,提高重复计算性能maxCacheSize: 10, // 最大缓存数量,避免内存泄漏enableLogging: false // 启用日志,调试时开启};}步骤2:实现键盘避让管理器目的:实现整个键盘避让功能的核心协调者,采用单例模式管理所有键盘相关状态,协调各个组件工作。核心功能说明:单例管理:确保全局只有一个管理器实例,统一状态管理事件监听:监听系统键盘事件、窗口变化事件输入框管理:注册、更新、管理所有输入框状态智能计算:根据键盘状态和输入框位置计算需要滚动的距离动画控制:实现平滑的滚动动画效果回调机制:支持多组件监听键盘事件关键技术点:● 使用HarmonyOS的keyboard API监听键盘事件● 实时计算输入框位置和屏幕可用空间● 智能判断是否需要滚动以及滚动距离● 缓动函数实现自然的动画效果● 节流防抖优化性能步骤说明:这是整个键盘避让功能的核心,负责协调所有组件的工作,采用单例模式确保全局状态一致。步骤2实现代码:// KeyboardAvoidManager.etsimport { keyboard } from ‘@kit.ArkUI’;import { display } from ‘@kit.ArkUI’;import { window } from ‘@kit.ArkUI’;import { hilog } from ‘@kit.PerformanceAnalysisKit’;export class KeyboardAvoidManager {private static instance: KeyboardAvoidManager;private keyboardHeight: number = 0;private isKeyboardVisible: boolean = false;private lastKeyboardHeight: number = 0;private scrollOffset: number = 0;private targetOffset: number = 0;private isAnimating: boolean = false;private onKeyboardShowCallbacks: Array<(height: number) => void> = [];private onKeyboardHideCallbacks: Array<() => void> = [];private onScrollCallbacks: Array<(offset: number) => void> = [];private inputFields: Map<string, InputFieldInfo> = new Map();private focusedInputId: string | null = null;private screenMetrics: LayoutMetrics = {screenWidth: 0,screenHeight: 0,safeAreaTop: 0,safeAreaBottom: 0,statusBarHeight: 0,navigationBarHeight: 0,availableHeight: 0};private constructor() {this.initialize();}public static getInstance(): KeyboardAvoidManager {if (!KeyboardAvoidManager.instance) {KeyboardAvoidManager.instance = new KeyboardAvoidManager();}return KeyboardAvoidManager.instance;}private initialize(): void {this.initScreenMetrics();this.setupKeyboardListeners();this.setupWindowListeners();hilog.info(0x0000, ‘KeyboardAvoidManager’, ‘键盘避让管理器初始化完成’);}private initScreenMetrics(): void {try {const displayInfo = display.getDefaultDisplaySync();this.screenMetrics.screenWidth = displayInfo.width;this.screenMetrics.screenHeight = displayInfo.height; const windowClass = window.getLastWindow(getContext(this) as common.UIAbilityContext); if (windowClass) { const avoidArea = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM); this.screenMetrics.safeAreaTop = avoidArea.topRect.height; this.screenMetrics.safeAreaBottom = avoidArea.bottomRect.height; } this.screenMetrics.availableHeight = this.screenMetrics.screenHeight - this.screenMetrics.safeAreaTop - this.screenMetrics.safeAreaBottom; hilog.info(0x0000, 'KeyboardAvoidManager', `屏幕信息: 宽度=${this.screenMetrics.screenWidth}, 高度=${this.screenMetrics.screenHeight}, 可用高度=${this.screenMetrics.availableHeight}`); } catch (error) { hilog.error(0x0000, 'KeyboardAvoidManager', '获取屏幕信息失败: ' + JSON.stringify(error)); }}private setupKeyboardListeners(): void {try {keyboard.onAppear(() => {hilog.info(0x0000, ‘KeyboardAvoidManager’, ‘键盘显示’);this.handleKeyboardAppear();}); keyboard.onDisappear(() => { hilog.info(0x0000, 'KeyboardAvoidManager', '键盘隐藏'); this.handleKeyboardDisappear(); }); keyboard.onChange((height: number) => { hilog.info(0x0000, 'KeyboardAvoidManager', `键盘高度变化: ${height}`); this.handleKeyboardHeightChange(height); }); hilog.info(0x0000, 'KeyboardAvoidManager', '键盘事件监听器设置完成'); } catch (error) { hilog.error(0x0000, 'KeyboardAvoidManager', '设置键盘监听器失败: ' + JSON.stringify(error)); }}private setupWindowListeners(): void {try {const context = getContext(this) as common.UIAbilityContext;const windowClass = window.getLastWindow(context); if (windowClass) { windowClass.on('windowSizeChange', (windowSize: window.Size) => { this.handleWindowSizeChange(windowSize); }); windowClass.on('windowFocus', () => { this.handleWindowFocus(); }); windowClass.on('windowUnfocus', () => { this.handleWindowUnfocus(); }); } } catch (error) { hilog.error(0x0000, 'KeyboardAvoidManager', '设置窗口监听器失败: ' + JSON.stringify(error)); }}private handleKeyboardAppear(): void {this.isKeyboardVisible = true;if (this.keyboardHeight === 0) { this.keyboardHeight = KeyboardAvoidConfig.KEYBOARD_CONFIG.defaultHeight; } hilog.info(0x0000, 'KeyboardAvoidManager', `键盘显示,高度: ${this.keyboardHeight}`); this.onKeyboardShowCallbacks.forEach(callback => { callback(this.keyboardHeight); }); if (this.focusedInputId) { this.adjustForFocusedInput(); }}private handleKeyboardDisappear(): void {this.isKeyboardVisible = false;this.lastKeyboardHeight = this.keyboardHeight;this.keyboardHeight = 0;hilog.info(0x0000, 'KeyboardAvoidManager', '键盘隐藏'); this.onKeyboardHideCallbacks.forEach(callback => { callback(); }); this.scrollToPosition(0, true);}private handleKeyboardHeightChange(height: number): void {if (height > 0 && height !== this.keyboardHeight) {this.keyboardHeight = height;hilog.info(0x0000, ‘KeyboardAvoidManager’, 键盘高度更新: ${height}); if (this.isKeyboardVisible && this.focusedInputId) { this.adjustForFocusedInput(); } }}private handleWindowSizeChange(windowSize: window.Size): void {this.screenMetrics.screenWidth = windowSize.width;this.screenMetrics.screenHeight = windowSize.height;this.screenMetrics.availableHeight = this.screenMetrics.screenHeight - this.screenMetrics.safeAreaTop - this.screenMetrics.safeAreaBottom; hilog.info(0x0000, 'KeyboardAvoidManager', `窗口尺寸变化: ${windowSize.width}x${windowSize.height}, 可用高度: ${this.screenMetrics.availableHeight}`); if (this.isKeyboardVisible && this.focusedInputId) { this.adjustForFocusedInput(); }}private adjustForFocusedInput(): void {if (!this.focusedInputId || !this.inputFields.has(this.focusedInputId)) {return;}const inputInfo = this.inputFields.get(this.focusedInputId)!; const inputBottom = inputInfo.position.y + inputInfo.position.height; const visibleAreaTop = this.screenMetrics.safeAreaTop; const visibleAreaBottom = this.screenMetrics.availableHeight - this.keyboardHeight; if (inputBottom > visibleAreaBottom) { const requiredOffset = inputBottom - visibleAreaBottom + KeyboardAvoidConfig.SCROLL_CONFIG.offsetPadding; hilog.info(0x0000, 'KeyboardAvoidManager', `输入框被遮挡,需要滚动偏移: ${requiredOffset}, 输入框底部: ${inputBottom}, 可视区域底部: ${visibleAreaBottom}`); this.scrollToPosition(requiredOffset, true); } else if (inputInfo.position.y < visibleAreaTop) { const requiredOffset = inputInfo.position.y - visibleAreaTop - KeyboardAvoidConfig.SCROLL_CONFIG.offsetPadding; hilog.info(0x0000, 'KeyboardAvoidManager', `输入框在上方,需要滚动偏移: ${requiredOffset}`); this.scrollToPosition(requiredOffset, true); } else { hilog.info(0x0000, 'KeyboardAvoidManager', '输入框在可视区域内,不需要滚动'); }}private scrollToPosition(offset: number, animated: boolean = true): void {const maxOffset = this.calculateMaxScrollOffset();const targetOffset = Math.max(KeyboardAvoidConfig.SCROLL_CONFIG.minOffset, Math.min(offset, maxOffset));if (targetOffset === this.scrollOffset) { return; } this.targetOffset = targetOffset; if (animated) { this.startScrollAnimation(); } else { this.scrollOffset = targetOffset; this.notifyScrollCallbacks(); } hilog.info(0x0000, 'KeyboardAvoidManager', `滚动到位置: ${targetOffset}, 动画: ${animated}, 最大偏移: ${maxOffset}`);}private calculateMaxScrollOffset(): number {return 1000;}private startScrollAnimation(): void {if (this.isAnimating) {return;}this.isAnimating = true; const startOffset = this.scrollOffset; const endOffset = this.targetOffset; const duration = KeyboardAvoidConfig.SCROLL_CONFIG.duration; const startTime = Date.now(); const animate = () => { const currentTime = Date.now(); const elapsed = currentTime - startTime; const progress = Math.min(elapsed / duration, 1); const easedProgress = this.easeOutCubic(progress); this.scrollOffset = startOffset + (endOffset - startOffset) * easedProgress; this.notifyScrollCallbacks(); if (progress < 1) { setTimeout(animate, 16); } else { this.scrollOffset = endOffset; this.isAnimating = false; this.notifyScrollCallbacks(); hilog.info(0x0000, 'KeyboardAvoidManager', '滚动动画完成'); } }; animate();}private easeOutCubic(t: number): number {return 1 - Math.pow(1 - t, 3);}public onKeyboardShow(callback: (height: number) => void): void {this.onKeyboardShowCallbacks.push(callback);}public onKeyboardHide(callback: () => void): void {this.onKeyboardHideCallbacks.push(callback);}public onScroll(callback: (offset: number) => void): void {this.onScrollCallbacks.push(callback);}public registerInputField(id: string, info: InputFieldInfo): void {this.inputFields.set(id, info);hilog.info(0x0000, ‘KeyboardAvoidManager’, 注册输入框: ${id}, 位置: (${info.position.x}, ${info.position.y}));}public updateInputFocus(id: string, isFocused: boolean): void {if (this.inputFields.has(id)) {const info = this.inputFields.get(id)!;info.isFocused = isFocused; if (isFocused) { this.focusedInputId = id; hilog.info(0x0000, 'KeyboardAvoidManager', `输入框获得焦点: ${id}`); if (this.isKeyboardVisible) { this.adjustForFocusedInput(); } } else if (this.focusedInputId === id) { this.focusedInputId = null; hilog.info(0x0000, 'KeyboardAvoidManager', `输入框失去焦点: ${id}`); } }}public updateInputPosition(id: string, x: number, y: number, width: number, height: number): void {if (this.inputFields.has(id)) {const info = this.inputFields.get(id)!;info.position = { x, y, width, height }; if (this.focusedInputId === id && this.isKeyboardVisible) { this.adjustForFocusedInput(); } }}public getScrollOffset(): number {return this.scrollOffset;}public isKeyboardShowing(): boolean {return this.isKeyboardVisible;}public getKeyboardHeight(): number {return this.keyboardHeight;}public getScreenMetrics(): LayoutMetrics {return { …this.screenMetrics };}public triggerAvoidance(inputId: string): void {if (this.inputFields.has(inputId)) {this.focusedInputId = inputId;this.adjustForFocusedInput();}}public destroy(): void {this.onKeyboardShowCallbacks = [];this.onKeyboardHideCallbacks = [];this.onScrollCallbacks = [];this.inputFields.clear();this.focusedInputId = null;hilog.info(0x0000, ‘KeyboardAvoidManager’, ‘键盘避让管理器已销毁’);}}步骤3:实现自适应Scroll容器组件目的:创建一个智能的Scroll容器组件,能够根据键盘状态自动调整布局和滚动行为,为用户提供无缝的键盘避让体验。核心功能说明:键盘状态响应:监听键盘事件,动态调整底部内边距滚动控制:管理滚动行为,支持外部控制和内部滚动内容适配:根据内容高度自动调整滚动范围动画支持:提供平滑的布局过渡效果配置灵活:支持启用/禁用键盘避让、动画等关键技术点:● 使用ScrollController精确控制滚动位置● 动态padding实现键盘避让效果● 支持内外部滚动同步● 边缘弹性效果增强用户体验● 区域变化监听优化性能步骤说明:这是一个智能的Scroll容器,能够根据键盘状态自动调整,它是键盘避让功能的主要承载组件。步骤3实现代码:// AdaptiveScroll.ets@Componentexport struct AdaptiveScroll {private keyboardManager: KeyboardAvoidManager = KeyboardAvoidManager.getInstance();private scrollController: ScrollController = new ScrollController();@State contentHeight: number = 0;@State scrollOffset: number = 0;@State keyboardHeight: number = 0;@State isScrolling: boolean = false;@BuilderParam content: () => void;@Prop scrollEnabled: boolean = true;@Prop avoidKeyboard: boolean = true;@Prop animationEnabled: boolean = true;@Prop paddingBottom: number = 0;@Prop maxOffset: number = 1000;aboutToAppear() {this.setupKeyboardListeners();this.setupScrollController();hilog.info(0x0000, ‘AdaptiveScroll’, ‘自适应Scroll容器初始化完成’);}aboutToDisappear() {this.cleanup();}private setupKeyboardListeners(): void {if (!this.avoidKeyboard) {return;}this.keyboardManager.onKeyboardShow((height: number) => { hilog.info(0x0000, 'AdaptiveScroll', `键盘显示,高度: ${height}`); this.keyboardHeight = height; this.adjustForKeyboard(height, true); }); this.keyboardManager.onKeyboardHide(() => { hilog.info(0x0000, 'AdaptiveScroll', '键盘隐藏'); this.keyboardHeight = 0; this.adjustForKeyboard(0, true); }); this.keyboardManager.onScroll((offset: number) => { this.handleExternalScroll(offset); });}private setupScrollController(): void {// 可以在这里配置滚动控制器的行为}private adjustForKeyboard(keyboardHeight: number, animated: boolean): void {const targetPaddingBottom = keyboardHeight > 0 ?keyboardHeight + this.paddingBottom :this.paddingBottom;hilog.info(0x0000, 'AdaptiveScroll', `调整键盘避让: 键盘高度=${keyboardHeight}, 目标底部内边距=${targetPaddingBottom}`);}private handleExternalScroll(offset: number): void {if (!this.avoidKeyboard || !this.scrollEnabled) {return;}const targetOffset = Math.max(0, Math.min(offset, this.maxOffset)); if (targetOffset !== this.scrollOffset) { this.scrollOffset = targetOffset; this.isScrolling = true; if (this.scrollController && this.animationEnabled) { this.scrollController.scrollTo({ xOffset: 0, yOffset: targetOffset, animation: { duration: KeyboardAvoidConfig.SCROLL_CONFIG.duration, curve: KeyboardAvoidConfig.SCROLL_CONFIG.curve } }); } setTimeout(() => { this.isScrolling = false; }, KeyboardAvoidConfig.SCROLL_CONFIG.duration); hilog.info(0x0000, 'AdaptiveScroll', `外部滚动到: ${targetOffset}`); }}private onScroll(event: ScrollEvent): void {if (!this.scrollEnabled || this.isScrolling) {return;}const offsetY = event.offsetY; this.scrollOffset = offsetY; hilog.debug(0x0000, 'AdaptiveScroll', `滚动位置: ${offsetY}`);}private cleanup(): void {// 清理监听器等资源}build() {const containerHeight = ‘100%’;const bottomPadding = this.keyboardHeight > 0 ?this.keyboardHeight + this.paddingBottom :this.paddingBottom;Scroll(this.scrollController) { Column() { this.content() } .width('100%') .onAreaChange((oldValue, newValue) => { this.contentHeight = newValue.height; }) } .width('100%') .height(containerHeight) .scrollable(this.scrollEnabled ? ScrollDirection.Vertical : ScrollDirection.None) .scrollBar(BarState.Auto) .edgeEffect(EdgeEffect.Spring) .onScroll((event: ScrollEvent) => { this.onScroll(event); }) .onScrollFrameBegin((offset: number, state: ScrollState) => { if (!this.scrollEnabled) { return { offsetRemain: 0 }; } return { offsetRemain: offset }; }) .padding({ bottom: bottomPadding })}}步骤4:实现智能输入框组件目的:创建一个增强的输入框组件,具备键盘避让能力,能够与键盘管理器协同工作,提供完整的输入体验。核心功能说明:自动注册:自动向键盘管理器注册输入框信息位置追踪:实时更新输入框在屏幕上的位置焦点管理:正确处理获得焦点和失去焦点事件输入验证:支持实时验证和错误提示样式定制:提供丰富的样式配置选项键盘协同:与键盘管理器通信,触发避让逻辑关键技术点:● 自动生成唯一ID标识每个输入框● 实时监听区域变化更新位置信息● 与键盘管理器双向通信● 支持多种输入类型和验证规则● 可定制的视觉反馈效果步骤说明:这是一个增强的输入框组件,支持键盘避让功能,能够与键盘管理器通信,提供焦点和位置信息。步骤4实现代码:// SmartInputField.ets@Componentexport struct SmartInputField {private keyboardManager: KeyboardAvoidManager = KeyboardAvoidManager.getInstance();private inputId: string = ‘’;private inputRef: TextInputController = new TextInputController();@State inputPosition: {x: number;y: number;width: number;height: number;} = { x: 0, y: 0, width: 0, height: 0 };@State inputValue: string = ‘’;@State isFocused: boolean = false;@State hasError: boolean = false;@Prop label: string = ‘’;@Prop placeholder: string = ‘’;@Prop value: string = ‘’;@Prop type: InputType = InputType.Normal;@Prop required: boolean = false;@Prop disabled: boolean = false;@Prop maxLength: number = 1000;@Prop validation?: (value: string) => boolean;@Prop onChange?: (value: string) => void;@Prop onFocus?: () => void;@Prop onBlur?: () => void;@Prop backgroundColor: ResourceColor = ‘#FFFFFF’;@Prop borderColor: ResourceColor = ‘#E0E0E0’;@Prop focusedBorderColor: ResourceColor = ‘#0066FF’;@Prop errorBorderColor: ResourceColor = ‘#FF3B30’;@Prop textColor: ResourceColor = ‘#000000’;@Prop placeholderColor: ResourceColor = ‘#999999’;@Prop fontSize: number = 16;@Prop borderRadius: number = 8;@Prop padding: Padding | number = 12;@Prop margin: Margin | number = 0;aboutToAppear() {this.inputId = input_${Date.now()}_${Math.random().toString(36).substr(2, 9)};this.inputValue = this.value;this.registerInputField();hilog.info(0x0000, ‘SmartInputField’, 智能输入框初始化完成,ID: ${this.inputId});}aboutToDisappear() {// 清理注册}private registerInputField(): void {const inputInfo: InputFieldInfo = {id: this.inputId,type: this.type,placeholder: this.placeholder,isFocused: this.isFocused,position: this.inputPosition,required: this.required,validation: this.validation};this.keyboardManager.registerInputField(this.inputId, inputInfo);}private updateInputPosition(): void {setTimeout(() => {const newPosition = {x: 0,y: this.inputPosition.y,width: 300,height: 50}; if (JSON.stringify(newPosition) !== JSON.stringify(this.inputPosition)) { this.inputPosition = newPosition; this.keyboardManager.updateInputPosition( this.inputId, newPosition.x, newPosition.y, newPosition.width, newPosition.height ); hilog.debug(0x0000, 'SmartInputField', `输入框位置更新: ID=${this.inputId}, 位置=(${newPosition.x}, ${newPosition.y})`); } }, 100);}private onInputFocus(): void {if (this.disabled) {return;}this.isFocused = true; this.keyboardManager.updateInputFocus(this.inputId, true); this.updateInputPosition(); this.onFocus?.(); hilog.info(0x0000, 'SmartInputField', `输入框获得焦点: ${this.inputId}`); this.keyboardManager.triggerAvoidance(this.inputId);}private onInputBlur(): void {this.isFocused = false;this.keyboardManager.updateInputFocus(this.inputId, false);this.onBlur?.();hilog.info(0x0000, ‘SmartInputField’, 输入框失去焦点: ${this.inputId});this.validateInput();}private onInputChange(value: string): void {this.inputValue = value;this.onChange?.(value);this.validateInput();}private validateInput(): void {if (!this.validation && !this.required) {this.hasError = false;return;}let isValid = true; if (this.required && !this.inputValue.trim()) { isValid = false; } if (isValid && this.validation && !this.validation(this.inputValue)) { isValid = false; } this.hasError = !isValid;}private getBorderColor(): ResourceColor {if (this.hasError) {return this.errorBorderColor;}if (this.isFocused) {return this.focusedBorderColor;}return this.borderColor;}private getBorderWidth(): number {if (this.hasError || this.isFocused) {return 2;}return 1;}@Builderprivate buildLabel() {if (this.label) {Text(this.label).fontSize(this.fontSize - 2).fontColor(this.hasError ? this.errorBorderColor : ‘#666666’).fontWeight(FontWeight.Medium).margin({ bottom: 8 })}}@Builderprivate buildInput() {Column() {TextInput({ controller: this.inputRef, text: this.inputValue, placeholder: this.placeholder }).width(‘100%’).height(50).fontSize(this.fontSize).fontColor(this.disabled ? ‘#999999’ : this.textColor).placeholderColor(this.placeholderColor).placeholderFont({ size: this.fontSize, weight: FontWeight.Normal }).caretColor(this.focusedBorderColor).type(this.type).maxLength(this.maxLength).enterKeyType(EnterKeyType.Next).enableKeyboardOnFocus(true).selectedBackgroundColor(‘#0066FF10’).onChange((value: string) => {this.onInputChange(value);}).onEditChange((isEditing: boolean) => {if (isEditing) {this.onInputFocus();} else {this.onInputBlur();}}).onSubmit(() => {this.onInputBlur();}).enabled(!this.disabled)}.backgroundColor(this.disabled ? ‘#F8F8F8’ : this.backgroundColor).border({width: this.getBorderWidth(),color: this.getBorderColor()}).borderRadius(this.borderRadius).padding(this.padding).width(‘100%’)}@Builderprivate buildError() {if (this.hasError) {Row({ space: 4 }) {Image($r(‘app.media.ic_error’)).width(16).height(16).objectFit(ImageFit.Contain) Text(this.required && !this.inputValue.trim() ? '此项为必填项' : '输入内容无效') .fontSize(12) .fontColor(this.errorBorderColor) } .width('100%') .margin({ top: 4 }) }}build() {Column() {this.buildLabel()this.buildInput()this.buildError()}.width(‘100%’).margin(this.margin).onAreaChange((oldValue, newValue) => {this.inputPosition = {x: newValue.globalPosition.x,y: newValue.globalPosition.y,width: newValue.width,height: newValue.height}; this.keyboardManager.updateInputPosition( this.inputId, this.inputPosition.x, this.inputPosition.y, this.inputPosition.width, this.inputPosition.height ); })}}步骤5:实现登录页面示例目的:提供一个完整的登录页面示例,展示键盘避让功能的实际应用效果,包含完整的用户交互流程。核心功能说明:表单布局:标准的登录表单结构,包含用户名、密码输入框键盘避让集成:使用自适应Scroll容器和智能输入框用户交互:完整的登录流程,包括输入、验证、提交视觉反馈:密码强度提示、错误提示、加载状态辅助功能:记住我、忘记密码、其他登录方式关键技术点:● 组合使用AdaptiveScroll和SmartInputField● 实现密码强度实时计算● 处理表单提交和验证● 提供多种登录方式选项● 完整的用户体验设计步骤说明:实现一个完整的登录页面示例,展示键盘避让功能的实际应用效果。步骤5实现代码:// KeyboardAvoidLoginPage.ets@Entry@Componentexport struct KeyboardAvoidLoginPage {@State username: string = ‘’;@State password: string = ‘’;@State rememberMe: boolean = false;@State isLoading: boolean = false;@State errorMessage: string = ‘’;@State showPassword: boolean = false;private keyboardManager: KeyboardAvoidManager = KeyboardAvoidManager.getInstance();private scrollController: ScrollController = new ScrollController();private usernameInputRef: TextInputController = new TextInputController();private passwordInputRef: TextInputController = new TextInputController();aboutToAppear() {hilog.info(0x0000, ‘KeyboardAvoidLoginPage’, ‘登录页面初始化完成’);}private async handleLogin() {if (this.isLoading) {return;}if (!this.username.trim()) { this.errorMessage = '请输入用户名'; return; } if (!this.password.trim()) { this.errorMessage = '请输入密码'; return; } this.isLoading = true; this.errorMessage = ''; try { await new Promise(resolve => setTimeout(resolve, 1500)); this.showLoginSuccess(); } catch (error) { this.errorMessage = '登录失败,请稍后重试'; } finally { this.isLoading = false; }}private showLoginSuccess() {prompt.showToast({message: ‘登录成功’,duration: 2000});}@Builderprivate buildHeader() {Column({ space: 16 }) {Image($r(‘app.media.app_logo’)).width(80).height(80).borderRadius(40).objectFit(ImageFit.Contain) Column({ space: 8 }) { Text('欢迎回来') .fontSize(24) .fontColor('#000000') .fontWeight(FontWeight.Bold) Text('请输入您的账号信息登录') .fontSize(14) .fontColor('#666666') } } .width('100%') .padding({ top: 40, bottom: 40 }) .alignItems(HorizontalAlign.Center)}@Builderprivate buildForm() {Column({ space: 24 }) {SmartInputField({label: ‘用户名’,placeholder: ‘请输入用户名/邮箱/手机号’,value: this.username,required: true,onChange: (value: string) => {this.username = value;this.errorMessage = ‘’;}}) Column({ space: 8 }) { Row({ space: 8 }) { SmartInputField({ label: '密码', placeholder: '请输入密码', value: this.password, type: this.showPassword ? InputType.Normal : InputType.Password, required: true, onChange: (value: string) => { this.password = value; this.errorMessage = ''; } }) .layoutWeight(1) Button() { Image(this.showPassword ? $r('app.media.ic_visibility_off') : $r('app.media.ic_visibility')) .width(20) .height(20) } .width(40) .height(40) .backgroundColor('#F8F8F8') .borderRadius(8) .onClick(() => { this.showPassword = !this.showPassword; }) } if (this.password.length > 0) { this.buildPasswordStrength() } } Row() { Row({ space: 8 }) { Checkbox() .select(this.rememberMe) .selectedColor('#0066FF') .onChange((value: boolean) => { this.rememberMe = value; }) Text('记住我') .fontSize(14) .fontColor('#666666') } Blank() Text('忘记密码?') .fontSize(14) .fontColor('#0066FF') .onClick(() => { // 跳转到忘记密码页面 }) } .width('100%') if (this.errorMessage) { Row({ space: 8 }) { Image($r('app.media.ic_error')) .width(16) .height(16) Text(this.errorMessage) .fontSize(14) .fontColor('#FF3B30') } .width('100%') .padding(12) .backgroundColor('#FF3B3010') .borderRadius(8) } Button('登录') { if (this.isLoading) { LoadingProgress() .width(20) .height(20) .color('#FFFFFF') } else { Text('登录') .fontSize(16) .fontColor('#FFFFFF') } } .width('100%') .height(50) .backgroundColor('#0066FF') .borderRadius(25) .enabled(!this.isLoading && this.username.trim() && this.password.trim()) .onClick(() => { this.handleLogin(); }) Row({ space: 4 }) { Text('还没有账号?') .fontSize(14) .fontColor('#666666') Text('立即注册') .fontSize(14) .fontColor('#0066FF') .fontWeight(FontWeight.Medium) .onClick(() => { // 跳转到注册页面 }) } .margin({ top: 20 }) }}@Builderprivate buildPasswordStrength() {const strength = this.calculatePasswordStrength(this.password);Column({ space: 4 }) { Row() { ForEach([1, 2, 3, 4], (index: number) => { Column() .width('25%') .height(4) .margin({ right: 4 }) .backgroundColor(index <= strength.level ? strength.color : '#F0F0F0') .borderRadius(2) }) } .width('100%') Text(strength.text) .fontSize(12) .fontColor(strength.color) }}private calculatePasswordStrength(password: string): { level: number; text: string; color: ResourceColor } {if (password.length === 0) {return { level: 0, text: ‘’, color: ‘#666666’ };}let score = 0; if (password.length >= 8) score += 1; if (password.length >= 12) score += 1; if (/[a-z]/.test(password)) score += 1; if (/[A-Z]/.test(password)) score += 1; if (/[0-9]/.test(password)) score += 1; if (/[^a-zA-Z0-9]/.test(password)) score += 1; if (score <= 2) { return { level: 1, text: '密码强度:弱', color: '#FF3B30' }; } else if (score <= 4) { return { level: 2, text: '密码强度:中', color: '#FF9500' }; } else { return { level: 4, text: '密码强度:强', color: '#34C759' }; }}@Builderprivate buildFooter() {Column({ space: 20 }) {Row() {Divider().width(‘30%’).color(‘#E0E0E0’) Text('其他登录方式') .fontSize(12) .fontColor('#999999') .margin({ left: 12, right: 12 }) Divider() .width('30%') .color('#E0E0E0') } .width('100%') Row({ space: 20 }) { Button() { Image($r('app.media.ic_wechat')) .width(24) .height(24) } .width(50) .height(50) .backgroundColor('#07C16010') .borderRadius(25) Button() { Image($r('app.media.ic_qq')) .width(24) .height(24) } .width(50) .height(50) .backgroundColor('#0066FF10') .borderRadius(25) Button() { Image($r('app.media.ic_weibo')) .width(24) .height(24) } .width(50) .height(50) .backgroundColor('#FF3B3010') .borderRadius(25) } }}build() {AdaptiveScroll({avoidKeyboard: true,animationEnabled: true,paddingBottom: 20}) {Column({ space: 0 }) {this.buildHeader()this.buildForm().padding({ left: 20, right: 20 })this.buildFooter().padding({ top: 40, bottom: 20, left: 20, right: 20 })}}.width(‘100%’).height(‘100%’).backgroundColor(‘#FFFFFF’)}}3. 技术架构总结这个完整的软键盘避让案例展示了如何在HarmonyOS应用中实现专业的键盘避让功能,提供了从底层数据模型到上层应用界面的完整解决方案,可以显著提升应用的输入体验。
上滑加载中
推荐直播
-
华为云码道 × 仓颉编程:工程化AI编码探索2026/05/27 周三 19:00-21:00
刘俊杰-华为云仓颉语言专家/李炎-华为云码道技术专家/王智鹏-OpenCangjie开源社区发起人
本场直播围绕华为云仓颉语言与华为云码道的深度结合,展示华为云智能编程从零基础到高效落地的完整生态能力。以华为云码道为引擎,仓颉语言为载体,带给大家日常提效、趣味创新到极速量产的开发体验。
回顾中
热门标签