mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2025-12-21 01:36:02 +08:00
[flutter] Add SkeletonDrawable.renderToRawImageData(), clean-up dress up example.
This commit is contained in:
parent
dbc2e164f0
commit
b0c63b8fa8
@ -16,8 +16,7 @@ class DressUp extends StatefulWidget {
|
||||
|
||||
class DressUpState extends State<DressUp> {
|
||||
static const double thumbnailSize = 200;
|
||||
late SpineWidgetController _controller;
|
||||
SkeletonDrawable? _drawable;
|
||||
late SkeletonDrawable _drawable;
|
||||
Skin? _customSkin;
|
||||
final Map<String, RawImageData> _skinImages = {};
|
||||
final Map<String, bool> _selectedSkins = {};
|
||||
@ -27,42 +26,18 @@ class DressUpState extends State<DressUp> {
|
||||
reportLeaks();
|
||||
super.initState();
|
||||
SkeletonDrawable.fromAsset("assets/mix-and-match.atlas", "assets/mix-and-match-pro.skel").then((drawable) async {
|
||||
_drawable = drawable;
|
||||
for (var skin in drawable.skeletonData.getSkins()) {
|
||||
if (skin.getName() == "default") continue;
|
||||
|
||||
var skeleton = drawable.skeleton;
|
||||
skeleton.setSkin(skin);
|
||||
skeleton.setToSetupPose();
|
||||
skeleton.updateWorldTransform();
|
||||
var bounds = skeleton.getBounds();
|
||||
var scale = 1 / (bounds.width > bounds.height ? bounds.width / thumbnailSize : bounds.height / thumbnailSize);
|
||||
|
||||
var recorder = ui.PictureRecorder();
|
||||
var canvas = Canvas(recorder);
|
||||
var bgColor = Random().nextInt(0xffffffff) | 0xff0000000;
|
||||
var paint = Paint()
|
||||
..color = ui.Color(bgColor)
|
||||
..style = PaintingStyle.fill;
|
||||
canvas.drawRect(const Rect.fromLTWH(0, 0, thumbnailSize, thumbnailSize), paint);
|
||||
canvas.translate(thumbnailSize / 2, thumbnailSize / 2);
|
||||
canvas.scale(scale, scale);
|
||||
canvas.translate(-(bounds.x + bounds.width / 2), -(bounds.y + bounds.height / 2));
|
||||
canvas.drawRect(Rect.fromLTRB(-5, -5, 5, -5), paint..color = Colors.red);
|
||||
drawable.renderToCanvas(canvas);
|
||||
|
||||
var rawImageData = (await (await recorder.endRecording().toImage(thumbnailSize.toInt(), thumbnailSize.toInt())).toByteData(format: ui.ImageByteFormat.rawRgba))!.buffer.asUint8List();
|
||||
_skinImages[skin.getName()] = (RawImageData(rawImageData, thumbnailSize.toInt(), thumbnailSize.toInt()));
|
||||
_skinImages[skin.getName()] = await drawable.renderToRawImageData(thumbnailSize, thumbnailSize);
|
||||
_selectedSkins[skin.getName()] = false;
|
||||
}
|
||||
_drawable = drawable;
|
||||
_controller = SpineWidgetController(onInitialized: (controller) {
|
||||
controller.animationState.setAnimationByName(0, "dance", true);
|
||||
});
|
||||
setState(() {
|
||||
_selectedSkins["full-skins/girl"] = true;
|
||||
drawable.skeleton.setSkinByName("full-skins/girl");
|
||||
drawable.skeleton.setToSetupPose();
|
||||
});
|
||||
_toggleSkin("full-skins/girl");
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@ -72,16 +47,20 @@ class DressUpState extends State<DressUp> {
|
||||
_customSkin = Skin("custom-skin");
|
||||
for (var skinName in _selectedSkins.keys) {
|
||||
if (_selectedSkins[skinName] == true) {
|
||||
var skin = _controller.skeletonData.findSkin(skinName);
|
||||
var skin = _drawable.skeletonData.findSkin(skinName);
|
||||
if (skin != null) _customSkin?.addSkin(skin);
|
||||
}
|
||||
}
|
||||
_controller.skeleton.setSkin(_customSkin!);
|
||||
_controller.skeleton.setToSetupPose();
|
||||
_drawable.skeleton.setSkin(_customSkin!);
|
||||
_drawable.skeleton.setToSetupPose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = SpineWidgetController(onInitialized: (controller) {
|
||||
controller.animationState.setAnimationByName(0, "dance", true);
|
||||
});
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Dress Up')),
|
||||
body: _skinImages.isEmpty
|
||||
@ -107,7 +86,7 @@ class DressUpState extends State<DressUp> {
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SpineWidget.drawable(_drawable, _controller, boundsProvider: SkinAndAnimationBounds(skins: ["full-skins/girl"]),)
|
||||
child: SpineWidget.drawable(_drawable, controller, boundsProvider: SkinAndAnimationBounds(skins: ["full-skins/girl"]),)
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
@ -37,9 +37,8 @@ class SpineComponent extends PositionComponent {
|
||||
Iterable<Component>? children,
|
||||
int? priority,
|
||||
}) async {
|
||||
final drawable = await SkeletonDrawable.fromAsset(atlasFile, skeletonFile, bundle: bundle);
|
||||
return SpineComponent(
|
||||
drawable,
|
||||
await SkeletonDrawable.fromAsset(atlasFile, skeletonFile, bundle: bundle),
|
||||
ownsDrawable: true,
|
||||
boundsProvider: boundsProvider,
|
||||
position: position,
|
||||
@ -54,6 +53,11 @@ class SpineComponent extends PositionComponent {
|
||||
if (_ownsDrawable) {
|
||||
_drawable.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void onDetach() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@ -133,9 +137,11 @@ class PreloadAndShareSpineDataExample extends FlameGame {
|
||||
|
||||
@override
|
||||
void onDetach() {
|
||||
// Dispose the pre-loaded atlas and skeleton data when the game/scene is removed
|
||||
// Dispose the pre-loaded atlas and skeleton data when the game/scene is removed.
|
||||
cachedAtlas.dispose();
|
||||
cachedSkeletonData.dispose();
|
||||
|
||||
// Dispose each spineboy and its internal SkeletonDrawable.
|
||||
for (final spineboy in spineboys) {
|
||||
spineboy.dispose();
|
||||
}
|
||||
|
||||
@ -6,7 +6,6 @@ import 'flame_example.dart';
|
||||
import 'simple_animation.dart';
|
||||
import 'animation_state_events.dart';
|
||||
import 'pause_play_animation.dart';
|
||||
import 'skins.dart';
|
||||
import 'dress_up.dart';
|
||||
import 'ik_following.dart';
|
||||
|
||||
@ -70,18 +69,6 @@ class ExampleSelector extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
spacer,
|
||||
ElevatedButton(
|
||||
child: const Text('Skins'),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => const Skins(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
spacer,
|
||||
ElevatedButton(
|
||||
child: const Text('Dress Up'),
|
||||
onPressed: () {
|
||||
|
||||
@ -1,85 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:esotericsoftware_spine_flutter/spine_flutter.dart';
|
||||
|
||||
class Skins extends StatefulWidget {
|
||||
const Skins({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
SkinsState createState() => SkinsState();
|
||||
}
|
||||
|
||||
class SkinsState extends State<Skins> {
|
||||
SkeletonDrawable? _drawable;
|
||||
late SpineWidgetController _controller;
|
||||
final Map<String, bool> _selectedSkins = {};
|
||||
Skin? _customSkin;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
SkeletonDrawable.fromAsset("assets/mix-and-match.atlas", "assets/mix-and-match-pro.skel").then((drawable) {
|
||||
for (var skin in drawable.skeletonData.getSkins()) {
|
||||
_selectedSkins[skin.getName()] = false;
|
||||
}
|
||||
_controller = SpineWidgetController(onInitialized: (controller) {
|
||||
controller.animationState.setAnimationByName(0, "walk", true);
|
||||
});
|
||||
drawable.skeleton.setSkinByName("full-skins/girl");
|
||||
_selectedSkins["full-skins/girl"] = true;
|
||||
_drawable = drawable;
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
void _toggleSkin(String skinName) {
|
||||
_selectedSkins[skinName] = !_selectedSkins[skinName]!;
|
||||
|
||||
if (_customSkin != null) _customSkin?.dispose();
|
||||
|
||||
_customSkin = Skin("custom-skin");
|
||||
for (var skinName in _selectedSkins.keys) {
|
||||
if (_selectedSkins[skinName] == true) {
|
||||
var skin = _controller.skeletonData.findSkin(skinName);
|
||||
if (skin != null) _customSkin?.addSkin(skin);
|
||||
}
|
||||
}
|
||||
_controller.skeleton.setSkin(_customSkin!);
|
||||
_controller.skeleton.setToSetupPose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Skins')),
|
||||
body: _drawable == null
|
||||
? const SizedBox()
|
||||
: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child:ListView(
|
||||
children: _selectedSkins.keys.map((skinName) {
|
||||
return CheckboxListTile(
|
||||
title: Text(skinName),
|
||||
value: _selectedSkins[skinName],
|
||||
onChanged: (bool? value) {
|
||||
_toggleSkin(skinName);
|
||||
setState(() => {});
|
||||
},
|
||||
);
|
||||
}).toList()
|
||||
)
|
||||
),
|
||||
Expanded(
|
||||
child: SpineWidget.drawable(_drawable, _controller, boundsProvider: SkinAndAnimationBounds(skins: ["full-skins/girl"]))
|
||||
)
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_drawable?.dispose();
|
||||
}
|
||||
}
|
||||
@ -132,15 +132,8 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.2"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
raw_image_provider:
|
||||
dependency: "direct main"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: raw_image_provider
|
||||
url: "https://pub.dartlang.org"
|
||||
|
||||
@ -13,7 +13,6 @@ dependencies:
|
||||
esotericsoftware_spine_flutter:
|
||||
path: ../
|
||||
cupertino_icons: ^1.0.2
|
||||
raw_image_provider: ^0.2.0
|
||||
flame: ^1.4.0
|
||||
|
||||
dev_dependencies:
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import 'dart:convert' as convert;
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui';
|
||||
|
||||
@ -8,6 +9,7 @@ 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;
|
||||
import 'package:raw_image_provider/raw_image_provider.dart';
|
||||
|
||||
import 'spine_flutter_bindings_generated.dart';
|
||||
import 'ffi_proxy.dart';
|
||||
@ -2595,7 +2597,6 @@ class Skeleton {
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME expose timelines and apply?
|
||||
class Animation {
|
||||
final spine_animation _animation;
|
||||
|
||||
@ -3307,6 +3308,26 @@ class SkeletonDrawable {
|
||||
}
|
||||
}
|
||||
|
||||
Future<RawImageData> renderToRawImageData(double width, double height) async {
|
||||
var bounds = skeleton.getBounds();
|
||||
var scale = 1 / (bounds.width > bounds.height ? bounds.width / width : bounds.height / height);
|
||||
|
||||
var recorder = PictureRecorder();
|
||||
var canvas = Canvas(recorder);
|
||||
var bgColor = Random().nextInt(0xffffffff) | 0xff0000000;
|
||||
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));
|
||||
canvas.drawRect(const Rect.fromLTRB(-5, -5, 5, -5), paint..color = material.Colors.red);
|
||||
renderToCanvas(canvas);
|
||||
var rawImageData = (await (await recorder.endRecording().toImage(width.toInt(), height.toInt())).toByteData(format: ImageByteFormat.rawRgba))!.buffer.asUint8List();
|
||||
return RawImageData(rawImageData, width.toInt(), height.toInt());
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
@ -106,17 +106,18 @@ class SkinAndAnimationBounds extends BoundsProvider {
|
||||
|
||||
@override
|
||||
Bounds computeBounds(SkeletonDrawable drawable) {
|
||||
var data = drawable.skeletonData;
|
||||
var customSkin = Skin("custom-skin");
|
||||
for (var skinName in skins) {
|
||||
var skin = data.findSkin(skinName);
|
||||
final data = drawable.skeletonData;
|
||||
final oldSkin = drawable.skeleton.getSkin();
|
||||
final customSkin = Skin("custom-skin");
|
||||
for (final skinName in skins) {
|
||||
final skin = data.findSkin(skinName);
|
||||
if (skin == null) continue;
|
||||
customSkin.addSkin(skin);
|
||||
}
|
||||
drawable.skeleton.setSkin(customSkin);
|
||||
drawable.skeleton.setToSetupPose();
|
||||
|
||||
final animation = data.findAnimation(this.animation!);
|
||||
final animation = this.animation != null ? data.findAnimation(this.animation!) : null;
|
||||
double minX = double.infinity;
|
||||
double minY = double.infinity;
|
||||
double maxX = double.negativeInfinity;
|
||||
@ -142,6 +143,7 @@ class SkinAndAnimationBounds extends BoundsProvider {
|
||||
customSkin.dispose();
|
||||
drawable.skeleton.setSkinByName("default");
|
||||
drawable.animationState.clearTracks();
|
||||
if (oldSkin != null) drawable.skeleton.setSkin(oldSkin);
|
||||
drawable.skeleton.setToSetupPose();
|
||||
drawable.update(0);
|
||||
return Bounds(minX, minY, maxX - minX, maxY - minY);
|
||||
|
||||
@ -13,7 +13,6 @@ environment:
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
plugin_platform_interface: ^2.0.2
|
||||
ffi: ^2.0.1
|
||||
web_ffi_fork: ^0.7.4
|
||||
inject_js: ^2.0.0
|
||||
@ -21,6 +20,7 @@ dependencies:
|
||||
meta: ^1.3.0
|
||||
http: ^0.13.5
|
||||
path: ^1.8.2
|
||||
raw_image_provider: ^0.2.0
|
||||
|
||||
dev_dependencies:
|
||||
ffigen: ^6.1.2
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user