yumi-flutter/lib/ui_kit/widgets/svga/sc_svga_asset_widget.dart
2026-05-11 14:08:43 +08:00

352 lines
9.5 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svga/flutter_svga.dart';
typedef SCSvgaMovieConfigurer =
FutureOr<void> Function(MovieEntity movieEntity);
class SCSvgaAssetWidget extends StatefulWidget {
static const int _maxCacheEntries = 8;
static final Map<String, MovieEntity> _cache = <String, MovieEntity>{};
static final Map<String, Future<MovieEntity>> _loadingTasks =
<String, Future<MovieEntity>>{};
static final Map<MovieEntity, int> _activeUseCounts = <MovieEntity, int>{};
const SCSvgaAssetWidget({
super.key,
required this.assetPath,
this.width,
this.height,
this.active = true,
this.loop = false,
this.fit = BoxFit.contain,
this.filterQuality = FilterQuality.low,
this.allowDrawingOverflow = false,
this.fallback,
this.clearsAfterStop = false,
this.onPlaybackStarted,
this.onPlaybackCompleted,
this.dynamicIdentity,
this.movieConfigurer,
});
final String assetPath;
final double? width;
final double? height;
final bool active;
final bool loop;
final BoxFit fit;
final FilterQuality filterQuality;
final bool allowDrawingOverflow;
final Widget? fallback;
final bool clearsAfterStop;
final VoidCallback? onPlaybackStarted;
final VoidCallback? onPlaybackCompleted;
final Object? dynamicIdentity;
final SCSvgaMovieConfigurer? movieConfigurer;
static Future<MovieEntity> preloadAsset(String assetPath) {
return _obtainMovieEntity(assetPath);
}
static void clearMemoryCache({bool includeActive = false}) {
final entries = _cache.entries.toList(growable: false);
for (final entry in entries) {
final entity = entry.value;
if (!includeActive && _isActive(entity)) {
continue;
}
_cache.remove(entry.key);
_disposeMovieEntity(entity);
}
}
static Future<MovieEntity> _acquireMovieEntity(String assetPath) async {
final entity = await _obtainMovieEntity(assetPath);
_retainMovieEntity(entity);
return entity;
}
static Future<MovieEntity> _obtainMovieEntity(String assetPath) async {
final cached = _touchCachedMovieEntity(assetPath);
if (cached != null) {
return cached;
}
final loadingTask = _loadingTasks[assetPath];
if (loadingTask != null) {
return loadingTask;
}
final future = () async {
MovieEntity entity;
try {
entity = await SVGAParser.shared.decodeFromAssets(assetPath);
} catch (_) {
final byteData = await rootBundle.load(assetPath);
entity = await SVGAParser.shared.decodeFromBuffer(
byteData.buffer.asUint8List(),
);
}
entity.autorelease = false;
_cacheMovieEntity(assetPath, entity);
return entity;
}();
_loadingTasks[assetPath] = future;
try {
return await future;
} finally {
_loadingTasks.remove(assetPath);
}
}
static MovieEntity? _touchCachedMovieEntity(String assetPath) {
final cached = _cache.remove(assetPath);
if (cached != null) {
_cache[assetPath] = cached;
}
return cached;
}
static void _cacheMovieEntity(String assetPath, MovieEntity entity) {
final previous = _cache.remove(assetPath);
if (previous != null && !identical(previous, entity)) {
_disposeMovieEntityIfInactive(previous);
}
_cache[assetPath] = entity;
_trimCache();
}
static void _trimCache() {
while (_cache.length > _maxCacheEntries) {
final removableEntry = _cache.entries.firstWhere(
(entry) => !_isActive(entry.value),
orElse: () => _cache.entries.first,
);
if (_isActive(removableEntry.value)) {
_cache.remove(removableEntry.key);
_cache[removableEntry.key] = removableEntry.value;
break;
}
_cache.remove(removableEntry.key);
_disposeMovieEntity(removableEntry.value);
}
}
static void _retainMovieEntity(MovieEntity entity) {
_activeUseCounts[entity] = (_activeUseCounts[entity] ?? 0) + 1;
}
static void _releaseMovieEntity(MovieEntity? entity) {
if (entity == null) {
return;
}
final count = _activeUseCounts[entity] ?? 0;
if (count <= 1) {
_activeUseCounts.remove(entity);
if (!_cache.containsValue(entity)) {
_disposeMovieEntity(entity);
} else {
_trimCache();
}
return;
}
_activeUseCounts[entity] = count - 1;
}
static bool _isActive(MovieEntity entity) =>
(_activeUseCounts[entity] ?? 0) > 0;
static void _disposeMovieEntityIfInactive(MovieEntity entity) {
if (_isActive(entity)) {
return;
}
_disposeMovieEntity(entity);
}
static void _disposeMovieEntity(MovieEntity entity) {
try {
_activeUseCounts.remove(entity);
entity.dispose();
} catch (_) {}
}
@override
State<SCSvgaAssetWidget> createState() => _SCSvgaAssetWidgetState();
}
class _SCSvgaAssetWidgetState extends State<SCSvgaAssetWidget>
with SingleTickerProviderStateMixin {
late final SVGAAnimationController _controller;
String? _loadedAssetPath;
MovieEntity? _retainedMovieEntity;
String? _retainedAssetPath;
bool _hasError = false;
@override
void initState() {
super.initState();
_controller = SVGAAnimationController(vsync: this);
_controller.addStatusListener(_handleAnimationStatusChanged);
_loadAsset();
}
void _handleAnimationStatusChanged(AnimationStatus status) {
if (status == AnimationStatus.forward) {
widget.onPlaybackStarted?.call();
return;
}
if (status == AnimationStatus.completed) {
widget.onPlaybackCompleted?.call();
}
}
@override
void didUpdateWidget(covariant SCSvgaAssetWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.assetPath != widget.assetPath ||
oldWidget.dynamicIdentity != widget.dynamicIdentity) {
_loadAsset();
return;
}
if (oldWidget.active != widget.active || oldWidget.loop != widget.loop) {
_syncPlayback(
restartIfActive: widget.active && oldWidget.active != widget.active,
);
}
}
Future<void> _loadAsset() async {
final assetPath = widget.assetPath;
setState(() {
_hasError = false;
});
MovieEntity? acquiredMovieEntity;
try {
final movieEntity =
_retainedAssetPath == assetPath && _retainedMovieEntity != null
? _retainedMovieEntity!
: await SCSvgaAssetWidget._acquireMovieEntity(assetPath);
final acquiredNewEntity = !identical(movieEntity, _retainedMovieEntity);
if (acquiredNewEntity) {
acquiredMovieEntity = movieEntity;
}
if (widget.movieConfigurer != null) {
movieEntity.dynamicItem.reset();
await widget.movieConfigurer!(movieEntity);
}
if (!mounted || widget.assetPath != assetPath) {
if (acquiredNewEntity) {
SCSvgaAssetWidget._releaseMovieEntity(movieEntity);
acquiredMovieEntity = null;
}
return;
}
setState(() {
_releaseRetainedMovieEntity(except: movieEntity);
_retainedMovieEntity = movieEntity;
_retainedAssetPath = assetPath;
_loadedAssetPath = assetPath;
_controller.videoItem = movieEntity;
});
acquiredMovieEntity = null;
_syncPlayback(restartIfActive: true);
} catch (error) {
SCSvgaAssetWidget._releaseMovieEntity(acquiredMovieEntity);
if (!mounted || widget.assetPath != assetPath) {
return;
}
setState(() {
_controller.videoItem = null;
_releaseRetainedMovieEntity();
_hasError = true;
});
}
}
void _syncPlayback({bool restartIfActive = false}) {
if (_controller.videoItem == null) {
return;
}
if (!widget.active) {
_controller.stop();
_controller.reset();
widget.onPlaybackCompleted?.call();
return;
}
if (widget.loop) {
if (restartIfActive || !_controller.isAnimating) {
_controller.reset();
_controller.repeat();
}
return;
}
if (restartIfActive || !_controller.isAnimating) {
_controller.reset();
_controller.repeat(count: 1);
}
}
@override
void dispose() {
_controller.removeStatusListener(_handleAnimationStatusChanged);
_controller.dispose();
_releaseRetainedMovieEntity();
super.dispose();
}
void _releaseRetainedMovieEntity({MovieEntity? except}) {
final entity = _retainedMovieEntity;
if (entity == null || identical(entity, except)) {
return;
}
_retainedMovieEntity = null;
_retainedAssetPath = null;
SCSvgaAssetWidget._releaseMovieEntity(entity);
}
@override
Widget build(BuildContext context) {
if (_hasError) {
return _buildFallback();
}
final videoItem = _controller.videoItem;
if (videoItem == null || _loadedAssetPath != widget.assetPath) {
return _buildFallback();
}
return SizedBox(
width: widget.width,
height: widget.height,
child: IgnorePointer(
child: SVGAImage(
_controller,
fit: widget.fit,
clearsAfterStop: widget.clearsAfterStop,
filterQuality: widget.filterQuality,
allowDrawingOverflow: widget.allowDrawingOverflow,
preferredSize:
widget.width != null && widget.height != null
? Size(widget.width!, widget.height!)
: null,
),
),
);
}
Widget _buildFallback() {
return SizedBox(
width: widget.width,
height: widget.height,
child: widget.fallback ?? const SizedBox.shrink(),
);
}
}