鸿蒙应用开发实战【99】— 用户体验优化加载动画与触觉反馈

本文是「号码助手全栈开发系列」第 99 篇,持续更新中…
开源社区:https://openharmonycrossplatform.csdn.net


前言

优秀的用户体验不仅仅是功能完善——加载状态、转场动画和触觉反馈同样重要。号码助手项目在多个场景中应用了动画和 Loading 状态,本篇总结 ArkUI 中的动画最佳实践。

本篇涵盖:AppAnimations 动画令牌、animateTo 隐式动画、页面转场动画、LoadingProgress 加载状态、@ohos.vibrator 触觉反馈、Skeleton 骨架屏。

在这里插入图片描述


一、动画令牌系统

文件common/theme/AppAnimations.ets

import curves from '@ohos.curves'

export class AppAnimations {
  // 三档时长
  static readonly DURATION_FAST: number = 160    // 快速(hover/按下)
  static readonly DURATION_NORMAL: number = 260  // 常规(转场/展开)
  static readonly DURATION_SLOW: number = 380    // 慢速(强调动画)

  // 两条曲线
  static readonly CURVE_STANDARD: ICurve =
    curves.cubicBezierCurve(0.2, 0, 0, 1)      // 平滑缓入缓出
  static readonly CURVE_SPRING: ICurve =
    curves.springMotion(0.35, 0.9)              // 弹簧效果

  // 快捷方法
  static standard(duration: number = AppAnimations.DURATION_NORMAL): AnimateParam {
    return {
      duration,
      curve: AppAnimations.CURVE_STANDARD
    }
  }
}

二、animateTo 隐式动画

ArkUI 最常用的动画方式是通过 animateTo 包裹状态变化:

// FAB 展开/收起动画
private toggleFab(): void {
  animateTo(AppAnimations.standard(AppAnimations.DURATION_NORMAL), () => {
    this.fabExpanded = !this.fabExpanded
  })
}

// 列表项出现动画
ForEach(this.rows, (item: AppRow, index: number) => {
  AppListItem({ item, selected: this.isSelected(item.binding.id!) })
    .opacity(this.rows.indexOf(item) < this.visibleCount ? 1 : 0)
    .translate({
      y: this.rows.indexOf(item) < this.visibleCount ? 0 : 20
    })
}, (item: AppRow) => item.binding.id!.toString())

动画参数对照

参数 AppAnimations 值 效果
duration 160ms FASTE’ 反馈型(按钮按下)
duration 260ms NORMAL 转场型(页面切换)
duration 380ms SLOW 展示型(强调动画)
cubicBezier(0.2,0,0,1) CURVE_STANDARD 自然平滑
springMotion(0.35,0.9) CURVE_SPRING 弹性回弹

三、页面转场动画

通过 @EnterTransition@ExitTransition 定义页面切换动画:

@Entry
@Component
struct BackupPage {
  // 进入:从右侧滑入 + 透明度
  @EnterTransition
  enterTransition: TransitionEffect =
    TransitionEffect.slide(SlideEffect.Right)
      .combine(TransitionEffect.opacity(1))

  // 退出:从下侧滑出
  @ExitTransition
  exitTransition: TransitionEffect =
    TransitionEffect.slide(SlideEffect.Bottom)
}

或者使用 pageTransition API:

// 页面级转场
pageTransition() {
  PageTransitionEnter({ duration: 260, curve: AppAnimations.CURVE_STANDARD })
    .slide(SlideEffect.Right)
    .translate({ x: 100 })
  PageTransitionLeave({ duration: 160 })
    .slide(SlideEffect.Left)
}

四、加载状态(LoadingProgress)

4.1 全屏 Loading

// HomePage 加载中展示
if (this.loading) {
  Column() {
    LoadingProgress()
      .width(48)
      .height(48)
      .color(AppColors.PRIMARY)
    Text('加载中...')
      .fontSize(14)
      .fontColor(AppColors.TEXT_2)
      .margin({ top: 12 })
  }
  .width('100%')
  .height('100%')
  .justifyContent(FlexAlign.Center)
}

4.2 按钮内联 Loading

// 备份按钮 Loading
Row() {
  if (this.backing) {
    LoadingProgress()
      .width(20)
      .height(20)
      .color('#FFFFFF')
  } else {
    Text('立即备份')
  }
}
.width('100%')
.height(46)
.onClick(() => { this.doBackup() })

4.3 三态 UI 管理

@State loading: boolean = true
@State loadError: string | null = null

build() {
  if (this.loading) {
    // Loading 态
    this.LoadingView()
  } else if (this.loadError) {
    // 错误态
    this.ErrorView(this.loadError)
  } else {
    // 正常内容
    this.ContentView()
  }
}

五、触觉反馈

HarmonyOS 提供 @ohos.vibrator 模块实现触觉反馈:

import { vibrator } from '@kit.SensorServiceKit'

// 短促点击反馈
function hapticTap(): void {
  try {
    vibrator.startVibration({
      type: 'time',
      duration: 10  // 10ms
    }, {
      usage: 'touch'
    })
  } catch (e) {
    // 设备不支持震动时静默忽略
  }
}

// 长按反馈
function hapticLongPress(): void {
  try {
    vibrator.startVibration({
      type: 'time',
      duration: 30  // 30ms
    }, {
      usage: 'touch'
    })
  } catch (e) {
    // 静默忽略
  }
}

使用场景

// FAB 按钮点击
Image($r('app.media.ic_add'))
  .onClick(() => {
    hapticTap()     // 触觉反馈
    this.toggleFab()
  })

// 长按列表项进入选择模式
Column()
  .gesture(
    LongPressGesture({ duration: 500 })
      .onAction(() => {
        hapticLongPress()  // 长按震动
        this.enterSelectMode()
      })
  )

六、Skeleton 骨架屏

数据加载前展示骨架屏,提升感知性能:

@Builder
private SkeletonView() {
  Column() {
    // 统计卡片骨架
    Row() {
      ForEach([1, 2, 3], () => {
        Column() {
          Row()
            .width(60)
            .height(16)
            .borderRadius(8)
            .backgroundColor('#1A141E2E')
          Row()
            .width(40)
            .height(32)
            .borderRadius(8)
            .backgroundColor('#1A141E2E')
            .margin({ top: 8 })
        }
        .padding(16)
        .backgroundColor('#0F141E2E')
        .borderRadius(16)
      })
    }
    .width('100%')
    .justifyContent(FlexAlign.SpaceBetween)

    // 列表骨架
    ForEach([1, 2, 3, 4], () => {
      Row() {
        Circle()
          .width(44)
          .height(44)
          .fill('#1A141E2E')
        Column() {
          Row()
            .width('60%')
            .height(14)
            .borderRadius(7)
            .backgroundColor('#1A141E2E')
          Row()
            .width('40%')
            .height(12)
            .borderRadius(6)
            .backgroundColor('#1A141E2E')
            .margin({ top: 6 })
        }
        .margin({ left: 12 })
      }
      .width('100%')
      .height(64)
      .padding({ left: 16, right: 16 })
    })
  }
  .padding(16)
}

七、动画性能注意事项

注意项 原因 建议 难度
避免重排 layout 属性变化触发重新测量 opacity / translate 代替改宽高
减少逐帧计算 复杂动画卡顿 使用 springMotion 代替逐帧 animate
动画复用 每个组件的 animateTo 独立 提取到动画令牌统一管理
在页面销毁时停止 组件销毁后动画回调报错 aboutToDisappear 中清理定时器

小结

技术 用途 实现方式
动画令牌 统一管理时长和曲线 AppAnimations
animateTo 状态变化动画 包裹状态更新代码
页面转场 页面间切换动画 @EnterTransition / PageTransition
Loading 加载中状态 LoadingProgress + 条件渲染
触觉反馈 按键/长按反馈 @ohos.vibrator
Skeleton 骨架屏 纯 UI 组件填充占位

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐