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

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

在移动应用开发中,卡片是一种常见且实用的UI元素,能够将信息以紧凑且易于理解的方式呈现给用户。

本教程将详细讲解如何在HarmonyOS中实现卡片的展开/收起效果,通过这个实例,你将掌握ArkUI中状态管理和动画实现的核心技巧。

一、实现效果预览

我们将实现一个包含多个卡片的页面,整个交互过程都有平滑的动画效果。

  1. 每个卡片默认只显示标题,点击右侧箭头按钮后可以展开显示详细内容,再次点击则收起。
  2. 实现"全部展开"和"全部收起"的功能按钮。

二、实现步骤

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

首先,我们需要创建一个基本的页面结构,包含一个标题和一个简单的卡片:

typescript 复制代码
@Entry
@Component
struct ToggleCard {
  build() {
    Column() {
      Text('卡片展开/收起示例')
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20 })
      
      // 一个简单的卡片
      Column() {
        Text('个人信息')
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
      }
      .width('90%')
      .padding(16)
      .backgroundColor('#ECF2FF')
      .borderRadius(12)
      .margin({ top: 20 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
    .alignItems(HorizontalAlign.Center)
    .expandSafeArea()
  }
}

这段代码创建了一个基本的页面,顶部有一个标题,下方有一个简单的卡片,卡片只包含一个标题文本。

步骤2:添加卡片标题行和展开按钮

接下来,我们为卡片添加一个标题行,并在右侧添加一个展开/收起按钮:

typescript 复制代码
@Entry
@Component
struct ToggleCard {
  build() {
    Column() {
      Text('卡片展开/收起示例')
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20 })
      
      // 一个带展开按钮的卡片
      Column() {
        Row() {
          Text('个人信息')
            .fontSize(16)
            .fontWeight(FontWeight.Medium)
          
          Blank()  // 占位,使按钮靠右显示
          
          Button() {
            Image($r('sys.media.ohos_ic_public_arrow_down'))
              .width(24)
              .height(24)
              .fillColor('#3F72AF')
          }
          .width(36)
          .height(36)
          .backgroundColor(Color.Transparent)
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .alignItems(VerticalAlign.Center)
      }
      .width('90%')
      .padding(16)
      .backgroundColor('#ECF2FF')
      .borderRadius(12)
      .margin({ top: 20 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
    .alignItems(HorizontalAlign.Center)
    .expandSafeArea()
  }
}

现在我们的卡片有了标题和一个展开按钮,但点击按钮还没有任何效果。接下来我们将添加状态管理和交互逻辑。

步骤3:添加状态变量控制卡片展开/收起

要实现卡片的展开/收起效果,我们需要添加一个状态变量来跟踪卡片是否处于展开状态:

typescript 复制代码
@Entry
@Component
struct ToggleCard {
  @State isExpanded: boolean = false  // 控制卡片展开/收起状态
  
  build() {
    Column() {
      Text('卡片展开/收起示例')
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20 })
      
      // 一个带展开按钮的卡片
      Column() {
        Row() {
          Text('个人信息')
            .fontSize(16)
            .fontWeight(FontWeight.Medium)
          
          Blank()
          
          Button() {
            Image($r('sys.media.ohos_ic_public_arrow_down'))
              .width(24)
              .height(24)
              .fillColor('#3F72AF')
          }
          .width(36)
          .height(36)
          .backgroundColor(Color.Transparent)
          .onClick(() => {
            this.isExpanded = !this.isExpanded  // 点击按钮切换状态
          })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .alignItems(VerticalAlign.Center)
        
        // 根据展开状态条件渲染内容
        if (this.isExpanded) {
          Text('这是展开后显示的内容,包含详细信息。')
            .fontSize(14)
            .margin({ top: 8 })
        }
      }
      .width('90%')
      .padding(16)
      .backgroundColor('#ECF2FF')
      .borderRadius(12)
      .margin({ top: 20 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
    .alignItems(HorizontalAlign.Center)
    .expandSafeArea()
  }
}

现在我们添加了一个@State状态变量isExpanded,并在按钮的onClick事件中切换它的值。同时,我们使用if条件语句根据isExpanded的值决定是否显示卡片的详细内容。

步骤4:添加基本动画效果

接下来,我们将为卡片的展开/收起添加动画效果,让交互更加流畅自然。HarmonyOS提供了两种主要的动画实现方式:

  1. animation属性:直接应用于组件的声明式动画
  2. animateTo函数:通过改变状态触发的命令式动画

首先,我们使用这两种方式来实现箭头旋转和内容展开的动画效果:

typescript 复制代码
@Entry
@Component
struct ToggleCard {
  @State isExpanded: boolean = false
  
  // 切换卡片展开/收起状态
  toggleCard() {
    // 使用animateTo实现状态变化的动画
    animateTo({
      duration: 300,  // 动画持续时间(毫秒)
      curve: Curve.EaseOut,  // 缓动曲线
      onFinish: () => {
        console.info('卡片动画完成')  // 动画完成回调
      }
    }, () => {
      this.isExpanded = !this.isExpanded  // 在动画函数中切换状态
    })
  }
  
  build() {
    Column() {
      Text('卡片展开/收起示例')
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20 })
      
      // 带动画效果的卡片
      Column() {
        Row() {
          Text('个人信息')
            .fontSize(16)
            .fontWeight(FontWeight.Medium)
          
          Blank()
          
          Button() {
            Image($r('sys.media.ohos_ic_public_arrow_down'))
              .width(24)
              .height(24)
              .fillColor('#3F72AF')
              .rotate({ angle: this.isExpanded ? 180 : 0 })  // 根据状态控制旋转角度
              .animation({  // 为旋转添加动画效果
                duration: 300,
                curve: Curve.FastOutSlowIn
              })
          }
          .width(36)
          .height(36)
          .backgroundColor(Color.Transparent)
          .onClick(() => this.toggleCard())  // 调用切换函数
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .alignItems(VerticalAlign.Center)
        
        if (this.isExpanded) {
          Column() {
            Text('这是展开后显示的内容,包含详细信息。')
              .fontSize(14)
              .layoutWeight(1)
          }
          .animation({  // 为内容添加动画效果
            duration: 300,
            curve: Curve.EaseOut
          })
          .height(80)  // 固定高度便于观察动画效果
          .width('100%')
        }
      }
      .width('90%')
      .padding(16)
      .backgroundColor('#ECF2FF')
      .borderRadius(12)
      .margin({ top: 20 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
    .alignItems(HorizontalAlign.Center)
    .expandSafeArea()
  }
}

在这个版本中,我们添加了两种动画实现:

  1. 使用animateTo函数来实现状态变化时的动画效果
  2. 使用.animation()属性为箭头旋转和内容展示添加过渡动画

这两种动画方式的区别:

  • animation属性:简单直接,适用于属性变化的过渡动画
  • animateTo函数:更灵活,可以一次性动画多个状态变化,有完成回调

步骤5:扩展为多卡片结构

现在让我们扩展代码,实现多个可独立展开/收起的卡片:

typescript 复制代码
// 定义卡片数据接口
interface CardInfo {
  title: string
  content: string
  color: string
}

@Entry
@Component
struct ToggleCard {
  // 使用数组管理多个卡片的展开状态
  @State cardsExpanded: boolean[] = [false, false, false]
  
  // 卡片数据
  private cardsData: CardInfo[] = [
    {
      title: '个人信息',
      content: '这是个人信息卡片的内容区域,可以放置用户的基本信息,如姓名、年龄、电话等。',
      color: '#ECF2FF'
    },
    {
      title: '支付设置',
      content: '这是支付设置卡片的内容区域,可以放置用户的支付相关信息,包括支付方式、银行卡等信息。',
      color: '#E7F5EF'
    },
    {
      title: '隐私设置',
      content: '这是隐私设置卡片的内容区域,可以放置隐私相关的设置选项,如账号安全、数据权限等内容。',
      color: '#FFF1E6'
    }
  ]

  // 切换指定卡片的展开/收起状态
  toggleCard(index: number) {
    animateTo({
      duration: 300,
      curve: Curve.EaseOut,
      onFinish: () => {
        console.info(`卡片${index}动画完成`)
      }
    }, () => {
      // 创建新数组并更新特定索引的值
      let newExpandedState = [...this.cardsExpanded]
      newExpandedState[index] = !newExpandedState[index]
      this.cardsExpanded = newExpandedState
    })
  }

  build() {
    Column() {
      Text('多卡片展开/收起示例')
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20 })
      
      // 使用ForEach遍历卡片数据,创建多个卡片
      ForEach(this.cardsData, (card: CardInfo, index: number) => {
        // 卡片组件
        Column() {
          Row() {
            Text(card.title)
              .fontSize(16)
              .fontWeight(FontWeight.Medium)
            
            Blank()
            
            Button() {
              Image($r('sys.media.ohos_ic_public_arrow_down'))
                .width(24)
                .height(24)
                .fillColor('#3F72AF')
                .rotate({ angle: this.cardsExpanded[index] ? 180 : 0 })
                .animation({
                  duration: 300,
                  curve: Curve.FastOutSlowIn
                })
            }
            .width(36)
            .height(36)
            .backgroundColor(Color.Transparent)
            .onClick(() => this.toggleCard(index))
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .alignItems(VerticalAlign.Center)
          
          if (this.cardsExpanded[index]) {
            Column() {
              Text(card.content)
                .fontSize(14)
                .layoutWeight(1)
            }
            .animation({
              duration: 300,
              curve: Curve.EaseOut
            })
            .height(80)
            .width('100%')
          }
        }
        .padding(16)
        .borderRadius(12)
        .backgroundColor(card.color)
        .width('90%')
        .margin({ top: 16 })
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
    .alignItems(HorizontalAlign.Center)
    .expandSafeArea()
  }
}

在这个版本中,我们添加了以下改进:

  1. 使用interface定义卡片数据结构
  2. 创建卡片数据数组和对应的展开状态数组
  3. 使用ForEach循环创建多个卡片
  4. 修改toggleCard函数接受索引参数,只切换特定卡片的状态

步骤6:添加滚动容器和全局控制按钮

最后,我们添加滚动容器和全局控制按钮,完善整个页面功能:

typescript 复制代码
// 定义卡片数据接口
interface CardInfo {
  title: string
  content: string
  color: string
}

@Entry
@Component
struct ToggleCard {
  // 使用数组管理多个卡片的展开状态
  @State cardsExpanded: boolean[] = [false, false, false, false]
  
  // 卡片数据
  @State cardsData: CardInfo[] = [
    {
      title: '个人信息',
      content: '这是个人信息卡片的内容区域,可以放置用户的基本信息,如姓名、年龄、电话等。点击上方按钮可以收起卡片。',
      color: '#ECF2FF'
    },
    {
      title: '支付设置',
      content: '这是支付设置卡片的内容区域,可以放置用户的支付相关信息,包括支付方式、银行卡等信息。点击上方按钮可以收起卡片。',
      color: '#E7F5EF'
    },
    {
      title: '隐私设置',
      content: '这是隐私设置卡片的内容区域,可以放置隐私相关的设置选项,如账号安全、数据权限等内容。点击上方按钮可以收起卡片。',
      color: '#FFF1E6'
    },
    {
      title: '关于系统',
      content: '这是关于系统卡片的内容区域,包含系统版本、更新状态、法律信息等内容。点击上方按钮可以收起卡片。',
      color: '#F5EDFF'
    }
  ]

  // 切换指定卡片的展开/收起状态
  toggleCard(index: number) {
    animateTo({
      duration: 300,
      curve: Curve.EaseOut,
      onFinish: () => {
        console.info(`卡片${index}动画完成`)
      }
    }, () => {
      // 创建新数组并更新特定索引的值
      let newExpandedState = [...this.cardsExpanded]
      newExpandedState[index] = !newExpandedState[index]
      this.cardsExpanded = newExpandedState
    })
  }

  build() {
    Column({ space: 20 }) {
      Text('多卡片展开/收起示例')
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20 })
        
      // 使用滚动容器,以便在内容较多时可以滚动查看
      Scroll() {
        Column({ space: 16 }) {
          // 使用ForEach遍历卡片数据,创建多个卡片
          ForEach(this.cardsData, (card: CardInfo, index: number) => {
            // 卡片组件
            Column() {
              Row() {
                Text(card.title)
                  .fontSize(16)
                  .fontWeight(FontWeight.Medium)
                
                Blank()
                
                Button() {
                  Image($r('sys.media.ohos_ic_public_arrow_down'))
                    .width(24)
                    .height(24)
                    .fillColor('#3F72AF')
                    .rotate({ angle: this.cardsExpanded[index] ? 180 : 0 })
                    .animation({
                      duration: 300,
                      curve: Curve.FastOutSlowIn
                    })
                }
                .width(36)
                .height(36)
                .backgroundColor(Color.Transparent)
                .onClick(() => this.toggleCard(index))
              }
              .width('100%')
              .justifyContent(FlexAlign.SpaceBetween)
              .alignItems(VerticalAlign.Center)
              
              if (this.cardsExpanded[index]) {
                Column({ space: 8 }) {
                  Text(card.content)
                    .fontSize(14)
                    .layoutWeight(1)
                }
                .animation({
                  duration: 300,
                  curve: Curve.EaseOut
                })
                .height(100)
                .width('100%')
              }
            }
            .padding(16)
            .borderRadius(12)
            .backgroundColor(card.color)
            .width('100%')
            // 添加阴影效果增强立体感
            .shadow({
              radius: 4,
              color: 'rgba(0, 0, 0, 0.1)',
              offsetX: 0,
              offsetY: 2
            })
          })
          
          // 底部间距
          Blank()
            .height(20)
        }
        .alignItems(HorizontalAlign.Center)
      }
      .align(Alignment.Top)
      .padding(20)
      .layoutWeight(1)
      
      // 添加底部按钮控制所有卡片
      Row({ space: 20 }) {
        Button('全部展开')
          .width('40%')
          .onClick(() => {
            animateTo({
              duration: 300
            }, () => {
              this.cardsExpanded = this.cardsData.map((_: CardInfo) => true)
            })
          })
        
        Button('全部收起')
          .width('40%')
          .onClick(() => {
            animateTo({
              duration: 300
            }, () => {
              this.cardsExpanded = this.cardsData.map((_: CardInfo) => false)
            })
          })
      }
      .margin({ bottom: 30 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
    .alignItems(HorizontalAlign.Center)
    .expandSafeArea()
  }
}

这个最终版本添加了以下功能:

  1. 使用Scroll容器,允许内容超出屏幕时滚动查看
  2. 添加"全部展开"和"全部收起"按钮,使用map函数批量更新状态
  3. 使用space参数优化布局间距
  4. 添加阴影效果增强卡片的立体感

三、关键技术点讲解

1. 状态管理

在HarmonyOS的ArkUI框架中,@State装饰器用于声明组件的状态变量。当状态变量改变时,UI会自动更新。在这个示例中:

  • 对于单个卡片,我们使用isExpanded布尔值跟踪其展开状态
  • 对于多个卡片,我们使用cardsExpanded数组,数组中的每个元素对应一个卡片的状态

更新数组类型的状态时,需要创建一个新数组而不是直接修改原数组,这样框架才能检测到变化并更新UI:

typescript 复制代码
let newExpandedState = [...this.cardsExpanded]  // 创建副本
newExpandedState[index] = !newExpandedState[index]  // 修改副本
this.cardsExpanded = newExpandedState  // 赋值给状态变量

2. 动画实现

HarmonyOS提供了两种主要的动画实现方式:

A. animation属性(声明式动画)

直接应用于组件,当属性值变化时自动触发动画:

typescript 复制代码
.rotate({ angle: this.isExpanded ? 180 : 0 })  // 属性根据状态变化
.animation({  // 动画配置
  duration: 300,  // 持续时间(毫秒)
  curve: Curve.FastOutSlowIn,  // 缓动曲线
  delay: 0,  // 延迟时间(毫秒)
  iterations: 1,  // 重复次数
  playMode: PlayMode.Normal  // 播放模式
})
B. animateTo函数(命令式动画)

通过回调函数中改变状态值来触发动画:

typescript 复制代码
animateTo({
  duration: 300,  // 持续时间
  curve: Curve.EaseOut,  // 缓动曲线
  onFinish: () => {  // 动画完成回调
    console.info('动画完成')
  }
}, () => {
  // 在这个函数中更改状态值,这些变化将以动画方式呈现
  this.isExpanded = !this.isExpanded
})

3. 条件渲染

使用if条件语句实现内容的动态显示:

typescript 复制代码
if (this.cardsExpanded[index]) {
  Column() {
    // 这里的内容只在卡片展开时渲染
  }
  .animation({ duration: 300 })  // 为条件渲染的内容添加动画
}

4. 数据驱动的UI

通过ForEach循环根据数据动态创建UI元素:

typescript 复制代码
ForEach(this.cardsData, (card: CardInfo, index: number) => {
  // 根据每个数据项创建卡片
})

四、动画曲线详解

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为内容展开提供平滑的过渡

五、常见问题与解决方案

  1. 动画不流畅 :可能是因为在动画过程中执行了复杂操作。解决方法是将复杂计算从动画函数中移出,或者使用onFinish回调在动画完成后执行。

  2. 条件渲染内容闪烁 :为条件渲染的内容添加.animation()属性可以实现平滑过渡。

  3. 卡片高度跳变:为卡片内容设置固定高度,或者使用更复杂的布局计算动态高度。

  4. 多卡片状态管理复杂:使用数组管理多个状态,并记得创建数组副本而不是直接修改原数组。

六、扩展与优化

你可以进一步扩展这个效果:

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

七、总结

通过本教程,我们学习了如何在HarmonyOS中实现卡片展开/收起效果,掌握了ArkUI中状态管理和动画实现的核心技巧。关键技术点包括:

  1. 使用@State管理组件状态
  2. 使用.animation()属性和animateTo()函数实现动画
  3. 使用条件渲染动态显示内容
  4. 实现数据驱动的UI创建
  5. 为多个卡片独立管理状态

这些技术不仅适用于卡片展开/收起效果,也是构建其他复杂交互界面的基础。

希望这篇 HarmonyOS Next 教程对你有所帮助,期待您的 👍点赞、💬评论、🌟收藏 支持。

相关推荐
mldong2 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排2 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60613 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角4 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!4 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰4 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi6 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen6 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马7 小时前
AI源码分析:拆解模型底层实现逻辑
前端
IT_陈寒7 小时前
React子组件莫名其妙重渲染?你可能漏了这个Hook
前端·人工智能·后端