From 147439fe95174f4d619f29219337e03905c210f0 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 9 Feb 2023 12:48:32 +0100 Subject: [PATCH] [flutter] Rename SpineWidget factory methods, complete doc comments. --- .../example/lib/animation_state_events.dart | 2 +- .../example/lib/debug_rendering.dart | 2 +- spine-flutter/example/lib/dress_up.dart | 4 +- spine-flutter/example/lib/ik_following.dart | 6 +- .../example/lib/pause_play_animation.dart | 2 +- .../example/lib/simple_animation.dart | 2 +- spine-flutter/lib/spine_flutter.dart | 1 - spine-flutter/lib/spine_widget.dart | 134 +++++++++++++++--- 8 files changed, 121 insertions(+), 32 deletions(-) diff --git a/spine-flutter/example/lib/animation_state_events.dart b/spine-flutter/example/lib/animation_state_events.dart index 70bb4231f..51b1a885a 100644 --- a/spine-flutter/example/lib/animation_state_events.dart +++ b/spine-flutter/example/lib/animation_state_events.dart @@ -33,7 +33,7 @@ class AnimationStateEvents extends StatelessWidget { appBar: AppBar(title: const Text('Spineboy')), body: Column(children: [ const Text("See output in console!"), - Expanded(child: SpineWidget.asset("assets/spineboy.atlas", "assets/spineboy-pro.skel", controller)) + Expanded(child: SpineWidget.fromAsset("assets/spineboy.atlas", "assets/spineboy-pro.skel", controller)) ])); } } diff --git a/spine-flutter/example/lib/debug_rendering.dart b/spine-flutter/example/lib/debug_rendering.dart index 7d20b4650..3216140d1 100644 --- a/spine-flutter/example/lib/debug_rendering.dart +++ b/spine-flutter/example/lib/debug_rendering.dart @@ -17,7 +17,7 @@ class DebugRendering extends StatelessWidget { return Scaffold( appBar: AppBar(title: const Text('Debug Renderer')), - body: SpineWidget.asset("assets/spineboy.atlas", "assets/spineboy-pro.skel", controller), + body: SpineWidget.fromAsset("assets/spineboy.atlas", "assets/spineboy-pro.skel", controller), ); } } diff --git a/spine-flutter/example/lib/dress_up.dart b/spine-flutter/example/lib/dress_up.dart index a62cbaeaf..9ac9d5ed0 100644 --- a/spine-flutter/example/lib/dress_up.dart +++ b/spine-flutter/example/lib/dress_up.dart @@ -29,7 +29,7 @@ class DressUpState extends State { skeleton.setSkin(skin); skeleton.setToSetupPose(); skeleton.updateWorldTransform(); - _skinImages[skin.getName()] = await drawable.renderToRawImageData(thumbnailSize, thumbnailSize); + _skinImages[skin.getName()] = await drawable.renderToRawImageData(thumbnailSize, thumbnailSize, 0xffffffff); _selectedSkins[skin.getName()] = false; } _toggleSkin("full-skins/girl"); @@ -87,7 +87,7 @@ class DressUpState extends State { }).toList()), ), Expanded( - child: SpineWidget.drawable( + child: SpineWidget.fromDrawable( _drawable, controller, boundsProvider: SkinAndAnimationBounds(skins: ["full-skins/girl"]), diff --git a/spine-flutter/example/lib/ik_following.dart b/spine-flutter/example/lib/ik_following.dart index b05875090..40277e8bd 100644 --- a/spine-flutter/example/lib/ik_following.dart +++ b/spine-flutter/example/lib/ik_following.dart @@ -25,7 +25,9 @@ class IkFollowingState extends State { if (worldPosition == null) return; var bone = controller.skeleton.findBone("crosshair"); if (bone == null) return; - var position = bone.getParent()?.worldToLocal(worldPosition.dx, worldPosition.dy) ?? Vec2(0, 0); + var parent = bone.getParent(); + if (parent == null) return; + var position = parent.worldToLocal(worldPosition.dx, worldPosition.dy); bone.setX(position.x); bone.setY(position.y); }); @@ -44,7 +46,7 @@ class IkFollowingState extends State { body: GestureDetector( onPanDown: (drag) => _updateBonePosition(drag.localPosition), onPanUpdate: (drag) => _updateBonePosition(drag.localPosition), - child: SpineWidget.asset("assets/spineboy.atlas", "assets/spineboy-pro.skel", controller), + child: SpineWidget.fromAsset("assets/spineboy.atlas", "assets/spineboy-pro.skel", controller), )); } } diff --git a/spine-flutter/example/lib/pause_play_animation.dart b/spine-flutter/example/lib/pause_play_animation.dart index beb69b323..8eca6633a 100644 --- a/spine-flutter/example/lib/pause_play_animation.dart +++ b/spine-flutter/example/lib/pause_play_animation.dart @@ -33,7 +33,7 @@ class PlayPauseAnimationState extends State { return Scaffold( appBar: AppBar(title: const Text('Play/Pause')), - body: SpineWidget.asset( + body: SpineWidget.fromAsset( "assets/dragon.atlas", "assets/dragon-ess.skel", controller, diff --git a/spine-flutter/example/lib/simple_animation.dart b/spine-flutter/example/lib/simple_animation.dart index 666b8ba84..d113f90eb 100644 --- a/spine-flutter/example/lib/simple_animation.dart +++ b/spine-flutter/example/lib/simple_animation.dart @@ -14,7 +14,7 @@ class SimpleAnimation extends StatelessWidget { return Scaffold( appBar: AppBar(title: const Text('Simple Animation')), - body: SpineWidget.asset("assets/spineboy.atlas", "assets/spineboy-pro.skel", controller), + body: SpineWidget.fromAsset("assets/spineboy.atlas", "assets/spineboy-pro.skel", controller), // body: SpineWidget.file( "/Users/badlogic/workspaces/spine-runtimes/examples/spineboy/export/spineboy.atlas", "/Users/badlogic/workspaces/spine-runtimes/examples/spineboy/export/spineboy-pro.skel", controller), // body: const SpineWidget.http("https://marioslab.io/dump/spineboy/spineboy.atlas", "https://marioslab.io/dump/spineboy/spineboy-pro.json"), ); diff --git a/spine-flutter/lib/spine_flutter.dart b/spine-flutter/lib/spine_flutter.dart index 161e11983..0aaa4ee13 100644 --- a/spine-flutter/lib/spine_flutter.dart +++ b/spine-flutter/lib/spine_flutter.dart @@ -1,6 +1,5 @@ import 'dart:convert' as convert; import 'dart:io'; -import 'dart:math'; import 'dart:typed_data'; import 'dart:ui'; diff --git a/spine-flutter/lib/spine_widget.dart b/spine-flutter/lib/spine_widget.dart index 943eb0a82..5248bb88c 100644 --- a/spine-flutter/lib/spine_widget.dart +++ b/spine-flutter/lib/spine_widget.dart @@ -7,6 +7,27 @@ import 'package:flutter/widgets.dart'; import 'spine_flutter.dart'; +/// Controls how the skeleton of a [SpineWidget] is animated and rendered. +/// +/// Upon initialization of a [SpineWidget] the provided [onInitialized] callback method is called once. This method can be used +/// to setup the initial animation(s) of the skeleton, among other things. +/// +/// After initialization is complete, the [SpineWidget] is rendered at the screen refresh rate. In each frame, +/// the [AnimationState] is updated and applied to the [Skeleton]. +/// +/// Next the optionally provided method [onBeforeUpdateWorldTransforms] is called, which can modify the +/// skeleton before its current pose is calculated using [Skeleton.updateWorldTransforms]. After +/// [Skeleton.updateWorldTransforms] has completed, the optional [onAfterUpdateWorldTransforms] method is +/// called, which can modify the current pose before rendering the skeleton. +/// +/// Before the skeleton's current pose is rendered by the [SpineWidget] the optional [onBeforePaint] is called, +/// which allows rendering backgrounds or other objects that should go behind the skeleton on the [Canvas]. The +/// [SpineWidget] then renderes the skeleton's current pose, and finally calls the optional [onAfterPaint], which +/// can render additional objects on top of the skeleton. +/// +/// The underlying [Atlas], [SkeletonData], [Skeleton], [AnimationStateData], [AnimationState], and [SkeletonDrawable] +/// can be accessed through their respective getters to inspect and/or modify the skeleton and its associated data. Accessing +/// this data is only allowed if the [SpineWidget] and its data have been initialized and have not been disposed yet. class SpineWidgetController { SkeletonDrawable? _drawable; double _offsetX = 0, _offsetY = 0, _scaleX = 1, _scaleY = 1; @@ -16,6 +37,8 @@ class SpineWidgetController { final void Function(SpineWidgetController controller, Canvas canvas)? onBeforePaint; final void Function(SpineWidgetController controller, Canvas canvas, List commands)? onAfterPaint; + /// Constructs a new [SpineWidget] controller. See the class documentation of [SpineWidgetController] for information on + /// the optional arguments. SpineWidgetController( {this.onInitialized, this.onBeforeUpdateWorldTransforms, this.onAfterUpdateWorldTransforms, this.onBeforePaint, this.onAfterPaint}); @@ -25,31 +48,38 @@ class SpineWidgetController { onInitialized?.call(this); } + /// The [Atlas] from which images to render the skeleton are sourced. Atlas get atlas { if (_drawable == null) throw Exception("Controller is not initialized yet."); return _drawable!.atlas; } + /// The setup-pose data used by the skeleton. SkeletonData get skeletonData { if (_drawable == null) throw Exception("Controller is not initialized yet."); return _drawable!.skeletonData; } + /// The mixing information used by the [AnimationState] AnimationStateData get animationStateData { if (_drawable == null) throw Exception("Controller is not initialized yet."); return _drawable!.animationStateData; } + /// The [AnimationState] used to manage animations that are being applied to the + /// skeleton. AnimationState get animationState { if (_drawable == null) throw Exception("Controller is not initialized yet."); return _drawable!.animationState; } + /// The [Skeleton] Skeleton get skeleton { if (_drawable == null) throw Exception("Controller is not initialized yet."); return _drawable!.skeleton; } + /// The [SkeletonDrawable] SkeletonDrawable get drawable { if (_drawable == null) throw Exception("Controller is not initialized yet."); return _drawable!; @@ -62,6 +92,9 @@ class SpineWidgetController { _scaleY = scaleY; } + /// Transforms the coordinates given in the [SpineWidget] coordinate system in [position] to + /// the skeleton coordinate system. See the `ik_following.dart` example how to use this + /// to move a bone based on user touch input. Offset toSkeletonCoordinates(Offset position) { var x = position.dx; var y = position.dy; @@ -69,14 +102,18 @@ class SpineWidgetController { } } -enum AssetType { asset, file, http, drawable } +enum _AssetType { asset, file, http, drawable } +/// Base class for bounds providers. A bounds provider calculates the axis aligned bounding box +/// used to scale and fit a skeleton inside the bounds of a [SpineWidget]. abstract class BoundsProvider { const BoundsProvider(); Bounds computeBounds(SkeletonDrawable drawable); } +/// A [BoundsProvider] that calculates the bounding box of the skeleton based on the visible +/// attachments in the setup pose. class SetupPoseBounds extends BoundsProvider { const SetupPoseBounds(); @@ -86,6 +123,7 @@ class SetupPoseBounds extends BoundsProvider { } } +/// A [BoundsProvider] that returns fixed bounds. class RawBounds extends BoundsProvider { final double x, y, width, height; @@ -97,11 +135,17 @@ class RawBounds extends BoundsProvider { } } +/// A [BoundsProvider] that calculates the bounding box needed for a combination of skins +/// and an animation. class SkinAndAnimationBounds extends BoundsProvider { final List skins; final String? animation; final double stepTime; + /// Constructs a new provider that will use the given [skins] and [animation] to calculate + /// the bounding box of the skeleton. If no skins are given, the default skin is used. + /// The [stepTime], given in seconds, defines at what interval the bounds should be sampled + /// across the entire animation. SkinAndAnimationBounds({List? skins, this.animation, this.stepTime = 0.1}) : skins = skins == null || skins.isEmpty ? ["default"] : skins; @@ -151,15 +195,15 @@ class SkinAndAnimationBounds extends BoundsProvider { } } -class ComputedBounds extends BoundsProvider { - @override - Bounds computeBounds(SkeletonDrawable drawable) { - return Bounds(0, 0, 0, 0); - } -} - +/// A [StatefulWidget] to display a Spine skeleton. The skeleton can be loaded from an asset bundle ([SpineWidget.fromAsset], +/// local files [SpineWidget.fromFile], URLs [SpineWidget.fromHttp], or a pre-loaded [SkeletonDrawable] ([SpineWidget.fromDrawable]). +/// +/// The skeleton displayed by a `SpineWidget` can be controlled via a [SpineWidgetController]. +/// +/// The size of the widget can be derived from the bounds provided by a [BoundsProvider]. If the widget is not sized by the bounds +/// computed by the [BoundsProvider], the widget will use the computed bounds to fit the skeleton inside the widget's dimensions. class SpineWidget extends StatefulWidget { - final AssetType _assetType; + final _AssetType _assetType; final AssetBundle? _bundle; final String? _skeletonFile; final String? _atlasFile; @@ -170,9 +214,21 @@ class SpineWidget extends StatefulWidget { final BoundsProvider _boundsProvider; final bool _sizedByBounds; - SpineWidget.asset(this._atlasFile, this._skeletonFile, this._controller, + /// Constructs a new [SpineWidget] from files in the root bundle or the optionally specified [bundle]. The [_atlasFile] specifies the + /// `.atlas` file to be loaded for the images used to render the skeleton. The [_skeletonFile] specifies either a Skeleton `.json` or + /// `.skel` file containing the skeleton data. + /// + /// After initialization is complete, the provided [_controller] is invoked as per the [SpineWidgetController] semantics, to allow + /// modifying how the skeleton inside the widget is animated and rendered. + /// + /// The skeleton is fitted and aligned inside the widget as per the [fit] and [alignment] arguments. For this purpose, the skeleton + /// bounds must be computed via a [BoundsProvider]. By default, [BoxFit.contain], [Alignment.center], and a [SetupPoseBounds] provider + /// are used. + /// + /// The widget can optionally by sized by the bounds provided by the [BoundsProvider] by passing `true` for [sizedByBounds]. + SpineWidget.fromAsset(this._atlasFile, this._skeletonFile, this._controller, {AssetBundle? bundle, BoxFit? fit, Alignment? alignment, BoundsProvider? boundsProvider, bool? sizedByBounds, super.key}) - : _assetType = AssetType.asset, + : _assetType = _AssetType.asset, _fit = fit ?? BoxFit.contain, _alignment = alignment ?? Alignment.center, _boundsProvider = boundsProvider ?? const SetupPoseBounds(), @@ -180,9 +236,20 @@ class SpineWidget extends StatefulWidget { _drawable = null, _bundle = bundle ?? rootBundle; - const SpineWidget.file(this._atlasFile, this._skeletonFile, this._controller, + /// Constructs a new [SpineWidget] from files. The [_atlasFile] specifies the `.atlas` file to be loaded for the images used to render + /// the skeleton. The [_skeletonFile] specifies either a Skeleton `.json` or `.skel` file containing the skeleton data. + /// + /// After initialization is complete, the provided [_controller] is invoked as per the [SpineWidgetController] semantics, to allow + /// modifying how the skeleton inside the widget is animated and rendered. + /// + /// The skeleton is fitted and aligned inside the widget as per the [fit] and [alignment] arguments. For this purpose, the skeleton + /// bounds must be computed via a [BoundsProvider]. By default, [BoxFit.contain], [Alignment.center], and a [SetupPoseBounds] provider + /// are used. + /// + /// The widget can optionally by sized by the bounds provided by the [BoundsProvider] by passing `true` for [sizedByBounds]. + const SpineWidget.fromFile(this._atlasFile, this._skeletonFile, this._controller, {BoxFit? fit, Alignment? alignment, BoundsProvider? boundsProvider, bool? sizedByBounds, super.key}) - : _assetType = AssetType.file, + : _assetType = _AssetType.file, _bundle = null, _fit = fit ?? BoxFit.contain, _alignment = alignment ?? Alignment.center, @@ -190,9 +257,20 @@ class SpineWidget extends StatefulWidget { _sizedByBounds = sizedByBounds ?? false, _drawable = null; - const SpineWidget.http(this._atlasFile, this._skeletonFile, this._controller, + /// Constructs a new [SpineWidget] from HTTP URLs. The [_atlasFile] specifies the `.atlas` file to be loaded for the images used to render + /// the skeleton. The [_skeletonFile] specifies either a Skeleton `.json` or `.skel` file containing the skeleton data. + /// + /// After initialization is complete, the provided [_controller] is invoked as per the [SpineWidgetController] semantics, to allow + /// modifying how the skeleton inside the widget is animated and rendered. + /// + /// The skeleton is fitted and aligned inside the widget as per the [fit] and [alignment] arguments. For this purpose, the skeleton + /// bounds must be computed via a [BoundsProvider]. By default, [BoxFit.contain], [Alignment.center], and a [SetupPoseBounds] provider + /// are used. + /// + /// The widget can optionally by sized by the bounds provided by the [BoundsProvider] by passing `true` for [sizedByBounds]. + const SpineWidget.fromHttp(this._atlasFile, this._skeletonFile, this._controller, {BoxFit? fit, Alignment? alignment, BoundsProvider? boundsProvider, bool? sizedByBounds, super.key}) - : _assetType = AssetType.http, + : _assetType = _AssetType.http, _bundle = null, _fit = fit ?? BoxFit.contain, _alignment = alignment ?? Alignment.center, @@ -200,9 +278,19 @@ class SpineWidget extends StatefulWidget { _sizedByBounds = sizedByBounds ?? false, _drawable = null; - const SpineWidget.drawable(this._drawable, this._controller, + /// Constructs a new [SpineWidget] from a [SkeletonDrawable]. + /// + /// After initialization is complete, the provided [_controller] is invoked as per the [SpineWidgetController] semantics, to allow + /// modifying how the skeleton inside the widget is animated and rendered. + /// + /// The skeleton is fitted and aligned inside the widget as per the [fit] and [alignment] arguments. For this purpose, the skeleton + /// bounds must be computed via a [BoundsProvider]. By default, [BoxFit.contain], [Alignment.center], and a [SetupPoseBounds] provider + /// are used. + /// + /// The widget can optionally by sized by the bounds provided by the [BoundsProvider] by passing `true` for [sizedByBounds]. + const SpineWidget.fromDrawable(this._drawable, this._controller, {BoxFit? fit, Alignment? alignment, BoundsProvider? boundsProvider, bool? sizedByBounds, super.key}) - : _assetType = AssetType.drawable, + : _assetType = _AssetType.drawable, _bundle = null, _fit = fit ?? BoxFit.contain, _alignment = alignment ?? Alignment.center, @@ -222,7 +310,7 @@ class _SpineWidgetState extends State { @override void initState() { super.initState(); - if (widget._assetType == AssetType.drawable) { + if (widget._assetType == _AssetType.drawable) { loadDrawable(widget._drawable!); } else { loadFromAsset(widget._bundle, widget._atlasFile!, widget._skeletonFile!, widget._assetType); @@ -236,18 +324,18 @@ class _SpineWidgetState extends State { setState(() {}); } - void loadFromAsset(AssetBundle? bundle, String atlasFile, String skeletonFile, AssetType assetType) async { + void loadFromAsset(AssetBundle? bundle, String atlasFile, String skeletonFile, _AssetType assetType) async { switch (assetType) { - case AssetType.asset: + case _AssetType.asset: loadDrawable(await SkeletonDrawable.fromAsset(atlasFile, skeletonFile, bundle: bundle)); break; - case AssetType.file: + case _AssetType.file: loadDrawable(await SkeletonDrawable.fromFile(atlasFile, skeletonFile)); break; - case AssetType.http: + case _AssetType.http: loadDrawable(await SkeletonDrawable.fromHttp(atlasFile, skeletonFile)); break; - case AssetType.drawable: + case _AssetType.drawable: throw Exception("Drawable can not be loaded via loadFromAsset()."); } }