Переглянути джерело

Merge branch '4.2' into gdextension

# Conflicts:
#	spine-godot/spine_godot/SpineAtlasResource.cpp
Mario Zechner 11 місяців тому
батько
коміт
f8a0b5b6f7
100 змінених файлів з 6512 додано та 76 видалено
  1. 35 0
      .github/workflows/spine-android.yml
  2. 2 2
      .github/workflows/spine-godot-v4-all.yml
  3. 105 24
      .github/workflows/spine-godot-v4.yml
  4. 31 31
      .github/workflows/spine-godot.yml
  5. 18 0
      .github/workflows/spine-haxe.yml
  6. 11 17
      .github/workflows/spine-libgdx.yml
  7. 10 1
      CHANGELOG.md
  8. 36 0
      examples/export/runtimes.sh
  9. 2 1
      formatters/build.gradle
  10. 15 0
      spine-android/.gitignore
  11. 1 0
      spine-android/app/.gitignore
  12. 75 0
      spine-android/app/build.gradle.kts
  13. 21 0
      spine-android/app/proguard-rules.pro
  14. 24 0
      spine-android/app/src/androidTest/java/com/esotericsoftware/android/ExampleInstrumentedTest.kt
  15. 34 0
      spine-android/app/src/main/AndroidManifest.xml
  16. BIN
      spine-android/app/src/main/assets/celestial-circus-pro.skel
  17. 173 0
      spine-android/app/src/main/assets/celestial-circus.atlas
  18. BIN
      spine-android/app/src/main/assets/celestial-circus.png
  19. BIN
      spine-android/app/src/main/assets/dragon-ess.skel
  20. 112 0
      spine-android/app/src/main/assets/dragon.atlas
  21. BIN
      spine-android/app/src/main/assets/dragon.png
  22. BIN
      spine-android/app/src/main/assets/dragon_2.png
  23. BIN
      spine-android/app/src/main/assets/dragon_3.png
  24. BIN
      spine-android/app/src/main/assets/dragon_4.png
  25. BIN
      spine-android/app/src/main/assets/dragon_5.png
  26. BIN
      spine-android/app/src/main/assets/mix-and-match-pro.skel
  27. 358 0
      spine-android/app/src/main/assets/mix-and-match.atlas
  28. BIN
      spine-android/app/src/main/assets/mix-and-match.png
  29. 557 0
      spine-android/app/src/main/assets/spineboy-pro.json
  30. BIN
      spine-android/app/src/main/assets/spineboy-pro.skel
  31. 94 0
      spine-android/app/src/main/assets/spineboy.atlas
  32. BIN
      spine-android/app/src/main/assets/spineboy.png
  33. 174 0
      spine-android/app/src/main/java/com/esotericsoftware/spine/AnimationStateEvents.kt
  34. 91 0
      spine-android/app/src/main/java/com/esotericsoftware/spine/DebugRendering.kt
  35. 237 0
      spine-android/app/src/main/java/com/esotericsoftware/spine/DisableRendering.kt
  36. 228 0
      spine-android/app/src/main/java/com/esotericsoftware/spine/DressUp.kt
  37. 140 0
      spine-android/app/src/main/java/com/esotericsoftware/spine/IKFollowing.kt
  38. 220 0
      spine-android/app/src/main/java/com/esotericsoftware/spine/MainActivity.kt
  39. 153 0
      spine-android/app/src/main/java/com/esotericsoftware/spine/Physics.kt
  40. 99 0
      spine-android/app/src/main/java/com/esotericsoftware/spine/PlayPause.kt
  41. 91 0
      spine-android/app/src/main/java/com/esotericsoftware/spine/SimpleAnimation.kt
  42. 76 0
      spine-android/app/src/main/java/com/esotericsoftware/spine/SimpleAnimationActivity.java
  43. 40 0
      spine-android/app/src/main/java/com/esotericsoftware/spine/ui/theme/Color.kt
  44. 99 0
      spine-android/app/src/main/java/com/esotericsoftware/spine/ui/theme/Theme.kt
  45. 63 0
      spine-android/app/src/main/java/com/esotericsoftware/spine/ui/theme/Type.kt
  46. 170 0
      spine-android/app/src/main/res/drawable/ic_launcher_background.xml
  47. 30 0
      spine-android/app/src/main/res/drawable/ic_launcher_foreground.xml
  48. BIN
      spine-android/app/src/main/res/drawable/img.png
  49. 21 0
      spine-android/app/src/main/res/layout/activity_simple_animation.xml
  50. 6 0
      spine-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  51. 6 0
      spine-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  52. BIN
      spine-android/app/src/main/res/mipmap-hdpi/ic_launcher.webp
  53. BIN
      spine-android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
  54. BIN
      spine-android/app/src/main/res/mipmap-mdpi/ic_launcher.webp
  55. BIN
      spine-android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
  56. BIN
      spine-android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
  57. BIN
      spine-android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
  58. BIN
      spine-android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
  59. BIN
      spine-android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
  60. BIN
      spine-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
  61. BIN
      spine-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
  62. 10 0
      spine-android/app/src/main/res/values/colors.xml
  63. 3 0
      spine-android/app/src/main/res/values/strings.xml
  64. 5 0
      spine-android/app/src/main/res/values/themes.xml
  65. 13 0
      spine-android/app/src/main/res/xml/backup_rules.xml
  66. 19 0
      spine-android/app/src/main/res/xml/data_extraction_rules.xml
  67. 17 0
      spine-android/app/src/test/java/com/esotericsoftware/android/ExampleUnitTest.kt
  68. 6 0
      spine-android/build.gradle.kts
  69. 23 0
      spine-android/gradle.properties
  70. 38 0
      spine-android/gradle/libs.versions.toml
  71. BIN
      spine-android/gradle/wrapper/gradle-wrapper.jar
  72. 6 0
      spine-android/gradle/wrapper/gradle-wrapper.properties
  73. 185 0
      spine-android/gradlew
  74. 89 0
      spine-android/gradlew.bat
  75. 14 0
      spine-android/publish.sh
  76. 38 0
      spine-android/settings.gradle.kts
  77. 1 0
      spine-android/spine-android/.gitignore
  78. 134 0
      spine-android/spine-android/build.gradle.kts
  79. 0 0
      spine-android/spine-android/consumer-rules.pro
  80. 21 0
      spine-android/spine-android/proguard-rules.pro
  81. 25 0
      spine-android/spine-android/src/androidTest/java/com/esotericsoftware/android/ExampleInstrumentedTest.java
  82. 4 0
      spine-android/spine-android/src/main/AndroidManifest.xml
  83. 108 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidAtlasAttachmentLoader.java
  84. 158 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidSkeletonDrawable.java
  85. 99 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidTexture.java
  86. 221 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidTextureAtlas.java
  87. 54 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/DebugRenderer.java
  88. 301 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/SkeletonRenderer.java
  89. 287 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/SpineController.java
  90. 419 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/SpineView.java
  91. 52 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/Alignment.java
  92. 101 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/Bounds.java
  93. 38 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/BoundsProvider.java
  94. 38 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/ContentMode.java
  95. 52 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/RawBounds.java
  96. 42 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/SetupPoseBounds.java
  97. 111 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/SkinAndAnimationBounds.java
  98. 37 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/callbacks/AndroidSkeletonDrawableLoader.java
  99. 43 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/callbacks/SpineControllerAfterPaintCallback.java
  100. 42 0
      spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/callbacks/SpineControllerBeforePaintCallback.java

+ 35 - 0
.github/workflows/spine-android.yml

@@ -0,0 +1,35 @@
+name: Build spine-android
+
+on:
+  push:
+    paths:
+      - 'spine-android/**'
+  workflow_dispatch:
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v3
+    - name: Set up JDK 17
+      uses: actions/setup-java@v3
+      with:
+        distribution: 'zulu'
+        java-version: "17"
+
+    - name: Setup Android SDK
+      uses: android-actions/setup-android@v3
+      with:
+        api-level: 34
+        build-tools: 35.0.0
+
+    - name: Cache Gradle packages
+      uses: actions/cache@v3
+      with:
+        path: ~/.gradle/caches
+        key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
+        restore-keys: ${{ runner.os }}-gradle
+
+    - name: Build spine-android
+      working-directory: spine-android
+      run: ./gradlew publishReleasePublicationToSonaType -PossrhUsername=${{ secrets.SONATYPE_USER }} -PossrhPassword=${{ secrets.SONATYPE_PASSWORD }}

+ 2 - 2
.github/workflows/spine-godot-v4-all.yml

@@ -14,8 +14,8 @@ jobs:
       matrix:
         version:
           [
-            {"tag": "4.1.4-stable", "version": "4.1.4.stable", "mono": false},
-            {"tag": "4.1.4-stable", "version": "4.1.4.stable", "mono": true},
+            {"tag": "4.3-stable", "version": "4.3.stable", "mono": false},
+            {"tag": "4.3-stable", "version": "4.3.stable", "mono": true},
             {"tag": "4.2.2-stable", "version": "4.2.2.stable", "mono": false},
             {"tag": "4.2.2-stable", "version": "4.2.2.stable", "mono": true},
           ]

+ 105 - 24
.github/workflows/spine-godot-v4.yml

@@ -34,7 +34,7 @@ env:
   AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
   AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
   AWS_EC2_METADATA_DISABLED: true
-  EM_VERSION: 3.1.18
+  EM_VERSION: 3.1.26
   GODOT_TAG: ${{ inputs.godot_tag }}
   GODOT_VERSION: ${{ inputs.godot_version }}
   GODOT_MONO: ${{ inputs.godot_mono }}
@@ -302,6 +302,42 @@ jobs:
           name: ${{ format('{0}-{1}{2}.zip', 'godot-template-web-release', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
           path: spine-godot/godot/bin/web_release.zip
 
+      - name: Upload artifacts no threads debug
+        uses: actions/upload-artifact@v3
+        with:
+          name: ${{ format('{0}-{1}{2}.zip', 'godot-template-web-nothreads-debug', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
+          path: spine-godot/godot/bin/web_nothreads_debug.zip
+
+      - name: Upload artifacts no threads release
+        uses: actions/upload-artifact@v3
+        with:
+          name: ${{ format('{0}-{1}{2}.zip', 'godot-template-web-nothreads-release', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
+          path: spine-godot/godot/bin/web_nothreads_release.zip
+
+      - name: Upload artifacts dlink debug
+        uses: actions/upload-artifact@v3
+        with:
+          name: ${{ format('{0}-{1}{2}.zip', 'godot-template-web-dlink-debug', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
+          path: spine-godot/godot/bin/web_dlink_debug.zip
+
+      - name: Upload artifacts dlink release
+        uses: actions/upload-artifact@v3
+        with:
+          name: ${{ format('{0}-{1}{2}.zip', 'godot-template-web-dlink-release', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
+          path: spine-godot/godot/bin/web_dlink_release.zip
+
+      - name: Upload artifacts dlink nothreads debug
+        uses: actions/upload-artifact@v3
+        with:
+          name: ${{ format('{0}-{1}{2}.zip', 'godot-template-web-dlink-nothreads-debug', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
+          path: spine-godot/godot/bin/web_dlink_nothreads_debug.zip
+
+      - name: Upload artifacts dlink nothreads release
+        uses: actions/upload-artifact@v3
+        with:
+          name: ${{ format('{0}-{1}{2}.zip', 'godot-template-web-dlink-nothreads-release', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
+          path: spine-godot/godot/bin/web_dlink_nothreads_release.zip
+
   upload-to-s3:
     needs: [godot-editor-windows, godot-editor-linux, godot-editor-macos, godot-template-ios, godot-template-macos, godot-template-windows, godot-template-linux, godot-template-android, godot-template-web]
     runs-on: ubuntu-latest
@@ -309,75 +345,105 @@ jobs:
 
     steps:
       - name: Download godot-editor-windows artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: ${{ format('{0}-{1}{2}.zip', 'godot-editor-windows', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
 
       - name: Download godot-editor-linux artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: ${{ format('{0}-{1}{2}.zip', 'godot-editor-linux', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
 
       - name: Download godot-editor-macos artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: ${{ format('{0}-{1}{2}.zip', 'godot-editor-macos', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
 
       - name: Download godot-template-ios artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: ${{ format('{0}-{1}{2}.zip', 'godot-template-ios', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
 
       - name: Download godot-template-macos artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: ${{ format('{0}-{1}{2}.zip', 'godot-template-macos', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
 
       - name: Download godot-template-windows-release artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: ${{ format('{0}-{1}{2}.zip', 'godot-template-windows-release', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
 
       - name: Download godot-template-windows-debug artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: ${{ format('{0}-{1}{2}.zip', 'godot-template-windows-debug', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
 
       - name: Download godot-template-linux-release artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: ${{ format('{0}-{1}{2}.zip', 'godot-template-linux-release', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
 
       - name: Download godot-template-linux-debug artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: ${{ format('{0}-{1}{2}.zip', 'godot-template-linux-debug', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
 
       - name: Download godot-template-android-release artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: ${{ format('{0}-{1}{2}.zip', 'godot-template-android-release', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
 
       - name: Download godot-template-android-debug artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: ${{ format('{0}-{1}{2}.zip', 'godot-template-android-debug', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
 
       - name: Download godot-template-android-source artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: ${{ format('{0}-{1}{2}.zip', 'godot-template-android-source', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
 
       - name: Download godot-template-web-release artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: ${{ format('{0}-{1}{2}.zip', 'godot-template-web-release', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
 
       - name: Download godot-template-web-debug artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: ${{ format('{0}-{1}{2}.zip', 'godot-template-web-debug', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
 
+      - name: Download godot-template-web-nothreads-release artifact
+        uses: actions/download-artifact@v4
+        with:
+          name: ${{ format('{0}-{1}{2}.zip', 'godot-template-web-nothreads-release', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
+
+      - name: Download godot-template-web-nothreads-debug artifact
+        uses: actions/download-artifact@v4
+        with:
+          name: ${{ format('{0}-{1}{2}.zip', 'godot-template-web-nothreads-debug', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
+
+      - name: Download godot-template-web-dlink-release artifact
+        uses: actions/download-artifact@v4
+        with:
+          name: ${{ format('{0}-{1}{2}.zip', 'godot-template-web-dlink-release', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
+
+      - name: Download godot-template-web-dlink-debug artifact
+        uses: actions/download-artifact@v4
+        with:
+          name: ${{ format('{0}-{1}{2}.zip', 'godot-template-web-dlink-debug', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
+
+      - name: Download godot-template-web-dlink-nothreads-release artifact
+        uses: actions/download-artifact@v4
+        with:
+          name: ${{ format('{0}-{1}{2}.zip', 'godot-template-web-dlink-nothreads-release', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
+
+      - name: Download godot-template-web-dlink-nothreads-debug artifact
+        uses: actions/download-artifact@v4
+        with:
+          name: ${{ format('{0}-{1}{2}.zip', 'godot-template-web-dlink-nothreads-debug', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
+
       - name: Upload artifacts to S3
         shell: bash
         if: env.AWS_ACCESS_KEY_ID != null
@@ -391,9 +457,24 @@ jobs:
           aws s3 cp godot-editor-windows.zip s3://spine-godot/$BRANCH/$GODOT_TAG/
           aws s3 cp godot-editor-linux.zip s3://spine-godot/$BRANCH/$GODOT_TAG/
           aws s3 cp godot-editor-macos.zip s3://spine-godot/$BRANCH/$GODOT_TAG/
+
           echo "$GODOT_VERSION" > version.txt
+          # Extract the major and minor version from GODOT_VERSION
+          GODOT_MAJOR_MINOR=$(echo "$GODOT_VERSION" | sed -E 's/^([0-9]+\.[0-9]+).*/\1/')
+
+          # Check if the version is >= 4.3
+          if [[ $(echo "$GODOT_MAJOR_MINOR >= 4.3" | bc) -eq 1 ]]; then
+            mv web_release.zip web_nothreads_release.zip
+            mv web_debug.zip web_nothreads_debug.zip
+            WEB_RELEASE_FILE="web_nothreads_release.zip"
+            WEB_DEBUG_FILE="web_nothreads_debug.zip"
+          else
+            WEB_RELEASE_FILE="web_release.zip"
+            WEB_DEBUG_FILE="web_debug.zip"
+          fi
+
           ls -lah
-          zip spine-godot-templates-$BRANCH-$GODOT_TAG.zip ios.zip macos.zip windows_debug_x86_64.exe windows_release_x86_64.exe linux_debug.x86_64 linux_release.x86_64 web_debug.zip web_release.zip android_release.apk android_debug.apk android_source.zip version.txt
+          zip spine-godot-templates-$BRANCH-$GODOT_TAG.zip ios.zip macos.zip windows_debug_x86_64.exe windows_release_x86_64.exe linux_debug.x86_64 linux_release.x86_64 "$WEB_DEBUG_FILE" "$WEB_RELEASE_FILE" android_release.apk android_debug.apk android_source.zip version.txt
           aws s3 cp spine-godot-templates-$BRANCH-$GODOT_TAG.zip s3://spine-godot/$BRANCH/$GODOT_TAG/spine-godot-templates-$BRANCH-$GODOT_TAG.tpz
 
   upload-to-s3-mono:
@@ -403,42 +484,42 @@ jobs:
 
     steps:
       - name: Download godot-editor-windows artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: ${{ format('{0}-{1}{2}.zip', 'godot-editor-windows', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
 
       - name: Download godot-editor-linux artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: ${{ format('{0}-{1}{2}.zip', 'godot-editor-linux', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
 
       - name: Download godot-editor-macos artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: ${{ format('{0}-{1}{2}.zip', 'godot-editor-macos', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
 
       - name: Download godot-template-macos artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: ${{ format('{0}-{1}{2}.zip', 'godot-template-macos', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
 
       - name: Download godot-template-windows-release artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: ${{ format('{0}-{1}{2}.zip', 'godot-template-windows-release', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
 
       - name: Download godot-template-windows-debug artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: ${{ format('{0}-{1}{2}.zip', 'godot-template-windows-debug', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
 
       - name: Download godot-template-linux-release artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: ${{ format('{0}-{1}{2}.zip', 'godot-template-linux-release', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
 
       - name: Download godot-template-linux-debug artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: ${{ format('{0}-{1}{2}.zip', 'godot-template-linux-debug', env.GODOT_TAG, env.GODOT_MONO_UPLOAD_SUFFIX) }}
 

+ 31 - 31
.github/workflows/spine-godot.yml

@@ -12,8 +12,8 @@ env:
   AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
   AWS_EC2_METADATA_DISABLED: true
   EM_VERSION: 3.1.14
-  GODOT_TAG: 3.5.3-stable
-  GODOT_VERSION: 3.5.3.stable
+  GODOT_TAG: 3.6-stable
+  GODOT_VERSION: 3.6.stable
 
 jobs:
   godot-editor-windows:
@@ -33,7 +33,7 @@ jobs:
           ./spine-godot/build/build.sh release_debug
 
       - name: Upload artifacts
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: godot-editor-windows.zip
           path: spine-godot/godot/bin/godot.windows.opt.tools.64.exe
@@ -57,7 +57,7 @@ jobs:
           ./spine-godot/build/build.sh release_debug
 
       - name: Upload artifacts
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: godot-editor-linux.zip
           path: spine-godot/godot/bin/godot.x11.opt.tools.64
@@ -82,7 +82,7 @@ jobs:
           popd
 
       - name: Upload artifacts
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: godot-editor-macos.zip
           path: spine-godot/godot/bin/godot-editor-macos.zip
@@ -103,7 +103,7 @@ jobs:
           ./spine-godot/build/build-templates.sh ios
 
       - name: Upload artifacts
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: godot-template-ios.zip
           path: spine-godot/godot/bin/iphone.zip
@@ -124,7 +124,7 @@ jobs:
           ./spine-godot/build/build-templates.sh macos
 
       - name: Upload artifacts
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: godot-template-macos.zip
           path: spine-godot/godot/bin/osx.zip
@@ -147,13 +147,13 @@ jobs:
           ./spine-godot/build/build-templates.sh linux
 
       - name: Upload artifacts debug
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: godot-template-linux-debug.zip
           path: spine-godot/godot/bin/linux_x11_64_debug
 
       - name: Upload artifacts release
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: godot-template-linux-release.zip
           path: spine-godot/godot/bin/linux_x11_64_release
@@ -175,13 +175,13 @@ jobs:
           ./spine-godot/build/build-templates.sh windows
 
       - name: Upload artifacts debug
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: godot-template-windows-debug.zip
           path: spine-godot/godot/bin/windows_64_debug.exe
 
       - name: Upload artifacts release
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: godot-template-windows-release.zip
           path: spine-godot/godot/bin/windows_64_release.exe
@@ -203,7 +203,7 @@ jobs:
       - name: Set up Java 11
         uses: actions/setup-java@v1
         with:
-          java-version: 11
+          java-version: 17
 
       - name: Setup python and scons
         uses: ./.github/actions/setup-godot-deps-3
@@ -215,19 +215,19 @@ jobs:
           ./spine-godot/build/build-templates.sh android
 
       - name: Upload artifacts debug
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: godot-template-android-debug.zip
           path: spine-godot/godot/bin/android_debug.apk
 
       - name: Upload artifacts release
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: godot-template-android-release.zip
           path: spine-godot/godot/bin/android_release.apk
 
       - name: Upload artifacts source
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: godot-template-android-source.zip
           path: spine-godot/godot/bin/android_source.zip
@@ -257,13 +257,13 @@ jobs:
           ./spine-godot/build/build-templates.sh web
 
       - name: Upload artifacts debug
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: godot-template-web-debug.zip
           path: spine-godot/godot/bin/webassembly_debug.zip
 
       - name: Upload artifacts release
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: godot-template-web-release.zip
           path: spine-godot/godot/bin/webassembly_release.zip
@@ -273,72 +273,72 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Download godot-editor-windows artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: godot-editor-windows.zip
 
       - name: Download godot-editor-linux artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: godot-editor-linux.zip
 
       - name: Download godot-editor-macos artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: godot-editor-macos.zip
 
       - name: Download godot-template-ios artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: godot-template-ios.zip
 
       - name: Download godot-template-macos artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: godot-template-macos.zip
 
       - name: Download godot-template-windows-release artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: godot-template-windows-release.zip
 
       - name: Download godot-template-windows-debug artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: godot-template-windows-debug.zip
 
       - name: Download godot-template-linux-release artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: godot-template-linux-release.zip
 
       - name: Download godot-template-linux-debug artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: godot-template-linux-debug.zip
 
       - name: Download godot-template-android-release artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: godot-template-android-release.zip
 
       - name: Download godot-template-android-debug artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: godot-template-android-debug.zip
 
       - name: Download godot-template-android-source artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: godot-template-android-source.zip
 
       - name: Download godot-template-web-release artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: godot-template-web-release.zip
 
       - name: Download godot-template-web-debug artifact
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v4
         with:
           name: godot-template-web-debug.zip
 

+ 18 - 0
.github/workflows/spine-haxe.yml

@@ -0,0 +1,18 @@
+name: Build spine-haxe
+
+on:
+  push:
+    paths:
+      - 'spine-haxe/**'
+  workflow_dispatch:
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v3
+    - name: Build spine-haxe
+      working-directory: spine-haxe
+      env:
+        HAXE_UPDATE_URL: ${{secrets.HAXE_UPDATE_URL}}
+      run: ./build.sh

+ 11 - 17
.github/workflows/spine-libgdx.yml

@@ -10,26 +10,20 @@ jobs:
   build:
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v3
-    - name: Set up JDK 1.8
-      uses: actions/setup-java@v3 
+    - uses: actions/checkout@v2
+    - name: Set up JDK 17
+      uses: actions/setup-java@v3
       with:
-        distribution: 'zulu'
-        java-version: "8"       
-        server-id: sonatype-nexus-snapshots
-        server-username: MAVEN_USERNAME
-        server-password: MAVEN_PASSWORD
+        java-version: '17'
+        distribution: 'temurin'
 
-    - name: Cache Maven packages
+    - name: Cache Gradle packages
       uses: actions/cache@v3
       with:
-        path: ~/.m2
-        key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
-        restore-keys: ${{ runner.os }}-m2
+        path: ~/.gradle/caches
+        key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
+        restore-keys: ${{ runner.os }}-gradle
 
     - name: Build spine-libgdx
-      working-directory: spine-libgdx/spine-libgdx      
-      run: mvn clean deploy
-      env:
-          MAVEN_USERNAME: ${{ secrets.SONATYPE_USER }}
-          MAVEN_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
+      working-directory: spine-libgdx
+      run: ./gradlew publishReleasePublicationToSonaType -PossrhUsername=${{ secrets.SONATYPE_USER }} -PossrhPassword=${{ secrets.SONATYPE_PASSWORD }}

+ 10 - 1
CHANGELOG.md

@@ -88,6 +88,7 @@
 - **Breaking**: Starting with Unreal Engine 5.3 imported `.skel`/`.json` and `.atlas` files in the same folder must NOT have a common prefix. E.g. `skeleton.json` and `skeleton.atlas` will not work. Make sure to rename at least one of the two files so there is no prefix collision, e.g. `skeleton-data.json` and `skeleton.atlas`.
 - Added compatibility with UE 5.3
 - Added more example maps
+- Added blueprint-callable methods `PhysicsTranslate()`, `PhysicsRotate()` and `ResetPhysicsConstraints()` (which will reset all physics constraints in the skeleton) to `SpineSkeletonComponent` and `SpineWidget`.
 
 ### Godot
 
@@ -122,7 +123,7 @@
 
 ### Unity
 
-- **Officially supported Unity versions are 2017.1-2022.1**.
+- **Officially supported Unity versions are 2017.1-2023.1**.
 
 - **Additions**
 
@@ -160,6 +161,9 @@
   - All Spine Outline shaders, including the URP outline shader, now provide an additional parameter `Width in Screen Space`. Enable it to keep the outline width constant in screen space instead of texture space. Requires more expensive computations, so enable only where necessary. Defaults to `disabled` to maintain existing behaviour.
   - Added support for BlendModeMaterials at runtime instantiation from files via an additional method `SkeletonDataAsset.SetupRuntimeBlendModeMaterials`. See example scene `Spine Examples/Other Examples/Instantiate from Script` for a usage example.
   - SkeletonGraphic: You can now offset the skeleton mesh relative to the pivot via a newly added green circle handle. This allows you to e.g. frame only the face of a skeleton inside a masked frame. Previously offsetting the pivot downwards fails when `Layout Scale Mode` scales the mesh smaller and towards the pivot (e.g. the feet) and thus out of the frame. Now you can keep the pivot in the center of the `RectTransform` while offsetting only the mesh downwards, keeping the desired skeleton area (e.g. the face) centered while resizing. Moving the new larger green circle handle moves the mesh offset, while moving the blue pivot circle handle moves the pivot as usual.
+  - `Universal Render Pipeline/Spine/Skeleton` shader now performs proper alpha-testing when `Depth Write` is enabled, using the existing `Shadow alpha cutoff` parameter.
+  - `SkeletonRootMotion` components now provide a public `Initialize()` method which is automatically called when calling `skeletonAnimation.Initialize(true)` to update the necessary skeleton references. If a different root bone shall be used, be sure to set `skeletonRootMotion.rootMotionBoneName` before calling `skeletonAnimation.Initialize(true)`.
+  - Skeleton Mecanim: Added new `Mix Mode` `Match`. When selected, Spine animation weights are calculated to best match the provided Mecanim clip weights. This mix mode is recommended on any layer using blend tree nodes.
 
 - **Breaking changes**
 
@@ -172,6 +176,9 @@
   - Inspector: String attribute `SpineSkin()` now allows to include `<None>` in the list of parameters. Previously the `includeNone=true` parameter of the `SpineSkin()` attribute defaulted to `true` but was ignored. Now it defaults to `false` and has an effect on the list. Only the Inspector GUI is affected by this behaviour change.
   - `SkeletonGraphicRenderTexture` example component: `protected RawImage quadRawImage` was changed to `protected SkeletonSubmeshGraphic quadMaskableGraphic` for a bugfix. This is only relevant for subclasses of `SkeletonGraphicRenderTexture` or when querying the `RawImage` component via e.g. `skeletonGraphicRenderTexture.quad.GetComponent<RawImage>()`.
   - Fixed a bug where when Linear color space is used and `PMA vertex colors` enabled, additive slots add a too dark (too transparent) color value. If you want the old incorrect behaviour (darker additive slots) or are not using Linear but Gamma color space, you can comment-out the define `LINEAR_COLOR_SPACE_FIX_ADDITIVE_ALPHA` in `MeshGenerator.cs` to deactivate the fix or just to skip unnecessary instructions.
+  - Fixed SkeletonRootMotion components ignoring parent bone scale when set by transform constraints. Using applied scale of parent bone now. If you need the old behaviour, comment out the line `#define USE_APPLIED_PARENT_SCALE` in SkeletonRootMotionBase.cs.
+  - Fixed SkeletonUtility callback update order when used with SkeletonRootMotion components so that the position when following a bone is updated after SkeletonRootMotion clears root-bone position. The order of SkeletonUtilityBone callbacks is changed to be later to achieve this. This is a breaking change in the unlikely case that you are using SkeletonRootMotion together with SkeletonUtility and subscribed to `UpdateLocal`, `UpdateWorld` or `UpdateComplete` yourself and relied on a certain callback order. One solution is to then resubscribe your own callback events accordingly by calling
+  `.UpdateLocal -= Callback; .UpdateLocal += Callback;`.
 
 - **Changes of default values**
 
@@ -337,6 +344,7 @@
   - `VertexEffect` has been removed.
 
 ### Cocos2d-x
+- Renamed `SkeletonRenderer` to `SkeletonRendererCocos2dX` to avoid name clash with spine-cpp class.
 
 ### SFML
 
@@ -421,6 +429,7 @@
 
   - Made `SkeletonGraphic.unscaledTime` parameter protected, use the new property `UnscaledTime` instead.
   - `SkeletonGraphic` `OnRebuild` callback delegate is now issued after the skeleton has been initialized, before the `AnimationState` component is initialized. This makes behaviour consistent with `SkeletonAnimation` and `SkeletonMecanim` component behaviour. Use the new callback `OnAnimationRebuild` if you want to receive a callback after the `SkeletonGraphic` `AnimationState` has been initialized.
+  - Changed name of prefab skeleton meshes stored at prefabs from `Skeleton Prefab Mesh "name"` to `Skeleton Prefab Mesh [name]` to avoid issues with quotes in mesh asset names (see [this issue](https://github.com/EsotericSoftware/spine-runtimes/issues/2572)). Likely this change poses no problems at all, however if you are parsing the prefab's mesh name for whatever reason, be sure to adjust the pattern accordingly.
 
 - **Changes of default values**
 

+ 36 - 0
examples/export/runtimes.sh

@@ -35,6 +35,29 @@ cp -f ../mix-and-match/export/*.json "$ROOT/spine-libgdx/spine-libgdx-tests/asse
 cp -f ../mix-and-match/export/*.skel "$ROOT/spine-libgdx/spine-libgdx-tests/assets/mix-and-match/"
 cp -f ../mix-and-match/export/*-pma.* "$ROOT/spine-libgdx/spine-libgdx-tests/assets/mix-and-match/"
 
+echo "spine-android"
+rm "$ROOT/spine-android/app/src/main/assets/"*
+cp -f ../celestial-circus/export/celestial-circus-pro.skel "$ROOT/spine-android/app/src/main/assets/"
+cp -f ../celestial-circus/export/celestial-circus.atlas "$ROOT/spine-android/app/src/main/assets"
+cp -f ../celestial-circus/export/celestial-circus.png "$ROOT/spine-android/app/src/main/assets"
+
+cp -f ../dragon/export/dragon-ess.skel "$ROOT/spine-android/app/src/main/assets/"
+cp -f ../dragon/export/dragon.atlas "$ROOT/spine-android/app/src/main/assets"
+cp -f ../dragon/export/dragon.png "$ROOT/spine-android/app/src/main/assets"
+cp -f ../dragon/export/dragon_2.png "$ROOT/spine-android/app/src/main/assets"
+cp -f ../dragon/export/dragon_3.png "$ROOT/spine-android/app/src/main/assets"
+cp -f ../dragon/export/dragon_4.png "$ROOT/spine-android/app/src/main/assets"
+cp -f ../dragon/export/dragon_5.png "$ROOT/spine-android/app/src/main/assets"
+
+cp -f ../mix-and-match/export/mix-and-match-pro.skel "$ROOT/spine-android/app/src/main/assets/"
+cp -f ../mix-and-match/export/mix-and-match.atlas "$ROOT/spine-android/app/src/main/assets/"
+cp -f ../mix-and-match/export/mix-and-match.png "$ROOT/spine-android/app/src/main/assets/"
+
+cp -f ../spineboy/export/spineboy-pro.skel "$ROOT/spine-android/app/src/main/assets/"
+cp -f ../spineboy/export/spineboy-pro.json "$ROOT/spine-android/app/src/main/assets/"
+cp -f ../spineboy/export/spineboy.atlas "$ROOT/spine-android/app/src/main/assets/"
+cp -f ../spineboy/export/spineboy.png "$ROOT/spine-android/app/src/main/assets/"
+
 rm -f "$ROOT/spine-libgdx/spine-libgdx-tests/assets/sack/"*
 mkdir -p "$ROOT/spine-libgdx/spine-libgdx-tests/assets/sack/"
 cp -f ../sack/export/sack-pro.json "$ROOT/spine-libgdx/spine-libgdx-tests/assets/sack/"
@@ -453,6 +476,19 @@ cp -f ../spineboy/export/spineboy-ess.json "$ROOT/spine-ts/spine-canvas/example/
 cp -f ../spineboy/export/spineboy.atlas "$ROOT/spine-ts/spine-canvas/example/assets/"
 cp -f ../spineboy/export/spineboy.png "$ROOT/spine-ts/spine-canvas/example/assets/"
 
+rm "$ROOT/spine-ts/spine-canvaskit/example/assets/"*
+cp -f ../spineboy/export/spineboy-pro.skel "$ROOT/spine-ts/spine-canvaskit/example/assets/"
+cp -f ../spineboy/export/spineboy.atlas "$ROOT/spine-ts/spine-canvaskit/example/assets/"
+cp -f ../spineboy/export/spineboy.png "$ROOT/spine-ts/spine-canvaskit/example/assets/"
+
+cp -f ../mix-and-match/export/mix-and-match-pro.skel "$ROOT/spine-ts/spine-canvaskit/example/assets/"
+cp -f ../mix-and-match/export/mix-and-match.atlas "$ROOT/spine-ts/spine-canvaskit/example/assets/"
+cp -f ../mix-and-match/export/mix-and-match.png "$ROOT/spine-ts/spine-canvaskit/example/assets/"
+
+cp -f ../celestial-circus/export/celestial-circus-pro.json "$ROOT/spine-ts/spine-canvaskit/example/assets/"
+cp -f ../celestial-circus/export/celestial-circus.atlas "$ROOT/spine-ts/spine-canvaskit/example/assets/"
+cp -f ../celestial-circus/export/celestial-circus.png "$ROOT/spine-ts/spine-canvaskit/example/assets/"
+
 rm "$ROOT/spine-ts/spine-threejs/example/assets/"*
 cp -f ../raptor/export/raptor-pro.json "$ROOT/spine-ts/spine-threejs/example/assets/"
 cp -f ../raptor/export/raptor.atlas "$ROOT/spine-ts/spine-threejs/example/assets/"

+ 2 - 1
formatters/build.gradle

@@ -8,7 +8,8 @@ spotless {
     lineEndings 'UNIX'
 
     java {
-        target 'spine-libgdx/**/*.java'
+        target 'spine-libgdx/**/*.java',
+               'spine-android/**/*.java'
         eclipse().configFile('formatters/eclipse-formatter.xml')
     }
 

+ 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

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

@@ -0,0 +1,75 @@
+plugins {
+    alias(libs.plugins.androidApplication)
+    alias(libs.plugins.jetbrainsKotlinAndroid)
+}
+
+android {
+    namespace = "com.esotericsoftware.spine"
+    compileSdk = 34
+
+    defaultConfig {
+        applicationId = "com.esotericsoftware.spine"
+        minSdk = 23
+        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"))
+    // implementation("com.esotericsoftware.spine:spine-android:4.2.2-SNAPSHOT")
+}

+ 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?.set(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)
+        )
+    }
+}

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

@@ -0,0 +1,76 @@
+/******************************************************************************
+ * 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

+ 14 - 0
spine-android/publish.sh

@@ -0,0 +1,14 @@
+#!/bin/sh
+
+#
+# 1. Set up PGP key for signing
+# 2. Create ~/.gradle/gradle.properties
+# 3. Add
+#    ossrhUsername=<sonatype-token-user-name>
+#    ossrhPassword=<sonatype-token>
+#    signing.gnupg.passphrase=<pgp-key-passphrase>
+#
+# After publishing via this script, log into https://oss.sonatype.org and release it manually after
+# checks pass ("Release & Drop").
+set -e
+ ./gradlew publishReleasePublicationToSonaTypeRepository --info

+ 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

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

@@ -0,0 +1,134 @@
+plugins {
+    alias(libs.plugins.androidLibrary)
+    `maven-publish`
+    signing
+}
+
+android {
+    namespace = "com.esotericsoftware.spine"
+    compileSdk = 34
+
+    defaultConfig {
+        minSdk = 23
+
+        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.7")
+
+    testImplementation(libs.junit)
+    androidTestImplementation(libs.androidx.junit)
+    androidTestImplementation(libs.androidx.espresso.core)
+}
+
+val libraryVersion = "4.2.8-SNAPSHOT";
+
+tasks.register<Jar>("sourceJar") {
+    archiveClassifier.set("sources")
+    from(android.sourceSets["main"].java.srcDirs)
+}
+
+afterEvaluate {
+    publishing {
+        publications {
+            create<MavenPublication>("release") {
+                artifact(tasks.getByName("bundleReleaseAar"))
+                artifact(tasks.getByName("sourceJar"))
+
+                groupId = "com.esotericsoftware.spine"
+                artifactId = "spine-android"
+                version = libraryVersion
+
+                pom {
+                    packaging = "aar"
+                    name.set("spine-android")
+                    description.set("Spine Runtime for Android")
+                    url.set("https://github.com/esotericsoftware/spine-runtimes")
+                    licenses {
+                        license {
+                            name.set("Spine Runtimes License")
+                            url.set("http://esotericsoftware.com/spine-runtimes-license")
+                        }
+                    }
+                    developers {
+                        developer {
+                            name.set("Esoteric Software")
+                            email.set("[email protected]")
+                        }
+                    }
+                    scm {
+                        url.set(pom.url.get())
+                        connection.set("scm:git:${url.get()}.git")
+                        developerConnection.set("scm:git:${url.get()}.git")
+                    }
+
+                    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")
+                            }
+                        }
+                    }
+
+                }
+            }
+        }
+
+        repositories {
+            maven {
+                name = "SonaType"
+                url = uri(if (libraryVersion.endsWith("-SNAPSHOT")) {
+                    "https://oss.sonatype.org/content/repositories/snapshots"
+                } else {
+                    "https://oss.sonatype.org/service/local/staging/deploy/maven2"
+                })
+
+                credentials {
+                    username = project.findProperty("ossrhUsername") as String?
+                    password = project.findProperty("ossrhPassword") as String?
+                }
+            }
+        }
+    }
+
+    signing {
+        useGpgCmd()
+        sign(publishing.publications["release"])
+        sign(tasks.getByName("sourceJar"))
+    }
+
+    tasks.withType<Sign> {
+        onlyIf { !libraryVersion.endsWith("-SNAPSHOT") }
+    }
+}

+ 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

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

@@ -0,0 +1,25 @@
+
+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);
+	}
+}

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

@@ -0,0 +1,158 @@
+/******************************************************************************
+ * 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);
+	}
+}

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

@@ -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.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();
+	}
+}

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

@@ -0,0 +1,221 @@
+/******************************************************************************
+ * 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;
+	}
+}

+ 54 - 0
spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/DebugRenderer.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;
+
+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);
+		}
+	}
+}

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

@@ -0,0 +1,301 @@
+/******************************************************************************
+ * 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;
+import android.os.Build;
+
+/** 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);
+
+			int indicesStart = command.indices.size;
+			int indicesLength = indices.length;
+			if (clipper.isClipping()) {
+				clipper.clipTrianglesUnpacked(command.vertices.items, vertexStart, indices, indices.length, uvs);
+
+				// Copy clipped vertices over, overwritting the previous vertices of this attachment
+				FloatArray clippedVertices = clipper.getClippedVertices();
+				command.vertices.setSize(vertexStart + clippedVertices.size);
+				System.arraycopy(clippedVertices.items, 0, command.vertices.items, vertexStart, clippedVertices.size);
+
+				// Copy UVs over, post-processing below
+				command.uvs.addAll(clipper.getClippedUvs());
+
+				// Copy indices over, post-processing below
+				command.indices.addAll(clipper.getClippedTriangles());
+
+				// Update verticesLength with the clipped number of vertices * 2, and indices length
+				// with the number of clipped indices.
+				verticesLength = clippedVertices.size;
+				indicesLength = clipper.getClippedTriangles().size;
+			} else {
+				// Copy UVs over, post-processing below
+				command.uvs.addAll(uvs);
+
+				// Copy indices over, post-processing below
+				command.indices.addAll(indices);
+			}
+
+			// Post-process UVs, require scaling by bitmap size
+			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;
+			}
+
+			// Fill colors array
+			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;
+			}
+
+			// Post-process indices array, need to be offset by index of the mesh's first vertex.
+			int firstIndex = vertexStart >> 1;
+			short[] indicesArray = command.indices.items;
+			for (int ii = indicesStart, nn = indicesStart + indicesLength; ii < nn; ii++) {
+				indicesArray[ii] += firstIndex;
+			}
+
+			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);
+
+			if (Build.VERSION.SDK_INT >= 29) {
+				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));
+			} else {
+				// See https://github.com/EsotericSoftware/spine-runtimes/issues/2638
+				int[] colors = command.colors.items;
+				int[] colorsCopy = new int[command.vertices.size];
+				System.arraycopy(colors, 0, colorsCopy, 0, command.colors.size);
+
+				canvas.drawVertices(Canvas.VertexMode.TRIANGLES, command.vertices.size, command.vertices.items, 0, command.uvs.items,
+					0, colorsCopy, 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;
+	}
+}

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

@@ -0,0 +1,287 @@
+/******************************************************************************
+ * 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);
+		}
+	}
+}

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

@@ -0,0 +1,419 @@
+/******************************************************************************
+ * 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.Build;
+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;
+		// See https://github.com/EsotericSoftware/spine-runtimes/issues/2638
+		if (Build.VERSION.SDK_INT < 29) {
+			this.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
+		}
+	}
+
+	/** 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);
+	}
+}

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

@@ -0,0 +1,52 @@
+/******************************************************************************
+ * 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;
+	}
+}

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

@@ -0,0 +1,101 @@
+/******************************************************************************
+ * 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;
+	}
+}

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

@@ -0,0 +1,38 @@
+/******************************************************************************
+ * 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);
+}

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

@@ -0,0 +1,38 @@
+/******************************************************************************
+ * 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
+}

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

@@ -0,0 +1,52 @@
+/******************************************************************************
+ * 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);
+	}
+}

+ 42 - 0
spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/SetupPoseBounds.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.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());
+	}
+}

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

@@ -0,0 +1,111 @@
+/******************************************************************************
+ * 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);
+}

Деякі файли не було показано, через те що забагато файлів було змінено