[flutter] Rename SpineWidget factory methods, complete doc comments.

This commit is contained in:
Mario Zechner 2023-02-09 12:48:32 +01:00
parent b424cb811d
commit 147439fe95
8 changed files with 121 additions and 32 deletions

View File

@ -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))
]));
}
}

View File

@ -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),
);
}
}

View File

@ -29,7 +29,7 @@ class DressUpState extends State<DressUp> {
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<DressUp> {
}).toList()),
),
Expanded(
child: SpineWidget.drawable(
child: SpineWidget.fromDrawable(
_drawable,
controller,
boundsProvider: SkinAndAnimationBounds(skins: ["full-skins/girl"]),

View File

@ -25,7 +25,9 @@ class IkFollowingState extends State<IkFollowing> {
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<IkFollowing> {
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),
));
}
}

View File

@ -33,7 +33,7 @@ class PlayPauseAnimationState extends State<PlayPauseAnimation> {
return Scaffold(
appBar: AppBar(title: const Text('Play/Pause')),
body: SpineWidget.asset(
body: SpineWidget.fromAsset(
"assets/dragon.atlas",
"assets/dragon-ess.skel",
controller,

View File

@ -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"),
);

View File

@ -1,6 +1,5 @@
import 'dart:convert' as convert;
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'dart:ui';

View File

@ -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<RenderCommand> 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<String> 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<String>? 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<SpineWidget> {
@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<SpineWidget> {
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().");
}
}