查看: 111|回复: 0

鸿蒙特效教程10-卡片展开/收起效果

[复制链接]

1

主题

3

回帖

14

积分

新手上路

积分
14
发表于 2025-4-3 14:58:08 | 显示全部楼层 |阅读模式
鸿蒙特效教程10-卡片展开/收起效果

在移动应用开发中,卡片是一种常见且实用的UI元素,能够将信息以紧凑且易于理解的方式呈现给用户。
本教程将详细讲解如何在HarmonyOS中实现卡片的展开/收起效果,通过这个实例,你将掌握ArkUI中状态管理和动画实现的核心技巧。
一、实现效果预览

我们将实现一个包含多个卡片的页面,整个交互过程都有平滑的动画效果。
    每个卡片默认只显示标题,点击右侧箭头按钮后可以展开显示详细内容,再次点击则收起。实现"全部展开"和"全部收起"的功能按钮。

二、实现步骤

步骤1:创建基础页面结构

首先,我们需要创建一个基本的页面结构,包含一个标题和一个简单的卡片:
  1. @Entry
  2. @Component
  3. struct ToggleCard {
  4.   build() {
  5.     Column() {
  6.       Text('卡片展开/收起示例')
  7.         .fontSize(22)
  8.         .fontWeight(FontWeight.Bold)
  9.         .margin({ top: 20 })
  10.       // 一个简单的卡片
  11.       Column() {
  12.         Text('个人信息')
  13.           .fontSize(16)
  14.           .fontWeight(FontWeight.Medium)
  15.       }
  16.       .width('90%')
  17.       .padding(16)
  18.       .backgroundColor('#ECF2FF')
  19.       .borderRadius(12)
  20.       .margin({ top: 20 })
  21.     }
  22.     .width('100%')
  23.     .height('100%')
  24.     .backgroundColor('#F5F5F5')
  25.     .alignItems(HorizontalAlign.Center)
  26.     .expandSafeArea()
  27.   }
  28. }
复制代码
这段代码创建了一个基本的页面,顶部有一个标题,下方有一个简单的卡片,卡片只包含一个标题文本。
步骤2:添加卡片标题行和展开按钮

接下来,我们为卡片添加一个标题行,并在右侧添加一个展开/收起按钮:
  1. @Entry
  2. @Component
  3. struct ToggleCard {
  4.   build() {
  5.     Column() {
  6.       Text('卡片展开/收起示例')
  7.         .fontSize(22)
  8.         .fontWeight(FontWeight.Bold)
  9.         .margin({ top: 20 })
  10.       // 一个带展开按钮的卡片
  11.       Column() {
  12.         Row() {
  13.           Text('个人信息')
  14.             .fontSize(16)
  15.             .fontWeight(FontWeight.Medium)
  16.           Blank()  // 占位,使按钮靠右显示
  17.           Button() {
  18.             Image($r('sys.media.ohos_ic_public_arrow_down'))
  19.               .width(24)
  20.               .height(24)
  21.               .fillColor('#3F72AF')
  22.           }
  23.           .width(36)
  24.           .height(36)
  25.           .backgroundColor(Color.Transparent)
  26.         }
  27.         .width('100%')
  28.         .justifyContent(FlexAlign.SpaceBetween)
  29.         .alignItems(VerticalAlign.Center)
  30.       }
  31.       .width('90%')
  32.       .padding(16)
  33.       .backgroundColor('#ECF2FF')
  34.       .borderRadius(12)
  35.       .margin({ top: 20 })
  36.     }
  37.     .width('100%')
  38.     .height('100%')
  39.     .backgroundColor('#F5F5F5')
  40.     .alignItems(HorizontalAlign.Center)
  41.     .expandSafeArea()
  42.   }
  43. }
复制代码
现在我们的卡片有了标题和一个展开按钮,但点击按钮还没有任何效果。接下来我们将添加状态管理和交互逻辑。
步骤3:添加状态变量控制卡片展开/收起

要实现卡片的展开/收起效果,我们需要添加一个状态变量来跟踪卡片是否处于展开状态:
  1. @Entry
  2. @Component
  3. struct ToggleCard {
  4.   @State isExpanded: boolean = false  // 控制卡片展开/收起状态
  5.   build() {
  6.     Column() {
  7.       Text('卡片展开/收起示例')
  8.         .fontSize(22)
  9.         .fontWeight(FontWeight.Bold)
  10.         .margin({ top: 20 })
  11.       // 一个带展开按钮的卡片
  12.       Column() {
  13.         Row() {
  14.           Text('个人信息')
  15.             .fontSize(16)
  16.             .fontWeight(FontWeight.Medium)
  17.           Blank()
  18.           Button() {
  19.             Image($r('sys.media.ohos_ic_public_arrow_down'))
  20.               .width(24)
  21.               .height(24)
  22.               .fillColor('#3F72AF')
  23.           }
  24.           .width(36)
  25.           .height(36)
  26.           .backgroundColor(Color.Transparent)
  27.           .onClick(() => {
  28.             this.isExpanded = !this.isExpanded  // 点击按钮切换状态
  29.           })
  30.         }
  31.         .width('100%')
  32.         .justifyContent(FlexAlign.SpaceBetween)
  33.         .alignItems(VerticalAlign.Center)
  34.         // 根据展开状态条件渲染内容
  35.         if (this.isExpanded) {
  36.           Text('这是展开后显示的内容,包含详细信息。')
  37.             .fontSize(14)
  38.             .margin({ top: 8 })
  39.         }
  40.       }
  41.       .width('90%')
  42.       .padding(16)
  43.       .backgroundColor('#ECF2FF')
  44.       .borderRadius(12)
  45.       .margin({ top: 20 })
  46.     }
  47.     .width('100%')
  48.     .height('100%')
  49.     .backgroundColor('#F5F5F5')
  50.     .alignItems(HorizontalAlign.Center)
  51.     .expandSafeArea()
  52.   }
  53. }
复制代码
现在我们添加了一个 @State状态变量 isExpanded,并在按钮的 onClick事件中切换它的值。同时,我们使用 if条件语句根据 isExpanded的值决定是否显示卡片的详细内容。
步骤4:添加基本动画效果

接下来,我们将为卡片的展开/收起添加动画效果,让交互更加流畅自然。HarmonyOS提供了两种主要的动画实现方式:
    animation属性:直接应用于组件的声明式动画animateTo函数:通过改变状态触发的命令式动画
首先,我们使用这两种方式来实现箭头旋转和内容展开的动画效果:
  1. @Entry
  2. @Component
  3. struct ToggleCard {
  4.   @State isExpanded: boolean = false
  5.   // 切换卡片展开/收起状态
  6.   toggleCard() {
  7.     // 使用animateTo实现状态变化的动画
  8.     animateTo({
  9.       duration: 300,  // 动画持续时间(毫秒)
  10.       curve: Curve.EaseOut,  // 缓动曲线
  11.       onFinish: () => {
  12.         console.info('卡片动画完成')  // 动画完成回调
  13.       }
  14.     }, () => {
  15.       this.isExpanded = !this.isExpanded  // 在动画函数中切换状态
  16.     })
  17.   }
  18.   build() {
  19.     Column() {
  20.       Text('卡片展开/收起示例')
  21.         .fontSize(22)
  22.         .fontWeight(FontWeight.Bold)
  23.         .margin({ top: 20 })
  24.       // 带动画效果的卡片
  25.       Column() {
  26.         Row() {
  27.           Text('个人信息')
  28.             .fontSize(16)
  29.             .fontWeight(FontWeight.Medium)
  30.           Blank()
  31.           Button() {
  32.             Image($r('sys.media.ohos_ic_public_arrow_down'))
  33.               .width(24)
  34.               .height(24)
  35.               .fillColor('#3F72AF')
  36.               .rotate({ angle: this.isExpanded ? 180 : 0 })  // 根据状态控制旋转角度
  37.               .animation({  // 为旋转添加动画效果
  38.                 duration: 300,
  39.                 curve: Curve.FastOutSlowIn
  40.               })
  41.           }
  42.           .width(36)
  43.           .height(36)
  44.           .backgroundColor(Color.Transparent)
  45.           .onClick(() => this.toggleCard())  // 调用切换函数
  46.         }
  47.         .width('100%')
  48.         .justifyContent(FlexAlign.SpaceBetween)
  49.         .alignItems(VerticalAlign.Center)
  50.         if (this.isExpanded) {
  51.           Column() {
  52.             Text('这是展开后显示的内容,包含详细信息。')
  53.               .fontSize(14)
  54.               .layoutWeight(1)
  55.           }
  56.           .animation({  // 为内容添加动画效果
  57.             duration: 300,
  58.             curve: Curve.EaseOut
  59.           })
  60.           .height(80)  // 固定高度便于观察动画效果
  61.           .width('100%')
  62.         }
  63.       }
  64.       .width('90%')
  65.       .padding(16)
  66.       .backgroundColor('#ECF2FF')
  67.       .borderRadius(12)
  68.       .margin({ top: 20 })
  69.     }
  70.     .width('100%')
  71.     .height('100%')
  72.     .backgroundColor('#F5F5F5')
  73.     .alignItems(HorizontalAlign.Center)
  74.     .expandSafeArea()
  75.   }
  76. }
复制代码
在这个版本中,我们添加了两种动画实现:
    使用 animateTo函数来实现状态变化时的动画效果使用 .animation()属性为箭头旋转和内容展示添加过渡动画
这两种动画方式的区别:
    animation属性:简单直接,适用于属性变化的过渡动画animateTo函数:更灵活,可以一次性动画多个状态变化,有完成回调
步骤5:扩展为多卡片结构

现在让我们扩展代码,实现多个可独立展开/收起的卡片:
  1. // 定义卡片数据接口
  2. interface CardInfo {
  3.   title: string
  4.   content: string
  5.   color: string
  6. }
  7. @Entry
  8. @Component
  9. struct ToggleCard {
  10.   // 使用数组管理多个卡片的展开状态
  11.   @State cardsExpanded: boolean[] = [false, false, false]
  12.   // 卡片数据
  13.   private cardsData: CardInfo[] = [
  14.     {
  15.       title: '个人信息',
  16.       content: '这是个人信息卡片的内容区域,可以放置用户的基本信息,如姓名、年龄、电话等。',
  17.       color: '#ECF2FF'
  18.     },
  19.     {
  20.       title: '支付设置',
  21.       content: '这是支付设置卡片的内容区域,可以放置用户的支付相关信息,包括支付方式、银行卡等信息。',
  22.       color: '#E7F5EF'
  23.     },
  24.     {
  25.       title: '隐私设置',
  26.       content: '这是隐私设置卡片的内容区域,可以放置隐私相关的设置选项,如账号安全、数据权限等内容。',
  27.       color: '#FFF1E6'
  28.     }
  29.   ]
  30.   // 切换指定卡片的展开/收起状态
  31.   toggleCard(index: number) {
  32.     animateTo({
  33.       duration: 300,
  34.       curve: Curve.EaseOut,
  35.       onFinish: () => {
  36.         console.info(`卡片${index}动画完成`)
  37.       }
  38.     }, () => {
  39.       // 创建新数组并更新特定索引的值
  40.       let newExpandedState = [...this.cardsExpanded]
  41.       newExpandedState[index] = !newExpandedState[index]
  42.       this.cardsExpanded = newExpandedState
  43.     })
  44.   }
  45.   build() {
  46.     Column() {
  47.       Text('多卡片展开/收起示例')
  48.         .fontSize(22)
  49.         .fontWeight(FontWeight.Bold)
  50.         .margin({ top: 20 })
  51.       // 使用ForEach遍历卡片数据,创建多个卡片
  52.       ForEach(this.cardsData, (card: CardInfo, index: number) => {
  53.         // 卡片组件
  54.         Column() {
  55.           Row() {
  56.             Text(card.title)
  57.               .fontSize(16)
  58.               .fontWeight(FontWeight.Medium)
  59.             Blank()
  60.             Button() {
  61.               Image($r('sys.media.ohos_ic_public_arrow_down'))
  62.                 .width(24)
  63.                 .height(24)
  64.                 .fillColor('#3F72AF')
  65.                 .rotate({ angle: this.cardsExpanded[index] ? 180 : 0 })
  66.                 .animation({
  67.                   duration: 300,
  68.                   curve: Curve.FastOutSlowIn
  69.                 })
  70.             }
  71.             .width(36)
  72.             .height(36)
  73.             .backgroundColor(Color.Transparent)
  74.             .onClick(() => this.toggleCard(index))
  75.           }
  76.           .width('100%')
  77.           .justifyContent(FlexAlign.SpaceBetween)
  78.           .alignItems(VerticalAlign.Center)
  79.           if (this.cardsExpanded[index]) {
  80.             Column() {
  81.               Text(card.content)
  82.                 .fontSize(14)
  83.                 .layoutWeight(1)
  84.             }
  85.             .animation({
  86.               duration: 300,
  87.               curve: Curve.EaseOut
  88.             })
  89.             .height(80)
  90.             .width('100%')
  91.           }
  92.         }
  93.         .padding(16)
  94.         .borderRadius(12)
  95.         .backgroundColor(card.color)
  96.         .width('90%')
  97.         .margin({ top: 16 })
  98.       })
  99.     }
  100.     .width('100%')
  101.     .height('100%')
  102.     .backgroundColor('#F5F5F5')
  103.     .alignItems(HorizontalAlign.Center)
  104.     .expandSafeArea()
  105.   }
  106. }
复制代码
在这个版本中,我们添加了以下改进:
    使用 interface定义卡片数据结构创建卡片数据数组和对应的展开状态数组使用 ForEach循环创建多个卡片修改 toggleCard函数接受索引参数,只切换特定卡片的状态
步骤6:添加滚动容器和全局控制按钮

最后,我们添加滚动容器和全局控制按钮,完善整个页面功能:
  1. // 定义卡片数据接口
  2. interface CardInfo {
  3.   title: string
  4.   content: string
  5.   color: string
  6. }
  7. @Entry
  8. @Component
  9. struct ToggleCard {
  10.   // 使用数组管理多个卡片的展开状态
  11.   @State cardsExpanded: boolean[] = [false, false, false, false]
  12.   // 卡片数据
  13.   @State cardsData: CardInfo[] = [
  14.     {
  15.       title: '个人信息',
  16.       content: '这是个人信息卡片的内容区域,可以放置用户的基本信息,如姓名、年龄、电话等。点击上方按钮可以收起卡片。',
  17.       color: '#ECF2FF'
  18.     },
  19.     {
  20.       title: '支付设置',
  21.       content: '这是支付设置卡片的内容区域,可以放置用户的支付相关信息,包括支付方式、银行卡等信息。点击上方按钮可以收起卡片。',
  22.       color: '#E7F5EF'
  23.     },
  24.     {
  25.       title: '隐私设置',
  26.       content: '这是隐私设置卡片的内容区域,可以放置隐私相关的设置选项,如账号安全、数据权限等内容。点击上方按钮可以收起卡片。',
  27.       color: '#FFF1E6'
  28.     },
  29.     {
  30.       title: '关于系统',
  31.       content: '这是关于系统卡片的内容区域,包含系统版本、更新状态、法律信息等内容。点击上方按钮可以收起卡片。',
  32.       color: '#F5EDFF'
  33.     }
  34.   ]
  35.   // 切换指定卡片的展开/收起状态
  36.   toggleCard(index: number) {
  37.     animateTo({
  38.       duration: 300,
  39.       curve: Curve.EaseOut,
  40.       onFinish: () => {
  41.         console.info(`卡片${index}动画完成`)
  42.       }
  43.     }, () => {
  44.       // 创建新数组并更新特定索引的值
  45.       let newExpandedState = [...this.cardsExpanded]
  46.       newExpandedState[index] = !newExpandedState[index]
  47.       this.cardsExpanded = newExpandedState
  48.     })
  49.   }
  50.   build() {
  51.     Column({ space: 20 }) {
  52.       Text('多卡片展开/收起示例')
  53.         .fontSize(22)
  54.         .fontWeight(FontWeight.Bold)
  55.         .margin({ top: 20 })
  56.       // 使用滚动容器,以便在内容较多时可以滚动查看
  57.       Scroll() {
  58.         Column({ space: 16 }) {
  59.           // 使用ForEach遍历卡片数据,创建多个卡片
  60.           ForEach(this.cardsData, (card: CardInfo, index: number) => {
  61.             // 卡片组件
  62.             Column() {
  63.               Row() {
  64.                 Text(card.title)
  65.                   .fontSize(16)
  66.                   .fontWeight(FontWeight.Medium)
  67.                 Blank()
  68.                 Button() {
  69.                   Image($r('sys.media.ohos_ic_public_arrow_down'))
  70.                     .width(24)
  71.                     .height(24)
  72.                     .fillColor('#3F72AF')
  73.                     .rotate({ angle: this.cardsExpanded[index] ? 180 : 0 })
  74.                     .animation({
  75.                       duration: 300,
  76.                       curve: Curve.FastOutSlowIn
  77.                     })
  78.                 }
  79.                 .width(36)
  80.                 .height(36)
  81.                 .backgroundColor(Color.Transparent)
  82.                 .onClick(() => this.toggleCard(index))
  83.               }
  84.               .width('100%')
  85.               .justifyContent(FlexAlign.SpaceBetween)
  86.               .alignItems(VerticalAlign.Center)
  87.               if (this.cardsExpanded[index]) {
  88.                 Column({ space: 8 }) {
  89.                   Text(card.content)
  90.                     .fontSize(14)
  91.                     .layoutWeight(1)
  92.                 }
  93.                 .animation({
  94.                   duration: 300,
  95.                   curve: Curve.EaseOut
  96.                 })
  97.                 .height(100)
  98.                 .width('100%')
  99.               }
  100.             }
  101.             .padding(16)
  102.             .borderRadius(12)
  103.             .backgroundColor(card.color)
  104.             .width('100%')
  105.             // 添加阴影效果增强立体感
  106.             .shadow({
  107.               radius: 4,
  108.               color: 'rgba(0, 0, 0, 0.1)',
  109.               offsetX: 0,
  110.               offsetY: 2
  111.             })
  112.           })
  113.           // 底部间距
  114.           Blank()
  115.             .height(20)
  116.         }
  117.         .alignItems(HorizontalAlign.Center)
  118.       }
  119.       .align(Alignment.Top)
  120.       .padding(20)
  121.       .layoutWeight(1)
  122.       // 添加底部按钮控制所有卡片
  123.       Row({ space: 20 }) {
  124.         Button('全部展开')
  125.           .width('40%')
  126.           .onClick(() => {
  127.             animateTo({
  128.               duration: 300
  129.             }, () => {
  130.               this.cardsExpanded = this.cardsData.map((_: CardInfo) => true)
  131.             })
  132.           })
  133.         Button('全部收起')
  134.           .width('40%')
  135.           .onClick(() => {
  136.             animateTo({
  137.               duration: 300
  138.             }, () => {
  139.               this.cardsExpanded = this.cardsData.map((_: CardInfo) => false)
  140.             })
  141.           })
  142.       }
  143.       .margin({ bottom: 30 })
  144.     }
  145.     .width('100%')
  146.     .height('100%')
  147.     .backgroundColor('#F5F5F5')
  148.     .alignItems(HorizontalAlign.Center)
  149.     .expandSafeArea()
  150.   }
  151. }
复制代码
这个最终版本添加了以下功能:
    使用 Scroll容器,允许内容超出屏幕时滚动查看添加"全部展开"和"全部收起"按钮,使用 map函数批量更新状态使用 space参数优化布局间距添加阴影效果增强卡片的立体感
三、关键技术点讲解

1. 状态管理

在HarmonyOS的ArkUI框架中,@State装饰器用于声明组件的状态变量。当状态变量改变时,UI会自动更新。在这个示例中:
    对于单个卡片,我们使用 isExpanded布尔值跟踪其展开状态对于多个卡片,我们使用 cardsExpanded数组,数组中的每个元素对应一个卡片的状态
更新数组类型的状态时,需要创建一个新数组而不是直接修改原数组,这样框架才能检测到变化并更新UI:
  1. let newExpandedState = [...this.cardsExpanded]  // 创建副本
  2. newExpandedState[index] = !newExpandedState[index]  // 修改副本
  3. this.cardsExpanded = newExpandedState  // 赋值给状态变量
复制代码
2. 动画实现

HarmonyOS提供了两种主要的动画实现方式:
A. animation属性(声明式动画)

直接应用于组件,当属性值变化时自动触发动画:
  1. .rotate({ angle: this.isExpanded ? 180 : 0 })  // 属性根据状态变化
  2. .animation({  // 动画配置
  3.   duration: 300,  // 持续时间(毫秒)
  4.   curve: Curve.FastOutSlowIn,  // 缓动曲线
  5.   delay: 0,  // 延迟时间(毫秒)
  6.   iterations: 1,  // 重复次数
  7.   playMode: PlayMode.Normal  // 播放模式
  8. })
复制代码
B. animateTo函数(命令式动画)

通过回调函数中改变状态值来触发动画:
  1. animateTo({
  2.   duration: 300,  // 持续时间
  3.   curve: Curve.EaseOut,  // 缓动曲线
  4.   onFinish: () => {  // 动画完成回调
  5.     console.info('动画完成')
  6.   }
  7. }, () => {
  8.   // 在这个函数中更改状态值,这些变化将以动画方式呈现
  9.   this.isExpanded = !this.isExpanded
  10. })
复制代码
3. 条件渲染

使用 if条件语句实现内容的动态显示:
  1. if (this.cardsExpanded[index]) {
  2.   Column() {
  3.     // 这里的内容只在卡片展开时渲染
  4.   }
  5. }
复制代码
4. 数据驱动的UI

通过 ForEach循环根据数据动态创建UI元素:
  1. ForEach(this.cardsData, (card: CardInfo, index: number) => {
  2.   // 根据每个数据项创建卡片
  3. })
复制代码
四、动画曲线详解

HarmonyOS提供了多种缓动曲线,可以实现不同的动画效果:
    Curve.Linear:线性曲线,匀速动画Curve.EaseIn:缓入曲线,动画开始慢,结束快Curve.EaseOut:缓出曲线,动画开始快,结束慢Curve.EaseInOut:缓入缓出曲线,动画开始和结束都慢,中间快Curve.FastOutSlowIn:标准曲线,类似Android标准曲线Curve.LinearOutSlowIn:减速曲线Curve.FastOutLinearIn:加速曲线Curve.ExtremeDeceleration:急缓曲线Curve.Sharp:锐利曲线Curve.Rhythm:节奏曲线Curve.Smooth:平滑曲线Curve.Friction:摩擦曲线/阻尼曲线
在我们的示例中:
    使用 Curve.FastOutSlowIn为箭头旋转提供更自然的视觉效果使用 Curve.EaseOut为内容展开提供平滑的过渡
五、常见问题与解决方案

    动画不流畅:可能是因为在动画过程中执行了复杂操作。解决方法是将复杂计算从动画函数中移出,或者使用 onFinish回调在动画完成后执行。条件渲染内容闪烁:为条件渲染的内容添加 .animation()属性可以实现平滑过渡。卡片高度跳变:为卡片内容设置固定高度,或者使用更复杂的布局计算动态高度。多卡片状态管理复杂:使用数组管理多个状态,并记得创建数组副本而不是直接修改原数组。
六、扩展与优化

你可以进一步扩展这个效果:
    自定义卡片内容:为每个卡片添加更丰富的内容,如表单、图表或列表记住展开状态:使用持久化存储记住用户的卡片展开偏好添加手势交互:支持滑动展开/收起卡片添加动态效果:比如展开时显示阴影或改变背景优化性能:对于非常多的卡片,可以实现虚拟列表或懒加载
七、总结

通过本教程,我们学习了如何在HarmonyOS中实现卡片展开/收起效果,掌握了ArkUI中状态管理和动画实现的核心技巧。关键技术点包括:
    使用 @State管理组件状态使用 .animation()属性和 animateTo()函数实现动画使用条件渲染动态显示内容实现数据驱动的UI创建为多个卡片独立管理状态
这些技术不仅适用于卡片展开/收起效果,也是构建其他复杂交互界面的基础。
希望这篇 HarmonyOS Next 教程对你有所帮助,期待您的 👍点赞、💬评论、🌟收藏 支持。

本帖子中包含更多资源

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

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

本版积分规则

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