From ada8a44076076e28d6a5100a14fcaa89ccadf992 Mon Sep 17 00:00:00 2001 From: hailin Date: Sun, 14 Dec 2025 20:00:20 -0800 Subject: [PATCH] =?UTF-8?q?feat(splash):=20=E6=B7=BB=E5=8A=A0=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E6=92=AD=E6=94=BE=E5=A4=B1=E8=B4=A5=E6=97=B6=E7=9A=84?= =?UTF-8?q?=20fallback=20=E5=8A=A8=E7=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 当设备硬件解码器无法播放视频时(如部分华为老机型), 显示 Logo 缩放 + 文字淡入的 Flutter 动画作为替代方案。 - 添加 fallback 动画控制器和动画序列 - Logo 从小到大弹性缩放 + 透明度渐显 - 文字延迟淡入显示 - 动画时长 3 秒,支持跳过按钮 - 保持与视频相同的用户体验流程 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../auth/presentation/pages/splash_page.dart | 186 +++++++++++++++++- 1 file changed, 182 insertions(+), 4 deletions(-) diff --git a/frontend/mobile-app/lib/features/auth/presentation/pages/splash_page.dart b/frontend/mobile-app/lib/features/auth/presentation/pages/splash_page.dart index 04527313..ffee27ac 100644 --- a/frontend/mobile-app/lib/features/auth/presentation/pages/splash_page.dart +++ b/frontend/mobile-app/lib/features/auth/presentation/pages/splash_page.dart @@ -16,7 +16,8 @@ class SplashPage extends ConsumerStatefulWidget { ConsumerState createState() => _SplashPageState(); } -class _SplashPageState extends ConsumerState { +class _SplashPageState extends ConsumerState + with SingleTickerProviderStateMixin { /// 视频播放控制器 VideoPlayerController? _videoController; @@ -29,6 +30,21 @@ class _SplashPageState extends ConsumerState { /// 是否已经开始跳转(防止重复跳转) bool _isNavigating = false; + /// 视频加载是否失败(使用 fallback 动画) + bool _videoFailed = false; + + /// Fallback 动画控制器 + AnimationController? _fallbackAnimationController; + + /// Logo 缩放动画 + Animation? _logoScaleAnimation; + + /// Logo 透明度动画 + Animation? _logoOpacityAnimation; + + /// 文字透明度动画 + Animation? _textOpacityAnimation; + @override void initState() { super.initState(); @@ -39,6 +55,7 @@ class _SplashPageState extends ConsumerState { void dispose() { _videoController?.removeListener(_onVideoStateChanged); _videoController?.dispose(); + _fallbackAnimationController?.dispose(); super.dispose(); } @@ -108,11 +125,99 @@ class _SplashPageState extends ConsumerState { } } - // 如果视频加载失败,直接进行跳转 - _navigateToNextPage(); + // 如果视频加载失败,启动 fallback 动画 + _startFallbackAnimation(); } } + /// 启动 fallback 动画(视频加载失败时使用) + void _startFallbackAnimation() { + debugPrint('[SplashPage] 启动 fallback 动画'); + + setState(() { + _videoFailed = true; + }); + + // 创建动画控制器,总时长 3 秒 + _fallbackAnimationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 3000), + ); + + // Logo 缩放动画:0.5 -> 1.0 -> 1.05 -> 1.0(弹性效果) + _logoScaleAnimation = TweenSequence([ + TweenSequenceItem( + tween: Tween(begin: 0.5, end: 1.05) + .chain(CurveTween(curve: Curves.easeOutBack)), + weight: 40, + ), + TweenSequenceItem( + tween: Tween(begin: 1.05, end: 1.0) + .chain(CurveTween(curve: Curves.easeInOut)), + weight: 10, + ), + TweenSequenceItem( + tween: ConstantTween(1.0), + weight: 50, + ), + ]).animate(_fallbackAnimationController!); + + // Logo 透明度动画:0 -> 1 + _logoOpacityAnimation = TweenSequence([ + TweenSequenceItem( + tween: Tween(begin: 0.0, end: 1.0) + .chain(CurveTween(curve: Curves.easeOut)), + weight: 30, + ), + TweenSequenceItem( + tween: ConstantTween(1.0), + weight: 70, + ), + ]).animate(_fallbackAnimationController!); + + // 文字透明度动画:延迟出现 + _textOpacityAnimation = TweenSequence([ + TweenSequenceItem( + tween: ConstantTween(0.0), + weight: 30, + ), + TweenSequenceItem( + tween: Tween(begin: 0.0, end: 1.0) + .chain(CurveTween(curve: Curves.easeOut)), + weight: 20, + ), + TweenSequenceItem( + tween: ConstantTween(1.0), + weight: 50, + ), + ]).animate(_fallbackAnimationController!); + + // 动画完成后跳转 + _fallbackAnimationController!.addStatusListener((status) { + if (status == AnimationStatus.completed) { + _navigateToNextPage(); + } + }); + + // 开始动画 + _fallbackAnimationController!.forward(); + + // 1秒后显示跳过按钮 + Future.delayed(const Duration(seconds: 1), () { + if (mounted) { + setState(() { + _showSkipButton = true; + }); + } + }); + } + + /// 跳过 fallback 动画 + void _skipFallbackAnimation() { + _fallbackAnimationController?.stop(); + _navigateToNextPage(); + } + /// 视频状态变化监听 void _onVideoStateChanged() { if (_videoController == null) return; @@ -197,6 +302,9 @@ class _SplashPageState extends ConsumerState { ), ), ) + else if (_videoFailed) + // 视频加载失败,显示 fallback 动画 + _buildFallbackAnimationView() else // 视频加载中显示 Logo _buildLoadingView(), @@ -264,10 +372,80 @@ class _SplashPageState extends ConsumerState { ); } + /// 构建 fallback 动画视图(视频加载失败时使用) + Widget _buildFallbackAnimationView() { + return AnimatedBuilder( + animation: _fallbackAnimationController!, + builder: (context, child) { + return Container( + width: double.infinity, + height: double.infinity, + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFFFFF5E6), + Color(0xFFFFE4B5), + ], + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Logo 带缩放和透明度动画 + Opacity( + opacity: _logoOpacityAnimation?.value ?? 1.0, + child: Transform.scale( + scale: _logoScaleAnimation?.value ?? 1.0, + child: Image.asset( + 'assets/images/logo/app_icon.png', + width: 128, + height: 128, + ), + ), + ), + const SizedBox(height: 24), + // 文字带透明度动画 + Opacity( + opacity: _textOpacityAnimation?.value ?? 1.0, + child: const Text( + '榴莲皇后', + style: TextStyle( + fontSize: 32, + fontFamily: 'Noto Sans SC', + fontWeight: FontWeight.w700, + height: 1.25, + letterSpacing: 1.6, + color: Color(0xFF4A3F35), + ), + ), + ), + const SizedBox(height: 16), + // 副标题(延迟显示) + Opacity( + opacity: _textOpacityAnimation?.value ?? 1.0, + child: const Text( + '认种榴莲 · 共享收益', + style: TextStyle( + fontSize: 16, + fontFamily: 'Noto Sans SC', + fontWeight: FontWeight.w400, + color: Color(0xFF8B7355), + ), + ), + ), + ], + ), + ); + }, + ); + } + /// 构建跳过按钮 Widget _buildSkipButton() { return GestureDetector( - onTap: _skipVideo, + onTap: _videoFailed ? _skipFallbackAnimation : _skipVideo, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration(