优化:卡片播放视频增加淡入效果

This commit is contained in:
ivan 2026-03-29 18:13:52 +08:00
parent f5bb5ff346
commit a8c9a59167

View File

@ -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;
/// 01 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!),
),
),
),
),
),