spine_widget.dart 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678
  1. ///
  2. /// Spine Runtimes License Agreement
  3. /// Last updated April 5, 2025. Replaces all prior versions.
  4. ///
  5. /// Copyright (c) 2013-2025, Esoteric Software LLC
  6. ///
  7. /// Integration of the Spine Runtimes into software or otherwise creating
  8. /// derivative works of the Spine Runtimes is permitted under the terms and
  9. /// conditions of Section 2 of the Spine Editor License Agreement:
  10. /// http://esotericsoftware.com/spine-editor-license
  11. ///
  12. /// Otherwise, it is permitted to integrate the Spine Runtimes into software
  13. /// or otherwise create derivative works of the Spine Runtimes (collectively,
  14. /// "Products"), provided that each user of the Products must obtain their own
  15. /// Spine Editor license and redistribution of the Products in any form must
  16. /// include this license and copyright notice.
  17. ///
  18. /// THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
  19. /// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  20. /// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  21. /// DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
  22. /// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  23. /// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
  24. /// BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
  25. /// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  26. /// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
  27. /// THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  28. ///
  29. import 'dart:math';
  30. import 'package:flutter/rendering.dart' as rendering;
  31. import 'package:flutter/scheduler.dart';
  32. import 'package:flutter/services.dart';
  33. import 'package:flutter/widgets.dart';
  34. import 'spine_flutter.dart';
  35. /// Controls how the skeleton of a [SpineWidget] is animated and rendered.
  36. ///
  37. /// Upon initialization of a [SpineWidget] the provided [onInitialized] callback method is called once. This method can be used
  38. /// to setup the initial animation(s) of the skeleton, among other things.
  39. ///
  40. /// After initialization is complete, the [SpineWidget] is rendered at the screen refresh rate. In each frame,
  41. /// the [AnimationState] is updated and applied to the [Skeleton].
  42. ///
  43. /// Next the optionally provided method [onBeforeUpdateWorldTransforms] is called, which can modify the
  44. /// skeleton before its current pose is calculated using [Skeleton.updateWorldTransforms]. After
  45. /// [Skeleton.updateWorldTransforms] has completed, the optional [onAfterUpdateWorldTransforms] method is
  46. /// called, which can modify the current pose before rendering the skeleton.
  47. ///
  48. /// Before the skeleton's current pose is rendered by the [SpineWidget] the optional [onBeforePaint] is called,
  49. /// which allows rendering backgrounds or other objects that should go behind the skeleton on the [Canvas]. The
  50. /// [SpineWidget] then renderes the skeleton's current pose, and finally calls the optional [onAfterPaint], which
  51. /// can render additional objects on top of the skeleton.
  52. ///
  53. /// The underlying [Atlas], [SkeletonData], [Skeleton], [AnimationStateData], [AnimationState], and [SkeletonDrawable]
  54. /// can be accessed through their respective getters to inspect and/or modify the skeleton and its associated data. Accessing
  55. /// this data is only allowed if the [SpineWidget] and its data have been initialized and have not been disposed yet.
  56. ///
  57. /// By default, the widget updates and renders the skeleton every frame. The [pause] method can be used to pause updating
  58. /// and rendering the skeleton. The [resume] method resumes updating and rendering the skeleton. The [isPlaying] getter
  59. /// reports the current state.
  60. class SpineWidgetController {
  61. SkeletonDrawable? _drawable;
  62. double _offsetX = 0, _offsetY = 0, _scaleX = 1, _scaleY = 1;
  63. bool _isPlaying = true;
  64. _SpineRenderObject? _renderObject;
  65. final void Function(SpineWidgetController controller)? onInitialized;
  66. final void Function(SpineWidgetController controller)? onBeforeUpdateWorldTransforms;
  67. final void Function(SpineWidgetController controller)? onAfterUpdateWorldTransforms;
  68. final void Function(SpineWidgetController controller, Canvas canvas)? onBeforePaint;
  69. final void Function(SpineWidgetController controller, Canvas canvas, List<RenderCommand> commands)? onAfterPaint;
  70. /// Constructs a new [SpineWidget] controller. See the class documentation of [SpineWidgetController] for information on
  71. /// the optional arguments.
  72. SpineWidgetController(
  73. {this.onInitialized, this.onBeforeUpdateWorldTransforms, this.onAfterUpdateWorldTransforms, this.onBeforePaint, this.onAfterPaint});
  74. void _initialize(SkeletonDrawable drawable) {
  75. var wasInitialized = _drawable != null;
  76. _drawable = drawable;
  77. if (!wasInitialized) onInitialized?.call(this);
  78. }
  79. /// The [Atlas] from which images to render the skeleton are sourced.
  80. Atlas get atlas {
  81. if (_drawable == null) throw Exception("Controller is not initialized yet.");
  82. return _drawable!.atlas;
  83. }
  84. /// The setup-pose data used by the skeleton.
  85. SkeletonData get skeletonData {
  86. if (_drawable == null) throw Exception("Controller is not initialized yet.");
  87. return _drawable!.skeletonData;
  88. }
  89. /// The mixing information used by the [AnimationState]
  90. AnimationStateData get animationStateData {
  91. if (_drawable == null) throw Exception("Controller is not initialized yet.");
  92. return _drawable!.animationStateData;
  93. }
  94. /// The [AnimationState] used to manage animations that are being applied to the
  95. /// skeleton.
  96. AnimationState get animationState {
  97. if (_drawable == null) throw Exception("Controller is not initialized yet.");
  98. return _drawable!.animationState;
  99. }
  100. /// The [Skeleton]
  101. Skeleton get skeleton {
  102. if (_drawable == null) throw Exception("Controller is not initialized yet.");
  103. return _drawable!.skeleton;
  104. }
  105. /// The [SkeletonDrawable]
  106. SkeletonDrawable get drawable {
  107. if (_drawable == null) throw Exception("Controller is not initialized yet.");
  108. return _drawable!;
  109. }
  110. void _setCoordinateTransform(double offsetX, double offsetY, double scaleX, double scaleY) {
  111. _offsetX = offsetX;
  112. _offsetY = offsetY;
  113. _scaleX = scaleX;
  114. _scaleY = scaleY;
  115. }
  116. void _setRenderObject(_SpineRenderObject? renderObject) {
  117. _renderObject = renderObject;
  118. }
  119. /// Transforms the coordinates given in the [SpineWidget] coordinate system in [position] to
  120. /// the skeleton coordinate system. See the `ik_following.dart` example how to use this
  121. /// to move a bone based on user touch input.
  122. Offset toSkeletonCoordinates(Offset position) {
  123. var x = position.dx;
  124. var y = position.dy;
  125. return Offset(x / _scaleX - _offsetX, y / _scaleY - _offsetY);
  126. }
  127. /// Pauses updating and rendering the skeleton.
  128. void pause() {
  129. _isPlaying = false;
  130. }
  131. /// Resumes updating and rendering the skeleton.
  132. void resume() {
  133. _isPlaying = true;
  134. _renderObject?._stopwatch.reset();
  135. _renderObject?._stopwatch.start();
  136. _renderObject?._scheduleFrame();
  137. }
  138. bool get isPlaying {
  139. return _isPlaying;
  140. }
  141. }
  142. enum _AssetType { asset, file, http, drawable }
  143. /// Base class for bounds providers. A bounds provider calculates the axis aligned bounding box
  144. /// used to scale and fit a skeleton inside the bounds of a [SpineWidget].
  145. abstract class BoundsProvider {
  146. const BoundsProvider();
  147. Bounds computeBounds(SkeletonDrawable drawable);
  148. }
  149. /// A [BoundsProvider] that calculates the bounding box of the skeleton based on the visible
  150. /// attachments in the setup pose.
  151. class SetupPoseBounds extends BoundsProvider {
  152. const SetupPoseBounds();
  153. @override
  154. Bounds computeBounds(SkeletonDrawable drawable) {
  155. return drawable.skeleton.getBounds();
  156. }
  157. }
  158. /// A [BoundsProvider] that returns fixed bounds.
  159. class RawBounds extends BoundsProvider {
  160. final double x, y, width, height;
  161. RawBounds(this.x, this.y, this.width, this.height);
  162. @override
  163. Bounds computeBounds(SkeletonDrawable drawable) {
  164. return Bounds(x, y, width, height);
  165. }
  166. }
  167. /// A [BoundsProvider] that calculates the bounding box needed for a combination of skins
  168. /// and an animation.
  169. class SkinAndAnimationBounds extends BoundsProvider {
  170. final List<String> skins;
  171. final String? animation;
  172. final double stepTime;
  173. /// Constructs a new provider that will use the given [skins] and [animation] to calculate
  174. /// the bounding box of the skeleton. If no skins are given, the default skin is used.
  175. /// The [stepTime], given in seconds, defines at what interval the bounds should be sampled
  176. /// across the entire animation.
  177. SkinAndAnimationBounds({List<String>? skins, this.animation, this.stepTime = 0.1})
  178. : skins = skins == null || skins.isEmpty ? ["default"] : skins;
  179. @override
  180. Bounds computeBounds(SkeletonDrawable drawable) {
  181. final data = drawable.skeletonData;
  182. final oldSkin = drawable.skeleton.getSkin();
  183. final customSkin = Skin("custom-skin");
  184. for (final skinName in skins) {
  185. final skin = data.findSkin(skinName);
  186. if (skin == null) continue;
  187. customSkin.addSkin(skin);
  188. }
  189. drawable.skeleton.setSkin(customSkin);
  190. drawable.skeleton.setToSetupPose();
  191. final animation = this.animation != null ? data.findAnimation(this.animation!) : null;
  192. double minX = double.infinity;
  193. double minY = double.infinity;
  194. double maxX = double.negativeInfinity;
  195. double maxY = double.negativeInfinity;
  196. if (animation == null) {
  197. final bounds = drawable.skeleton.getBounds();
  198. minX = bounds.x;
  199. minY = bounds.y;
  200. maxX = minX + bounds.width;
  201. maxY = minY + bounds.height;
  202. } else {
  203. drawable.animationState.setAnimation(0, animation, false);
  204. final steps = max(animation.getDuration() / stepTime, 1.0).toInt();
  205. for (int i = 0; i < steps; i++) {
  206. drawable.update(i > 0 ? stepTime : 0);
  207. final bounds = drawable.skeleton.getBounds();
  208. minX = min(minX, bounds.x);
  209. minY = min(minY, bounds.y);
  210. maxX = max(maxX, minX + bounds.width);
  211. maxY = max(maxY, minY + bounds.height);
  212. }
  213. }
  214. drawable.skeleton.setSkinByName("default");
  215. drawable.animationState.clearTracks();
  216. if (oldSkin != null) drawable.skeleton.setSkin(oldSkin);
  217. drawable.skeleton.setToSetupPose();
  218. drawable.update(0);
  219. customSkin.dispose();
  220. return Bounds(minX, minY, maxX - minX, maxY - minY);
  221. }
  222. }
  223. /// A [StatefulWidget] to display a Spine skeleton. The skeleton can be loaded from an asset bundle ([SpineWidget.fromAsset],
  224. /// local files [SpineWidget.fromFile], URLs [SpineWidget.fromHttp], or a pre-loaded [SkeletonDrawable] ([SpineWidget.fromDrawable]).
  225. ///
  226. /// The skeleton displayed by a `SpineWidget` can be controlled via a [SpineWidgetController].
  227. ///
  228. /// The size of the widget can be derived from the bounds provided by a [BoundsProvider]. If the widget is not sized by the bounds
  229. /// computed by the [BoundsProvider], the widget will use the computed bounds to fit the skeleton inside the widget's dimensions.
  230. class SpineWidget extends StatefulWidget {
  231. final _AssetType _assetType;
  232. final AssetBundle? _bundle;
  233. final String? _skeletonFile;
  234. final String? _atlasFile;
  235. final SkeletonDrawable? _drawable;
  236. final SpineWidgetController _controller;
  237. final BoxFit _fit;
  238. final Alignment _alignment;
  239. final BoundsProvider _boundsProvider;
  240. final bool _sizedByBounds;
  241. /// Constructs a new [SpineWidget] from files in the root bundle or the optionally specified [bundle]. The [_atlasFile] specifies the
  242. /// `.atlas` file to be loaded for the images used to render the skeleton. The [_skeletonFile] specifies either a Skeleton `.json` or
  243. /// `.skel` file containing the skeleton data.
  244. ///
  245. /// After initialization is complete, the provided [_controller] is invoked as per the [SpineWidgetController] semantics, to allow
  246. /// modifying how the skeleton inside the widget is animated and rendered.
  247. ///
  248. /// The skeleton is fitted and aligned inside the widget as per the [fit] and [alignment] arguments. For this purpose, the skeleton
  249. /// bounds must be computed via a [BoundsProvider]. By default, [BoxFit.contain], [Alignment.center], and a [SetupPoseBounds] provider
  250. /// are used.
  251. ///
  252. /// The widget can optionally by sized by the bounds provided by the [BoundsProvider] by passing `true` for [sizedByBounds].
  253. SpineWidget.fromAsset(this._atlasFile, this._skeletonFile, this._controller,
  254. {AssetBundle? bundle, BoxFit? fit, Alignment? alignment, BoundsProvider? boundsProvider, bool? sizedByBounds, super.key})
  255. : _assetType = _AssetType.asset,
  256. _fit = fit ?? BoxFit.contain,
  257. _alignment = alignment ?? Alignment.center,
  258. _boundsProvider = boundsProvider ?? const SetupPoseBounds(),
  259. _sizedByBounds = sizedByBounds ?? false,
  260. _drawable = null,
  261. _bundle = bundle ?? rootBundle;
  262. /// Constructs a new [SpineWidget] from files. The [_atlasFile] specifies the `.atlas` file to be loaded for the images used to render
  263. /// the skeleton. The [_skeletonFile] specifies either a Skeleton `.json` or `.skel` file containing the skeleton data.
  264. ///
  265. /// After initialization is complete, the provided [_controller] is invoked as per the [SpineWidgetController] semantics, to allow
  266. /// modifying how the skeleton inside the widget is animated and rendered.
  267. ///
  268. /// The skeleton is fitted and aligned inside the widget as per the [fit] and [alignment] arguments. For this purpose, the skeleton
  269. /// bounds must be computed via a [BoundsProvider]. By default, [BoxFit.contain], [Alignment.center], and a [SetupPoseBounds] provider
  270. /// are used.
  271. ///
  272. /// The widget can optionally by sized by the bounds provided by the [BoundsProvider] by passing `true` for [sizedByBounds].
  273. const SpineWidget.fromFile(this._atlasFile, this._skeletonFile, this._controller,
  274. {BoxFit? fit, Alignment? alignment, BoundsProvider? boundsProvider, bool? sizedByBounds, super.key})
  275. : _assetType = _AssetType.file,
  276. _bundle = null,
  277. _fit = fit ?? BoxFit.contain,
  278. _alignment = alignment ?? Alignment.center,
  279. _boundsProvider = boundsProvider ?? const SetupPoseBounds(),
  280. _sizedByBounds = sizedByBounds ?? false,
  281. _drawable = null;
  282. /// Constructs a new [SpineWidget] from HTTP URLs. The [_atlasFile] specifies the `.atlas` file to be loaded for the images used to render
  283. /// the skeleton. The [_skeletonFile] specifies either a Skeleton `.json` or `.skel` file containing the skeleton data.
  284. ///
  285. /// After initialization is complete, the provided [_controller] is invoked as per the [SpineWidgetController] semantics, to allow
  286. /// modifying how the skeleton inside the widget is animated and rendered.
  287. ///
  288. /// The skeleton is fitted and aligned inside the widget as per the [fit] and [alignment] arguments. For this purpose, the skeleton
  289. /// bounds must be computed via a [BoundsProvider]. By default, [BoxFit.contain], [Alignment.center], and a [SetupPoseBounds] provider
  290. /// are used.
  291. ///
  292. /// The widget can optionally by sized by the bounds provided by the [BoundsProvider] by passing `true` for [sizedByBounds].
  293. const SpineWidget.fromHttp(this._atlasFile, this._skeletonFile, this._controller,
  294. {BoxFit? fit, Alignment? alignment, BoundsProvider? boundsProvider, bool? sizedByBounds, super.key})
  295. : _assetType = _AssetType.http,
  296. _bundle = null,
  297. _fit = fit ?? BoxFit.contain,
  298. _alignment = alignment ?? Alignment.center,
  299. _boundsProvider = boundsProvider ?? const SetupPoseBounds(),
  300. _sizedByBounds = sizedByBounds ?? false,
  301. _drawable = null;
  302. /// Constructs a new [SpineWidget] from a [SkeletonDrawable].
  303. ///
  304. /// After initialization is complete, the provided [_controller] is invoked as per the [SpineWidgetController] semantics, to allow
  305. /// modifying how the skeleton inside the widget is animated and rendered.
  306. ///
  307. /// The skeleton is fitted and aligned inside the widget as per the [fit] and [alignment] arguments. For this purpose, the skeleton
  308. /// bounds must be computed via a [BoundsProvider]. By default, [BoxFit.contain], [Alignment.center], and a [SetupPoseBounds] provider
  309. /// are used.
  310. ///
  311. /// The widget can optionally by sized by the bounds provided by the [BoundsProvider] by passing `true` for [sizedByBounds].
  312. const SpineWidget.fromDrawable(this._drawable, this._controller,
  313. {BoxFit? fit, Alignment? alignment, BoundsProvider? boundsProvider, bool? sizedByBounds, super.key})
  314. : _assetType = _AssetType.drawable,
  315. _bundle = null,
  316. _fit = fit ?? BoxFit.contain,
  317. _alignment = alignment ?? Alignment.center,
  318. _boundsProvider = boundsProvider ?? const SetupPoseBounds(),
  319. _sizedByBounds = sizedByBounds ?? false,
  320. _skeletonFile = null,
  321. _atlasFile = null;
  322. @override
  323. State<SpineWidget> createState() => _SpineWidgetState();
  324. }
  325. class _SpineWidgetState extends State<SpineWidget> {
  326. late Bounds _computedBounds;
  327. SkeletonDrawable? _drawable;
  328. @override
  329. void initState() {
  330. super.initState();
  331. if (widget._assetType == _AssetType.drawable) {
  332. loadDrawable(widget._drawable!);
  333. } else {
  334. loadFromAsset(widget._bundle, widget._atlasFile!, widget._skeletonFile!, widget._assetType);
  335. }
  336. }
  337. @override
  338. void didUpdateWidget(covariant SpineWidget oldWidget) {
  339. super.didUpdateWidget(oldWidget);
  340. // Check if the skeleton/atlas data has changed. Only re-create
  341. // everything if it has, otherwise, keep using what's already been
  342. // loaded.
  343. bool hasChanged = true;
  344. if (oldWidget._assetType == widget._assetType) {
  345. if (oldWidget._assetType == _AssetType.drawable &&
  346. oldWidget._drawable == widget._drawable) {
  347. hasChanged = false;
  348. } else if (oldWidget._skeletonFile == widget._skeletonFile &&
  349. oldWidget._atlasFile == widget._atlasFile &&
  350. oldWidget._controller == widget._controller &&
  351. oldWidget._bundle == widget._bundle) {
  352. hasChanged = false;
  353. }
  354. }
  355. if (hasChanged) {
  356. widget._controller._drawable?.dispose();
  357. _drawable = null;
  358. if (widget._assetType == _AssetType.drawable) {
  359. loadDrawable(widget._drawable!);
  360. } else {
  361. loadFromAsset(widget._bundle, widget._atlasFile!, widget._skeletonFile!, widget._assetType);
  362. }
  363. }
  364. }
  365. void loadDrawable(SkeletonDrawable drawable) {
  366. _drawable = drawable;
  367. _computedBounds = widget._boundsProvider.computeBounds(drawable);
  368. widget._controller._initialize(drawable);
  369. setState(() {});
  370. }
  371. void loadFromAsset(AssetBundle? bundle, String atlasFile, String skeletonFile, _AssetType assetType) async {
  372. switch (assetType) {
  373. case _AssetType.asset:
  374. loadDrawable(await SkeletonDrawable.fromAsset(atlasFile, skeletonFile, bundle: bundle));
  375. break;
  376. case _AssetType.file:
  377. loadDrawable(await SkeletonDrawable.fromFile(atlasFile, skeletonFile));
  378. break;
  379. case _AssetType.http:
  380. loadDrawable(await SkeletonDrawable.fromHttp(atlasFile, skeletonFile));
  381. break;
  382. case _AssetType.drawable:
  383. throw Exception("Drawable can not be loaded via loadFromAsset().");
  384. }
  385. }
  386. @override
  387. Widget build(BuildContext context) {
  388. if (_drawable != null) {
  389. return _SpineRenderObjectWidget(
  390. _drawable!, widget._controller, widget._fit, widget._alignment, _computedBounds, widget._sizedByBounds);
  391. } else {
  392. return const SizedBox();
  393. }
  394. }
  395. @override
  396. void dispose() {
  397. super.dispose();
  398. widget._controller._drawable?.dispose();
  399. }
  400. }
  401. class _SpineRenderObjectWidget extends LeafRenderObjectWidget {
  402. final SkeletonDrawable _skeletonDrawable;
  403. final SpineWidgetController _controller;
  404. final BoxFit _fit;
  405. final Alignment _alignment;
  406. final Bounds _bounds;
  407. final bool _sizedByBounds;
  408. const _SpineRenderObjectWidget(this._skeletonDrawable, this._controller, this._fit, this._alignment, this._bounds, this._sizedByBounds);
  409. @override
  410. RenderObject createRenderObject(BuildContext context) {
  411. return _SpineRenderObject(_skeletonDrawable, _controller, _fit, _alignment, _bounds, _sizedByBounds);
  412. }
  413. @override
  414. void updateRenderObject(BuildContext context, covariant _SpineRenderObject renderObject) {
  415. renderObject.skeletonDrawable = _skeletonDrawable;
  416. renderObject.fit = _fit;
  417. renderObject.alignment = _alignment;
  418. renderObject.bounds = _bounds;
  419. renderObject.sizedByBounds = _sizedByBounds;
  420. }
  421. }
  422. class _SpineRenderObject extends RenderBox {
  423. SkeletonDrawable _skeletonDrawable;
  424. final SpineWidgetController _controller;
  425. double _deltaTime = 0;
  426. final Stopwatch _stopwatch = Stopwatch();
  427. BoxFit _fit;
  428. Alignment _alignment;
  429. Bounds _bounds;
  430. bool _sizedByBounds;
  431. bool _disposed = false;
  432. bool _firstUpdated = false;
  433. _SpineRenderObject(this._skeletonDrawable, this._controller, this._fit, this._alignment, this._bounds, this._sizedByBounds);
  434. set skeletonDrawable(SkeletonDrawable skeletonDrawable) {
  435. if (_skeletonDrawable == skeletonDrawable) return;
  436. _skeletonDrawable = skeletonDrawable;
  437. markNeedsLayout();
  438. markNeedsPaint();
  439. }
  440. BoxFit get fit => _fit;
  441. set fit(BoxFit fit) {
  442. if (fit != _fit) {
  443. _fit = fit;
  444. markNeedsLayout();
  445. markNeedsPaint();
  446. }
  447. }
  448. Alignment get alignment => _alignment;
  449. set alignment(Alignment alignment) {
  450. if (alignment != _alignment) {
  451. _alignment = alignment;
  452. markNeedsLayout();
  453. markNeedsPaint();
  454. }
  455. }
  456. Bounds get bounds => _bounds;
  457. set bounds(Bounds bounds) {
  458. if (bounds != _bounds) {
  459. _bounds = bounds;
  460. markNeedsLayout();
  461. markNeedsPaint();
  462. }
  463. }
  464. bool get sizedByBounds => _sizedByBounds;
  465. set sizedByBounds(bool sizedByBounds) {
  466. if (sizedByBounds != _sizedByBounds) {
  467. _sizedByBounds = _sizedByBounds;
  468. markNeedsLayout();
  469. markNeedsPaint();
  470. }
  471. }
  472. @override
  473. bool get sizedByParent => !_sizedByBounds;
  474. @override
  475. bool get isRepaintBoundary => true;
  476. @override
  477. bool hitTestSelf(Offset position) => true;
  478. @override
  479. double computeMinIntrinsicWidth(double height) {
  480. return _computeConstrainedSize(BoxConstraints.tightForFinite(height: height)).width;
  481. }
  482. @override
  483. double computeMaxIntrinsicWidth(double height) {
  484. return _computeConstrainedSize(BoxConstraints.tightForFinite(height: height)).width;
  485. }
  486. @override
  487. double computeMinIntrinsicHeight(double width) {
  488. return _computeConstrainedSize(BoxConstraints.tightForFinite(width: width)).height;
  489. }
  490. @override
  491. double computeMaxIntrinsicHeight(double width) {
  492. return _computeConstrainedSize(BoxConstraints.tightForFinite(width: width)).height;
  493. }
  494. // Called when not sizedByParent, uses the intrinsic width/height for sizing, while trying to retain aspect ratio.
  495. @override
  496. void performLayout() {
  497. if (!sizedByParent) size = _computeConstrainedSize(constraints);
  498. }
  499. // Called when sizedByParent, we want to go as big as possible.
  500. @override
  501. void performResize() {
  502. size = constraints.biggest;
  503. }
  504. Size _computeConstrainedSize(BoxConstraints constraints) {
  505. return sizedByParent
  506. ? constraints.smallest
  507. : constraints.constrainSizeAndAttemptToPreserveAspectRatio(Size(_bounds.width, _bounds.height));
  508. }
  509. @override
  510. void attach(rendering.PipelineOwner owner) {
  511. super.attach(owner);
  512. _stopwatch.start();
  513. SchedulerBinding.instance.scheduleFrameCallback(_beginFrame);
  514. _controller._setRenderObject(this);
  515. }
  516. @override
  517. void detach() {
  518. _stopwatch.stop();
  519. super.detach();
  520. _controller._setRenderObject(null);
  521. }
  522. @override
  523. void dispose() {
  524. super.dispose();
  525. _disposed = true;
  526. }
  527. void _scheduleFrame() {
  528. SchedulerBinding.instance.scheduleFrameCallback(_beginFrame);
  529. }
  530. void _beginFrame(Duration duration) {
  531. if (_disposed) return;
  532. _deltaTime = _stopwatch.elapsedTicks / _stopwatch.frequency;
  533. _stopwatch.reset();
  534. _stopwatch.start();
  535. if (_controller.isPlaying) {
  536. _controller.onBeforeUpdateWorldTransforms?.call(_controller);
  537. _skeletonDrawable.update(_deltaTime);
  538. _controller.onAfterUpdateWorldTransforms?.call(_controller);
  539. markNeedsPaint();
  540. _scheduleFrame();
  541. }
  542. _firstUpdated = true;
  543. }
  544. void _setCanvasTransform(Canvas canvas, Offset offset) {
  545. final double x = -_bounds.x - _bounds.width / 2.0 - (_alignment.x * _bounds.width / 2.0);
  546. final double y = -_bounds.y - _bounds.height / 2.0 - (_alignment.y * _bounds.height / 2.0);
  547. double scaleX = 1.0, scaleY = 1.0;
  548. switch (_fit) {
  549. case BoxFit.fill:
  550. scaleX = size.width / _bounds.width;
  551. scaleY = size.height / _bounds.height;
  552. break;
  553. case BoxFit.contain:
  554. scaleX = scaleY = min(size.width / _bounds.width, size.height / _bounds.height);
  555. break;
  556. case BoxFit.cover:
  557. scaleX = scaleY = max(size.width / _bounds.width, size.height / _bounds.height);
  558. break;
  559. case BoxFit.fitHeight:
  560. scaleX = scaleY = size.height / _bounds.height;
  561. break;
  562. case BoxFit.fitWidth:
  563. scaleX = scaleY = size.width / _bounds.width;
  564. break;
  565. case BoxFit.none:
  566. scaleX = scaleY = 1.0;
  567. break;
  568. case BoxFit.scaleDown:
  569. final double scale = min(size.width / _bounds.width, size.height / _bounds.height);
  570. scaleX = scaleY = scale < 1.0 ? scale : 1.0;
  571. break;
  572. }
  573. var offsetX = offset.dx + size.width / 2.0 + (_alignment.x * size.width / 2.0);
  574. var offsetY = offset.dy + size.height / 2.0 + (_alignment.y * size.height / 2.0);
  575. canvas
  576. ..translate(offsetX, offsetY)
  577. ..scale(scaleX, scaleY)
  578. ..translate(x, y);
  579. _controller._setCoordinateTransform(x + offsetX / scaleX, y + offsetY / scaleY, scaleX, scaleY);
  580. }
  581. @override
  582. void paint(PaintingContext context, Offset offset) {
  583. final Canvas canvas = context.canvas
  584. ..save()
  585. ..clipRect(offset & size);
  586. canvas.save();
  587. _setCanvasTransform(canvas, offset);
  588. if (_firstUpdated) {
  589. _controller.onBeforePaint?.call(_controller, canvas);
  590. final commands = _skeletonDrawable.renderToCanvas(canvas);
  591. _controller.onAfterPaint?.call(_controller, canvas, commands);
  592. }
  593. canvas.restore();
  594. }
  595. }