From 208147cdf223a28cf963da9a00e91b7b2ad05a11 Mon Sep 17 00:00:00 2001 From: Songzex <2402265378@qq.com> Date: Wed, 23 Sep 2026 16:52:18 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=AF=E5=8A=A8=E5=8A=A8=E7=94=BB=E5=92=8C?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E9=A1=B5=E9=9D=A2=E7=9A=84=E5=8A=A8=E7=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/presentation/pages/splash_page.dart | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 lib/features/auth/presentation/pages/splash_page.dart diff --git a/lib/features/auth/presentation/pages/splash_page.dart b/lib/features/auth/presentation/pages/splash_page.dart new file mode 100644 index 00000000..fe02ea0d --- /dev/null +++ b/lib/features/auth/presentation/pages/splash_page.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/router/route_paths.dart'; +import '../bloc/auth_cubit.dart'; +import '../bloc/auth_state.dart'; + +/// 启动页:展示 App Logo 并循环播放「呼吸缩放」动画。 +/// +/// 该页面作为 App 的第一个路由,衔接原生启动屏(静态 Logo)。 +/// 动画会一直循环,直到 [AuthCubit.appStarted] 完成、登录态确定 +/// ([AuthAuthenticated] / [AuthUnauthenticated])这一「就绪信号」到来, +/// 才停止动画并跳转到首页或登录页。 +class SplashPage extends StatefulWidget { + const SplashPage({super.key}); + + @override + State createState() => _SplashPageState(); +} + +class _SplashPageState extends State + with SingleTickerProviderStateMixin { + /// 呼吸动画控制器(循环往复) + late final AnimationController _breathController; + + /// 就绪信号是否已到达 + bool _ready = false; + + /// 就绪后的目标登录态 + AuthState? _resolvedState; + + /// 防止重复跳转 + bool _navigated = false; + + @override + void initState() { + super.initState(); + _breathController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 900), + )..repeat(reverse: true); + + // 处理进入 splash 时登录态已经确定的情况(appStarted 先于首帧完成) + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) _onReady(context.read().state); + }); + } + + @override + void dispose() { + _breathController.dispose(); + super.dispose(); + } + + /// 记录就绪信号,并尝试跳转 + void _onReady(AuthState state) { + if (state is AuthAuthenticated || state is AuthUnauthenticated) { + _resolvedState = state; + _ready = true; + _maybeNavigate(); + } + // AuthInitial:尚未就绪,继续停留在启动页播放动画 + } + + /// 就绪信号到达即离开启动页,不额外增加打开时间 + void _maybeNavigate() { + if (_navigated || !mounted) return; + if (!_ready || _resolvedState == null) return; + + _navigated = true; + _breathController.stop(); + if (_resolvedState is AuthAuthenticated) { + context.go(RoutePaths.home); + } else { + context.go(RoutePaths.login); + } + } + + @override + Widget build(BuildContext context) { + return BlocListener( + listenWhen: (prev, cur) => + cur is AuthAuthenticated || cur is AuthUnauthenticated, + listener: (context, state) => _onReady(state), + child: Scaffold( + // 与原生启动屏白底保持一致,避免切换时闪色 + backgroundColor: Colors.white, + body: Center( + child: AnimatedBuilder( + animation: _breathController, + builder: (context, child) { + // easeInOut 让呼吸更柔和;scale 从 1.0 起始以贴合原生屏尺寸 + final t = Curves.easeInOut.transform(_breathController.value); + final scale = 1.0 - 0.22 * t; // 1.0 -> 0.78,幅度更大更明显 + final opacity = 1.0 - 0.55 * t; // 1.0 -> 0.45 + return Opacity( + opacity: opacity, + child: Transform.scale(scale: scale, child: child), + ); + }, + // 透明底应用图标,避免白底上出现浅灰"阴影"底块 + child: Image.asset( + 'assets/images/app_logo_gray_transparent.png', + width: 180, + height: 180, + fit: BoxFit.contain, + ), + ), + ), + ), + ); + } +}