启动动画和登录页面的动画

This commit is contained in:
2026-09-23 16:52:18 +08:00
parent b3a700491a
commit 208147cdf2

View File

@@ -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<SplashPage> createState() => _SplashPageState();
}
class _SplashPageState extends State<SplashPage>
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<AuthCubit>().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<AuthCubit, AuthState>(
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,
),
),
),
),
);
}
}