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