115 lines
3.6 KiB
Dart
115 lines
3.6 KiB
Dart
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,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|