diff --git a/spine-flutter/example/lib/main.dart b/spine-flutter/example/lib/main.dart index 3c68e0ecf..5400c999f 100644 --- a/spine-flutter/example/lib/main.dart +++ b/spine-flutter/example/lib/main.dart @@ -50,17 +50,13 @@ class SimpleAnimation extends StatelessWidget { reportLeaks(); final controller = SpineWidgetController((controller) { // Set the walk animation on track 0, let it loop - controller.animationState?.setAnimationByName(0, "walk", true); - - print("Skeleton name: ${controller.skeletonData?.getName()}"); - print("Skeleton version: ${controller.skeletonData?.getVersion()}"); - print("Skeleton hash: ${controller.skeletonData?.getHash()}"); - print("Bones: ${controller.skeletonData!.getBones()}"); + // controller.animationState?.setAnimationByName(0, "walk", true); }); return Scaffold( appBar: AppBar(title: const Text('Spineboy')), - body: SpineWidget.asset("assets/spineboy-pro.skel", "assets/spineboy.atlas", controller), + // body: SpineWidget.asset("assets/skeleton.json", "assets/skeleton.atlas", controller, alignment: Alignment.center, fit: BoxFit.none), + body: SpineWidget.asset("assets/spineboy-pro.skel", "assets/spineboy.atlas", controller) // body: SpineWidget.file("/Users/badlogic/workspaces/spine-runtimes/examples/spineboy/export/spineboy-pro.skel", "/Users/badlogic/workspaces/spine-runtimes/examples/spineboy/export/spineboy.atlas", controller), // body: const SpineWidget.http("https://marioslab.io/dump/spineboy/spineboy-pro.json", "https://marioslab.io/dump/spineboy/spineboy.atlas"), ); @@ -84,7 +80,8 @@ class AnimationStateEvents extends StatelessWidget { controller.animationState?.setAnimationByName(0, "walk", true)?.setListener((type, trackEntry, event) { print("Walk animation event ${type}"); }); - controller.animationState?.addAnimationByName(0, "run", true, 2)?.setListener((type, trackEntry, event) { + controller.animationState?.addAnimationByName(0, "jump", false, 2); + controller.animationState?.addAnimationByName(0, "run", true, 0)?.setListener((type, trackEntry, event) { print("Run animation event ${type}"); }); controller.animationState?.setListener((type, trackEntry, event) { diff --git a/spine-flutter/lib/spine_widget.dart b/spine-flutter/lib/spine_widget.dart index f02e7a9dd..42c7b0643 100644 --- a/spine-flutter/lib/spine_widget.dart +++ b/spine-flutter/lib/spine_widget.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:io'; +import 'dart:math'; import 'package:flutter/rendering.dart' as rendering; import 'package:flutter/scheduler.dart'; @@ -47,24 +48,26 @@ class SpineWidget extends StatefulWidget { final SkeletonData? skeletonData; final Atlas? atlas; final SpineWidgetController controller; + final BoxFit? fit; + final Alignment? alignment; final AssetType _assetType; - const SpineWidget.asset(this.skeletonFile, this.atlasFile, this.controller, {super.key}) + const SpineWidget.asset(this.skeletonFile, this.atlasFile, this.controller, {this.fit, this.alignment, super.key}) : _assetType = AssetType.Asset, atlas = null, skeletonData = null; - const SpineWidget.file(this.skeletonFile, this.atlasFile, this.controller, {super.key}) + const SpineWidget.file(this.skeletonFile, this.atlasFile, this.controller, {this.fit, this.alignment, super.key}) : _assetType = AssetType.File, atlas = null, skeletonData = null; - const SpineWidget.http(this.skeletonFile, this.atlasFile, this.controller, {super.key}) + const SpineWidget.http(this.skeletonFile, this.atlasFile, this.controller, {this.fit, this.alignment, super.key}) : _assetType = AssetType.Http, atlas = null, skeletonData = null; - const SpineWidget.raw(this.skeletonData, this.atlas, this.controller, {super.key}) + const SpineWidget.raw(this.skeletonData, this.atlas, this.controller, {this.fit, this.alignment, super.key}) : _assetType = AssetType.Raw, atlasFile = null, skeletonFile = null; @@ -88,7 +91,7 @@ class _SpineWidgetState extends State { void loadRaw(SkeletonData skeletonData, Atlas atlas) { skeletonDrawable = SkeletonDrawable(atlas, skeletonData, false); - skeletonDrawable?.update(0.016); + skeletonDrawable?.update(0); } void loadFromAsset(String skeletonFile, String atlasFile, AssetType assetType) async { @@ -114,6 +117,8 @@ class _SpineWidgetState extends State { ? SkeletonData.fromJson(atlas, utf8.decode((await http.get(Uri.parse(skeletonFile))).bodyBytes)) : SkeletonData.fromBinary(atlas, (await http.get(Uri.parse(skeletonFile))).bodyBytes); break; + case AssetType.Raw: + throw Exception("Raw assets can not be loaded via loadFromAsset()."); } skeletonDrawable = SkeletonDrawable(atlas, skeletonData, true); @@ -126,10 +131,10 @@ class _SpineWidgetState extends State { Widget build(BuildContext context) { if (skeletonDrawable != null) { print("Skeleton loaded, rebuilding painter"); - return _SpineRenderObjectWidget(skeletonDrawable!, widget.controller); + return _SpineRenderObjectWidget(skeletonDrawable!, widget.controller, widget.fit, widget.alignment); } else { print("Skeleton not loaded yet"); - return SizedBox(); + return const SizedBox(); } } @@ -143,12 +148,16 @@ class _SpineWidgetState extends State { class _SpineRenderObjectWidget extends LeafRenderObjectWidget { final SkeletonDrawable _skeletonDrawable; final SpineWidgetController _controller; + final BoxFit _fit; + final Alignment _alignment; - _SpineRenderObjectWidget(this._skeletonDrawable, this._controller); + _SpineRenderObjectWidget(this._skeletonDrawable, this._controller, BoxFit? fit, Alignment? alignment) : + _fit = fit ?? BoxFit.contain, + _alignment = alignment ?? Alignment.center; @override RenderObject createRenderObject(BuildContext context) { - return _SpineRenderObject(_skeletonDrawable, _controller); + return _SpineRenderObject(_skeletonDrawable, _controller, _fit, _alignment); } @override @@ -162,13 +171,39 @@ class _SpineRenderObject extends RenderBox { SpineWidgetController _controller; double _deltaTime = 0; final Stopwatch _stopwatch = Stopwatch(); + BoxFit _fit; + Alignment _alignment; + Bounds _bounds; - _SpineRenderObject(this._skeletonDrawable, this._controller); + _SpineRenderObject(this._skeletonDrawable, this._controller, this._fit, this._alignment): _bounds = _computeBounds(_skeletonDrawable); + + static Bounds _computeBounds(SkeletonDrawable drawable) { + return drawable.skeleton.getBounds(); + } + + BoxFit get fit => _fit; + + set fit(BoxFit fit) { + if (fit != _fit) { + _fit = fit; + markNeedsPaint(); + } + } + + Alignment get alignment => _alignment; + + set alignment(Alignment alignment) { + if (alignment != _alignment) { + _alignment = alignment; + markNeedsPaint(); + } + } set skeletonDrawable(SkeletonDrawable skeletonDrawable) { if (_skeletonDrawable == skeletonDrawable) return; _skeletonDrawable = skeletonDrawable; + _bounds = _computeBounds(_skeletonDrawable); markNeedsPaint(); } @@ -181,11 +216,42 @@ class _SpineRenderObject extends RenderBox { @override bool hitTestSelf(Offset position) => true; + @override + double computeMinIntrinsicWidth(double height) { + return _computeConstrainedSize(BoxConstraints.tightForFinite(height: height)).width; + } + + @override + double computeMaxIntrinsicWidth(double height) { + return _computeConstrainedSize(BoxConstraints.tightForFinite(height: height)).width; + } + + @override + double computeMinIntrinsicHeight(double width) { + return _computeConstrainedSize(BoxConstraints.tightForFinite(width: width)).height; + } + + @override + double computeMaxIntrinsicHeight(double width) { + return _computeConstrainedSize(BoxConstraints.tightForFinite(width: width)).height; + } + + // Called when not sizedByParent, uses the intrinsic width/height for sizing, while trying to retain aspect ratio. + @override + void performLayout() { + if (!sizedByParent) size = _computeConstrainedSize(constraints); + } + + // Called when sizedByParent, we want to go as big as possible. @override void performResize() { size = constraints.biggest; } + Size _computeConstrainedSize(BoxConstraints constraints) { + return sizedByParent ? constraints.smallest : constraints.constrainSizeAndAttemptToPreserveAspectRatio(Size(_bounds.width, _bounds.height)); + } + @override void attach(rendering.PipelineOwner owner) { super.attach(owner); @@ -206,6 +272,45 @@ class _SpineRenderObject extends RenderBox { markNeedsPaint(); } + void _setCanvasTransform(Canvas canvas, Offset offset) { + final double x = -_bounds.x - _bounds.width / 2.0 - (_alignment.x * _bounds.width / 2.0); + final double y = -_bounds.y - _bounds.height / 2.0 + (_alignment.y * _bounds.height / 2.0); + double scaleX = 1.0, scaleY = 1.0; + + switch (_fit) { + case BoxFit.fill: + scaleX = size.width / _bounds.width; + scaleY = size.height / _bounds.height; + break; + case BoxFit.contain: + scaleX = scaleY = min(size.width / _bounds.width, size.height / _bounds.height); + break; + case BoxFit.cover: + scaleX = scaleY = max(size.width / _bounds.width, size.height / _bounds.height); + break; + case BoxFit.fitHeight: + scaleX = scaleY = size.height / _bounds.height; + break; + case BoxFit.fitWidth: + scaleX = scaleY = size.width / _bounds.width; + break; + case BoxFit.none: + scaleX = scaleY = 1.0; + break; + case BoxFit.scaleDown: + final double scale = min(size.width / _bounds.width, size.height / _bounds.height); + scaleX = scaleY = scale < 1.0 ? scale : 1.0; + break; + } + + canvas + ..translate( + offset.dx + size.width / 2.0 + (_alignment.x * size.width / 2.0), + offset.dy + size.height / 2.0 + (_alignment.y * size.height / 2.0)) + ..scale(scaleX, scaleY) + ..translate(x, y); + } + @override void paint(PaintingContext context, Offset offset) { final Canvas canvas = context.canvas @@ -213,7 +318,7 @@ class _SpineRenderObject extends RenderBox { ..clipRect(offset & size); canvas.save(); - canvas.translate(offset.dx + size.width / 2, offset.dy + size.height); + _setCanvasTransform(canvas, offset); final commands = _skeletonDrawable.render(); for (final cmd in commands) {