mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2026-03-26 22:49:01 +08:00
[flutter] AnimationStateEventsManager for idiomatic animation state listeners, Flutter versions of Atlas, SkeletonDrawable, RenderCommand for convenience.
This commit is contained in:
parent
09969f5649
commit
4eddd24787
@ -6922,6 +6922,20 @@ class SpineDartBindings {
|
|||||||
late final _spine_skin_entry_get_attachment =
|
late final _spine_skin_entry_get_attachment =
|
||||||
_spine_skin_entry_get_attachmentPtr.asFunction<spine_attachment Function(spine_skin_entry)>();
|
_spine_skin_entry_get_attachmentPtr.asFunction<spine_attachment Function(spine_skin_entry)>();
|
||||||
|
|
||||||
|
/// Skeleton bounds function
|
||||||
|
spine_bounds spine_skeleton_get_bounds(
|
||||||
|
spine_skeleton skeleton,
|
||||||
|
) {
|
||||||
|
return _spine_skeleton_get_bounds(
|
||||||
|
skeleton,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
late final _spine_skeleton_get_boundsPtr =
|
||||||
|
_lookup<ffi.NativeFunction<spine_bounds Function(spine_skeleton)>>('spine_skeleton_get_bounds');
|
||||||
|
late final _spine_skeleton_get_bounds =
|
||||||
|
_spine_skeleton_get_boundsPtr.asFunction<spine_bounds Function(spine_skeleton)>();
|
||||||
|
|
||||||
spine_alpha_timeline spine_alpha_timeline_create(
|
spine_alpha_timeline spine_alpha_timeline_create(
|
||||||
int frameCount,
|
int frameCount,
|
||||||
int bezierCount,
|
int bezierCount,
|
||||||
@ -31442,7 +31456,6 @@ class SpineDartBindings {
|
|||||||
ffi.Pointer<ffi.Float> outY,
|
ffi.Pointer<ffi.Float> outY,
|
||||||
ffi.Pointer<ffi.Float> outWidth,
|
ffi.Pointer<ffi.Float> outWidth,
|
||||||
ffi.Pointer<ffi.Float> outHeight,
|
ffi.Pointer<ffi.Float> outHeight,
|
||||||
spine_array_float outVertexBuffer,
|
|
||||||
) {
|
) {
|
||||||
return _spine_skeleton_get_bounds_1(
|
return _spine_skeleton_get_bounds_1(
|
||||||
self,
|
self,
|
||||||
@ -31450,17 +31463,16 @@ class SpineDartBindings {
|
|||||||
outY,
|
outY,
|
||||||
outWidth,
|
outWidth,
|
||||||
outHeight,
|
outHeight,
|
||||||
outVertexBuffer,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
late final _spine_skeleton_get_bounds_1Ptr = _lookup<
|
late final _spine_skeleton_get_bounds_1Ptr = _lookup<
|
||||||
ffi.NativeFunction<
|
ffi.NativeFunction<
|
||||||
ffi.Void Function(spine_skeleton, ffi.Pointer<ffi.Float>, ffi.Pointer<ffi.Float>, ffi.Pointer<ffi.Float>,
|
ffi.Void Function(spine_skeleton, ffi.Pointer<ffi.Float>, ffi.Pointer<ffi.Float>, ffi.Pointer<ffi.Float>,
|
||||||
ffi.Pointer<ffi.Float>, spine_array_float)>>('spine_skeleton_get_bounds_1');
|
ffi.Pointer<ffi.Float>)>>('spine_skeleton_get_bounds_1');
|
||||||
late final _spine_skeleton_get_bounds_1 = _spine_skeleton_get_bounds_1Ptr.asFunction<
|
late final _spine_skeleton_get_bounds_1 = _spine_skeleton_get_bounds_1Ptr.asFunction<
|
||||||
void Function(spine_skeleton, ffi.Pointer<ffi.Float>, ffi.Pointer<ffi.Float>, ffi.Pointer<ffi.Float>,
|
void Function(spine_skeleton, ffi.Pointer<ffi.Float>, ffi.Pointer<ffi.Float>, ffi.Pointer<ffi.Float>,
|
||||||
ffi.Pointer<ffi.Float>, spine_array_float)>();
|
ffi.Pointer<ffi.Float>)>();
|
||||||
|
|
||||||
void spine_skeleton_get_bounds_2(
|
void spine_skeleton_get_bounds_2(
|
||||||
spine_skeleton self,
|
spine_skeleton self,
|
||||||
@ -36906,6 +36918,36 @@ class SpineDartBindings {
|
|||||||
late final _spine_track_entry_is_next_ready =
|
late final _spine_track_entry_is_next_ready =
|
||||||
_spine_track_entry_is_next_readyPtr.asFunction<bool Function(spine_track_entry)>();
|
_spine_track_entry_is_next_readyPtr.asFunction<bool Function(spine_track_entry)>();
|
||||||
|
|
||||||
|
spine_animation_state spine_track_entry_get_animation_state(
|
||||||
|
spine_track_entry self,
|
||||||
|
) {
|
||||||
|
return _spine_track_entry_get_animation_state(
|
||||||
|
self,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
late final _spine_track_entry_get_animation_statePtr =
|
||||||
|
_lookup<ffi.NativeFunction<spine_animation_state Function(spine_track_entry)>>(
|
||||||
|
'spine_track_entry_get_animation_state');
|
||||||
|
late final _spine_track_entry_get_animation_state =
|
||||||
|
_spine_track_entry_get_animation_statePtr.asFunction<spine_animation_state Function(spine_track_entry)>();
|
||||||
|
|
||||||
|
void spine_track_entry_set_animation_state(
|
||||||
|
spine_track_entry self,
|
||||||
|
spine_animation_state state,
|
||||||
|
) {
|
||||||
|
return _spine_track_entry_set_animation_state(
|
||||||
|
self,
|
||||||
|
state,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
late final _spine_track_entry_set_animation_statePtr =
|
||||||
|
_lookup<ffi.NativeFunction<ffi.Void Function(spine_track_entry, spine_animation_state)>>(
|
||||||
|
'spine_track_entry_set_animation_state');
|
||||||
|
late final _spine_track_entry_set_animation_state =
|
||||||
|
_spine_track_entry_set_animation_statePtr.asFunction<void Function(spine_track_entry, spine_animation_state)>();
|
||||||
|
|
||||||
ffi.Pointer<ffi.Void> spine_track_entry_get_renderer_object(
|
ffi.Pointer<ffi.Void> spine_track_entry_get_renderer_object(
|
||||||
spine_track_entry self,
|
spine_track_entry self,
|
||||||
) {
|
) {
|
||||||
@ -40802,6 +40844,22 @@ typedef spine_animation_state_data = ffi.Pointer<spine_animation_state_data_wrap
|
|||||||
typedef spine_animation_state_events = ffi.Pointer<spine_animation_state_events_wrapper>;
|
typedef spine_animation_state_events = ffi.Pointer<spine_animation_state_events_wrapper>;
|
||||||
typedef spine_skin_entries = ffi.Pointer<spine_skin_entries_wrapper>;
|
typedef spine_skin_entries = ffi.Pointer<spine_skin_entries_wrapper>;
|
||||||
typedef spine_skin_entry = ffi.Pointer<spine_skin_entry_wrapper>;
|
typedef spine_skin_entry = ffi.Pointer<spine_skin_entry_wrapper>;
|
||||||
|
|
||||||
|
/// Bounds struct
|
||||||
|
final class spine_bounds extends ffi.Struct {
|
||||||
|
@ffi.Float()
|
||||||
|
external double x;
|
||||||
|
|
||||||
|
@ffi.Float()
|
||||||
|
external double y;
|
||||||
|
|
||||||
|
@ffi.Float()
|
||||||
|
external double width;
|
||||||
|
|
||||||
|
@ffi.Float()
|
||||||
|
external double height;
|
||||||
|
}
|
||||||
|
|
||||||
typedef spine_alpha_timeline = ffi.Pointer<spine_alpha_timeline_wrapper>;
|
typedef spine_alpha_timeline = ffi.Pointer<spine_alpha_timeline_wrapper>;
|
||||||
typedef spine_rtti = ffi.Pointer<spine_rtti_wrapper>;
|
typedef spine_rtti = ffi.Pointer<spine_rtti_wrapper>;
|
||||||
typedef spine_atlas_attachment_loader = ffi.Pointer<spine_atlas_attachment_loader_wrapper>;
|
typedef spine_atlas_attachment_loader = ffi.Pointer<spine_atlas_attachment_loader_wrapper>;
|
||||||
|
|||||||
@ -33,6 +33,7 @@ import 'dart:ffi';
|
|||||||
import 'spine_dart_bindings_generated.dart';
|
import 'spine_dart_bindings_generated.dart';
|
||||||
import '../spine_bindings.dart';
|
import '../spine_bindings.dart';
|
||||||
import 'animation.dart';
|
import 'animation.dart';
|
||||||
|
import 'animation_state.dart';
|
||||||
import 'mix_blend.dart';
|
import 'mix_blend.dart';
|
||||||
|
|
||||||
/// TrackEntry wrapper
|
/// TrackEntry wrapper
|
||||||
@ -288,6 +289,16 @@ class TrackEntry {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AnimationState? get animationState {
|
||||||
|
final result = SpineBindings.bindings.spine_track_entry_get_animation_state(_ptr);
|
||||||
|
return result.address == 0 ? null : AnimationState.fromPointer(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
set animationState(AnimationState? value) {
|
||||||
|
SpineBindings.bindings
|
||||||
|
.spine_track_entry_set_animation_state(_ptr, value?.nativePtr.cast() ?? Pointer.fromAddress(0));
|
||||||
|
}
|
||||||
|
|
||||||
Pointer<Void>? get rendererObject {
|
Pointer<Void>? get rendererObject {
|
||||||
final result = SpineBindings.bindings.spine_track_entry_get_renderer_object(_ptr);
|
final result = SpineBindings.bindings.spine_track_entry_get_renderer_object(_ptr);
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@ -42,13 +42,13 @@ import 'generated/bounding_box_attachment.dart';
|
|||||||
import 'generated/clipping_attachment.dart';
|
import 'generated/clipping_attachment.dart';
|
||||||
import 'generated/path_attachment.dart';
|
import 'generated/path_attachment.dart';
|
||||||
import 'generated/point_attachment.dart';
|
import 'generated/point_attachment.dart';
|
||||||
import 'generated/rtti.dart';
|
|
||||||
import 'generated/skeleton.dart';
|
import 'generated/skeleton.dart';
|
||||||
import 'generated/animation_state.dart';
|
import 'generated/animation_state.dart';
|
||||||
import 'generated/animation_state_data.dart';
|
import 'generated/animation_state_data.dart';
|
||||||
import 'generated/track_entry.dart';
|
import 'generated/track_entry.dart';
|
||||||
import 'generated/event.dart';
|
import 'generated/event.dart';
|
||||||
import 'generated/event_type.dart';
|
import 'generated/event_type.dart';
|
||||||
|
import 'generated/render_command.dart';
|
||||||
|
|
||||||
// Export generated classes
|
// Export generated classes
|
||||||
export 'generated/api.dart';
|
export 'generated/api.dart';
|
||||||
@ -223,11 +223,136 @@ extension SkinExtensions on Skin {
|
|||||||
/// Event listener callback for animation state events
|
/// Event listener callback for animation state events
|
||||||
typedef AnimationStateListener = void Function(EventType type, TrackEntry entry, Event? event);
|
typedef AnimationStateListener = void Function(EventType type, TrackEntry entry, Event? event);
|
||||||
|
|
||||||
|
/// Manager for animation state event listeners
|
||||||
|
class AnimationStateEventManager {
|
||||||
|
// Use pointer addresses as keys since Dart wrapper objects might be recreated
|
||||||
|
final Map<int, AnimationStateListener?> _stateListeners = {};
|
||||||
|
final Map<int, Map<int, AnimationStateListener>> _trackEntryListeners = {};
|
||||||
|
|
||||||
|
static final instance = AnimationStateEventManager._();
|
||||||
|
AnimationStateEventManager._();
|
||||||
|
|
||||||
|
void setStateListener(AnimationState state, AnimationStateListener? listener) {
|
||||||
|
final key = state.nativePtr.address;
|
||||||
|
if (listener == null) {
|
||||||
|
_stateListeners.remove(key);
|
||||||
|
} else {
|
||||||
|
_stateListeners[key] = listener;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimationStateListener? getStateListener(AnimationState state) {
|
||||||
|
final key = state.nativePtr.address;
|
||||||
|
return _stateListeners[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
void setTrackEntryListener(TrackEntry entry, AnimationStateListener? listener) {
|
||||||
|
// Get the animation state from the track entry itself!
|
||||||
|
final state = entry.animationState;
|
||||||
|
if (state == null) {
|
||||||
|
throw StateError('TrackEntry does not have an associated AnimationState');
|
||||||
|
}
|
||||||
|
|
||||||
|
final stateKey = state.nativePtr.address;
|
||||||
|
final entryKey = entry.nativePtr.address;
|
||||||
|
final listeners = _trackEntryListeners.putIfAbsent(stateKey, () => {});
|
||||||
|
if (listener == null) {
|
||||||
|
listeners.remove(entryKey);
|
||||||
|
} else {
|
||||||
|
listeners[entryKey] = listener;
|
||||||
|
print('DEBUG: Registered listener for TrackEntry at address: $entryKey for AnimationState at address: $stateKey');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimationStateListener? getTrackEntryListener(AnimationState state, TrackEntry entry) {
|
||||||
|
final stateKey = state.nativePtr.address;
|
||||||
|
final entryKey = entry.nativePtr.address;
|
||||||
|
final listener = _trackEntryListeners[stateKey]?[entryKey];
|
||||||
|
if (listener == null) {
|
||||||
|
print('DEBUG: No listener found for TrackEntry at address: $entryKey in AnimationState at address: $stateKey');
|
||||||
|
print('DEBUG: Available state keys: ${_trackEntryListeners.keys.toList()}');
|
||||||
|
print('DEBUG: Available entry keys for state $stateKey: ${_trackEntryListeners[stateKey]?.keys.toList()}');
|
||||||
|
}
|
||||||
|
return listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeTrackEntry(AnimationState state, TrackEntry entry) {
|
||||||
|
final stateKey = state.nativePtr.address;
|
||||||
|
final entryKey = entry.nativePtr.address;
|
||||||
|
_trackEntryListeners[stateKey]?.remove(entryKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearState(AnimationState state) {
|
||||||
|
final key = state.nativePtr.address;
|
||||||
|
_stateListeners.remove(key);
|
||||||
|
_trackEntryListeners.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Debug method to inspect current state of the manager
|
||||||
|
void debugPrint() {
|
||||||
|
print('\nAnimationStateEventManager contents:');
|
||||||
|
print(' State listeners: ${_stateListeners.keys.toList()} (${_stateListeners.length} total)');
|
||||||
|
print(' Track entry listeners by state:');
|
||||||
|
for (final entry in _trackEntryListeners.entries) {
|
||||||
|
print(' State ${entry.key}: ${entry.value.keys.toList()} (${entry.value.length} entries)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extension to manage event listeners on AnimationState
|
||||||
|
extension AnimationStateListeners on AnimationState {
|
||||||
|
/// Set a listener for all animation state events
|
||||||
|
void setListener(AnimationStateListener? listener) {
|
||||||
|
AnimationStateEventManager.instance.setStateListener(this, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current state listener
|
||||||
|
AnimationStateListener? get listener => AnimationStateEventManager.instance.getStateListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extension to add setListener to TrackEntry
|
||||||
|
extension TrackEntryExtensions on TrackEntry {
|
||||||
|
/// Set a listener for events from this track entry
|
||||||
|
void setListener(AnimationStateListener? listener) {
|
||||||
|
AnimationStateEventManager.instance.setTrackEntryListener(this, listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a bounding box with position and dimensions
|
||||||
|
class Bounds {
|
||||||
|
final double x;
|
||||||
|
final double y;
|
||||||
|
final double width;
|
||||||
|
final double height;
|
||||||
|
|
||||||
|
const Bounds({
|
||||||
|
required this.x,
|
||||||
|
required this.y,
|
||||||
|
required this.width,
|
||||||
|
required this.height,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'Bounds(x: $x, y: $y, width: $width, height: $height)';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extension to add bounds property to Skeleton
|
||||||
|
extension SkeletonExtensions on Skeleton {
|
||||||
|
/// Get the axis-aligned bounding box (AABB) containing all world vertices of the skeleton
|
||||||
|
Bounds get bounds {
|
||||||
|
final spineBounds = SpineBindings.bindings.spine_skeleton_get_bounds(nativePtr.cast());
|
||||||
|
return Bounds(
|
||||||
|
x: spineBounds.x,
|
||||||
|
y: spineBounds.y,
|
||||||
|
width: spineBounds.width,
|
||||||
|
height: spineBounds.height,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Convenient drawable that combines skeleton, animation state, and rendering
|
/// Convenient drawable that combines skeleton, animation state, and rendering
|
||||||
class SkeletonDrawable {
|
class SkeletonDrawable {
|
||||||
final Pointer<spine_skeleton_drawable_wrapper> _drawable;
|
final Pointer<spine_skeleton_drawable_wrapper> _drawable;
|
||||||
final Map<TrackEntry, AnimationStateListener> _trackEntryListeners = {};
|
|
||||||
AnimationStateListener? _stateListener;
|
|
||||||
|
|
||||||
late final Skeleton skeleton;
|
late final Skeleton skeleton;
|
||||||
late final AnimationState animationState;
|
late final AnimationState animationState;
|
||||||
@ -275,16 +400,15 @@ class SkeletonDrawable {
|
|||||||
final event = eventPtr.address == 0 ? null : Event.fromPointer(eventPtr);
|
final event = eventPtr.address == 0 ? null : Event.fromPointer(eventPtr);
|
||||||
|
|
||||||
// Call track entry listener if registered
|
// Call track entry listener if registered
|
||||||
if (_trackEntryListeners.containsKey(trackEntry)) {
|
final trackListener = AnimationStateEventManager.instance.getTrackEntryListener(animationState, trackEntry);
|
||||||
_trackEntryListeners[trackEntry]?.call(type, trackEntry, event);
|
trackListener?.call(type, trackEntry, event);
|
||||||
}
|
|
||||||
|
|
||||||
// Call global state listener
|
// Call global state listener
|
||||||
_stateListener?.call(type, trackEntry, event);
|
animationState.listener?.call(type, trackEntry, event);
|
||||||
|
|
||||||
// Remove listener if track entry is being disposed
|
// Remove listener if track entry is being disposed
|
||||||
if (type == EventType.dispose) {
|
if (type == EventType.dispose) {
|
||||||
_trackEntryListeners.remove(trackEntry);
|
AnimationStateEventManager.instance.removeTrackEntry(animationState, trackEntry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -296,31 +420,14 @@ class SkeletonDrawable {
|
|||||||
animationState.apply(skeleton);
|
animationState.apply(skeleton);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set a listener for all animation state events
|
/// Render the skeleton and get render commands
|
||||||
void setListener(AnimationStateListener? listener) {
|
RenderCommand? render() {
|
||||||
_stateListener = listener;
|
final renderCommand = SpineBindings.bindings.spine_skeleton_drawable_render(_drawable.cast());
|
||||||
}
|
return renderCommand.address == 0 ? null : RenderCommand.fromPointer(renderCommand);
|
||||||
|
|
||||||
/// Internal method to set a listener for a specific track entry
|
|
||||||
void _setTrackEntryListener(TrackEntry entry, AnimationStateListener? listener) {
|
|
||||||
if (listener == null) {
|
|
||||||
_trackEntryListeners.remove(entry);
|
|
||||||
} else {
|
|
||||||
_trackEntryListeners[entry] = listener;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_trackEntryListeners.clear();
|
AnimationStateEventManager.instance.clearState(animationState);
|
||||||
_stateListener = null;
|
|
||||||
SpineBindings.bindings.spine_skeleton_drawable_dispose(_drawable.cast());
|
SpineBindings.bindings.spine_skeleton_drawable_dispose(_drawable.cast());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extension to add setListener to TrackEntry
|
|
||||||
extension TrackEntryExtensions on TrackEntry {
|
|
||||||
/// Set a listener for events from this track entry
|
|
||||||
void setListener(SkeletonDrawable drawable, AnimationStateListener? listener) {
|
|
||||||
drawable._setTrackEntryListener(this, listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -83,9 +83,7 @@ Future<SpineDartFFI> initSpineDartFFI(bool useStaticLinkage) async {
|
|||||||
registerOpaqueType<spine_atlas_wrapper>();
|
registerOpaqueType<spine_atlas_wrapper>();
|
||||||
registerOpaqueType<spine_skeleton_data_result_wrapper>();
|
registerOpaqueType<spine_skeleton_data_result_wrapper>();
|
||||||
registerOpaqueType<spine_render_command_wrapper>();
|
registerOpaqueType<spine_render_command_wrapper>();
|
||||||
registerOpaqueType<spine_bounds_wrapper>();
|
|
||||||
registerOpaqueType<spine_color_wrapper>();
|
registerOpaqueType<spine_color_wrapper>();
|
||||||
registerOpaqueType<spine_vector_wrapper>();
|
|
||||||
registerOpaqueType<spine_skeleton_drawable_wrapper>();
|
registerOpaqueType<spine_skeleton_drawable_wrapper>();
|
||||||
registerOpaqueType<spine_skin_entry_wrapper>();
|
registerOpaqueType<spine_skin_entry_wrapper>();
|
||||||
registerOpaqueType<spine_skin_entries_wrapper>();
|
registerOpaqueType<spine_skin_entries_wrapper>();
|
||||||
|
|||||||
@ -1,9 +1,457 @@
|
|||||||
import 'spine_dart.dart';
|
import 'spine_dart.dart';
|
||||||
export 'spine_dart.dart';
|
export 'spine_dart.dart';
|
||||||
|
import 'raw_image_provider.dart';
|
||||||
export 'spine_widget.dart';
|
export 'spine_widget.dart';
|
||||||
|
export 'raw_image_provider.dart';
|
||||||
|
|
||||||
|
import 'dart:convert' as convert;
|
||||||
|
import 'dart:ffi';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
|
import 'package:flutter/material.dart' as material;
|
||||||
|
import 'package:flutter/rendering.dart' as rendering;
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
// Backwards compatibility
|
// Backwards compatibility
|
||||||
Future<void> initSpineFlutter({bool useStaticLinkage = false, bool enableMemoryDebugging = false}) async {
|
Future<void> initSpineFlutter({bool useStaticLinkage = false, bool enableMemoryDebugging = false}) async {
|
||||||
await initSpineDart(useStaticLinkage: useStaticLinkage, enableMemoryDebugging: enableMemoryDebugging);
|
await initSpineDart(useStaticLinkage: useStaticLinkage, enableMemoryDebugging: enableMemoryDebugging);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Flutter wrapper for Atlas that manages texture loading and Paint creation
|
||||||
|
class AtlasFlutter {
|
||||||
|
static FilterQuality filterQuality = FilterQuality.low;
|
||||||
|
final Atlas atlas;
|
||||||
|
final List<Image> atlasPages;
|
||||||
|
final List<Map<BlendMode, Paint>> atlasPagePaints;
|
||||||
|
bool _disposed = false;
|
||||||
|
|
||||||
|
AtlasFlutter._(this.atlas, this.atlasPages, this.atlasPagePaints);
|
||||||
|
|
||||||
|
/// Internal method to load atlas and images
|
||||||
|
static Future<AtlasFlutter> _load(String atlasFileName, Future<Uint8List> Function(String name) loadFile) async {
|
||||||
|
// Load atlas data
|
||||||
|
final atlasBytes = await loadFile(atlasFileName);
|
||||||
|
final atlasData = convert.utf8.decode(atlasBytes);
|
||||||
|
final atlas = loadAtlas(atlasData);
|
||||||
|
|
||||||
|
// Load images for each atlas page
|
||||||
|
final atlasDir = path.dirname(atlasFileName);
|
||||||
|
final pages = <Image>[];
|
||||||
|
final paints = <Map<BlendMode, Paint>>[];
|
||||||
|
|
||||||
|
// Load images for each atlas page
|
||||||
|
for (int i = 0; i < atlas.pages.length; i++) {
|
||||||
|
final page = atlas.pages[i];
|
||||||
|
if (page == null) continue;
|
||||||
|
|
||||||
|
// Get the texture path from the atlas page
|
||||||
|
final texturePath = page.texturePath;
|
||||||
|
final imagePath = "$atlasDir/$texturePath";
|
||||||
|
|
||||||
|
final imageData = await loadFile(imagePath);
|
||||||
|
final codec = await instantiateImageCodec(imageData);
|
||||||
|
final frameInfo = await codec.getNextFrame();
|
||||||
|
final image = frameInfo.image;
|
||||||
|
pages.add(image);
|
||||||
|
|
||||||
|
// Create paints for each blend mode
|
||||||
|
final pagePaints = <BlendMode, Paint>{};
|
||||||
|
for (final blendMode in BlendMode.values) {
|
||||||
|
pagePaints[blendMode] = Paint()
|
||||||
|
..shader = ImageShader(
|
||||||
|
image,
|
||||||
|
TileMode.clamp,
|
||||||
|
TileMode.clamp,
|
||||||
|
Matrix4.identity().storage,
|
||||||
|
filterQuality: filterQuality,
|
||||||
|
)
|
||||||
|
..isAntiAlias = true
|
||||||
|
..blendMode = blendMode.toFlutterBlendMode();
|
||||||
|
}
|
||||||
|
paints.add(pagePaints);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AtlasFlutter._(atlas, pages, paints);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads an [AtlasFlutter] from the file [atlasFileName].
|
||||||
|
static Future<AtlasFlutter> fromFile(String atlasFileName) async {
|
||||||
|
return _load(atlasFileName, (file) => File(file).readAsBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads an [AtlasFlutter] from the URL [atlasURL].
|
||||||
|
static Future<AtlasFlutter> fromHttp(String atlasURL) async {
|
||||||
|
return _load(atlasURL, (file) async {
|
||||||
|
final response = await http.get(Uri.parse(file));
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception('Failed to load $file: ${response.statusCode}');
|
||||||
|
}
|
||||||
|
return response.bodyBytes;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disposes all resources including the native atlas and images
|
||||||
|
void dispose() {
|
||||||
|
if (_disposed) return;
|
||||||
|
_disposed = true;
|
||||||
|
atlas.dispose();
|
||||||
|
for (final image in atlasPages) {
|
||||||
|
image.dispose();
|
||||||
|
}
|
||||||
|
atlasPagePaints.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extension to convert Spine BlendMode to Flutter BlendMode
|
||||||
|
extension BlendModeExtensions on BlendMode {
|
||||||
|
rendering.BlendMode toFlutterBlendMode() {
|
||||||
|
switch (this) {
|
||||||
|
case BlendMode.normal:
|
||||||
|
return rendering.BlendMode.srcOver;
|
||||||
|
case BlendMode.additive:
|
||||||
|
return rendering.BlendMode.plus;
|
||||||
|
case BlendMode.multiply:
|
||||||
|
return rendering.BlendMode.multiply;
|
||||||
|
case BlendMode.screen:
|
||||||
|
return rendering.BlendMode.screen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flutter-specific render command that wraps the native RenderCommand and provides
|
||||||
|
/// Flutter Vertices for efficient rendering.
|
||||||
|
class RenderCommandFlutter {
|
||||||
|
final RenderCommand _nativeCommand;
|
||||||
|
late final Vertices vertices;
|
||||||
|
late final int atlasPageIndex;
|
||||||
|
late final BlendMode blendMode;
|
||||||
|
|
||||||
|
RenderCommandFlutter._(this._nativeCommand, double pageWidth, double pageHeight) {
|
||||||
|
// Get atlas page index from texture pointer (which is actually the page index when using spine_atlas_load)
|
||||||
|
final texturePtr = _nativeCommand.texture;
|
||||||
|
atlasPageIndex = texturePtr?.address ?? 0;
|
||||||
|
|
||||||
|
final numVertices = _nativeCommand.numVertices;
|
||||||
|
final numIndices = _nativeCommand.numIndices;
|
||||||
|
|
||||||
|
// Get native data pointers
|
||||||
|
final positionsPtr = _nativeCommand.positions;
|
||||||
|
final uvsPtr = _nativeCommand.uvs;
|
||||||
|
final colorsPtr = _nativeCommand.colors;
|
||||||
|
final indicesPtr = _nativeCommand.indices;
|
||||||
|
|
||||||
|
if (positionsPtr == null || uvsPtr == null || colorsPtr == null || indicesPtr == null) {
|
||||||
|
throw Exception('Invalid render command data');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to typed lists
|
||||||
|
final positions = positionsPtr.asTypedList(numVertices * 2);
|
||||||
|
final uvs = uvsPtr.asTypedList(numVertices * 2);
|
||||||
|
final indices = indicesPtr.asTypedList(numIndices);
|
||||||
|
|
||||||
|
// Scale UVs by texture dimensions
|
||||||
|
for (int i = 0; i < numVertices * 2; i += 2) {
|
||||||
|
uvs[i] *= pageWidth;
|
||||||
|
uvs[i + 1] *= pageHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get blend mode
|
||||||
|
blendMode = _nativeCommand.blendMode;
|
||||||
|
|
||||||
|
// Handle colors - convert Uint32 to Int32 view without copying
|
||||||
|
final colorsUint32 = colorsPtr.asTypedList(numVertices);
|
||||||
|
final colors = Int32List.view(colorsUint32.buffer, colorsUint32.offsetInBytes, colorsUint32.length);
|
||||||
|
|
||||||
|
if (!kIsWeb) {
|
||||||
|
// We pass the native data as views directly to Vertices.raw. According to the sources, the data
|
||||||
|
// is copied, so it doesn't matter that we free up the underlying memory on the next
|
||||||
|
// render call. See the implementation of Vertices.raw() here:
|
||||||
|
// https://github.com/flutter/engine/blob/5c60785b802ad2c8b8899608d949342d5c624952/lib/ui/painting/vertices.cc#L21
|
||||||
|
//
|
||||||
|
// Impeller is currently using a slow path when using vertex colors.
|
||||||
|
// 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.
|
||||||
|
// See spine_flutter.cpp, batch_commands().
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
// has to render to an offscreen surface.
|
||||||
|
if (colors.isNotEmpty && colors[0] == -1) {
|
||||||
|
// Fast path: no vertex colors (all white)
|
||||||
|
vertices = Vertices.raw(VertexMode.triangles, positions, textureCoordinates: uvs, indices: indices);
|
||||||
|
} else {
|
||||||
|
vertices = Vertices.raw(
|
||||||
|
VertexMode.triangles,
|
||||||
|
positions,
|
||||||
|
textureCoordinates: uvs,
|
||||||
|
colors: colors,
|
||||||
|
indices: indices,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// On web, we need to copy the data
|
||||||
|
final positionsCopy = Float32List.fromList(positions);
|
||||||
|
final uvsCopy = Float32List.fromList(uvs);
|
||||||
|
final colorsCopy = Int32List.fromList(colors);
|
||||||
|
final indicesCopy = Uint16List.fromList(indices);
|
||||||
|
vertices = Vertices.raw(
|
||||||
|
VertexMode.triangles,
|
||||||
|
positionsCopy,
|
||||||
|
textureCoordinates: uvsCopy,
|
||||||
|
colors: colorsCopy,
|
||||||
|
indices: indicesCopy,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A SkeletonDrawable bundles loading, updating, and rendering an [Atlas], [Skeleton], and [AnimationState]
|
||||||
|
/// into a single easy to use class.
|
||||||
|
///
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
/// You can then directly access the [atlas], [skeletonData], [skeleton], [animationStateData], and [animationState]
|
||||||
|
/// to query and animate the skeleton. Use the [AnimationState] to queue animations on one or more tracks
|
||||||
|
/// via [AnimationState.setAnimation] or [AnimationState.addAnimation].
|
||||||
|
///
|
||||||
|
/// To update the [AnimationState] and apply it to the [Skeleton] call the [update] function, providing it
|
||||||
|
/// a delta time in seconds to advance the animations.
|
||||||
|
///
|
||||||
|
/// To render the current pose of the [Skeleton], use the rendering methods [render], [renderToCanvas], [renderToPictureRecorder],
|
||||||
|
/// [renderToPng], or [renderToRawImageData], depending on your needs.
|
||||||
|
///
|
||||||
|
/// 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
|
||||||
|
/// atlas and skeleton data as well, if no skeleton drawable references them anymore.
|
||||||
|
class SkeletonDrawableFlutter {
|
||||||
|
final AtlasFlutter atlasFlutter;
|
||||||
|
final SkeletonData skeletonData;
|
||||||
|
late final SkeletonDrawable _drawable;
|
||||||
|
late final Skeleton skeleton;
|
||||||
|
late final AnimationStateData animationStateData;
|
||||||
|
late final AnimationState animationState;
|
||||||
|
final bool _ownsAtlasAndSkeletonData;
|
||||||
|
bool _disposed = false;
|
||||||
|
|
||||||
|
/// Constructs a new skeleton drawable from the given (possibly shared) [Atlas] and [SkeletonData]. If
|
||||||
|
/// 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.
|
||||||
|
SkeletonDrawableFlutter(this.atlasFlutter, this.skeletonData, this._ownsAtlasAndSkeletonData) {
|
||||||
|
_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
|
||||||
|
/// or the optionally provided [bundle].
|
||||||
|
///
|
||||||
|
/// Throws an exception in case the data could not be loaded.
|
||||||
|
static Future<SkeletonDrawableFlutter> fromAsset(String atlasFile, String skeletonFile, {AssetBundle? bundle}) async {
|
||||||
|
bundle ??= rootBundle;
|
||||||
|
final atlasFlutter = await AtlasFlutter.fromAsset(atlasFile, bundle: bundle);
|
||||||
|
|
||||||
|
final skeletonData = await bundle.loadString(skeletonFile);
|
||||||
|
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].
|
||||||
|
///
|
||||||
|
/// Throws an exception in case the data could not be loaded.
|
||||||
|
static Future<SkeletonDrawableFlutter> fromFile(String atlasFile, String skeletonFile) async {
|
||||||
|
final atlasFlutter = await AtlasFlutter.fromFile(atlasFile);
|
||||||
|
|
||||||
|
final SkeletonData skeleton;
|
||||||
|
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].
|
||||||
|
///
|
||||||
|
/// Throws an exception in case the data could not be loaded.
|
||||||
|
static Future<SkeletonDrawableFlutter> fromHttp(String atlasUrl, String skeletonUrl) async {
|
||||||
|
final atlasFlutter = await AtlasFlutter.fromHttp(atlasUrl);
|
||||||
|
|
||||||
|
final SkeletonData skeleton;
|
||||||
|
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
|
||||||
|
/// can be rendered via [Canvas.drawVertices].
|
||||||
|
List<RenderCommandFlutter> render() {
|
||||||
|
if (_disposed) return [];
|
||||||
|
|
||||||
|
var commands = <RenderCommandFlutter>[];
|
||||||
|
var nativeCmd = _drawable.render();
|
||||||
|
|
||||||
|
while (nativeCmd != null) {
|
||||||
|
// Get page dimensions from atlas
|
||||||
|
final pageIndex = nativeCmd.texture?.address ?? 0;
|
||||||
|
final pages = atlasFlutter.atlas.pages;
|
||||||
|
final page = pages[pageIndex];
|
||||||
|
if (page != null) {
|
||||||
|
commands.add(RenderCommandFlutter._(nativeCmd, page.width.toDouble(), page.height.toDouble()));
|
||||||
|
} else {
|
||||||
|
commands.add(RenderCommandFlutter._(nativeCmd, 1.0, 1.0));
|
||||||
|
}
|
||||||
|
nativeCmd = nativeCmd.next;
|
||||||
|
}
|
||||||
|
|
||||||
|
return commands;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders the skeleton drawable's current pose to the given [canvas]. Does not perform any
|
||||||
|
/// scaling or fitting.
|
||||||
|
List<RenderCommandFlutter> renderToCanvas(Canvas canvas) {
|
||||||
|
var commands = render();
|
||||||
|
|
||||||
|
for (final cmd in commands) {
|
||||||
|
// Get the paint for this atlas page and blend mode
|
||||||
|
Paint? paint;
|
||||||
|
if (cmd.atlasPageIndex < atlasFlutter.atlasPagePaints.length) {
|
||||||
|
paint = atlasFlutter.atlasPagePaints[cmd.atlasPageIndex][cmd.blendMode];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to a simple paint if textures aren't loaded
|
||||||
|
paint ??= Paint()
|
||||||
|
..color = material.Colors.white
|
||||||
|
..style = PaintingStyle.fill;
|
||||||
|
|
||||||
|
canvas.drawVertices(
|
||||||
|
cmd.vertices,
|
||||||
|
rendering.BlendMode.modulate,
|
||||||
|
paint,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return commands;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders the skeleton drawable's current pose to a [PictureRecorder] with the given [width] and [height].
|
||||||
|
/// Uses [bgColor], a 32-bit ARGB color value, to paint the background.
|
||||||
|
/// Scales and centers the skeleton to fit the within the bounds of [width] and [height].
|
||||||
|
PictureRecorder renderToPictureRecorder(double width, double height, int bgColor) {
|
||||||
|
var bounds = skeleton.bounds;
|
||||||
|
var scale = 1 / (bounds.width > bounds.height ? bounds.width / width : bounds.height / height);
|
||||||
|
|
||||||
|
var recorder = PictureRecorder();
|
||||||
|
var canvas = Canvas(recorder);
|
||||||
|
var paint = Paint()
|
||||||
|
..color = material.Color(bgColor)
|
||||||
|
..style = PaintingStyle.fill;
|
||||||
|
canvas.drawRect(Rect.fromLTWH(0, 0, width, height), paint);
|
||||||
|
canvas.translate(width / 2, height / 2);
|
||||||
|
canvas.scale(scale, scale);
|
||||||
|
canvas.translate(-(bounds.x + bounds.width / 2), -(bounds.y + bounds.height / 2));
|
||||||
|
renderToCanvas(canvas);
|
||||||
|
return recorder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders the skeleton drawable's current pose to a PNG encoded in a [Uint8List], with the given [width] and [height].
|
||||||
|
/// Uses [bgColor], a 32-bit ARGB color value, to paint the background.
|
||||||
|
/// Scales and centers the skeleton to fit the within the bounds of [width] and [height].
|
||||||
|
Future<Uint8List> renderToPng(double width, double height, int bgColor) async {
|
||||||
|
final recorder = renderToPictureRecorder(width, height, bgColor);
|
||||||
|
final image = await recorder.endRecording().toImage(width.toInt(), height.toInt());
|
||||||
|
return (await image.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders the skeleton drawable's current pose to a [RawImageData], with the given [width] and [height].
|
||||||
|
/// Uses [bgColor], a 32-bit ARGB color value, to paint the background.
|
||||||
|
/// Scales and centers the skeleton to fit the within the bounds of [width] and [height].
|
||||||
|
Future<RawImageData> renderToRawImageData(double width, double height, int bgColor) async {
|
||||||
|
final recorder = renderToPictureRecorder(width, height, bgColor);
|
||||||
|
var rawImageData = (await (await recorder.endRecording().toImage(
|
||||||
|
width.toInt(),
|
||||||
|
height.toInt(),
|
||||||
|
))
|
||||||
|
.toByteData(format: ImageByteFormat.rawRgba))!
|
||||||
|
.buffer
|
||||||
|
.asUint8List();
|
||||||
|
return RawImageData(rawImageData, width.toInt(), height.toInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a listener for all animation state events
|
||||||
|
void setListener(AnimationStateListener? listener) {
|
||||||
|
animationState.setListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
/// is no longer in use.
|
||||||
|
void dispose() {
|
||||||
|
if (_disposed) return;
|
||||||
|
_disposed = true;
|
||||||
|
_drawable.dispose();
|
||||||
|
|
||||||
|
if (_ownsAtlasAndSkeletonData) {
|
||||||
|
atlasFlutter.dispose();
|
||||||
|
skeletonData.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders debug information for a [SkeletonDrawableFlutter], like bone locations, to a [Canvas].
|
||||||
|
/// See [DebugRenderer.render].
|
||||||
|
class DebugRenderer {
|
||||||
|
const DebugRenderer();
|
||||||
|
|
||||||
|
void render(SkeletonDrawableFlutter drawable, Canvas canvas, List<RenderCommandFlutter> commands) {
|
||||||
|
final bonePaint = Paint()
|
||||||
|
..color = material.Colors.blue
|
||||||
|
..style = PaintingStyle.fill;
|
||||||
|
for (final bone in drawable.skeleton.bones) {
|
||||||
|
if (bone == null) continue;
|
||||||
|
canvas.drawRect(
|
||||||
|
Rect.fromCenter(center: Offset(bone.appliedPose.worldX, bone.appliedPose.worldY), width: 5, height: 5),
|
||||||
|
bonePaint,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ void main() async {
|
|||||||
print('Testing SkeletonDrawable and event listeners...');
|
print('Testing SkeletonDrawable and event listeners...');
|
||||||
|
|
||||||
// Initialize with debug extension enabled
|
// Initialize with debug extension enabled
|
||||||
await initSpineDart(enableMemoryDebugging: true);
|
await initSpineDart(enableMemoryDebugging: false);
|
||||||
|
|
||||||
// Load atlas and skeleton data
|
// Load atlas and skeleton data
|
||||||
final atlasData = File('../example/assets/spineboy.atlas').readAsStringSync();
|
final atlasData = File('../example/assets/spineboy.atlas').readAsStringSync();
|
||||||
@ -17,11 +17,22 @@ void main() async {
|
|||||||
final drawable = SkeletonDrawable(skeletonData);
|
final drawable = SkeletonDrawable(skeletonData);
|
||||||
print('SkeletonDrawable created successfully');
|
print('SkeletonDrawable created successfully');
|
||||||
|
|
||||||
|
// Test skeleton bounds
|
||||||
|
print('\nTesting skeleton bounds:');
|
||||||
|
final bounds = drawable.skeleton.bounds;
|
||||||
|
print(' Initial bounds: $bounds');
|
||||||
|
|
||||||
|
// Set skeleton to pose and update bounds
|
||||||
|
drawable.skeleton.setupPose();
|
||||||
|
drawable.skeleton.updateWorldTransform(Physics.none);
|
||||||
|
final boundsAfterPose = drawable.skeleton.bounds;
|
||||||
|
print(' Bounds after setupPose: $boundsAfterPose');
|
||||||
|
|
||||||
// Track events
|
// Track events
|
||||||
final events = <String>[];
|
final events = <String>[];
|
||||||
|
|
||||||
// Set global animation state listener
|
// Set global animation state listener
|
||||||
drawable.setListener((type, entry, event) {
|
drawable.animationState.setListener((type, entry, event) {
|
||||||
final eventName = event?.data.name ?? 'null';
|
final eventName = event?.data.name ?? 'null';
|
||||||
events.add('State: $type, event: $eventName');
|
events.add('State: $type, event: $eventName');
|
||||||
});
|
});
|
||||||
@ -30,12 +41,15 @@ void main() async {
|
|||||||
final trackEntry = drawable.animationState.setAnimation(0, 'walk', true);
|
final trackEntry = drawable.animationState.setAnimation(0, 'walk', true);
|
||||||
print('Set animation: walk');
|
print('Set animation: walk');
|
||||||
|
|
||||||
// Set track entry specific listener
|
// Set track entry specific listener (no drawable needed!)
|
||||||
trackEntry.setListener(drawable, (type, entry, event) {
|
print('TrackEntry.animationState: ${trackEntry.animationState}');
|
||||||
|
trackEntry.setListener((type, entry, event) {
|
||||||
final eventName = event?.data.name ?? 'null';
|
final eventName = event?.data.name ?? 'null';
|
||||||
events.add('TrackEntry: $type, event: $eventName');
|
events.add('TrackEntry: $type, event: $eventName');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
print('TrackEntry listener set for: ${trackEntry.hashCode}');
|
||||||
|
|
||||||
// Update several times to trigger events
|
// Update several times to trigger events
|
||||||
print('\nUpdating animation state...');
|
print('\nUpdating animation state...');
|
||||||
for (int i = 0; i < 5; i++) {
|
for (int i = 0; i < 5; i++) {
|
||||||
@ -63,11 +77,33 @@ void main() async {
|
|||||||
print(' $event');
|
print(' $event');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test bounds after animation updates
|
||||||
|
print('\nTesting bounds after animation:');
|
||||||
|
drawable.skeleton.updateWorldTransform(Physics.none);
|
||||||
|
final boundsAfterAnimation = drawable.skeleton.bounds;
|
||||||
|
print(' Bounds after animation: $boundsAfterAnimation');
|
||||||
|
|
||||||
|
// Test with different animations that might have different bounds
|
||||||
|
print('\nTesting bounds with jump animation:');
|
||||||
|
drawable.animationState.setAnimation(0, 'jump', false);
|
||||||
|
drawable.update(0.5); // Update to middle of jump
|
||||||
|
drawable.skeleton.updateWorldTransform(Physics.none);
|
||||||
|
final boundsInJump = drawable.skeleton.bounds;
|
||||||
|
print(' Bounds during jump: $boundsInJump');
|
||||||
|
|
||||||
|
// Check manager state before cleanup
|
||||||
|
print('\nBefore cleanup:');
|
||||||
|
AnimationStateEventManager.instance.debugPrint();
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
drawable.dispose();
|
drawable.dispose();
|
||||||
skeletonData.dispose();
|
skeletonData.dispose();
|
||||||
atlas.dispose();
|
atlas.dispose();
|
||||||
|
|
||||||
|
// Check manager state after cleanup
|
||||||
|
print('\nAfter cleanup:');
|
||||||
|
AnimationStateEventManager.instance.debugPrint();
|
||||||
|
|
||||||
// Report memory leaks
|
// Report memory leaks
|
||||||
reportLeaks();
|
reportLeaks();
|
||||||
print('\nTest complete');
|
print('\nTest complete');
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user