[flutter] Added AtlasFlutter.fromMemroy, SkeletonDataFlutter.fromMemory, SkeletonDrawableFlutter.fromMemory, and SpineWidget.fromMemory. See CHANGELOG.md for details and #2939

This commit is contained in:
Mario Zechner 2025-11-06 12:36:53 +01:00
parent 332a001192
commit 5658eec015
8 changed files with 353 additions and 43 deletions

View File

@ -351,7 +351,7 @@
- Removed rather useless old menu entries `GameObject - Spine - SkeletonRenderer` and the like which are spawning e.g. a GameObject with an empty `SkeletonRenderer` component without `SkeletonDataAsset` assigned and thus also not initialized properly.
- **Changes of default values**
- Changed default atlas texture workflow from PMA to straight alpha textures. This move was done because straight alpha textures are compatible with both Gamma and Linear color space, with the latter being the default for quite some time now in Unity. Note that `PMA Vertex Color` is unaffected and shall be enabled as usual to allow for single-pass additive rendering.
- **Additions**
- Added Spine Preferences `Switch Texture Workflow` functionality to quickly switch to the respective PMA or straight-alpha texture and material presets.
- Added a workflow mismatch dialog showing whenever problematic PMA vs. straight alpha settings are detected at a newly imported `.atlas.txt` file. Invalid settings include the atlas being PMA and project using Linear color space, and a mismatch of Auto-Import presets set to straight alpha compared to the atlas being PMA and vice versa. The dialog offers an option to automatically fix the problematic setting on the import side and links website documentation for export settings. This dialog can be disabled and re-enabled via Spine preferences.
@ -359,7 +359,7 @@
- `Threaded MeshGeneration`: Default value for SkeletonRenderer and SkeletonGraphic threaded mesh generation
- `Threaded Animation`: Default value for SkeletonAnimation and SkeletonMecanim threaded animation updates
- Even when threading is enabled, the threading system defaults to `SkeletonRenderer` and `SkeletonAnimation` user callbacks like `UpdateWorld` (not including `AnimationState` callbacks) being issued on the main thread to support existing user code. Can be configured via `SkeletonUpdateSystem.Instance.MainThreadUpdateCallbacks = false` to perform callbacks on worker threads if parallel execution is supported and desired by the user code. `OnPostProcessVertices` is an exception, as it it's deliberately left on worker threads so that parallellization can be utilized. Note that most Unity API calls are restricted to the main thread.
- For `SkeletonAnimation.AnimationState` callbacks, there are additional main thread callbacks `MainThreadStart`, `MainThreadInterrupt`, `MainThreadEnd`, `MainThreadDispose`, `MainThreadComplete` and `MainThreadEvent` provided directly at `SkeletonAnimation`, e.g. `SkeletonAnimation.MainThreadComplete` for `SkeletonAnimation.AnimationState.Complete` and so on. Please note that this requires a change of user code to subscribe to these main thread delegate variants instead.
- For `SkeletonAnimation.AnimationState` callbacks, there are additional main thread callbacks `MainThreadStart`, `MainThreadInterrupt`, `MainThreadEnd`, `MainThreadDispose`, `MainThreadComplete` and `MainThreadEvent` provided directly at `SkeletonAnimation`, e.g. `SkeletonAnimation.MainThreadComplete` for `SkeletonAnimation.AnimationState.Complete` and so on. Please note that this requires a change of user code to subscribe to these main thread delegate variants instead.
- The same applies to the `TrackEntry.Start`, `Interrupt`, `End`, `Dispose`, `Complete`, and `Event` events. If you need these callbacks to run on the main thread instead of worker threads, you should register them using the corresponding `SkeletonAnimation.MainThreadStart`, `MainThreadInterrupt`, etc. callbacks. Note that this does require a small code change, since these events are **not** automatically unregistered when the `TrackEntry` is removed. Youll need to handle that manually, typically with logic such as `if (trackEntry.Animation == attackAnimation) ..`.
- Added `SkeletonUpdateSystem.Instance.GroupRenderersBySkeletonType` and `GroupAnimationBySkeletonType` properties. Defaults to disabled. Later when smart partitioning is implemented, enabling this parameter might slightly improve cache locality. Until then having it enabled combined with different skeleton complexity would lead to worse load balancing.
- Added previously missing editor drag & drop skeleton instantiation option *SkeletonGraphic (UI) Mecanim* combining components `SkeletonGraphic` and `SkeletonMecanim`.
@ -399,6 +399,10 @@
### Flutter
- **Additions**
- Added `fromMemory` methods to `AtlasFlutter`, `SkeletonDataFlutter`, `SkeletonDrawableFlutter`, and `SpineWidget` for loading Spine data from custom sources (memory, encrypted storage, databases, custom caching, etc.)
- Added example `load_from_memory.dart` demonstrating how to load all assets into memory and use the `fromMemory` API
- **Breaking changes**
- Updated to use the new auto-generated Dart runtime with all the Dart API changes above

View File

@ -1,3 +1,11 @@
# 4.3.1
## Flutter
- **Additions**
- Added `fromMemory` methods to `AtlasFlutter`, `SkeletonDataFlutter`, `SkeletonDrawableFlutter`, and `SpineWidget` for loading Spine data from custom sources (memory, encrypted storage, databases, custom caching, etc.)
- Added example `load_from_memory.dart` demonstrating how to load all assets into memory and use the `fromMemory` API
# 4.3.0
## Dart

View File

@ -0,0 +1,162 @@
//
// Spine Runtimes License Agreement
// Last updated April 5, 2025. Replaces all prior versions.
//
// Copyright (c) 2013-2025, Esoteric Software LLC
//
// Integration of the Spine Runtimes into software or otherwise creating
// derivative works of the Spine Runtimes is permitted under the terms and
// conditions of Section 2 of the Spine Editor License Agreement:
// http://esotericsoftware.com/spine-editor-license
//
// Otherwise, it is permitted to integrate the Spine Runtimes into software
// or otherwise create derivative works of the Spine Runtimes (collectively,
// "Products"), provided that each user of the Products must obtain their own
// Spine Editor license and redistribution of the Products in any form must
// include this license and copyright notice.
//
// THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
// BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
// THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
import 'dart:typed_data';
import 'package:spine_flutter/spine_flutter.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
/// This example demonstrates loading Spine skeleton data from memory using the
/// fromMemory methods. This is useful when you want to:
/// - Load data from custom storage (e.g., encrypted assets, databases)
/// - Implement custom caching strategies
/// - Download and cache assets at runtime
/// - Pre-process assets before loading
///
/// The example loads all files (atlas, skeleton, images) into memory first,
/// then uses the fromMemory API to create a SpineWidget.
class LoadFromMemory extends StatefulWidget {
const LoadFromMemory({super.key});
@override
State<LoadFromMemory> createState() => _LoadFromMemoryState();
}
class _LoadFromMemoryState extends State<LoadFromMemory> {
// In-memory cache of all loaded files
final Map<String, Uint8List> _fileCache = {};
bool _isLoading = true;
String _loadingStatus = 'Loading assets into memory...';
@override
void initState() {
super.initState();
_loadAssetsIntoMemory();
}
Future<void> _loadAssetsIntoMemory() async {
try {
// Step 1: Load atlas file into memory
setState(() => _loadingStatus = 'Loading atlas file...');
final atlasBytes = await rootBundle.load('assets/spineboy.atlas');
_fileCache['assets/spineboy.atlas'] = atlasBytes.buffer.asUint8List();
// Step 2: Load skeleton file into memory
setState(() => _loadingStatus = 'Loading skeleton file...');
final skelBytes = await rootBundle.load('assets/spineboy-pro.skel');
_fileCache['assets/spineboy-pro.skel'] = skelBytes.buffer.asUint8List();
// Step 3: Load image file(s) into memory
setState(() => _loadingStatus = 'Loading image files...');
final imageBytes = await rootBundle.load('assets/spineboy.png');
_fileCache['assets/spineboy.png'] = imageBytes.buffer.asUint8List();
// All files loaded into memory!
setState(() {
_loadingStatus = 'All assets loaded into memory (${_fileCache.length} files, ${_getTotalSize()} bytes)';
_isLoading = false;
});
} catch (e) {
setState(() {
_loadingStatus = 'Error loading assets: $e';
_isLoading = false;
});
}
}
int _getTotalSize() {
return _fileCache.values.fold(0, (sum, bytes) => sum + bytes.length);
}
// Custom file loader that returns data from our in-memory cache
Future<Uint8List> _loadFromCache(String filename) async {
final data = _fileCache[filename];
if (data == null) {
throw Exception('File not found in cache: $filename');
}
return data;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Load From Memory')),
body: _isLoading
? Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(_loadingStatus),
],
),
)
: Column(
children: [
Container(
padding: const EdgeInsets.all(16),
color: Colors.blue.shade50,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Files in Memory Cache:',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
..._fileCache.entries.map((entry) => Text(
' ${entry.key}: ${entry.value.length} bytes',
style: Theme.of(context).textTheme.bodySmall,
)),
const SizedBox(height: 8),
Text(
'Total: ${_getTotalSize()} bytes',
style: Theme.of(context).textTheme.titleSmall,
),
],
),
),
Expanded(
child: SpineWidget.fromMemory(
'assets/spineboy.atlas',
'assets/spineboy-pro.skel',
_loadFromCache,
SpineWidgetController(
onInitialized: (controller) {
controller.animationState.setAnimation(0, 'walk', true);
},
),
),
),
],
),
);
}
}

View File

@ -35,6 +35,7 @@ import 'animation_state_events.dart';
import 'dress_up.dart';
import 'flame_example.dart';
import 'ik_following.dart';
import 'load_from_memory.dart';
import 'pause_play_animation.dart';
import 'physics.dart';
import 'simple_animation.dart';
@ -100,6 +101,13 @@ class ExampleSelector extends StatelessWidget {
},
),
spacer,
ElevatedButton(
child: const Text('Load From Memory'),
onPressed: () {
Navigator.push(context, MaterialPageRoute<void>(builder: (context) => const LoadFromMemory()));
},
),
spacer,
ElevatedButton(
child: const Text('Flame: Simple Example'),
onPressed: () {

View File

@ -5,5 +5,6 @@
import FlutterMacOS
import Foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
}

View File

@ -33,8 +33,37 @@ class AtlasFlutter extends Atlas {
AtlasFlutter._(super.ptr, this.atlasPages, this.atlasPagePaints) : super.fromPointer();
/// Internal method to load atlas and images
static Future<AtlasFlutter> _load(String atlasFileName, Future<Uint8List> Function(String name) loadFile) async {
/// Loads an [AtlasFlutter] using a custom file loading function.
///
/// This is the most flexible loading method that allows loading atlas data and images from any source
/// (memory, custom storage, network with caching, etc.).
///
/// Parameters:
/// - [atlasFileName]: The path/name of the atlas file. This is passed to [loadFile] to load the atlas data.
/// - [loadFile]: A function that takes a filename and returns the file data as [Uint8List].
/// This function will be called once for the atlas file, and once for each atlas page image.
/// The image paths are relative to the atlas file's directory (as specified in the atlas file).
///
/// Example - Loading from memory:
/// ```dart
/// final atlasData = Uint8List.fromList([...]); // Your atlas file data
/// final page1Data = Uint8List.fromList([...]); // Your first page image data
/// final page2Data = Uint8List.fromList([...]); // Your second page image data
///
/// final atlas = await AtlasFlutter.fromMemory(
/// 'character.atlas',
/// (filename) async {
/// if (filename == 'character.atlas') return atlasData;
/// if (filename == 'character.png') return page1Data;
/// if (filename == 'character2.png') return page2Data;
/// throw Exception('Unknown file: $filename');
/// },
/// );
/// ```
///
/// Note: The [loadFile] function receives the full relative path for images
/// (e.g., "directory/page.png" if the atlas file specifies that path).
static Future<AtlasFlutter> fromMemory(String atlasFileName, Future<Uint8List> Function(String name) loadFile) async {
// Load atlas data
final atlasBytes = await loadFile(atlasFileName);
final atlasData = convert.utf8.decode(atlasBytes);
@ -83,7 +112,7 @@ class AtlasFlutter extends Atlas {
/// Loads an [AtlasFlutter] from the file [atlasFileName] in the root bundle or the optionally provided [bundle].
static Future<AtlasFlutter> fromAsset(String atlasFileName, {AssetBundle? bundle}) async {
bundle ??= rootBundle;
return _load(atlasFileName, (file) async => (await bundle!.load(file)).buffer.asUint8List());
return fromMemory(atlasFileName, (file) async => (await bundle!.load(file)).buffer.asUint8List());
}
/// Loads an [AtlasFlutter] from the file [atlasFileName].
@ -91,12 +120,12 @@ class AtlasFlutter extends Atlas {
if (kIsWeb) {
throw UnsupportedError('File operations are not supported on web. Use fromAsset or fromHttp instead.');
}
return _load(atlasFileName, (file) => File(file).readAsBytes());
return fromMemory(atlasFileName, (file) => File(file).readAsBytes());
}
/// Loads an [AtlasFlutter] from the URL [atlasURL].
static Future<AtlasFlutter> fromHttp(String atlasURL) async {
return _load(atlasURL, (file) async {
return fromMemory(atlasURL, (file) async {
final response = await http.get(Uri.parse(file));
if (response.statusCode != 200) {
throw Exception('Failed to load $file: ${response.statusCode}');
@ -122,21 +151,54 @@ class AtlasFlutter extends Atlas {
class SkeletonDataFlutter extends SkeletonData {
SkeletonDataFlutter._(super.ptr) : super.fromPointer();
/// Loads a [SkeletonDataFlutter] using a custom file loading function.
///
/// This is the most flexible loading method that allows loading skeleton data from any source
/// (memory, custom storage, network with caching, etc.).
///
/// Parameters:
/// - [atlas]: The [AtlasFlutter] to use for resolving attachment images.
/// - [skeletonFile]: The path/name of the skeleton file. This is passed to [loadFile] to load the skeleton data.
/// - [loadFile]: A function that takes a filename and returns the file data as [Uint8List].
///
/// Example - Loading from memory:
/// ```dart
/// final skeletonData = Uint8List.fromList([...]); // Your skeleton file data
///
/// final skeleton = await SkeletonDataFlutter.fromMemory(
/// atlas,
/// 'character.json',
/// (filename) async {
/// if (filename == 'character.json') return skeletonData;
/// throw Exception('Unknown file: $filename');
/// },
/// );
/// ```
///
/// Throws an [Exception] in case the skeleton data could not be loaded.
static Future<SkeletonDataFlutter> fromMemory(
AtlasFlutter atlas,
String skeletonFile,
Future<Uint8List> Function(String name) loadFile,
) async {
final fileData = await loadFile(skeletonFile);
if (skeletonFile.endsWith(".json")) {
final jsonData = convert.utf8.decode(fileData);
final skeletonData = loadSkeletonDataJson(atlas, jsonData, path: skeletonFile);
return SkeletonDataFlutter._(skeletonData.nativePtr.cast());
} else {
final skeletonData = loadSkeletonDataBinary(atlas, fileData, path: skeletonFile);
return SkeletonDataFlutter._(skeletonData.nativePtr.cast());
}
}
/// Loads a [SkeletonDataFlutter] from the file [skeletonFile] in the root bundle or the optionally provided [bundle].
/// Uses the provided [atlasFlutter] to resolve attachment images.
///
/// Throws an [Exception] in case the skeleton data could not be loaded.
static Future<SkeletonDataFlutter> fromAsset(AtlasFlutter atlas, String skeletonFile, {AssetBundle? bundle}) async {
bundle ??= rootBundle;
if (skeletonFile.endsWith(".json")) {
final jsonData = await bundle.loadString(skeletonFile);
final skeletonData = loadSkeletonDataJson(atlas, jsonData, path: skeletonFile);
return SkeletonDataFlutter._(skeletonData.nativePtr.cast());
} else {
final binaryData = (await bundle.load(skeletonFile)).buffer.asUint8List();
final skeletonData = loadSkeletonDataBinary(atlas, binaryData, path: skeletonFile);
return SkeletonDataFlutter._(skeletonData.nativePtr.cast());
}
return fromMemory(atlas, skeletonFile, (file) async => (await bundle!.load(file)).buffer.asUint8List());
}
/// Loads a [SkeletonDataFlutter] from the file [skeletonFile]. Uses the provided [atlasFlutter] to resolve attachment images.
@ -146,36 +208,20 @@ class SkeletonDataFlutter extends SkeletonData {
if (kIsWeb) {
throw UnsupportedError('File operations are not supported on web. Use fromAsset or fromHttp instead.');
}
if (skeletonFile.endsWith(".json")) {
final jsonData = await File(skeletonFile).readAsString();
final skeletonData = loadSkeletonDataJson(atlasFlutter, jsonData, path: skeletonFile);
return SkeletonDataFlutter._(skeletonData.nativePtr.cast());
} else {
final binaryData = await File(skeletonFile).readAsBytes();
final skeletonData = loadSkeletonDataBinary(atlasFlutter, binaryData, path: skeletonFile);
return SkeletonDataFlutter._(skeletonData.nativePtr.cast());
}
return fromMemory(atlasFlutter, skeletonFile, (file) => File(file).readAsBytes());
}
/// Loads a [SkeletonDataFlutter] from the URL [skeletonURL]. Uses the provided [atlasFlutter] to resolve attachment images.
///
/// Throws an [Exception] in case the skeleton data could not be loaded.
static Future<SkeletonDataFlutter> fromHttp(AtlasFlutter atlasFlutter, String skeletonURL) async {
if (skeletonURL.endsWith(".json")) {
final response = await http.get(Uri.parse(skeletonURL));
return fromMemory(atlasFlutter, skeletonURL, (file) async {
final response = await http.get(Uri.parse(file));
if (response.statusCode != 200) {
throw Exception('Failed to load skeleton from $skeletonURL: ${response.statusCode}');
throw Exception('Failed to load $file: ${response.statusCode}');
}
final skeletonData = loadSkeletonDataJson(atlasFlutter, response.body, path: skeletonURL);
return SkeletonDataFlutter._(skeletonData.nativePtr.cast());
} else {
final response = await http.get(Uri.parse(skeletonURL));
if (response.statusCode != 200) {
throw Exception('Failed to load skeleton from $skeletonURL: ${response.statusCode}');
}
final skeletonData = loadSkeletonDataBinary(atlasFlutter, response.bodyBytes, path: skeletonURL);
return SkeletonDataFlutter._(skeletonData.nativePtr.cast());
}
return response.bodyBytes;
});
}
}
@ -312,6 +358,46 @@ class SkeletonDrawableFlutter extends SkeletonDrawable {
/// In that case a call to [dispose] will also dispose the atlas and skeleton data.
SkeletonDrawableFlutter(this.atlasFlutter, this.skeletonData, this._ownsAtlasAndSkeletonData) : super(skeletonData);
/// Constructs a new skeleton drawable using a custom file loading function.
///
/// This is the most flexible loading method that allows loading atlas and skeleton data from any source
/// (memory, custom storage, network with caching, etc.).
///
/// Parameters:
/// - [atlasFile]: The path/name of the atlas file. This is passed to [loadFile] to load the atlas data.
/// - [skeletonFile]: The path/name of the skeleton file. This is passed to [loadFile] to load the skeleton data.
/// - [loadFile]: A function that takes a filename and returns the file data as [Uint8List].
/// This function will be called for the atlas file, skeleton file, and each atlas page image.
///
/// Example - Loading from memory:
/// ```dart
/// final atlasData = Uint8List.fromList([...]); // Your atlas file data
/// final skeletonData = Uint8List.fromList([...]); // Your skeleton file data
/// final imageData = Uint8List.fromList([...]); // Your image file data
///
/// final drawable = await SkeletonDrawableFlutter.fromMemory(
/// 'character.atlas',
/// 'character.json',
/// (filename) async {
/// if (filename == 'character.atlas') return atlasData;
/// if (filename == 'character.json') return skeletonData;
/// if (filename == 'character.png') return imageData;
/// throw Exception('Unknown file: $filename');
/// },
/// );
/// ```
///
/// Throws an exception in case the data could not be loaded.
static Future<SkeletonDrawableFlutter> fromMemory(
String atlasFile,
String skeletonFile,
Future<Uint8List> Function(String name) loadFile,
) async {
final atlasFlutter = await AtlasFlutter.fromMemory(atlasFile, loadFile);
final skeletonDataFlutter = await SkeletonDataFlutter.fromMemory(atlasFlutter, skeletonFile, loadFile);
return SkeletonDrawableFlutter(atlasFlutter, skeletonDataFlutter, true);
}
/// Constructs a new skeleton drawable from the [atlasFile] and [skeletonFile] from the root asset bundle
/// or the optionally provided [bundle].
///

View File

@ -28,6 +28,7 @@
//
import 'dart:math';
import 'dart:typed_data';
import 'package:flutter/rendering.dart' as rendering;
import 'package:flutter/scheduler.dart';
@ -164,7 +165,7 @@ class SpineWidgetController {
}
}
enum _AssetType { asset, file, http, drawable }
enum _AssetType { asset, file, http, memory, 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].
@ -262,7 +263,8 @@ class SkinAndAnimationBounds extends BoundsProvider {
}
/// 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 [SkeletonDrawableFlutter] ([SpineWidget.fromDrawable]).
/// local files [SpineWidget.fromFile], URLs [SpineWidget.fromHttp], memory ([SpineWidget.fromMemory]), or a pre-loaded
/// [SkeletonDrawableFlutter] ([SpineWidget.fromDrawable]).
///
/// The skeleton displayed by a `SpineWidget` can be controlled via a [SpineWidgetController].
///
@ -274,6 +276,7 @@ class SpineWidget extends StatefulWidget {
final String? _skeletonFile;
final String? _atlasFile;
final SkeletonDrawableFlutter? _drawable;
final Future<Uint8List> Function(String)? _loadFile;
final SpineWidgetController _controller;
final BoxFit _fit;
final Alignment _alignment;
@ -308,6 +311,7 @@ class SpineWidget extends StatefulWidget {
_boundsProvider = boundsProvider ?? const SetupPoseBounds(),
_sizedByBounds = sizedByBounds ?? false,
_drawable = null,
_loadFile = null,
_bundle = bundle ?? rootBundle;
/// Constructs a new [SpineWidget] from files. The [_atlasFile] specifies the `.atlas` file to be loaded for the images used to render
@ -336,7 +340,8 @@ class SpineWidget extends StatefulWidget {
_alignment = alignment ?? Alignment.center,
_boundsProvider = boundsProvider ?? const SetupPoseBounds(),
_sizedByBounds = sizedByBounds ?? false,
_drawable = null;
_drawable = null,
_loadFile = null;
/// 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.
@ -359,6 +364,38 @@ class SpineWidget extends StatefulWidget {
bool? sizedByBounds,
super.key,
}) : _assetType = _AssetType.http,
_bundle = null,
_fit = fit ?? BoxFit.contain,
_alignment = alignment ?? Alignment.center,
_boundsProvider = boundsProvider ?? const SetupPoseBounds(),
_sizedByBounds = sizedByBounds ?? false,
_drawable = null,
_loadFile = null;
/// Constructs a new [SpineWidget] using a custom file loading function.
///
/// This is the most flexible loading method that allows loading skeleton data from any source
/// (memory, custom storage, network with caching, etc.).
///
/// 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.fromMemory(
this._atlasFile,
this._skeletonFile,
this._loadFile,
this._controller, {
BoxFit? fit,
Alignment? alignment,
BoundsProvider? boundsProvider,
bool? sizedByBounds,
super.key,
}) : _assetType = _AssetType.memory,
_bundle = null,
_fit = fit ?? BoxFit.contain,
_alignment = alignment ?? Alignment.center,
@ -391,7 +428,8 @@ class SpineWidget extends StatefulWidget {
_boundsProvider = boundsProvider ?? const SetupPoseBounds(),
_sizedByBounds = sizedByBounds ?? false,
_skeletonFile = null,
_atlasFile = null;
_atlasFile = null,
_loadFile = null;
@override
State<SpineWidget> createState() => _SpineWidgetState();
@ -459,6 +497,9 @@ class _SpineWidgetState extends State<SpineWidget> {
case _AssetType.http:
loadDrawable(await SkeletonDrawableFlutter.fromHttp(atlasFile, skeletonFile));
break;
case _AssetType.memory:
loadDrawable(await SkeletonDrawableFlutter.fromMemory(atlasFile, skeletonFile, widget._loadFile!));
break;
case _AssetType.drawable:
throw Exception("Drawable can not be loaded via loadFromAsset().");
}

View File

@ -1,6 +1,6 @@
name: spine_flutter
description: The official Spine Flutter Runtime to load, display and interact with Spine animations.
version: 4.3.0
version: 4.3.1
homepage: https://esotericsoftware.com
repository: https://github.com/esotericsoftware/spine-runtimes
issue_tracker: https://github.com/esotericsoftware/spine-runtimes/issues