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:
parent
7bfd6822a7
commit
ada8a44076
|
|
@ -16,7 +16,8 @@ class SplashPage extends ConsumerStatefulWidget {
|
||||||
ConsumerState<SplashPage> createState() => _SplashPageState();
|
ConsumerState<SplashPage> createState() => _SplashPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SplashPageState extends ConsumerState<SplashPage> {
|
class _SplashPageState extends ConsumerState<SplashPage>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
/// 视频播放控制器
|
/// 视频播放控制器
|
||||||
VideoPlayerController? _videoController;
|
VideoPlayerController? _videoController;
|
||||||
|
|
||||||
|
|
@ -29,6 +30,21 @@ class _SplashPageState extends ConsumerState<SplashPage> {
|
||||||
/// 是否已经开始跳转(防止重复跳转)
|
/// 是否已经开始跳转(防止重复跳转)
|
||||||
bool _isNavigating = false;
|
bool _isNavigating = false;
|
||||||
|
|
||||||
|
/// 视频加载是否失败(使用 fallback 动画)
|
||||||
|
bool _videoFailed = false;
|
||||||
|
|
||||||
|
/// Fallback 动画控制器
|
||||||
|
AnimationController? _fallbackAnimationController;
|
||||||
|
|
||||||
|
/// Logo 缩放动画
|
||||||
|
Animation<double>? _logoScaleAnimation;
|
||||||
|
|
||||||
|
/// Logo 透明度动画
|
||||||
|
Animation<double>? _logoOpacityAnimation;
|
||||||
|
|
||||||
|
/// 文字透明度动画
|
||||||
|
Animation<double>? _textOpacityAnimation;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
@ -39,6 +55,7 @@ class _SplashPageState extends ConsumerState<SplashPage> {
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_videoController?.removeListener(_onVideoStateChanged);
|
_videoController?.removeListener(_onVideoStateChanged);
|
||||||
_videoController?.dispose();
|
_videoController?.dispose();
|
||||||
|
_fallbackAnimationController?.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,11 +125,99 @@ class _SplashPageState extends ConsumerState<SplashPage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果视频加载失败,直接进行跳转
|
// 如果视频加载失败,启动 fallback 动画
|
||||||
_navigateToNextPage();
|
_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() {
|
void _onVideoStateChanged() {
|
||||||
if (_videoController == null) return;
|
if (_videoController == null) return;
|
||||||
|
|
@ -197,6 +302,9 @@ class _SplashPageState extends ConsumerState<SplashPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
else if (_videoFailed)
|
||||||
|
// 视频加载失败,显示 fallback 动画
|
||||||
|
_buildFallbackAnimationView()
|
||||||
else
|
else
|
||||||
// 视频加载中显示 Logo
|
// 视频加载中显示 Logo
|
||||||
_buildLoadingView(),
|
_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() {
|
Widget _buildSkipButton() {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: _skipVideo,
|
onTap: _videoFailed ? _skipFallbackAnimation : _skipVideo,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue