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

如何在Flutter启动页实现字母聚合及底部按钮弹出动画?

Flutter启动页动画实现方案

核心实现思路

  1. 用AnimationController统筹动画时序:先执行四个字母从四方向向中心聚合的位移动画,完成后触发底部社交按钮的弹出动画(结合透明度+位移动画)。
  2. 借助Animation对象定义每个字母的偏移路径,以及按钮的透明度和位置变化。
  3. 监听动画完成状态,在所有动画结束后执行页面跳转逻辑。

完整代码实现

import 'package:flutter/material.dart';

class SplashScreen extends StatefulWidget {
  const SplashScreen({Key? key}) : super(key: key);

  @override
  State<SplashScreen> createState() => _SplashScreenState();
}

class _SplashScreenState extends State<SplashScreen> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  // 四个字母的位移动画
  late Animation<Offset> _bOffset;
  late Animation<Offset> _cOffset;
  late Animation<Offset> _dOffset;
  late Animation<Offset> _eOffset;
  // 底部按钮的透明度和位移动画
  late Animation<double> _buttonOpacity;
  late Animation<Offset> _buttonOffset;

  @override
  void initState() {
    super.initState();
    // 初始化动画控制器,总时长3秒(字母聚合2秒,按钮弹出1秒)
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 3),
    );

    // 配置字母动画:从四个屏幕外方向向中心移动
    _bOffset = Tween<Offset>(
      begin: const Offset(-1.5, 0), // 左侧屏幕外
      end: Offset.zero,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: const Interval(0.0, 0.66, curve: Curves.easeOut), // 前2秒执行
    ));

    _cOffset = Tween<Offset>(
      begin: const Offset(0, -1.5), // 顶部屏幕外
      end: Offset.zero,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: const Interval(0.0, 0.66, curve: Curves.easeOut),
    ));

    _dOffset = Tween<Offset>(
      begin: const Offset(0, 1.5), // 底部屏幕外
      end: Offset.zero,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: const Interval(0.0, 0.66, curve: Curves.easeOut),
    ));

    _eOffset = Tween<Offset>(
      begin: const Offset(1.5, 0), // 右侧屏幕外
      end: Offset.zero,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: const Interval(0.0, 0.66, curve: Curves.easeOut),
    ));

    // 配置按钮动画:从底部向上弹出并渐显
    _buttonOpacity = Tween<double>(
      begin: 0.0,
      end: 1.0,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: const Interval(0.66, 1.0, curve: Curves.easeInOut), // 最后1秒执行
    ));

    _buttonOffset = Tween<Offset>(
      begin: const Offset(0, 1), // 初始在底部下方
      end: Offset.zero,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: const Interval(0.66, 1.0, curve: Curves.easeInOut),
    ));

    // 监听动画完成状态,执行页面跳转
    _controller.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        navigatePage();
      }
    });

    // 启动动画
    _controller.forward();
  }

  void navigatePage() async {
    // 替换为你的登录状态判断逻辑
    bool isLoggedIn = false;
    await Future.delayed(const Duration(milliseconds: 500)); // 按钮显示后稍作停留
    if (mounted) {
      Navigator.pushReplacementNamed(context, isLoggedIn ? "home" : "login");
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xffF7F8F8),
      body: Stack(
        alignment: Alignment.center,
        children: [
          // 字母聚合动画区域
          Stack(
            alignment: Alignment.center,
            children: [
              Transform.translate(
                offset: _bOffset.value * MediaQuery.of(context).size.width,
                child: const Text(
                  "B",
                  style: TextStyle(fontSize: 48, fontWeight: FontWeight.bold),
                ),
              ),
              Transform.translate(
                offset: _cOffset.value * MediaQuery.of(context).size.height,
                child: const Text(
                  "C",
                  style: TextStyle(fontSize: 48, fontWeight: FontWeight.bold),
                ),
              ),
              Transform.translate(
                offset: _dOffset.value * MediaQuery.of(context).size.height,
                child: const Text(
                  "D",
                  style: TextStyle(fontSize: 48, fontWeight: FontWeight.bold),
                ),
              ),
              Transform.translate(
                offset: _eOffset.value * MediaQuery.of(context).size.width,
                child: const Text(
                  "E",
                  style: TextStyle(fontSize: 48, fontWeight: FontWeight.bold),
                ),
              ),
            ],
          ),
          // 底部社交按钮区域
          Positioned(
            bottom: 40,
            left: 0,
            right: 0,
            child: SlideTransition(
              position: _buttonOffset,
              child: FadeTransition(
                opacity: _buttonOpacity,
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    IconButton(
                      icon: const Icon(Icons.facebook),
                      onPressed: () {},
                    ),
                    IconButton(
                      icon: const Icon(Icons.google),
                      onPressed: () {},
                    ),
                    IconButton(
                      icon: const Icon(Icons.apple),
                      onPressed: () {},
                    ),
                  ],
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

关键细节说明

  • 动画时序通过Interval精确控制:字母动画在前2秒(0.0到0.66区间)执行,按钮动画在最后1秒(0.66到1.0区间)触发,确保顺序执行。
  • 用屏幕尺寸计算偏移量,让字母从屏幕外进入,适配不同设备分辨率。
  • 动画完成后延迟500毫秒再跳转,给按钮显示留足视觉停留时间。
  • 混入SingleTickerProviderStateMixin为动画控制器提供帧同步支持,避免资源浪费。

内容的提问来源于stack exchange,提问作者Shruti Ramnandan Sharma

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 14:25:29