You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter中如何为自定义Expansion Tile添加展开/收起动画?

自定义ExpansionTile添加展开/收起动画方案

我实现了一个自定义ExpansionTile组件,功能完全正常,但希望为其添加展开/收起时的动画效果。尝试使用AnimatedContainer但未达到预期效果,由于对Flutter动画相关知识不太熟悉,恳请提供相关方向的帮助。

当前代码

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primaryColor: Color.fromARGB(255, 196, 196, 196),
      ),
      home: HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  int active = 1;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('App bar')),
      body: Column(
        children: [
          const SizedBox(
            height: 20,
          ),
          CustomExpansionTile(),
        ],
      ),
    );
  }
}

class CustomExpansionTile extends StatefulWidget {
  const CustomExpansionTile({super.key});

  @override
  State<CustomExpansionTile> createState() => _CustomExpansionTileState();
}

class _CustomExpansionTileState extends State<CustomExpansionTile> {
  bool isExpanded = false;

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: Theme.of(context).primaryColor,
        borderRadius: isExpanded
            ? BorderRadius.circular(10)
            : BorderRadius.circular(
                100,
              ),
        border: Border.all(width: 2, color: Theme.of(context).primaryColor),
      ),
      child: Column(
        children: [
          GestureDetector(
            onTap: () {
              setState(() {
                isExpanded = !isExpanded;
              });
            },
            child: Container(
              decoration: BoxDecoration(
                color: Theme.of(context).primaryColor,
                borderRadius: isExpanded
                    ? const BorderRadius.only(
                        topRight: Radius.circular(10),
                        topLeft: Radius.circular(10),
                      )
                    : BorderRadius.circular(100),
              ),
              child: Row(
                children: [
                  Expanded(
                    child: Text(
                      'Title',
                      textAlign: TextAlign.center,
                      style: Theme.of(context).textTheme.bodyMedium!.copyWith(
                            fontWeight: isExpanded
                                ? FontWeight.w500
                                : FontWeight.normal,
                            overflow: TextOverflow.ellipsis,
                          ),
                      maxLines: 1,
                    ),
                  ),
                  Container(
                    padding: const EdgeInsets.all(4.5),
                    decoration: BoxDecoration(
                      color: isExpanded
                          ? Theme.of(context).primaryColor
                          : Colors.white,
                      shape: BoxShape.circle,
                    ),
                    child: Icon(
                      isExpanded
                          ? Icons.keyboard_arrow_up
                          : Icons.keyboard_arrow_down,
                      color: Colors.black,
                    ),
                  )
                ],
              ),
            ),
          ),
          if (isExpanded)
            Container(
              width: double.infinity,
              padding: const EdgeInsets.only(
                  top: 10, bottom: 20, right: 10, left: 10),
              decoration: BoxDecoration(
                color: Theme.of(context)
                    .primaryColor, // this is important so that GestureDetector takes in the padding in account
                borderRadius: const BorderRadius.only(
                  bottomLeft: Radius.circular(10),
                  bottomRight: Radius.circular(10),
                ),
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Wrap(
                    spacing: 10,
                    runSpacing: 10,
                    children: const [
                      Text('child 1'),
                      Text('child 2'),
                      Text('child 3'),
                    ],
                  ),
                  const SizedBox(
                    height: 20,
                  ),
                ],
              ),
            ),
        ],
      ),
    );
  }
}

当前效果

组件效果截图


解决方案

关键修改点

  1. 外层容器圆角动画:将外层Container替换为AnimatedContainer,实现圆角从圆形到普通圆角的平滑过渡
  2. 箭头旋转动画:用AnimatedRotation包裹箭头图标,实现展开/收起时的旋转效果
  3. 内容区域展开动画:用AnimatedSize包裹展开内容,实现高度从0到自适应的平滑变化,避免内容突兀显示/隐藏

修改后的完整代码

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primaryColor: Color.fromARGB(255, 196, 196, 196),
      ),
      home: HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  int active = 1;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('App bar')),
      body: Column(
        children: [
          const SizedBox(height: 20),
          CustomExpansionTile(),
        ],
      ),
    );
  }
}

class CustomExpansionTile extends StatefulWidget {
  const CustomExpansionTile({super.key});

  @override
  State<CustomExpansionTile> createState() => _CustomExpansionTileState();
}

class _CustomExpansionTileState extends State<CustomExpansionTile> {
  bool isExpanded = false;
  final Duration _animationDuration = Duration(milliseconds: 300);

  @override
  Widget build(BuildContext context) {
    return AnimatedContainer(
      duration: _animationDuration,
      decoration: BoxDecoration(
        color: Theme.of(context).primaryColor,
        borderRadius: isExpanded
            ? BorderRadius.circular(10)
            : BorderRadius.circular(100),
        border: Border.all(width: 2, color: Theme.of(context).primaryColor),
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          GestureDetector(
            onTap: () => setState(() => isExpanded = !isExpanded),
            child: Container(
              padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
              decoration: BoxDecoration(
                color: Theme.of(context).primaryColor,
                borderRadius: isExpanded
                    ? BorderRadius.only(
                        topLeft: Radius.circular(10),
                        topRight: Radius.circular(10),
                      )
                    : BorderRadius.circular(100),
              ),
              child: Row(
                children: [
                  Expanded(
                    child: Text(
                      'Title',
                      textAlign: TextAlign.center,
                      style: Theme.of(context).textTheme.bodyMedium!.copyWith(
                            fontWeight: isExpanded ? FontWeight.w500 : FontWeight.normal,
                            overflow: TextOverflow.ellipsis,
                          ),
                      maxLines: 1,
                    ),
                  ),
                  AnimatedRotation(
                    duration: _animationDuration,
                    turns: isExpanded ? 0.5 : 0.0,
                    child: Container(
                      padding: const EdgeInsets.all(4.5),
                      decoration: BoxDecoration(
                        color: isExpanded
                            ? Theme.of(context).primaryColor
                            : Colors.white,
                        shape: BoxShape.circle,
                      ),
                      child: Icon(
                        Icons.keyboard_arrow_down,
                        color: Colors.black,
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
          AnimatedSize(
            duration: _animationDuration,
            curve: Curves.easeInOut,
            child: isExpanded
                ? Container(
                    width: double.infinity,
                    padding: const EdgeInsets.only(
                      top: 10,
                      bottom: 20,
                      left: 16,
                      right: 16,
                    ),
                    decoration: BoxDecoration(
                      color: Theme.of(context).primaryColor,
                      borderRadius: const BorderRadius.only(
                        bottomLeft: Radius.circular(10),
                        bottomRight: Radius.circular(10),
                      ),
                    ),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Wrap(
                          spacing: 10,
                          runSpacing: 10,
                          children: const [
                            Text('child 1'),
                            Text('child 2'),
                            Text('child 3'),
                          ],
                        ),
                        const SizedBox(height: 20),
                      ],
                    ),
                  )
                : SizedBox.shrink(),
          ),
        ],
      ),
    );
  }
}

说明

  • 统一设置_animationDuration变量,方便统一调整动画速度
  • 箭头通过旋转实现上下切换,无需切换图标,过渡更自然
  • AnimatedSize自动计算内容高度,实现平滑的展开/收起过渡
  • 外层AnimatedContainer负责圆角的平滑变化,与内容动画同步

内容的提问来源于stack exchange,提问作者dipansh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 05:37:51