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

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,8 +270,30 @@ 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(
onTap: widget.onGenerateSimilar,
behavior: HitTestBehavior.opaque,
child: CachedNetworkImage(
imageUrl: widget.imageUrl,
fit: BoxFit.cover,
placeholder: (_, __) => Container(
color: AppColors.surfaceAlt,
),
errorWidget: (_, __, ___) => Container(
color: AppColors.surfaceAlt,
),
),
),
),
if (showVideoLayer)
Positioned.fill(
child: IgnorePointer(
ignoring: !_videoOpacityTarget,
child: AnimatedOpacity(
opacity: _videoOpacityTarget ? 1.0 : 0.0,
duration: _videoFadeDuration,
curve: Curves.easeInOutCubic,
child: GestureDetector( child: GestureDetector(
onTap: widget.onGenerateSimilar, onTap: widget.onGenerateSimilar,
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
@ -234,21 +310,6 @@ class _VideoCardState extends State<VideoCard> {
), ),
), ),
), ),
)
else
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,
),
), ),
), ),
), ),