优化:卡片播放视频增加淡入效果
This commit is contained in:
parent
f5bb5ff346
commit
a8c9a59167
@ -1,3 +1,4 @@
|
||||
import 'dart:async' show unawaited;
|
||||
import 'dart:io' show File;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
@ -39,6 +40,8 @@ class _VideoCardState extends State<VideoCard> {
|
||||
VideoPlayerController? _controller;
|
||||
/// 递增以作废过期的异步 [ _loadAndPlay ],避免多路初始化互相抢同一个 State
|
||||
int _loadGen = 0;
|
||||
/// 视频层是否已淡入(先 0 再置 true,触发 [AnimatedOpacity])
|
||||
bool _videoOpacityTarget = false;
|
||||
/// 从网络拉取视频时显示底部细进度条(缓存命中则不会出现)
|
||||
bool _showBottomProgress = false;
|
||||
/// 0–1 为确定进度;null 为不确定(无 Content-Length 等)
|
||||
@ -71,7 +74,50 @@ class _VideoCardState extends State<VideoCard> {
|
||||
void _stop() {
|
||||
_controller?.removeListener(_onVideoUpdate);
|
||||
_controller?.pause();
|
||||
if (mounted) setState(() {});
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_videoOpacityTarget = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 淡入略长 + easeInOut,避免「闪一下」的感觉
|
||||
static const Duration _videoFadeDuration = Duration(milliseconds: 820);
|
||||
/// 在 [play] 之后最多等到首帧再开始淡入,否则解码器突然出画会盖过 opacity 动画
|
||||
static const Duration _videoFadeLeadInDeadline = Duration(milliseconds: 1200);
|
||||
|
||||
void _scheduleVideoFadeIn(int gen) {
|
||||
unawaited(_runVideoFadeInSequence(gen));
|
||||
}
|
||||
|
||||
Future<void> _runVideoFadeInSequence(int gen) async {
|
||||
final c = _controller;
|
||||
if (c == null || !mounted || gen != _loadGen || !widget.isActive) return;
|
||||
|
||||
// 与「视频层首帧 build」错开,保证 AnimatedOpacity 从 0 开始做完整动画
|
||||
await Future<void>.delayed(const Duration(milliseconds: 16));
|
||||
if (!mounted || gen != _loadGen || !widget.isActive) return;
|
||||
|
||||
final leadInEnd = DateTime.now().add(_videoFadeLeadInDeadline);
|
||||
while (mounted && gen == _loadGen && widget.isActive && DateTime.now().isBefore(leadInEnd)) {
|
||||
final v = c.value;
|
||||
if (v.isInitialized && v.isPlaying) {
|
||||
final likelyHasFrame = v.position > const Duration(milliseconds: 56) ||
|
||||
(!v.isBuffering && v.size.width > 1 && v.size.height > 1);
|
||||
if (likelyHasFrame) break;
|
||||
}
|
||||
await Future<void>.delayed(const Duration(milliseconds: 24));
|
||||
}
|
||||
if (!mounted || gen != _loadGen || !widget.isActive) return;
|
||||
|
||||
// 再给 Texture 一瞬提交首帧,减轻与透明度不同步的突兀感
|
||||
await Future<void>.delayed(const Duration(milliseconds: 52));
|
||||
if (!mounted || gen != _loadGen || !widget.isActive) return;
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted || gen != _loadGen || !widget.isActive) return;
|
||||
setState(() => _videoOpacityTarget = true);
|
||||
});
|
||||
}
|
||||
|
||||
void _disposeController() {
|
||||
@ -113,7 +159,8 @@ class _VideoCardState extends State<VideoCard> {
|
||||
_stop();
|
||||
return;
|
||||
}
|
||||
setState(() {});
|
||||
setState(() => _videoOpacityTarget = false);
|
||||
_scheduleVideoFadeIn(gen);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -175,12 +222,14 @@ class _VideoCardState extends State<VideoCard> {
|
||||
_disposeController();
|
||||
return;
|
||||
}
|
||||
setState(() {});
|
||||
setState(() => _videoOpacityTarget = false);
|
||||
_scheduleVideoFadeIn(gen);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
_disposeController();
|
||||
setState(() {
|
||||
_clearBottomProgress();
|
||||
_videoOpacityTarget = false;
|
||||
});
|
||||
widget.onStopRequested();
|
||||
}
|
||||
@ -193,7 +242,12 @@ class _VideoCardState extends State<VideoCard> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final showVideo = widget.isActive && _controller != null && _controller!.value.isInitialized;
|
||||
final hasVideoUrl =
|
||||
widget.videoUrl != null && widget.videoUrl!.isNotEmpty;
|
||||
final videoInitialized =
|
||||
_controller != null && _controller!.value.isInitialized;
|
||||
final showVideoLayer =
|
||||
widget.isActive && hasVideoUrl && videoInitialized;
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
@ -216,38 +270,45 @@ class _VideoCardState extends State<VideoCard> {
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
if (showVideo)
|
||||
Positioned.fill(
|
||||
child: GestureDetector(
|
||||
onTap: widget.onGenerateSimilar,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.cover,
|
||||
child: SizedBox(
|
||||
width: _controller!.value.size.width > 0
|
||||
? _controller!.value.size.width
|
||||
: 16,
|
||||
height: _controller!.value.size.height > 0
|
||||
? _controller!.value.size.height
|
||||
: 9,
|
||||
child: VideoPlayer(_controller!),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: GestureDetector(
|
||||
onTap: widget.onGenerateSimilar,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: widget.imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (_, __) => Container(
|
||||
color: AppColors.surfaceAlt,
|
||||
),
|
||||
errorWidget: (_, __, ___) => Container(
|
||||
color: AppColors.surfaceAlt,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
),
|
||||
),
|
||||
if (showVideoLayer)
|
||||
Positioned.fill(
|
||||
child: GestureDetector(
|
||||
onTap: widget.onGenerateSimilar,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: widget.imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (_, __) => Container(
|
||||
color: AppColors.surfaceAlt,
|
||||
),
|
||||
errorWidget: (_, __, ___) => Container(
|
||||
color: AppColors.surfaceAlt,
|
||||
child: IgnorePointer(
|
||||
ignoring: !_videoOpacityTarget,
|
||||
child: AnimatedOpacity(
|
||||
opacity: _videoOpacityTarget ? 1.0 : 0.0,
|
||||
duration: _videoFadeDuration,
|
||||
curve: Curves.easeInOutCubic,
|
||||
child: GestureDetector(
|
||||
onTap: widget.onGenerateSimilar,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.cover,
|
||||
child: SizedBox(
|
||||
width: _controller!.value.size.width > 0
|
||||
? _controller!.value.size.width
|
||||
: 16,
|
||||
height: _controller!.value.size.height > 0
|
||||
? _controller!.value.size.height
|
||||
: 9,
|
||||
child: VideoPlayer(_controller!),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user