frame_animation_view.dart 6.7 KB

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