Browse Source

[New]新增帧动画控件

zhipeng 1 year ago
parent
commit
bb2bec4772
3 changed files with 220 additions and 1 deletions
  1. 216 0
      lib/widget/frame_animation_view.dart
  2. 1 1
      pubspec.lock
  3. 3 0
      pubspec.yaml

+ 216 - 0
lib/widget/frame_animation_view.dart

@@ -0,0 +1,216 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+import 'dart:ui' as ui;
+
+import 'package:archive/archive.dart';
+import 'package:crypto/crypto.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/services.dart';
+import 'package:path_provider/path_provider.dart';
+
+class FrameAnimationView extends StatefulWidget {
+  final String framePath;
+
+  final int frameRate;
+
+  final double? width;
+
+  final double? height;
+
+  const FrameAnimationView(
+      {super.key,
+      required this.framePath,
+      this.frameRate = 25,
+      this.width,
+      this.height});
+
+  @override
+  State<StatefulWidget> createState() {
+    return _FrameAnimationViewState();
+  }
+}
+
+class _FrameAnimationViewState extends State<FrameAnimationView> {
+  int _currentFrame = 0;
+  Timer? _timer;
+  List<File> imageFiles = [];
+  List<ui.Image> images = [];
+
+  @override
+  void initState() {
+    super.initState();
+    loadFrameFromAssets()
+        .then((_) => initializeImageFiles())
+        .then((_) => precacheImageFiles())
+        .then((_) => startAnimation())
+        .catchError((error) => debugPrint('FrameAnimationView error: $error'));
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _timer?.cancel();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    if (images.isEmpty || widget.width == null || widget.height == null) {
+      return Container();
+    }
+    return RawImage(
+      image: images[_currentFrame],
+      width: widget.width,
+      height: widget.height,
+    );
+  }
+
+  Future<void> loadFrameFromAssets() {
+    if (widget.framePath.isEmpty) {
+      throw Exception('framePath is null or empty');
+    }
+    if (widget.framePath.endsWith('.zip')) {
+      return unZipToCache();
+    } else {
+      return copyToCache();
+    }
+  }
+
+  Future<void> unZipToCache() async {
+    // Load the zip file from assets
+    final ByteData data = await rootBundle.load(widget.framePath);
+    final List<int> bytes = data.buffer.asUint8List();
+
+    // Decode the zip file
+    final Archive archive = ZipDecoder().decodeBytes(bytes);
+
+    // Create a cache directory
+    final Directory cacheDir = await createCacheDirectory();
+
+    // if the cache directory is not empty and frame images count is equal to the zip file's files count
+    if (cacheDir.listSync().isNotEmpty &&
+        cacheDir.listSync().length == archive.length) {
+      return;
+    }
+
+    // Extract the contents of the zip file
+    for (final ArchiveFile file in archive) {
+      final String filename = file.name;
+      final List<int> fileData = file.content as List<int>;
+
+      final File newFile = File('${cacheDir.path}/$filename');
+      if (newFile.existsSync()) {
+        newFile.deleteSync();
+      } else {
+        newFile.createSync(recursive: true);
+      }
+      newFile.writeAsBytesSync(fileData);
+    }
+  }
+
+  Future<Directory> createCacheDirectory() async {
+    // Get the temporary directory
+    final Directory tempDir = await getTemporaryDirectory();
+
+    // Create a new directory within the temporary directory
+    final Directory cacheDir =
+        Directory('${tempDir.path}/frame_anim_cache');
+
+    // Check if the directory exists, if not, create it
+    if (!await cacheDir.exists()) {
+      await cacheDir.create(recursive: true);
+    }
+
+    // create unique directory by framePath's md5
+    final Directory frameDir = Directory(
+        '${cacheDir.path}/${md5.convert(utf8.encode(widget.framePath)).toString()}');
+
+    // Check if the directory exists, if not, create it
+    if (!await frameDir.exists()) {
+      await frameDir.create(recursive: true);
+    }
+
+    return frameDir;
+  }
+
+  Future<void> copyToCache() async {
+    // Read AssetManifest.json file
+    final String manifestContent =
+        await rootBundle.loadString('AssetManifest.json');
+
+    // Parse JSON string into Map
+    final Map<String, dynamic> manifestMap = json.decode(manifestContent);
+
+    // Filter the Map to get file paths that start with folderPath
+    final List<String> filePaths = manifestMap.keys
+        .where((String key) => key.startsWith(widget.framePath))
+        .toList();
+
+    // Create a cache directory
+    final Directory cacheDir = await createCacheDirectory();
+
+    // if the cache directory is not empty and frame images count is equal to the zip file's files count
+    if (cacheDir.listSync().isNotEmpty &&
+        cacheDir.listSync().length == filePaths.length) {
+      return;
+    }
+
+    // Copy the files to the cache directory
+    for (final String path in filePaths) {
+      final ByteData data = await rootBundle.load(path);
+      final List<int> bytes = data.buffer.asUint8List();
+      final File newFile = File('${cacheDir.path}/${path.split('/').last}');
+      if (newFile.existsSync()) {
+        newFile.deleteSync();
+      } else {
+        newFile.createSync(recursive: true);
+      }
+      newFile.writeAsBytesSync(bytes);
+      imageFiles.add(newFile);
+    }
+  }
+
+  Future<void> startAnimation() async {
+    if (imageFiles.isEmpty) {
+      return;
+    }
+
+    _timer =
+        Timer.periodic(Duration(milliseconds: 1000 ~/ widget.frameRate), (_) {
+      setState(() {
+        int targetFrame = (_currentFrame + 1) % imageFiles.length;
+        _currentFrame =
+            targetFrame >= images.length ? images.length - 1 : targetFrame;
+      });
+    });
+  }
+
+  Future<void> initializeImageFiles() async {
+    final Directory cacheDir = await createCacheDirectory();
+
+    imageFiles = cacheDir
+        .listSync()
+        .map((file) => File(file.path))
+        .where((file) =>
+            file.path.endsWith('.png') ||
+            file.path.endsWith('.jpg') ||
+            file.path.endsWith('.jpeg') ||
+            file.path.endsWith('.webp'))
+        .toList();
+
+    if (imageFiles.isEmpty) {
+      throw Exception('frame images not found');
+    }
+  }
+
+  precacheImageFiles() {
+    Stream<File>.fromIterable(imageFiles).asyncMap((file) async {
+      final Uint8List bytes = await file.readAsBytes();
+      final ui.Codec codec = await ui.instantiateImageCodec(bytes);
+      final ui.FrameInfo frameInfo = await codec.getNextFrame();
+      return frameInfo.image;
+    }).listen((image) {
+      images.add(image);
+    });
+  }
+}

+ 1 - 1
pubspec.lock

@@ -18,7 +18,7 @@ packages:
     source: hosted
     version: "6.4.1"
   archive:
-    dependency: transitive
+    dependency: "direct main"
     description:
       name: archive
       sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d

+ 3 - 0
pubspec.yaml

@@ -60,6 +60,9 @@ dependencies:
   #lottie
   lottie: ^3.1.2
 
+  #解压
+  archive: ^3.6.1
+
 dev_dependencies:
   flutter_test:
     sdk: flutter