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

Flutter底部导航页面切换:实现左右滑入自定义动画

实现Tasks标签页卡片左滑退出动画

我来帮你解决这个问题——要实现切换到Goals标签时Tasks页卡片左滑退出的效果,核心是要在标签切换的时机触发退出动画,并且确保动画能完整播放而不被页面销毁打断。下面是具体的解决方案:

步骤1:改造TasksTab,添加退出动画逻辑

首先我们需要给TasksTab的State添加独立的退出动画控制器,并且提供一个外部可以调用的方法来触发退出动画:

class TasksTab extends StatefulWidget {
  // 定义GlobalKey,方便外部获取State实例
  static final GlobalKey<TasksTabState> tabKey = GlobalKey<TasksTabState>();
  
  const TasksTab({Key? key}) : super(key: key ?? TasksTab.tabKey);

  @override
  TasksTabState createState() => TasksTabState();
}

class TasksTabState extends State<TasksTab> with TickerProviderStateMixin {
  late AnimationController _enterController;
  late AnimationController _exitController;
  late Animation<double> _enterAnimation;
  late Animation<double> _exitAnimation;
  bool _isExiting = false;

  @override
  void initState() {
    super.initState();
    // 初始化进入和退出动画控制器
    _enterController = AnimationController(vsync: this, duration: const Duration(milliseconds: 300));
    _exitController = AnimationController(vsync: this, duration: const Duration(milliseconds: 300));

    // 进入动画:从屏幕左侧滑入(-1到0)
    _enterAnimation = Tween<double>(begin: -1.0, end: 0.0).animate(CurvedAnimation(
      parent: _enterController,
      curve: Curves.fastOutSlowIn,
    ));

    // 退出动画:向屏幕左侧滑出(0到-1)
    _exitAnimation = Tween<double>(begin: 0.0, end: -1.0).animate(CurvedAnimation(
      parent: _exitController,
      curve: Curves.fastOutSlowIn,
    ));

    // 页面初始化时触发进入动画
    _enterController.forward();
  }

  @override
  void dispose() {
    _enterController.dispose();
    _exitController.dispose();
    super.dispose();
  }

  // 供外部调用的触发退出动画方法
  Future<void> startExitAnimation() async {
    if (!_isExiting) {
      setState(() => _isExiting = true);
      await _exitController.forward();
    }
  }

  Widget todoCard(Todo todo) {
    final double screenWidth = MediaQuery.of(context).size.width;
    // 根据当前状态选择使用进入或退出动画
    final targetAnimation = _isExiting ? _exitAnimation : _enterAnimation;

    return AnimatedBuilder(
      animation: targetAnimation,
      builder: (context, child) {
        return Transform(
          transform: Matrix4.translationValues(targetAnimation.value * screenWidth, 0.0, 0.0),
          child: child,
        );
      },
      child: ExpansionTile(
        leading: CircularCheckBox(
          activeColor: Colors.redAccent,
          value: todo.complete,
          materialTapTargetSize: MaterialTapTargetSize.padded,
          onChanged: (value) {
            print(value);
            print("Current todo: $todo");
          },
        ),
        title: Text(
          todo.task,
          style: const TextStyle(
            fontSize: 20.0,
            fontWeight: FontWeight.bold,
            fontStyle: FontStyle.italic,
          ),
        ),
        children: [
          Column(
            children: [Text(todo.note)],
          ),
        ],
      ),
    );
  }

  // 其余_buildTodosList和build方法保持不变
}

步骤2:改造Home页面,监听标签切换并触发动画

之前的问题是切换标签时TasksTab会被立即销毁,导致动画无法完整播放。我们可以用IndexedStack保持页面状态,或者延迟页面切换的时机,确保动画播放完成:

class Home extends StatefulWidget {
  final String name;
  const Home({Key? key, required this.name}) : super(key: key);

  @override
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  int _currentPageIndex = 0;
  final List<AppTab> _tabs = [AppTab.tasks, AppTab.goals, AppTab.account];

  // 预加载所有页面,用IndexedStack保持状态
  final List<Widget> _pages = [
    BlocProvider(
      create: (context) => TodosBloc(todosRepository: TodosRepository())..add(LoadTodos()),
      child: const Padding(
        padding: EdgeInsets.only(top: 64.0, left: 8.0, right: 8.0),
        child: TasksTab(),
      ),
    ),
    const GoalsTab(),
    Container(child: Center(child: Text('Account Tab'))),
  ];

  @override
  Widget build(BuildContext context) {
    final tabBloc = BlocProvider.of<TabBloc>(context);
    return BlocListener<TabBloc, AppTab>(
      bloc: tabBloc,
      listener: (context, activeTab) {
        final targetIndex = _tabs.indexOf(activeTab);
        // 检测是否从Tasks标签切换到Goals标签
        if (_currentPageIndex == 0 && targetIndex == 1) {
          // 获取TasksTab的State并触发退出动画
          final tasksTabState = TasksTab.tabKey.currentState;
          tasksTabState?.startExitAnimation().then((_) {
            // 动画完成后再切换页面
            if (mounted) {
              setState(() => _currentPageIndex = targetIndex);
            }
          });
        } else {
          // 其他标签切换直接执行
          setState(() => _currentPageIndex = targetIndex);
        }
      },
      child: Scaffold(
        body: IndexedStack(
          index: _currentPageIndex,
          children: _pages,
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {},
          child: const Icon(Icons.add),
          tooltip: 'Add Todo',
        ),
        bottomNavigationBar: TabSelector(
          activeTab: _tabs[_currentPageIndex],
          onTabSelected: (tab) => tabBloc.add(UpdateTab(tab)),
        ),
      ),
    );
  }
}

关键说明

  1. 分离动画控制器:用两个独立的控制器处理进入和退出动画,避免同一控制器状态冲突,确保动画逻辑清晰。
  2. GlobalKey跨组件调用:通过GlobalKey可以在Home页面直接调用TasksTabState的方法,触发退出动画。
  3. 保持页面状态:使用IndexedStack让所有标签页保持存活,避免切换时页面被立即销毁,确保动画能完整播放。
  4. 延迟页面切换:在触发退出动画后,等待动画完成再切换到Goals页面,保证用户能看到完整的退出效果。

这样修改后,当你点击Goals标签时,Tasks页的所有卡片会同步执行左滑退出动画,动画完成后才会切换到Goals页面,完美实现你想要的效果。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.13 08:57:35