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

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 'dart:io' show File;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -39,6 +40,8 @@ class _VideoCardState extends State<VideoCard> {
VideoPlayerController? _controller; VideoPlayerController? _controller;
/// [ _loadAndPlay ] State /// [ _loadAndPlay ] State
int _loadGen = 0; int _loadGen = 0;
/// 0 true [AnimatedOpacity]
bool _videoOpacityTarget = false;
/// ///
bool _showBottomProgress = false; bool _showBottomProgress = false;
/// 01 null Content-Length /// 01 null Content-Length
@ -71,7 +74,50 @@ class _VideoCardState extends State<VideoCard> {
void _stop() { void _stop() {
_controller?.removeListener(_onVideoUpdate); _controller?.removeListener(_onVideoUpdate);
_controller?.pause(); _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() { void _disposeController() {
@ -113,7 +159,8 @@ class _VideoCardState extends State<VideoCard> {
_stop(); _stop();
return; return;
} }
setState(() {}); setState(() => _videoOpacityTarget = false);
_scheduleVideoFadeIn(gen);
return; return;
} }
@ -175,12 +222,14 @@ class _VideoCardState extends State<VideoCard> {
_disposeController(); _disposeController();
return; return;
} }
setState(() {}); setState(() => _videoOpacityTarget = false);
_scheduleVideoFadeIn(gen);
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
_disposeController(); _disposeController();
setState(() { setState(() {
_clearBottomProgress(); _clearBottomProgress();
_videoOpacityTarget = false;
}); });
widget.onStopRequested(); widget.onStopRequested();
} }
@ -193,7 +242,12 @@ class _VideoCardState extends State<VideoCard> {
@override @override
Widget build(BuildContext context) { 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( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
@ -216,38 +270,45 @@ class _VideoCardState extends State<VideoCard> {
child: Stack( child: Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
if (showVideo) Positioned.fill(
Positioned.fill( child: GestureDetector(
child: GestureDetector( onTap: widget.onGenerateSimilar,
onTap: widget.onGenerateSimilar, behavior: HitTestBehavior.opaque,
behavior: HitTestBehavior.opaque, child: CachedNetworkImage(
child: FittedBox( imageUrl: widget.imageUrl,
fit: BoxFit.cover, fit: BoxFit.cover,
child: SizedBox( placeholder: (_, __) => Container(
width: _controller!.value.size.width > 0 color: AppColors.surfaceAlt,
? _controller!.value.size.width ),
: 16, errorWidget: (_, __, ___) => Container(
height: _controller!.value.size.height > 0 color: AppColors.surfaceAlt,
? _controller!.value.size.height
: 9,
child: VideoPlayer(_controller!),
),
), ),
), ),
) ),
else ),
if (showVideoLayer)
Positioned.fill( Positioned.fill(
child: GestureDetector( child: IgnorePointer(
onTap: widget.onGenerateSimilar, ignoring: !_videoOpacityTarget,
behavior: HitTestBehavior.opaque, child: AnimatedOpacity(
child: CachedNetworkImage( opacity: _videoOpacityTarget ? 1.0 : 0.0,
imageUrl: widget.imageUrl, duration: _videoFadeDuration,
fit: BoxFit.cover, curve: Curves.easeInOutCubic,
placeholder: (_, __) => Container( child: GestureDetector(
color: AppColors.surfaceAlt, onTap: widget.onGenerateSimilar,
), behavior: HitTestBehavior.opaque,
errorWidget: (_, __, ___) => Container( child: FittedBox(
color: AppColors.surfaceAlt, 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!),
),
),
), ),
), ),
), ),