Эх сурвалжийг харах

Merge branch 'spine-android' into 4.2

Mario Zechner 1 жил өмнө
parent
commit
5d1f4d2c71
97 өөрчлөгдсөн 6618 нэмэгдсэн , 3 устгасан
  1. 15 0
      spine-android/.gitignore
  2. 1 0
      spine-android/app/.gitignore
  3. 76 0
      spine-android/app/build.gradle.kts
  4. 21 0
      spine-android/app/proguard-rules.pro
  5. 24 0
      spine-android/app/src/androidTest/java/com/esotericsoftware/android/ExampleInstrumentedTest.kt
  6. 34 0
      spine-android/app/src/main/AndroidManifest.xml
  7. BIN
      spine-android/app/src/main/assets/celestial-circus-pro.skel
  8. 173 0
      spine-android/app/src/main/assets/celestial-circus.atlas
  9. BIN
      spine-android/app/src/main/assets/celestial-circus.png
  10. BIN
      spine-android/app/src/main/assets/dragon-ess.skel
  11. 112 0
      spine-android/app/src/main/assets/dragon.atlas
  12. BIN
      spine-android/app/src/main/assets/dragon.png
  13. BIN
      spine-android/app/src/main/assets/dragon_2.png
  14. BIN
      spine-android/app/src/main/assets/dragon_3.png
  15. BIN
      spine-android/app/src/main/assets/dragon_4.png
  16. BIN
      spine-android/app/src/main/assets/dragon_5.png
  17. BIN
      spine-android/app/src/main/assets/mix-and-match-pro.skel
  18. 358 0
      spine-android/app/src/main/assets/mix-and-match.atlas
  19. BIN
      spine-android/app/src/main/assets/mix-and-match.png
  20. 557 0
      spine-android/app/src/main/assets/spineboy-pro.json
  21. BIN
      spine-android/app/src/main/assets/spineboy-pro.skel
  22. 94 0
      spine-android/app/src/main/assets/spineboy.atlas
  23. BIN
      spine-android/app/src/main/assets/spineboy.png
  24. 174 0
      spine-android/app/src/main/java/com/esotericsoftware/spine/AnimationStateEvents.kt
  25. 91 0
      spine-android/app/src/main/java/com/esotericsoftware/spine/DebugRendering.kt
  26. 237 0
      spine-android/app/src/main/java/com/esotericsoftware/spine/DisableRendering.kt
  27. 228 0
      spine-android/app/src/main/java/com/esotericsoftware/spine/DressUp.kt
  28. 140 0
      spine-android/app/src/main/java/com/esotericsoftware/spine/IKFollowing.kt
  29. 220 0
      spine-android/app/src/main/java/com/esotericsoftware/spine/MainActivity.kt
  30. 153 0
      spine-android/app/src/main/java/com/esotericsoftware/spine/Physics.kt
  31. 99 0
      spine-android/app/src/main/java/com/esotericsoftware/spine/PlayPause.kt
  32. 91 0
      spine-android/app/src/main/java/com/esotericsoftware/spine/SimpleAnimation.kt
  33. 78 0
      spine-android/app/src/main/java/com/esotericsoftware/spine/SimpleAnimationActivity.java
  34. 40 0
      spine-android/app/src/main/java/com/esotericsoftware/spine/ui/theme/Color.kt
  35. 99 0
      spine-android/app/src/main/java/com/esotericsoftware/spine/ui/theme/Theme.kt
  36. 63 0
      spine-android/app/src/main/java/com/esotericsoftware/spine/ui/theme/Type.kt
  37. 170 0
      spine-android/app/src/main/res/drawable/ic_launcher_background.xml
  38. 30 0
      spine-android/app/src/main/res/drawable/ic_launcher_foreground.xml
  39. BIN
      spine-android/app/src/main/res/drawable/img.png
  40. 21 0
      spine-android/app/src/main/res/layout/activity_simple_animation.xml
  41. 6 0
      spine-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  42. 6 0
      spine-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  43. BIN
      spine-android/app/src/main/res/mipmap-hdpi/ic_launcher.webp
  44. BIN
      spine-android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
  45. BIN
      spine-android/app/src/main/res/mipmap-mdpi/ic_launcher.webp
  46. BIN
      spine-android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
  47. BIN
      spine-android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
  48. BIN
      spine-android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
  49. BIN
      spine-android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
  50. BIN
      spine-android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
  51. BIN
      spine-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
  52. BIN
      spine-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
  53. 10 0
      spine-android/app/src/main/res/values/colors.xml
  54. 3 0
      spine-android/app/src/main/res/values/strings.xml
  55. 5 0
      spine-android/app/src/main/res/values/themes.xml
  56. 13 0
      spine-android/app/src/main/res/xml/backup_rules.xml
  57. 19 0
      spine-android/app/src/main/res/xml/data_extraction_rules.xml
  58. 17 0
      spine-android/app/src/test/java/com/esotericsoftware/android/ExampleUnitTest.kt
  59. 6 0
      spine-android/build.gradle.kts
  60. 23 0
      spine-android/gradle.properties
  61. 38 0
      spine-android/gradle/libs.versions.toml
  62. BIN
      spine-android/gradle/wrapper/gradle-wrapper.jar
  63. 6 0
      spine-android/gradle/wrapper/gradle-wrapper.properties
  64. 185 0
      spine-android/gradlew
  65. 89 0
      spine-android/gradlew.bat
  66. 38 0
      spine-android/settings.gradle.kts
  67. 1 0
      spine-android/spine-android/.gitignore
  68. 75 0
      spine-android/spine-android/build.gradle.kts
  69. 0 0
      spine-android/spine-android/consumer-rules.pro
  70. 21 0
      spine-android/spine-android/proguard-rules.pro
  71. 26 0
      spine-android/spine-android/src/androidTest/java/com/esotericsoftware/android/ExampleInstrumentedTest.java
  72. 4 0
      spine-android/spine-android/src/main/AndroidManifest.xml
  73. 108 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidAtlasAttachmentLoader.java
  74. 176 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidSkeletonDrawable.java
  75. 102 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidTexture.java
  76. 232 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidTextureAtlas.java
  77. 56 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/DebugRenderer.java
  78. 281 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/SkeletonRenderer.java
  79. 331 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/SpineController.java
  80. 469 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/SpineView.java
  81. 61 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/Alignment.java
  82. 104 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/Bounds.java
  83. 40 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/BoundsProvider.java
  84. 44 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/ContentMode.java
  85. 54 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/RawBounds.java
  86. 44 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/SetupPoseBounds.java
  87. 122 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/SkinAndAnimationBounds.java
  88. 37 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/callbacks/AndroidSkeletonDrawableLoader.java
  89. 43 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/callbacks/SpineControllerAfterPaintCallback.java
  90. 42 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/callbacks/SpineControllerBeforePaintCallback.java
  91. 37 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/callbacks/SpineControllerCallback.java
  92. 113 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/utils/HttpUtils.java
  93. 108 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/utils/SkeletonDataUtils.java
  94. 17 0
      spine-android/spine-android/src/test/java/com/esotericsoftware/android/ExampleUnitTest.java
  95. 1 1
      spine-libgdx/build.gradle
  96. 1 1
      spine-libgdx/settings.gradle
  97. 5 1
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Slot.java

+ 15 - 0
spine-android/.gitignore

@@ -0,0 +1,15 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties

+ 1 - 0
spine-android/app/.gitignore

@@ -0,0 +1 @@
+/build

+ 76 - 0
spine-android/app/build.gradle.kts

@@ -0,0 +1,76 @@
+plugins {
+    alias(libs.plugins.androidApplication)
+    alias(libs.plugins.jetbrainsKotlinAndroid)
+}
+
+android {
+    namespace = "com.esotericsoftware.spine"
+    compileSdk = 34
+
+    defaultConfig {
+        applicationId = "com.esotericsoftware.spine"
+        minSdk = 24
+        targetSdk = 34
+        versionCode = 1
+        versionName = "1.0"
+
+        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+        vectorDrawables {
+            useSupportLibrary = true
+        }
+    }
+
+    buildTypes {
+        release {
+            isMinifyEnabled = false
+            proguardFiles(
+                getDefaultProguardFile("proguard-android-optimize.txt"),
+                "proguard-rules.pro"
+            )
+        }
+    }
+    compileOptions {
+        sourceCompatibility = JavaVersion.VERSION_1_8
+        targetCompatibility = JavaVersion.VERSION_1_8
+    }
+    kotlinOptions {
+        jvmTarget = "1.8"
+    }
+    buildFeatures {
+        compose = true
+    }
+    composeOptions {
+        kotlinCompilerExtensionVersion = "1.5.1"
+    }
+    packaging {
+        resources {
+            excludes += "/META-INF/{AL2.0,LGPL2.1}"
+        }
+    }
+}
+
+dependencies {
+
+    implementation(libs.androidx.core.ktx)
+    implementation(libs.androidx.lifecycle.runtime.ktx)
+    implementation(libs.androidx.activity.compose)
+    implementation(platform(libs.androidx.compose.bom))
+    implementation(libs.androidx.ui)
+    implementation(libs.androidx.ui.graphics)
+    implementation(libs.androidx.ui.tooling.preview)
+    implementation(libs.androidx.material3)
+    implementation(libs.androidx.navigation.compose)
+    implementation(libs.appcompat)
+
+    testImplementation(libs.junit)
+    androidTestImplementation(libs.androidx.junit)
+    androidTestImplementation(libs.androidx.espresso.core)
+    androidTestImplementation(platform(libs.androidx.compose.bom))
+    androidTestImplementation(libs.androidx.ui.test.junit4)
+    debugImplementation(libs.androidx.ui.tooling)
+    debugImplementation(libs.androidx.ui.test.manifest)
+
+    implementation(project(":spine-android"))
+    // Run `./gradlew publishToMavenLocal` in `spine-android` to use from local maven repo.
+//    implementation("com.esotericsoftware:spine-android:4.2")
+}

+ 21 - 0
spine-android/app/proguard-rules.pro

@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile

+ 24 - 0
spine-android/app/src/androidTest/java/com/esotericsoftware/android/ExampleInstrumentedTest.kt

@@ -0,0 +1,24 @@
+package com.esotericsoftware.android
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+    @Test
+    fun useAppContext() {
+        // Context of the app under test.
+        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+        assertEquals("com.esotericsoftware.spine", appContext.packageName)
+    }
+}

+ 34 - 0
spine-android/app/src/main/AndroidManifest.xml

@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <uses-permission android:name="android.permission.INTERNET"/>
+
+    <application
+        android:allowBackup="true"
+        android:dataExtractionRules="@xml/data_extraction_rules"
+        android:fullBackupContent="@xml/backup_rules"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:roundIcon="@mipmap/ic_launcher_round"
+        android:supportsRtl="true"
+        android:theme="@style/Theme.SpineAndroidExamples"
+        tools:targetApi="34">
+        <activity
+            android:name="MainActivity"
+            android:exported="true"
+            android:theme="@style/Theme.SpineAndroidExamples">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+
+        <activity android:name=".SimpleAnimationActivity"
+            android:theme="@style/Theme.AppCompat.Light.NoActionBar">
+        </activity>
+
+    </application>
+
+</manifest>

BIN
spine-android/app/src/main/assets/celestial-circus-pro.skel


+ 173 - 0
spine-android/app/src/main/assets/celestial-circus.atlas

@@ -0,0 +1,173 @@
+celestial-circus.png
+	size: 1024, 1024
+	filter: Linear, Linear
+	scale: 0.4
+arm-back-down
+	bounds: 324, 401, 38, 82
+	rotate: 90
+arm-back-up
+	bounds: 290, 44, 83, 116
+	rotate: 90
+arm-front-down
+	bounds: 706, 2, 36, 78
+	rotate: 90
+arm-front-up
+	bounds: 860, 138, 77, 116
+bench
+	bounds: 725, 256, 189, 48
+body-bottom
+	bounds: 879, 868, 154, 124
+	rotate: 90
+body-top
+	bounds: 725, 128, 126, 133
+	rotate: 90
+chest
+	bounds: 408, 26, 104, 93
+cloud-back
+	bounds: 752, 378, 202, 165
+cloud-front
+	bounds: 2, 2, 325, 196
+	rotate: 90
+collar
+	bounds: 786, 13, 47, 26
+ear
+	bounds: 1002, 643, 20, 28
+eye-back-shadow
+	bounds: 428, 395, 14, 10
+eye-front-shadow
+	bounds: 704, 529, 24, 14
+eye-reflex-back
+	bounds: 860, 128, 8, 7
+	rotate: 90
+eye-reflex-front
+	bounds: 726, 386, 10, 7
+eye-white-back
+	bounds: 835, 23, 13, 16
+eye-white-front
+	bounds: 1005, 1000, 22, 17
+	rotate: 90
+eyelashes-down-back
+	bounds: 232, 329, 11, 6
+	rotate: 90
+eyelashes-down-front
+	bounds: 913, 851, 15, 6
+	rotate: 90
+eyelashes-top-back
+	bounds: 408, 395, 18, 10
+eyelashes-top-front
+	bounds: 702, 179, 30, 16
+	rotate: 90
+face
+	bounds: 514, 26, 93, 102
+	rotate: 90
+feathers-back
+	bounds: 954, 625, 46, 46
+feathers-front
+	bounds: 706, 40, 72, 86
+fringe-middle-back
+	bounds: 200, 6, 33, 52
+	rotate: 90
+fringe-middle-front
+	bounds: 878, 76, 60, 50
+	rotate: 90
+fringe-side-back
+	bounds: 780, 41, 27, 94
+	rotate: 90
+fringe-side-front
+	bounds: 939, 161, 26, 93
+glove-bottom-back
+	bounds: 954, 572, 51, 41
+	rotate: 90
+glove-bottom-front
+	bounds: 916, 256, 47, 48
+hair-back-1
+	bounds: 444, 395, 132, 306
+	rotate: 90
+hair-back-2
+	bounds: 438, 211, 80, 285
+	rotate: 90
+hair-back-3
+	bounds: 719, 306, 70, 268
+	rotate: 90
+hair-back-4
+	bounds: 438, 121, 88, 262
+	rotate: 90
+hair-back-5
+	bounds: 438, 293, 88, 279
+	rotate: 90
+hair-back-6
+	bounds: 200, 41, 88, 286
+hair-hat-shadow
+	bounds: 232, 398, 90, 41
+hand-back
+	bounds: 954, 673, 60, 47
+	rotate: 90
+hand-front
+	bounds: 967, 172, 53, 60
+hat-back
+	bounds: 954, 802, 64, 45
+	rotate: 90
+hat-front
+	bounds: 780, 70, 96, 56
+head-back
+	bounds: 618, 17, 102, 86
+	rotate: 90
+jabot
+	bounds: 967, 234, 70, 55
+	rotate: 90
+leg-back
+	bounds: 232, 441, 210, 333
+leg-front
+	bounds: 444, 529, 258, 320
+logo-brooch
+	bounds: 954, 545, 16, 25
+mouth
+	bounds: 408, 121, 22, 6
+neck
+	bounds: 232, 342, 39, 56
+	rotate: 90
+nose
+	bounds: 742, 529, 6, 7
+	rotate: 90
+nose-highlight
+	bounds: 719, 300, 4, 4
+nose-shadow
+	bounds: 869, 128, 7, 8
+pupil-back
+	bounds: 730, 529, 10, 14
+pupil-front
+	bounds: 254, 21, 12, 18
+rope-back
+	bounds: 232, 383, 10, 492
+	rotate: 90
+rope-front
+	bounds: 232, 383, 10, 492
+	rotate: 90
+rope-front-bottom
+	bounds: 954, 735, 42, 65
+skirt
+	bounds: 2, 776, 440, 246
+sock-bow
+	bounds: 408, 407, 33, 32
+spine-logo-body
+	bounds: 879, 853, 13, 32
+	rotate: 90
+star-big
+	bounds: 939, 141, 18, 24
+	rotate: 90
+star-medium
+	bounds: 742, 537, 6, 8
+	rotate: 90
+star-small
+	bounds: 719, 378, 3, 4
+	rotate: 90
+underskirt
+	bounds: 2, 329, 445, 228
+	rotate: 90
+underskirt-back
+	bounds: 444, 851, 433, 171
+wing-back
+	bounds: 290, 129, 146, 252
+wing-front
+	bounds: 704, 545, 304, 248
+	rotate: 90

BIN
spine-android/app/src/main/assets/celestial-circus.png


BIN
spine-android/app/src/main/assets/dragon-ess.skel


+ 112 - 0
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

BIN
spine-android/app/src/main/assets/dragon.png


BIN
spine-android/app/src/main/assets/dragon_2.png


BIN
spine-android/app/src/main/assets/dragon_3.png


BIN
spine-android/app/src/main/assets/dragon_4.png


BIN
spine-android/app/src/main/assets/dragon_5.png


BIN
spine-android/app/src/main/assets/mix-and-match-pro.skel


+ 358 - 0
spine-android/app/src/main/assets/mix-and-match.atlas

@@ -0,0 +1,358 @@
+mix-and-match.png
+	size: 1024, 512
+	filter: Linear, Linear
+	scale: 0.5
+base-head
+	bounds: 118, 70, 95, 73
+boy/arm-front
+	bounds: 831, 311, 36, 115
+	rotate: 90
+boy/backpack
+	bounds: 249, 357, 119, 153
+boy/backpack-pocket
+	bounds: 628, 193, 34, 62
+	rotate: 90
+boy/backpack-strap-front
+	bounds: 330, 263, 38, 88
+	rotate: 90
+boy/backpack-up
+	bounds: 482, 171, 21, 70
+boy/body
+	bounds: 845, 413, 97, 132
+	rotate: 90
+boy/boot-ribbon-front
+	bounds: 234, 304, 9, 11
+boy/collar
+	bounds: 471, 243, 73, 29
+	rotate: 90
+boy/ear
+	bounds: 991, 352, 19, 23
+	rotate: 90
+boy/eye-back-low-eyelid
+	bounds: 66, 72, 17, 6
+boy/eye-back-pupil
+	bounds: 694, 279, 8, 9
+	rotate: 90
+boy/eye-back-up-eyelid
+	bounds: 460, 101, 23, 5
+	rotate: 90
+boy/eye-back-up-eyelid-back
+	bounds: 979, 414, 19, 10
+	rotate: 90
+boy/eye-front-low-eyelid
+	bounds: 1015, 203, 22, 7
+	rotate: 90
+boy/eye-front-pupil
+	bounds: 309, 50, 9, 9
+boy/eye-front-up-eyelid
+	bounds: 991, 373, 31, 6
+boy/eye-front-up-eyelid-back
+	bounds: 107, 76, 26, 9
+	rotate: 90
+boy/eye-iris-back
+	bounds: 810, 260, 17, 17
+boy/eye-iris-front
+	bounds: 902, 230, 18, 18
+boy/eye-white-back
+	bounds: 599, 179, 20, 12
+boy/eye-white-front
+	bounds: 544, 183, 27, 13
+boy/eyebrow-back
+	bounds: 1002, 225, 20, 11
+	rotate: 90
+boy/eyebrow-front
+	bounds: 975, 234, 25, 11
+boy/hair-back
+	bounds: 629, 289, 122, 81
+	rotate: 90
+boy/hair-bangs
+	bounds: 505, 180, 70, 37
+	rotate: 90
+boy/hair-side
+	bounds: 979, 435, 25, 43
+	rotate: 90
+boy/hand-backfingers
+	bounds: 858, 183, 19, 21
+boy/hand-front-fingers
+	bounds: 879, 183, 19, 21
+boy/hat
+	bounds: 218, 121, 93, 56
+boy/leg-front
+	bounds: 85, 104, 31, 158
+boy/mouth-close
+	bounds: 467, 100, 21, 5
+girl-blue-cape/mouth-close
+	bounds: 467, 100, 21, 5
+girl-spring-dress/mouth-close
+	bounds: 467, 100, 21, 5
+girl/mouth-close
+	bounds: 467, 100, 21, 5
+boy/mouth-smile
+	bounds: 1015, 258, 29, 7
+	rotate: 90
+boy/nose
+	bounds: 323, 79, 17, 10
+boy/pompom
+	bounds: 979, 462, 48, 43
+	rotate: 90
+boy/zip
+	bounds: 922, 231, 14, 23
+	rotate: 90
+girl-blue-cape/back-eyebrow
+	bounds: 527, 106, 18, 12
+	rotate: 90
+girl-blue-cape/body-dress
+	bounds: 2, 264, 109, 246
+girl-blue-cape/body-ribbon
+	bounds: 576, 193, 50, 38
+girl-blue-cape/cape-back
+	bounds: 113, 317, 134, 193
+girl-blue-cape/cape-back-up
+	bounds: 504, 305, 123, 106
+girl-blue-cape/cape-ribbon
+	bounds: 396, 118, 50, 18
+	rotate: 90
+girl-blue-cape/cape-shoulder-back
+	bounds: 420, 243, 49, 59
+girl-blue-cape/cape-shoulder-front
+	bounds: 2, 2, 62, 76
+girl-blue-cape/cape-up-front
+	bounds: 118, 145, 98, 117
+girl-blue-cape/ear
+	bounds: 837, 181, 19, 23
+girl-spring-dress/ear
+	bounds: 837, 181, 19, 23
+girl/ear
+	bounds: 837, 181, 19, 23
+girl-blue-cape/eye-back-low-eyelid
+	bounds: 810, 252, 17, 6
+girl-spring-dress/eye-back-low-eyelid
+	bounds: 810, 252, 17, 6
+girl/eye-back-low-eyelid
+	bounds: 810, 252, 17, 6
+girl-blue-cape/eye-back-pupil
+	bounds: 309, 40, 8, 9
+	rotate: 90
+girl-spring-dress/eye-back-pupil
+	bounds: 309, 40, 8, 9
+	rotate: 90
+girl/eye-back-pupil
+	bounds: 309, 40, 8, 9
+	rotate: 90
+girl-blue-cape/eye-back-up-eyelid
+	bounds: 573, 179, 24, 12
+girl-spring-dress/eye-back-up-eyelid
+	bounds: 573, 179, 24, 12
+girl/eye-back-up-eyelid
+	bounds: 573, 179, 24, 12
+girl-blue-cape/eye-back-up-eyelid-back
+	bounds: 380, 105, 17, 11
+	rotate: 90
+girl-spring-dress/eye-back-up-eyelid-back
+	bounds: 380, 105, 17, 11
+	rotate: 90
+girl/eye-back-up-eyelid-back
+	bounds: 380, 105, 17, 11
+	rotate: 90
+girl-blue-cape/eye-front-low-eyelid
+	bounds: 1016, 353, 18, 6
+	rotate: 90
+girl-spring-dress/eye-front-low-eyelid
+	bounds: 1016, 353, 18, 6
+	rotate: 90
+girl/eye-front-low-eyelid
+	bounds: 1016, 353, 18, 6
+	rotate: 90
+girl-blue-cape/eye-front-pupil
+	bounds: 363, 94, 9, 9
+girl-spring-dress/eye-front-pupil
+	bounds: 363, 94, 9, 9
+girl/eye-front-pupil
+	bounds: 363, 94, 9, 9
+girl-blue-cape/eye-front-up-eyelid
+	bounds: 679, 413, 30, 14
+	rotate: 90
+girl-spring-dress/eye-front-up-eyelid
+	bounds: 679, 413, 30, 14
+	rotate: 90
+girl/eye-front-up-eyelid
+	bounds: 679, 413, 30, 14
+	rotate: 90
+girl-blue-cape/eye-front-up-eyelid-back
+	bounds: 947, 234, 26, 11
+girl-spring-dress/eye-front-up-eyelid-back
+	bounds: 947, 234, 26, 11
+girl/eye-front-up-eyelid-back
+	bounds: 947, 234, 26, 11
+girl-blue-cape/eye-iris-back
+	bounds: 323, 105, 17, 17
+girl-blue-cape/eye-iris-front
+	bounds: 467, 107, 18, 18
+girl-blue-cape/eye-white-back
+	bounds: 621, 175, 20, 16
+girl-spring-dress/eye-white-back
+	bounds: 621, 175, 20, 16
+girl-blue-cape/eye-white-front
+	bounds: 643, 175, 20, 16
+girl-spring-dress/eye-white-front
+	bounds: 643, 175, 20, 16
+girl/eye-white-front
+	bounds: 643, 175, 20, 16
+girl-blue-cape/front-eyebrow
+	bounds: 309, 101, 18, 12
+	rotate: 90
+girl-blue-cape/hair-back
+	bounds: 712, 317, 117, 98
+girl-blue-cape/hair-bangs
+	bounds: 313, 170, 91, 40
+	rotate: 90
+girl-blue-cape/hair-head-side-back
+	bounds: 544, 198, 30, 52
+girl-blue-cape/hair-head-side-front
+	bounds: 466, 127, 41, 42
+girl-blue-cape/hair-side
+	bounds: 175, 2, 36, 71
+	rotate: 90
+girl-blue-cape/hand-front-fingers
+	bounds: 902, 207, 19, 21
+girl-spring-dress/hand-front-fingers
+	bounds: 902, 207, 19, 21
+girl-blue-cape/leg-front
+	bounds: 519, 413, 30, 158
+	rotate: 90
+girl-blue-cape/mouth-smile
+	bounds: 1015, 227, 29, 7
+	rotate: 90
+girl-spring-dress/mouth-smile
+	bounds: 1015, 227, 29, 7
+	rotate: 90
+girl/mouth-smile
+	bounds: 1015, 227, 29, 7
+	rotate: 90
+girl-blue-cape/nose
+	bounds: 342, 82, 11, 7
+girl-spring-dress/nose
+	bounds: 342, 82, 11, 7
+girl/nose
+	bounds: 342, 82, 11, 7
+girl-blue-cape/sleeve-back
+	bounds: 416, 95, 42, 29
+girl-blue-cape/sleeve-front
+	bounds: 249, 303, 52, 119
+	rotate: 90
+girl-spring-dress/arm-front
+	bounds: 829, 292, 17, 111
+	rotate: 90
+girl-spring-dress/back-eyebrow
+	bounds: 309, 81, 18, 12
+	rotate: 90
+girl-spring-dress/body-up
+	bounds: 66, 2, 64, 66
+girl-spring-dress/cloak-down
+	bounds: 758, 227, 50, 50
+girl-spring-dress/cloak-up
+	bounds: 628, 229, 64, 58
+girl-spring-dress/eye-iris-back
+	bounds: 342, 105, 17, 17
+girl-spring-dress/eye-iris-front
+	bounds: 487, 107, 18, 18
+girl-spring-dress/front-eyebrow
+	bounds: 323, 91, 18, 12
+girl-spring-dress/hair-back
+	bounds: 370, 417, 147, 93
+girl-spring-dress/hair-bangs
+	bounds: 829, 250, 91, 40
+girl-spring-dress/hair-head-side-back
+	bounds: 509, 126, 30, 52
+girl-spring-dress/hair-head-side-front
+	bounds: 816, 206, 41, 42
+girl-spring-dress/hair-side
+	bounds: 248, 2, 36, 71
+	rotate: 90
+girl-spring-dress/leg-front
+	bounds: 831, 381, 30, 158
+	rotate: 90
+girl-spring-dress/neck
+	bounds: 85, 70, 20, 32
+girl-spring-dress/shoulder-ribbon
+	bounds: 175, 44, 36, 24
+girl-spring-dress/skirt
+	bounds: 2, 80, 182, 81
+	rotate: 90
+girl-spring-dress/underskirt
+	bounds: 519, 445, 175, 65
+girl/arm-front
+	bounds: 712, 279, 36, 115
+	rotate: 90
+girl/back-eyebrow
+	bounds: 309, 61, 18, 12
+	rotate: 90
+girl/bag-base
+	bounds: 694, 219, 62, 58
+girl/bag-strap-front
+	bounds: 370, 304, 12, 96
+	rotate: 90
+girl/bag-top
+	bounds: 765, 175, 49, 50
+girl/body
+	bounds: 370, 318, 97, 132
+	rotate: 90
+girl/boot-ribbon-front
+	bounds: 323, 64, 13, 13
+girl/eye-iris-back
+	bounds: 361, 105, 17, 17
+girl/eye-iris-front
+	bounds: 507, 106, 18, 18
+girl/eye-white-back
+	bounds: 665, 175, 20, 16
+girl/front-eyebrow
+	bounds: 343, 91, 18, 12
+girl/hair-back
+	bounds: 696, 417, 147, 93
+girl/hair-bangs
+	bounds: 922, 247, 91, 40
+girl/hair-flap-down-front
+	bounds: 415, 171, 70, 65
+	rotate: 90
+girl/hair-head-side-back
+	bounds: 991, 381, 30, 52
+girl/hair-head-side-front
+	bounds: 859, 206, 41, 42
+girl/hair-patch
+	bounds: 132, 2, 66, 41
+	rotate: 90
+girl/hair-side
+	bounds: 692, 181, 36, 71
+	rotate: 90
+girl/hair-strand-back-1
+	bounds: 948, 289, 58, 74
+	rotate: 90
+girl/hair-strand-back-2
+	bounds: 355, 170, 91, 58
+	rotate: 90
+girl/hair-strand-back-3
+	bounds: 215, 40, 92, 79
+girl/hair-strand-front-1
+	bounds: 234, 263, 38, 94
+	rotate: 90
+girl/hair-strand-front-2
+	bounds: 576, 233, 70, 50
+	rotate: 90
+girl/hair-strand-front-3
+	bounds: 313, 124, 44, 81
+	rotate: 90
+girl/hand-front-fingers
+	bounds: 923, 208, 19, 21
+girl/hat
+	bounds: 218, 179, 93, 82
+girl/leg-front
+	bounds: 831, 349, 30, 158
+	rotate: 90
+girl/pompom
+	bounds: 416, 126, 48, 43
+girl/scarf
+	bounds: 113, 264, 119, 51
+girl/scarf-back
+	bounds: 502, 252, 72, 51
+girl/zip
+	bounds: 816, 179, 19, 25

BIN
spine-android/app/src/main/assets/mix-and-match.png


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 557 - 0
spine-android/app/src/main/assets/spineboy-pro.json


BIN
spine-android/app/src/main/assets/spineboy-pro.skel


+ 94 - 0
spine-android/app/src/main/assets/spineboy.atlas

@@ -0,0 +1,94 @@
+spineboy.png
+	size: 1024, 256
+	filter: Linear, Linear
+	scale: 0.5
+crosshair
+	bounds: 352, 7, 45, 45
+eye-indifferent
+	bounds: 862, 105, 47, 45
+eye-surprised
+	bounds: 505, 79, 47, 45
+front-bracer
+	bounds: 826, 66, 29, 40
+front-fist-closed
+	bounds: 786, 65, 38, 41
+front-fist-open
+	bounds: 710, 51, 43, 44
+	rotate: 90
+front-foot
+	bounds: 210, 6, 63, 35
+front-shin
+	bounds: 665, 128, 41, 92
+	rotate: 90
+front-thigh
+	bounds: 2, 2, 23, 56
+	rotate: 90
+front-upper-arm
+	bounds: 250, 205, 23, 49
+goggles
+	bounds: 665, 171, 131, 83
+gun
+	bounds: 798, 152, 105, 102
+head
+	bounds: 2, 27, 136, 149
+hoverboard-board
+	bounds: 2, 178, 246, 76
+hoverboard-thruster
+	bounds: 722, 96, 30, 32
+	rotate: 90
+hoverglow-small
+	bounds: 275, 81, 137, 38
+mouth-grind
+	bounds: 614, 97, 47, 30
+mouth-oooo
+	bounds: 612, 65, 47, 30
+mouth-smile
+	bounds: 661, 64, 47, 30
+muzzle-glow
+	bounds: 382, 54, 25, 25
+muzzle-ring
+	bounds: 275, 54, 25, 105
+	rotate: 90
+muzzle01
+	bounds: 911, 95, 67, 40
+	rotate: 90
+muzzle02
+	bounds: 792, 108, 68, 42
+muzzle03
+	bounds: 956, 171, 83, 53
+	rotate: 90
+muzzle04
+	bounds: 275, 7, 75, 45
+muzzle05
+	bounds: 140, 3, 68, 38
+neck
+	bounds: 250, 182, 18, 21
+portal-bg
+	bounds: 140, 43, 133, 133
+portal-flare1
+	bounds: 554, 65, 56, 30
+portal-flare2
+	bounds: 759, 112, 57, 31
+	rotate: 90
+portal-flare3
+	bounds: 554, 97, 58, 30
+portal-shade
+	bounds: 275, 121, 133, 133
+portal-streaks1
+	bounds: 410, 126, 126, 128
+portal-streaks2
+	bounds: 538, 129, 125, 125
+rear-bracer
+	bounds: 857, 67, 28, 36
+rear-foot
+	bounds: 663, 96, 57, 30
+rear-shin
+	bounds: 414, 86, 38, 89
+	rotate: 90
+rear-thigh
+	bounds: 756, 63, 28, 47
+rear-upper-arm
+	bounds: 60, 5, 20, 44
+	rotate: 90
+torso
+	bounds: 905, 164, 49, 90

BIN
spine-android/app/src/main/assets/spineboy.png


+ 174 - 0
spine-android/app/src/main/java/com/esotericsoftware/spine/AnimationStateEvents.kt

@@ -0,0 +1,174 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine
+
+import android.util.Log
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+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.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.navigation.NavHostController
+import com.badlogic.gdx.graphics.Color
+import com.esotericsoftware.spine.android.SpineController
+import com.esotericsoftware.spine.android.SpineView
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AnimationState(nav: NavHostController) {
+
+    val TAG = "AnimationState"
+
+    val controller = remember {
+        SpineController { controller ->
+            controller.skeleton.setScaleX(0.5f)
+            controller.skeleton.setScaleY(0.5f)
+
+            controller.skeleton.findSlot("gun")?.color = Color(1f, 0f, 0f, 1f)
+
+            controller.animationStateData.setDefaultMix(0.2f)
+            controller.animationState.setAnimation(0, "walk", true).setListener(object : AnimationState.AnimationStateListener {
+                override fun start(entry: AnimationState.TrackEntry?) {
+                    Log.d(TAG, "Walk animation event start")
+                }
+
+                override fun interrupt(entry: AnimationState.TrackEntry?) {
+                    Log.d(TAG, "Walk animation event interrupt")
+                }
+
+                override fun end(entry: AnimationState.TrackEntry?) {
+                    Log.d(TAG, "Walk animation event end")
+                }
+
+                override fun dispose(entry: AnimationState.TrackEntry?) {
+                    Log.d(TAG, "Walk animation event dispose")
+                }
+
+                override fun complete(entry: AnimationState.TrackEntry?) {
+                    Log.d(TAG, "Walk animation event complete")
+                }
+
+                override fun event(entry: AnimationState.TrackEntry?, event: Event?) {
+                    Log.d(TAG, "Walk animation event event")
+                }
+            })
+            controller.animationState.addAnimation(0, "jump", false, 2f)
+            controller.animationState.addAnimation(0, "run", true, 0f).setListener(object : AnimationState.AnimationStateListener {
+                override fun start(entry: AnimationState.TrackEntry?) {
+                    Log.d(TAG, "Run animation event start")
+                }
+
+                override fun interrupt(entry: AnimationState.TrackEntry?) {
+                    Log.d(TAG, "Run animation event interrupt")
+                }
+
+                override fun end(entry: AnimationState.TrackEntry?) {
+                    Log.d(TAG, "Run animation event end")
+                }
+
+                override fun dispose(entry: AnimationState.TrackEntry?) {
+                    Log.d(TAG, "Run animation event dispose")
+                }
+
+                override fun complete(entry: AnimationState.TrackEntry?) {
+                    Log.d(TAG, "Run animation event complete")
+                }
+
+                override fun event(entry: AnimationState.TrackEntry?, event: Event?) {
+                    Log.d(TAG, "Run animation event event")
+                }
+            })
+
+            controller.animationState.addListener(object : AnimationState.AnimationStateListener {
+                override fun start(entry: AnimationState.TrackEntry?) {}
+
+                override fun interrupt(entry: AnimationState.TrackEntry?) {}
+
+                override fun end(entry: AnimationState.TrackEntry?) {}
+
+                override fun dispose(entry: AnimationState.TrackEntry?) {}
+
+                override fun complete(entry: AnimationState.TrackEntry?) {}
+
+                override fun event(entry: AnimationState.TrackEntry?, event: Event?) {
+                    if (event != null) {
+                        Log.d(TAG, "User event: { name: ${event.data.name}, intValue: ${event.int}, floatValue: ${event.float}, stringValue: ${event.string} }")
+                    }
+                }
+            })
+            Log.d(TAG, "Current: ${controller.animationState.getCurrent(0)?.getAnimation()?.getName()}");
+        }
+    }
+
+    Scaffold(
+        topBar = {
+            TopAppBar(
+                title = { Text(text = Destination.AnimationStateEvents.title) },
+                navigationIcon = {
+                    IconButton({ nav.navigateUp() }) {
+                        Icon(
+                            Icons.Rounded.ArrowBack,
+                            null,
+                        )
+                    }
+                }
+            )
+        }
+    ) { paddingValues ->
+        Column(
+            modifier = Modifier.padding(paddingValues),
+            horizontalAlignment = Alignment.CenterHorizontally,
+            verticalArrangement = Arrangement.Center
+        ) {
+            Text("See output in console!")
+            AndroidView(
+                factory = { context ->
+                    SpineView.loadFromAssets(
+                        "spineboy.atlas",
+                        "spineboy-pro.json",
+                        context,
+                        controller
+                    )
+                }
+            )
+        }
+    }
+}

+ 91 - 0
spine-android/app/src/main/java/com/esotericsoftware/spine/DebugRendering.kt

@@ -0,0 +1,91 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+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.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.navigation.NavHostController
+import com.esotericsoftware.spine.android.DebugRenderer
+import com.esotericsoftware.spine.android.SpineController
+import com.esotericsoftware.spine.android.SpineView
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun DebugRendering(nav: NavHostController) {
+
+    val debugRenderer = remember {
+        DebugRenderer()
+    }
+
+    Scaffold(
+        topBar = {
+            TopAppBar(
+                title = { Text(text = Destination.DebugRendering.title) },
+                navigationIcon = {
+                    IconButton({ nav.navigateUp() }) {
+                        Icon(
+                            Icons.Rounded.ArrowBack,
+                            null,
+                        )
+                    }
+                }
+            )
+        }
+    ) { paddingValues ->
+        AndroidView(
+            factory = { context ->
+                SpineView.loadFromAssets(
+                    "spineboy.atlas",
+                    "spineboy-pro.json",
+                    context,
+                    SpineController.Builder { controller ->
+                        controller.animationState.setAnimation(0, "walk", true)
+                    }
+                    .setOnAfterPaint { controller, canvas, commands ->
+                        debugRenderer.render(controller.drawable, canvas, commands)
+                    }
+                    .build()
+                )
+            },
+            modifier = Modifier.padding(paddingValues)
+        )
+    }
+}

+ 237 - 0
spine-android/app/src/main/java/com/esotericsoftware/spine/DisableRendering.kt

@@ -0,0 +1,237 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine
+
+import androidx.compose.foundation.gestures.detectDragGestures
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+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.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clipToBounds
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.positionInParent
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.toSize
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.navigation.NavHostController
+import com.esotericsoftware.spine.android.AndroidSkeletonDrawable
+import com.esotericsoftware.spine.android.AndroidTextureAtlas
+import com.esotericsoftware.spine.android.SpineController
+import com.esotericsoftware.spine.android.SpineView
+import com.esotericsoftware.spine.android.utils.SkeletonDataUtils
+import kotlin.random.Random
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun DisableRendering(nav: NavHostController) {
+    Scaffold(
+        topBar = {
+            TopAppBar(
+                title = { Text(text = Destination.DisableRendering.title) },
+                navigationIcon = {
+                    IconButton({ nav.navigateUp() }) {
+                        Icon(
+                            Icons.Rounded.ArrowBack,
+                            null,
+                        )
+                    }
+                }
+            )
+        }
+    ) { paddingValues ->
+
+        val visibleSpineBoys = remember {
+            mutableStateListOf<Int>()
+        }
+
+        Column(
+            modifier = Modifier
+                .padding(paddingValues)
+                .padding()
+                .onGloballyPositioned { coordinates ->
+                    print(coordinates.size.toSize())
+                }
+        ) {
+            Column(
+                modifier = Modifier
+                    .padding(8.dp)
+            ) {
+                Text("There are ${visibleSpineBoys.count()} spine boys visible. Scroll around to find the odd one out...")
+                Text("Rendering is disabled when the spine view moves out of the viewport, preserving CPU/GPU resources.", color = Color.Gray)
+            }
+            SpineBoys(visibleSpineBoys)
+        }
+    }
+}
+
+@Composable
+fun SpineBoys(visibleSpineBoys: MutableList<Int>) {
+    var boxSize by remember { mutableStateOf(Size.Zero) }
+    val offsetX = remember { mutableFloatStateOf(0f) }
+    val offsetY = remember { mutableFloatStateOf(0f) }
+
+    Box(
+        modifier = Modifier
+            .fillMaxSize()
+            .clipToBounds()
+            .onGloballyPositioned { coordinates ->
+                boxSize = coordinates.size.toSize()
+            }
+            .pointerInput(Unit) {
+                detectDragGestures { change, dragAmount ->
+                    change.consume()
+                    offsetX.floatValue += dragAmount.x
+                    offsetY.floatValue += dragAmount.y
+                }
+            }
+    ) {
+        if (boxSize != Size.Zero) {
+            val contentSize = boxSize * 4f
+
+            val context = LocalContext.current
+            val cachedAtlas =
+                remember { AndroidTextureAtlas.fromAsset("spineboy.atlas", context) }
+            val cachedSkeletonData = remember {
+                SkeletonDataUtils.fromAsset(
+                    cachedAtlas,
+                    "spineboy-pro.json",
+                    context
+                )
+            }
+
+            val spineboys = remember {
+                val rng = Random(System.currentTimeMillis())
+                List(100) { index ->
+                    val scale = 0.1f + rng.nextFloat() * 0.2f
+                    val position = Offset(
+                        rng.nextFloat() * contentSize.width,
+                        rng.nextFloat() * contentSize.height
+                    )
+                    SpineBoyData(
+                        index,
+                        scale,
+                        position,
+                        if (index == 99) "hoverboard" else "walk"
+                    )
+                }
+            }
+
+            spineboys.forEach { spineBoyData ->
+
+                val isSpineBoyVisible = remember { mutableStateOf(false) }
+
+                Box(modifier = Modifier
+                    .offset {
+                        IntOffset(
+                            (-(contentSize.width / 2) + spineBoyData.position.x + offsetX.floatValue.toInt()).toInt(),
+                            (-(contentSize.height / 2) + spineBoyData.position.y + offsetY.floatValue.toInt()).toInt(),
+                        )
+                    }
+                    .size(
+                        (boxSize.width * spineBoyData.scale).dp,
+                        (boxSize.height * spineBoyData.scale).dp
+                    )
+                    .onGloballyPositioned { coordinates ->
+                        val positionInRoot = coordinates.positionInParent()
+                        val size = coordinates.size.toSize()
+
+                        val isInViewport = positionInRoot.x < boxSize.width &&
+                            positionInRoot.x + size.width > 0 &&
+                            positionInRoot.y < boxSize.height &&
+                            positionInRoot.y + size.height > 0
+
+                        isSpineBoyVisible.value = isInViewport
+
+                        val visibleSpineBoysAsSet = visibleSpineBoys.toMutableSet()
+                        if (isInViewport) {
+                            visibleSpineBoysAsSet.add(spineBoyData.id)
+                        } else {
+                            visibleSpineBoysAsSet.remove(spineBoyData.id)
+                        }
+                        visibleSpineBoys.clear()
+                        visibleSpineBoys.addAll(visibleSpineBoysAsSet)
+                    }
+                ) {
+                    AndroidView(
+                        factory = { ctx ->
+                            SpineView.loadFromDrawable(
+                                AndroidSkeletonDrawable(cachedAtlas, cachedSkeletonData),
+                                ctx,
+                                SpineController {
+                                    it.animationState.setAnimation(
+                                        0,
+                                        spineBoyData.animation,
+                                        true
+                                    )
+                                }
+                            ).apply {
+                                isRendering = false
+                            }
+                        },
+                        update = { view ->
+                            view.isRendering = isSpineBoyVisible.value
+                        }
+                    )
+                }
+            }
+        }
+    }
+}
+
+data class SpineBoyData(
+    val id: Int,
+    val scale: Float,
+    val position: Offset,
+    val animation: String
+)

+ 228 - 0
spine-android/app/src/main/java/com/esotericsoftware/spine/DressUp.kt

@@ -0,0 +1,228 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+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.LaunchedEffect
+import androidx.compose.runtime.mutableStateMapOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clipToBounds
+import androidx.compose.ui.draw.drawWithCache
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.ColorMatrix
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.Paint
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
+import androidx.compose.ui.graphics.painter.BitmapPainter
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.navigation.NavHostController
+import com.esotericsoftware.spine.android.AndroidSkeletonDrawable
+import com.esotericsoftware.spine.android.SkeletonRenderer
+import com.esotericsoftware.spine.android.SpineController
+import com.esotericsoftware.spine.android.SpineView
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun DressUp(nav: NavHostController) {
+
+    val context = LocalContext.current
+    val thumbnailSize = 150f
+
+    val drawable = remember {
+        AndroidSkeletonDrawable.fromAsset(
+            "mix-and-match.atlas",
+            "mix-and-match-pro.skel",
+            context
+        )
+    }
+
+    val renderer = remember {
+        SkeletonRenderer()
+    }
+
+    val customSkin = remember {
+        mutableStateOf<Skin?>(null)
+    }
+
+    val skinImages = remember {
+        mutableStateMapOf<String, ImageBitmap>()
+    }
+
+    val selectedSkins = remember {
+        mutableStateMapOf<String, Boolean>()
+    }
+
+    val controller = remember {
+        SpineController { controller ->
+            controller.animationState.setAnimation(0, "dance", true)
+        }
+    }
+
+    fun toggleSkin(skinName: String) {
+        selectedSkins[skinName] = !(selectedSkins[skinName] ?: false)
+        drawable.skeleton.setSkin("default")
+        customSkin.value = Skin("custom-skin");
+        for (selectedSkinKey in selectedSkins.keys) {
+            if (selectedSkins[selectedSkinKey] == true) {
+                val selectedSkin = drawable.skeletonData.findSkin(selectedSkinKey)
+                if (selectedSkin != null) customSkin.value?.addSkin(selectedSkin)
+            }
+        }
+        val customSkinValue = customSkin.value
+        if (customSkinValue != null) {
+            drawable.skeleton.setSkin(customSkinValue)
+        }
+        drawable.skeleton.setSlotsToSetupPose()
+    }
+
+    val localDensity = LocalDensity.current
+
+    LaunchedEffect(Unit) {
+        for (skin in drawable.skeletonData.getSkins()) {
+            if (skin.getName() == "default") continue
+            val skeleton = drawable.skeleton
+            skeleton.setSkin(skin)
+            skeleton.setToSetupPose()
+            skeleton.update(0f)
+            skeleton.updateWorldTransform(Skeleton.Physics.update)
+            skinImages[skin.getName()] = renderer.renderToBitmap(
+                with(localDensity) { thumbnailSize.dp.toPx() },
+                with(localDensity) { thumbnailSize.dp.toPx() },
+                0xffffffff.toInt(),
+                skeleton,
+            ).asImageBitmap()
+            selectedSkins[skin.getName()] = false
+        }
+        toggleSkin("full-skins/girl");
+    }
+
+    Scaffold(
+        topBar = {
+            TopAppBar(
+                title = { Text(text = Destination.DressUp.title) },
+                navigationIcon = {
+                    IconButton({ nav.navigateUp() }) {
+                        Icon(
+                            Icons.Rounded.ArrowBack,
+                            null,
+                        )
+                    }
+                }
+            )
+        }
+    ) { paddingValues ->
+        Row(
+            modifier = Modifier
+                .padding(paddingValues)
+        ) {
+            Column(
+                modifier = Modifier
+                    .width(thumbnailSize.dp)
+                    .verticalScroll(rememberScrollState())
+            ) {
+                skinImages.keys.forEach { skinName ->
+                    Box(modifier = Modifier
+                        .clickable {
+                            toggleSkin(skinName)
+                        }
+                        .then(
+                            if (selectedSkins[skinName] == true) {
+                                Modifier
+                            } else {
+                                Modifier.grayScale()
+                            }
+                        )
+                    ) {
+                        Image(
+                            painter = BitmapPainter(skinImages[skinName]!!),
+                            contentDescription = null
+                        )
+                    }
+                }
+            }
+            Column(
+                modifier = Modifier
+                    .clipToBounds()
+            ) {
+                AndroidView(
+                    factory = { context ->
+                        SpineView.loadFromDrawable(drawable, context, controller)
+                    }
+                )
+            }
+        }
+    }
+}
+
+fun Modifier.grayScale(): Modifier {
+    val saturationMatrix = ColorMatrix().apply { setToSaturation(0f) }
+    val saturationFilter = ColorFilter.colorMatrix(saturationMatrix)
+    val paint = Paint().apply { colorFilter = saturationFilter }
+
+    return drawWithCache {
+        val canvasBounds = Rect(Offset.Zero, size)
+        onDrawWithContent {
+            drawIntoCanvas {
+                it.saveLayer(canvasBounds, paint)
+                drawContent()
+                it.restore()
+            }
+        }
+    }
+}

+ 140 - 0
spine-android/app/src/main/java/com/esotericsoftware/spine/IKFollowing.kt

@@ -0,0 +1,140 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+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
+import com.esotericsoftware.spine.android.bounds.Alignment
+
+@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 { controller ->
+            controller.animationState.setAnimation(0, "walk", true)
+            controller.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.IKFollowing.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 = { context ->
+                    SpineView.loadFromAssets(
+                        "spineboy.atlas",
+                        "spineboy-pro.json",
+                        context,
+                        controller
+                    ).apply {
+                        alignment = Alignment.CENTER_LEFT
+                    }
+                }
+            )
+        }
+    }
+}

+ 220 - 0
spine-android/app/src/main/java/com/esotericsoftware/spine/MainActivity.kt

@@ -0,0 +1,220 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine
+
+import android.content.Intent
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+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.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() {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContent {
+            AppContent()
+        }
+    }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AppContent() {
+    val navController = rememberNavController()
+
+    SpineAndroidExamplesTheme {
+        Surface(
+            modifier = Modifier.fillMaxSize(),
+            color = MaterialTheme.colorScheme.background
+        ) {
+            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,
+                                Destination.AnimationStateEvents,
+                                Destination.DebugRendering,
+                                Destination.DressUp,
+                                Destination.IKFollowing,
+                                Destination.Physics,
+                                Destination.DisableRendering
+                            ),
+                            paddingValues
+                        )
+                    }
+                }
+
+                composable(
+                    Destination.SimpleAnimation.route
+                ) {
+                    SimpleAnimation(navController)
+                }
+
+                composable(
+                    Destination.PlayPause.route
+                ) {
+                    PlayPause(navController)
+                }
+
+                composable(
+                    Destination.AnimationStateEvents.route
+                ) {
+                    AnimationState(navController)
+                }
+
+                composable(
+                    Destination.DebugRendering.route
+                ) {
+                    DebugRendering(navController)
+                }
+
+                composable(
+                    Destination.DressUp.route
+                ) {
+                    DressUp(navController)
+                }
+
+                composable(
+                    Destination.IKFollowing.route
+                ) {
+                    IKFollowing(navController)
+                }
+
+                composable(
+                    Destination.Physics.route
+                ) {
+                    Physics(navController)
+                }
+
+                composable(
+                    Destination.DisableRendering.route
+                ) {
+                    DisableRendering(navController)
+                }
+            }
+        }
+    }
+}
+
+@Composable
+fun Samples(
+    nav: NavHostController,
+    samples: List<Destination>,
+    paddingValues: PaddingValues
+) {
+    LazyColumn(
+        verticalArrangement = Arrangement.spacedBy(8.dp),
+        modifier = Modifier
+            .padding(8.dp)
+            .padding(paddingValues)
+    ) {
+        item {
+            Text(text = "Kotlin + Jetpack Compose", Modifier.padding(8.dp))
+        }
+
+        samples.forEach {
+            item {
+                Card(
+                    Modifier
+                        .fillMaxWidth()
+                        .clickable(onClick = { nav.navigate(it.route) }),
+                    shape = MaterialTheme.shapes.large
+                ) {
+                    Text(text = it.title, Modifier.padding(24.dp))
+                }
+            }
+        }
+
+        item {
+            Text(text = "Java + XML", Modifier.padding(8.dp))
+        }
+
+        item {
+            Card(
+                Modifier
+                    .fillMaxWidth()
+                    .clickable(onClick = {
+                        nav.context.startActivity(
+                            Intent(
+                                nav.context,
+                                SimpleAnimationActivity::class.java
+                            )
+                        )
+                    }),
+                shape = MaterialTheme.shapes.large
+            ) {
+                Text(text = "Simple Animation", Modifier.padding(24.dp))
+            }
+        }
+    }
+}
+
+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 DebugRendering: Destination("debugRendering", "Debug Renderer")
+    data object AnimationStateEvents : Destination("animationStateEvents", "Animation State Listener")
+    data object DressUp : Destination("dressUp", "Dress Up")
+    data object IKFollowing : Destination("ikFollowing", "IK Following")
+    data object Physics: Destination("physics", "Physics (drag anywhere)")
+    data object DisableRendering: Destination("disableRendering", "Disable Rendering")
+}

+ 153 - 0
spine-android/app/src/main/java/com/esotericsoftware/spine/Physics.kt

@@ -0,0 +1,153 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+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.esotericsoftware.spine.android.SpineController
+import com.esotericsoftware.spine.android.SpineView
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun Physics(nav: NavHostController) {
+
+    val containerHeight = remember { mutableIntStateOf(0) }
+    val dragPosition = remember { mutableStateOf(Point(0, 0)) }
+
+    val mousePosition = remember { mutableStateOf<Point?>(null) }
+    val lastMousePosition = remember { mutableStateOf<Point?>(null) }
+
+    val controller = remember {
+        SpineController.Builder { controller ->
+            controller.animationState.setAnimation(0, "eyeblink-long", true)
+            controller.animationState.setAnimation(1, "wings-and-feet", true)
+        }
+        .setOnAfterUpdateWorldTransforms { controller ->
+            val lastMousePositionValue = lastMousePosition.value
+            if (lastMousePositionValue == null) {
+                lastMousePosition.value = mousePosition.value
+                return@setOnAfterUpdateWorldTransforms
+            }
+            val mousePositionValue = mousePosition.value ?: return@setOnAfterUpdateWorldTransforms
+
+            val dx = mousePositionValue.x - lastMousePositionValue.x
+            val dy = mousePositionValue.y - lastMousePositionValue.y
+            val position = Point(
+                controller.skeleton.x.toInt(),
+                controller.skeleton.y.toInt()
+            )
+            position.x += dx
+            position.y += dy
+            controller.skeleton.setPosition(position.x.toFloat(), position.y.toFloat());
+            lastMousePosition.value = mousePositionValue
+        }
+        .build()
+    }
+
+    Scaffold(
+        topBar = {
+            TopAppBar(
+                title = { Text(text = Destination.Physics.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,
+                        )
+                        mousePosition.value = controller.toSkeletonCoordinates(
+                            invertedYDragPosition
+                        )
+                    },
+                    onDragEnd = { ->
+                        mousePosition.value = null;
+                        lastMousePosition.value = null;
+                    }
+                )
+            }
+        ) {
+            AndroidView(
+                factory = { context ->
+                    SpineView.loadFromAssets(
+                        "celestial-circus.atlas",
+                        "celestial-circus-pro.skel",
+                        context,
+                        controller
+                    )
+                },
+                modifier = Modifier.padding(paddingValues)
+            )
+        }
+    }
+}

+ 99 - 0
spine-android/app/src/main/java/com/esotericsoftware/spine/PlayPause.kt

@@ -0,0 +1,99 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+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
+import com.esotericsoftware.spine.android.bounds.SkinAndAnimationBounds
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun PlayPause(
+    nav: NavHostController
+) {
+    val controller = remember {
+        SpineController { controller ->
+            controller.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.Builder(ctx, controller)
+                    .setLoadFromAssets("dragon.atlas", "dragon-ess.skel")
+                    .setBoundsProvider(SkinAndAnimationBounds("flying"))
+                    .build()
+            },
+            modifier = Modifier.padding(paddingValues)
+        )
+    }
+}

+ 91 - 0
spine-android/app/src/main/java/com/esotericsoftware/spine/SimpleAnimation.kt

@@ -0,0 +1,91 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+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
+import java.io.File
+import java.net.URL
+
+@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 = { context ->
+                SpineView.loadFromAssets(
+                    "spineboy.atlas",
+                    "spineboy-pro.json",
+                    context,
+                    SpineController {
+                        it.animationState.setAnimation(0, "walk", true)
+                    }
+                )
+//                SpineView.loadFromHttp(
+//                    URL("https://raw.githubusercontent.com/EsotericSoftware/spine-runtimes/4.2/examples/spineboy/export/spineboy.atlas"),
+//                    URL("https://raw.githubusercontent.com/EsotericSoftware/spine-runtimes/4.2/examples/spineboy/export/spineboy-pro.skel"),
+//                    context.filesDir,
+//                    context,
+//                    SpineController {
+//                        it.animationState.setAnimation(0, "walk", true)
+//                    }
+//                )
+            },
+            modifier = Modifier.padding(paddingValues)
+        )
+    }
+}

+ 78 - 0
spine-android/app/src/main/java/com/esotericsoftware/spine/SimpleAnimationActivity.java

@@ -0,0 +1,78 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import android.os.Bundle;
+import android.view.MenuItem;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+
+import com.esotericsoftware.spine.android.SpineController;
+import com.esotericsoftware.spine.android.SpineView;
+
+public class SimpleAnimationActivity extends AppCompatActivity {
+    /** @noinspection FieldCanBeLocal*/
+    private SpineView spineView;
+    /** @noinspection FieldCanBeLocal*/
+    private SpineController spineController;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_simple_animation);
+
+        // Set up the toolbar
+        Toolbar toolbar = findViewById(R.id.toolbar);
+        setSupportActionBar(toolbar);
+        if (getSupportActionBar() != null) {
+            getSupportActionBar().setTitle("Simple Animation");
+            getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+            getSupportActionBar().setDisplayShowHomeEnabled(true);
+        }
+
+        spineView = findViewById(R.id.spineView);
+        spineController = new SpineController( controller ->
+            controller.getAnimationState().setAnimation(0, "walk", true)
+        );
+
+        spineView.setController(spineController);
+        spineView.loadFromAsset("spineboy.atlas","spineboy-pro.json");
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (item.getItemId() == android.R.id.home) {
+            finish();
+            return true;
+        }
+        return super.onOptionsItemSelected(item);
+    }
+}

+ 40 - 0
spine-android/app/src/main/java/com/esotericsoftware/spine/ui/theme/Color.kt

@@ -0,0 +1,40 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Purple80 = Color(0xFFD0BCFF)
+val PurpleGrey80 = Color(0xFFCCC2DC)
+val Pink80 = Color(0xFFEFB8C8)
+
+val Purple40 = Color(0xFF6650a4)
+val PurpleGrey40 = Color(0xFF625b71)
+val Pink40 = Color(0xFF7D5260)

+ 99 - 0
spine-android/app/src/main/java/com/esotericsoftware/spine/ui/theme/Theme.kt

@@ -0,0 +1,99 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.ui.theme
+
+import android.app.Activity
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.core.view.WindowCompat
+
+private val DarkColorScheme = darkColorScheme(
+    primary = Purple80,
+    secondary = PurpleGrey80,
+    tertiary = Pink80
+)
+
+private val LightColorScheme = lightColorScheme(
+    primary = Purple40,
+    secondary = PurpleGrey40,
+    tertiary = Pink40
+
+    /* Other default colors to override
+    background = Color(0xFFFFFBFE),
+    surface = Color(0xFFFFFBFE),
+    onPrimary = Color.White,
+    onSecondary = Color.White,
+    onTertiary = Color.White,
+    onBackground = Color(0xFF1C1B1F),
+    onSurface = Color(0xFF1C1B1F),
+    */
+)
+
+@Composable
+fun SpineAndroidExamplesTheme(
+    darkTheme: Boolean = isSystemInDarkTheme(),
+    // Dynamic color is available on Android 12+
+    dynamicColor: Boolean = true,
+    content: @Composable () -> Unit
+) {
+    val colorScheme = when {
+        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+            val context = LocalContext.current
+            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+        }
+
+        darkTheme -> DarkColorScheme
+        else -> LightColorScheme
+    }
+    val view = LocalView.current
+    if (!view.isInEditMode) {
+        SideEffect {
+            val window = (view.context as Activity).window
+            window.statusBarColor = colorScheme.primary.toArgb()
+            WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
+        }
+    }
+
+    MaterialTheme(
+        colorScheme = colorScheme,
+        typography = Typography,
+        content = content
+    )
+}

+ 63 - 0
spine-android/app/src/main/java/com/esotericsoftware/spine/ui/theme/Type.kt

@@ -0,0 +1,63 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+    bodyLarge = TextStyle(
+        fontFamily = FontFamily.Default,
+        fontWeight = FontWeight.Normal,
+        fontSize = 16.sp,
+        lineHeight = 24.sp,
+        letterSpacing = 0.5.sp
+    )
+    /* Other default text styles to override
+    titleLarge = TextStyle(
+        fontFamily = FontFamily.Default,
+        fontWeight = FontWeight.Normal,
+        fontSize = 22.sp,
+        lineHeight = 28.sp,
+        letterSpacing = 0.sp
+    ),
+    labelSmall = TextStyle(
+        fontFamily = FontFamily.Default,
+        fontWeight = FontWeight.Medium,
+        fontSize = 11.sp,
+        lineHeight = 16.sp,
+        letterSpacing = 0.5.sp
+    )
+    */
+)

+ 170 - 0
spine-android/app/src/main/res/drawable/ic_launcher_background.xml

@@ -0,0 +1,170 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="108"
+    android:viewportHeight="108">
+    <path
+        android:fillColor="#3DDC84"
+        android:pathData="M0,0h108v108h-108z" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M9,0L9,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,0L19,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,0L29,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,0L39,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,0L49,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,0L59,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,0L69,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,0L79,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M89,0L89,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M99,0L99,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,9L108,9"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,19L108,19"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,29L108,29"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,39L108,39"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,49L108,49"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,59L108,59"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,69L108,69"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,79L108,79"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,89L108,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,99L108,99"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,29L89,29"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,39L89,39"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,49L89,49"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,59L89,59"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,69L89,69"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,79L89,79"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,19L29,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,19L39,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,19L49,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,19L59,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,19L69,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,19L79,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+</vector>

+ 30 - 0
spine-android/app/src/main/res/drawable/ic_launcher_foreground.xml

@@ -0,0 +1,30 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="108"
+    android:viewportHeight="108">
+    <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
+        <aapt:attr name="android:fillColor">
+            <gradient
+                android:endX="85.84757"
+                android:endY="92.4963"
+                android:startX="42.9492"
+                android:startY="49.59793"
+                android:type="linear">
+                <item
+                    android:color="#44000000"
+                    android:offset="0.0" />
+                <item
+                    android:color="#00000000"
+                    android:offset="1.0" />
+            </gradient>
+        </aapt:attr>
+    </path>
+    <path
+        android:fillColor="#FFFFFF"
+        android:fillType="nonZero"
+        android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
+        android:strokeWidth="1"
+        android:strokeColor="#00000000" />
+</vector>

BIN
spine-android/app/src/main/res/drawable/img.png


+ 21 - 0
spine-android/app/src/main/res/layout/activity_simple_animation.xml

@@ -0,0 +1,21 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".SimpleAnimationActivity">
+
+    <androidx.appcompat.widget.Toolbar
+        android:id="@+id/toolbar"
+        android:layout_width="match_parent"
+        android:layout_height="?attr/actionBarSize"
+        android:background="?attr/colorPrimary"
+        android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
+        android:elevation="4dp"
+        android:layout_alignParentTop="true" />
+
+    <com.esotericsoftware.spine.android.SpineView
+        android:id="@+id/spineView"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_below="@id/toolbar" />
+</RelativeLayout>

+ 6 - 0
spine-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background" />
+    <foreground android:drawable="@drawable/ic_launcher_foreground" />
+    <monochrome android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>

+ 6 - 0
spine-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background" />
+    <foreground android:drawable="@drawable/ic_launcher_foreground" />
+    <monochrome android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>

BIN
spine-android/app/src/main/res/mipmap-hdpi/ic_launcher.webp


BIN
spine-android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp


BIN
spine-android/app/src/main/res/mipmap-mdpi/ic_launcher.webp


BIN
spine-android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp


BIN
spine-android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp


BIN
spine-android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp


BIN
spine-android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp


BIN
spine-android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp


BIN
spine-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp


BIN
spine-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp


+ 10 - 0
spine-android/app/src/main/res/values/colors.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="purple_200">#FFBB86FC</color>
+    <color name="purple_500">#FF6200EE</color>
+    <color name="purple_700">#FF3700B3</color>
+    <color name="teal_200">#FF03DAC5</color>
+    <color name="teal_700">#FF018786</color>
+    <color name="black">#FF000000</color>
+    <color name="white">#FFFFFFFF</color>
+</resources>

+ 3 - 0
spine-android/app/src/main/res/values/strings.xml

@@ -0,0 +1,3 @@
+<resources>
+    <string name="app_name">Spine Android Examples</string>
+</resources>

+ 5 - 0
spine-android/app/src/main/res/values/themes.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <style name="Theme.SpineAndroidExamples" parent="android:Theme.Material.Light.NoActionBar" />
+</resources>

+ 13 - 0
spine-android/app/src/main/res/xml/backup_rules.xml

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+   Sample backup rules file; uncomment and customize as necessary.
+   See https://developer.android.com/guide/topics/data/autobackup
+   for details.
+   Note: This file is ignored for devices older that API 31
+   See https://developer.android.com/about/versions/12/backup-restore
+-->
+<full-backup-content>
+    <!--
+   <include domain="sharedpref" path="."/>
+   <exclude domain="sharedpref" path="device.xml"/>
+-->
+</full-backup-content>

+ 19 - 0
spine-android/app/src/main/res/xml/data_extraction_rules.xml

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+   Sample data extraction rules file; uncomment and customize as necessary.
+   See https://developer.android.com/about/versions/12/backup-restore#xml-changes
+   for details.
+-->
+<data-extraction-rules>
+    <cloud-backup>
+        <!-- TODO: Use <include> and <exclude> to control what is backed up.
+        <include .../>
+        <exclude .../>
+        -->
+    </cloud-backup>
+    <!--
+    <device-transfer>
+        <include .../>
+        <exclude .../>
+    </device-transfer>
+    -->
+</data-extraction-rules>

+ 17 - 0
spine-android/app/src/test/java/com/esotericsoftware/android/ExampleUnitTest.kt

@@ -0,0 +1,17 @@
+package com.esotericsoftware.android
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+    @Test
+    fun addition_isCorrect() {
+        assertEquals(4, 2 + 2)
+    }
+}

+ 6 - 0
spine-android/build.gradle.kts

@@ -0,0 +1,6 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+    alias(libs.plugins.androidApplication) apply false
+    alias(libs.plugins.jetbrainsKotlinAndroid) apply false
+    alias(libs.plugins.androidLibrary) apply false
+}

+ 23 - 0
spine-android/gradle.properties

@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true

+ 38 - 0
spine-android/gradle/libs.versions.toml

@@ -0,0 +1,38 @@
+[versions]
+agp = "8.3.1"
+kotlin = "1.9.0"
+coreKtx = "1.10.1"
+junit = "4.13.2"
+junitVersion = "1.1.5"
+espressoCore = "3.5.1"
+lifecycleRuntimeKtx = "2.6.1"
+activityCompose = "1.7.0"
+composeBom = "2023.08.00"
+appcompat = "1.6.1"
+navigationCompose = "2.7.7"
+appcompatVersion = "1.7.0"
+
+[libraries]
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
+androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
+androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
+androidx-ui = { group = "androidx.compose.ui", name = "ui" }
+androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
+androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
+androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
+androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
+androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
+androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
+androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
+appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompatVersion" }
+
+[plugins]
+androidApplication = { id = "com.android.application", version.ref = "agp" }
+jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+androidLibrary = { id = "com.android.library", version.ref = "agp" }
+

BIN
spine-android/gradle/wrapper/gradle-wrapper.jar


+ 6 - 0
spine-android/gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,6 @@
+#Thu Apr 25 11:12:13 CEST 2024
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists

+ 185 - 0
spine-android/gradlew

@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+    echo "$*"
+}
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=`expr $i + 1`
+    done
+    case $i in
+        0) set -- ;;
+        1) set -- "$args0" ;;
+        2) set -- "$args0" "$args1" ;;
+        3) set -- "$args0" "$args1" "$args2" ;;
+        4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Escape application args
+save () {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"

+ 89 - 0
spine-android/gradlew.bat

@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem      https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega

+ 38 - 0
spine-android/settings.gradle.kts

@@ -0,0 +1,38 @@
+pluginManagement {
+    repositories {
+        google {
+            content {
+                includeGroupByRegex("com\\.android.*")
+                includeGroupByRegex("com\\.google.*")
+                includeGroupByRegex("androidx.*")
+            }
+        }
+        mavenCentral()
+        gradlePluginPortal()
+    }
+}
+dependencyResolutionManagement {
+    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+    repositories {
+        google()
+        mavenCentral()
+        maven {
+            url = uri("https://oss.sonatype.org/content/repositories/snapshots")
+        }
+        mavenLocal()
+    }
+}
+
+rootProject.name = "Spine Android Examples"
+includeBuild("../spine-libgdx") {
+    dependencySubstitution {
+        substitute(module("com.esotericsoftware.spine:spine-libgdx")).using(project(":spine-libgdx"))
+    }
+}
+//includeBuild("../../libgdx") {
+//    dependencySubstitution {
+//        substitute(module("com.badlogicgames.gdx:gdx")).using(project(":gdx"))
+//    }
+//}
+include(":app")
+include(":spine-android")

+ 1 - 0
spine-android/spine-android/.gitignore

@@ -0,0 +1 @@
+/build

+ 75 - 0
spine-android/spine-android/build.gradle.kts

@@ -0,0 +1,75 @@
+plugins {
+    alias(libs.plugins.androidLibrary)
+    `maven-publish`
+}
+
+android {
+    namespace = "com.esotericsoftware.spine"
+    compileSdk = 34
+
+    defaultConfig {
+        minSdk = 24
+
+        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+        consumerProguardFiles("consumer-rules.pro")
+    }
+
+    buildTypes {
+        release {
+            isMinifyEnabled = false
+            proguardFiles(
+                getDefaultProguardFile("proguard-android-optimize.txt"),
+                "proguard-rules.pro"
+            )
+        }
+    }
+    compileOptions {
+        sourceCompatibility = JavaVersion.VERSION_1_8
+        targetCompatibility = JavaVersion.VERSION_1_8
+    }
+}
+
+dependencies {
+    implementation(libs.androidx.appcompat)
+    api("com.badlogicgames.gdx:gdx:1.12.2-SNAPSHOT")
+    api("com.esotericsoftware.spine:spine-libgdx:4.2.0")
+
+    testImplementation(libs.junit)
+    androidTestImplementation(libs.androidx.junit)
+    androidTestImplementation(libs.androidx.espresso.core)
+}
+
+afterEvaluate {
+    publishing {
+        publications {
+            create<MavenPublication>("mavenLocal") {
+                groupId = "com.esotericsoftware"
+                artifactId = "spine-android"
+                version = "4.2"
+                artifact(tasks.getByName("bundleReleaseAar"))
+
+                pom {
+                    withXml {
+                        val dependenciesNode = asNode().appendNode("dependencies")
+                        configurations.api.get().dependencies.forEach { dependency ->
+                            dependenciesNode.appendNode("dependency").apply {
+                                appendNode("groupId", dependency.group)
+                                appendNode("artifactId", dependency.name)
+                                appendNode("version", dependency.version)
+                                appendNode("scope", "compile")
+                            }
+                        }
+                        configurations.implementation.get().dependencies.forEach { dependency ->
+                            dependenciesNode.appendNode("dependency").apply {
+                                appendNode("groupId", dependency.group)
+                                appendNode("artifactId", dependency.name)
+                                appendNode("version", dependency.version)
+                                appendNode("scope", "runtime")
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}

+ 0 - 0
spine-android/spine-android/consumer-rules.pro


+ 21 - 0
spine-android/spine-android/proguard-rules.pro

@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile

+ 26 - 0
spine-android/spine-android/src/androidTest/java/com/esotericsoftware/android/ExampleInstrumentedTest.java

@@ -0,0 +1,26 @@
+package com.esotericsoftware.android;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+    @Test
+    public void useAppContext() {
+        // Context of the app under test.
+        Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        assertEquals("com.esotericsoftware.spine.test", appContext.getPackageName());
+    }
+}

+ 4 - 0
spine-android/spine-android/src/main/AndroidManifest.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+    <uses-permission android:name="android.permission.INTERNET"/>
+</manifest>

+ 108 - 0
spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidAtlasAttachmentLoader.java

@@ -0,0 +1,108 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android;
+
+import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
+import com.badlogic.gdx.graphics.g2d.TextureRegion;
+import com.badlogic.gdx.utils.Null;
+import com.esotericsoftware.spine.Skin;
+import com.esotericsoftware.spine.attachments.AttachmentLoader;
+import com.esotericsoftware.spine.attachments.BoundingBoxAttachment;
+import com.esotericsoftware.spine.attachments.ClippingAttachment;
+import com.esotericsoftware.spine.attachments.MeshAttachment;
+import com.esotericsoftware.spine.attachments.PathAttachment;
+import com.esotericsoftware.spine.attachments.PointAttachment;
+import com.esotericsoftware.spine.attachments.RegionAttachment;
+import com.esotericsoftware.spine.attachments.Sequence;
+
+/** An {@link AttachmentLoader} that configures attachments using texture regions from an {@link AndroidTextureAtlas}.
+ * <p>
+ * See <a href='http://esotericsoftware.com/spine-loading-skeleton-data#JSON-and-binary-data'>Loading skeleton data</a> in the
+ * Spine Runtimes Guide. */
+@SuppressWarnings("javadoc")
+public class AndroidAtlasAttachmentLoader implements AttachmentLoader {
+	private AndroidTextureAtlas atlas;
+
+	public AndroidAtlasAttachmentLoader (AndroidTextureAtlas atlas) {
+		if (atlas == null) throw new IllegalArgumentException("atlas cannot be null.");
+		this.atlas = atlas;
+	}
+
+	private void loadSequence (String name, String basePath, Sequence sequence) {
+		TextureRegion[] regions = sequence.getRegions();
+		for (int i = 0, n = regions.length; i < n; i++) {
+			String path = sequence.getPath(basePath, i);
+			regions[i] = atlas.findRegion(path);
+			if (regions[i] == null) throw new RuntimeException("Region not found in atlas: " + path + " (sequence: " + name + ")");
+		}
+	}
+
+	public RegionAttachment newRegionAttachment (Skin skin, String name, String path, @Null Sequence sequence) {
+		RegionAttachment attachment = new RegionAttachment(name);
+		if (sequence != null)
+			loadSequence(name, path, sequence);
+		else {
+			AtlasRegion region = atlas.findRegion(path);
+			if (region == null)
+				throw new RuntimeException("Region not found in atlas: " + path + " (region attachment: " + name + ")");
+			attachment.setRegion(region);
+		}
+		return attachment;
+	}
+
+	public MeshAttachment newMeshAttachment (Skin skin, String name, String path, @Null Sequence sequence) {
+		MeshAttachment attachment = new MeshAttachment(name);
+		if (sequence != null)
+			loadSequence(name, path, sequence);
+		else {
+			AtlasRegion region = atlas.findRegion(path);
+			if (region == null)
+				throw new RuntimeException("Region not found in atlas: " + path + " (mesh attachment: " + name + ")");
+			attachment.setRegion(region);
+		}
+		return attachment;
+	}
+
+	public BoundingBoxAttachment newBoundingBoxAttachment (Skin skin, String name) {
+		return new BoundingBoxAttachment(name);
+	}
+
+	public ClippingAttachment newClippingAttachment (Skin skin, String name) {
+		return new ClippingAttachment(name);
+	}
+
+	public PathAttachment newPathAttachment (Skin skin, String name) {
+		return new PathAttachment(name);
+	}
+
+	public PointAttachment newPointAttachment (Skin skin, String name) {
+		return new PointAttachment(name);
+	}
+}

+ 176 - 0
spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidSkeletonDrawable.java

@@ -0,0 +1,176 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+
+import com.badlogic.gdx.math.Vector2;
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.FloatArray;
+import com.esotericsoftware.spine.Animation;
+import com.esotericsoftware.spine.AnimationState;
+import com.esotericsoftware.spine.AnimationStateData;
+import com.esotericsoftware.spine.Skeleton;
+import com.esotericsoftware.spine.SkeletonData;
+import com.esotericsoftware.spine.android.utils.SkeletonDataUtils;
+
+import java.io.File;
+import java.net.URL;
+
+/**
+ * A {@link AndroidSkeletonDrawable} bundles loading updating updating an {@link AndroidTextureAtlas}, {@link Skeleton}, and {@link AnimationState}
+ * into a single easy-to-use class.
+ *
+ * Use the {@link AndroidSkeletonDrawable#fromAsset(String, String, Context)}, {@link AndroidSkeletonDrawable#fromFile(File, File)},
+ * or {@link AndroidSkeletonDrawable#fromHttp(URL, URL, File)} methods to construct a {@link AndroidSkeletonDrawable}. To have
+ * multiple skeleton drawable instances share the same {@link AndroidTextureAtlas} and {@link SkeletonData}, use the constructor.
+ *
+ * You can then directly access the {@link AndroidSkeletonDrawable#getAtlas()}, {@link AndroidSkeletonDrawable#getSkeletonData()},
+ * {@link AndroidSkeletonDrawable#getSkeleton()}, {@link AndroidSkeletonDrawable#getAnimationStateData()}, and {@link AndroidSkeletonDrawable#getAnimationState()}
+ * to query and animate the skeleton. Use the {@link AnimationState} to queue animations on one or more tracks
+ * via {@link AnimationState#setAnimation(int, Animation, boolean)} or {@link AnimationState#addAnimation(int, Animation, boolean, float)}.
+ *
+ * To update the {@link AnimationState} and apply it to the {@link Skeleton}, call the {@link AndroidSkeletonDrawable#update(float)} function, providing it
+ * a delta time in seconds to advance the animations.
+ *
+ * To render the current pose of the {@link Skeleton}, use {@link SkeletonRenderer#render(Skeleton)}, {@link SkeletonRenderer#renderToCanvas(Canvas, Array)},
+ * {@link SkeletonRenderer#renderToBitmap(float, float, int, Skeleton)}, depending on your needs.
+ */
+public class AndroidSkeletonDrawable {
+
+    private final AndroidTextureAtlas atlas;
+
+    private final SkeletonData skeletonData;
+
+    private final Skeleton skeleton;
+
+    private final AnimationStateData animationStateData;
+
+    private final AnimationState animationState;
+
+    /**
+     * Constructs a new skeleton drawable from the given (possibly shared) {@link AndroidTextureAtlas} and {@link SkeletonData}.
+     */
+    public AndroidSkeletonDrawable(AndroidTextureAtlas atlas, SkeletonData skeletonData) {
+        this.atlas = atlas;
+        this.skeletonData = skeletonData;
+
+        skeleton = new Skeleton(skeletonData);
+        animationStateData = new AnimationStateData(skeletonData);
+        animationState = new AnimationState(animationStateData);
+
+        skeleton.updateWorldTransform(Skeleton.Physics.none);
+    }
+
+    /**
+     * Updates the {@link AnimationState} using the {@code delta} time given in seconds, applies the
+     * animation state to the {@link Skeleton} and updates the world transforms of the skeleton
+     * to calculate its current pose.
+     */
+    public void update(float delta) {
+        animationState.update(delta);
+        animationState.apply(skeleton);
+
+        skeleton.update(delta);
+        skeleton.updateWorldTransform(Skeleton.Physics.update);
+    }
+
+    /**
+     * Get the {@link AndroidTextureAtlas}
+     */
+    public AndroidTextureAtlas getAtlas() {
+        return atlas;
+    }
+
+    /**
+     * Get the {@link Skeleton}
+     */
+    public Skeleton getSkeleton() {
+        return skeleton;
+    }
+
+    /**
+     * Get the {@link SkeletonData}
+     */
+    public SkeletonData getSkeletonData() {
+        return skeletonData;
+    }
+
+    /**
+     * Get the {@link AnimationStateData}
+     */
+    public AnimationStateData getAnimationStateData() {
+        return animationStateData;
+    }
+
+    /**
+     * Get the {@link AnimationState}
+     */
+    public AnimationState getAnimationState() {
+        return animationState;
+    }
+
+    /**
+     * Constructs a new skeleton drawable from the {@code atlasFileName} and {@code skeletonFileName} from the the apps resources using {@link Context}.
+     *
+     * Throws an exception in case the data could not be loaded.
+     */
+    public static AndroidSkeletonDrawable fromAsset (String atlasFileName, String skeletonFileName, Context context) {
+        AndroidTextureAtlas atlas = AndroidTextureAtlas.fromAsset(atlasFileName, context);
+        SkeletonData skeletonData = SkeletonDataUtils.fromAsset(atlas, skeletonFileName, context);
+        return new AndroidSkeletonDrawable(atlas, skeletonData);
+    }
+
+    /**
+     * Constructs a new skeleton drawable from the {@code atlasFile} and {@code skeletonFile}.
+     *
+     * Throws an exception in case the data could not be loaded.
+     */
+    public static AndroidSkeletonDrawable fromFile (File atlasFile, File skeletonFile) {
+        AndroidTextureAtlas atlas = AndroidTextureAtlas.fromFile(atlasFile);
+        SkeletonData skeletonData = SkeletonDataUtils.fromFile(atlas, skeletonFile);
+        return new AndroidSkeletonDrawable(atlas, skeletonData);
+    }
+
+    /**
+     * Constructs a new skeleton drawable from the {@code atlasUrl} and {@code skeletonUrl}.
+     *
+     * Throws an exception in case the data could not be loaded.
+     */
+    public static AndroidSkeletonDrawable fromHttp (URL atlasUrl, URL skeletonUrl, File targetDirectory) {
+        AndroidTextureAtlas atlas = AndroidTextureAtlas.fromHttp(atlasUrl, targetDirectory);
+        SkeletonData skeletonData = SkeletonDataUtils.fromHttp(atlas, skeletonUrl, targetDirectory);
+        return new AndroidSkeletonDrawable(atlas, skeletonData);
+    }
+}

+ 102 - 0
spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidTexture.java

@@ -0,0 +1,102 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android;
+
+import com.badlogic.gdx.graphics.Texture;
+import com.badlogic.gdx.utils.ObjectMap;
+import com.esotericsoftware.spine.BlendMode;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapShader;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Shader;
+
+/**
+ * A class holding an {@link Bitmap} of an {@link AndroidTextureAtlas} page image with it's associated
+ * blend modes and paints.
+ */
+public class AndroidTexture extends Texture {
+	private Bitmap bitmap;
+	private ObjectMap<BlendMode, Paint> paints = new ObjectMap<>();
+
+	protected AndroidTexture (Bitmap bitmap) {
+		super();
+		this.bitmap = bitmap;
+		for (BlendMode blendMode : BlendMode.values()) {
+			Paint paint = new Paint();
+			BitmapShader shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
+			paint.setShader(shader);
+
+			switch (blendMode) {
+			case normal:
+				paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER));
+				break;
+			case multiply:
+				paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER));
+				break;
+			case additive:
+				paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.ADD));
+				break;
+			case screen:
+				paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SCREEN));
+				break;
+			default:
+				break;
+			}
+
+			paints.put(blendMode, paint);
+		}
+	}
+
+	public Bitmap getBitmap () {
+		return bitmap;
+	}
+
+	public Paint getPaint (BlendMode blendMode) {
+		return paints.get(blendMode);
+	}
+
+	@Override
+	public int getWidth () {
+		return bitmap.getWidth();
+	}
+
+	@Override
+	public int getHeight () {
+		return bitmap.getHeight();
+	}
+
+	@Override
+	public void dispose () {
+		bitmap.recycle();
+	}
+}

+ 232 - 0
spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidTextureAtlas.java

@@ -0,0 +1,232 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.file.Files;
+
+import com.badlogic.gdx.files.FileHandle;
+import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
+import com.badlogic.gdx.graphics.g2d.TextureAtlas.TextureAtlasData;
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.Null;
+import com.esotericsoftware.spine.android.utils.HttpUtils;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.graphics.Bitmap;
+import android.graphics.Paint;
+import android.graphics.BitmapFactory;
+import android.os.Build;
+
+/**
+ * Atlas data loaded from a `.atlas` file and its corresponding `.png` files. For each atlas image,
+ * a corresponding {@link Bitmap} and {@link Paint} is constructed, which are used when rendering a skeleton
+ * that uses this atlas.
+ *
+ * Use the static methods {@link AndroidTextureAtlas#fromAsset(String, Context)}, {@link AndroidTextureAtlas#fromFile(File)},
+ * and {@link AndroidTextureAtlas#fromHttp(URL, File)} to load an atlas.
+ */
+public class AndroidTextureAtlas {
+	private interface BitmapLoader {
+		Bitmap load (String path);
+	}
+
+	private final Array<AndroidTexture> textures = new Array<>();
+	private final Array<AtlasRegion> regions = new Array<>();
+
+	private AndroidTextureAtlas (TextureAtlasData data, BitmapLoader bitmapLoader) {
+		for (TextureAtlasData.Page page : data.getPages()) {
+			page.texture = new AndroidTexture(bitmapLoader.load(page.textureFile.path()));
+			textures.add((AndroidTexture)page.texture);
+		}
+
+		for (TextureAtlasData.Region region : data.getRegions()) {
+			AtlasRegion atlasRegion = new AtlasRegion(region.page.texture, region.left, region.top, //
+				region.rotate ? region.height : region.width, //
+				region.rotate ? region.width : region.height);
+			atlasRegion.index = region.index;
+			atlasRegion.name = region.name;
+			atlasRegion.offsetX = region.offsetX;
+			atlasRegion.offsetY = region.offsetY;
+			atlasRegion.originalHeight = region.originalHeight;
+			atlasRegion.originalWidth = region.originalWidth;
+			atlasRegion.rotate = region.rotate;
+			atlasRegion.degrees = region.degrees;
+			atlasRegion.names = region.names;
+			atlasRegion.values = region.values;
+			if (region.flip) atlasRegion.flip(false, true);
+			regions.add(atlasRegion);
+		}
+	}
+
+	/**
+	 * Returns the first region found with the specified name. This method uses string comparison to find the region, so the
+	 * result should be cached rather than calling this method multiple times.
+	 */
+	public @Null AtlasRegion findRegion (String name) {
+		for (int i = 0, n = regions.size; i < n; i++)
+			if (regions.get(i).name.equals(name)) return regions.get(i);
+		return null;
+	}
+
+	public Array<AndroidTexture> getTextures () {
+		return textures;
+	}
+
+	public Array<AtlasRegion> getRegions () {
+		return regions;
+	}
+
+	/**
+	 * Loads an {@link AndroidTextureAtlas} from the file {@code atlasFileName} from assets using {@link Context}.
+	 *
+	 * Throws a {@link RuntimeException} in case the atlas could not be loaded.
+	 */
+	public static AndroidTextureAtlas fromAsset(String atlasFileName, Context context) {
+		TextureAtlasData data = new TextureAtlasData();
+		AssetManager assetManager = context.getAssets();
+
+		try {
+			FileHandle inputFile = new FileHandle() {
+				@Override
+				public InputStream read () {
+					try {
+						return assetManager.open(atlasFileName);
+					} catch (IOException e) {
+						throw new RuntimeException(e);
+					}
+				}
+			};
+			data.load(inputFile, new FileHandle(atlasFileName).parent(), false);
+		} catch (Throwable t) {
+			throw new RuntimeException(t);
+		}
+
+		return new AndroidTextureAtlas(data, path -> {
+            path = path.startsWith("/") ? path.substring(1) : path;
+            try (InputStream in = new BufferedInputStream(assetManager.open(path))) {
+                return BitmapFactory.decodeStream(in);
+            } catch (Throwable t) {
+                throw new RuntimeException(t);
+            }
+        });
+	}
+
+	/**
+	 * Loads an {@link AndroidTextureAtlas} from the file {@code atlasFileName}.
+	 *
+	 * Throws a {@link RuntimeException} in case the atlas could not be loaded.
+	 */
+	public static AndroidTextureAtlas fromFile(File atlasFile) {
+		TextureAtlasData data;
+		try {
+			data = loadTextureAtlasData(atlasFile);
+		} catch (Exception e) {
+			throw new RuntimeException(e);
+		}
+		return new AndroidTextureAtlas(data, path -> {
+			File imageFile = new File(path);
+			try (InputStream in = new BufferedInputStream(inputStream(imageFile))) {
+				return BitmapFactory.decodeStream(in);
+			} catch (Throwable t) {
+				throw new RuntimeException(t);
+			}
+		});
+	}
+
+	/**
+	 * Loads an {@link AndroidTextureAtlas} from the URL {@code atlasURL}.
+	 *
+	 * Throws a {@link Exception} in case the atlas could not be loaded.
+	 */
+	public static AndroidTextureAtlas fromHttp(URL atlasUrl, File targetDirectory) {
+		File atlasFile = HttpUtils.downloadFrom(atlasUrl, targetDirectory);
+		TextureAtlasData data;
+		try {
+			data = loadTextureAtlasData(atlasFile);
+		} catch (Exception e) {
+			throw new RuntimeException(e);
+		}
+		return new AndroidTextureAtlas(data, path -> {
+			String fileName = path.substring(path.lastIndexOf('/') + 1);
+
+			String atlasUrlPath = atlasUrl.getPath();
+			int lastSlashIndex = atlasUrlPath.lastIndexOf('/');
+			String imagePath = atlasUrlPath.substring(0, lastSlashIndex + 1) + fileName;
+
+			File imageFile;
+			try {
+				URL imageUrl = new URL(atlasUrl.getProtocol(), atlasUrl.getHost(), atlasUrl.getPort(), imagePath);
+				imageFile = HttpUtils.downloadFrom(imageUrl, targetDirectory);
+			} catch (MalformedURLException e) {
+				throw new RuntimeException(e);
+			}
+
+			try (InputStream in = new BufferedInputStream(inputStream(imageFile))) {
+				return BitmapFactory.decodeStream(in);
+			} catch (Throwable t) {
+				throw new RuntimeException(t);
+			}
+		});
+	}
+
+	private static InputStream inputStream(File file) throws Exception {
+		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+			return Files.newInputStream(file.toPath());
+		} else {
+			//noinspection IOStreamConstructor
+			return new FileInputStream(file);
+		}
+	}
+
+	private static TextureAtlasData loadTextureAtlasData(File atlasFile) {
+		TextureAtlasData data = new TextureAtlasData();
+		FileHandle inputFile = new FileHandle() {
+			@Override
+			public InputStream read() {
+				try {
+					return new FileInputStream(atlasFile);
+				} catch (FileNotFoundException e) {
+					throw new RuntimeException(e);
+				}
+			}
+		};
+		data.load(inputFile, new FileHandle(atlasFile).parent(), false);
+		return data;
+	}
+}

+ 56 - 0
spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/DebugRenderer.java

@@ -0,0 +1,56 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+
+import com.badlogic.gdx.utils.Array;
+import com.esotericsoftware.spine.Bone;
+
+/**
+ * Renders debug information for a {@link AndroidSkeletonDrawable}, like bone locations, to a {@link Canvas}.
+ * See {@link DebugRenderer#render}.
+ */
+public class DebugRenderer {
+
+    public void render(AndroidSkeletonDrawable drawable, Canvas canvas, Array<SkeletonRenderer.RenderCommand> commands) {
+        Paint bonePaint = new Paint();
+        bonePaint.setColor(android.graphics.Color.BLUE);
+        bonePaint.setStyle(Paint.Style.FILL);
+
+        for (Bone bone : drawable.getSkeleton().getBones()) {
+            float x = bone.getWorldX();
+            float y = bone.getWorldY();
+            canvas.drawRect(new RectF(x - 2.5f, y - 2.5f, x + 2.5f, y + 2.5f), bonePaint);
+        }
+    }
+}

+ 281 - 0
spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/SkeletonRenderer.java

@@ -0,0 +1,281 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android;
+
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.math.Vector2;
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.FloatArray;
+import com.badlogic.gdx.utils.IntArray;
+import com.badlogic.gdx.utils.Pool;
+import com.badlogic.gdx.utils.ShortArray;
+import com.esotericsoftware.spine.BlendMode;
+import com.esotericsoftware.spine.Skeleton;
+import com.esotericsoftware.spine.Slot;
+import com.esotericsoftware.spine.attachments.Attachment;
+import com.esotericsoftware.spine.attachments.ClippingAttachment;
+import com.esotericsoftware.spine.attachments.MeshAttachment;
+import com.esotericsoftware.spine.attachments.RegionAttachment;
+import com.esotericsoftware.spine.utils.SkeletonClipping;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+
+/**
+ * Is responsible to transform the {@link Skeleton} with its current pose to {@link SkeletonRenderer.RenderCommand} commands
+ * and render them to a {@link Canvas}.
+ */
+public class SkeletonRenderer {
+
+	/**
+	 * Stores the vertices, indices, and atlas page index to be used for rendering one or more attachments
+	 * of a {@link Skeleton} to a {@link Canvas}. See the implementation of {@link SkeletonRenderer#render(Skeleton)} and
+	 * {@link SkeletonRenderer#renderToCanvas(Canvas, Array)} on how to use this data to render it to a {@link Canvas}.
+	 */
+	public static class RenderCommand implements Pool.Poolable {
+		FloatArray vertices = new FloatArray(32);
+		FloatArray uvs = new FloatArray(32);
+		IntArray colors = new IntArray(32);
+		ShortArray indices = new ShortArray(32);
+		BlendMode blendMode;
+		AndroidTexture texture;
+
+		@Override
+		public void reset () {
+			vertices.setSize(0);
+			uvs.setSize(0);
+			colors.setSize(0);
+			indices.setSize(0);
+			blendMode = null;
+			texture = null;
+		}
+	}
+
+	static private final short[] quadTriangles = {0, 1, 2, 2, 3, 0};
+	private final SkeletonClipping clipper = new SkeletonClipping();
+	private final Pool<RenderCommand> commandPool = new Pool<RenderCommand>(10) {
+		@Override
+		protected RenderCommand newObject () {
+			return new RenderCommand();
+		}
+	};
+	private final Array<RenderCommand> commandList = new Array<RenderCommand>();
+
+	/**
+	 * Created the {@link RenderCommand} commands from the skeletons current pose.
+	 */
+	public Array<RenderCommand> render(Skeleton skeleton) {
+		Color color = null, skeletonColor = skeleton.getColor();
+		float r = skeletonColor.r, g = skeletonColor.g, b = skeletonColor.b, a = skeletonColor.a;
+
+		commandPool.freeAll(commandList);
+		commandList.clear();
+		RenderCommand command = commandPool.obtain();
+		commandList.add(command);
+		int vertexStart = 0;
+
+		Object[] drawOrder = skeleton.getDrawOrder().items;
+		for (int i = 0, n = skeleton.getDrawOrder().size; i < n; i++) {
+			Slot slot = (Slot)drawOrder[i];
+			if (!slot.getBone().isActive()) {
+				clipper.clipEnd(slot);
+				continue;
+			}
+
+			int verticesLength = 0;
+			int vertexSize = 2;
+			float[] uvs = null;
+			short[] indices = null;
+			Attachment attachment = slot.getAttachment();
+			if (attachment == null) {
+				continue;
+			}
+
+			if (attachment instanceof RegionAttachment) {
+				RegionAttachment region = (RegionAttachment)attachment;
+				verticesLength = vertexSize << 2;
+				if (region.getSequence() != null) region.getSequence().apply(slot, region);
+				AndroidTexture texture = (AndroidTexture)region.getRegion().getTexture();
+				BlendMode blendMode = slot.getData().getBlendMode();
+				if (command.blendMode == null && command.texture == null) {
+					command.blendMode = blendMode;
+					command.texture = texture;
+				}
+
+				if (command.blendMode != blendMode || command.texture != texture || command.vertices.size + verticesLength > 64000) {
+					command = commandPool.obtain();
+					commandList.add(command);
+					vertexStart = 0;
+					command.blendMode = blendMode;
+					command.texture = texture;
+				}
+
+				command.vertices.setSize(command.vertices.size + verticesLength);
+				region.computeWorldVertices(slot, command.vertices.items, vertexStart, vertexSize);
+				uvs = region.getUVs();
+				indices = quadTriangles;
+				color = region.getColor();
+			} else if (attachment instanceof MeshAttachment) {
+				MeshAttachment mesh = (MeshAttachment)attachment;
+				verticesLength = mesh.getWorldVerticesLength();
+				if (mesh.getSequence() != null) mesh.getSequence().apply(slot, mesh);
+				AndroidTexture texture = (AndroidTexture)mesh.getRegion().getTexture();
+				BlendMode blendMode = slot.getData().getBlendMode();
+
+				if (command.blendMode == null && command.texture == null) {
+					command.blendMode = blendMode;
+					command.texture = texture;
+				}
+
+				if (command.blendMode != blendMode || command.texture != texture || command.vertices.size + verticesLength > 64000) {
+					command = commandPool.obtain();
+					commandList.add(command);
+					vertexStart = 0;
+					command.blendMode = blendMode;
+					command.texture = texture;
+				}
+
+				command.vertices.setSize(command.vertices.size + verticesLength);
+				mesh.computeWorldVertices(slot, 0, verticesLength, command.vertices.items, vertexStart, vertexSize);
+				uvs = mesh.getUVs();
+				indices = mesh.getTriangles();
+				color = mesh.getColor();
+			} else if (attachment instanceof ClippingAttachment) {
+				ClippingAttachment clip = (ClippingAttachment)attachment;
+				clipper.clipStart(slot, clip);
+				continue;
+			} else {
+				continue;
+			}
+
+			Color slotColor = slot.getColor();
+			int c = (int)(a * slotColor.a * color.a * 255) << 24 //
+				| (int)(r * slotColor.r * color.r * 255) << 16 //
+				| (int)(g * slotColor.g * color.g * 255) << 8 //
+				| (int)(b * slotColor.b * color.b * 255);
+
+			if (clipper.isClipping()) {
+				// FIXME
+				throw new RuntimeException("Not implemented, need to split positions, uvs, colors");
+				// clipper.clipTriangles(vertices, verticesLength, triangles, triangles.length, uvs, c, 0, false);
+				// FloatArray clippedVertices = clipper.getClippedVertices();
+				// ShortArray clippedTriangles = clipper.getClippedTriangles();
+				// batch.draw(texture, clippedVertices.items, 0, clippedVertices.size, clippedTriangles.items, 0,
+				// clippedTriangles.size);
+			} else {
+				command.uvs.addAll(uvs);
+				float[] uvsArray = command.uvs.items;
+				for (int ii = vertexStart, w = command.texture.getWidth(), h = command.texture.getHeight(),
+					nn = vertexStart + verticesLength; ii < nn; ii += 2) {
+					uvsArray[ii] = uvsArray[ii] * w;
+					uvsArray[ii + 1] = uvsArray[ii + 1] * h;
+				}
+
+				command.colors.setSize(command.colors.size + (verticesLength >> 1));
+				int[] colorsArray = command.colors.items;
+				for (int ii = vertexStart >> 1, nn = (vertexStart >> 1) + (verticesLength >> 1); ii < nn; ii++) {
+					colorsArray[ii] = c;
+				}
+
+				int indicesStart = command.indices.size;
+				command.indices.addAll(indices);
+				int firstIndex = vertexStart >> 1;
+				short[] indicesArray = command.indices.items;
+				for (int ii = indicesStart, nn = indicesStart + indices.length; ii < nn; ii++) {
+					indicesArray[ii] += firstIndex;
+				}
+			}
+			// FIXME wrt clipping
+			vertexStart += verticesLength;
+			clipper.clipEnd(slot);
+		}
+		clipper.clipEnd();
+
+		if (commandList.size == 1 && commandList.get(0).vertices.size == 0) {
+			commandPool.freeAll(commandList);
+			commandList.clear();
+		}
+
+		return commandList;
+	}
+
+	/**
+	 * Renders the {@link RenderCommand} commands created from the skeleton current pose to the given {@link Canvas}.
+	 * Does not perform any scaling or fitting.
+	 */
+	public void renderToCanvas(Canvas canvas, Array<RenderCommand> commands) {
+		for (int i = 0; i < commands.size; i++) {
+			RenderCommand command = commands.get(i);
+
+			canvas.drawVertices(Canvas.VertexMode.TRIANGLES, command.vertices.size, command.vertices.items, 0, command.uvs.items, 0,
+				command.colors.items, 0, command.indices.items, 0, command.indices.size, command.texture.getPaint(command.blendMode));
+		}
+	}
+
+	/**
+	 * Renders the {@link Skeleton} with its current pose to a {@link Bitmap}.
+	 *
+	 * @param width    The width of the bitmap in pixels.
+	 * @param height   The height of the bitmap in pixels.
+	 * @param bgColor  The background color.
+	 * @param skeleton The skeleton to render.
+	 */
+	public Bitmap renderToBitmap(float width, float height, int bgColor, Skeleton skeleton) {
+		Vector2 offset = new Vector2(0, 0);
+		Vector2 size = new Vector2(0, 0);
+		FloatArray floatArray = new FloatArray();
+
+		skeleton.getBounds(offset, size, floatArray);
+
+		RectF bounds = new RectF(offset.x, offset.y, offset.x + size.x, offset.y + size.y);
+		float scale = (1 / (bounds.width() > bounds.height() ? bounds.width() / width : bounds.height() / height));
+
+		Bitmap bitmap = Bitmap.createBitmap((int) width, (int) height, Bitmap.Config.ARGB_8888);
+		Canvas canvas = new Canvas(bitmap);
+
+		Paint paint = new Paint();
+		paint.setColor(bgColor);
+		paint.setStyle(Paint.Style.FILL);
+
+		// Draw background
+		canvas.drawRect(0, 0, width, height, paint);
+
+		// Transform canvas
+		canvas.translate(width / 2, height / 2);
+		canvas.scale(scale, -scale);
+		canvas.translate(-(bounds.left + bounds.width() / 2), -(bounds.top + bounds.height() / 2));
+
+		renderToCanvas(canvas, render(skeleton));
+
+		return bitmap;
+	}
+}

+ 331 - 0
spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/SpineController.java

@@ -0,0 +1,331 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android;
+
+import android.graphics.Canvas;
+import android.graphics.Point;
+
+import androidx.annotation.Nullable;
+
+import com.badlogic.gdx.utils.Array;
+import com.esotericsoftware.spine.AnimationState;
+import com.esotericsoftware.spine.AnimationStateData;
+import com.esotericsoftware.spine.Skeleton;
+import com.esotericsoftware.spine.SkeletonData;
+import com.esotericsoftware.spine.android.callbacks.SpineControllerAfterPaintCallback;
+import com.esotericsoftware.spine.android.callbacks.SpineControllerBeforePaintCallback;
+import com.esotericsoftware.spine.android.callbacks.SpineControllerCallback;
+
+/**
+ * Controls how the skeleton of a {@link SpineView} is animated and rendered.
+ *
+ * Upon initialization of a {@link SpineView}, the provided {@code onInitialized} callback method is called once. This method can be used
+ * to set up the initial animation(s) of the skeleton, among other things.
+ *
+ * After initialization is complete, the {@link SpineView} is rendered at the screen refresh rate. In each frame,
+ * the {@link AnimationState} is updated and applied to the {@link Skeleton}.
+ *
+ * Next, the optionally provided method {@code onBeforeUpdateWorldTransforms} is called, which can modify the
+ * skeleton before its current pose is calculated using {@link Skeleton#updateWorldTransform(Skeleton.Physics)}. After
+ * {@link Skeleton#updateWorldTransform(Skeleton.Physics)} has completed, the optional {@code onAfterUpdateWorldTransforms} method is
+ * called, which can modify the current pose before rendering the skeleton.
+ *
+ * Before the skeleton's current pose is rendered by the {@link SpineView}, the optional {@code onBeforePaint} is called,
+ * which allows rendering backgrounds or other objects that should go behind the skeleton on the {@link Canvas}. The
+ * {@link SpineView} then renders the skeleton's current pose and finally calls the optional {@code onAfterPaint}, which
+ * can render additional objects on top of the skeleton.
+ *
+ * The underlying {@link AndroidTextureAtlas}, {@link SkeletonData}, {@link Skeleton}, {@link AnimationStateData}, {@link AnimationState}, and {@link AndroidSkeletonDrawable}
+ * can be accessed through their respective getters to inspect and/or modify the skeleton and its associated data. Accessing
+ * this data is only allowed if the {@link SpineView} and its data have been initialized and have not been disposed of yet.
+ *
+ * By default, the widget updates and renders the skeleton every frame. The {@code pause} method can be used to pause updating
+ * and rendering the skeleton. The {@link SpineController#resume()} method resumes updating and rendering the skeleton. The {@link SpineController#isPlaying()} getter
+ * reports the current state.
+ */
+public class SpineController {
+    /**
+     * Used to build {@link SpineController} instances.
+     * */
+    public static class Builder {
+        private final SpineControllerCallback onInitialized;
+        private SpineControllerCallback onBeforeUpdateWorldTransforms;
+        private SpineControllerCallback onAfterUpdateWorldTransforms;
+        private SpineControllerBeforePaintCallback onBeforePaint;
+        private SpineControllerAfterPaintCallback onAfterPaint;
+
+        /**
+         * Instantiate a {@link Builder} used to build a {@link SpineController}, which controls how the skeleton of a {@link SpineView}
+         * is animated and rendered. Upon initialization of a {@link SpineView}, the provided {@code onInitialized} callback
+         * method is called once. This method can be used to set up the initial animation(s) of the skeleton, among other things.
+         *
+         * @param onInitialized Upon initialization of a {@link SpineView}, the provided {@code onInitialized} callback
+         *                      method is called once. This method can be used to set up the initial animation(s) of the skeleton,
+         *                      among other things.
+         */
+        public Builder(SpineControllerCallback onInitialized) {
+            this.onInitialized = onInitialized;
+        }
+
+        /**
+         * Sets the {@code onBeforeUpdateWorldTransforms} callback. It is called before the skeleton's current pose is calculated
+         * using {@link Skeleton#updateWorldTransform(Skeleton.Physics)}. It can be used to modify the skeleton before the pose calculation.
+         */
+        public Builder setOnBeforeUpdateWorldTransforms(SpineControllerCallback onBeforeUpdateWorldTransforms) {
+            this.onBeforeUpdateWorldTransforms = onBeforeUpdateWorldTransforms;
+            return this;
+        }
+
+        /**
+         * Sets the {@code onAfterUpdateWorldTransforms} callback. This method is called after the skeleton's current pose is calculated using
+         * {@link Skeleton#updateWorldTransform(Skeleton.Physics)}. It can be used to modify the current pose before rendering the skeleton.
+         */
+        public Builder setOnAfterUpdateWorldTransforms(SpineControllerCallback onAfterUpdateWorldTransforms) {
+            this.onAfterUpdateWorldTransforms = onAfterUpdateWorldTransforms;
+            return this;
+        }
+
+        /**
+         * Sets the {@code onBeforePaint} callback. It is called before the skeleton's current pose is rendered by the
+         * {@link SpineView}. It allows rendering backgrounds or other objects that should go behind the skeleton on the
+         * {@link Canvas}.
+         */
+        public Builder setOnBeforePaint(SpineControllerBeforePaintCallback onBeforePaint) {
+            this.onBeforePaint = onBeforePaint;
+            return this;
+        }
+
+        /**
+         * Sets the {@code onAfterPaint} callback. It is called after the skeleton's current pose is rendered by the
+         * {@link SpineView}. It allows rendering additional objects on top of the skeleton.
+         */
+        public Builder setOnAfterPaint(SpineControllerAfterPaintCallback onAfterPaint) {
+            this.onAfterPaint = onAfterPaint;
+            return this;
+        }
+
+        public SpineController build() {
+            SpineController spineController = new SpineController(onInitialized);
+            spineController.onBeforeUpdateWorldTransforms = onBeforeUpdateWorldTransforms;
+            spineController.onAfterUpdateWorldTransforms = onAfterUpdateWorldTransforms;
+            spineController.onBeforePaint = onBeforePaint;
+            spineController.onAfterPaint = onAfterPaint;
+            return spineController;
+        }
+    }
+
+    private final SpineControllerCallback onInitialized;
+    private @Nullable SpineControllerCallback onBeforeUpdateWorldTransforms;
+    private @Nullable SpineControllerCallback onAfterUpdateWorldTransforms;
+    private @Nullable SpineControllerBeforePaintCallback onBeforePaint;
+    private @Nullable SpineControllerAfterPaintCallback onAfterPaint;
+    private AndroidSkeletonDrawable drawable;
+    private boolean playing = true;
+    private double offsetX = 0;
+    private double offsetY = 0;
+    private double scaleX = 1;
+    private double scaleY = 1;
+
+    /**
+     * Instantiate a {@link SpineController}, which controls how the skeleton of a {@link SpineView} is animated and rendered.
+     * Upon initialization of a {@link SpineView}, the provided {@code onInitialized} callback method is called once.
+     * This method can be used to set up the initial animation(s) of the skeleton, among other things.
+     *
+     * @param onInitialized Upon initialization of a {@link SpineView}, the provided {@code onInitialized} callback
+     *                      method is called once. This method can be used to set up the initial animation(s) of the skeleton,
+     *                      among other things.
+     */
+    public SpineController(SpineControllerCallback onInitialized) {
+        this.onInitialized = onInitialized;
+    }
+
+    protected void init(AndroidSkeletonDrawable drawable) {
+        this.drawable = drawable;
+        if (onInitialized != null) {
+            onInitialized.execute(this);
+        }
+    }
+
+    /**
+     * The {@link AndroidTextureAtlas} from which images to render the skeleton are sourced.
+     */
+    public AndroidTextureAtlas getAtlas() {
+        if (drawable == null) throw new RuntimeException("Controller is not initialized yet.");
+        return drawable.getAtlas();
+    }
+
+    /**
+     * The setup-pose data used by the skeleton.
+     */
+    public SkeletonData getSkeletonDate() {
+        if (drawable == null) throw new RuntimeException("Controller is not initialized yet.");
+        return drawable.getSkeletonData();
+    }
+
+    /**
+     * The {@link Skeleton}.
+     */
+    public Skeleton getSkeleton() {
+        if (drawable == null) throw new RuntimeException("Controller is not initialized yet.");
+        return drawable.getSkeleton();
+    }
+
+    /**
+     * The mixing information used by the {@link AnimationState}.
+     */
+    public AnimationStateData getAnimationStateData() {
+        if (drawable == null) throw new RuntimeException("Controller is not initialized yet.");
+        return drawable.getAnimationStateData();
+    }
+
+    /**
+     * The {@link AnimationState} used to manage animations that are being applied to the
+     * skeleton.
+     */
+    public AnimationState getAnimationState() {
+        if (drawable == null) throw new RuntimeException("Controller is not initialized yet.");
+        return drawable.getAnimationState();
+    }
+
+    /**
+     * The {@link AndroidSkeletonDrawable}.
+     */
+    public AndroidSkeletonDrawable getDrawable() {
+        if (drawable == null) throw new RuntimeException("Controller is not initialized yet.");
+        return drawable;
+    }
+
+    /**
+     * Checks if the {@link  SpineView} is initialized.
+     */
+    public boolean isInitialized() {
+        return drawable != null;
+    }
+
+    /**
+     * Checks if the animation is currently playing.
+     */
+    public boolean isPlaying() {
+        return playing;
+    }
+
+    /**
+     * Pauses updating and rendering the skeleton.
+     */
+    public void pause() {
+        if (playing) {
+            playing = false;
+        }
+    }
+
+    /**
+     * Resumes updating and rendering the skeleton.
+     */
+    public void resume() {
+        if (!playing) {
+            playing = true;
+        }
+    }
+
+    /**
+     * Transforms the coordinates given in the {@link SpineView} coordinate system in {@code position} to
+     * the skeleton coordinate system. See the {@code IKFollowing.kt} example for how to use this
+     * to move a bone based on user touch input.
+     */
+    public Point toSkeletonCoordinates(Point position) {
+        int x = position.x;
+        int y = position.y;
+        return new Point((int) (x / scaleX - offsetX), (int) (y / scaleY - offsetY));
+    }
+
+    /**
+     * Sets the {@code onBeforeUpdateWorldTransforms} callback. It is called before the skeleton's current pose is calculated
+     * using {@link Skeleton#updateWorldTransform(Skeleton.Physics)}. It can be used to modify the skeleton before the pose calculation.
+     */
+    public void setOnBeforeUpdateWorldTransforms(@Nullable SpineControllerCallback onBeforeUpdateWorldTransforms) {
+        this.onBeforeUpdateWorldTransforms = onBeforeUpdateWorldTransforms;
+    }
+
+    /**
+     * Sets the {@code onAfterUpdateWorldTransforms} callback. This method is called after the skeleton's current pose is calculated using
+     * {@link Skeleton#updateWorldTransform(Skeleton.Physics)}. It can be used to modify the current pose before rendering the skeleton.
+     */
+    public void setOnAfterUpdateWorldTransforms(@Nullable SpineControllerCallback onAfterUpdateWorldTransforms) {
+        this.onAfterUpdateWorldTransforms = onAfterUpdateWorldTransforms;
+    }
+
+    /**
+     * Sets the {@code onBeforePaint} callback. It is called before the skeleton's current pose is rendered by the
+     * {@link SpineView}. It allows rendering backgrounds or other objects that should go behind the skeleton on the
+     * {@link Canvas}.
+     */
+    public void setOnBeforePaint(@Nullable SpineControllerBeforePaintCallback onBeforePaint) {
+        this.onBeforePaint = onBeforePaint;
+    }
+
+    /**
+     * Sets the {@code onAfterPaint} callback. It is called after the skeleton's current pose is rendered by the
+     * {@link SpineView}. It allows rendering additional objects on top of the skeleton.
+     */
+    public void setOnAfterPaint(@Nullable SpineControllerAfterPaintCallback onAfterPaint) {
+        this.onAfterPaint = onAfterPaint;
+    }
+
+    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);
+        }
+    }
+
+    protected void callOnBeforePaint(Canvas canvas) {
+        if (onBeforePaint != null) {
+            onBeforePaint.execute(this, canvas);
+        }
+    }
+
+    protected void callOnAfterPaint(Canvas canvas, Array<SkeletonRenderer.RenderCommand> renderCommands) {
+        if (onAfterPaint != null) {
+            onAfterPaint.execute(this, canvas, renderCommands);
+        }
+    }
+}

+ 469 - 0
spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/SpineView.java

@@ -0,0 +1,469 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android;
+
+import com.badlogic.gdx.utils.Array;
+import com.esotericsoftware.spine.android.bounds.Alignment;
+import com.esotericsoftware.spine.android.bounds.Bounds;
+import com.esotericsoftware.spine.android.bounds.BoundsProvider;
+import com.esotericsoftware.spine.android.bounds.ContentMode;
+import com.esotericsoftware.spine.android.bounds.SetupPoseBounds;
+import com.esotericsoftware.spine.android.callbacks.AndroidSkeletonDrawableLoader;
+import com.esotericsoftware.spine.Skeleton;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.AttributeSet;
+import android.view.Choreographer;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+
+import java.io.File;
+import java.net.URL;
+
+/**
+ * A {@link View} to display a Spine skeleton. The skeleton can be loaded from an asset bundle ({@link SpineView#loadFromAssets(String, String, Context, SpineController)}),
+ * local files ({@link SpineView#loadFromFile(File, File, Context, SpineController)}), URLs ({@link SpineView#loadFromHttp(URL, URL, File, Context, SpineController)}), or a pre-loaded {@link AndroidSkeletonDrawable} using ({@link SpineView#loadFromDrawable(AndroidSkeletonDrawable, Context, SpineController)}).
+ *
+ * The skeleton displayed by a {@link SpineView} can be controlled via a {@link SpineController}.
+ *
+ * The size of the widget can be derived from the bounds provided by a {@link BoundsProvider}. If the widget is not sized by the bounds
+ * computed by the {@link BoundsProvider}, the widget will use the computed bounds to fit the skeleton inside the widget's dimensions.
+ */
+public class SpineView extends View implements Choreographer.FrameCallback {
+
+	/**
+	 * Used to build {@link SpineView} instances.
+	 * */
+	public static class Builder {
+		private final Context context;
+		private final SpineController controller;
+		private String atlasFileName;
+		private String skeletonFileName;
+		private File atlasFile;
+		private File skeletonFile;
+		private URL atlasUrl;
+		private URL skeletonUrl;
+		private File targetDirectory;
+		private AndroidSkeletonDrawable drawable;
+		private BoundsProvider boundsProvider = new SetupPoseBounds();
+		private Alignment alignment = Alignment.CENTER;
+		private ContentMode contentMode = ContentMode.FIT;
+
+		/**
+		 * Instantiate a {@link Builder} used to build a {@link SpineView}, which is a {@link View} to display a Spine skeleton.
+		 *
+		 * @param controller The skeleton displayed by a {@link SpineView} can be controlled via a {@link SpineController}.
+		 */
+		public Builder(Context context, SpineController controller) {
+			this.context = context;
+			this.controller = controller;
+		}
+
+		/**
+		 * Loads assets from your app assets for the {@link SpineView} if set. The {@code atlasFileName} specifies the
+		 * `.atlas` file to be loaded for the images used to render the skeleton. The {@code skeletonFileName} specifies either a Skeleton `.json` or
+		 * `.skel` file containing the skeleton data.
+		 */
+		public Builder setLoadFromAssets(String atlasFileName, String skeletonFileName) {
+			this.atlasFileName = atlasFileName;
+			this.skeletonFileName = skeletonFileName;
+			return this;
+		}
+
+		/**
+		 * Loads assets from files for the {@link SpineView} if set. The {@code atlasFile} specifies the `.atlas` file to be loaded for the images used to render
+		 * the skeleton. The {@code skeletonFile} specifies either a Skeleton `.json` or `.skel` file containing the skeleton data.
+		 */
+		public Builder setLoadFromFile(File atlasFile, File skeletonFile) {
+			this.atlasFile = atlasFile;
+			this.skeletonFile = skeletonFile;
+			return this;
+		}
+
+		/**
+		 * Loads assets from http for the {@link SpineView} if set. The {@code atlasUrl} specifies the `.atlas` url to be loaded for the images used to render
+		 * the skeleton. The {@code skeletonUrl} specifies either a Skeleton `.json` or `.skel` url containing the skeleton data.
+		 */
+		public Builder setLoadFromHttp(URL atlasUrl, URL skeletonUrl, File targetDirectory) {
+			this.atlasUrl = atlasUrl;
+			this.skeletonUrl = skeletonUrl;
+			this.targetDirectory = targetDirectory;
+			return this;
+		}
+
+		/**
+		 * Uses the {@link AndroidSkeletonDrawable} for the {@link SpineView} if set.
+		 */
+		public Builder setLoadFromDrawable(AndroidSkeletonDrawable drawable) {
+			this.drawable = drawable;
+			return this;
+		}
+
+		/**
+		 * Get the {@link BoundsProvider} used to compute the bounds of the {@link Skeleton} inside the view.
+		 * The default is {@link SetupPoseBounds}.
+		 */
+		public Builder setBoundsProvider(BoundsProvider boundsProvider) {
+			this.boundsProvider = boundsProvider;
+			return this;
+		}
+
+		/**
+		 * Get the {@link  ContentMode} used to fit the {@link Skeleton} inside the view.
+		 * The default is {@link  ContentMode#FIT}.
+		 */
+		public Builder setContentMode(ContentMode contentMode) {
+			this.contentMode = contentMode;
+			return this;
+		}
+
+		/**
+		 * Set the {@link  Alignment} used to align the {@link Skeleton} inside the view.
+		 * The default is {@link  Alignment#CENTER}
+		 */
+		public Builder setAlignment(Alignment alignment) {
+			this.alignment = alignment;
+			return this;
+		}
+
+		/**
+		 * Builds a new {@link SpineView}.
+		 *
+		 * After initialization is complete, the provided {@code SpineController} is invoked as per the {@link SpineController} semantics, to allow
+		 * modifying how the skeleton inside the widget is animated and rendered.
+		 */
+		public SpineView build() {
+			SpineView spineView = new SpineView(context, controller);
+			spineView.boundsProvider = boundsProvider;
+			spineView.alignment = alignment;
+			spineView.contentMode = contentMode;
+			if (atlasFileName != null && skeletonFileName != null) {
+				spineView.loadFromAsset(atlasFileName, skeletonFileName);
+			} else if (atlasFile != null && skeletonFile != null) {
+				spineView.loadFromFile(atlasFile, skeletonFile);
+			} else if (atlasUrl != null && skeletonUrl != null && targetDirectory != null) {
+				spineView.loadFromHttp(atlasUrl, skeletonUrl, targetDirectory);
+			} else if (drawable != null) {
+				spineView.loadFromDrawable(drawable);
+			}
+			return spineView;
+		}
+	}
+
+	private long lastTime = 0;
+	private float delta = 0;
+	private float offsetX = 0;
+	private float offsetY = 0;
+	private float scaleX = 1;
+	private float scaleY = 1;
+	private float x = 0;
+	private float y = 0;
+	private final SkeletonRenderer renderer = new SkeletonRenderer();
+	private Boolean rendering = true;
+	private Bounds computedBounds = new Bounds();
+
+	private SpineController controller;
+	private BoundsProvider boundsProvider = new SetupPoseBounds();
+	private Alignment alignment = Alignment.CENTER;
+	private ContentMode contentMode = ContentMode.FIT;
+
+	/**
+	 * Constructs a new {@link SpineView}.
+	 *
+	 * After initialization is complete, the provided {@code SpineController} is invoked as per the {@link SpineController} semantics, to allow
+	 * modifying how the skeleton inside the widget is animated and rendered.
+	 */
+	public SpineView (Context context, SpineController controller) {
+		super(context);
+		this.controller = controller;
+	}
+
+	/**
+	 * Constructs a new {@link SpineView} without providing a {@link SpineController}, which you need to provide using
+	 * {@link SpineView#setController(SpineController)}.
+	 */
+	public SpineView (Context context, AttributeSet attrs) {
+		super(context, attrs);
+		// Set properties by view id
+	}
+
+	/**
+	 * Constructs a new {@link SpineView} without providing a {@link SpineController}, which you need to provide using
+	 * {@link SpineView#setController(SpineController)}.
+	 */
+	public SpineView (Context context, AttributeSet attrs, int defStyle) {
+		super(context, attrs, defStyle);
+		// Set properties by view id
+	}
+
+	/**
+	 * Constructs a new {@link SpineView} from files in your app assets. The {@code atlasFileName} specifies the
+	 * `.atlas` file to be loaded for the images used to render the skeleton. The {@code skeletonFileName} specifies either a Skeleton `.json` or
+	 * `.skel` file containing the skeleton data.
+	 *
+	 * After initialization is complete, the provided {@code controller} is invoked as per the {@link SpineController} semantics, to allow
+	 * modifying how the skeleton inside the widget is animated and rendered.
+	 */
+	public static SpineView loadFromAssets(String atlasFileName, String skeletonFileName, Context context, SpineController controller) {
+		SpineView spineView = new SpineView(context, controller);
+		spineView.loadFromAsset(atlasFileName, skeletonFileName);
+		return spineView;
+	}
+
+	/**
+	 * Constructs a new {@link SpineView} from files. The {@code atlasFile} specifies the `.atlas` file to be loaded for the images used to render
+	 * the skeleton. The {@code skeletonFile} specifies either a Skeleton `.json` or `.skel` file containing the skeleton data.
+	 *
+	 * After initialization is complete, the provided {@code SpineController} is invoked as per the {@link SpineController} semantics, to allow
+	 * modifying how the skeleton inside the widget is animated and rendered.
+	 */
+	public static SpineView loadFromFile(File atlasFile, File skeletonFile, Context context, SpineController controller) {
+		SpineView spineView = new SpineView(context, controller);
+		spineView.loadFromFile(atlasFile, skeletonFile);
+		return spineView;
+	}
+
+	/**
+	 * Constructs a new {@link SpineView} from HTTP URLs. The {@code atlasUrl} specifies the `.atlas` url to be loaded for the images used to render
+	 * the skeleton. The {@code skeletonUrl} specifies either a Skeleton `.json` or `.skel` url containing the skeleton data.
+	 *
+	 * After initialization is complete, the provided {@code SpineController} is invoked as per the {@link SpineController} semantics, to allow
+	 * modifying how the skeleton inside the widget is animated and rendered.
+	 */
+	public static SpineView loadFromHttp(URL atlasUrl, URL skeletonUrl, File targetDirectory, Context context, SpineController controller) {
+		SpineView spineView = new SpineView(context, controller);
+		spineView.loadFromHttp(atlasUrl, skeletonUrl, targetDirectory);
+		return spineView;
+	}
+
+	/**
+	 * Constructs a new {@link SpineView} from a {@link AndroidSkeletonDrawable}.
+	 *
+	 * After initialization is complete, the provided {@code SpineController} is invoked as per the {@link SpineController} semantics, to allow
+	 * modifying how the skeleton inside the widget is animated and rendered.
+	 */
+	public static SpineView loadFromDrawable(AndroidSkeletonDrawable drawable, Context context, SpineController controller) {
+		SpineView spineView = new SpineView(context, controller);
+		spineView.loadFromDrawable(drawable);
+		return spineView;
+	}
+
+	/**
+	 * The same as {@link SpineView#loadFromAssets(String, String, Context, SpineController)}, but can be used after
+	 * instantiating the view via {@link SpineView#SpineView(Context, SpineController)}.
+	 */
+	public void loadFromAsset(String atlasFileName, String skeletonFileName) {
+		loadFrom(() -> AndroidSkeletonDrawable.fromAsset(atlasFileName, skeletonFileName, getContext()));
+	}
+
+	/**
+	 * The same as {@link SpineView#loadFromFile(File, File, Context, SpineController)}, but can be used after
+	 * instantiating the view via {@link SpineView#SpineView(Context, SpineController)}.
+	 */
+	public void loadFromFile(File atlasFile, File skeletonFile) {
+		loadFrom(() -> AndroidSkeletonDrawable.fromFile(atlasFile, skeletonFile));
+	}
+
+	/**
+	 * The same as {@link SpineView#loadFromHttp(URL, URL, File, Context, SpineController)}, but can be used after
+	 * instantiating the view via {@link SpineView#SpineView(Context, SpineController)}.
+	 */
+	public void loadFromHttp(URL atlasUrl, URL skeletonUrl, File targetDirectory) {
+		loadFrom(() -> AndroidSkeletonDrawable.fromHttp(atlasUrl, skeletonUrl, targetDirectory));
+	}
+
+	/**
+	 * The same as {@link SpineView#loadFromDrawable(AndroidSkeletonDrawable, Context, SpineController)}, but can be used after
+	 * instantiating the view via {@link SpineView#SpineView(Context, SpineController)}.
+	 */
+	public void loadFromDrawable(AndroidSkeletonDrawable drawable) {
+		loadFrom(() -> drawable);
+	}
+
+	/**
+	 * Get the {@link  SpineController}
+	 */
+	public SpineController getController() {
+		return controller;
+	}
+
+	/**
+	 * Set the {@link  SpineController}. Only do this if you use {@link SpineView#SpineView(Context, AttributeSet)},
+	 * {@link SpineView#SpineView(Context, AttributeSet, int)}, or create the {@link SpineView} in an XML layout.
+	 */
+	public void setController(SpineController controller) {
+		this.controller = controller;
+	}
+
+	/**
+	 * Get the {@link  Alignment} used to align the {@link Skeleton} inside the view.
+	 * The default is {@link  Alignment#CENTER}
+	 */
+	public Alignment getAlignment() {
+		return alignment;
+	}
+
+	/**
+	 * Set the {@link  Alignment}.
+	 */
+	public void setAlignment(Alignment alignment) {
+		this.alignment = alignment;
+		updateCanvasTransform();
+	}
+
+	/**
+	 * Get the {@link  ContentMode} used to fit the {@link Skeleton} inside the view.
+	 * The default is {@link  ContentMode#FIT}.
+	 */
+	public ContentMode getContentMode() {
+		return contentMode;
+	}
+
+	/**
+	 * Set the {@link  ContentMode}.
+	 */
+	public void setContentMode(ContentMode contentMode) {
+		this.contentMode = contentMode;
+		updateCanvasTransform();
+	}
+
+	/**
+	 * Get the {@link BoundsProvider} used to compute the bounds of the {@link Skeleton} inside the view.
+	 * The default is {@link SetupPoseBounds}.
+	 */
+	public BoundsProvider getBoundsProvider() {
+		return boundsProvider;
+	}
+
+	/**
+	 * Set the {@link  BoundsProvider}.
+	 */
+	public void setBoundsProvider(BoundsProvider boundsProvider) {
+		this.boundsProvider = boundsProvider;
+		updateCanvasTransform();
+	}
+
+	/**
+	 * Check if rendering is enabled.
+	 */
+	public Boolean isRendering() {
+		return rendering;
+	}
+
+	/**
+	 * Set to disable or enable rendering. Disable it when the spine view is out of bounds and you want to preserve CPU/GPU resources.
+	 */
+	public void setRendering(Boolean rendering) {
+		this.rendering = rendering;
+	}
+
+	private void loadFrom(AndroidSkeletonDrawableLoader loader) {
+		Handler mainHandler = new Handler(Looper.getMainLooper());
+		Thread backgroundThread = new Thread(() -> {
+			final AndroidSkeletonDrawable skeletonDrawable = loader.load();
+			mainHandler.post(() -> {
+				computedBounds = boundsProvider.computeBounds(skeletonDrawable);
+				updateCanvasTransform();
+
+				controller.init(skeletonDrawable);
+				Choreographer.getInstance().postFrameCallback(SpineView.this);
+			});
+		});
+		backgroundThread.start();
+	}
+
+	@Override
+	public void onDraw (@NonNull Canvas canvas) {
+		super.onDraw(canvas);
+		if (controller == null || !controller.isInitialized() || !rendering) {
+			return;
+		}
+
+		if (controller.isPlaying()) {
+			controller.callOnBeforeUpdateWorldTransforms();
+			controller.getDrawable().update(delta);
+			controller.callOnAfterUpdateWorldTransforms();
+		}
+
+		canvas.save();
+
+		canvas.translate(offsetX, offsetY);
+		canvas.scale(scaleX, scaleY * -1);
+		canvas.translate(x, y);
+
+		controller.callOnBeforePaint(canvas);
+		Array<SkeletonRenderer.RenderCommand> commands = renderer.render(controller.getSkeleton());
+		renderer.renderToCanvas(canvas, commands);
+		controller.callOnAfterPaint(canvas, commands);
+
+		canvas.restore();
+	}
+
+	@Override
+	protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+		super.onSizeChanged(w, h, oldw, oldh);
+		updateCanvasTransform();
+	}
+
+	private void updateCanvasTransform() {
+		if (controller == null) {
+			return;
+		}
+		x = (float) (-computedBounds.getX() - computedBounds.getWidth() / 2.0 - (alignment.getX() * computedBounds.getWidth() / 2.0));
+		y = (float) (-computedBounds.getY() - computedBounds.getHeight() / 2.0 - (alignment.getY() * computedBounds.getHeight() / 2.0));
+
+		switch (contentMode) {
+			case FIT:
+				scaleX = scaleY = (float) Math.min(getWidth() / computedBounds.getWidth(), getHeight() / computedBounds.getHeight());
+				break;
+			case FILL:
+				scaleX = scaleY = (float) Math.max(getWidth() / computedBounds.getWidth(), getHeight() / computedBounds.getHeight());
+				break;
+		}
+		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
+
+	@Override
+	public void doFrame (long frameTimeNanos) {
+		if (lastTime != 0) delta = (frameTimeNanos - lastTime) / 1e9f;
+		lastTime = frameTimeNanos;
+		invalidate();
+		Choreographer.getInstance().postFrameCallback(this);
+	}
+}

+ 61 - 0
spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/Alignment.java

@@ -0,0 +1,61 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android.bounds;
+
+/**
+ * How a view should be aligned within another view.
+ */
+public enum Alignment {
+    TOP_LEFT(-1.0f, -1.0f),
+    TOP_CENTER(0.0f, -1.0f),
+    TOP_RIGHT(1.0f, -1.0f),
+    CENTER_LEFT(-1.0f, 0.0f),
+    CENTER(0.0f, 0.0f),
+    CENTER_RIGHT(1.0f, 0.0f),
+    BOTTOM_LEFT(-1.0f, 1.0f),
+    BOTTOM_CENTER(0.0f, 1.0f),
+    BOTTOM_RIGHT(1.0f, 1.0f);
+
+    private final float x;
+    private final float y;
+
+    Alignment(float x, float y) {
+        this.x = x;
+        this.y = y;
+    }
+
+    public float getX() {
+        return x;
+    }
+
+    public float getY() {
+        return y;
+    }
+}

+ 104 - 0
spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/Bounds.java

@@ -0,0 +1,104 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android.bounds;
+
+import com.badlogic.gdx.math.Vector2;
+import com.badlogic.gdx.utils.FloatArray;
+import com.esotericsoftware.spine.Skeleton;
+
+/**
+ * Bounds denoted by the top left corner coordinates {@code x} and {@code y}
+ * and the {@code width} and {@code height}.
+ */
+public class Bounds {
+    private double x;
+    private double y;
+    private double width;
+    private double height;
+
+    public Bounds() {
+        this.x = 0;
+        this.y = 0;
+        this.width = 0;
+        this.height = 0;
+    }
+
+    public Bounds(double x, double y, double width, double height) {
+        this.x = x;
+        this.y = y;
+        this.width = width;
+        this.height = height;
+    }
+
+    public Bounds(Skeleton skeleton) {
+        Vector2 offset = new Vector2(0, 0);
+        Vector2 size = new Vector2(0, 0);
+        FloatArray floatArray = new FloatArray();
+
+        skeleton.getBounds(offset, size, floatArray);
+
+        x = offset.x;
+        y = offset.y;
+        width = size.x;
+        height = size.y;
+    }
+
+    public double getX() {
+        return x;
+    }
+
+    public void setX(double x) {
+        this.x = x;
+    }
+
+    public double getY() {
+        return y;
+    }
+
+    public void setY(double y) {
+        this.y = y;
+    }
+
+    public double getWidth() {
+        return width;
+    }
+
+    public void setWidth(double width) {
+        this.width = width;
+    }
+
+    public double getHeight() {
+        return height;
+    }
+
+    public void setHeight(double height) {
+        this.height = height;
+    }
+}

+ 40 - 0
spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/BoundsProvider.java

@@ -0,0 +1,40 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android.bounds;
+
+import com.esotericsoftware.spine.android.AndroidSkeletonDrawable;
+
+/**
+ * A {@link BoundsProvider} that calculates the bounding box of the skeleton based on the visible
+ * attachments in the setup pose.
+ */
+public interface BoundsProvider {
+    Bounds computeBounds(AndroidSkeletonDrawable drawable);
+}

+ 44 - 0
spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/ContentMode.java

@@ -0,0 +1,44 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android.bounds;
+
+/**
+ * How a view should be inscribed into another view.
+ */
+public enum ContentMode {
+    /**
+     * As large as possible while still containing the source view entirely within the target view.
+     */
+    FIT,
+    /**
+     * Fill the target view by distorting the source's aspect ratio.
+     */
+    FILL
+}

+ 54 - 0
spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/RawBounds.java

@@ -0,0 +1,54 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android.bounds;
+
+import com.esotericsoftware.spine.android.AndroidSkeletonDrawable;
+
+/**
+ * A {@link BoundsProvider} that returns fixed bounds.
+ */
+public class RawBounds implements BoundsProvider {
+    final Double x;
+    final Double y;
+    final Double width;
+    final Double height;
+
+    public RawBounds(Double x, Double y, Double width, Double height) {
+        this.x = x;
+        this.y = y;
+        this.width = width;
+        this.height = height;
+    }
+
+    @Override
+    public Bounds computeBounds(AndroidSkeletonDrawable drawable) {
+        return new Bounds(x, y, width, height);
+    }
+}

+ 44 - 0
spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/SetupPoseBounds.java

@@ -0,0 +1,44 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android.bounds;
+
+import com.esotericsoftware.spine.android.AndroidSkeletonDrawable;
+
+/**
+ * A {@link BoundsProvider} that calculates the bounding box of the skeleton based on the visible
+ * attachments in the setup pose.
+ */
+public class SetupPoseBounds implements BoundsProvider {
+
+    @Override
+    public Bounds computeBounds(AndroidSkeletonDrawable drawable) {
+        return new Bounds(drawable.getSkeleton());
+    }
+}

+ 122 - 0
spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/SkinAndAnimationBounds.java

@@ -0,0 +1,122 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android.bounds;
+
+import com.esotericsoftware.spine.Animation;
+import com.esotericsoftware.spine.SkeletonData;
+import com.esotericsoftware.spine.Skin;
+import com.esotericsoftware.spine.android.AndroidSkeletonDrawable;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A {@link BoundsProvider} that calculates the bounding box needed for a combination of skins
+ * and an animation.
+ */
+public class SkinAndAnimationBounds implements BoundsProvider {
+    private final List<String> skins;
+    private final String animation;
+    private final double stepTime;
+
+    /**
+     * Constructs a new provider that will use the given {@code skins} and {@code animation} to calculate
+     * the bounding box of the skeleton. If no skins are given, the default skin is used.
+     * The {@code stepTime}, given in seconds, defines at what interval the bounds should be sampled
+     * across the entire animation.
+     */
+    public SkinAndAnimationBounds(List<String> skins, String animation, double stepTime) {
+        this.skins = (skins == null || skins.isEmpty()) ? Collections.singletonList("default") : skins;
+        this.animation = animation;
+        this.stepTime = stepTime;
+    }
+
+    /**
+     * Constructs a new provider that will use the given {@code skins} and {@code animation} to calculate
+     * the bounding box of the skeleton. If no skins are given, the default skin is used.
+     * The {@code stepTime} has default value 0.1.
+     */
+    public SkinAndAnimationBounds(List<String> skins, String animation) {
+        this(skins, animation, 0.1);
+    }
+
+    /**
+     * Constructs a new provider that will use the given {@code skins} and {@code animation} to calculate
+     * the bounding box of the skeleton. The default skin is used. The {@code stepTime} has default value 0.1.
+     */
+    public SkinAndAnimationBounds(String animation) {
+        this(Collections.emptyList(), animation, 0.1);
+    }
+
+    @Override
+    public Bounds computeBounds(AndroidSkeletonDrawable drawable) {
+        SkeletonData data = drawable.getSkeletonData();
+        Skin oldSkin = drawable.getSkeleton().getSkin();
+        Skin customSkin = new Skin("custom-skin");
+        for (String skinName : skins) {
+            Skin skin = data.findSkin(skinName);
+            if (skin == null) continue;
+            customSkin.addSkin(skin);
+        }
+        drawable.getSkeleton().setSkin(customSkin);
+        drawable.getSkeleton().setToSetupPose();
+
+        Animation animation = (this.animation != null) ? data.findAnimation(this.animation) : null;
+        double minX = Double.POSITIVE_INFINITY;
+        double minY = Double.POSITIVE_INFINITY;
+        double maxX = Double.NEGATIVE_INFINITY;
+        double maxY = Double.NEGATIVE_INFINITY;
+        if (animation == null) {
+            Bounds bounds = new Bounds(drawable.getSkeleton());
+            minX = bounds.getX();
+            minY = bounds.getY();
+            maxX = minX + bounds.getWidth();
+            maxY = minY + bounds.getHeight();
+        } else {
+            drawable.getAnimationState().setAnimation(0, animation, false);
+            int steps = (int) Math.max( (animation.getDuration() / stepTime), 1.0);
+            for (int i = 0; i < steps; i++) {
+                drawable.update(i > 0 ? (float) stepTime : 0);
+                Bounds bounds = new Bounds(drawable.getSkeleton());
+                minX = Math.min(minX, bounds.getX());
+                minY = Math.min(minY, bounds.getY());
+                maxX = Math.max(maxX, minX + bounds.getWidth());
+                maxY = Math.max(maxY, minY + bounds.getHeight());
+            }
+        }
+
+        drawable.getSkeleton().setSkin("default");
+        drawable.getAnimationState().clearTracks();
+        if (oldSkin != null) drawable.getSkeleton().setSkin(oldSkin);
+        drawable.getSkeleton().setToSetupPose();
+        drawable.update(0);
+        return new Bounds(minX, minY, maxX - minX, maxY - minY);
+    }
+}

+ 37 - 0
spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/callbacks/AndroidSkeletonDrawableLoader.java

@@ -0,0 +1,37 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android.callbacks;
+
+import com.esotericsoftware.spine.android.AndroidSkeletonDrawable;
+
+@FunctionalInterface
+public interface AndroidSkeletonDrawableLoader {
+    AndroidSkeletonDrawable load();
+}

+ 43 - 0
spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/callbacks/SpineControllerAfterPaintCallback.java

@@ -0,0 +1,43 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android.callbacks;
+
+import android.graphics.Canvas;
+
+import com.badlogic.gdx.utils.Array;
+import com.esotericsoftware.spine.android.SkeletonRenderer;
+import com.esotericsoftware.spine.android.SpineController;
+
+import java.util.List;
+
+@FunctionalInterface
+public interface SpineControllerAfterPaintCallback {
+    void execute (SpineController controller, Canvas canvas, Array<SkeletonRenderer.RenderCommand> commands);
+}

+ 42 - 0
spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/callbacks/SpineControllerBeforePaintCallback.java

@@ -0,0 +1,42 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android.callbacks;
+
+import android.graphics.Canvas;
+
+import com.esotericsoftware.spine.android.SkeletonRenderer;
+import com.esotericsoftware.spine.android.SpineController;
+
+import java.util.List;
+
+@FunctionalInterface
+public interface SpineControllerBeforePaintCallback {
+    void execute (SpineController controller, Canvas canvas);
+}

+ 37 - 0
spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/callbacks/SpineControllerCallback.java

@@ -0,0 +1,37 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android.callbacks;
+
+import com.esotericsoftware.spine.android.SpineController;
+
+@FunctionalInterface
+public interface SpineControllerCallback {
+    void execute (SpineController controller);
+}

+ 113 - 0
spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/utils/HttpUtils.java

@@ -0,0 +1,113 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android.utils;
+
+import android.os.Build;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.file.Files;
+
+/**
+ * Helper to load http resources.
+ */
+public class HttpUtils {
+    /**
+     * Download a file from an url into a target directory. It keeps the name from the {@code url}.
+     * This should NOT be executed on the main run loop.
+     */
+    public static File downloadFrom(URL url, File targetDirectory) throws RuntimeException {
+        HttpURLConnection urlConnection = null;
+        InputStream inputStream = null;
+        OutputStream outputStream = null;
+
+        try {
+            urlConnection = (HttpURLConnection) url.openConnection();
+            urlConnection.connect();
+
+            if (urlConnection.getResponseCode() != HttpURLConnection.HTTP_OK) {
+                throw new RuntimeException("Failed to connect: HTTP response code " + urlConnection.getResponseCode());
+            }
+
+            inputStream = new BufferedInputStream(urlConnection.getInputStream());
+
+            String atlasUrlPath = url.getPath();
+            String fileName = atlasUrlPath.substring(atlasUrlPath.lastIndexOf('/') + 1);
+            File file = new File(targetDirectory, fileName);
+
+            // Create an OutputStream to write to the file
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                outputStream = Files.newOutputStream(file.toPath());
+            } else {
+                //noinspection IOStreamConstructor
+                outputStream = new FileOutputStream(file);
+            }
+
+            byte[] buffer = new byte[1024];
+            int bytesRead;
+
+            // Write the input stream to the output stream
+            while ((bytesRead = inputStream.read(buffer)) != -1) {
+                outputStream.write(buffer, 0, bytesRead);
+            }
+            return file;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        } finally {
+            if (outputStream != null) {
+                try {
+                    outputStream.flush();
+                    outputStream.close();
+                } catch (IOException e) {
+                    // Nothing we can do
+                }
+            }
+
+            if (inputStream != null) {
+                try {
+                    inputStream.close();
+                } catch (IOException e) {
+                    // Nothing we can do
+                }
+            }
+
+            if (urlConnection != null) {
+                urlConnection.disconnect();
+            }
+        }
+    }
+}
+

+ 108 - 0
spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/utils/SkeletonDataUtils.java

@@ -0,0 +1,108 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android.utils;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+
+import com.badlogic.gdx.files.FileHandle;
+import com.esotericsoftware.spine.SkeletonBinary;
+import com.esotericsoftware.spine.SkeletonData;
+import com.esotericsoftware.spine.SkeletonJson;
+import com.esotericsoftware.spine.SkeletonLoader;
+import com.esotericsoftware.spine.android.AndroidAtlasAttachmentLoader;
+import com.esotericsoftware.spine.android.AndroidTextureAtlas;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+
+/**
+ * Helper to load {@link SkeletonData} from assets.
+ */
+public class SkeletonDataUtils {
+
+    /**
+     * Loads a {@link SkeletonData} from the file {@code skeletonFile} in assets using {@link Context}.
+     * Uses the provided {@link AndroidTextureAtlas} to resolve attachment images.
+     *
+     * Throws a {@link RuntimeException} in case the skeleton data could not be loaded.
+     */
+    public static SkeletonData fromAsset(AndroidTextureAtlas atlas, String skeletonFileName, Context context) {
+        AndroidAtlasAttachmentLoader attachmentLoader = new AndroidAtlasAttachmentLoader(atlas);
+
+        SkeletonLoader skeletonLoader;
+        if (skeletonFileName.endsWith(".json")) {
+            skeletonLoader = new SkeletonJson(attachmentLoader);
+        } else {
+            skeletonLoader = new SkeletonBinary(attachmentLoader);
+        }
+
+        SkeletonData skeletonData;
+
+        AssetManager assetManager = context.getAssets();
+        try (InputStream in = new BufferedInputStream(assetManager.open(skeletonFileName))) {
+            skeletonData = skeletonLoader.readSkeletonData(in);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+        return skeletonData;
+    }
+
+    /**
+     * Loads a {@link SkeletonData} from the file {@code skeletonFile}. Uses the provided {@link AndroidTextureAtlas} to resolve attachment images.
+     *
+     * Throws a {@link RuntimeException} in case the skeleton data could not be loaded.
+     */
+    public static SkeletonData fromFile(AndroidTextureAtlas atlas, File skeletonFile) {
+        AndroidAtlasAttachmentLoader attachmentLoader = new AndroidAtlasAttachmentLoader(atlas);
+
+        SkeletonLoader skeletonLoader;
+        if (skeletonFile.getPath().endsWith(".json")) {
+            skeletonLoader = new SkeletonJson(attachmentLoader);
+        } else {
+            skeletonLoader = new SkeletonBinary(attachmentLoader);
+        }
+
+        return skeletonLoader.readSkeletonData(new FileHandle(skeletonFile));
+    }
+
+    /**
+     * Loads a {@link SkeletonData} from the URL {@code skeletonURL}. Uses the provided {@link AndroidTextureAtlas} to resolve attachment images.
+     *
+     * Throws a {@link RuntimeException} in case the skeleton data could not be loaded.
+     */
+    public static SkeletonData fromHttp(AndroidTextureAtlas atlas, URL skeletonUrl, File targetDirectory) {
+        File skeletonFile = HttpUtils.downloadFrom(skeletonUrl, targetDirectory);
+        return fromFile(atlas, skeletonFile);
+    }
+}

+ 17 - 0
spine-android/spine-android/src/test/java/com/esotericsoftware/android/ExampleUnitTest.java

@@ -0,0 +1,17 @@
+package com.esotericsoftware.android;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+public class ExampleUnitTest {
+    @Test
+    public void addition_isCorrect() {
+        assertEquals(4, 2 + 2);
+    }
+}

+ 1 - 1
spine-libgdx/build.gradle

@@ -2,7 +2,7 @@ group = "com.esotericsoftware.spine"
 version = "4.2.0"
 
 ext {
-    libgdxVersion = "1.12.1"
+    libgdxVersion = "1.12.2-SNAPSHOT"
     javaVersion = 8
 }
 

+ 1 - 1
spine-libgdx/settings.gradle

@@ -1,4 +1,4 @@
 // includeBuild "../../libgdx"
 include ":spine-libgdx"
 include ":spine-libgdx-tests"
-include ":spine-skeletonviewer"
+include ":spine-skeletonviewer"

+ 5 - 1
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Slot.java

@@ -44,7 +44,7 @@ import com.esotericsoftware.spine.attachments.VertexAttachment;
 public class Slot {
 	final SlotData data;
 	final Bone bone;
-	final Color color = new Color();
+	Color color = new Color();
 	@Null final Color darkColor;
 	@Null Attachment attachment;
 	int sequenceIndex;
@@ -95,6 +95,10 @@ public class Slot {
 		return color;
 	}
 
+	public void setColor(Color color) {
+		this.color = color;
+	}
+
 	/** The dark color used to tint the slot's attachment for two color tinting, or null if two color tinting is not used. The dark
 	 * color's alpha is not used. */
 	public @Null Color getDarkColor () {

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно