diff --git a/spine-flutter/example/lib/pause_play_animation.dart b/spine-flutter/example/lib/pause_play_animation.dart index 8eca6633a..02ccf01ca 100644 --- a/spine-flutter/example/lib/pause_play_animation.dart +++ b/spine-flutter/example/lib/pause_play_animation.dart @@ -10,7 +10,6 @@ class PlayPauseAnimation extends StatefulWidget { class PlayPauseAnimationState extends State { late SpineWidgetController controller; - late bool isPlaying; @override void initState() { @@ -18,12 +17,14 @@ class PlayPauseAnimationState extends State { controller = SpineWidgetController(onInitialized: (controller) { controller.animationState.setAnimationByName(0, "flying", true); }); - isPlaying = true; } void _togglePlay() { - isPlaying = !isPlaying; - controller.animationState.setTimeScale(isPlaying ? 1 : 0); + if (controller.isPlaying) { + controller.pause(); + } else { + controller.resume(); + } setState(() {}); } @@ -41,7 +42,7 @@ class PlayPauseAnimationState extends State { ), floatingActionButton: FloatingActionButton( onPressed: _togglePlay, - child: Icon(isPlaying ? Icons.pause : Icons.play_arrow), + child: Icon(controller.isPlaying ? Icons.pause : Icons.play_arrow), ), ); } diff --git a/spine-flutter/example/lib/simple_animation.dart b/spine-flutter/example/lib/simple_animation.dart index d113f90eb..07c9cdb6f 100644 --- a/spine-flutter/example/lib/simple_animation.dart +++ b/spine-flutter/example/lib/simple_animation.dart @@ -14,9 +14,7 @@ class SimpleAnimation extends StatelessWidget { return Scaffold( appBar: AppBar(title: const Text('Simple Animation')), - 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"), + body: SpineWidget.fromAsset("assets/spineboy.atlas", "assets/spineboy-pro.skel", controller) ); } } diff --git a/spine-flutter/lib/spine_widget.dart b/spine-flutter/lib/spine_widget.dart index 5248bb88c..46e827b67 100644 --- a/spine-flutter/lib/spine_widget.dart +++ b/spine-flutter/lib/spine_widget.dart @@ -28,9 +28,15 @@ import 'spine_flutter.dart'; /// 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. +/// +/// By default, the widget updates and renders the skeleton every frame. The [pause] method can be used to pause updating +/// and rendering the skeleton. The [resume] method resumes updating and rendering the skeleton. The [isPlaying] getter +/// reports the current state. class SpineWidgetController { SkeletonDrawable? _drawable; double _offsetX = 0, _offsetY = 0, _scaleX = 1, _scaleY = 1; + bool _isPlaying = true; + _SpineRenderObject? _renderObject = null; final void Function(SpineWidgetController controller)? onInitialized; final void Function(SpineWidgetController controller)? onBeforeUpdateWorldTransforms; final void Function(SpineWidgetController controller)? onAfterUpdateWorldTransforms; @@ -92,6 +98,10 @@ class SpineWidgetController { _scaleY = scaleY; } + void _setRenderObject(_SpineRenderObject? renderObject) { + _renderObject = renderObject; + } + /// 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. @@ -100,6 +110,23 @@ class SpineWidgetController { var y = position.dy; return Offset(x / _scaleX - _offsetX, y / _scaleY - _offsetY); } + + /// Pauses updating and rendering the skeleton. + void pause() { + _isPlaying = false; + } + + /// Resumes updating and rendering the skeleton. + void resume() { + _isPlaying = true; + _renderObject?._stopwatch.reset(); + _renderObject?._stopwatch.start(); + _renderObject?._scheduleFrame(); + } + + bool get isPlaying { + return _isPlaying; + } } enum _AssetType { asset, file, http, drawable } @@ -391,6 +418,7 @@ class _SpineRenderObject extends RenderBox { Alignment _alignment; Bounds _bounds; bool _sizedByBounds; + bool _disposed = false; _SpineRenderObject(this._skeletonDrawable, this._controller, this._fit, this._alignment, this._bounds, this._sizedByBounds); @@ -493,22 +521,39 @@ class _SpineRenderObject extends RenderBox { void attach(rendering.PipelineOwner owner) { super.attach(owner); _stopwatch.start(); + SchedulerBinding.instance.scheduleFrameCallback(_beginFrame); + _controller._setRenderObject(this); } @override void detach() { _stopwatch.stop(); super.detach(); + _controller._setRenderObject(null); + } + + @override + void dispose() { + super.dispose(); + _disposed = true; + } + + void _scheduleFrame() { + SchedulerBinding.instance.scheduleFrameCallback(_beginFrame); } void _beginFrame(Duration duration) { + if (_disposed) return; _deltaTime = _stopwatch.elapsedTicks / _stopwatch.frequency; _stopwatch.reset(); _stopwatch.start(); - _controller.onBeforeUpdateWorldTransforms?.call(_controller); - _skeletonDrawable.update(_deltaTime); - _controller.onAfterUpdateWorldTransforms?.call(_controller); - markNeedsPaint(); + if (_controller.isPlaying) { + _controller.onBeforeUpdateWorldTransforms?.call(_controller); + _skeletonDrawable.update(_deltaTime); + _controller.onAfterUpdateWorldTransforms?.call(_controller); + markNeedsPaint(); + _scheduleFrame(); + } } void _setCanvasTransform(Canvas canvas, Offset offset) { @@ -568,6 +613,5 @@ class _SpineRenderObject extends RenderBox { _controller.onAfterPaint?.call(_controller, canvas, commands); canvas.restore(); - SchedulerBinding.instance.scheduleFrameCallback(_beginFrame); } }