mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2026-02-06 07:14:55 +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,
|
||||
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")
|
||||
}
|
||||
|
||||
@ -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) }
|
||||
|
||||
@ -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()
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user