import 'dart:async'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; class SpriteAnimationWidget extends StatefulWidget { final String assetPath; final double tileWidth; final double tileHeight; final int frameCount; final double scale; const SpriteAnimationWidget({ super.key, required this.assetPath, this.tileWidth = 100.0, this.tileHeight = 100.0, this.frameCount = 6, // Default guess, will adjust logic to use actual image width if possible this.scale = 1.0, }); @override State createState() => _SpriteAnimationWidgetState(); } class _SpriteAnimationWidgetState extends State with SingleTickerProviderStateMixin { ui.Image? _image; late AnimationController _controller; bool _isLoading = true; int _calculatedFrameCount = 6; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 600), // 100ms per frame approx ); _loadImage(); } Future _loadImage() async { try { final ByteData data = await rootBundle.load(widget.assetPath); final List bytes = data.buffer.asUint8List(); final Completer completer = Completer(); ui.decodeImageFromList(Uint8List.fromList(bytes), (ui.Image img) { completer.complete(img); }); final image = await completer.future; if (mounted) { setState(() { _image = image; _isLoading = false; // Use provided frameCount, but clamp to available frames in image int maxFrames = (image.width / widget.tileWidth).floor(); _calculatedFrameCount = widget.frameCount > maxFrames ? maxFrames : widget.frameCount; // Adjust duration based on frame count _controller.duration = Duration( milliseconds: _calculatedFrameCount * 100, ); _controller.repeat(); }); } } catch (e) { debugPrint('Failed to load sprite image: $e'); } } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { if (_isLoading || _image == null) { return SizedBox( width: widget.tileWidth * widget.scale, height: widget.tileHeight * widget.scale, child: const Center(child: CircularProgressIndicator()), ); } return AnimatedBuilder( animation: _controller, builder: (context, child) { return CustomPaint( size: Size( widget.tileWidth * widget.scale, widget.tileHeight * widget.scale, ), painter: SpriteSheetPainter( image: _image!, currentFrame: (_controller.value * _calculatedFrameCount).floor() % _calculatedFrameCount, tileWidth: widget.tileWidth, tileHeight: widget.tileHeight, scale: widget.scale, ), ); }, ); } } class SpriteSheetPainter extends CustomPainter { final ui.Image image; final int currentFrame; final double tileWidth; final double tileHeight; final double scale; SpriteSheetPainter({ required this.image, required this.currentFrame, required this.tileWidth, required this.tileHeight, required this.scale, }); @override void paint(Canvas canvas, Size size) { // Correct src rect calculation // Assuming horizontal strip for the animation row. // Ideally we would want to select which 'row' (Y) to animate, but for now assuming row 0. // If the image is a single row, srcY is 0. final double srcX = currentFrame * tileWidth; final double srcY = 0.0; // Default to first row final Rect src = Rect.fromLTWH(srcX, srcY, tileWidth, tileHeight); final Rect dst = Rect.fromLTWH(0, 0, tileWidth * scale, tileHeight * scale); canvas.drawImageRect( image, src, dst, Paint()..filterQuality = FilterQuality.none, ); } @override bool shouldRepaint(covariant SpriteSheetPainter oldDelegate) { return oldDelegate.currentFrame != currentFrame || oldDelegate.image != image; } }