frame_animation_view.dart 6.0 KB

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