diff --git a/spine-android/.gitignore b/spine-android/.gitignore
new file mode 100644
index 000000000..aa724b770
--- /dev/null
+++ b/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
diff --git a/spine-android/app/.gitignore b/spine-android/app/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/spine-android/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/spine-android/app/build.gradle.kts b/spine-android/app/build.gradle.kts
new file mode 100644
index 000000000..0065be9d0
--- /dev/null
+++ b/spine-android/app/build.gradle.kts
@@ -0,0 +1,70 @@
+plugins {
+ alias(libs.plugins.androidApplication)
+ alias(libs.plugins.jetbrainsKotlinAndroid)
+}
+
+android {
+ namespace = "com.esotericsoftware.spine"
+ compileSdk = 34
+
+ defaultConfig {
+ applicationId = "com.esotericsoftware.spine"
+ minSdk = 24
+ targetSdk = 34
+ versionCode = 1
+ versionName = "1.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ vectorDrawables {
+ useSupportLibrary = true
+ }
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+ buildFeatures {
+ compose = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.5.1"
+ }
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
+}
+
+dependencies {
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.activity.compose)
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.ui)
+ implementation(libs.androidx.ui.graphics)
+ implementation(libs.androidx.ui.tooling.preview)
+ implementation(libs.androidx.material3)
+ implementation(project(":spine-android"))
+ 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)
+}
\ No newline at end of file
diff --git a/spine-android/app/proguard-rules.pro b/spine-android/app/proguard-rules.pro
new file mode 100644
index 000000000..481bb4348
--- /dev/null
+++ b/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
\ No newline at end of file
diff --git a/spine-android/app/src/androidTest/java/com/esotericsoftware/android/ExampleInstrumentedTest.kt b/spine-android/app/src/androidTest/java/com/esotericsoftware/android/ExampleInstrumentedTest.kt
new file mode 100644
index 000000000..efa76c79c
--- /dev/null
+++ b/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)
+ }
+}
\ No newline at end of file
diff --git a/spine-android/app/src/main/AndroidManifest.xml b/spine-android/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..17792df33
--- /dev/null
+++ b/spine-android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spine-android/app/src/main/assets/spineboy-copy.webp b/spine-android/app/src/main/assets/spineboy-copy.webp
new file mode 100644
index 000000000..b5bef489a
Binary files /dev/null and b/spine-android/app/src/main/assets/spineboy-copy.webp differ
diff --git a/spine-android/app/src/main/assets/spineboy-pro.skel b/spine-android/app/src/main/assets/spineboy-pro.skel
new file mode 100644
index 000000000..4dfe536cd
Binary files /dev/null and b/spine-android/app/src/main/assets/spineboy-pro.skel differ
diff --git a/spine-android/app/src/main/assets/spineboy.atlas b/spine-android/app/src/main/assets/spineboy.atlas
new file mode 100644
index 000000000..eca542b71
--- /dev/null
+++ b/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
diff --git a/spine-android/app/src/main/assets/spineboy.mesh b/spine-android/app/src/main/assets/spineboy.mesh
new file mode 100644
index 000000000..2eb9d274b
--- /dev/null
+++ b/spine-android/app/src/main/assets/spineboy.mesh
@@ -0,0 +1,2411 @@
+276
+1029
+42.879013
+-297.047607
+6.236506
+-281.006226
+-28.653503
+-360.703705
+7.989008
+-376.745087
+62.652306
+-268.730927
+11.352795
+-246.272964
+-17.521706
+-312.229492
+33.777809
+-334.687439
+229.816147
+-154.047729
+25.090149
+-107.279572
+-20.119045
+-305.181427
+184.606964
+-351.949554
+55.973713
+-50.431744
+60.000725
+-45.838646
+63.459427
+-41.895344
+67.670807
+-40.684006
+72.906685
+-39.176933
+88.494637
+-34.689171
+113.775513
+-13.848475
+113.528633
+2.136607
+62.500793
+1.265033
+35.758568
+0.943742
+0.551171
+0.417753
+1.076124
+-34.735672
+16.845055
+-59.338657
+48.578522
+-58.865547
+43.379440
+-162.329895
+-11.614739
+-161.529526
+-12.982636
+-255.519562
+42.011539
+-256.319916
+65.043198
+-11.982086
+-9.951412
+-12.882217
+-7.815128
+-190.869385
+67.179474
+-189.969254
+-3.026524
+-370.122864
+-37.433113
+-359.531067
+-49.496010
+-398.716339
+-15.089422
+-409.308167
+-22.821608
+-380.509888
+17.435993
+-362.172028
+18.170435
+-357.714722
+29.042627
+-291.733337
+29.693506
+-287.783203
+22.567608
+-284.760193
+29.863823
+-257.309418
+16.866358
+-227.740295
+-9.695295
+-223.363510
+-31.426403
+-219.782776
+-50.997978
+-258.157837
+-57.465271
+-273.560638
+-64.414291
+-290.107788
+-73.625572
+-312.043945
+-77.089432
+-333.065277
+-81.286545
+-358.537018
+-72.989655
+-395.361450
+-60.091599
+-397.486786
+-8.700058
+-355.176208
+0.127717
+-323.098633
+14.998151
+-285.877106
+-2.462405
+-291.598358
+-23.514177
+-333.334900
+-52.957638
+-339.976227
+-41.758320
+-299.855865
+-25.460911
+-258.150757
+-56.479904
+-367.056519
+-30.831459
+-368.242188
+4.588335
+-268.304260
+13.812860
+-276.573578
+-47.665607
+-321.018127
+-11.210316
+-308.941681
+-34.052513
+-280.136780
+-26.962830
+-349.786926
+-5.811657
+-276.585388
+-51.092575
+-268.483643
+-68.176712
+-363.967316
+-22.895794
+-372.069061
+-41.517307
+-678.262756
+4.756733
+-662.967102
+27.035517
+-631.043701
+21.181705
+-665.676025
+44.648727
+-654.730042
+66.348602
+-613.574951
+63.094486
+-595.710632
+60.145405
+-579.537415
+48.342926
+-566.127686
+47.067940
+-524.576538
+50.305164
+-495.177551
+76.461578
+-507.918457
+86.074051
+-488.016388
+82.666870
+-476.777222
+68.601036
+-467.543640
+76.467094
+-456.328857
+64.882347
+-418.107849
+-1.124334
+-383.302216
+-68.765465
+-396.801880
+-88.752258
+-400.786804
+-121.365784
+-423.613708
+-150.618195
+-444.090759
+-174.983337
+-490.293304
+-182.793427
+-554.658447
+-153.730392
+-590.830017
+-92.385025
+-624.156860
+-101.022026
+-642.420532
+-131.935181
+-651.167725
+-93.884949
+-671.229187
+22.045732
+-610.792419
+4.060351
+-577.345825
+-24.378452
+-602.536499
+-35.769398
+-645.546875
+-92.140846
+-658.155212
+-92.876022
+-594.650452
+-137.738342
+-558.649292
+0.035936
+-572.425293
+-42.157799
+-569.496704
+-85.525314
+-562.776550
+-117.991913
+-552.428101
+-0.445878
+-570.610779
+-41.981926
+-567.314148
+-84.644432
+-560.175903
+-117.190552
+-547.730408
+51.034946
+-578.638794
+-57.058067
+-443.197510
+-77.297791
+-458.759583
+-95.078400
+-463.162659
+-110.531441
+-452.958221
+-114.658646
+-435.820129
+-106.326714
+-420.266724
+-87.203674
+-408.095825
+-70.355499
+-409.597839
+-53.781849
+-422.371063
+-45.719143
+-448.175842
+-97.368286
+-477.148438
+-126.345207
+-449.629730
+-48.403282
+-421.397003
+65.129303
+-459.058716
+56.715298
+-456.420074
+37.337090
+-472.026398
+-81.100037
+-442.776611
+-103.435188
+-439.148804
+56.140377
+-488.318420
+-13.882811
+-427.133514
+-72.438599
+-494.144257
+-2.415684
+-555.328430
+-9.752207
+-133.299133
+-53.597122
+-143.429474
+-28.383848
+-252.554596
+15.461065
+-242.424255
+0.280756
+-45.509258
+3.817970
+-40.700172
+7.250431
+-36.034451
+10.562510
+-35.046749
+16.135471
+-33.386559
+21.410233
+-31.814659
+51.100018
+-12.496605
+50.862698
+7.939594
+7.424946
+7.435127
+-15.988001
+7.130658
+-75.103821
+6.607407
+-74.704819
+-35.344917
+-51.963093
+-62.175610
+-11.707943
+-61.792732
+-4.850842
+-178.681305
+3.297174
+-165.704422
+3.263706
+-149.529526
+-4.766674
+-132.281067
+-14.226085
+-123.686310
+-14.779051
+-115.977768
+-15.510324
+-105.783768
+-15.629786
+-94.454346
+-9.939468
+-72.033401
+-2.601040
+-51.485832
+-0.615288
+-45.925728
+-14.924071
+-28.016983
+-33.008259
+-5.382965
+-57.001526
+-5.432617
+-79.034515
+-5.478195
+-78.791306
+-12.632462
+-77.848518
+-40.365326
+-76.570442
+-77.960693
+-71.999954
+-106.402145
+-63.270332
+-130.782532
+-54.491909
+-145.303955
+-53.917458
+-161.935043
+-47.619102
+-178.553772
+-34.960701
+-189.387360
+-20.812759
+-189.358093
+-45.230839
+-56.986832
+-45.453281
+-88.667320
+-45.129532
+-105.946686
+-40.050804
+-124.656250
+-51.398773
+-138.215729
+-37.589401
+-131.131165
+-25.341869
+-65.153694
+-29.029552
+-92.377365
+-29.576653
+-106.346527
+-27.344521
+-120.292831
+-24.884426
+-127.082634
+-55.947918
+-128.850571
+-63.272888
+-106.272240
+-65.339859
+-81.652481
+-63.969269
+-48.097580
+-37.311478
+-144.065002
+-29.954123
+-156.316788
+-29.362101
+-169.851608
+-36.529163
+-181.428452
+-19.561584
+-136.273285
+-11.209936
+-152.048019
+-7.510107
+-168.396408
+-13.406410
+-180.816635
+-41.929737
+-160.853561
+-58.138187
+-12.392563
+-35.415283
+-15.545456
+-17.216610
+-36.189270
+59.851063
+-426.057495
+-22.414417
+-382.681976
+-49.933273
+-434.870422
+32.333668
+-478.247375
+-71.253433
+-569.188293
+-17.546474
+-575.142944
+39.032654
+-572.722046
+49.985153
+-582.050476
+69.749718
+-561.091187
+67.134560
+-542.310364
+55.744041
+-525.135559
+63.388222
+-499.740784
+47.547886
+-476.782471
+46.784439
+-451.506104
+14.528856
+-446.019623
+-15.556076
+-440.891754
+-44.274376
+-450.349915
+-68.898865
+-446.637878
+-88.967331
+-458.249664
+-109.572525
+-437.958740
+-100.400833
+-420.669861
+-111.548637
+-405.734772
+-173.281952
+-462.488678
+-188.633789
+-491.601959
+-175.587204
+-508.105988
+-154.378281
+-512.934448
+-133.983353
+-517.571655
+-126.570023
+-543.502441
+-117.953842
+-552.279785
+-45.517567
+-460.661987
+-67.649040
+-483.031342
+-82.742340
+-511.720032
+-74.425705
+-531.700500
+-46.103344
+-537.426453
+-6.433783
+-537.497681
+33.053020
+-530.199219
+47.890587
+-522.756897
+51.820316
+-545.348450
+-5.725768
+-551.145752
+-47.459347
+-552.656006
+-78.592064
+-551.894775
+-106.722252
+-547.885376
+-108.586868
+-523.697815
+-101.043076
+-503.968384
+-73.456360
+-474.797241
+-50.950211
+-456.127899
+-85.674408
+-464.526978
+-125.814529
+-495.315521
+-152.884552
+-479.553986
+-33.213146
+-515.003784
+-56.159058
+-515.149109
+-39.806046
+-495.741577
+2.324126
+-504.425873
+30.676798
+-501.983337
+4.470346
+-535.486511
+-5.209473
+-233.373047
+-62.302799
+-223.157776
+-76.392807
+-301.907196
+-19.299480
+-312.122467
+39.466541
+-181.314575
+-34.361038
+-168.105194
+-48.803307
+-248.823334
+25.024269
+-262.032715
+0.807617
+0.531250
+0.807617
+0.609375
+0.764648
+0.609375
+0.764648
+0.531250
+0.833984
+0.191406
+0.806641
+0.191406
+0.806641
+0.050781
+0.833984
+0.050781
+0.438477
+0.054688
+0.438477
+0.464844
+0.338867
+0.464844
+0.338867
+0.054688
+0.729650
+0.388398
+0.731862
+0.380333
+0.733762
+0.373411
+0.734321
+0.365086
+0.735017
+0.354732
+0.737090
+0.323886
+0.747074
+0.273438
+0.754883
+0.273438
+0.754849
+0.374010
+0.754883
+0.426712
+0.754883
+0.496094
+0.737714
+0.496094
+0.725586
+0.465744
+0.725586
+0.403209
+0.791992
+0.800781
+0.764648
+0.800781
+0.764648
+0.617188
+0.791992
+0.617188
+0.207031
+0.402344
+0.169922
+0.402344
+0.169922
+0.054688
+0.207031
+0.054688
+0.804688
+0.113281
+0.787109
+0.113281
+0.787109
+0.031250
+0.804688
+0.031250
+0.150029
+0.095333
+0.167969
+0.143456
+0.167969
+0.152279
+0.167969
+0.282887
+0.167969
+0.290706
+0.164296
+0.294269
+0.165632
+0.349487
+0.157022
+0.402344
+0.143878
+0.402344
+0.133124
+0.402344
+0.126741
+0.322175
+0.124848
+0.290443
+0.122814
+0.256342
+0.120117
+0.211144
+0.120117
+0.169533
+0.120117
+0.119112
+0.127038
+0.050781
+0.133421
+0.050781
+0.154821
+0.148639
+0.156528
+0.213260
+0.160737
+0.289713
+0.152779
+0.273143
+0.145950
+0.186026
+0.132292
+0.163877
+0.134503
+0.244751
+0.139044
+0.330298
+0.132745
+0.110571
+0.145196
+0.116431
+0.154327
+0.320273
+0.159428
+0.307266
+0.133337
+0.202093
+0.149942
+0.236942
+0.136650
+0.285200
+0.145595
+0.153225
+0.625977
+0.210938
+0.603516
+0.210938
+0.603516
+0.019531
+0.625977
+0.019531
+0.178955
+0.445703
+0.195521
+0.514296
+0.197887
+0.589749
+0.203266
+0.524585
+0.210938
+0.564884
+0.210938
+0.655771
+0.205444
+0.683669
+0.200469
+0.708931
+0.192287
+0.721366
+0.182234
+0.791999
+0.176917
+0.845739
+0.191170
+0.847560
+0.190788
+0.890704
+0.186740
+0.907022
+0.178530
+0.910159
+0.179377
+0.936707
+0.165616
+0.992188
+0.129041
+0.992188
+0.102801
+0.907234
+0.095048
+0.882130
+0.086127
+0.812974
+0.078125
+0.750945
+0.078125
+0.648911
+0.089454
+0.530587
+0.110323
+0.494576
+0.144531
+0.492861
+0.144961
+0.453419
+0.133559
+0.410156
+0.154643
+0.410156
+0.191094
+0.620193
+0.175651
+0.661630
+0.169076
+0.592162
+0.173969
+0.507459
+0.152409
+0.434334
+0.137577
+0.543412
+0.109901
+0.564740
+0.172783
+0.666458
+0.153819
+0.633071
+0.133479
+0.605199
+0.117039
+0.593478
+0.172158
+0.669171
+0.153398
+0.637012
+0.133269
+0.610470
+0.116313
+0.602326
+0.196312
+0.702200
+0.118485
+0.837731
+0.113268
+0.792404
+0.106566
+0.768593
+0.097532
+0.772144
+0.091826
+0.797999
+0.091881
+0.832460
+0.097390
+0.870913
+0.105037
+0.883669
+0.115143
+0.876705
+0.124539
+0.839463
+0.108769
+0.742339
+0.089916
+0.763488
+0.117252
+0.883290
+0.175086
+0.921663
+0.170835
+0.918551
+0.166001
+0.873925
+0.107965
+0.816554
+0.097452
+0.802473
+0.601562
+0.183594
+0.555664
+0.183594
+0.555664
+0.007812
+0.601562
+0.007812
+0.758789
+0.265625
+0.736328
+0.265625
+0.736328
+0.046875
+0.758789
+0.046875
+0.697459
+0.349872
+0.699822
+0.342882
+0.702112
+0.336108
+0.702579
+0.329642
+0.703368
+0.318739
+0.704114
+0.308412
+0.713507
+0.250000
+0.723633
+0.250000
+0.723633
+0.334860
+0.723632
+0.380619
+0.723633
+0.496094
+0.702850
+0.496094
+0.689453
+0.452177
+0.689453
+0.373550
+0.114172
+0.063757
+0.118164
+0.089070
+0.118164
+0.120661
+0.114260
+0.154382
+0.109650
+0.171207
+0.109388
+0.186265
+0.109041
+0.206178
+0.108994
+0.228306
+0.111795
+0.272074
+0.115399
+0.312176
+0.116375
+0.323028
+0.109406
+0.358064
+0.100599
+0.402344
+0.088883
+0.402344
+0.078125
+0.402344
+0.078237
+0.388370
+0.078669
+0.334200
+0.079255
+0.260767
+0.081458
+0.205199
+0.085696
+0.157545
+0.089967
+0.129148
+0.090231
+0.096663
+0.093290
+0.064179
+0.099460
+0.042969
+0.106368
+0.042969
+0.094579
+0.301605
+0.094438
+0.239730
+0.094579
+0.205980
+0.097040
+0.169417
+0.091485
+0.142979
+0.098235
+0.156761
+0.104282
+0.285573
+0.102454
+0.232417
+0.102172
+0.205136
+0.103248
+0.177888
+0.104443
+0.164617
+0.089273
+0.161289
+0.085719
+0.205417
+0.084735
+0.253511
+0.085438
+0.319042
+0.098358
+0.131498
+0.101938
+0.107539
+0.102213
+0.081102
+0.098702
+0.058520
+0.107032
+0.146645
+0.111094
+0.115801
+0.112884
+0.083856
+0.109993
+0.059621
+0.096086
+0.098727
+0.088321
+0.388755
+0.099413
+0.382505
+0.108278
+0.342111
+0.793945
+0.808594
+0.793945
+0.992188
+0.764648
+0.992188
+0.764648
+0.808594
+0.277623
+0.091463
+0.302273
+0.130116
+0.326254
+0.185839
+0.333136
+0.179700
+0.336914
+0.233922
+0.331484
+0.263999
+0.322618
+0.283290
+0.320127
+0.334136
+0.308007
+0.359383
+0.301897
+0.402344
+0.286655
+0.382445
+0.272438
+0.363885
+0.262149
+0.321377
+0.250622
+0.305352
+0.244575
+0.267005
+0.231001
+0.283295
+0.231025
+0.321520
+0.222776
+0.337169
+0.208984
+0.182861
+0.208984
+0.118573
+0.218416
+0.101934
+0.228715
+0.112926
+0.238620
+0.123496
+0.247764
+0.085440
+0.253507
+0.078125
+0.263967
+0.302431
+0.259486
+0.243611
+0.259501
+0.180292
+0.267676
+0.153336
+0.281266
+0.169258
+0.298485
+0.205271
+0.313937
+0.253864
+0.318669
+0.280252
+0.325537
+0.244797
+0.301911
+0.182351
+0.284161
+0.141703
+0.270486
+0.114661
+0.257371
+0.095961
+0.251034
+0.136051
+0.249794
+0.177011
+0.255085
+0.252551
+0.260575
+0.305313
+0.247438
+0.259162
+0.237073
+0.169394
+0.221730
+0.171963
+0.281729
+0.219745
+0.271811
+0.198592
+0.274465
+0.247020
+0.294720
+0.270399
+0.306455
+0.300457
+0.302752
+0.218690
+0.820312
+0.355469
+0.791992
+0.355469
+0.791992
+0.199219
+0.820312
+0.199219
+0.825195
+0.523438
+0.788086
+0.523438
+0.788086
+0.363281
+0.825195
+0.363281
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+0
+1
+2
+2
+3
+0
+4
+5
+6
+6
+7
+4
+8
+9
+10
+10
+11
+8
+20
+15
+16
+20
+16
+17
+20
+17
+18
+20
+18
+19
+23
+13
+22
+15
+21
+14
+14
+22
+13
+24
+25
+12
+12
+23
+24
+13
+23
+12
+14
+21
+22
+15
+20
+21
+26
+27
+28
+28
+29
+26
+30
+31
+32
+32
+33
+30
+34
+35
+36
+36
+37
+34
+56
+39
+40
+57
+40
+41
+56
+38
+39
+61
+53
+64
+65
+64
+54
+52
+53
+61
+53
+54
+64
+55
+65
+54
+51
+52
+61
+38
+65
+55
+51
+61
+68
+49
+50
+62
+59
+69
+57
+50
+51
+68
+62
+60
+69
+69
+60
+57
+50
+68
+62
+70
+62
+69
+62
+68
+60
+41
+58
+57
+70
+69
+59
+49
+62
+70
+42
+43
+41
+46
+66
+45
+45
+67
+44
+45
+66
+67
+47
+63
+46
+46
+63
+66
+47
+48
+63
+67
+43
+44
+48
+70
+63
+63
+59
+66
+63
+70
+59
+48
+49
+70
+66
+59
+67
+67
+58
+43
+67
+59
+58
+43
+58
+41
+58
+59
+57
+71
+64
+65
+60
+56
+57
+57
+56
+40
+71
+65
+56
+68
+61
+60
+60
+71
+56
+61
+71
+60
+71
+61
+64
+65
+38
+56
+72
+73
+74
+74
+75
+72
+126
+125
+138
+110
+101
+107
+115
+111
+110
+114
+115
+110
+113
+114
+110
+118
+115
+114
+119
+115
+118
+108
+78
+107
+107
+113
+110
+118
+114
+113
+117
+118
+113
+119
+98
+115
+106
+107
+105
+112
+113
+107
+106
+112
+107
+116
+117
+113
+112
+116
+113
+112
+106
+120
+131
+98
+119
+131
+124
+132
+123
+124
+131
+122
+131
+130
+118
+131
+119
+123
+131
+122
+138
+125
+124
+137
+123
+122
+138
+124
+123
+137
+138
+123
+122
+130
+121
+118
+117
+131
+137
+122
+121
+131
+117
+130
+137
+127
+126
+137
+126
+138
+136
+117
+116
+130
+117
+136
+129
+137
+121
+128
+127
+137
+133
+129
+121
+133
+121
+130
+129
+128
+137
+128
+95
+127
+133
+94
+128
+133
+128
+129
+93
+130
+136
+133
+130
+93
+94
+133
+93
+95
+126
+127
+109
+103
+104
+102
+103
+109
+76
+109
+104
+108
+109
+76
+108
+76
+77
+109
+101
+102
+109
+108
+101
+107
+101
+108
+78
+108
+77
+78
+79
+80
+105
+107
+78
+78
+80
+81
+105
+78
+81
+82
+105
+81
+106
+105
+82
+120
+106
+82
+94
+95
+128
+125
+132
+124
+110
+100
+101
+111
+99
+100
+111
+100
+110
+115
+98
+111
+98
+99
+111
+83
+120
+82
+84
+112
+120
+116
+112
+84
+84
+120
+83
+132
+97
+98
+131
+132
+98
+85
+116
+84
+96
+97
+132
+96
+132
+125
+85
+136
+116
+86
+136
+85
+96
+126
+95
+88
+86
+87
+89
+86
+88
+90
+136
+86
+89
+90
+86
+135
+136
+90
+134
+135
+90
+134
+90
+91
+92
+93
+136
+135
+92
+136
+91
+92
+135
+91
+135
+134
+96
+125
+126
+140
+142
+139
+140
+141
+142
+143
+144
+145
+145
+146
+143
+155
+156
+150
+151
+155
+150
+152
+155
+151
+153
+155
+152
+155
+153
+154
+158
+148
+157
+147
+159
+160
+147
+158
+159
+147
+148
+158
+156
+149
+150
+148
+149
+157
+156
+157
+149
+185
+161
+208
+204
+184
+185
+208
+204
+185
+204
+183
+184
+203
+204
+208
+207
+208
+161
+203
+208
+207
+207
+161
+162
+209
+183
+204
+209
+204
+203
+182
+183
+209
+202
+209
+203
+206
+203
+207
+202
+203
+206
+207
+162
+163
+206
+207
+163
+201
+209
+202
+209
+181
+182
+190
+209
+201
+190
+181
+209
+205
+202
+206
+201
+202
+205
+164
+206
+163
+205
+206
+164
+191
+190
+201
+196
+191
+201
+197
+180
+181
+197
+181
+190
+205
+196
+201
+189
+190
+191
+165
+205
+164
+196
+205
+165
+195
+191
+196
+166
+196
+165
+195
+189
+191
+194
+189
+195
+198
+180
+197
+179
+180
+198
+188
+190
+189
+188
+189
+194
+197
+190
+188
+198
+197
+188
+166
+195
+196
+167
+195
+166
+194
+195
+167
+167
+193
+194
+168
+193
+167
+187
+198
+188
+199
+179
+198
+199
+198
+187
+178
+179
+199
+192
+193
+168
+192
+168
+169
+193
+186
+187
+199
+187
+186
+188
+194
+193
+193
+187
+188
+200
+199
+186
+178
+199
+200
+177
+178
+200
+212
+192
+169
+212
+169
+170
+172
+212
+170
+172
+170
+171
+192
+211
+186
+192
+186
+193
+211
+192
+212
+210
+200
+186
+210
+186
+211
+176
+177
+200
+210
+176
+200
+174
+210
+211
+175
+176
+210
+174
+175
+210
+173
+211
+212
+173
+212
+172
+174
+211
+173
+214
+216
+213
+214
+215
+216
+235
+261
+232
+238
+236
+237
+234
+235
+232
+261
+236
+238
+219
+220
+221
+235
+236
+261
+219
+250
+251
+250
+219
+221
+222
+250
+221
+222
+223
+250
+224
+249
+223
+248
+267
+250
+249
+248
+250
+223
+249
+250
+248
+266
+267
+266
+248
+249
+266
+249
+224
+225
+266
+224
+250
+267
+251
+234
+232
+233
+226
+265
+225
+266
+265
+267
+267
+265
+262
+264
+262
+265
+267
+262
+247
+262
+264
+263
+262
+263
+246
+247
+262
+246
+247
+246
+251
+247
+251
+267
+264
+243
+263
+242
+227
+228
+229
+242
+228
+258
+229
+259
+259
+261
+260
+260
+238
+239
+258
+257
+242
+258
+259
+257
+246
+252
+251
+257
+243
+242
+242
+243
+264
+254
+241
+217
+253
+254
+217
+259
+260
+256
+259
+256
+257
+245
+255
+253
+257
+256
+243
+245
+244
+255
+243
+256
+244
+254
+255
+240
+256
+260
+255
+255
+254
+253
+244
+256
+255
+260
+239
+255
+254
+240
+241
+239
+240
+255
+253
+217
+252
+245
+253
+252
+246
+245
+252
+244
+245
+263
+243
+244
+263
+252
+217
+218
+251
+252
+218
+229
+258
+242
+264
+227
+242
+261
+238
+260
+259
+231
+261
+231
+232
+261
+230
+231
+259
+229
+230
+259
+263
+245
+246
+264
+265
+227
+265
+226
+227
+266
+225
+265
+219
+251
+218
+268
+269
+270
+270
+271
+268
+272
+273
+274
+274
+275
+272
diff --git a/spine-android/app/src/main/assets/spineboy.png b/spine-android/app/src/main/assets/spineboy.png
new file mode 100644
index 000000000..0ea9737f3
Binary files /dev/null and b/spine-android/app/src/main/assets/spineboy.png differ
diff --git a/spine-android/app/src/main/java/com/esotericsoftware/spine/MainActivity.kt b/spine-android/app/src/main/java/com/esotericsoftware/spine/MainActivity.kt
new file mode 100644
index 000000000..440b9bb2c
--- /dev/null
+++ b/spine-android/app/src/main/java/com/esotericsoftware/spine/MainActivity.kt
@@ -0,0 +1,64 @@
+package com.esotericsoftware.spine
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.ui.Modifier
+import com.esotericsoftware.spine.ui.theme.SpineAndroidExamplesTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.viewinterop.AndroidView
+import com.esotericsoftware.spine.android.SpineView
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ AppContent()
+ }
+ }
+}
+
+@Composable
+fun AppContent() {
+ SpineAndroidExamplesTheme {
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ color = MaterialTheme.colorScheme.background
+ ) {
+ Box {
+ BackgroundImage()
+ SpineViewComposable()
+ }
+ }
+ }
+}
+
+@Composable
+fun SpineViewComposable(modifier: Modifier = Modifier.fillMaxSize()) {
+ val context = LocalContext.current
+ AndroidView(
+ factory = { ctx ->
+ SpineView(ctx).apply {
+ }
+ },
+ modifier = modifier
+ )
+}
+
+@Composable
+fun BackgroundImage() {
+ val image: Painter = painterResource(id = com.esotericsoftware.spine.R.drawable.img) // Replace with your image resource
+ Image(
+ painter = image,
+ contentDescription = null,
+ modifier = Modifier.fillMaxSize()
+ )
+}
diff --git a/spine-android/app/src/main/java/com/esotericsoftware/spine/ui/theme/Color.kt b/spine-android/app/src/main/java/com/esotericsoftware/spine/ui/theme/Color.kt
new file mode 100644
index 000000000..9e8e25d6c
--- /dev/null
+++ b/spine-android/app/src/main/java/com/esotericsoftware/spine/ui/theme/Color.kt
@@ -0,0 +1,11 @@
+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)
\ No newline at end of file
diff --git a/spine-android/app/src/main/java/com/esotericsoftware/spine/ui/theme/Theme.kt b/spine-android/app/src/main/java/com/esotericsoftware/spine/ui/theme/Theme.kt
new file mode 100644
index 000000000..a40d4c82c
--- /dev/null
+++ b/spine-android/app/src/main/java/com/esotericsoftware/spine/ui/theme/Theme.kt
@@ -0,0 +1,70 @@
+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
+ )
+}
\ No newline at end of file
diff --git a/spine-android/app/src/main/java/com/esotericsoftware/spine/ui/theme/Type.kt b/spine-android/app/src/main/java/com/esotericsoftware/spine/ui/theme/Type.kt
new file mode 100644
index 000000000..26034fc05
--- /dev/null
+++ b/spine-android/app/src/main/java/com/esotericsoftware/spine/ui/theme/Type.kt
@@ -0,0 +1,34 @@
+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
+ )
+ */
+)
\ No newline at end of file
diff --git a/spine-android/app/src/main/res/drawable/ic_launcher_background.xml b/spine-android/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 000000000..07d5da9cb
--- /dev/null
+++ b/spine-android/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/spine-android/app/src/main/res/drawable/ic_launcher_foreground.xml b/spine-android/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 000000000..2b068d114
--- /dev/null
+++ b/spine-android/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spine-android/app/src/main/res/drawable/img.png b/spine-android/app/src/main/res/drawable/img.png
new file mode 100644
index 000000000..1033d1ed8
Binary files /dev/null and b/spine-android/app/src/main/res/drawable/img.png differ
diff --git a/spine-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/spine-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..6f3b755bf
--- /dev/null
+++ b/spine-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spine-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/spine-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 000000000..6f3b755bf
--- /dev/null
+++ b/spine-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spine-android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/spine-android/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 000000000..c209e78ec
Binary files /dev/null and b/spine-android/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/spine-android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/spine-android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..b2dfe3d1b
Binary files /dev/null and b/spine-android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/spine-android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/spine-android/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 000000000..4f0f1d64e
Binary files /dev/null and b/spine-android/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/spine-android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/spine-android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..62b611da0
Binary files /dev/null and b/spine-android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/spine-android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/spine-android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 000000000..948a3070f
Binary files /dev/null and b/spine-android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/spine-android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/spine-android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..1b9a6956b
Binary files /dev/null and b/spine-android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/spine-android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/spine-android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 000000000..28d4b77f9
Binary files /dev/null and b/spine-android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/spine-android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/spine-android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..9287f5083
Binary files /dev/null and b/spine-android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/spine-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/spine-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 000000000..aa7d6427e
Binary files /dev/null and b/spine-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/spine-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/spine-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..9126ae37c
Binary files /dev/null and b/spine-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/spine-android/app/src/main/res/values/colors.xml b/spine-android/app/src/main/res/values/colors.xml
new file mode 100644
index 000000000..f8c6127d3
--- /dev/null
+++ b/spine-android/app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/spine-android/app/src/main/res/values/strings.xml b/spine-android/app/src/main/res/values/strings.xml
new file mode 100644
index 000000000..4c4059fd2
--- /dev/null
+++ b/spine-android/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ Spine Android Examples
+
\ No newline at end of file
diff --git a/spine-android/app/src/main/res/values/themes.xml b/spine-android/app/src/main/res/values/themes.xml
new file mode 100644
index 000000000..bbe8ed79b
--- /dev/null
+++ b/spine-android/app/src/main/res/values/themes.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/spine-android/app/src/main/res/xml/backup_rules.xml b/spine-android/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 000000000..fa0f996d2
--- /dev/null
+++ b/spine-android/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/spine-android/app/src/main/res/xml/data_extraction_rules.xml b/spine-android/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 000000000..9ee9997b0
--- /dev/null
+++ b/spine-android/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spine-android/app/src/test/java/com/esotericsoftware/android/ExampleUnitTest.kt b/spine-android/app/src/test/java/com/esotericsoftware/android/ExampleUnitTest.kt
new file mode 100644
index 000000000..beb91b570
--- /dev/null
+++ b/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)
+ }
+}
\ No newline at end of file
diff --git a/spine-android/build.gradle.kts b/spine-android/build.gradle.kts
new file mode 100644
index 000000000..9e4ae6cfd
--- /dev/null
+++ b/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
+}
\ No newline at end of file
diff --git a/spine-android/gradle.properties b/spine-android/gradle.properties
new file mode 100644
index 000000000..20e2a0152
--- /dev/null
+++ b/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
\ No newline at end of file
diff --git a/spine-android/gradle/libs.versions.toml b/spine-android/gradle/libs.versions.toml
new file mode 100644
index 000000000..5c964c35c
--- /dev/null
+++ b/spine-android/gradle/libs.versions.toml
@@ -0,0 +1,36 @@
+[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"
+material = "1.10.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" }
+material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+
+[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" }
+
diff --git a/spine-android/gradle/wrapper/gradle-wrapper.jar b/spine-android/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..e708b1c02
Binary files /dev/null and b/spine-android/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/spine-android/gradle/wrapper/gradle-wrapper.properties b/spine-android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..2cf9d9560
--- /dev/null
+++ b/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
diff --git a/spine-android/gradlew b/spine-android/gradlew
new file mode 100755
index 000000000..4f906e0c8
--- /dev/null
+++ b/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" "$@"
diff --git a/spine-android/gradlew.bat b/spine-android/gradlew.bat
new file mode 100644
index 000000000..107acd32c
--- /dev/null
+++ b/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
diff --git a/spine-android/settings.gradle.kts b/spine-android/settings.gradle.kts
new file mode 100644
index 000000000..7a60cd157
--- /dev/null
+++ b/spine-android/settings.gradle.kts
@@ -0,0 +1,34 @@
+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()
+ }
+}
+
+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")
diff --git a/spine-android/spine-android/.gitignore b/spine-android/spine-android/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/spine-android/spine-android/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/spine-android/spine-android/build.gradle.kts b/spine-android/spine-android/build.gradle.kts
new file mode 100644
index 000000000..358ff1a8a
--- /dev/null
+++ b/spine-android/spine-android/build.gradle.kts
@@ -0,0 +1,40 @@
+plugins {
+ alias(libs.plugins.androidLibrary)
+}
+
+android {
+ namespace = "com.esotericsoftware.spine"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 24
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+
+ implementation(libs.androidx.appcompat)
+ implementation(libs.material)
+ implementation("com.badlogicgames.gdx:gdx:1.12.1")
+ implementation("com.esotericsoftware.spine:spine-libgdx:4.2.0")
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+}
\ No newline at end of file
diff --git a/spine-android/spine-android/consumer-rules.pro b/spine-android/spine-android/consumer-rules.pro
new file mode 100644
index 000000000..e69de29bb
diff --git a/spine-android/spine-android/proguard-rules.pro b/spine-android/spine-android/proguard-rules.pro
new file mode 100644
index 000000000..481bb4348
--- /dev/null
+++ b/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
\ No newline at end of file
diff --git a/spine-android/spine-android/src/androidTest/java/com/esotericsoftware/android/ExampleInstrumentedTest.java b/spine-android/spine-android/src/androidTest/java/com/esotericsoftware/android/ExampleInstrumentedTest.java
new file mode 100644
index 000000000..4d375b3ab
--- /dev/null
+++ b/spine-android/spine-android/src/androidTest/java/com/esotericsoftware/android/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package com.esotericsoftware.android;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@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());
+ }
+}
\ No newline at end of file
diff --git a/spine-android/spine-android/src/main/AndroidManifest.xml b/spine-android/spine-android/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..a5918e68a
--- /dev/null
+++ b/spine-android/spine-android/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidAtlasAttachmentLoader.java b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidAtlasAttachmentLoader.java
new file mode 100644
index 000000000..44e29e2b7
--- /dev/null
+++ b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidAtlasAttachmentLoader.java
@@ -0,0 +1,110 @@
+/******************************************************************************
+ * 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;
+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}.
+ *
+ * See Loading skeleton data 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);
+ }
+}
diff --git a/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidTexture.java b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidTexture.java
new file mode 100644
index 000000000..42e3299a1
--- /dev/null
+++ b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidTexture.java
@@ -0,0 +1,45 @@
+package com.esotericsoftware.spine.android;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapShader;
+import android.graphics.Paint;
+import android.graphics.Shader;
+
+import com.badlogic.gdx.graphics.Texture;
+import com.badlogic.gdx.graphics.TextureData;
+
+public class AndroidTexture extends Texture {
+ private Bitmap bitmap;
+ private Paint paint;
+
+ protected AndroidTexture(Bitmap bitmap) {
+ super();
+ this.bitmap = bitmap;
+ this.paint = new Paint();
+ BitmapShader shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
+ paint.setShader(shader);
+ }
+
+ public Bitmap getBitmap() {
+ return bitmap;
+ }
+
+ public Paint getPaint() {
+ return paint;
+ }
+
+ @Override
+ public int getWidth() {
+ return bitmap.getWidth();
+ }
+
+ @Override
+ public int getHeight() {
+ return bitmap.getHeight();
+ }
+
+ @Override
+ public void dispose() {
+ bitmap.recycle();
+ }
+}
diff --git a/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidTextureAtlas.java b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidTextureAtlas.java
new file mode 100644
index 000000000..11f483b74
--- /dev/null
+++ b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidTextureAtlas.java
@@ -0,0 +1,98 @@
+package com.esotericsoftware.spine.android;
+
+import android.content.res.AssetManager;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+
+import com.badlogic.gdx.files.FileHandle;
+import com.badlogic.gdx.graphics.g2d.TextureAtlas;
+import com.badlogic.gdx.graphics.g2d.TextureAtlas.TextureAtlasData;
+import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.Null;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.Buffer;
+
+public class AndroidTextureAtlas {
+ private static interface BitmapLoader {
+ Bitmap load(String path);
+ }
+
+ private Array textures = new Array<>();
+ private Array 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 getTextures() {
+ return textures;
+ }
+
+ public Array getRegions() {
+ return regions;
+ }
+
+ static public AndroidTextureAtlas loadFromAssets(String atlasFile, AssetManager assetManager) {
+ TextureAtlasData data = new TextureAtlasData();
+
+ try {
+ FileHandle inputFile = new FileHandle() {
+ @Override
+ public InputStream read() {
+ try {
+ return assetManager.open(atlasFile);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ };
+ data.load(inputFile, new FileHandle(atlasFile).parent(), false);
+ } catch (Throwable t) {
+ throw new RuntimeException(t);
+ }
+
+ return new AndroidTextureAtlas(data, new BitmapLoader() {
+ @Override
+ public Bitmap load(String 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);
+ }
+ }
+ });
+ }
+}
diff --git a/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/SpineView.java b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/SpineView.java
new file mode 100644
index 000000000..7a24f57e9
--- /dev/null
+++ b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/SpineView.java
@@ -0,0 +1,231 @@
+package com.esotericsoftware.spine.android;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.view.Choreographer;
+import android.view.View;
+
+import com.badlogic.gdx.math.MathUtils;
+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.esotericsoftware.spine.AnimationState;
+import com.esotericsoftware.spine.AnimationStateData;
+import com.esotericsoftware.spine.BlendMode;
+import com.esotericsoftware.spine.Skeleton;
+import com.esotericsoftware.spine.SkeletonBinary;
+import com.esotericsoftware.spine.SkeletonData;
+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 java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class SpineView extends View implements Choreographer.FrameCallback {
+ private long lastTime = 0;
+ private long delta = 0;
+ private Paint textPaint;
+ int instances = 100;
+ Vector2[] coords = new Vector2[instances];
+ AndroidTextureAtlas atlas;
+ SkeletonData data;
+ Array skeletons = new Array<>();
+
+ Array states = new Array<>();
+
+ public SpineView(Context context) {
+ super(context);
+ init();
+ }
+
+ public SpineView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public SpineView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init();
+ }
+
+ private void loadSkeleton() {
+ AssetManager assetManager = this.getContext().getAssets();
+ atlas = AndroidTextureAtlas.loadFromAssets("spineboy.atlas", assetManager);
+ AndroidAtlasAttachmentLoader attachmentLoader = new AndroidAtlasAttachmentLoader(atlas);
+ SkeletonBinary binary = new SkeletonBinary(attachmentLoader);
+ try (InputStream in = new BufferedInputStream(assetManager.open("spineboy-pro.skel"))) {
+ data = binary.readSkeletonData(in);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ static private final short[] quadTriangles = {0, 1, 2, 2, 3, 0};
+ private final FloatArray vertices = new FloatArray(32);
+ private final FloatArray texCoords = new FloatArray(32);
+ private final IntArray colors = new IntArray(32);
+ private final SkeletonClipping clipper = new SkeletonClipping();
+
+ public void render (Canvas canvas, Skeleton skeleton, float x, float y) {
+ canvas.save();
+ canvas.translate(x, y);
+ canvas.scale(1, -1);
+ BlendMode blendMode = null;
+ int verticesLength = 0;
+ short[] triangles = null;
+ com.badlogic.gdx.graphics.Color color = null, skeletonColor = skeleton.getColor();
+ float r = skeletonColor.r, g = skeletonColor.g, b = skeletonColor.b, a = skeletonColor.a;
+ 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;
+ }
+ AndroidTexture texture = null;
+ int vertexSize = 2;
+ Attachment attachment = slot.getAttachment();
+ if (attachment instanceof RegionAttachment) {
+ RegionAttachment region = (RegionAttachment)attachment;
+ verticesLength = vertexSize << 2;
+ region.computeWorldVertices(slot, vertices.items, 0, vertexSize);
+ triangles = quadTriangles;
+ texture = (AndroidTexture)region.getRegion().getTexture();
+ texCoords.clear();
+ texCoords.addAll(region.getUVs());
+ color = region.getColor();
+
+ } else if (attachment instanceof MeshAttachment) {
+ MeshAttachment mesh = (MeshAttachment)attachment;
+ int count = mesh.getWorldVerticesLength();
+ verticesLength = (count >> 1) * vertexSize;
+ this.vertices.setSize(verticesLength);
+ mesh.computeWorldVertices(slot, 0, count, vertices.items, 0, vertexSize);
+ triangles = mesh.getTriangles();
+ texture = (AndroidTexture)mesh.getRegion().getTexture();
+ texCoords.clear();;
+ texCoords.addAll(mesh.getUVs());
+ color = mesh.getColor();
+
+ } else if (attachment instanceof ClippingAttachment) {
+ ClippingAttachment clip = (ClippingAttachment)attachment;
+ clipper.clipStart(slot, clip);
+ continue;
+
+ } else {
+ continue;
+ }
+
+ if (texture != null) {
+ com.badlogic.gdx.graphics.Color slotColor = slot.getColor();
+ float alpha = a * slotColor.a * color.a * 255;
+ float multiplier = 255;
+
+ BlendMode slotBlendMode = slot.getData().getBlendMode();
+ if (slotBlendMode != blendMode) {
+ if (slotBlendMode == BlendMode.additive) {
+ slotBlendMode = BlendMode.normal;
+ alpha = 0;
+ }
+ blendMode = slotBlendMode;
+ // FIXME
+ // blendMode.apply(batch, pmaBlendModes);
+ }
+
+ int c = (int)alpha << 24 //
+ | (int)(b * slotColor.b * color.b * multiplier) << 16 //
+ | (int)(g * slotColor.g * color.g * multiplier) << 8 //
+ | (int)(r * slotColor.r * color.r * multiplier);
+
+ if (clipper.isClipping()) {
+ // FIXME
+ throw new RuntimeException("Not implemented, need to split positions, uvs, colors");
+ // clipper.clipTriangles(vertices, verticesLength, triangles, triangles.length, uvs, c, 0, false);
+ // FloatArray clippedVertices = clipper.getClippedVertices();
+ // ShortArray clippedTriangles = clipper.getClippedTriangles();
+ // batch.draw(texture, clippedVertices.items, 0, clippedVertices.size, clippedTriangles.items, 0,
+ // clippedTriangles.size);
+ } else {
+ float[] uvsArray = texCoords.items;
+ for (int ii = 0, w = texture.getWidth(), h = texture.getHeight(); ii < verticesLength; ii += 2) {
+ uvsArray[ii] = uvsArray[ii] * w;
+ uvsArray[ii + 1] = uvsArray[ii + 1] * h;
+ }
+ colors.setSize(verticesLength >> 1);
+ int[] colorsArray = colors.items;
+ for (int ii = 0, nn = verticesLength >> 1; ii < nn; ii++) {
+ colorsArray[ii] = c;
+ }
+ canvas.drawVertices(Canvas.VertexMode.TRIANGLES, verticesLength, vertices.items, 0, uvsArray, 0, colorsArray, 0, triangles, 0, triangles.length, texture.getPaint());
+ }
+ }
+
+ clipper.clipEnd(slot);
+ }
+ clipper.clipEnd();
+ canvas.restore();
+ }
+
+ private void init() {
+ textPaint = new Paint();
+ textPaint.setColor(Color.WHITE); // Set the color of the paint
+ textPaint.setTextSize(48);
+ Choreographer.getInstance().postFrameCallback(this);
+
+ loadSkeleton();
+
+ for (int i = 0; i < instances; i++) {
+ Skeleton skeleton = new Skeleton(data);
+ skeleton.setToSetupPose();
+ skeletons.add(skeleton);
+
+ AnimationStateData stateData = new AnimationStateData(data);
+ stateData.setDefaultMix(0.2f);
+ AnimationState state = new AnimationState(stateData);
+ state.setAnimation(0, "walk", true);
+ states.add(state);
+
+ coords[i] = new Vector2(MathUtils.random(1000), MathUtils.random(2000));
+ }
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ float deltaF = delta / 1e9f;
+
+ for (int i = 0; i < instances; i++) {
+ AnimationState state = states.get(i);
+ Skeleton skeleton = skeletons.get(i);
+ state.update(deltaF);
+ state.apply(skeleton);
+ skeleton.update(deltaF);
+ skeleton.updateWorldTransform(Skeleton.Physics.update);
+ render(canvas, skeleton, coords[i].x, coords[i].y);
+ }
+ // canvas.drawVertices(Canvas.VertexMode.TRIANGLES, vertices.size, vertices.items, 0, uvs.items, 0, null, 0, indices.items, 0, 3 * 75, paint);
+ canvas.drawText(delta / 1e6 + " ms", 100, 100, textPaint);
+ canvas.drawText(instances + " instances", 100, 150, textPaint);
+ }
+
+ @Override
+ public void doFrame(long frameTimeNanos) {
+ if (lastTime != 0) delta = frameTimeNanos - lastTime;
+ lastTime = frameTimeNanos;
+
+ // Invalidate this view, causing onDraw to be called at the next animation frame
+ invalidate();
+ Choreographer.getInstance().postFrameCallback(this);
+ }
+}
diff --git a/spine-android/spine-android/src/test/java/com/esotericsoftware/android/ExampleUnitTest.java b/spine-android/spine-android/src/test/java/com/esotericsoftware/android/ExampleUnitTest.java
new file mode 100644
index 000000000..cc5cf30cc
--- /dev/null
+++ b/spine-android/spine-android/src/test/java/com/esotericsoftware/android/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package com.esotericsoftware.android;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file