mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2026-03-06 10:46:53 +08:00
Add IKFollowing sample and extend SpineController functionality
This commit is contained in:
parent
2c8dd49a85
commit
4903ee361f
@ -0,0 +1,110 @@
|
|||||||
|
package com.esotericsoftware.spine
|
||||||
|
|
||||||
|
import android.graphics.Point
|
||||||
|
import androidx.compose.foundation.gestures.detectDragGestures
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.ArrowBack
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import com.badlogic.gdx.math.Vector2
|
||||||
|
import com.esotericsoftware.spine.android.SpineController
|
||||||
|
import com.esotericsoftware.spine.android.SpineView
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun IKFollowing(nav: NavHostController) {
|
||||||
|
|
||||||
|
val containerHeight = remember { mutableIntStateOf(0) }
|
||||||
|
val dragPosition = remember { mutableStateOf(Point(0, 0)) }
|
||||||
|
val crossHairPosition = remember { mutableStateOf<Point?>(null) }
|
||||||
|
|
||||||
|
val controller = remember {
|
||||||
|
SpineController.Builder()
|
||||||
|
.setOnInitialized {
|
||||||
|
it.animationState.setAnimation(0, "walk", true)
|
||||||
|
it.animationState.setAnimation(1, "aim", true)
|
||||||
|
}
|
||||||
|
.setOnAfterUpdateWorldTransforms {
|
||||||
|
val worldPosition = crossHairPosition.value ?: return@setOnAfterUpdateWorldTransforms
|
||||||
|
val skeleton = it.skeleton
|
||||||
|
val bone = skeleton.findBone("crosshair") ?: return@setOnAfterUpdateWorldTransforms
|
||||||
|
val parent = bone.parent ?: return@setOnAfterUpdateWorldTransforms
|
||||||
|
val position = parent.worldToLocal(Vector2(worldPosition.x.toFloat(), worldPosition.y.toFloat()))
|
||||||
|
bone.x = position.x
|
||||||
|
bone.y = position.y
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text(text = Destination.SimpleAnimation.title) },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton({ nav.navigateUp() }) {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.ArrowBack,
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
Box(modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
.onGloballyPositioned { coordinates ->
|
||||||
|
containerHeight.intValue = coordinates.size.height
|
||||||
|
}
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectDragGestures(
|
||||||
|
onDragStart = { offset ->
|
||||||
|
dragPosition.value = Point(offset.x.toInt(), offset.y.toInt())
|
||||||
|
},
|
||||||
|
onDrag = { _, dragAmount ->
|
||||||
|
dragPosition.value = Point(
|
||||||
|
(dragPosition.value.x + dragAmount.x).toInt(),
|
||||||
|
(dragPosition.value.y + dragAmount.y).toInt()
|
||||||
|
)
|
||||||
|
val invertedYDragPosition = Point(
|
||||||
|
dragPosition.value.x,
|
||||||
|
containerHeight.intValue - dragPosition.value.y,
|
||||||
|
)
|
||||||
|
crossHairPosition.value = controller.toSkeletonCoordinates(
|
||||||
|
invertedYDragPosition
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
AndroidView(
|
||||||
|
factory = { ctx ->
|
||||||
|
SpineView(ctx).apply {
|
||||||
|
loadFromAsset(
|
||||||
|
"spineboy.atlas",
|
||||||
|
"spineboy-pro.json",
|
||||||
|
controller
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -59,7 +59,8 @@ fun AppContent() {
|
|||||||
navController,
|
navController,
|
||||||
listOf(
|
listOf(
|
||||||
Destination.SimpleAnimation,
|
Destination.SimpleAnimation,
|
||||||
Destination.PlayPause
|
Destination.PlayPause,
|
||||||
|
Destination.IKFollowing
|
||||||
),
|
),
|
||||||
paddingValues
|
paddingValues
|
||||||
)
|
)
|
||||||
@ -77,6 +78,12 @@ fun AppContent() {
|
|||||||
) {
|
) {
|
||||||
PlayPause(navController)
|
PlayPause(navController)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
Destination.IKFollowing.route
|
||||||
|
) {
|
||||||
|
IKFollowing(navController)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -113,4 +120,5 @@ sealed class Destination(val route: String, val title: String) {
|
|||||||
data object Samples: Destination("samples", "Spine Android Examples")
|
data object Samples: Destination("samples", "Spine Android Examples")
|
||||||
data object SimpleAnimation : Destination("simpleAnimation", "Simple Animation")
|
data object SimpleAnimation : Destination("simpleAnimation", "Simple Animation")
|
||||||
data object PlayPause : Destination("playPause", "Play/Pause")
|
data object PlayPause : Destination("playPause", "Play/Pause")
|
||||||
|
data object IKFollowing : Destination("ikFollowing", "IK Following")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,9 +26,12 @@ fun PlayPause(
|
|||||||
nav: NavHostController
|
nav: NavHostController
|
||||||
) {
|
) {
|
||||||
val controller = remember {
|
val controller = remember {
|
||||||
SpineController {
|
SpineController.Builder()
|
||||||
it.animationState.setAnimation(0, "flying", true)
|
.setOnInitialized {
|
||||||
}
|
it.animationState.setAnimation(0, "flying", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
val isPlaying = remember { mutableStateOf(controller.isPlaying) }
|
val isPlaying = remember { mutableStateOf(controller.isPlaying) }
|
||||||
|
|||||||
@ -40,9 +40,11 @@ fun SimpleAnimation(nav: NavHostController) {
|
|||||||
loadFromAsset(
|
loadFromAsset(
|
||||||
"spineboy.atlas",
|
"spineboy.atlas",
|
||||||
"spineboy-pro.json",
|
"spineboy-pro.json",
|
||||||
SpineController {
|
SpineController.Builder()
|
||||||
it.animationState.setAnimation(0, "walk", true)
|
.setOnInitialized {
|
||||||
}
|
it.animationState.setAnimation(0, "walk", true)
|
||||||
|
}
|
||||||
|
.build()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
package com.esotericsoftware.spine.android;
|
package com.esotericsoftware.spine.android;
|
||||||
|
|
||||||
|
import android.graphics.Point;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.esotericsoftware.spine.AnimationState;
|
import com.esotericsoftware.spine.AnimationState;
|
||||||
import com.esotericsoftware.spine.AnimationStateData;
|
import com.esotericsoftware.spine.AnimationStateData;
|
||||||
import com.esotericsoftware.spine.Skeleton;
|
import com.esotericsoftware.spine.Skeleton;
|
||||||
@ -7,18 +11,51 @@ import com.esotericsoftware.spine.SkeletonData;
|
|||||||
import com.esotericsoftware.spine.android.utils.SpineControllerCallback;
|
import com.esotericsoftware.spine.android.utils.SpineControllerCallback;
|
||||||
|
|
||||||
public class SpineController {
|
public class SpineController {
|
||||||
private final SpineControllerCallback onInitialized;
|
|
||||||
private AndroidSkeletonDrawable drawable;
|
|
||||||
|
|
||||||
private boolean playing = true;
|
public static class Builder {
|
||||||
|
private SpineControllerCallback onInitialized;
|
||||||
|
private SpineControllerCallback onBeforeUpdateWorldTransforms;
|
||||||
|
private SpineControllerCallback onAfterUpdateWorldTransforms;
|
||||||
|
|
||||||
public SpineController(SpineControllerCallback onInitialized) {
|
public Builder setOnInitialized(SpineControllerCallback onInitialized) {
|
||||||
this.onInitialized = onInitialized;
|
this.onInitialized = onInitialized;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setOnBeforeUpdateWorldTransforms(SpineControllerCallback onBeforeUpdateWorldTransforms) {
|
||||||
|
this.onBeforeUpdateWorldTransforms = onBeforeUpdateWorldTransforms;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setOnAfterUpdateWorldTransforms(SpineControllerCallback onAfterUpdateWorldTransforms) {
|
||||||
|
this.onAfterUpdateWorldTransforms = onAfterUpdateWorldTransforms;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SpineController build() {
|
||||||
|
SpineController spineController = new SpineController();
|
||||||
|
spineController.onInitialized = onInitialized;
|
||||||
|
spineController.onBeforeUpdateWorldTransforms = onBeforeUpdateWorldTransforms;
|
||||||
|
spineController.onAfterUpdateWorldTransforms = onAfterUpdateWorldTransforms;
|
||||||
|
return spineController;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private @Nullable SpineControllerCallback onInitialized;
|
||||||
|
private @Nullable SpineControllerCallback onBeforeUpdateWorldTransforms;
|
||||||
|
private @Nullable SpineControllerCallback onAfterUpdateWorldTransforms;
|
||||||
|
private AndroidSkeletonDrawable drawable;
|
||||||
|
private boolean playing = true;
|
||||||
|
private double offsetX = 0;
|
||||||
|
private double offsetY = 0;
|
||||||
|
private double scaleX = 1;
|
||||||
|
private double scaleY = 1;
|
||||||
|
|
||||||
protected void init(AndroidSkeletonDrawable drawable) {
|
protected void init(AndroidSkeletonDrawable drawable) {
|
||||||
this.drawable = drawable;
|
this.drawable = drawable;
|
||||||
onInitialized.execute(this);
|
if (onInitialized != null) {
|
||||||
|
onInitialized.execute(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public AndroidTextureAtlas getAtlas() {
|
public AndroidTextureAtlas getAtlas() {
|
||||||
@ -53,7 +90,7 @@ public class SpineController {
|
|||||||
|
|
||||||
public boolean isInitialized() {
|
public boolean isInitialized() {
|
||||||
return drawable != null;
|
return drawable != null;
|
||||||
};
|
}
|
||||||
|
|
||||||
public boolean isPlaying() {
|
public boolean isPlaying() {
|
||||||
return playing;
|
return playing;
|
||||||
@ -66,4 +103,29 @@ public class SpineController {
|
|||||||
public void resume() {
|
public void resume() {
|
||||||
playing = true;
|
playing = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Point toSkeletonCoordinates(Point position) {
|
||||||
|
int x = position.x;
|
||||||
|
int y = position.y;
|
||||||
|
return new Point((int) (x / scaleX - offsetX), (int) (y / scaleY - offsetY));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setCoordinateTransform(double offsetX, double offsetY, double scaleX, double scaleY) {
|
||||||
|
this.offsetX = offsetX;
|
||||||
|
this.offsetY = offsetY;
|
||||||
|
this.scaleX = scaleX;
|
||||||
|
this.scaleY = scaleY;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void callOnBeforeUpdateWorldTransforms() {
|
||||||
|
if (onBeforeUpdateWorldTransforms != null) {
|
||||||
|
onBeforeUpdateWorldTransforms.execute(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void callOnAfterUpdateWorldTransforms() {
|
||||||
|
if (onAfterUpdateWorldTransforms != null) {
|
||||||
|
onAfterUpdateWorldTransforms.execute(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -147,7 +147,9 @@ public class SpineView extends View implements Choreographer.FrameCallback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (controller.isPlaying()) {
|
if (controller.isPlaying()) {
|
||||||
|
controller.callOnBeforeUpdateWorldTransforms();
|
||||||
controller.getDrawable().update(delta);
|
controller.getDrawable().update(delta);
|
||||||
|
controller.callOnAfterUpdateWorldTransforms();
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas.save();
|
canvas.save();
|
||||||
@ -175,6 +177,8 @@ public class SpineView extends View implements Choreographer.FrameCallback {
|
|||||||
|
|
||||||
offsetX = (float) (getWidth() / 2.0 + (alignment.getX() * getWidth() / 2.0));
|
offsetX = (float) (getWidth() / 2.0 + (alignment.getX() * getWidth() / 2.0));
|
||||||
offsetY = (float) (getHeight() / 2.0 + (alignment.getY() * getHeight() / 2.0));
|
offsetY = (float) (getHeight() / 2.0 + (alignment.getY() * getHeight() / 2.0));
|
||||||
|
|
||||||
|
controller.setCoordinateTransform(x + offsetX / scaleX, y + offsetY / scaleY, scaleX, scaleY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Choreographer.FrameCallback
|
// Choreographer.FrameCallback
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user