frame_animation_view.dart 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'dart:io';
  4. import 'dart:ui' as ui;
  5. import 'package:archive/archive.dart';
  6. import 'package:crypto/crypto.dart';
  7. import 'package:flutter/cupertino.dart';
  8. import 'package:flutter/services.dart';
  9. import 'package:path_provider/path_provider.dart';
  10. class FrameAnimationView extends StatefulWidget {
  11. final String framePath;
  12. final int frameRate;
  13. final FrameAnimationController? controller;
  14. final double? speed;
  15. final double? width;
  16. final double? height;
  17. const FrameAnimationView(
  18. {super.key,
  19. required this.framePath,
  20. this.frameRate = 25,
  21. this.controller,
  22. this.speed,
  23. this.width,
  24. this.height});
  25. @override
  26. State<StatefulWidget> createState() {
  27. return FrameAnimationViewState();
  28. }
  29. }
  30. class FrameAnimationViewState extends State<FrameAnimationView>
  31. with SingleTickerProviderStateMixin {
  32. late final AnimationController _frameAnimationController;
  33. List<File> imageFiles = [];
  34. List<ui.Image> images = [];
  35. StreamSubscription? _precacheStreamSubscription;
  36. @override
  37. void initState() {
  38. super.initState();
  39. _frameAnimationController = AnimationController(
  40. vsync: this,
  41. );
  42. _frameAnimationController.addListener(() => setState(() {}));
  43. widget.controller?.bindState(this);
  44. loadFrameFromAssets()
  45. .then((_) => initializeImageFiles())
  46. .then((_) => precacheImageFiles())
  47. .then((_) => widget.controller == null || widget.controller!.autoPlay
  48. ? startAnimation()
  49. : null)
  50. .catchError((error) => debugPrint('FrameAnimationView error: $error'));
  51. }
  52. @override
  53. void dispose() {
  54. _frameAnimationController.dispose();
  55. super.dispose();
  56. _precacheStreamSubscription?.cancel();
  57. imageFiles.clear();
  58. for (var image in images) {
  59. image.dispose();
  60. }
  61. images.clear();
  62. }
  63. @override
  64. Widget build(BuildContext context) {
  65. if (imageFiles.isEmpty || !_frameAnimationController.isAnimating) {
  66. return SizedBox(width: widget.width, height: widget.height);
  67. }
  68. final currentFrame = (_frameAnimationController.value * imageFiles.length)
  69. .floor()
  70. .clamp(0, images.length - 1);
  71. debugPrint('currentFrame: $currentFrame');
  72. return RawImage(
  73. image: images[currentFrame],
  74. width: widget.width,
  75. height: widget.height,
  76. );
  77. }
  78. Future<void> loadFrameFromAssets() {
  79. if (widget.framePath.isEmpty) {
  80. throw Exception('framePath is null or empty');
  81. }
  82. if (widget.framePath.endsWith('.zip')) {
  83. return unZipToCache();
  84. } else {
  85. return copyToCache();
  86. }
  87. }
  88. Future<void> unZipToCache() async {
  89. // Load the zip file from assets
  90. final ByteData data = await rootBundle.load(widget.framePath);
  91. final List<int> bytes = data.buffer.asUint8List();
  92. // Decode the zip file
  93. final Archive archive = ZipDecoder().decodeBytes(bytes);
  94. // Create a cache directory
  95. final Directory cacheDir = await createCacheDirectory();
  96. // if the cache directory is not empty and frame images count is equal to the zip file's files count
  97. if (cacheDir.listSync().isNotEmpty &&
  98. cacheDir.listSync().length == archive.length) {
  99. return;
  100. }
  101. // Extract the contents of the zip file
  102. for (final ArchiveFile file in archive) {
  103. final String filename = file.name;
  104. final List<int> fileData = file.content as List<int>;
  105. final File newFile = File('${cacheDir.path}/$filename');
  106. if (newFile.existsSync()) {
  107. newFile.deleteSync();
  108. } else {
  109. newFile.createSync(recursive: true);
  110. }
  111. newFile.writeAsBytesSync(fileData);
  112. }
  113. }
  114. Future<void> copyToCache() async {
  115. // Read AssetManifest.json file
  116. final String manifestContent =
  117. await rootBundle.loadString('AssetManifest.json');
  118. // Parse JSON string into Map
  119. final Map<String, dynamic> manifestMap = json.decode(manifestContent);
  120. // Filter the Map to get file paths that start with folderPath
  121. final List<String> filePaths = manifestMap.keys
  122. .where((String key) => key.startsWith(widget.framePath))
  123. .toList();
  124. // Create a cache directory
  125. final Directory cacheDir = await createCacheDirectory();
  126. // if the cache directory is not empty and frame images count is equal to the zip file's files count
  127. if (cacheDir.listSync().isNotEmpty &&
  128. cacheDir.listSync().length == filePaths.length) {
  129. return;
  130. }
  131. // Copy the files to the cache directory
  132. for (final String path in filePaths) {
  133. final ByteData data = await rootBundle.load(path);
  134. final List<int> bytes = data.buffer.asUint8List();
  135. final File newFile = File('${cacheDir.path}/${path.split('/').last}');
  136. if (newFile.existsSync()) {
  137. newFile.deleteSync();
  138. } else {
  139. newFile.createSync(recursive: true);
  140. }
  141. newFile.writeAsBytesSync(bytes);
  142. imageFiles.add(newFile);
  143. }
  144. }
  145. Future<Directory> createCacheDirectory() async {
  146. // Get the temporary directory
  147. final Directory tempDir = await getTemporaryDirectory();
  148. // Create a new directory within the temporary directory
  149. final Directory cacheDir = Directory('${tempDir.path}/frame_anim_cache');
  150. // Check if the directory exists, if not, create it
  151. if (!await cacheDir.exists()) {
  152. await cacheDir.create(recursive: true);
  153. }
  154. // create unique directory by framePath's md5
  155. final Directory frameDir = Directory(
  156. '${cacheDir.path}/${md5.convert(utf8.encode(widget.framePath)).toString()}');
  157. // Check if the directory exists, if not, create it
  158. if (!await frameDir.exists()) {
  159. await frameDir.create(recursive: true);
  160. }
  161. return frameDir;
  162. }
  163. startAnimation() {
  164. if (imageFiles.isEmpty) {
  165. return;
  166. }
  167. if (_frameAnimationController.isAnimating) {
  168. return;
  169. }
  170. double speed = widget.speed ?? 1.0;
  171. int duration = (1000 ~/ widget.frameRate * imageFiles.length ~/ speed);
  172. debugPrint('frame animation duration: $duration');
  173. _frameAnimationController.duration = Duration(milliseconds: duration);
  174. _frameAnimationController.repeat();
  175. }
  176. initializeImageFiles() async {
  177. final Directory cacheDir = await createCacheDirectory();
  178. imageFiles = cacheDir
  179. .listSync()
  180. .map((file) => File(file.path))
  181. .where((file) =>
  182. file.path.endsWith('.png') ||
  183. file.path.endsWith('.jpg') ||
  184. file.path.endsWith('.jpeg') ||
  185. file.path.endsWith('.webp'))
  186. .toList();
  187. if (imageFiles.isEmpty) {
  188. throw Exception('frame images not found');
  189. }
  190. }
  191. precacheImageFiles() {
  192. _precacheStreamSubscription =
  193. Stream<File>.fromIterable(imageFiles).asyncMap((file) async {
  194. final Uint8List bytes = await file.readAsBytes();
  195. final ui.Codec codec = await ui.instantiateImageCodec(bytes);
  196. final ui.FrameInfo frameInfo = await codec.getNextFrame();
  197. return frameInfo.image;
  198. }).listen((image) {
  199. images.add(image);
  200. });
  201. }
  202. }
  203. class FrameAnimationController {
  204. final bool autoPlay;
  205. FrameAnimationViewState? _state;
  206. FrameAnimationController({this.autoPlay = true});
  207. void play() {
  208. _state?.startAnimation();
  209. }
  210. void stop() {
  211. _state?._frameAnimationController.stop();
  212. }
  213. bool get isPlaying => _state?._frameAnimationController.isAnimating ?? false;
  214. void bindState(FrameAnimationViewState state) {
  215. _state = state;
  216. }
  217. }