[flutter] macOS/iOS builds, fixed examples wrt new API

This commit is contained in:
Mario Zechner 2025-07-30 09:44:13 +02:00
parent 83f5bd0e0a
commit 4225214caf
24 changed files with 1252 additions and 5383 deletions

View File

@ -24,7 +24,7 @@ class AnimationStateEvents extends StatelessWidget {
controller.animationState.setListener((type, trackEntry, event) { controller.animationState.setListener((type, trackEntry, event) {
if (type == EventType.event) { if (type == EventType.event) {
print( print(
"User event: { name: ${event?.getData().getName()}, intValue: ${event?.getIntValue()}, floatValue: ${event?.getFloatValue()}, stringValue: ${event?.getStringValue()} }", "User event: { name: ${event?.data.name}, intValue: ${event?.intValue}, floatValue: ${event?.floatValue}, stringValue: ${event?.stringValue} }",
); );
} }
}); });

View File

@ -40,7 +40,7 @@ class DebugRendering extends StatelessWidget {
const debugRenderer = DebugRenderer(); const debugRenderer = DebugRenderer();
final controller = SpineWidgetController( final controller = SpineWidgetController(
onInitialized: (controller) { onInitialized: (controller) {
controller.animationState.setAnimationByName(0, "walk", true); controller.animationState.setAnimation(0, "walk", true);
}, },
onBeforePaint: (controller, canvas) { onBeforePaint: (controller, canvas) {
// Save the current transform and other canvas state // Save the current transform and other canvas state

View File

@ -41,7 +41,7 @@ class DressUp extends StatefulWidget {
class DressUpState extends State<DressUp> { class DressUpState extends State<DressUp> {
static const double thumbnailSize = 200; static const double thumbnailSize = 200;
late SkeletonDrawable _drawable; late SkeletonDrawableFlutter _drawable;
Skin? _customSkin; Skin? _customSkin;
final Map<String, RawImageData> _skinImages = {}; final Map<String, RawImageData> _skinImages = {};
final Map<String, bool> _selectedSkins = {}; final Map<String, bool> _selectedSkins = {};
@ -50,17 +50,19 @@ class DressUpState extends State<DressUp> {
void initState() { void initState() {
reportLeaks(); reportLeaks();
super.initState(); super.initState();
SkeletonDrawable.fromAsset("assets/mix-and-match.atlas", "assets/mix-and-match-pro.skel").then((drawable) async { SkeletonDrawableFlutter.fromAsset("assets/mix-and-match.atlas", "assets/mix-and-match-pro.skel")
.then((drawable) async {
_drawable = drawable; _drawable = drawable;
for (var skin in drawable.skeletonData.getSkins()) { for (var skin in drawable.skeletonData.skins) {
if (skin.getName() == "default") continue; if (skin == null) continue;
if (skin.name == "default") continue;
var skeleton = drawable.skeleton; var skeleton = drawable.skeleton;
skeleton.setSkin(skin); skeleton.setSkin2(skin);
skeleton.setToSetupPose(); skeleton.setupPose();
skeleton.update(0); skeleton.update(0);
skeleton.updateWorldTransform(Physics.update); skeleton.updateWorldTransform(Physics.update);
_skinImages[skin.getName()] = await drawable.renderToRawImageData(thumbnailSize, thumbnailSize, 0xffffffff); _skinImages[skin.name] = await drawable.renderToRawImageData(thumbnailSize, thumbnailSize, 0xffffffff);
_selectedSkins[skin.getName()] = false; _selectedSkins[skin.name] = false;
} }
_toggleSkin("full-skins/girl"); _toggleSkin("full-skins/girl");
setState(() {}); setState(() {});
@ -69,7 +71,7 @@ class DressUpState extends State<DressUp> {
void _toggleSkin(String skinName) { void _toggleSkin(String skinName) {
_selectedSkins[skinName] = !_selectedSkins[skinName]!; _selectedSkins[skinName] = !_selectedSkins[skinName]!;
_drawable.skeleton.setSkinByName("default"); _drawable.skeleton.setSkin("default");
if (_customSkin != null) _customSkin?.dispose(); if (_customSkin != null) _customSkin?.dispose();
_customSkin = Skin("custom-skin"); _customSkin = Skin("custom-skin");
for (var skinName in _selectedSkins.keys) { for (var skinName in _selectedSkins.keys) {
@ -78,15 +80,15 @@ class DressUpState extends State<DressUp> {
if (skin != null) _customSkin?.addSkin(skin); if (skin != null) _customSkin?.addSkin(skin);
} }
} }
_drawable.skeleton.setSkin(_customSkin!); _drawable.skeleton.setSkin2(_customSkin!);
_drawable.skeleton.setSlotsToSetupPose(); _drawable.skeleton.setupPoseSlots();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final controller = SpineWidgetController( final controller = SpineWidgetController(
onInitialized: (controller) { onInitialized: (controller) {
controller.animationState.setAnimationByName(0, "dance", true); controller.animationState.setAnimation(0, "dance", true);
}, },
); );

View File

@ -36,7 +36,7 @@ import 'package:flutter/material.dart';
class SpineComponent extends PositionComponent { class SpineComponent extends PositionComponent {
final BoundsProvider _boundsProvider; final BoundsProvider _boundsProvider;
final SkeletonDrawable _drawable; final SkeletonDrawableFlutter _drawable;
late final Bounds _bounds; late final Bounds _bounds;
final bool _ownsDrawable; final bool _ownsDrawable;
@ -70,7 +70,7 @@ class SpineComponent extends PositionComponent {
int? priority, int? priority,
}) async { }) async {
return SpineComponent( return SpineComponent(
await SkeletonDrawable.fromAsset(atlasFile, skeletonFile, bundle: bundle), await SkeletonDrawableFlutter.fromAsset(atlasFile, skeletonFile, bundle: bundle),
ownsDrawable: true, ownsDrawable: true,
boundsProvider: boundsProvider, boundsProvider: boundsProvider,
position: position, position: position,
@ -137,15 +137,15 @@ class SimpleFlameExample extends FlameGame {
} }
class DragonExample extends FlameGame { class DragonExample extends FlameGame {
late final Atlas cachedAtlas; late final AtlasFlutter cachedAtlas;
late final SkeletonData cachedSkeletonData; late final SkeletonData cachedSkeletonData;
late final SpineComponent dragon; late final SpineComponent dragon;
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
cachedAtlas = await Atlas.fromAsset("assets/dragon.atlas"); cachedAtlas = await AtlasFlutter.fromAsset("assets/dragon.atlas");
cachedSkeletonData = await SkeletonData.fromAsset(cachedAtlas, "assets/dragon-ess.skel"); cachedSkeletonData = await SkeletonDataFlutter.fromAsset(cachedAtlas, "assets/dragon-ess.skel");
final drawable = SkeletonDrawable(cachedAtlas, cachedSkeletonData, false); final drawable = SkeletonDrawableFlutter(cachedAtlas, cachedSkeletonData, false);
dragon = SpineComponent( dragon = SpineComponent(
drawable, drawable,
scale: Vector2(0.4, 0.4), scale: Vector2(0.4, 0.4),
@ -168,21 +168,21 @@ class DragonExample extends FlameGame {
class PreloadAndShareSpineDataExample extends FlameGame { class PreloadAndShareSpineDataExample extends FlameGame {
late final SkeletonData cachedSkeletonData; late final SkeletonData cachedSkeletonData;
late final Atlas cachedAtlas; late final AtlasFlutter cachedAtlas;
late final List<SpineComponent> spineboys = []; late final List<SpineComponent> spineboys = [];
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
// Pre-load the atlas and skeleton data once. // Pre-load the atlas and skeleton data once.
cachedAtlas = await Atlas.fromAsset("assets/spineboy.atlas"); cachedAtlas = await AtlasFlutter.fromAsset("assets/spineboy.atlas");
cachedSkeletonData = await SkeletonData.fromAsset(cachedAtlas, "assets/spineboy-pro.skel"); cachedSkeletonData = await SkeletonDataFlutter.fromAsset(cachedAtlas, "assets/spineboy-pro.skel");
// Instantiate many spineboys from the pre-loaded data. Each SpineComponent // Instantiate many spineboys from the pre-loaded data. Each SpineComponent
// gets their own SkeletonDrawable copy derived from the cached data. The // gets their own SkeletonDrawable copy derived from the cached data. The
// SkeletonDrawable copies do not own the underlying skeleton data and atlas. // SkeletonDrawable copies do not own the underlying skeleton data and atlas.
final rng = Random(); final rng = Random();
for (int i = 0; i < 100; i++) { for (int i = 0; i < 100; i++) {
final drawable = SkeletonDrawable(cachedAtlas, cachedSkeletonData, false); final drawable = SkeletonDrawableFlutter(cachedAtlas, cachedSkeletonData, false);
final scale = 0.1 + rng.nextDouble() * 0.2; final scale = 0.1 + rng.nextDouble() * 0.2;
final position = Vector2(rng.nextDouble() * size.x, rng.nextDouble() * size.y); final position = Vector2(rng.nextDouble() * size.x, rng.nextDouble() * size.y);
final spineboy = SpineComponent(drawable, scale: Vector2(scale, scale), position: position); final spineboy = SpineComponent(drawable, scale: Vector2(scale, scale), position: position);

View File

@ -48,17 +48,19 @@ class IkFollowingState extends State<IkFollowing> {
controller = SpineWidgetController( controller = SpineWidgetController(
onInitialized: (controller) { onInitialized: (controller) {
// Set the walk animation on track 0, let it loop // Set the walk animation on track 0, let it loop
controller.animationState.setAnimationByName(0, "walk", true); controller.animationState.setAnimation(0, "walk", true);
controller.animationState.setAnimationByName(1, "aim", true); controller.animationState.setAnimation(1, "aim", true);
}, },
onAfterUpdateWorldTransforms: (controller) { onAfterUpdateWorldTransforms: (controller) {
final worldPosition = crossHairPosition; final worldPosition = crossHairPosition;
if (worldPosition == null) return; if (worldPosition == null) return;
final bone = controller.skeleton.findBone("crosshair")!; final bone = controller.skeleton.findBone("crosshair")!;
final parent = bone.getParent()!; final parent = bone.parent;
final position = parent.worldToLocal(worldPosition.dx, worldPosition.dy); if (parent != null) {
bone.setX(position.x); final position = parent.appliedPose.worldToLocal(worldPosition.dx, worldPosition.dy);
bone.setY(position.y); bone.appliedPose.x = position.x;
bone.appliedPose.y = position.y;
}
}, },
); );
} }

View File

@ -31,7 +31,7 @@ import 'package:spine_flutter/spine_flutter.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class PlayPauseAnimation extends StatefulWidget { class PlayPauseAnimation extends StatefulWidget {
const PlayPauseAnimation({Key? key}) : super(key: key); const PlayPauseAnimation({super.key});
@override @override
PlayPauseAnimationState createState() => PlayPauseAnimationState(); PlayPauseAnimationState createState() => PlayPauseAnimationState();
@ -45,7 +45,7 @@ class PlayPauseAnimationState extends State<PlayPauseAnimation> {
super.initState(); super.initState();
controller = SpineWidgetController( controller = SpineWidgetController(
onInitialized: (controller) { onInitialized: (controller) {
controller.animationState.setAnimationByName(0, "flying", true); controller.animationState.setAnimation(0, "flying", true);
}, },
); );
} }

View File

@ -48,8 +48,8 @@ class PhysicsState extends State<PhysicsTest> {
controller = SpineWidgetController( controller = SpineWidgetController(
onInitialized: (controller) { onInitialized: (controller) {
controller.animationState.setAnimationByName(0, "eyeblink-long", true); controller.animationState.setAnimation(0, "eyeblink-long", true);
controller.animationState.setAnimationByName(1, "wings-and-feet", true); controller.animationState.setAnimation(1, "wings-and-feet", true);
}, },
onAfterUpdateWorldTransforms: (controller) { onAfterUpdateWorldTransforms: (controller) {
if (lastMousePosition == null) { if (lastMousePosition == null) {

View File

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

View File

@ -15,8 +15,8 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS: SPEC CHECKSUMS:
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
spine_flutter: 75f9d54a630ac150d238210f9c211529c37c11ba spine_flutter: 8469a2cfb87c5a7101a7c87dc7c14ee49699ea3b
PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7 PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7
COCOAPODS: 1.15.2 COCOAPODS: 1.16.2

View File

@ -1,7 +1,7 @@
import Cocoa import Cocoa
import FlutterMacOS import FlutterMacOS
@NSApplicationMain @main
class AppDelegate: FlutterAppDelegate { class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true return true

View File

@ -21,6 +21,16 @@ if [ ! -d "codegen/node_modules" ]; then
fi fi
fi fi
# Generating spine-c bindings
log_action "Generating spine-c bindings"
if LOG=$(cd ../spine-c && ./build.sh codegen 2>&1); then
log_ok
else
log_fail
log_error_output "$LOG"
exit 1
fi
# Copy spine-c and spine-cpp sources # Copy spine-c and spine-cpp sources
log_action "Setting up source files" log_action "Setting up source files"
if ./setup.sh > /dev/null 2>&1; then if ./setup.sh > /dev/null 2>&1; then
@ -32,19 +42,21 @@ fi
# Run the codegen # Run the codegen
log_action "Generating Dart bindings" log_action "Generating Dart bindings"
if npx tsx codegen/src/index.ts > /dev/null 2>&1; then if LOG=$(npx tsx codegen/src/index.ts 2>&1); then
log_ok log_ok
else else
log_fail log_fail
log_error_output "$LOG"
exit 1 exit 1
fi fi
# Build test spine_flutter shared library # Build test spine_flutter shared library
log_action "Building test library" log_action "Building test library"
if (cd test && ./build.sh > /dev/null 2>&1); then if LOG=$(cd test && ./build.sh 2>&1); then
log_ok log_ok
else else
log_fail log_fail
log_error_output "$LOG"
exit 1 exit 1
fi fi

View File

@ -1,3 +0,0 @@
// Relative import to be able to reuse the C sources.
// See the comment in ../{projectName}}.podspec for more information.
#include "../../src/spine-cpp-lite/spine-cpp-lite.cpp"

View File

@ -12,14 +12,9 @@ A new Flutter FFI plugin project.
s.homepage = 'http://example.com' s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' } s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' } s.author = { 'Your Company' => 'email@example.com' }
# This will ensure the source files in Classes/ are included in the native
# builds of apps using this FFI plugin. Podspec does not support relative
# paths, so Classes contains a forwarder C file that relatively imports
# `../src/*` so that the C sources can be shared among all target platforms.
s.source = { :path => '.' } s.source = { :path => '.' }
s.source_files = 'Classes/**/*.{cpp}' s.source_files = 'Classes/**/*.{cpp}'
s.xcconfig = { 'HEADER_SEARCH_PATHS' => '"$(PODS_TARGET_SRCROOT)/Classes/spine-cpp/include"' } s.xcconfig = { 'HEADER_SEARCH_PATHS' => '"$(PODS_TARGET_SRCROOT)/Classes/spine-cpp/include" "$(PODS_TARGET_SRCROOT)/Classes/spine-c/include"' }
s.dependency 'Flutter' s.dependency 'Flutter'
s.platform = :ios, '9.0' s.platform = :ios, '9.0'

File diff suppressed because it is too large Load Diff

View File

@ -80,10 +80,10 @@ class Animation {
SpineBindings.bindings.spine_animation_set_duration(_ptr, value); SpineBindings.bindings.spine_animation_set_duration(_ptr, value);
} }
void apply(Skeleton skeleton, double lastTime, double time, bool loop, ArrayEvent? pEvents, double alpha, void apply(Skeleton skeleton, double lastTime, double time, bool loop, ArrayEvent? events, double alpha,
MixBlend blend, MixDirection direction, bool appliedPose) { MixBlend blend, MixDirection direction, bool appliedPose) {
SpineBindings.bindings.spine_animation_apply(_ptr, skeleton.nativePtr.cast(), lastTime, time, loop, SpineBindings.bindings.spine_animation_apply(_ptr, skeleton.nativePtr.cast(), lastTime, time, loop,
pEvents?.nativePtr.cast() ?? Pointer.fromAddress(0), alpha, blend.value, direction.value, appliedPose); events?.nativePtr.cast() ?? Pointer.fromAddress(0), alpha, blend.value, direction.value, appliedPose);
} }
String get name { String get name {

File diff suppressed because it is too large Load Diff

View File

@ -52,10 +52,10 @@ abstract class Timeline {
return Rtti.fromPointer(result); return Rtti.fromPointer(result);
} }
void apply(Skeleton skeleton, double lastTime, double time, ArrayEvent? pEvents, double alpha, MixBlend blend, void apply(Skeleton skeleton, double lastTime, double time, ArrayEvent? events, double alpha, MixBlend blend,
MixDirection direction, bool appliedPose) { MixDirection direction, bool appliedPose) {
SpineBindings.bindings.spine_timeline_apply(_ptr, skeleton.nativePtr.cast(), lastTime, time, SpineBindings.bindings.spine_timeline_apply(_ptr, skeleton.nativePtr.cast(), lastTime, time,
pEvents?.nativePtr.cast() ?? Pointer.fromAddress(0), alpha, blend.value, direction.value, appliedPose); events?.nativePtr.cast() ?? Pointer.fromAddress(0), alpha, blend.value, direction.value, appliedPose);
} }
int get frameEntries { int get frameEntries {

View File

@ -50,6 +50,7 @@ import 'generated/event.dart';
import 'generated/event_type.dart'; import 'generated/event_type.dart';
import 'generated/render_command.dart'; import 'generated/render_command.dart';
import 'generated/physics.dart'; import 'generated/physics.dart';
import 'generated/bone_pose.dart';
// Export generated classes // Export generated classes
export 'generated/api.dart'; export 'generated/api.dart';
@ -321,20 +322,24 @@ extension TrackEntryExtensions on TrackEntry {
/// Represents a bounding box with position and dimensions /// Represents a bounding box with position and dimensions
class Bounds { class Bounds {
final double x; double x;
final double y; double y;
final double width; double width;
final double height; double height;
const Bounds({ Bounds({
required this.x, required this.x,
required this.y, required this.y,
required this.width, required this.width,
required this.height, required this.height,
}); });
}
@override class Vector {
String toString() => 'Bounds(x: $x, y: $y, width: $width, height: $height)'; double x;
double y;
Vector({required this.x, required this.y});
} }
/// Extension to add bounds property to Skeleton /// Extension to add bounds property to Skeleton
@ -349,6 +354,33 @@ extension SkeletonExtensions on Skeleton {
height: spineBounds.height, height: spineBounds.height,
); );
} }
Vector getPosition() {
final position = SpineBindings.bindings.spine_skeleton_get_position_v(nativePtr.cast());
return Vector(x: position.x, y: position.y);
}
}
extension BonePoseExtensions on BonePose {
Vector worldToLocal(double worldX, double worldY) {
final result = SpineBindings.bindings.spine_bone_pose_world_to_local_v(nativePtr.cast(), worldX, worldY);
return Vector(x: result.x, y: result.y);
}
Vector localToWorld(double localX, double localY) {
final result = SpineBindings.bindings.spine_bone_pose_local_to_world_v(nativePtr.cast(), localX, localY);
return Vector(x: result.x, y: result.y);
}
Vector worldToParent(double worldX, double worldY) {
final result = SpineBindings.bindings.spine_bone_pose_world_to_parent_v(nativePtr.cast(), worldX, worldY);
return Vector(x: result.x, y: result.y);
}
Vector parentToWorld(double parentX, double parentY) {
final result = SpineBindings.bindings.spine_bone_pose_parent_to_world_v(nativePtr.cast(), parentX, parentY);
return Vector(x: result.x, y: result.y);
}
} }
/// Convenient drawable that combines skeleton, animation state, and rendering /// Convenient drawable that combines skeleton, animation state, and rendering

View File

@ -24,14 +24,13 @@ Future<void> initSpineFlutter({bool useStaticLinkage = false, bool enableMemoryD
} }
/// Flutter wrapper for Atlas that manages texture loading and Paint creation /// Flutter wrapper for Atlas that manages texture loading and Paint creation
class AtlasFlutter { class AtlasFlutter extends Atlas {
static FilterQuality filterQuality = FilterQuality.low; static FilterQuality filterQuality = FilterQuality.low;
final Atlas atlas;
final List<Image> atlasPages; final List<Image> atlasPages;
final List<Map<BlendMode, Paint>> atlasPagePaints; final List<Map<BlendMode, Paint>> atlasPagePaints;
bool _disposed = false; bool _disposed = false;
AtlasFlutter._(this.atlas, this.atlasPages, this.atlasPagePaints); AtlasFlutter._(super.ptr, this.atlasPages, this.atlasPagePaints) : super.fromPointer();
/// Internal method to load atlas and images /// Internal method to load atlas and images
static Future<AtlasFlutter> _load(String atlasFileName, Future<Uint8List> Function(String name) loadFile) async { static Future<AtlasFlutter> _load(String atlasFileName, Future<Uint8List> Function(String name) loadFile) async {
@ -77,7 +76,7 @@ class AtlasFlutter {
paints.add(pagePaints); paints.add(pagePaints);
} }
return AtlasFlutter._(atlas, pages, paints); return AtlasFlutter._(atlas.nativePtr.cast(), pages, paints);
} }
/// Loads an [AtlasFlutter] from the file [atlasFileName] in the root bundle or the optionally provided [bundle]. /// Loads an [AtlasFlutter] from the file [atlasFileName] in the root bundle or the optionally provided [bundle].
@ -103,10 +102,11 @@ class AtlasFlutter {
} }
/// Disposes all resources including the native atlas and images /// Disposes all resources including the native atlas and images
@override
void dispose() { void dispose() {
if (_disposed) return; if (_disposed) return;
_disposed = true; _disposed = true;
atlas.dispose(); super.dispose();
for (final image in atlasPages) { for (final image in atlasPages) {
image.dispose(); image.dispose();
} }
@ -114,6 +114,64 @@ class AtlasFlutter {
} }
} }
/// Flutter wrapper for SkeletonData that provides convenient loading methods
class SkeletonDataFlutter extends SkeletonData {
SkeletonDataFlutter._(super.ptr) : super.fromPointer();
/// 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());
}
}
/// Loads a [SkeletonDataFlutter] from the file [skeletonFile]. Uses the provided [atlasFlutter] to resolve attachment images.
///
/// Throws an [Exception] in case the skeleton data could not be loaded.
static Future<SkeletonDataFlutter> fromFile(AtlasFlutter atlasFlutter, String skeletonFile) async {
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());
}
}
/// 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));
if (response.statusCode != 200) {
throw Exception('Failed to load skeleton from $skeletonURL: ${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());
}
}
}
/// Extension to convert Spine BlendMode to Flutter BlendMode /// Extension to convert Spine BlendMode to Flutter BlendMode
extension BlendModeExtensions on BlendMode { extension BlendModeExtensions on BlendMode {
rendering.BlendMode toFlutterBlendMode() { rendering.BlendMode toFlutterBlendMode() {
@ -184,7 +242,6 @@ class RenderCommandFlutter {
// See https://github.com/flutter/flutter/issues/127486 // See https://github.com/flutter/flutter/issues/127486
// //
// We thus batch all meshes not only by atlas page and blend mode, but also vertex color. // We thus batch all meshes not only by atlas page and blend mode, but also vertex color.
// See spine_flutter.cpp, batch_commands().
// //
// If the vertex color equals (1, 1, 1, 1), we do not store // If the vertex color equals (1, 1, 1, 1), we do not store
// colors, which will trigger the fast path in Impeller. Otherwise we have to go the slow path, which // colors, which will trigger the fast path in Impeller. Otherwise we have to go the slow path, which
@ -218,13 +275,13 @@ class RenderCommandFlutter {
} }
} }
/// A SkeletonDrawable bundles loading, updating, and rendering an [Atlas], [Skeleton], and [AnimationState] /// A SkeletonDrawable bundles loading, updating, and rendering an [AtlasFlutter], [Skeleton], and [AnimationState]
/// into a single easy to use class. /// into a single easy to use class.
/// ///
/// Use the [fromAsset], [fromFile], or [fromHttp] methods to construct a SkeletonDrawable. To have /// Use the [fromAsset], [fromFile], or [fromHttp] methods to construct a SkeletonDrawable. To have
/// multiple skeleton drawable instances share the same [Atlas] and [SkeletonData], use the constructor. /// multiple skeleton drawable instances share the same [AtlasFlutter] and [SkeletonDataFlutter], use the constructor.
/// ///
/// You can then directly access the [atlas], [skeletonData], [skeleton], [animationStateData], and [animationState] /// You can then directly access the [atlasFlutter], [skeletonDataFlutter], [skeleton], [animationStateData], and [animationState]
/// to query and animate the skeleton. Use the [AnimationState] to queue animations on one or more tracks /// to query and animate the skeleton. Use the [AnimationState] to queue animations on one or more tracks
/// via [AnimationState.setAnimation] or [AnimationState.addAnimation]. /// via [AnimationState.setAnimation] or [AnimationState.addAnimation].
/// ///
@ -235,27 +292,18 @@ class RenderCommandFlutter {
/// [renderToPng], or [renderToRawImageData], depending on your needs. /// [renderToPng], or [renderToRawImageData], depending on your needs.
/// ///
/// When the skeleton drawable is no longer needed, call the [dispose] method to release its resources. If /// When the skeleton drawable is no longer needed, call the [dispose] method to release its resources. If
/// the skeleton drawable was constructed from a shared [Atlas] and [SkeletonData], make sure to dispose the /// the skeleton drawable was constructed from a shared [AtlasFlutter] and [SkeletonDataFlutter], make sure to dispose the
/// atlas and skeleton data as well, if no skeleton drawable references them anymore. /// atlas and skeleton data as well, if no skeleton drawable references them anymore.
class SkeletonDrawableFlutter { class SkeletonDrawableFlutter extends SkeletonDrawable {
final AtlasFlutter atlasFlutter; final AtlasFlutter atlasFlutter;
final SkeletonData skeletonData; final SkeletonData skeletonData;
late final SkeletonDrawable _drawable;
late final Skeleton skeleton;
late final AnimationStateData animationStateData;
late final AnimationState animationState;
final bool _ownsAtlasAndSkeletonData; final bool _ownsAtlasAndSkeletonData;
bool _disposed = false; bool _disposed = false;
/// Constructs a new skeleton drawable from the given (possibly shared) [Atlas] and [SkeletonData]. If /// Constructs a new skeleton drawable from the given (possibly shared) [AtlasFlutter] and [SkeletonDataFlutter]. If
/// the atlas and skeleton data are not shared, the drawable can take ownership by passing true for [_ownsAtlasAndSkeletonData]. /// the atlas and skeleton data are not shared, the drawable can take ownership by passing true for [_ownsAtlasAndSkeletonData].
/// In that case a call to [dispose] will also dispose the atlas and skeleton data. /// In that case a call to [dispose] will also dispose the atlas and skeleton data.
SkeletonDrawableFlutter(this.atlasFlutter, this.skeletonData, this._ownsAtlasAndSkeletonData) { SkeletonDrawableFlutter(this.atlasFlutter, this.skeletonData, this._ownsAtlasAndSkeletonData) : super(skeletonData);
_drawable = SkeletonDrawable(skeletonData);
skeleton = _drawable.skeleton;
animationStateData = _drawable.animationStateData;
animationState = _drawable.animationState;
}
/// Constructs a new skeleton drawable from the [atlasFile] and [skeletonFile] from the root asset bundle /// Constructs a new skeleton drawable from the [atlasFile] and [skeletonFile] from the root asset bundle
/// or the optionally provided [bundle]. /// or the optionally provided [bundle].
@ -264,13 +312,8 @@ class SkeletonDrawableFlutter {
static Future<SkeletonDrawableFlutter> fromAsset(String atlasFile, String skeletonFile, {AssetBundle? bundle}) async { static Future<SkeletonDrawableFlutter> fromAsset(String atlasFile, String skeletonFile, {AssetBundle? bundle}) async {
bundle ??= rootBundle; bundle ??= rootBundle;
final atlasFlutter = await AtlasFlutter.fromAsset(atlasFile, bundle: bundle); final atlasFlutter = await AtlasFlutter.fromAsset(atlasFile, bundle: bundle);
final skeletonDataFlutter = await SkeletonDataFlutter.fromAsset(atlasFlutter, skeletonFile, bundle: bundle);
final skeletonData = await bundle.loadString(skeletonFile); return SkeletonDrawableFlutter(atlasFlutter, skeletonDataFlutter, true);
final skeleton = skeletonFile.endsWith('.json')
? loadSkeletonDataJson(atlasFlutter.atlas, skeletonData)
: throw Exception('Binary skeleton data loading from assets not yet implemented');
return SkeletonDrawableFlutter(atlasFlutter, skeleton, true);
} }
/// Constructs a new skeleton drawable from the [atlasFile] and [skeletonFile]. /// Constructs a new skeleton drawable from the [atlasFile] and [skeletonFile].
@ -278,17 +321,8 @@ class SkeletonDrawableFlutter {
/// Throws an exception in case the data could not be loaded. /// Throws an exception in case the data could not be loaded.
static Future<SkeletonDrawableFlutter> fromFile(String atlasFile, String skeletonFile) async { static Future<SkeletonDrawableFlutter> fromFile(String atlasFile, String skeletonFile) async {
final atlasFlutter = await AtlasFlutter.fromFile(atlasFile); final atlasFlutter = await AtlasFlutter.fromFile(atlasFile);
final skeletonDataFlutter = await SkeletonDataFlutter.fromFile(atlasFlutter, skeletonFile);
final SkeletonData skeleton; return SkeletonDrawableFlutter(atlasFlutter, skeletonDataFlutter, true);
if (skeletonFile.endsWith('.json')) {
final skeletonData = await File(skeletonFile).readAsString();
skeleton = loadSkeletonDataJson(atlasFlutter.atlas, skeletonData);
} else {
final skeletonData = await File(skeletonFile).readAsBytes();
skeleton = loadSkeletonDataBinary(atlasFlutter.atlas, skeletonData);
}
return SkeletonDrawableFlutter(atlasFlutter, skeleton, true);
} }
/// Constructs a new skeleton drawable from the [atlasUrl] and [skeletonUrl]. /// Constructs a new skeleton drawable from the [atlasUrl] and [skeletonUrl].
@ -296,45 +330,22 @@ class SkeletonDrawableFlutter {
/// Throws an exception in case the data could not be loaded. /// Throws an exception in case the data could not be loaded.
static Future<SkeletonDrawableFlutter> fromHttp(String atlasUrl, String skeletonUrl) async { static Future<SkeletonDrawableFlutter> fromHttp(String atlasUrl, String skeletonUrl) async {
final atlasFlutter = await AtlasFlutter.fromHttp(atlasUrl); final atlasFlutter = await AtlasFlutter.fromHttp(atlasUrl);
final skeletonDataFlutter = await SkeletonDataFlutter.fromHttp(atlasFlutter, skeletonUrl);
final SkeletonData skeleton; return SkeletonDrawableFlutter(atlasFlutter, skeletonDataFlutter, true);
if (skeletonUrl.endsWith('.json')) {
final skeletonResponse = await http.get(Uri.parse(skeletonUrl));
if (skeletonResponse.statusCode != 200) {
throw Exception('Failed to load skeleton from $skeletonUrl: ${skeletonResponse.statusCode}');
}
skeleton = loadSkeletonDataJson(atlasFlutter.atlas, skeletonResponse.body);
} else {
final skeletonResponse = await http.get(Uri.parse(skeletonUrl));
if (skeletonResponse.statusCode != 200) {
throw Exception('Failed to load skeleton from $skeletonUrl: ${skeletonResponse.statusCode}');
}
skeleton = loadSkeletonDataBinary(atlasFlutter.atlas, skeletonResponse.bodyBytes);
}
return SkeletonDrawableFlutter(atlasFlutter, skeleton, true);
}
/// Updates the [AnimationState] using the [delta] time given in seconds, applies the
/// animation state to the [Skeleton] and updates the world transforms of the skeleton
/// to calculate its current pose.
void update(double delta) {
if (_disposed) return;
_drawable.update(delta);
} }
/// Renders to current skeleton pose to a list of [RenderCommandFlutter] instances. The render commands /// Renders to current skeleton pose to a list of [RenderCommandFlutter] instances. The render commands
/// can be rendered via [Canvas.drawVertices]. /// can be rendered via [Canvas.drawVertices].
List<RenderCommandFlutter> render() { List<RenderCommandFlutter> renderFlutter() {
if (_disposed) return []; if (_disposed) return [];
var commands = <RenderCommandFlutter>[]; var commands = <RenderCommandFlutter>[];
var nativeCmd = _drawable.render(); var nativeCmd = render();
while (nativeCmd != null) { while (nativeCmd != null) {
// Get page dimensions from atlas // Get page dimensions from atlas
final pageIndex = nativeCmd.texture?.address ?? 0; final pageIndex = nativeCmd.texture?.address ?? 0;
final pages = atlasFlutter.atlas.pages; final pages = atlasFlutter.pages;
final page = pages[pageIndex]; final page = pages[pageIndex];
if (page != null) { if (page != null) {
commands.add(RenderCommandFlutter._(nativeCmd, page.width.toDouble(), page.height.toDouble())); commands.add(RenderCommandFlutter._(nativeCmd, page.width.toDouble(), page.height.toDouble()));
@ -350,7 +361,7 @@ class SkeletonDrawableFlutter {
/// Renders the skeleton drawable's current pose to the given [canvas]. Does not perform any /// Renders the skeleton drawable's current pose to the given [canvas]. Does not perform any
/// scaling or fitting. /// scaling or fitting.
List<RenderCommandFlutter> renderToCanvas(Canvas canvas) { List<RenderCommandFlutter> renderToCanvas(Canvas canvas) {
var commands = render(); var commands = renderFlutter();
for (final cmd in commands) { for (final cmd in commands) {
// Get the paint for this atlas page and blend mode // Get the paint for this atlas page and blend mode
@ -425,10 +436,11 @@ class SkeletonDrawableFlutter {
/// Disposes the skeleton drawable's resources. If the skeleton drawable owns the atlas /// Disposes the skeleton drawable's resources. If the skeleton drawable owns the atlas
/// and skeleton data, they are disposed as well. Must be called when the skeleton drawable /// and skeleton data, they are disposed as well. Must be called when the skeleton drawable
/// is no longer in use. /// is no longer in use.
@override
void dispose() { void dispose() {
if (_disposed) return; if (_disposed) return;
_disposed = true; _disposed = true;
_drawable.dispose(); super.dispose();
if (_ownsAtlasAndSkeletonData) { if (_ownsAtlasAndSkeletonData) {
atlasFlutter.dispose(); atlasFlutter.dispose();

View File

@ -26,6 +26,7 @@
/// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF /// (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. /// THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
/// ///
library;
import 'dart:math'; import 'dart:math';
@ -34,7 +35,7 @@ import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'spine_dart.dart'; import 'spine_flutter.dart';
/// Controls how the skeleton of a [SpineWidget] is animated and rendered. /// Controls how the skeleton of a [SpineWidget] is animated and rendered.
/// ///
@ -54,7 +55,7 @@ import 'spine_dart.dart';
/// [SpineWidget] then renderes the skeleton's current pose, and finally calls the optional [onAfterPaint], which /// [SpineWidget] then renderes the skeleton's current pose, and finally calls the optional [onAfterPaint], which
/// can render additional objects on top of the skeleton. /// can render additional objects on top of the skeleton.
/// ///
/// The underlying [Atlas], [SkeletonData], [Skeleton], [AnimationStateData], [AnimationState], and [SkeletonDrawable] /// The underlying [AtlasFlutter], [SkeletonData], [Skeleton], [AnimationStateData], [AnimationState], and [SkeletonDrawableFlutter]
/// can be accessed through their respective getters to inspect and/or modify the skeleton and its associated data. Accessing /// 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. /// this data is only allowed if the [SpineWidget] and its data have been initialized and have not been disposed yet.
/// ///
@ -62,7 +63,7 @@ import 'spine_dart.dart';
/// and rendering the skeleton. The [resume] method resumes updating and rendering the skeleton. The [isPlaying] getter /// and rendering the skeleton. The [resume] method resumes updating and rendering the skeleton. The [isPlaying] getter
/// reports the current state. /// reports the current state.
class SpineWidgetController { class SpineWidgetController {
SkeletonDrawable? _drawable; SkeletonDrawableFlutter? _drawable;
double _offsetX = 0, _offsetY = 0, _scaleX = 1, _scaleY = 1; double _offsetX = 0, _offsetY = 0, _scaleX = 1, _scaleY = 1;
bool _isPlaying = true; bool _isPlaying = true;
_SpineRenderObject? _renderObject; _SpineRenderObject? _renderObject;
@ -70,7 +71,8 @@ class SpineWidgetController {
final void Function(SpineWidgetController controller)? onBeforeUpdateWorldTransforms; final void Function(SpineWidgetController controller)? onBeforeUpdateWorldTransforms;
final void Function(SpineWidgetController controller)? onAfterUpdateWorldTransforms; final void Function(SpineWidgetController controller)? onAfterUpdateWorldTransforms;
final void Function(SpineWidgetController controller, Canvas canvas)? onBeforePaint; final void Function(SpineWidgetController controller, Canvas canvas)? onBeforePaint;
final void Function(SpineWidgetController controller, Canvas canvas, List<RenderCommand> commands)? onAfterPaint; final void Function(SpineWidgetController controller, Canvas canvas, List<RenderCommandFlutter> commands)?
onAfterPaint;
/// Constructs a new [SpineWidget] controller. See the class documentation of [SpineWidgetController] for information on /// Constructs a new [SpineWidget] controller. See the class documentation of [SpineWidgetController] for information on
/// the optional arguments. /// the optional arguments.
@ -82,16 +84,16 @@ class SpineWidgetController {
this.onAfterPaint, this.onAfterPaint,
}); });
void _initialize(SkeletonDrawable drawable) { void _initialize(SkeletonDrawableFlutter drawable) {
var wasInitialized = _drawable != null; var wasInitialized = _drawable != null;
_drawable = drawable; _drawable = drawable;
if (!wasInitialized) onInitialized?.call(this); if (!wasInitialized) onInitialized?.call(this);
} }
/// The [Atlas] from which images to render the skeleton are sourced. /// The [AtlasFlutter] from which images to render the skeleton are sourced.
Atlas get atlas { AtlasFlutter get atlasFlutter {
if (_drawable == null) throw Exception("Controller is not initialized yet."); if (_drawable == null) throw Exception("Controller is not initialized yet.");
return _drawable!.atlas; return _drawable!.atlasFlutter;
} }
/// The setup-pose data used by the skeleton. /// The setup-pose data used by the skeleton.
@ -119,8 +121,8 @@ class SpineWidgetController {
return _drawable!.skeleton; return _drawable!.skeleton;
} }
/// The [SkeletonDrawable] /// The [SkeletonDrawableFlutter]
SkeletonDrawable get drawable { SkeletonDrawableFlutter get drawable {
if (_drawable == null) throw Exception("Controller is not initialized yet."); if (_drawable == null) throw Exception("Controller is not initialized yet.");
return _drawable!; return _drawable!;
} }
@ -170,7 +172,7 @@ enum _AssetType { asset, file, http, drawable }
abstract class BoundsProvider { abstract class BoundsProvider {
const BoundsProvider(); const BoundsProvider();
Bounds computeBounds(SkeletonDrawable drawable); Bounds computeBounds(SkeletonDrawableFlutter drawable);
} }
/// A [BoundsProvider] that calculates the bounding box of the skeleton based on the visible /// A [BoundsProvider] that calculates the bounding box of the skeleton based on the visible
@ -179,8 +181,10 @@ class SetupPoseBounds extends BoundsProvider {
const SetupPoseBounds(); const SetupPoseBounds();
@override @override
Bounds computeBounds(SkeletonDrawable drawable) { Bounds computeBounds(SkeletonDrawableFlutter drawable) {
return drawable.skeleton.getBounds(); drawable.skeleton.setupPose();
drawable.skeleton.updateWorldTransform(Physics.none);
return drawable.skeleton.bounds;
} }
} }
@ -191,8 +195,8 @@ class RawBounds extends BoundsProvider {
RawBounds(this.x, this.y, this.width, this.height); RawBounds(this.x, this.y, this.width, this.height);
@override @override
Bounds computeBounds(SkeletonDrawable drawable) { Bounds computeBounds(SkeletonDrawableFlutter drawable) {
return Bounds(x, y, width, height); return Bounds(x: x, y: y, width: width, height: height);
} }
} }
@ -211,17 +215,17 @@ class SkinAndAnimationBounds extends BoundsProvider {
: skins = skins == null || skins.isEmpty ? ["default"] : skins; : skins = skins == null || skins.isEmpty ? ["default"] : skins;
@override @override
Bounds computeBounds(SkeletonDrawable drawable) { Bounds computeBounds(SkeletonDrawableFlutter drawable) {
final data = drawable.skeletonData; final data = drawable.skeletonData;
final oldSkin = drawable.skeleton.getSkin(); final oldSkin = drawable.skeleton.skin;
final customSkin = Skin("custom-skin"); final customSkin = Skin("custom-skin");
for (final skinName in skins) { for (final skinName in skins) {
final skin = data.findSkin(skinName); final skin = data.findSkin(skinName);
if (skin == null) continue; if (skin == null) continue;
customSkin.addSkin(skin); customSkin.addSkin(skin);
} }
drawable.skeleton.setSkin(customSkin); drawable.skeleton.setSkin2(customSkin);
drawable.skeleton.setToSetupPose(); drawable.skeleton.setupPose();
final animation = this.animation != null ? data.findAnimation(this.animation!) : null; final animation = this.animation != null ? data.findAnimation(this.animation!) : null;
double minX = double.infinity; double minX = double.infinity;
@ -229,35 +233,37 @@ class SkinAndAnimationBounds extends BoundsProvider {
double maxX = double.negativeInfinity; double maxX = double.negativeInfinity;
double maxY = double.negativeInfinity; double maxY = double.negativeInfinity;
if (animation == null) { if (animation == null) {
final bounds = drawable.skeleton.getBounds(); drawable.skeleton.updateWorldTransform(Physics.none);
final bounds = drawable.skeleton.bounds;
minX = bounds.x; minX = bounds.x;
minY = bounds.y; minY = bounds.y;
maxX = minX + bounds.width; maxX = minX + bounds.width;
maxY = minY + bounds.height; maxY = minY + bounds.height;
} else { } else {
drawable.animationState.setAnimation(0, animation, false); drawable.animationState.setAnimation(0, animation.name, false);
final steps = max(animation.getDuration() / stepTime, 1.0).toInt(); final steps = max(animation.duration / stepTime, 1.0).toInt();
for (int i = 0; i < steps; i++) { for (int i = 0; i < steps; i++) {
drawable.update(i > 0 ? stepTime : 0); drawable.update(i > 0 ? stepTime : 0);
final bounds = drawable.skeleton.getBounds(); drawable.skeleton.updateWorldTransform(Physics.none);
final bounds = drawable.skeleton.bounds;
minX = min(minX, bounds.x); minX = min(minX, bounds.x);
minY = min(minY, bounds.y); minY = min(minY, bounds.y);
maxX = max(maxX, minX + bounds.width); maxX = max(maxX, minX + bounds.width);
maxY = max(maxY, minY + bounds.height); maxY = max(maxY, minY + bounds.height);
} }
} }
drawable.skeleton.setSkinByName("default"); drawable.skeleton.setSkin("default");
drawable.animationState.clearTracks(); drawable.animationState.clearTracks();
if (oldSkin != null) drawable.skeleton.setSkin(oldSkin); if (oldSkin != null) drawable.skeleton.setSkin2(oldSkin);
drawable.skeleton.setToSetupPose(); drawable.skeleton.setupPose();
drawable.update(0); drawable.update(0);
customSkin.dispose(); customSkin.dispose();
return Bounds(minX, minY, maxX - minX, maxY - minY); return Bounds(x: minX, y: minY, width: maxX - minX, height: maxY - minY);
} }
} }
/// A [StatefulWidget] to display a Spine skeleton. The skeleton can be loaded from an asset bundle ([SpineWidget.fromAsset], /// 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]). /// local files [SpineWidget.fromFile], URLs [SpineWidget.fromHttp], or a pre-loaded [SkeletonDrawableFlutter] ([SpineWidget.fromDrawable]).
/// ///
/// The skeleton displayed by a `SpineWidget` can be controlled via a [SpineWidgetController]. /// The skeleton displayed by a `SpineWidget` can be controlled via a [SpineWidgetController].
/// ///
@ -268,7 +274,7 @@ class SpineWidget extends StatefulWidget {
final AssetBundle? _bundle; final AssetBundle? _bundle;
final String? _skeletonFile; final String? _skeletonFile;
final String? _atlasFile; final String? _atlasFile;
final SkeletonDrawable? _drawable; final SkeletonDrawableFlutter? _drawable;
final SpineWidgetController _controller; final SpineWidgetController _controller;
final BoxFit _fit; final BoxFit _fit;
final Alignment _alignment; final Alignment _alignment;
@ -361,7 +367,7 @@ class SpineWidget extends StatefulWidget {
_sizedByBounds = sizedByBounds ?? false, _sizedByBounds = sizedByBounds ?? false,
_drawable = null; _drawable = null;
/// Constructs a new [SpineWidget] from a [SkeletonDrawable]. /// Constructs a new [SpineWidget] from a [SkeletonDrawableFlutter].
/// ///
/// After initialization is complete, the provided [_controller] is invoked as per the [SpineWidgetController] semantics, to allow /// 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. /// modifying how the skeleton inside the widget is animated and rendered.
@ -394,7 +400,7 @@ class SpineWidget extends StatefulWidget {
class _SpineWidgetState extends State<SpineWidget> { class _SpineWidgetState extends State<SpineWidget> {
late Bounds _computedBounds; late Bounds _computedBounds;
SkeletonDrawable? _drawable; SkeletonDrawableFlutter? _drawable;
@override @override
void initState() { void initState() {
@ -436,7 +442,7 @@ class _SpineWidgetState extends State<SpineWidget> {
} }
} }
void loadDrawable(SkeletonDrawable drawable) { void loadDrawable(SkeletonDrawableFlutter drawable) {
_drawable = drawable; _drawable = drawable;
_computedBounds = widget._boundsProvider.computeBounds(drawable); _computedBounds = widget._boundsProvider.computeBounds(drawable);
widget._controller._initialize(drawable); widget._controller._initialize(drawable);
@ -446,13 +452,13 @@ class _SpineWidgetState extends State<SpineWidget> {
void loadFromAsset(AssetBundle? bundle, String atlasFile, String skeletonFile, _AssetType assetType) async { void loadFromAsset(AssetBundle? bundle, String atlasFile, String skeletonFile, _AssetType assetType) async {
switch (assetType) { switch (assetType) {
case _AssetType.asset: case _AssetType.asset:
loadDrawable(await SkeletonDrawable.fromAsset(atlasFile, skeletonFile, bundle: bundle)); loadDrawable(await SkeletonDrawableFlutter.fromAsset(atlasFile, skeletonFile, bundle: bundle));
break; break;
case _AssetType.file: case _AssetType.file:
loadDrawable(await SkeletonDrawable.fromFile(atlasFile, skeletonFile)); loadDrawable(await SkeletonDrawableFlutter.fromFile(atlasFile, skeletonFile));
break; break;
case _AssetType.http: case _AssetType.http:
loadDrawable(await SkeletonDrawable.fromHttp(atlasFile, skeletonFile)); loadDrawable(await SkeletonDrawableFlutter.fromHttp(atlasFile, skeletonFile));
break; break;
case _AssetType.drawable: case _AssetType.drawable:
throw Exception("Drawable can not be loaded via loadFromAsset()."); throw Exception("Drawable can not be loaded via loadFromAsset().");
@ -483,7 +489,7 @@ class _SpineWidgetState extends State<SpineWidget> {
} }
class _SpineRenderObjectWidget extends LeafRenderObjectWidget { class _SpineRenderObjectWidget extends LeafRenderObjectWidget {
final SkeletonDrawable _skeletonDrawable; final SkeletonDrawableFlutter _skeletonDrawable;
final SpineWidgetController _controller; final SpineWidgetController _controller;
final BoxFit _fit; final BoxFit _fit;
final Alignment _alignment; final Alignment _alignment;
@ -515,7 +521,7 @@ class _SpineRenderObjectWidget extends LeafRenderObjectWidget {
} }
class _SpineRenderObject extends RenderBox { class _SpineRenderObject extends RenderBox {
SkeletonDrawable _skeletonDrawable; SkeletonDrawableFlutter _skeletonDrawable;
final SpineWidgetController _controller; final SpineWidgetController _controller;
double _deltaTime = 0; double _deltaTime = 0;
final Stopwatch _stopwatch = Stopwatch(); final Stopwatch _stopwatch = Stopwatch();
@ -535,7 +541,7 @@ class _SpineRenderObject extends RenderBox {
this._sizedByBounds, this._sizedByBounds,
); );
set skeletonDrawable(SkeletonDrawable skeletonDrawable) { set skeletonDrawable(SkeletonDrawableFlutter skeletonDrawable) {
if (_skeletonDrawable == skeletonDrawable) return; if (_skeletonDrawable == skeletonDrawable) return;
_skeletonDrawable = skeletonDrawable; _skeletonDrawable = skeletonDrawable;
@ -721,7 +727,11 @@ class _SpineRenderObject extends RenderBox {
if (_firstUpdated) { if (_firstUpdated) {
_controller.onBeforePaint?.call(_controller, canvas); _controller.onBeforePaint?.call(_controller, canvas);
final commands = _skeletonDrawable.renderToCanvas(canvas); final commands = _skeletonDrawable.renderFlutter();
for (final cmd in commands) {
final paint = _skeletonDrawable.atlasFlutter.atlasPagePaints[cmd.atlasPageIndex][cmd.blendMode]!;
canvas.drawVertices(cmd.vertices, rendering.BlendMode.modulate, paint);
}
_controller.onAfterPaint?.call(_controller, canvas, commands); _controller.onAfterPaint?.call(_controller, canvas, commands);
} }

View File

@ -1,3 +0,0 @@
// Relative import to be able to reuse the C sources.
// See the comment in ../{projectName}}.podspec for more information.
#include "../../src/spine-cpp-lite/spine-cpp-lite.cpp"

View File

@ -12,14 +12,9 @@ A new Flutter FFI plugin project.
s.homepage = 'http://example.com' s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' } s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' } s.author = { 'Your Company' => 'email@example.com' }
# This will ensure the source files in Classes/ are included in the native
# builds of apps using this FFI plugin. Podspec does not support relative
# paths, so Classes contains a forwarder C file that relatively imports
# `../src/*` so that the C sources can be shared among all target platforms.
s.source = { :path => '.' } s.source = { :path => '.' }
s.source_files = 'Classes/**/*.{cpp}' s.source_files = 'Classes/spine-c/src/**/*.{cpp,h}', 'Classes/spine-cpp/src/**/*.{cpp,h}'
s.xcconfig = { 'HEADER_SEARCH_PATHS' => '"$(PODS_TARGET_SRCROOT)/Classes/spine-cpp/include"' } s.xcconfig = { 'HEADER_SEARCH_PATHS' => '"$(PODS_TARGET_SRCROOT)/Classes/spine-cpp/include" "$(PODS_TARGET_SRCROOT)/Classes/spine-c/include"' }
s.dependency 'FlutterMacOS' s.dependency 'FlutterMacOS'
s.platform = :osx, '10.11' s.platform = :osx, '10.11'

View File

@ -44,5 +44,3 @@ flutter:
assets: assets:
- lib/assets/libspine_flutter.js - lib/assets/libspine_flutter.js
- lib/assets/libspine_flutter.wasm - lib/assets/libspine_flutter.wasm
- src/spine-cpp-lite/spine-cpp-lite.cpp
- src/spine-cpp-lite/spine-cpp-lite.h