查看: 93|回复: 0

鸿蒙特效教程05-鸿蒙很开门

[复制链接]

1

主题

2

回帖

14

积分

新手上路

积分
14
发表于 2025-4-1 08:30:22 | 显示全部楼层 |阅读模式
鸿蒙特效教程05-鸿蒙很开门

本教程适合HarmonyOS初学者,通过简单到复杂的步骤,通过 Stack 层叠布局 + animation 动画,一步步实现这个"鸿蒙很开门"特效。
开发环境准备

    DevEco Studio 5.0.3HarmonyOS Next API 15
下载代码仓库
最终效果预览

屏幕上有一个双开门,点击中间的按钮后,两侧门会向打开,露出开门后面的内容。当用户再次点击按钮时,门会关闭。

实现步骤

我们将通过以下步骤逐步构建这个效果:
    用层叠布局搭建基础UI结构用层叠布局创建门的装饰实现开关门动画效果
步骤1:搭建基础UI结构

首先,我们需要创建一个基本的页面结构。在这个效果中,最关键的是使用 Stack组件来实现层叠效果。
  1. @Entry
  2. @Component
  3. struct OpenTheDoor {
  4.   build() {
  5.     Stack() {
  6.       // 背景层
  7.       Column() {
  8.         Text('鸿蒙很开门')
  9.           .fontSize(28)
  10.           .fontWeight(FontWeight.Bold)
  11.           .fontColor(Color.White)
  12.       }
  13.       .width('100%')
  14.       .height('100%')
  15.       .backgroundColor('#1E2247')
  16.       // 按钮
  17.       Button({ type: ButtonType.Circle }) {
  18.         Text('开')
  19.           .fontSize(20)
  20.           .fontColor(Color.White)
  21.       }
  22.       .width(60)
  23.       .height(60)
  24.       .backgroundColor('#4CAF50')
  25.       .position({ x: '50%', y: '85%' })
  26.       .translate({ x: '-50%', y: '-50%' })
  27.     }
  28.     .width('100%')
  29.     .height('100%')
  30.     .backgroundColor(Color.Black)
  31.   }
  32. }
复制代码
代码说明:
    Stack组件是一个层叠布局容器,子组件会按照添加顺序从底到顶叠放。我们首先放置了一个背景层,它包含了将来门打开后要显示的内容。然后放置了一个圆形按钮,用于触发开门动作。使用 position和 translate组合定位按钮在屏幕底部中间。
此时,只有一个简单的背景和按钮,还没有门的效果。
步骤2:创建门的设计

接下来,我们在Stack层叠布局中添加左右两扇门:
  1. @Entry
  2. @Component
  3. struct OpenTheDoor {
  4.   build() {
  5.     Stack() {
  6.       // 背景层
  7.       Column() {
  8.         Text('鸿蒙很开门')
  9.           .fontSize(28)
  10.           .fontWeight(FontWeight.Bold)
  11.           .fontColor(Color.White)
  12.       }
  13.       .width('100%')
  14.       .height('100%')
  15.       .backgroundColor('#1E2247')
  16.       // 左门
  17.       Stack() {
  18.         // 门本体
  19.         Column()
  20.           .width('96%')
  21.           .height('100%')
  22.           .backgroundColor('#333333')
  23.           .borderWidth({ right: 2 })
  24.           .borderColor('#444444')
  25.         // 门上装饰
  26.         Column() {
  27.           Circle()
  28.             .width(40)
  29.             .height(40)
  30.             .fill('#666666')
  31.           Rect()
  32.             .width(120)
  33.             .height(200)
  34.             .radiusWidth(10)
  35.             .stroke('#555555')
  36.             .strokeWidth(2)
  37.             .fill('none')
  38.             .margin({ top: 40 })
  39.         }
  40.         .width('80%')
  41.         .alignItems(HorizontalAlign.Center)
  42.       }
  43.       .width('50%')
  44.       .height('100%')
  45.       // 右门
  46.       Stack() {
  47.         // 门本体
  48.         Column()
  49.           .width('96%')
  50.           .height('100%')
  51.           .backgroundColor('#333333')
  52.           .borderWidth({ left: 2 })
  53.           .borderColor('#444444')
  54.         // 门上装饰
  55.         Column() {
  56.           Circle()
  57.             .width(40)
  58.             .height(40)
  59.             .fill('#666666')
  60.           Rect()
  61.             .width(120)
  62.             .height(200)
  63.             .radiusWidth(10)
  64.             .stroke('#555555')
  65.             .strokeWidth(2)
  66.             .fill('none')
  67.             .margin({ top: 40 })
  68.         }
  69.         .width('80%')
  70.         .alignItems(HorizontalAlign.Center)
  71.       }
  72.       .width('50%')
  73.       .height('100%')
  74.       // 门框
  75.       Column()
  76.         .width('100%')
  77.         .height('100%')
  78.         .border({ width: 8, color: '#666' })
  79.       // 按钮
  80.       Button({ type: ButtonType.Circle }) {
  81.         Text('开')
  82.           .fontSize(20)
  83.           .fontColor(Color.White)
  84.       }
  85.       .width(60)
  86.       .height(60)
  87.       .backgroundColor('#4CAF50')
  88.       .position({ x: '50%', y: '85%' })
  89.       .translate({ x: '-50%', y: '-50%' })
  90.     }
  91.     .width('100%')
  92.     .height('100%')
  93.     .backgroundColor(Color.Black)
  94.   }
  95. }
复制代码
代码说明:
    我们添加了左右两扇门,每扇门占屏幕宽度的50%。每扇门自身是一个 Stack,包含门本体和装饰元素。门本体使用 Column组件,设置背景色和边框。装饰元素包括圆形"门把手"和矩形装饰。添加门框作为装饰元素,增强立体感。使用 zIndex控制层叠顺序(虽然代码中未显示,但在最终代码中会用到)。
此时我们有了一个静态的门的外观,但它还不能打开和关闭。
步骤3:实现开关门动画

现在我们需要添加状态变量和动画逻辑,使门能够打开和关闭:
  1. @Entry
  2. @Component
  3. struct OpenTheDoor {
  4.   // 门打开的最大位移(百分比)
  5.   private doorOpenMaxOffset: number = 110
  6.   // 当前门打开的位移
  7.   @State doorOpenOffset: number = 0
  8.   // 是否正在动画中
  9.   @State isAnimating: boolean = false
  10.   // 切换门的状态
  11.   toggleDoor() {
  12.     this.isAnimating = true
  13.     if (this.doorOpenOffset  {
  14.           this.isAnimating = false
  15.         }
  16.       }, () => {
  17.         this.doorOpenOffset = this.doorOpenMaxOffset
  18.       })
  19.     } else {
  20.       // 关门动画
  21.       animateTo({
  22.         duration: 1500,
  23.         curve: Curve.EaseInOut,
  24.         iterations: 1,
  25.         playMode: PlayMode.Normal,
  26.         onFinish: () => {
  27.           this.isAnimating = false
  28.         }
  29.       }, () => {
  30.         this.doorOpenOffset = 0
  31.       })
  32.     }
  33.   }
  34.   build() {
  35.     Stack() {
  36.       // 背景层(保持不变)
  37.       ...
  38.       // 左门
  39.       Stack() {
  40.         // 门本体和装饰(保持不变)
  41.         ...
  42.       }
  43.       .width('50%')
  44.       .height('100%')
  45.       .translate({ x: this.doorOpenOffset  0 ? '#FF5252' : '#4CAF50')
  46.       .position({ x: '50%', y: '85%' })
  47.       .translate({ x: '-50%', y: '-50%' })
  48.       .onClick(() => {
  49.         if (!this.isAnimating) {
  50.           this.toggleDoor()
  51.         }
  52.       })
  53.     }
  54.     .width('100%')
  55.     .height('100%')
  56.     .backgroundColor(Color.Black)
  57.   }
  58. }
复制代码
代码说明:
    添加了状态变量:
      doorOpenMaxOffset: 门打开的最大位移doorOpenOffset: 当前门的位移状态isAnimating: 标记动画是否正在进行
    使用 translate属性绑定到 doorOpenOffset状态,实现门的移动效果:
      左门向左移动:translate({ x: (-this.doorOpenOffset) + '%' })右门向右移动:translate({ x: this.doorOpenOffset + '%' })
    实现 toggleDoor方法,使用 animateTo函数创建动画:
      animateTo是HarmonyOS中用于创建显式动画的API设置动画时长1500毫秒使用 EaseInOut曲线使动画更加平滑通过改变 doorOpenOffset状态触发UI更新
    按钮样式和文本随门的状态变化:
      门关闭时显示"开",背景绿色门打开时显示"关",背景红色添加点击事件调用 toggleDoor方法使用 isAnimating防止动画进行中重复触发

此时,门可以通过动画打开和关闭,但门后的内容没有渐变效果。
步骤4:添加门后内容和渐变效果

现在我们为门后的内容添加渐变显示效果:
  1. @Entry
  2. @Component
  3. struct OpenTheDoor {
  4.   // 已有的状态变量
  5.   private doorOpenMaxOffset: number = 110
  6.   @State doorOpenOffset: number = 0
  7.   @State isAnimating: boolean = false
  8.   // 新增状态变量
  9.   @State showContent: boolean = false
  10.   @State backgroundOpacity: number = 0
  11.   toggleDoor() {
  12.     this.isAnimating = true
  13.     if (this.doorOpenOffset  {
  14.           this.isAnimating = false
  15.           this.showContent = true
  16.         }
  17.       }, () => {
  18.         this.doorOpenOffset = this.doorOpenMaxOffset
  19.         this.backgroundOpacity = 1
  20.       })
  21.     } else {
  22.       // 关门动画
  23.       this.showContent = false
  24.       animateTo({
  25.         duration: 1500,
  26.         curve: Curve.EaseInOut,
  27.         iterations: 1,
  28.         playMode: PlayMode.Normal,
  29.         onFinish: () => {
  30.           this.isAnimating = false
  31.         }
  32.       }, () => {
  33.         this.doorOpenOffset = 0
  34.         this.backgroundOpacity = 0
  35.       })
  36.     }
  37.   }
  38.   build() {
  39.     Stack() {
  40.       // 背景层 - 门后内容
  41.       Column() {
  42.         Text('鸿蒙很开门')
  43.           .fontSize(28)
  44.           .fontWeight(FontWeight.Bold)
  45.           .fontColor(Color.White)
  46.           .opacity(this.backgroundOpacity)
  47.           .margin({ bottom: 20 })
  48.         Image($r('app.media.startIcon'))
  49.           .width(100)
  50.           .height(100)
  51.           .objectFit(ImageFit.Contain)
  52.           .opacity(this.backgroundOpacity)
  53.           .animation({
  54.             duration: 800,
  55.             curve: Curve.EaseOut,
  56.             delay: 500,
  57.             iterations: 1,
  58.             playMode: PlayMode.Normal
  59.           })
  60.         Text('探索无限可能')
  61.           .fontSize(20)
  62.           .fontColor(Color.White)
  63.           .opacity(this.backgroundOpacity)
  64.           .margin({ top: 20 })
  65.           .visibility(this.showContent ? Visibility.Visible : Visibility.Hidden)
  66.           .animation({
  67.             duration: 800,
  68.             curve: Curve.EaseOut,
  69.             delay: 100,
  70.             iterations: 1,
  71.             playMode: PlayMode.Normal
  72.           })
  73.       }
  74.       .width('100%')
  75.       .height('100%')
  76.       .justifyContent(FlexAlign.Center)
  77.       .alignItems(HorizontalAlign.Center)
  78.       .backgroundColor('#1E2247')
  79.       // 其他部分(左门、右门、按钮等)保持不变
  80.       ...
  81.     }
  82.     .width('100%')
  83.     .height('100%')
  84.     .backgroundColor(Color.Black)
  85.   }
  86. }
复制代码
代码说明:
    添加新的状态变量:
      showContent: 控制额外内容的显示与隐藏backgroundOpacity: 控制背景内容的透明度
    在 toggleDoor方法中同时控制门的位移和内容的透明度:
      开门时,门位移增加到最大值,同时透明度从0变为1关门时,门位移减少到0,同时透明度从1变为0在开门动画完成后设置 showContent为true,显示额外内容
    为内容元素添加动画效果:
      使用 opacity属性绑定到 backgroundOpacity状态为图片添加 animation属性,设置渐入效果为第二段文本添加条件显示 visibility属性两个元素使用不同的延迟时间,创造错落有致的动画效果

这样,当门打开时,背景内容会平滑地渐入,创造更加连贯的用户体验。
步骤5:优化交互体验

最后,我们添加一些细节来增强交互体验:
  1. @Entry
  2. @Component
  3. struct OpenTheDoor {
  4.   // 状态变量保持不变
  5.   private doorOpenMaxOffset: number = 110
  6.   @State doorOpenOffset: number = 0
  7.   @State isAnimating: boolean = false
  8.   @State showContent: boolean = false
  9.   @State backgroundOpacity: number = 0
  10.   // toggleDoor方法保持不变
  11.   ...
  12.   build() {
  13.     Stack() {
  14.       // 背景层保持不变
  15.       ...
  16.       // 左门和右门保持不变,但添加zIndex
  17.       Stack() { ... }
  18.       .width('50%')
  19.       .height('100%')
  20.       .translate({ x: this.doorOpenOffset  0 ? '#FF5252' : '#4CAF50')
  21.       .position({ x: '50%', y: '85%' })
  22.       .translate({ x: '-50%', y: '-50%' })
  23.       .zIndex(10)
  24.       .onClick(() => {
  25.         if (!this.isAnimating) {
  26.           this.toggleDoor()
  27.         }
  28.       })
  29.     }
  30.     .width('100%')
  31.     .height('100%')
  32.     .backgroundColor(Color.Black)
  33.     .expandSafeArea()
  34.   }
  35. }
复制代码
代码说明:
    添加了 zIndex属性来控制组件的层叠顺序:
      背景内容:默认层级最低左右门:zIndex为3门框:zIndex为5,确保在门的上层按钮:zIndex为10,确保始终在最上层
    改进按钮状态反馈:
      添加 stateEffect: true使按钮有按下效果在动画过程中显示 LoadingProgress加载指示器非动画状态下显示"开"或"关"文本
    添加 expandSafeArea()以全屏显示效果,覆盖刘海屏、挖孔屏的安全区域
完整代码

以下是完整的实现代码:
  1. @Entry
  2. @Component
  3. struct OpenTheDoor {
  4.   // 门打开的位移
  5.   private doorOpenMaxOffset: number = 110
  6.   // 门打开的幅度
  7.   @State doorOpenOffset: number = 0
  8.   // 是否正在动画
  9.   @State isAnimating: boolean = false
  10.   // 是否显示内容
  11.   @State showContent: boolean = false
  12.   // 背景透明度
  13.   @State backgroundOpacity: number = 0
  14.   toggleDoor() {
  15.     this.isAnimating = true
  16.     if (this.doorOpenOffset  {
  17.           this.isAnimating = false
  18.           this.showContent = true
  19.         }
  20.       }, () => {
  21.         this.doorOpenOffset = this.doorOpenMaxOffset
  22.         this.backgroundOpacity = 1
  23.       })
  24.     } else {
  25.       // 关门动画
  26.       this.showContent = false
  27.       animateTo({
  28.         duration: 1500,
  29.         curve: Curve.EaseInOut,
  30.         iterations: 1,
  31.         playMode: PlayMode.Normal,
  32.         onFinish: () => {
  33.           this.isAnimating = false
  34.         }
  35.       }, () => {
  36.         this.doorOpenOffset = 0
  37.         this.backgroundOpacity = 0
  38.       })
  39.     }
  40.   }
  41.   build() {
  42.     // 层叠布局
  43.     Stack() {
  44.       // 背景层 - 门后内容
  45.       Column() {
  46.         Text('鸿蒙很开门')
  47.           .fontSize(28)
  48.           .fontWeight(FontWeight.Bold)
  49.           .fontColor(Color.White)
  50.           .opacity(this.backgroundOpacity)
  51.           .margin({ bottom: 20 })
  52.         // 图片
  53.         Image($r('app.media.startIcon'))
  54.           .width(100)
  55.           .height(100)
  56.           .objectFit(ImageFit.Contain)
  57.           .opacity(this.backgroundOpacity)
  58.           .animation({
  59.             duration: 800,
  60.             curve: Curve.EaseOut,
  61.             delay: 500,
  62.             iterations: 1,
  63.             playMode: PlayMode.Normal
  64.           })
  65.         Text('探索无限可能')
  66.           .fontSize(20)
  67.           .fontColor(Color.White)
  68.           .opacity(this.backgroundOpacity)
  69.           .margin({ top: 20 })
  70.           .visibility(this.showContent ? Visibility.Visible : Visibility.Hidden)
  71.           .animation({
  72.             duration: 800,
  73.             curve: Curve.EaseOut,
  74.             delay: 100,
  75.             iterations: 1,
  76.             playMode: PlayMode.Normal
  77.           })
  78.       }
  79.       .width('100%')
  80.       .height('100%')
  81.       .justifyContent(FlexAlign.Center)
  82.       .alignItems(HorizontalAlign.Center)
  83.       .backgroundColor('#1E2247')
  84.       .expandSafeArea()
  85.       // 左门
  86.       Stack() {
  87.         // 门
  88.         Column()
  89.           .width('96%')
  90.           .height('100%')
  91.           .backgroundColor('#333333')
  92.           .borderWidth({ right: 2 })
  93.           .borderColor('#444444')
  94.         // 装饰图案
  95.         Column() {
  96.           // 简单的门把手和几何图案设计
  97.           Circle()
  98.             .width(40)
  99.             .height(40)
  100.             .fill('#666666')
  101.             .opacity(0.8)
  102.           Rect()
  103.             .width(120)
  104.             .height(200)
  105.             .radiusWidth(10)
  106.             .stroke('#555555')
  107.             .strokeWidth(2)
  108.             .fill('none')
  109.             .margin({ top: 40 })
  110.           // 添加门上的小装饰
  111.           Grid() {
  112.             ForEach(Array.from({ length: 4 }), () => {
  113.               GridItem() {
  114.                 Circle()
  115.                   .width(8)
  116.                   .height(8)
  117.                   .fill('#777777')
  118.               }
  119.             })
  120.           }
  121.           .columnsTemplate('1fr 1fr')
  122.           .rowsTemplate('1fr 1fr')
  123.           .width(60)
  124.           .height(60)
  125.           .margin({ top: 20 })
  126.         }
  127.         .width('80%')
  128.         .alignItems(HorizontalAlign.Center)
  129.       }
  130.       .width('50%')
  131.       .height('100%')
  132.       .translate({ x: this.doorOpenOffset  {
  133.               GridItem() {
  134.                 Circle()
  135.                   .width(8)
  136.                   .height(8)
  137.                   .fill('#777777')
  138.               }
  139.             })
  140.           }
  141.           .columnsTemplate('1fr 1fr')
  142.           .rowsTemplate('1fr 1fr')
  143.           .width(60)
  144.           .height(60)
  145.           .margin({ top: 20 })
  146.         }
  147.         .width('80%')
  148.         .alignItems(HorizontalAlign.Center)
  149.       }
  150.       .width('50%')
  151.       .height('100%')
  152.       .translate({ x: this.doorOpenOffset  0 ? '关' : '开')
  153.               .fontSize(20)
  154.               .fontColor(Color.White)
  155.               .fontWeight(FontWeight.Bold)
  156.           } else {
  157.             // 加载动效
  158.             LoadingProgress()
  159.               .width(30)
  160.               .height(30)
  161.               .color(Color.White)
  162.           }
  163.         }
  164.       }
  165.       .width(60)
  166.       .height(60)
  167.       .backgroundColor(this.doorOpenOffset > 0 ? '#FF5252' : '#4CAF50')
  168.       .position({ x: '50%', y: '85%' })
  169.       .translate({ x: '-50%', y: '-50%' })
  170.       .zIndex(10) // 按钮位置在最上方
  171.       .onClick(() => {
  172.         // 防止多点
  173.         if (!this.isAnimating) {
  174.           this.toggleDoor()
  175.         }
  176.       })
  177.     }
  178.     .width('100%')
  179.     .height('100%')
  180.     .backgroundColor(Color.Black)
  181.     .expandSafeArea()
  182.   }
  183. }
复制代码
总结与技术要点

涉及了以下HarmonyOS开发中的重要技术点:
1. Stack布局

Stack组件是实现这种叠加效果,允许子组件按照添加顺序从底到顶叠放。使用时有以下注意点:
    使用 zIndex 属性控制层叠顺序使用 alignContent 参数控制子组件对齐
2. 动画系统

本教程中使用了两种动画机制:
    animateTo:显式动画API,用于创建状态变化时的过渡效果
  1.   animateTo({
  2.     duration: 1500,
  3.     curve: Curve.EaseInOut,
  4.     iterations: 1,
  5.     playMode: PlayMode.Normal,
  6.     onFinish: () => { /* 动画完成回调 */ }
  7.   }, () => {
  8.     // 状态变化,触发动画
  9.     this.doorOpenOffset = this.doorOpenMaxOffset
  10.   })
复制代码
    animation:属性动画,直接在组件上定义
  1.   .animation({
  2.     duration: 800,
  3.     curve: Curve.EaseOut,
  4.     delay: 500,
  5.     iterations: 1,
  6.     playMode: PlayMode.Normal
  7.   })
复制代码
3. 状态管理

我们使用以下几个状态来控制整个效果:
    doorOpenOffset:控制门的位移isAnimating:标记动画状态,防止重复触发backgroundOpacity:控制背景内容的透明度showContent:控制特定内容的显示与隐藏
4. translate 位移

使用 translate属性实现门的移动效果:
[code].translate({ x: this.doorOpenOffset

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表