Add IKFollowing sample and extend SpineController functionality

This commit is contained in:
Denis Andrasec 2024-07-04 15:10:48 +02:00
parent 2c8dd49a85
commit 4903ee361f
6 changed files with 203 additions and 14 deletions

View File

@ -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
)
}
}
)
}
}
}

View File

@ -59,7 +59,8 @@ fun AppContent() {
navController,
listOf(
Destination.SimpleAnimation,
Destination.PlayPause
Destination.PlayPause,
Destination.IKFollowing
),
paddingValues
)
@ -77,6 +78,12 @@ fun AppContent() {
) {
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 SimpleAnimation : Destination("simpleAnimation", "Simple Animation")
data object PlayPause : Destination("playPause", "Play/Pause")
data object IKFollowing : Destination("ikFollowing", "IK Following")
}

View File

@ -26,9 +26,12 @@ fun PlayPause(
nav: NavHostController
) {
val controller = remember {
SpineController {
it.animationState.setAnimation(0, "flying", true)
}
SpineController.Builder()
.setOnInitialized {
it.animationState.setAnimation(0, "flying", true)
}
.build()
}
val isPlaying = remember { mutableStateOf(controller.isPlaying) }

View File

@ -40,9 +40,11 @@ fun SimpleAnimation(nav: NavHostController) {
loadFromAsset(
"spineboy.atlas",
"spineboy-pro.json",
SpineController {
it.animationState.setAnimation(0, "walk", true)
}
SpineController.Builder()
.setOnInitialized {
it.animationState.setAnimation(0, "walk", true)
}
.build()
)
}
},

View File

@ -1,5 +1,9 @@
package com.esotericsoftware.spine.android;
import android.graphics.Point;
import androidx.annotation.Nullable;
import com.esotericsoftware.spine.AnimationState;
import com.esotericsoftware.spine.AnimationStateData;
import com.esotericsoftware.spine.Skeleton;
@ -7,18 +11,51 @@ import com.esotericsoftware.spine.SkeletonData;
import com.esotericsoftware.spine.android.utils.SpineControllerCallback;
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) {
this.onInitialized = onInitialized;
public Builder setOnInitialized(SpineControllerCallback 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) {
this.drawable = drawable;
onInitialized.execute(this);
if (onInitialized != null) {
onInitialized.execute(this);
}
}
public AndroidTextureAtlas getAtlas() {
@ -53,7 +90,7 @@ public class SpineController {
public boolean isInitialized() {
return drawable != null;
};
}
public boolean isPlaying() {
return playing;
@ -66,4 +103,29 @@ public class SpineController {
public void resume() {
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);
}
}
}

View File

@ -147,7 +147,9 @@ public class SpineView extends View implements Choreographer.FrameCallback {
}
if (controller.isPlaying()) {
controller.callOnBeforeUpdateWorldTransforms();
controller.getDrawable().update(delta);
controller.callOnAfterUpdateWorldTransforms();
}
canvas.save();
@ -175,6 +177,8 @@ public class SpineView extends View implements Choreographer.FrameCallback {
offsetX = (float) (getWidth() / 2.0 + (alignment.getX() * getWidth() / 2.0));
offsetY = (float) (getHeight() / 2.0 + (alignment.getY() * getHeight() / 2.0));
controller.setCoordinateTransform(x + offsetX / scaleX, y + offsetY / scaleY, scaleX, scaleY);
}
// Choreographer.FrameCallback