| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252 |
- 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;
- }
- }
|