Browse Source

Merge branch 'v1.0.0' of git.atmob.com:Atmob-Flutter/ElectronicAssistant into v1.0.0

Destiny 1 year ago
parent
commit
52677b27fc
3 changed files with 260 additions and 5 deletions
  1. 252 0
      lib/widget/frame_animation_view.dart
  2. 5 5
      pubspec.lock
  3. 3 0
      pubspec.yaml

+ 252 - 0
lib/widget/frame_animation_view.dart

@@ -0,0 +1,252 @@
+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 FrameAnimationController? controller;
+
+  final double? speed;
+
+  final double? width;
+
+  final double? height;
+
+  const FrameAnimationView(
+      {super.key,
+      required this.framePath,
+      this.frameRate = 25,
+      this.controller,
+      this.speed,
+      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();
+    widget.controller?.bindState(this);
+    loadFrameFromAssets()
+        .then((_) => initializeImageFiles())
+        .then((_) => precacheImageFiles())
+        .then((_) => widget.controller == null || widget.controller!.autoPlay
+            ? startAnimation()
+            : null)
+        .catchError((error) => debugPrint('FrameAnimationView error: $error'));
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _timer?.cancel();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    if (images.isEmpty) {
+      return SizedBox(width: widget.width, height: widget.height);
+    }
+    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<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<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;
+  }
+
+  startAnimation() {
+    if (imageFiles.isEmpty) {
+      return;
+    }
+
+    if (_timer != null && _timer!.isActive) {
+      return;
+    }
+
+    double speed = widget.speed ?? 1.0;
+
+    _timer = Timer.periodic(
+        Duration(milliseconds: 1000 ~/ (widget.frameRate * speed)), (_) {
+      setState(() {
+        int targetFrame = (_currentFrame + 1) % imageFiles.length;
+        _currentFrame =
+            targetFrame >= images.length ? images.length - 1 : targetFrame;
+      });
+    });
+  }
+
+  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);
+    });
+  }
+}
+
+class FrameAnimationController {
+  final bool autoPlay;
+
+  FrameAnimationViewState? _state;
+
+  FrameAnimationController({this.autoPlay = true});
+
+  void play() {
+    _state?.startAnimation();
+  }
+
+  void stop() {
+    _state?._timer?.cancel();
+  }
+
+  bool get isPlaying => _state?._timer?.isActive ?? false;
+
+  void bindState(FrameAnimationViewState state) {
+    _state = state;
+  }
+}

+ 5 - 5
pubspec.lock

@@ -18,7 +18,7 @@ packages:
     source: hosted
     version: "6.4.1"
   archive:
-    dependency: transitive
+    dependency: "direct main"
     description:
       name: archive
       sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
@@ -213,10 +213,10 @@ packages:
     dependency: "direct main"
     description:
       name: dio
-      sha256: "0dfb6b6a1979dac1c1245e17cef824d7b452ea29bd33d3467269f9bef3715fb0"
+      sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260"
       url: "https://pub.dev"
     source: hosted
-    version: "5.6.0"
+    version: "5.7.0"
   dio_web_adapter:
     dependency: transitive
     description:
@@ -695,10 +695,10 @@ packages:
     dependency: "direct main"
     description:
       name: retrofit
-      sha256: "1fceca35cc68d5f14ff70ae830acbf157263b42e1cdfd5b38045ca517535b734"
+      sha256: "479cc534c2d83296dac6ae16933dd77e7a52bb54cf7e75e6eb83ee51928c8465"
       url: "https://pub.dev"
     source: hosted
-    version: "4.2.0"
+    version: "4.3.0"
   retrofit_generator:
     dependency: "direct dev"
     description:

+ 3 - 0
pubspec.yaml

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