启动动画和登录页面的动画
This commit is contained in:
114
lib/features/auth/presentation/pages/splash_page.dart
Normal file
114
lib/features/auth/presentation/pages/splash_page.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user