feat(splash): 添加视频播放失败时的 fallback 动画

当设备硬件解码器无法播放视频时(如部分华为老机型),
显示 Logo 缩放 + 文字淡入的 Flutter 动画作为替代方案。

- 添加 fallback 动画控制器和动画序列
- Logo 从小到大弹性缩放 + 透明度渐显
- 文字延迟淡入显示
- 动画时长 3 秒,支持跳过按钮
- 保持与视频相同的用户体验流程

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-14 20:00:20 -08:00
parent 7bfd6822a7
commit ada8a44076
1 changed files with 182 additions and 4 deletions

View File

@ -16,7 +16,8 @@ class SplashPage extends ConsumerStatefulWidget {
ConsumerState<SplashPage> createState() => _SplashPageState();
}
class _SplashPageState extends ConsumerState<SplashPage> {
class _SplashPageState extends ConsumerState<SplashPage>
with SingleTickerProviderStateMixin {
///
VideoPlayerController? _videoController;
@ -29,6 +30,21 @@ class _SplashPageState extends ConsumerState<SplashPage> {
///
bool _isNavigating = false;
/// 使 fallback
bool _videoFailed = false;
/// Fallback
AnimationController? _fallbackAnimationController;
/// Logo
Animation<double>? _logoScaleAnimation;
/// Logo
Animation<double>? _logoOpacityAnimation;
///
Animation<double>? _textOpacityAnimation;
@override
void initState() {
super.initState();
@ -39,6 +55,7 @@ class _SplashPageState extends ConsumerState<SplashPage> {
void dispose() {
_videoController?.removeListener(_onVideoStateChanged);
_videoController?.dispose();
_fallbackAnimationController?.dispose();
super.dispose();
}
@ -108,11 +125,99 @@ class _SplashPageState extends ConsumerState<SplashPage> {
}
}
//
_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<double>([
TweenSequenceItem(
tween: Tween<double>(begin: 0.5, end: 1.05)
.chain(CurveTween(curve: Curves.easeOutBack)),
weight: 40,
),
TweenSequenceItem(
tween: Tween<double>(begin: 1.05, end: 1.0)
.chain(CurveTween(curve: Curves.easeInOut)),
weight: 10,
),
TweenSequenceItem(
tween: ConstantTween<double>(1.0),
weight: 50,
),
]).animate(_fallbackAnimationController!);
// Logo 0 -> 1
_logoOpacityAnimation = TweenSequence<double>([
TweenSequenceItem(
tween: Tween<double>(begin: 0.0, end: 1.0)
.chain(CurveTween(curve: Curves.easeOut)),
weight: 30,
),
TweenSequenceItem(
tween: ConstantTween<double>(1.0),
weight: 70,
),
]).animate(_fallbackAnimationController!);
//
_textOpacityAnimation = TweenSequence<double>([
TweenSequenceItem(
tween: ConstantTween<double>(0.0),
weight: 30,
),
TweenSequenceItem(
tween: Tween<double>(begin: 0.0, end: 1.0)
.chain(CurveTween(curve: Curves.easeOut)),
weight: 20,
),
TweenSequenceItem(
tween: ConstantTween<double>(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<SplashPage> {
),
),
)
else if (_videoFailed)
// fallback
_buildFallbackAnimationView()
else
// Logo
_buildLoadingView(),
@ -264,10 +372,80 @@ class _SplashPageState extends ConsumerState<SplashPage> {
);
}
/// 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(