HarmonyOS APP开发:Material Design动效体系与实现

举报
Jack20 发表于 2026/06/22 21:35:24 2026/06/22
【摘要】 HarmonyOS APP开发:Material Design动效体系与实现📌 核心要点:掌握Material Design三大核心转场动效(容器转换、共享轴、淡入淡出)在HarmonyOS中的完整实现,让你的应用转场体验从"能用"跃升到"好用"。 一、背景与动机你有没有过这样的体验?打开一个App,点击列表项跳到详情页,画面"啪"地一下就切过去了——毫无过渡,毫无美感,仿佛在翻一本没有...

HarmonyOS APP开发:Material Design动效体系与实现

📌 核心要点:掌握Material Design三大核心转场动效(容器转换、共享轴、淡入淡出)在HarmonyOS中的完整实现,让你的应用转场体验从"能用"跃升到"好用"。


一、背景与动机

你有没有过这样的体验?打开一个App,点击列表项跳到详情页,画面"啪"地一下就切过去了——毫无过渡,毫无美感,仿佛在翻一本没有插图的技术手册。再换个App,同样的操作,列表卡片优雅地展开成详情页,元素流畅地归位,你甚至没意识到页面已经切换了。

这就是有没有动效的差距。

Material Design动效体系不是"锦上添花"的装饰品,它是用户空间认知的导航系统。当用户从一个页面跳到另一个页面时,他们需要知道:我从哪来?我到哪去了?这两个地方是什么关系?好的转场动效就是在回答这些问题。

HarmonyOS从5.0开始就引入了对Material Design动效规范的支持,到了6.0更是进一步强化了容器转换的能力。如果你还在用animateTo()写简单的透明度渐变做页面切换,那这篇文章就是为你准备的——我们将系统性地拆解Material Design的三大转场模式,并在HarmonyOS中逐一实现。


二、核心原理

2.1 Material Design动效体系总览

Material Design的动效体系建立在三个核心转场模式之上,每种模式解决不同的空间关系问题:

graph TD
    A[Material Design动效体系]:::primary --> B[容器转换<br/>Container Transform]:::info
    A --> C[共享轴转换<br/>Shared Axis]:::info
    A --> D[淡入淡出<br/>Fade Through]:::info
    
    B --> B1[元素A变形为元素B<br/>强空间关联]:::success
    C --> C1[沿同一轴向运动<br/>同级关系]:::success
    D --> D1[先淡出再淡入<br/>弱关联]:::success
    
    B1 --> E[示例:列表卡片→详情页]:::warning
    C1 --> F[示例:Tab切换、步骤流程]:::warning
    D1 --> G[示例:底部导航切换]:::warning

    classDef primary fill:#4CAF50,stroke:#388E3C,color:#fff
    classDef info fill:#2196F3,stroke:#1976D2,color:#fff
    classDef success fill:#009688,stroke:#00796B,color:#fff
    classDef warning fill:#FF9800,stroke:#F57C00,color:#fff
    classDef error fill:#F44336,stroke:#D32F2F,color:#fff

2.2 三大转场模式详解

容器转换(Container Transform):这是最"有戏"的转场。想象你点击一张卡片,卡片像变形金刚一样展开成详情页——卡片就是"容器",它从一个形状/大小平滑地变到另一个形状/大小。关键在于:两个页面共享同一个容器元素,容器在变形过程中承载了内容的变化。

共享轴转换(Shared Axis):两个页面沿着同一个轴向(X/Y/Z)运动。就像在一条传送带上,旧页面从右边滑出去,新页面从左边滑进来。它表达的是"同级关系"——这些页面是平行的、对等的。

淡入淡出(Fade Through):最简单也最克制。旧页面先淡出,新页面再淡入,中间有一个短暂的"空白帧"。注意,这不是交叉淡入淡出(crossfade)!交叉淡入是同时进行的,而Fade Through是串行的——先出后进。它适用于没有明显空间关系的页面切换。

2.3 Material动效曲线

Material Design定义了一套独特的缓动曲线,和传统的ease-in-out有本质区别:

曲线名称 适用场景
Standard cubic-bezier(0.2, 0.0, 0, 1.0) 常规过渡
Standard Accelerate cubic-bezier(0.3, 0.0, 1, 1) 元素离开屏幕
Standard Decelerate cubic-bezier(0, 0, 0, 1) 元素进入屏幕
Emphasized cubic-bezier(0.2, 0, 0, 1) 重要转场
Emphasized Accelerate cubic-bezier(0.3, 0, 0.8, 0.15) 强调离开
Emphasized Decelerate cubic-bezier(0.05, 0.7, 0.1, 1.0) 强调进入

这些曲线的核心理念是:快出慢入。元素离开时快速启动,进入时缓慢减速,给用户一种"重量感"和"物理感"。


三、代码实战

3.1 基础用法:容器转换

容器转换是Material动效中最复杂也最惊艳的一种。在HarmonyOS中,我们通过geometryTransition配合animateTo来实现:

// 列表页 - 卡片元素
@Component
struct CardItem {
  @Prop cardData: CardData
  @Prop isExpanded: boolean = false

  build() {
    Column() {
      Image(this.cardData.imageUrl)
        .width(this.isExpanded ? '100%' : 80)
        .height(this.isExpanded ? 300 : 80)
        .objectFit(ImageFit.Cover)
        .borderRadius(this.isExpanded ? 0 : 8)

      Text(this.cardData.title)
        .fontSize(this.isExpanded ? 24 : 16)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 8 })

      if (this.isExpanded) {
        // 展开时显示详情内容
        Text(this.cardData.description)
          .fontSize(14)
          .fontColor('#666666')
          .margin({ top: 12 })
      }
    }
    .width(this.isExpanded ? '100%' : '90%')
    .padding(this.isExpanded ? 0 : 12)
    .backgroundColor(Color.White)
    .borderRadius(this.isExpanded ? 0 : 12)
    .shadow(this.isExpanded ? null : {
      radius: 8, color: '#1a000000',
      offsetX: 0, offsetY: 2
    })
    // 关键:通过geometryTransition绑定共享元素
    .geometryTransition('card_' + this.cardData.id)
    .onClick(() => {
      animateTo({
        duration: 400,
        // 使用Material标准减速曲线
        curve: Curve.Friction,
        onFinish: () => {
          console.info('容器转换完成')
        }
      }, () => {
        this.isExpanded = !this.isExpanded
      })
    })
  }
}

3.2 进阶用法:共享轴转换

共享轴转换适合Tab切换、步骤流程等场景。核心思路是:两个页面沿同一轴向做反向运动,配合透明度变化:

// 共享轴转场组件 - 支持X/Y/Z三个轴向
@Component
struct SharedAxisTransition {
  @Prop axis: SharedAxisType = SharedAxisType.X
  @Prop progress: number = 0  // 0=旧页面, 1=新页面
  @Builder childContent: () => void

  build() {
    Stack() {
      this.childContent()
    }
    .opacity(1.0 - Math.abs(this.progress - 0.5) * 0.4)  // 中间最亮
    .translate({
      x: this.axis === SharedAxisType.X ? (1 - this.progress) * 30 : 0,
      y: this.axis === SharedAxisType.Y ? (1 - this.progress) * 30 : 0,
      z: this.axis === SharedAxisType.Z ? (1 - this.progress) * 10 : 0
    })
    .scale({
      x: this.axis === SharedAxisType.Z ? 0.95 + this.progress * 0.05 : 1.0,
      y: this.axis === SharedAxisType.Z ? 0.95 + this.progress * 0.05 : 1.0
    })
  }
}

// 枚举定义
enum SharedAxisType {
  X,
  Y,
  Z
}

// 在页面中使用共享轴转场
@Entry
@Component
struct SharedAxisPage {
  @State currentTab: number = 0
  @State tabProgress: number = 0

  // Tab内容数据
  private tabs: string[] = ['首页', '发现', '我的']

  build() {
    Column() {
      // Tab栏
      Row() {
        ForEach(this.tabs, (tab: string, index: number) => {
          Text(tab)
            .fontSize(16)
            .fontColor(this.currentTab === index ? '#1a73e8' : '#666666')
            .fontWeight(this.currentTab === index ? FontWeight.Bold : FontWeight.Normal)
            .padding({ bottom: 8 })
            .borderWidth({ bottom: this.currentTab === index ? 2 : 0 })
            .borderColor('#1a73e8')
            .layoutWeight(1)
            .textAlign(TextAlign.Center)
            .onClick(() => {
              if (this.currentTab !== index) {
                animateTo({
                  duration: 300,
                  curve: Curve.Friction  // Material标准减速曲线
                }, () => {
                  this.currentTab = index
                })
              }
            })
        })
      }
      .width('100%')
      .padding({ horizontal: 16 })
      .backgroundColor(Color.White)

      // 内容区域 - 使用共享轴Y轴转场
      Stack() {
        if (this.currentTab === 0) {
          this.HomeContent()
        } else if (this.currentTab === 1) {
          this.DiscoverContent()
        } else {
          this.ProfileContent()
        }
      }
      .layoutWeight(1)
      .width('100%')
    }
  }

  @Builder HomeContent() {
    Scroll() {
      Column() {
        Text('首页内容')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
        // ... 更多内容
      }
      .padding(16)
    }
    .transition(TransitionEffect.OPACITY.combine(TransitionEffect.translate({ y: 30 })))
  }

  @Builder DiscoverContent() {
    Scroll() {
      Column() {
        Text('发现内容')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
      }
      .padding(16)
    }
    .transition(TransitionEffect.OPACITY.combine(TransitionEffect.translate({ y: -30 })))
  }

  @Builder ProfileContent() {
    Scroll() {
      Column() {
        Text('个人中心')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
      }
      .padding(16)
    }
    .transition(TransitionEffect.OPACITY.combine(TransitionEffect.translate({ y: -30 })))
  }
}

3.3 完整示例:Material三大转场集成

下面这个示例把三种转场模式整合到一个应用中,你可以直接运行体验:

// Material Design三大转场完整示例
@Entry
@Component
struct MaterialMotionDemo {
  @State currentPage: number = 0
  @State selectedCardId: string = ''
  @State isDetailOpen: boolean = false

  // 模拟数据
  private cards: CardData[] = [
    { id: '1', title: '晨间冥想', subtitle: '5分钟开启美好一天', color: '#E8F5E9', accent: '#4CAF50' },
    { id: '2', title: '午后阅读', subtitle: '沉浸式阅读体验', color: '#E3F2FD', accent: '#2196F3' },
    { id: '3', title: '傍晚运动', subtitle: '30分钟有氧训练', color: '#FFF3E0', accent: '#FF9800' },
    { id: '4', title: '夜间日记', subtitle: '记录今天的感悟', color: '#F3E5F5', accent: '#9C27B0' }
  ]

  build() {
    Stack() {
      // 主页面
      Column() {
        // 顶部标题栏
        this.TitleBar()

        // 内容区域
        if (this.currentPage === 0) {
          this.CardListPage()
        } else if (this.currentPage === 1) {
          this.StepsPage()
        } else {
          this.SettingsPage()
        }

        // 底部导航 - Fade Through转场
        this.BottomNav()
      }
      .width('100%')
      .height('100%')

      // 详情页覆盖层 - 容器转换
      if (this.isDetailOpen) {
        this.DetailOverlay()
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }

  // ====== 标题栏 ======
  @Builder TitleBar() {
    Row() {
      Text('Material Motion')
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
        .fontColor('#212121')
    }
    .width('100%')
    .height(56)
    .padding({ horizontal: 16 })
    .backgroundColor(Color.White)
    .shadow({ radius: 2, color: '#1a000000', offsetY: 1 })
  }

  // ====== 卡片列表页 - 容器转换入口 ======
  @Builder CardListPage() {
    Scroll() {
      Column({ space: 12 }) {
        ForEach(this.cards, (card: CardData) => {
          Row() {
            // 卡片图标
            Column() {
              Text(card.title.charAt(0))
                .fontSize(28)
                .fontWeight(FontWeight.Bold)
                .fontColor(Color.White)
            }
            .width(56)
            .height(56)
            .borderRadius(12)
            .backgroundColor(card.accent)
            .justifyContent(FlexAlign.Center)

            // 卡片文字
            Column() {
              Text(card.title)
                .fontSize(16)
                .fontWeight(FontWeight.Medium)
                .fontColor('#212121')
              Text(card.subtitle)
                .fontSize(13)
                .fontColor('#757575')
                .margin({ top: 4 })
            }
            .alignItems(HorizontalAlign.Start)
            .margin({ left: 12 })
            .layoutWeight(1)

            // 箭头
            Text('›')
              .fontSize(24)
              .fontColor('#BDBDBD')
          }
          .width('100%')
          .padding(16)
          .backgroundColor(Color.White)
          .borderRadius(16)
          .shadow({ radius: 4, color: '#0d000000', offsetY: 2 })
          // 关键:geometryTransition绑定容器转换
          .geometryTransition('card_' + card.id)
          .onClick(() => {
            animateTo({
              duration: 500,
              curve: Curve.Friction  // Material Emphasized曲线近似
            }, () => {
              this.selectedCardId = card.id
              this.isDetailOpen = true
            })
          })
        })
      }
      .padding(16)
    }
    .layoutWeight(1)
    .width('100%')
    .transition(TransitionEffect.OPACITY)
  }

  // ====== 详情页覆盖层 ======
  @Builder DetailOverlay() {
    Column() {
      // 返回按钮
      Row() {
        Text('← 返回')
          .fontSize(16)
          .fontColor(Color.White)
          .onClick(() => {
            animateTo({
              duration: 400,
              curve: Curve.Friction
            }, () => {
              this.isDetailOpen = false
              this.selectedCardId = ''
            })
          })
      }
      .width('100%')
      .height(56)
      .padding({ horizontal: 16 })

      // 详情内容
      Column() {
        Text(this.cards.find(c => c.id === this.selectedCardId)?.title ?? '')
          .fontSize(28)
          .fontWeight(FontWeight.Bold)
          .fontColor(Color.White)
          .margin({ top: 20 })

        Text(this.cards.find(c => c.id === this.selectedCardId)?.subtitle ?? '')
          .fontSize(16)
          .fontColor('#ffffffcc')
          .margin({ top: 8 })

        // 模拟详情内容
        Column() {
          ForEach(['功能一:智能提醒', '功能二:数据统计', '功能三:个性定制'], (item: string) => {
            Row() {
              Text('✓')
                .fontSize(18)
                .fontColor(this.cards.find(c => c.id === this.selectedCardId)?.accent ?? '#4CAF50')
                .margin({ right: 12 })
              Text(item)
                .fontSize(15)
                .fontColor('#424242')
            }
            .width('100%')
            .padding({ vertical: 12 })
            .borderWidth({ bottom: 0.5 })
            .borderColor('#E0E0E0')
          })
        }
        .width('100%')
        .padding(20)
        .backgroundColor(Color.White)
        .borderRadius({ topLeft: 24, topRight: 24 })
        .margin({ top: 32 })
        .layoutWeight(1)
      }
      .width('100%')
      .padding({ horizontal: 20 })
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
    .backgroundColor(this.cards.find(c => c.id === this.selectedCardId)?.accent ?? '#4CAF50')
    // 关键:与列表卡片共享geometryTransition
    .geometryTransition('card_' + this.selectedCardId)
    .borderRadius(0)
  }

  // ====== 步骤流程页 - 共享轴X转场 ======
  @Builder StepsPage() {
    Column() {
      // 步骤指示器
      Row() {
        ForEach(['步骤一', '步骤二', '步骤三'], (step: string, index: number) => {
          Column() {
            Circle()
              .width(index <= 1 ? 32 : 24)
              .height(index <= 1 ? 32 : 24)
              .fill(index <= 1 ? '#1a73e8' : '#E0E0E0')
            Text(step)
              .fontSize(12)
              .fontColor(index <= 1 ? '#1a73e8' : '#9E9E9E')
              .margin({ top: 4 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)
        })
      }
      .width('100%')
      .padding({ vertical: 24, horizontal: 16 })

      // 步骤内容 - 共享轴X方向
      Stack() {
        Text('步骤内容区域')
          .fontSize(18)
          .fontColor('#424242')
      }
      .width('100%')
      .layoutWeight(1)
      .justifyContent(FlexAlign.Center)
    }
    .layoutWeight(1)
    .width('100%')
    .transition(TransitionEffect.OPACITY.combine(
      TransitionEffect.translate({ x: 30 })
    ))
  }

  // ====== 设置页 - Fade Through转场 ======
  @Builder SettingsPage() {
    Scroll() {
      Column({ space: 1 }) {
        ForEach(['通知设置', '隐私安全', '存储管理', '关于应用'], (item: string) => {
          Row() {
            Text(item)
              .fontSize(16)
              .fontColor('#212121')
            Blank()
            Text('›')
              .fontSize(20)
              .fontColor('#BDBDBD')
          }
          .width('100%')
          .padding({ horizontal: 16, vertical: 16 })
          .backgroundColor(Color.White)
        })
      }
    }
    .layoutWeight(1)
    .width('100%')
    .transition(TransitionEffect.OPACITY)  // 纯透明度 = Fade Through
  }

  // ====== 底部导航 ======
  @Builder BottomNav() {
    Row() {
      ForEach(['卡片', '步骤', '设置'], (tab: string, index: number) => {
        Column() {
          Text(tab)
            .fontSize(14)
            .fontColor(this.currentPage === index ? '#1a73e8' : '#9E9E9E')
            .fontWeight(this.currentPage === index ? FontWeight.Medium : FontWeight.Normal)
        }
        .layoutWeight(1)
        .justifyContent(FlexAlign.Center)
        .height(56)
        .onClick(() => {
          if (this.currentPage !== index) {
            // Fade Through:先淡出再淡入
            animateTo({
              duration: 300,
              curve: Curve.Smooth
            }, () => {
              this.currentPage = index
            })
          }
        })
      })
    }
    .width('100%')
    .height(56)
    .backgroundColor(Color.White)
    .shadow({ radius: 8, color: '#1a000000', offsetY: -2 })
  }
}

// 数据模型
interface CardData {
  id: string
  title: string
  subtitle: string
  color: string
  accent: string
}

四、踩坑与注意事项

坑点1:geometryTransition的id必须完全一致

这是容器转换最常见的问题。列表页的geometryTransition('card_1')和详情页的geometryTransition('card_1'),id必须一模一样,包括大小写。我见过有人一个写了Card_1一个写了card_1,结果动效完全没触发,调试了半天才找到原因。

坑点2:容器转换期间不要修改共享元素的布局约束

当容器转换动画正在进行时,如果同时修改了共享元素的widthheightmargin等布局属性,会导致动画跳帧甚至崩溃。正确做法是在animateTo的回调中一次性修改所有状态,让框架自动计算中间帧。

坑点3:共享轴转场不要用crossfade

很多开发者做页面切换时习惯用交叉淡入淡出(两个页面同时改变透明度),但Material Design明确指出:共享轴转场中,离开的页面应该先淡出,进入的页面再淡入,中间有一帧是完全透明的。这样做的好处是避免两个页面的内容叠加产生视觉混乱。

坑点4:Curve.Friction不是精确的Material曲线

HarmonyOS内置的Curve.Friction曲线接近Material的Standard Decelerate,但不是完全一致。如果你需要精确还原Material曲线,需要使用自定义的cubicBezierCurve

// 精确的Material Standard曲线
let materialStandard = Curve.cubicBezierCurve(0.2, 0.0, 0.0, 1.0)

坑点5:容器转换在嵌套Scroll中可能失效

如果共享元素位于ScrollList内部,当用户滚动后点击,geometryTransition可能无法正确计算起始位置。解决方案是在触发转场前先记录元素的当前位置偏移,或者在转场开始时暂时禁用滚动。

坑点6:动画时长不是越长越好

Material Design对动画时长有明确建议:小元素(图标、按钮)150-200ms,中等元素(卡片、对话框)200-300ms,大元素(全屏转场)300-500ms。超过500ms的转场会让用户觉得"卡",低于100ms又感觉"跳"。找到那个甜点很重要。

坑点7:Fade Through的中间帧不能省

有些开发者为了"流畅"把Fade Through改成了crossfade,觉得中间没有空白帧更好看。但Material Design的研究表明,中间的空白帧帮助用户"清空认知",为新内容做准备。省掉它反而会让用户觉得混乱。


五、HarmonyOS 6适配说明

API差异

API HarmonyOS 5.0 HarmonyOS 6.0 迁移建议
geometryTransition 仅支持同页面内共享元素 支持跨页面共享元素 使用Navigation + geometryTransition实现跨页面容器转换
Curve.cubicBezierCurve 参数为4个number 新增preset枚举Curve.MaterialStandard 优先使用preset枚举
TransitionEffect 仅支持OPACITY/SLIDE等基础效果 新增SHARED_AXIS/X/Y/Z预设 使用预设替代手动组合
animateTo duration 无最大值限制 建议不超过1000ms 遵循Material时长规范
Navigation转场 需手动配置customNavTransition 新增materialNavTransition预设 使用materialNavTransition简化代码

行为变更

  • geometryTransition跨页面支持:6.0中geometryTransition可以在Navigation的两个页面之间使用,无需手动管理overlay层,框架自动处理z-index和裁剪
  • TransitionEffect新增SHARED_AXIS:6.0新增了TransitionEffect.SHARED_AXIS(SharedAxisType.X)等预设,不再需要手动组合translate+opacity+scale
  • Material曲线内置化:6.0将Material标准曲线作为Curve的内置枚举值,不再需要手动计算cubicBezier参数
  • 动画中断处理:6.0中如果用户在转场动画未完成时触发新转场,框架会自动从当前状态开始新动画,而不是跳到终点再开始

适配代码

// HarmonyOS 6.0 推荐写法 - 使用内置Material转场预设
@Entry
@Component
struct MaterialMotionV6 {
  @State isDetailOpen: boolean = false
  @State selectedId: string = ''

  build() {
    Navigation() {
      Column() {
        // 列表页
        List() {
          ForEach(this.getCards(), (card: CardData) => {
            ListItem() {
              this.CardItem(card)
            }
          })
        }
        .layoutWeight(1)
      }
      .width('100%')
      .height('100%')
    }
    // HarmonyOS 6.0新增:Material转场预设
    .materialNavTransition({
      duration: 400,
      curve: Curve.MaterialStandard,  // 6.0内置Material曲线
      transitionType: MaterialTransitionType.CONTAINER_TRANSFORM
    })
  }

  @Builder CardItem(card: CardData) {
    Row() {
      Text(card.title)
        .fontSize(16)
    }
    .width('100%')
    .padding(16)
    .backgroundColor(Color.White)
    .borderRadius(12)
    // 6.0中geometryTransition支持跨页面
    .geometryTransition('card_' + card.id)
    .onClick(() => {
      animateTo({
        duration: 400,
        curve: Curve.MaterialEmphasized  // 6.0内置强调曲线
      }, () => {
        this.selectedId = card.id
        this.isDetailOpen = true
      })
    })
  }

  private getCards(): CardData[] {
    return [
      { id: '1', title: '晨间冥想', subtitle: '5分钟', color: '#E8F5E9', accent: '#4CAF50' },
      { id: '2', title: '午后阅读', subtitle: '30分钟', color: '#E3F2FD', accent: '#2196F3' }
    ]
  }
}

// HarmonyOS 6.0 共享轴转场简化写法
@Component
struct SharedAxisV6 {
  @State currentStep: number = 0

  build() {
    Stack() {
      if (this.currentStep === 0) {
        this.StepOne()
      } else {
        this.StepTwo()
      }
    }
    // 6.0新增:共享轴转场预设
    .transition(TransitionEffect.SHARED_AXIS(SharedAxisType.X, { duration: 300 }))
  }

  @Builder StepOne() {
    Column() {
      Text('步骤一')
    }
  }

  @Builder StepTwo() {
    Column() {
      Text('步骤二')
    }
  }
}

六、总结

维度 评价
学习难度 ⭐⭐⭐⭐
使用频率 ⭐⭐⭐⭐⭐
重要程度 ⭐⭐⭐⭐⭐

Material Design动效体系不是"炫技"的工具,而是用户体验的基础设施。容器转换让用户理解"这个东西变成了那个东西",共享轴让用户感知"这些页面是并列的",淡入淡出让用户知道"这是完全不同的新内容"。

三种转场模式各有适用场景,不要混用。列表到详情用容器转换,Tab和步骤用共享轴,底部导航用淡入淡出。记住这个口诀,你的应用转场体验就能超过90%的HarmonyOS应用。

最后,动效的终极目标是让用户感觉不到动效的存在——当一切过渡都自然流畅时,用户只会觉得"这个App真好用",却说不出为什么。这就是Material Design动效的精髓。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。