diff --git a/spine-android/app/build.gradle.kts b/spine-android/app/build.gradle.kts index 14a051971..d3fa4cbee 100644 --- a/spine-android/app/build.gradle.kts +++ b/spine-android/app/build.gradle.kts @@ -59,6 +59,7 @@ dependencies { implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) + implementation(libs.androidx.navigation.compose) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) diff --git a/spine-android/app/src/main/assets/dragon-ess.skel b/spine-android/app/src/main/assets/dragon-ess.skel new file mode 100644 index 000000000..168a2f3c7 Binary files /dev/null and b/spine-android/app/src/main/assets/dragon-ess.skel differ diff --git a/spine-android/app/src/main/assets/dragon.atlas b/spine-android/app/src/main/assets/dragon.atlas new file mode 100644 index 000000000..47b3f3b2a --- /dev/null +++ b/spine-android/app/src/main/assets/dragon.atlas @@ -0,0 +1,112 @@ +dragon.png + size: 1024, 1024 + filter: Linear, Linear +front-toe-a + bounds: 797, 381, 29, 50 +front-toe-b + bounds: 942, 118, 56, 57 +head + bounds: 647, 81, 296, 260 + rotate: 90 +left-front-leg + bounds: 942, 250, 84, 57 + rotate: 90 +left-front-thigh + bounds: 852, 7, 84, 72 +left-wing01 + bounds: 736, 433, 264, 589 +right-rear-toe + bounds: 647, 2, 109, 77 +right-wing01 + bounds: 2, 379, 365, 643 +right-wing02 + bounds: 369, 379, 365, 643 +right-wing03 + bounds: 2, 12, 365, 643 + rotate: 90 +tail03 + bounds: 758, 6, 73, 92 + rotate: 90 +tail04 + bounds: 942, 177, 56, 71 +tail05 + bounds: 736, 379, 52, 59 + rotate: 90 +tail06 + bounds: 942, 336, 95, 68 + rotate: 90 +thiagobrayner + bounds: 909, 81, 350, 31 + rotate: 90 + +dragon_2.png + size: 1024, 1024 + filter: Linear, Linear +back + bounds: 795, 32, 190, 185 +chin + bounds: 647, 157, 214, 146 + rotate: 90 +left-rear-leg + bounds: 795, 219, 206, 177 + rotate: 90 +left-wing02 + bounds: 736, 427, 264, 589 +right-wing04 + bounds: 2, 373, 365, 643 +right-wing05 + bounds: 369, 373, 365, 643 +right-wing06 + bounds: 2, 6, 365, 643 + rotate: 90 +tail01 + bounds: 647, 2, 120, 153 + +dragon_3.png + size: 1024, 1024 + filter: Linear, Linear +chest + bounds: 740, 299, 136, 122 +left-rear-thigh + bounds: 647, 218, 91, 149 +left-wing03 + bounds: 736, 423, 264, 589 +right-front-leg + bounds: 850, 196, 101, 89 + rotate: 90 +right-front-thigh + bounds: 740, 189, 108, 108 +right-rear-leg + bounds: 878, 321, 116, 100 +right-rear-thigh + bounds: 647, 67, 91, 149 +right-wing07 + bounds: 2, 369, 365, 643 +right-wing08 + bounds: 369, 369, 365, 643 +right-wing09 + bounds: 2, 2, 365, 643 + rotate: 90 +tail02 + bounds: 740, 67, 95, 120 + +dragon_4.png + size: 1024, 1024 + filter: Linear, Linear +left-wing04 + bounds: 2, 268, 264, 589 +left-wing05 + bounds: 268, 268, 264, 589 +left-wing06 + bounds: 534, 268, 264, 589 +left-wing07 + bounds: 2, 2, 264, 589 + rotate: 90 + +dragon_5.png + size: 1024, 1024 + filter: Linear, Linear +left-wing08 + bounds: 2, 2, 264, 589 +left-wing09 + bounds: 268, 2, 264, 589 diff --git a/spine-android/app/src/main/assets/dragon.png b/spine-android/app/src/main/assets/dragon.png new file mode 100644 index 000000000..3bca338cd Binary files /dev/null and b/spine-android/app/src/main/assets/dragon.png differ diff --git a/spine-android/app/src/main/assets/dragon_2.png b/spine-android/app/src/main/assets/dragon_2.png new file mode 100644 index 000000000..3dbe2a5a0 Binary files /dev/null and b/spine-android/app/src/main/assets/dragon_2.png differ diff --git a/spine-android/app/src/main/assets/dragon_3.png b/spine-android/app/src/main/assets/dragon_3.png new file mode 100644 index 000000000..df2896d51 Binary files /dev/null and b/spine-android/app/src/main/assets/dragon_3.png differ diff --git a/spine-android/app/src/main/assets/dragon_4.png b/spine-android/app/src/main/assets/dragon_4.png new file mode 100644 index 000000000..270b0959d Binary files /dev/null and b/spine-android/app/src/main/assets/dragon_4.png differ diff --git a/spine-android/app/src/main/assets/dragon_5.png b/spine-android/app/src/main/assets/dragon_5.png new file mode 100644 index 000000000..4e9f7a843 Binary files /dev/null and b/spine-android/app/src/main/assets/dragon_5.png differ diff --git a/spine-android/app/src/main/java/com/esotericsoftware/spine/MainActivity.kt b/spine-android/app/src/main/java/com/esotericsoftware/spine/MainActivity.kt index 8704e7cde..c053e1a02 100644 --- a/spine-android/app/src/main/java/com/esotericsoftware/spine/MainActivity.kt +++ b/spine-android/app/src/main/java/com/esotericsoftware/spine/MainActivity.kt @@ -3,15 +3,27 @@ package com.esotericsoftware.spine import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.viewinterop.AndroidView -import com.esotericsoftware.spine.android.SpineController -import com.esotericsoftware.spine.android.SpineView +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController import com.esotericsoftware.spine.ui.theme.SpineAndroidExamplesTheme class MainActivity : ComponentActivity() { @@ -23,34 +35,82 @@ class MainActivity : ComponentActivity() { } } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun AppContent() { + val navController = rememberNavController() + SpineAndroidExamplesTheme { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { - Box { - SpineViewComposable() + NavHost( + navController = navController, + startDestination = Destination.Samples.route + ) { + composable( + Destination.Samples.route + ) { + Scaffold( + topBar = { TopAppBar(title = { Text(text = Destination.Samples.title) }) } + ) { paddingValues -> + Samples( + navController, + listOf( + Destination.SimpleAnimation, + Destination.PlayPause + ), + paddingValues + ) + } + } + + composable( + Destination.SimpleAnimation.route + ) { + SimpleAnimation(navController) + } + + composable( + Destination.PlayPause.route + ) { + PlayPause(navController) + } } } } } @Composable -fun SpineViewComposable(modifier: Modifier = Modifier.fillMaxSize()) { - AndroidView( - factory = { ctx -> - SpineView(ctx).apply { - loadFromAsset( - "spineboy.atlas", - "spineboy-pro.json", - SpineController { - it.animationState.setAnimation(0, "walk", true) - } - ) +fun Samples( + nav: NavHostController, + samples: List, + paddingValues: PaddingValues +) { + LazyColumn( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .padding(8.dp) + .padding(paddingValues) + ) { + samples.forEach { + item { + Card( + Modifier + .fillMaxWidth() + .clickable(onClick = { nav.navigate(it.route) }), + shape = MaterialTheme.shapes.large + ) { + Text(text = it.title, Modifier.padding(24.dp)) + } } - }, - modifier = modifier - ) + } + } +} + +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") } diff --git a/spine-android/app/src/main/java/com/esotericsoftware/spine/PlayPause.kt b/spine-android/app/src/main/java/com/esotericsoftware/spine/PlayPause.kt new file mode 100644 index 000000000..8fc9e489a --- /dev/null +++ b/spine-android/app/src/main/java/com/esotericsoftware/spine/PlayPause.kt @@ -0,0 +1,73 @@ +package com.esotericsoftware.spine + +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ArrowBack +import androidx.compose.material3.Button +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.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView +import androidx.navigation.NavHostController +import com.esotericsoftware.spine.android.SpineController +import com.esotericsoftware.spine.android.SpineView + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PlayPause( + nav: NavHostController +) { + val controller = remember { + SpineController { + it.animationState.setAnimation(0, "flying", true) + } + } + + val isPlaying = remember { mutableStateOf(controller.isPlaying) } + + Scaffold( + topBar = { + TopAppBar( + title = { Text(text = Destination.PlayPause.title) }, + navigationIcon = { + IconButton({ nav.navigateUp() }) { + Icon( + Icons.Rounded.ArrowBack, + null, + ) + } + }, + actions = { + Button(onClick = { + if (controller.isPlaying) controller.pause() else controller.resume() + isPlaying.value = controller.isPlaying + }) { + Text(text = if (isPlaying.value) "Pause" else "Play") + } + } + ) + } + ) { paddingValues -> + + AndroidView( + factory = { ctx -> + SpineView(ctx).apply { + loadFromAsset( + "dragon.atlas", + "dragon-ess.skel", + controller + ) + } + }, + modifier = Modifier.padding(paddingValues) + ) + } + +} \ No newline at end of file diff --git a/spine-android/app/src/main/java/com/esotericsoftware/spine/SimpleAnimation.kt b/spine-android/app/src/main/java/com/esotericsoftware/spine/SimpleAnimation.kt new file mode 100644 index 000000000..9876fef2c --- /dev/null +++ b/spine-android/app/src/main/java/com/esotericsoftware/spine/SimpleAnimation.kt @@ -0,0 +1,52 @@ +package com.esotericsoftware.spine + +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.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView +import androidx.navigation.NavHostController +import com.esotericsoftware.spine.android.SpineController +import com.esotericsoftware.spine.android.SpineView + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SimpleAnimation(nav: NavHostController) { + Scaffold( + topBar = { + TopAppBar( + title = { Text(text = Destination.SimpleAnimation.title) }, + navigationIcon = { + IconButton({ nav.navigateUp() }) { + Icon( + Icons.Rounded.ArrowBack, + null, + ) + } + } + ) + } + ) { paddingValues -> + AndroidView( + factory = { ctx -> + SpineView(ctx).apply { + loadFromAsset( + "spineboy.atlas", + "spineboy-pro.json", + SpineController { + it.animationState.setAnimation(0, "walk", true) + } + ) + } + }, + modifier = Modifier.padding(paddingValues) + ) + } +} diff --git a/spine-android/gradle/libs.versions.toml b/spine-android/gradle/libs.versions.toml index 5c964c35c..54793659d 100644 --- a/spine-android/gradle/libs.versions.toml +++ b/spine-android/gradle/libs.versions.toml @@ -10,6 +10,7 @@ activityCompose = "1.7.0" composeBom = "2023.08.00" appcompat = "1.6.1" material = "1.10.0" +navigationCompose = "2.7.7" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -28,6 +29,7 @@ androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit androidx-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } +androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } diff --git a/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/SpineView.java b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/SpineView.java index f863d3e0b..c00cc2fb3 100644 --- a/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/SpineView.java +++ b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/SpineView.java @@ -116,7 +116,9 @@ public class SpineView extends View implements Choreographer.FrameCallback { return; } - controller.getDrawable().update(delta); + if (controller.isPlaying()) { + controller.getDrawable().update(delta); + } canvas.save(); canvas.translate(offsetX, offsetY);