Skip to content

Commit 5658eec

Browse files
committed
[flutter] Added AtlasFlutter.fromMemroy, SkeletonDataFlutter.fromMemory, SkeletonDrawableFlutter.fromMemory, and SpineWidget.fromMemory. See CHANGELOG.md for details and #2939
1 parent 332a001 commit 5658eec

8 files changed

Lines changed: 351 additions & 41 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -351,15 +351,15 @@
351351
- 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.
352352
- **Changes of default values**
353353
- 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.
354-
354+
355355
- **Additions**
356356
- Added Spine Preferences `Switch Texture Workflow` functionality to quickly switch to the respective PMA or straight-alpha texture and material presets.
357357
- 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.
358358
- Added threading support for all skeleton rendering and animation components, disabled by default. Threading can be activated per component or globally via Edit → Preferences → Spine → Threading Defaults. Two threading options are available:
359359
- `Threaded MeshGeneration`: Default value for SkeletonRenderer and SkeletonGraphic threaded mesh generation
360360
- `Threaded Animation`: Default value for SkeletonAnimation and SkeletonMecanim threaded animation updates
361361
- 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.
362-
- 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.
362+
- 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.
363363
- 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. You’ll need to handle that manually, typically with logic such as `if (trackEntry.Animation == attackAnimation) ..`.
364364
- 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.
365365
- Added previously missing editor drag & drop skeleton instantiation option *SkeletonGraphic (UI) Mecanim* combining components `SkeletonGraphic` and `SkeletonMecanim`.
@@ -399,6 +399,10 @@
399399

400400
### Flutter
401401

402+
- **Additions**
403+
- Added `fromMemory` methods to `AtlasFlutter`, `SkeletonDataFlutter`, `SkeletonDrawableFlutter`, and `SpineWidget` for loading Spine data from custom sources (memory, encrypted storage, databases, custom caching, etc.)
404+
- Added example `load_from_memory.dart` demonstrating how to load all assets into memory and use the `fromMemory` API
405+
402406
- **Breaking changes**
403407
- Updated to use the new auto-generated Dart runtime with all the Dart API changes above
404408

spine-flutter/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
# 4.3.1
2+
3+
## Flutter
4+
5+
- **Additions**
6+
- Added `fromMemory` methods to `AtlasFlutter`, `SkeletonDataFlutter`, `SkeletonDrawableFlutter`, and `SpineWidget` for loading Spine data from custom sources (memory, encrypted storage, databases, custom caching, etc.)
7+
- Added example `load_from_memory.dart` demonstrating how to load all assets into memory and use the `fromMemory` API
8+
19
# 4.3.0
210

311
## Dart
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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+
30+
import 'dart:typed_data';
31+
import 'package:spine_flutter/spine_flutter.dart';
32+
import 'package:flutter/material.dart';
33+
import 'package:flutter/services.dart';
34+
35+
/// This example demonstrates loading Spine skeleton data from memory using the
36+
/// fromMemory methods. This is useful when you want to:
37+
/// - Load data from custom storage (e.g., encrypted assets, databases)
38+
/// - Implement custom caching strategies
39+
/// - Download and cache assets at runtime
40+
/// - Pre-process assets before loading
41+
///
42+
/// The example loads all files (atlas, skeleton, images) into memory first,
43+
/// then uses the fromMemory API to create a SpineWidget.
44+
class LoadFromMemory extends StatefulWidget {
45+
const LoadFromMemory({super.key});
46+
47+
@override
48+
State<LoadFromMemory> createState() => _LoadFromMemoryState();
49+
}
50+
51+
class _LoadFromMemoryState extends State<LoadFromMemory> {
52+
// In-memory cache of all loaded files
53+
final Map<String, Uint8List> _fileCache = {};
54+
bool _isLoading = true;
55+
String _loadingStatus = 'Loading assets into memory...';
56+
57+
@override
58+
void initState() {
59+
super.initState();
60+
_loadAssetsIntoMemory();
61+
}
62+
63+
Future<void> _loadAssetsIntoMemory() async {
64+
try {
65+
// Step 1: Load atlas file into memory
66+
setState(() => _loadingStatus = 'Loading atlas file...');
67+
final atlasBytes = await rootBundle.load('assets/spineboy.atlas');
68+
_fileCache['assets/spineboy.atlas'] = atlasBytes.buffer.asUint8List();
69+
70+
// Step 2: Load skeleton file into memory
71+
setState(() => _loadingStatus = 'Loading skeleton file...');
72+
final skelBytes = await rootBundle.load('assets/spineboy-pro.skel');
73+
_fileCache['assets/spineboy-pro.skel'] = skelBytes.buffer.asUint8List();
74+
75+
// Step 3: Load image file(s) into memory
76+
setState(() => _loadingStatus = 'Loading image files...');
77+
final imageBytes = await rootBundle.load('assets/spineboy.png');
78+
_fileCache['assets/spineboy.png'] = imageBytes.buffer.asUint8List();
79+
80+
// All files loaded into memory!
81+
setState(() {
82+
_loadingStatus = 'All assets loaded into memory (${_fileCache.length} files, ${_getTotalSize()} bytes)';
83+
_isLoading = false;
84+
});
85+
} catch (e) {
86+
setState(() {
87+
_loadingStatus = 'Error loading assets: $e';
88+
_isLoading = false;
89+
});
90+
}
91+
}
92+
93+
int _getTotalSize() {
94+
return _fileCache.values.fold(0, (sum, bytes) => sum + bytes.length);
95+
}
96+
97+
// Custom file loader that returns data from our in-memory cache
98+
Future<Uint8List> _loadFromCache(String filename) async {
99+
final data = _fileCache[filename];
100+
if (data == null) {
101+
throw Exception('File not found in cache: $filename');
102+
}
103+
return data;
104+
}
105+
106+
@override
107+
Widget build(BuildContext context) {
108+
return Scaffold(
109+
appBar: AppBar(title: const Text('Load From Memory')),
110+
body: _isLoading
111+
? Center(
112+
child: Column(
113+
mainAxisSize: MainAxisSize.min,
114+
children: [
115+
const CircularProgressIndicator(),
116+
const SizedBox(height: 16),
117+
Text(_loadingStatus),
118+
],
119+
),
120+
)
121+
: Column(
122+
children: [
123+
Container(
124+
padding: const EdgeInsets.all(16),
125+
color: Colors.blue.shade50,
126+
child: Column(
127+
crossAxisAlignment: CrossAxisAlignment.start,
128+
children: [
129+
Text(
130+
'Files in Memory Cache:',
131+
style: Theme.of(context).textTheme.titleMedium,
132+
),
133+
const SizedBox(height: 8),
134+
..._fileCache.entries.map((entry) => Text(
135+
' ${entry.key}: ${entry.value.length} bytes',
136+
style: Theme.of(context).textTheme.bodySmall,
137+
)),
138+
const SizedBox(height: 8),
139+
Text(
140+
'Total: ${_getTotalSize()} bytes',
141+
style: Theme.of(context).textTheme.titleSmall,
142+
),
143+
],
144+
),
145+
),
146+
Expanded(
147+
child: SpineWidget.fromMemory(
148+
'assets/spineboy.atlas',
149+
'assets/spineboy-pro.skel',
150+
_loadFromCache,
151+
SpineWidgetController(
152+
onInitialized: (controller) {
153+
controller.animationState.setAnimation(0, 'walk', true);
154+
},
155+
),
156+
),
157+
),
158+
],
159+
),
160+
);
161+
}
162+
}

spine-flutter/example/lib/main.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import 'animation_state_events.dart';
3535
import 'dress_up.dart';
3636
import 'flame_example.dart';
3737
import 'ik_following.dart';
38+
import 'load_from_memory.dart';
3839
import 'pause_play_animation.dart';
3940
import 'physics.dart';
4041
import 'simple_animation.dart';
@@ -100,6 +101,13 @@ class ExampleSelector extends StatelessWidget {
100101
},
101102
),
102103
spacer,
104+
ElevatedButton(
105+
child: const Text('Load From Memory'),
106+
onPressed: () {
107+
Navigator.push(context, MaterialPageRoute<void>(builder: (context) => const LoadFromMemory()));
108+
},
109+
),
110+
spacer,
103111
ElevatedButton(
104112
child: const Text('Flame: Simple Example'),
105113
onPressed: () {

spine-flutter/example/macos/Flutter/GeneratedPluginRegistrant.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55
import FlutterMacOS
66
import Foundation
77

8+
89
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
910
}

0 commit comments

Comments
 (0)