("mavenLocal") {
+ groupId = "com.esotericsoftware"
+ artifactId = "spine-android"
+ version = "4.2"
+ artifact(tasks.getByName("bundleReleaseAar"))
+
+ pom {
+ withXml {
+ val dependenciesNode = asNode().appendNode("dependencies")
+ configurations.api.get().dependencies.forEach { dependency ->
+ dependenciesNode.appendNode("dependency").apply {
+ appendNode("groupId", dependency.group)
+ appendNode("artifactId", dependency.name)
+ appendNode("version", dependency.version)
+ appendNode("scope", "compile")
+ }
+ }
+ configurations.implementation.get().dependencies.forEach { dependency ->
+ dependenciesNode.appendNode("dependency").apply {
+ appendNode("groupId", dependency.group)
+ appendNode("artifactId", dependency.name)
+ appendNode("version", dependency.version)
+ appendNode("scope", "runtime")
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
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..ecebd576e
--- /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..1f9aa289b
--- /dev/null
+++ b/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}.
+ *
+ * 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/AndroidSkeletonDrawable.java b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidSkeletonDrawable.java
new file mode 100644
index 000000000..e71a2fab7
--- /dev/null
+++ b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidSkeletonDrawable.java
@@ -0,0 +1,176 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+
+import com.badlogic.gdx.math.Vector2;
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.FloatArray;
+import com.esotericsoftware.spine.Animation;
+import com.esotericsoftware.spine.AnimationState;
+import com.esotericsoftware.spine.AnimationStateData;
+import com.esotericsoftware.spine.Skeleton;
+import com.esotericsoftware.spine.SkeletonData;
+import com.esotericsoftware.spine.android.utils.SkeletonDataUtils;
+
+import java.io.File;
+import java.net.URL;
+
+/**
+ * A {@link AndroidSkeletonDrawable} bundles loading updating updating an {@link AndroidTextureAtlas}, {@link Skeleton}, and {@link AnimationState}
+ * into a single easy-to-use class.
+ *
+ * Use the {@link AndroidSkeletonDrawable#fromAsset(String, String, Context)}, {@link AndroidSkeletonDrawable#fromFile(File, File)},
+ * or {@link AndroidSkeletonDrawable#fromHttp(URL, URL, File)} methods to construct a {@link AndroidSkeletonDrawable}. To have
+ * multiple skeleton drawable instances share the same {@link AndroidTextureAtlas} and {@link SkeletonData}, use the constructor.
+ *
+ * You can then directly access the {@link AndroidSkeletonDrawable#getAtlas()}, {@link AndroidSkeletonDrawable#getSkeletonData()},
+ * {@link AndroidSkeletonDrawable#getSkeleton()}, {@link AndroidSkeletonDrawable#getAnimationStateData()}, and {@link AndroidSkeletonDrawable#getAnimationState()}
+ * to query and animate the skeleton. Use the {@link AnimationState} to queue animations on one or more tracks
+ * via {@link AnimationState#setAnimation(int, Animation, boolean)} or {@link AnimationState#addAnimation(int, Animation, boolean, float)}.
+ *
+ * To update the {@link AnimationState} and apply it to the {@link Skeleton}, call the {@link AndroidSkeletonDrawable#update(float)} function, providing it
+ * a delta time in seconds to advance the animations.
+ *
+ * To render the current pose of the {@link Skeleton}, use {@link SkeletonRenderer#render(Skeleton)}, {@link SkeletonRenderer#renderToCanvas(Canvas, Array)},
+ * {@link SkeletonRenderer#renderToBitmap(float, float, int, Skeleton)}, depending on your needs.
+ */
+public class AndroidSkeletonDrawable {
+
+ private final AndroidTextureAtlas atlas;
+
+ private final SkeletonData skeletonData;
+
+ private final Skeleton skeleton;
+
+ private final AnimationStateData animationStateData;
+
+ private final AnimationState animationState;
+
+ /**
+ * Constructs a new skeleton drawable from the given (possibly shared) {@link AndroidTextureAtlas} and {@link SkeletonData}.
+ */
+ public AndroidSkeletonDrawable(AndroidTextureAtlas atlas, SkeletonData skeletonData) {
+ this.atlas = atlas;
+ this.skeletonData = skeletonData;
+
+ skeleton = new Skeleton(skeletonData);
+ animationStateData = new AnimationStateData(skeletonData);
+ animationState = new AnimationState(animationStateData);
+
+ skeleton.updateWorldTransform(Skeleton.Physics.none);
+ }
+
+ /**
+ * Updates the {@link AnimationState} using the {@code delta} time given in seconds, applies the
+ * animation state to the {@link Skeleton} and updates the world transforms of the skeleton
+ * to calculate its current pose.
+ */
+ public void update(float delta) {
+ animationState.update(delta);
+ animationState.apply(skeleton);
+
+ skeleton.update(delta);
+ skeleton.updateWorldTransform(Skeleton.Physics.update);
+ }
+
+ /**
+ * Get the {@link AndroidTextureAtlas}
+ */
+ public AndroidTextureAtlas getAtlas() {
+ return atlas;
+ }
+
+ /**
+ * Get the {@link Skeleton}
+ */
+ public Skeleton getSkeleton() {
+ return skeleton;
+ }
+
+ /**
+ * Get the {@link SkeletonData}
+ */
+ public SkeletonData getSkeletonData() {
+ return skeletonData;
+ }
+
+ /**
+ * Get the {@link AnimationStateData}
+ */
+ public AnimationStateData getAnimationStateData() {
+ return animationStateData;
+ }
+
+ /**
+ * Get the {@link AnimationState}
+ */
+ public AnimationState getAnimationState() {
+ return animationState;
+ }
+
+ /**
+ * Constructs a new skeleton drawable from the {@code atlasFileName} and {@code skeletonFileName} from the the apps resources using {@link Context}.
+ *
+ * Throws an exception in case the data could not be loaded.
+ */
+ public static AndroidSkeletonDrawable fromAsset (String atlasFileName, String skeletonFileName, Context context) {
+ AndroidTextureAtlas atlas = AndroidTextureAtlas.fromAsset(atlasFileName, context);
+ SkeletonData skeletonData = SkeletonDataUtils.fromAsset(atlas, skeletonFileName, context);
+ return new AndroidSkeletonDrawable(atlas, skeletonData);
+ }
+
+ /**
+ * Constructs a new skeleton drawable from the {@code atlasFile} and {@code skeletonFile}.
+ *
+ * Throws an exception in case the data could not be loaded.
+ */
+ public static AndroidSkeletonDrawable fromFile (File atlasFile, File skeletonFile) {
+ AndroidTextureAtlas atlas = AndroidTextureAtlas.fromFile(atlasFile);
+ SkeletonData skeletonData = SkeletonDataUtils.fromFile(atlas, skeletonFile);
+ return new AndroidSkeletonDrawable(atlas, skeletonData);
+ }
+
+ /**
+ * Constructs a new skeleton drawable from the {@code atlasUrl} and {@code skeletonUrl}.
+ *
+ * Throws an exception in case the data could not be loaded.
+ */
+ public static AndroidSkeletonDrawable fromHttp (URL atlasUrl, URL skeletonUrl, File targetDirectory) {
+ AndroidTextureAtlas atlas = AndroidTextureAtlas.fromHttp(atlasUrl, targetDirectory);
+ SkeletonData skeletonData = SkeletonDataUtils.fromHttp(atlas, skeletonUrl, targetDirectory);
+ return new AndroidSkeletonDrawable(atlas, skeletonData);
+ }
+}
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..95fe94665
--- /dev/null
+++ b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidTexture.java
@@ -0,0 +1,102 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android;
+
+import com.badlogic.gdx.graphics.Texture;
+import com.badlogic.gdx.utils.ObjectMap;
+import com.esotericsoftware.spine.BlendMode;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapShader;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Shader;
+
+/**
+ * A class holding an {@link Bitmap} of an {@link AndroidTextureAtlas} page image with it's associated
+ * blend modes and paints.
+ */
+public class AndroidTexture extends Texture {
+ private Bitmap bitmap;
+ private ObjectMap 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();
+ }
+}
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..67bdf33bb
--- /dev/null
+++ b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidTextureAtlas.java
@@ -0,0 +1,232 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.file.Files;
+
+import com.badlogic.gdx.files.FileHandle;
+import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
+import com.badlogic.gdx.graphics.g2d.TextureAtlas.TextureAtlasData;
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.Null;
+import com.esotericsoftware.spine.android.utils.HttpUtils;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.graphics.Bitmap;
+import android.graphics.Paint;
+import android.graphics.BitmapFactory;
+import android.os.Build;
+
+/**
+ * Atlas data loaded from a `.atlas` file and its corresponding `.png` files. For each atlas image,
+ * a corresponding {@link Bitmap} and {@link Paint} is constructed, which are used when rendering a skeleton
+ * that uses this atlas.
+ *
+ * Use the static methods {@link AndroidTextureAtlas#fromAsset(String, Context)}, {@link AndroidTextureAtlas#fromFile(File)},
+ * and {@link AndroidTextureAtlas#fromHttp(URL, File)} to load an atlas.
+ */
+public class AndroidTextureAtlas {
+ private interface BitmapLoader {
+ Bitmap load (String path);
+ }
+
+ private final Array textures = new Array<>();
+ private final 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;
+ }
+
+ /**
+ * 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;
+ }
+}
diff --git a/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/DebugRenderer.java b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/DebugRenderer.java
new file mode 100644
index 000000000..24f166f12
--- /dev/null
+++ b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/DebugRenderer.java
@@ -0,0 +1,56 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+
+import com.badlogic.gdx.utils.Array;
+import com.esotericsoftware.spine.Bone;
+
+/**
+ * Renders debug information for a {@link AndroidSkeletonDrawable}, like bone locations, to a {@link Canvas}.
+ * See {@link DebugRenderer#render}.
+ */
+public class DebugRenderer {
+
+ public void render(AndroidSkeletonDrawable drawable, Canvas canvas, Array 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);
+ }
+ }
+}
diff --git a/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/SkeletonRenderer.java b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/SkeletonRenderer.java
new file mode 100644
index 000000000..f0d496a3a
--- /dev/null
+++ b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/SkeletonRenderer.java
@@ -0,0 +1,281 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android;
+
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.math.Vector2;
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.FloatArray;
+import com.badlogic.gdx.utils.IntArray;
+import com.badlogic.gdx.utils.Pool;
+import com.badlogic.gdx.utils.ShortArray;
+import com.esotericsoftware.spine.BlendMode;
+import com.esotericsoftware.spine.Skeleton;
+import com.esotericsoftware.spine.Slot;
+import com.esotericsoftware.spine.attachments.Attachment;
+import com.esotericsoftware.spine.attachments.ClippingAttachment;
+import com.esotericsoftware.spine.attachments.MeshAttachment;
+import com.esotericsoftware.spine.attachments.RegionAttachment;
+import com.esotericsoftware.spine.utils.SkeletonClipping;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+
+/**
+ * Is responsible to transform the {@link Skeleton} with its current pose to {@link SkeletonRenderer.RenderCommand} commands
+ * and render them to a {@link Canvas}.
+ */
+public class SkeletonRenderer {
+
+ /**
+ * Stores the vertices, indices, and atlas page index to be used for rendering one or more attachments
+ * of a {@link Skeleton} to a {@link Canvas}. See the implementation of {@link SkeletonRenderer#render(Skeleton)} and
+ * {@link SkeletonRenderer#renderToCanvas(Canvas, Array)} on how to use this data to render it to a {@link Canvas}.
+ */
+ public static class RenderCommand implements Pool.Poolable {
+ FloatArray vertices = new FloatArray(32);
+ FloatArray uvs = new FloatArray(32);
+ IntArray colors = new IntArray(32);
+ ShortArray indices = new ShortArray(32);
+ BlendMode blendMode;
+ AndroidTexture texture;
+
+ @Override
+ public void reset () {
+ vertices.setSize(0);
+ uvs.setSize(0);
+ colors.setSize(0);
+ indices.setSize(0);
+ blendMode = null;
+ texture = null;
+ }
+ }
+
+ static private final short[] quadTriangles = {0, 1, 2, 2, 3, 0};
+ private final SkeletonClipping clipper = new SkeletonClipping();
+ private final Pool commandPool = new Pool(10) {
+ @Override
+ protected RenderCommand newObject () {
+ return new RenderCommand();
+ }
+ };
+ private final Array commandList = new Array();
+
+ /**
+ * Created the {@link RenderCommand} commands from the skeletons current pose.
+ */
+ public Array render(Skeleton skeleton) {
+ Color color = null, skeletonColor = skeleton.getColor();
+ float r = skeletonColor.r, g = skeletonColor.g, b = skeletonColor.b, a = skeletonColor.a;
+
+ commandPool.freeAll(commandList);
+ commandList.clear();
+ RenderCommand command = commandPool.obtain();
+ commandList.add(command);
+ int vertexStart = 0;
+
+ Object[] drawOrder = skeleton.getDrawOrder().items;
+ for (int i = 0, n = skeleton.getDrawOrder().size; i < n; i++) {
+ Slot slot = (Slot)drawOrder[i];
+ if (!slot.getBone().isActive()) {
+ clipper.clipEnd(slot);
+ continue;
+ }
+
+ int verticesLength = 0;
+ int vertexSize = 2;
+ float[] uvs = null;
+ short[] indices = null;
+ Attachment attachment = slot.getAttachment();
+ if (attachment == null) {
+ continue;
+ }
+
+ if (attachment instanceof RegionAttachment) {
+ RegionAttachment region = (RegionAttachment)attachment;
+ verticesLength = vertexSize << 2;
+ if (region.getSequence() != null) region.getSequence().apply(slot, region);
+ AndroidTexture texture = (AndroidTexture)region.getRegion().getTexture();
+ BlendMode blendMode = slot.getData().getBlendMode();
+ if (command.blendMode == null && command.texture == null) {
+ command.blendMode = blendMode;
+ command.texture = texture;
+ }
+
+ if (command.blendMode != blendMode || command.texture != texture || command.vertices.size + verticesLength > 64000) {
+ command = commandPool.obtain();
+ commandList.add(command);
+ vertexStart = 0;
+ command.blendMode = blendMode;
+ command.texture = texture;
+ }
+
+ command.vertices.setSize(command.vertices.size + verticesLength);
+ region.computeWorldVertices(slot, command.vertices.items, vertexStart, vertexSize);
+ uvs = region.getUVs();
+ indices = quadTriangles;
+ color = region.getColor();
+ } else if (attachment instanceof MeshAttachment) {
+ MeshAttachment mesh = (MeshAttachment)attachment;
+ verticesLength = mesh.getWorldVerticesLength();
+ if (mesh.getSequence() != null) mesh.getSequence().apply(slot, mesh);
+ AndroidTexture texture = (AndroidTexture)mesh.getRegion().getTexture();
+ BlendMode blendMode = slot.getData().getBlendMode();
+
+ if (command.blendMode == null && command.texture == null) {
+ command.blendMode = blendMode;
+ command.texture = texture;
+ }
+
+ if (command.blendMode != blendMode || command.texture != texture || command.vertices.size + verticesLength > 64000) {
+ command = commandPool.obtain();
+ commandList.add(command);
+ vertexStart = 0;
+ command.blendMode = blendMode;
+ command.texture = texture;
+ }
+
+ command.vertices.setSize(command.vertices.size + verticesLength);
+ mesh.computeWorldVertices(slot, 0, verticesLength, command.vertices.items, vertexStart, vertexSize);
+ uvs = mesh.getUVs();
+ indices = mesh.getTriangles();
+ color = mesh.getColor();
+ } else if (attachment instanceof ClippingAttachment) {
+ ClippingAttachment clip = (ClippingAttachment)attachment;
+ clipper.clipStart(slot, clip);
+ continue;
+ } else {
+ continue;
+ }
+
+ Color slotColor = slot.getColor();
+ int c = (int)(a * slotColor.a * color.a * 255) << 24 //
+ | (int)(r * slotColor.r * color.r * 255) << 16 //
+ | (int)(g * slotColor.g * color.g * 255) << 8 //
+ | (int)(b * slotColor.b * color.b * 255);
+
+ if (clipper.isClipping()) {
+ // FIXME
+ throw new RuntimeException("Not implemented, need to split positions, uvs, colors");
+ // clipper.clipTriangles(vertices, verticesLength, triangles, triangles.length, uvs, c, 0, false);
+ // FloatArray clippedVertices = clipper.getClippedVertices();
+ // ShortArray clippedTriangles = clipper.getClippedTriangles();
+ // batch.draw(texture, clippedVertices.items, 0, clippedVertices.size, clippedTriangles.items, 0,
+ // clippedTriangles.size);
+ } else {
+ command.uvs.addAll(uvs);
+ float[] uvsArray = command.uvs.items;
+ for (int ii = vertexStart, w = command.texture.getWidth(), h = command.texture.getHeight(),
+ nn = vertexStart + verticesLength; ii < nn; ii += 2) {
+ uvsArray[ii] = uvsArray[ii] * w;
+ uvsArray[ii + 1] = uvsArray[ii + 1] * h;
+ }
+
+ command.colors.setSize(command.colors.size + (verticesLength >> 1));
+ int[] colorsArray = command.colors.items;
+ for (int ii = vertexStart >> 1, nn = (vertexStart >> 1) + (verticesLength >> 1); ii < nn; ii++) {
+ colorsArray[ii] = c;
+ }
+
+ int indicesStart = command.indices.size;
+ command.indices.addAll(indices);
+ int firstIndex = vertexStart >> 1;
+ short[] indicesArray = command.indices.items;
+ for (int ii = indicesStart, nn = indicesStart + indices.length; ii < nn; ii++) {
+ indicesArray[ii] += firstIndex;
+ }
+ }
+ // FIXME wrt clipping
+ vertexStart += verticesLength;
+ clipper.clipEnd(slot);
+ }
+ clipper.clipEnd();
+
+ if (commandList.size == 1 && commandList.get(0).vertices.size == 0) {
+ commandPool.freeAll(commandList);
+ commandList.clear();
+ }
+
+ return commandList;
+ }
+
+ /**
+ * Renders the {@link RenderCommand} commands created from the skeleton current pose to the given {@link Canvas}.
+ * Does not perform any scaling or fitting.
+ */
+ public void renderToCanvas(Canvas canvas, Array commands) {
+ for (int i = 0; i < commands.size; i++) {
+ RenderCommand command = commands.get(i);
+
+ canvas.drawVertices(Canvas.VertexMode.TRIANGLES, command.vertices.size, command.vertices.items, 0, command.uvs.items, 0,
+ command.colors.items, 0, command.indices.items, 0, command.indices.size, command.texture.getPaint(command.blendMode));
+ }
+ }
+
+ /**
+ * Renders the {@link Skeleton} with its current pose to a {@link Bitmap}.
+ *
+ * @param width The width of the bitmap in pixels.
+ * @param height The height of the bitmap in pixels.
+ * @param bgColor The background color.
+ * @param skeleton The skeleton to render.
+ */
+ public Bitmap renderToBitmap(float width, float height, int bgColor, Skeleton skeleton) {
+ Vector2 offset = new Vector2(0, 0);
+ Vector2 size = new Vector2(0, 0);
+ FloatArray floatArray = new FloatArray();
+
+ skeleton.getBounds(offset, size, floatArray);
+
+ RectF bounds = new RectF(offset.x, offset.y, offset.x + size.x, offset.y + size.y);
+ float scale = (1 / (bounds.width() > bounds.height() ? bounds.width() / width : bounds.height() / height));
+
+ Bitmap bitmap = Bitmap.createBitmap((int) width, (int) height, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+
+ Paint paint = new Paint();
+ paint.setColor(bgColor);
+ paint.setStyle(Paint.Style.FILL);
+
+ // Draw background
+ canvas.drawRect(0, 0, width, height, paint);
+
+ // Transform canvas
+ canvas.translate(width / 2, height / 2);
+ canvas.scale(scale, -scale);
+ canvas.translate(-(bounds.left + bounds.width() / 2), -(bounds.top + bounds.height() / 2));
+
+ renderToCanvas(canvas, render(skeleton));
+
+ return bitmap;
+ }
+}
diff --git a/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/SpineController.java b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/SpineController.java
new file mode 100644
index 000000000..3f2c9d50f
--- /dev/null
+++ b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/SpineController.java
@@ -0,0 +1,331 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android;
+
+import android.graphics.Canvas;
+import android.graphics.Point;
+
+import androidx.annotation.Nullable;
+
+import com.badlogic.gdx.utils.Array;
+import com.esotericsoftware.spine.AnimationState;
+import com.esotericsoftware.spine.AnimationStateData;
+import com.esotericsoftware.spine.Skeleton;
+import com.esotericsoftware.spine.SkeletonData;
+import com.esotericsoftware.spine.android.callbacks.SpineControllerAfterPaintCallback;
+import com.esotericsoftware.spine.android.callbacks.SpineControllerBeforePaintCallback;
+import com.esotericsoftware.spine.android.callbacks.SpineControllerCallback;
+
+/**
+ * Controls how the skeleton of a {@link SpineView} is animated and rendered.
+ *
+ * Upon initialization of a {@link SpineView}, the provided {@code onInitialized} callback method is called once. This method can be used
+ * to set up the initial animation(s) of the skeleton, among other things.
+ *
+ * After initialization is complete, the {@link SpineView} is rendered at the screen refresh rate. In each frame,
+ * the {@link AnimationState} is updated and applied to the {@link Skeleton}.
+ *
+ * Next, the optionally provided method {@code onBeforeUpdateWorldTransforms} is called, which can modify the
+ * skeleton before its current pose is calculated using {@link Skeleton#updateWorldTransform(Skeleton.Physics)}. After
+ * {@link Skeleton#updateWorldTransform(Skeleton.Physics)} has completed, the optional {@code onAfterUpdateWorldTransforms} method is
+ * called, which can modify the current pose before rendering the skeleton.
+ *
+ * Before the skeleton's current pose is rendered by the {@link SpineView}, the optional {@code onBeforePaint} is called,
+ * which allows rendering backgrounds or other objects that should go behind the skeleton on the {@link Canvas}. The
+ * {@link SpineView} then renders the skeleton's current pose and finally calls the optional {@code onAfterPaint}, which
+ * can render additional objects on top of the skeleton.
+ *
+ * The underlying {@link AndroidTextureAtlas}, {@link SkeletonData}, {@link Skeleton}, {@link AnimationStateData}, {@link AnimationState}, and {@link AndroidSkeletonDrawable}
+ * can be accessed through their respective getters to inspect and/or modify the skeleton and its associated data. Accessing
+ * this data is only allowed if the {@link SpineView} and its data have been initialized and have not been disposed of yet.
+ *
+ * By default, the widget updates and renders the skeleton every frame. The {@code pause} method can be used to pause updating
+ * and rendering the skeleton. The {@link SpineController#resume()} method resumes updating and rendering the skeleton. The {@link SpineController#isPlaying()} getter
+ * reports the current state.
+ */
+public class SpineController {
+ /**
+ * Used to build {@link SpineController} instances.
+ * */
+ public static class Builder {
+ private final SpineControllerCallback onInitialized;
+ private SpineControllerCallback onBeforeUpdateWorldTransforms;
+ private SpineControllerCallback onAfterUpdateWorldTransforms;
+ private SpineControllerBeforePaintCallback onBeforePaint;
+ private SpineControllerAfterPaintCallback onAfterPaint;
+
+ /**
+ * Instantiate a {@link Builder} used to build a {@link SpineController}, which controls how the skeleton of a {@link SpineView}
+ * is animated and rendered. Upon initialization of a {@link SpineView}, the provided {@code onInitialized} callback
+ * method is called once. This method can be used to set up the initial animation(s) of the skeleton, among other things.
+ *
+ * @param onInitialized Upon initialization of a {@link SpineView}, the provided {@code onInitialized} callback
+ * method is called once. This method can be used to set up the initial animation(s) of the skeleton,
+ * among other things.
+ */
+ public Builder(SpineControllerCallback onInitialized) {
+ this.onInitialized = onInitialized;
+ }
+
+ /**
+ * Sets the {@code onBeforeUpdateWorldTransforms} callback. It is called before the skeleton's current pose is calculated
+ * using {@link Skeleton#updateWorldTransform(Skeleton.Physics)}. It can be used to modify the skeleton before the pose calculation.
+ */
+ public Builder setOnBeforeUpdateWorldTransforms(SpineControllerCallback onBeforeUpdateWorldTransforms) {
+ this.onBeforeUpdateWorldTransforms = onBeforeUpdateWorldTransforms;
+ return this;
+ }
+
+ /**
+ * Sets the {@code onAfterUpdateWorldTransforms} callback. This method is called after the skeleton's current pose is calculated using
+ * {@link Skeleton#updateWorldTransform(Skeleton.Physics)}. It can be used to modify the current pose before rendering the skeleton.
+ */
+ public Builder setOnAfterUpdateWorldTransforms(SpineControllerCallback onAfterUpdateWorldTransforms) {
+ this.onAfterUpdateWorldTransforms = onAfterUpdateWorldTransforms;
+ return this;
+ }
+
+ /**
+ * Sets the {@code onBeforePaint} callback. It is called before the skeleton's current pose is rendered by the
+ * {@link SpineView}. It allows rendering backgrounds or other objects that should go behind the skeleton on the
+ * {@link Canvas}.
+ */
+ public Builder setOnBeforePaint(SpineControllerBeforePaintCallback onBeforePaint) {
+ this.onBeforePaint = onBeforePaint;
+ return this;
+ }
+
+ /**
+ * Sets the {@code onAfterPaint} callback. It is called after the skeleton's current pose is rendered by the
+ * {@link SpineView}. It allows rendering additional objects on top of the skeleton.
+ */
+ public Builder setOnAfterPaint(SpineControllerAfterPaintCallback onAfterPaint) {
+ this.onAfterPaint = onAfterPaint;
+ return this;
+ }
+
+ public SpineController build() {
+ SpineController spineController = new SpineController(onInitialized);
+ spineController.onBeforeUpdateWorldTransforms = onBeforeUpdateWorldTransforms;
+ spineController.onAfterUpdateWorldTransforms = onAfterUpdateWorldTransforms;
+ spineController.onBeforePaint = onBeforePaint;
+ spineController.onAfterPaint = onAfterPaint;
+ return spineController;
+ }
+ }
+
+ private final SpineControllerCallback onInitialized;
+ private @Nullable SpineControllerCallback onBeforeUpdateWorldTransforms;
+ private @Nullable SpineControllerCallback onAfterUpdateWorldTransforms;
+ private @Nullable SpineControllerBeforePaintCallback onBeforePaint;
+ private @Nullable SpineControllerAfterPaintCallback onAfterPaint;
+ private AndroidSkeletonDrawable drawable;
+ private boolean playing = true;
+ private double offsetX = 0;
+ private double offsetY = 0;
+ private double scaleX = 1;
+ private double scaleY = 1;
+
+ /**
+ * Instantiate a {@link SpineController}, which controls how the skeleton of a {@link SpineView} is animated and rendered.
+ * Upon initialization of a {@link SpineView}, the provided {@code onInitialized} callback method is called once.
+ * This method can be used to set up the initial animation(s) of the skeleton, among other things.
+ *
+ * @param onInitialized Upon initialization of a {@link SpineView}, the provided {@code onInitialized} callback
+ * method is called once. This method can be used to set up the initial animation(s) of the skeleton,
+ * among other things.
+ */
+ public SpineController(SpineControllerCallback onInitialized) {
+ this.onInitialized = onInitialized;
+ }
+
+ protected void init(AndroidSkeletonDrawable drawable) {
+ this.drawable = drawable;
+ if (onInitialized != null) {
+ onInitialized.execute(this);
+ }
+ }
+
+ /**
+ * The {@link AndroidTextureAtlas} from which images to render the skeleton are sourced.
+ */
+ public AndroidTextureAtlas getAtlas() {
+ if (drawable == null) throw new RuntimeException("Controller is not initialized yet.");
+ return drawable.getAtlas();
+ }
+
+ /**
+ * The setup-pose data used by the skeleton.
+ */
+ public SkeletonData getSkeletonDate() {
+ if (drawable == null) throw new RuntimeException("Controller is not initialized yet.");
+ return drawable.getSkeletonData();
+ }
+
+ /**
+ * The {@link Skeleton}.
+ */
+ public Skeleton getSkeleton() {
+ if (drawable == null) throw new RuntimeException("Controller is not initialized yet.");
+ return drawable.getSkeleton();
+ }
+
+ /**
+ * The mixing information used by the {@link AnimationState}.
+ */
+ public AnimationStateData getAnimationStateData() {
+ if (drawable == null) throw new RuntimeException("Controller is not initialized yet.");
+ return drawable.getAnimationStateData();
+ }
+
+ /**
+ * The {@link AnimationState} used to manage animations that are being applied to the
+ * skeleton.
+ */
+ public AnimationState getAnimationState() {
+ if (drawable == null) throw new RuntimeException("Controller is not initialized yet.");
+ return drawable.getAnimationState();
+ }
+
+ /**
+ * The {@link AndroidSkeletonDrawable}.
+ */
+ public AndroidSkeletonDrawable getDrawable() {
+ if (drawable == null) throw new RuntimeException("Controller is not initialized yet.");
+ return drawable;
+ }
+
+ /**
+ * Checks if the {@link SpineView} is initialized.
+ */
+ public boolean isInitialized() {
+ return drawable != null;
+ }
+
+ /**
+ * Checks if the animation is currently playing.
+ */
+ public boolean isPlaying() {
+ return playing;
+ }
+
+ /**
+ * Pauses updating and rendering the skeleton.
+ */
+ public void pause() {
+ if (playing) {
+ playing = false;
+ }
+ }
+
+ /**
+ * Resumes updating and rendering the skeleton.
+ */
+ public void resume() {
+ if (!playing) {
+ playing = true;
+ }
+ }
+
+ /**
+ * Transforms the coordinates given in the {@link SpineView} coordinate system in {@code position} to
+ * the skeleton coordinate system. See the {@code IKFollowing.kt} example for how to use this
+ * to move a bone based on user touch input.
+ */
+ public Point toSkeletonCoordinates(Point position) {
+ int x = position.x;
+ int y = position.y;
+ return new Point((int) (x / scaleX - offsetX), (int) (y / scaleY - offsetY));
+ }
+
+ /**
+ * Sets the {@code onBeforeUpdateWorldTransforms} callback. It is called before the skeleton's current pose is calculated
+ * using {@link Skeleton#updateWorldTransform(Skeleton.Physics)}. It can be used to modify the skeleton before the pose calculation.
+ */
+ public void setOnBeforeUpdateWorldTransforms(@Nullable SpineControllerCallback onBeforeUpdateWorldTransforms) {
+ this.onBeforeUpdateWorldTransforms = onBeforeUpdateWorldTransforms;
+ }
+
+ /**
+ * Sets the {@code onAfterUpdateWorldTransforms} callback. This method is called after the skeleton's current pose is calculated using
+ * {@link Skeleton#updateWorldTransform(Skeleton.Physics)}. It can be used to modify the current pose before rendering the skeleton.
+ */
+ public void setOnAfterUpdateWorldTransforms(@Nullable SpineControllerCallback onAfterUpdateWorldTransforms) {
+ this.onAfterUpdateWorldTransforms = onAfterUpdateWorldTransforms;
+ }
+
+ /**
+ * Sets the {@code onBeforePaint} callback. It is called before the skeleton's current pose is rendered by the
+ * {@link SpineView}. It allows rendering backgrounds or other objects that should go behind the skeleton on the
+ * {@link Canvas}.
+ */
+ public void setOnBeforePaint(@Nullable SpineControllerBeforePaintCallback onBeforePaint) {
+ this.onBeforePaint = onBeforePaint;
+ }
+
+ /**
+ * Sets the {@code onAfterPaint} callback. It is called after the skeleton's current pose is rendered by the
+ * {@link SpineView}. It allows rendering additional objects on top of the skeleton.
+ */
+ public void setOnAfterPaint(@Nullable SpineControllerAfterPaintCallback onAfterPaint) {
+ this.onAfterPaint = onAfterPaint;
+ }
+
+ protected void setCoordinateTransform(double offsetX, double offsetY, double scaleX, double scaleY) {
+ this.offsetX = offsetX;
+ this.offsetY = offsetY;
+ this.scaleX = scaleX;
+ this.scaleY = scaleY;
+ }
+
+ protected void callOnBeforeUpdateWorldTransforms() {
+ if (onBeforeUpdateWorldTransforms != null) {
+ onBeforeUpdateWorldTransforms.execute(this);
+ }
+ }
+
+ protected void callOnAfterUpdateWorldTransforms() {
+ if (onAfterUpdateWorldTransforms != null) {
+ onAfterUpdateWorldTransforms.execute(this);
+ }
+ }
+
+ protected void callOnBeforePaint(Canvas canvas) {
+ if (onBeforePaint != null) {
+ onBeforePaint.execute(this, canvas);
+ }
+ }
+
+ protected void callOnAfterPaint(Canvas canvas, Array renderCommands) {
+ if (onAfterPaint != null) {
+ onAfterPaint.execute(this, canvas, renderCommands);
+ }
+ }
+}
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..808238b6c
--- /dev/null
+++ b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/SpineView.java
@@ -0,0 +1,469 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android;
+
+import com.badlogic.gdx.utils.Array;
+import com.esotericsoftware.spine.android.bounds.Alignment;
+import com.esotericsoftware.spine.android.bounds.Bounds;
+import com.esotericsoftware.spine.android.bounds.BoundsProvider;
+import com.esotericsoftware.spine.android.bounds.ContentMode;
+import com.esotericsoftware.spine.android.bounds.SetupPoseBounds;
+import com.esotericsoftware.spine.android.callbacks.AndroidSkeletonDrawableLoader;
+import com.esotericsoftware.spine.Skeleton;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.AttributeSet;
+import android.view.Choreographer;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+
+import java.io.File;
+import java.net.URL;
+
+/**
+ * A {@link View} to display a Spine skeleton. The skeleton can be loaded from an asset bundle ({@link SpineView#loadFromAssets(String, String, Context, SpineController)}),
+ * local files ({@link SpineView#loadFromFile(File, File, Context, SpineController)}), URLs ({@link SpineView#loadFromHttp(URL, URL, File, Context, SpineController)}), or a pre-loaded {@link AndroidSkeletonDrawable} using ({@link SpineView#loadFromDrawable(AndroidSkeletonDrawable, Context, SpineController)}).
+ *
+ * The skeleton displayed by a {@link SpineView} can be controlled via a {@link SpineController}.
+ *
+ * The size of the widget can be derived from the bounds provided by a {@link BoundsProvider}. If the widget is not sized by the bounds
+ * computed by the {@link BoundsProvider}, the widget will use the computed bounds to fit the skeleton inside the widget's dimensions.
+ */
+public class SpineView extends View implements Choreographer.FrameCallback {
+
+ /**
+ * Used to build {@link SpineView} instances.
+ * */
+ public static class Builder {
+ private final Context context;
+ private final SpineController controller;
+ private String atlasFileName;
+ private String skeletonFileName;
+ private File atlasFile;
+ private File skeletonFile;
+ private URL atlasUrl;
+ private URL skeletonUrl;
+ private File targetDirectory;
+ private AndroidSkeletonDrawable drawable;
+ private BoundsProvider boundsProvider = new SetupPoseBounds();
+ private Alignment alignment = Alignment.CENTER;
+ private ContentMode contentMode = ContentMode.FIT;
+
+ /**
+ * Instantiate a {@link Builder} used to build a {@link SpineView}, which is a {@link View} to display a Spine skeleton.
+ *
+ * @param controller The skeleton displayed by a {@link SpineView} can be controlled via a {@link SpineController}.
+ */
+ public Builder(Context context, SpineController controller) {
+ this.context = context;
+ this.controller = controller;
+ }
+
+ /**
+ * Loads assets from your app assets for the {@link SpineView} if set. The {@code atlasFileName} specifies the
+ * `.atlas` file to be loaded for the images used to render the skeleton. The {@code skeletonFileName} specifies either a Skeleton `.json` or
+ * `.skel` file containing the skeleton data.
+ */
+ public Builder setLoadFromAssets(String atlasFileName, String skeletonFileName) {
+ this.atlasFileName = atlasFileName;
+ this.skeletonFileName = skeletonFileName;
+ return this;
+ }
+
+ /**
+ * Loads assets from files for the {@link SpineView} if set. The {@code atlasFile} specifies the `.atlas` file to be loaded for the images used to render
+ * the skeleton. The {@code skeletonFile} specifies either a Skeleton `.json` or `.skel` file containing the skeleton data.
+ */
+ public Builder setLoadFromFile(File atlasFile, File skeletonFile) {
+ this.atlasFile = atlasFile;
+ this.skeletonFile = skeletonFile;
+ return this;
+ }
+
+ /**
+ * Loads assets from http for the {@link SpineView} if set. The {@code atlasUrl} specifies the `.atlas` url to be loaded for the images used to render
+ * the skeleton. The {@code skeletonUrl} specifies either a Skeleton `.json` or `.skel` url containing the skeleton data.
+ */
+ public Builder setLoadFromHttp(URL atlasUrl, URL skeletonUrl, File targetDirectory) {
+ this.atlasUrl = atlasUrl;
+ this.skeletonUrl = skeletonUrl;
+ this.targetDirectory = targetDirectory;
+ return this;
+ }
+
+ /**
+ * Uses the {@link AndroidSkeletonDrawable} for the {@link SpineView} if set.
+ */
+ public Builder setLoadFromDrawable(AndroidSkeletonDrawable drawable) {
+ this.drawable = drawable;
+ return this;
+ }
+
+ /**
+ * Get the {@link BoundsProvider} used to compute the bounds of the {@link Skeleton} inside the view.
+ * The default is {@link SetupPoseBounds}.
+ */
+ public Builder setBoundsProvider(BoundsProvider boundsProvider) {
+ this.boundsProvider = boundsProvider;
+ return this;
+ }
+
+ /**
+ * Get the {@link ContentMode} used to fit the {@link Skeleton} inside the view.
+ * The default is {@link ContentMode#FIT}.
+ */
+ public Builder setContentMode(ContentMode contentMode) {
+ this.contentMode = contentMode;
+ return this;
+ }
+
+ /**
+ * Set the {@link Alignment} used to align the {@link Skeleton} inside the view.
+ * The default is {@link Alignment#CENTER}
+ */
+ public Builder setAlignment(Alignment alignment) {
+ this.alignment = alignment;
+ return this;
+ }
+
+ /**
+ * Builds a new {@link SpineView}.
+ *
+ * After initialization is complete, the provided {@code SpineController} is invoked as per the {@link SpineController} semantics, to allow
+ * modifying how the skeleton inside the widget is animated and rendered.
+ */
+ public SpineView build() {
+ SpineView spineView = new SpineView(context, controller);
+ spineView.boundsProvider = boundsProvider;
+ spineView.alignment = alignment;
+ spineView.contentMode = contentMode;
+ if (atlasFileName != null && skeletonFileName != null) {
+ spineView.loadFromAsset(atlasFileName, skeletonFileName);
+ } else if (atlasFile != null && skeletonFile != null) {
+ spineView.loadFromFile(atlasFile, skeletonFile);
+ } else if (atlasUrl != null && skeletonUrl != null && targetDirectory != null) {
+ spineView.loadFromHttp(atlasUrl, skeletonUrl, targetDirectory);
+ } else if (drawable != null) {
+ spineView.loadFromDrawable(drawable);
+ }
+ return spineView;
+ }
+ }
+
+ private long lastTime = 0;
+ private float delta = 0;
+ private float offsetX = 0;
+ private float offsetY = 0;
+ private float scaleX = 1;
+ private float scaleY = 1;
+ private float x = 0;
+ private float y = 0;
+ private final SkeletonRenderer renderer = new SkeletonRenderer();
+ private Boolean rendering = true;
+ private Bounds computedBounds = new Bounds();
+
+ private SpineController controller;
+ private BoundsProvider boundsProvider = new SetupPoseBounds();
+ private Alignment alignment = Alignment.CENTER;
+ private ContentMode contentMode = ContentMode.FIT;
+
+ /**
+ * Constructs a new {@link SpineView}.
+ *
+ * After initialization is complete, the provided {@code SpineController} is invoked as per the {@link SpineController} semantics, to allow
+ * modifying how the skeleton inside the widget is animated and rendered.
+ */
+ public SpineView (Context context, SpineController controller) {
+ super(context);
+ this.controller = controller;
+ }
+
+ /**
+ * Constructs a new {@link SpineView} without providing a {@link SpineController}, which you need to provide using
+ * {@link SpineView#setController(SpineController)}.
+ */
+ public SpineView (Context context, AttributeSet attrs) {
+ super(context, attrs);
+ // Set properties by view id
+ }
+
+ /**
+ * Constructs a new {@link SpineView} without providing a {@link SpineController}, which you need to provide using
+ * {@link SpineView#setController(SpineController)}.
+ */
+ public SpineView (Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ // Set properties by view id
+ }
+
+ /**
+ * Constructs a new {@link SpineView} from files in your app assets. The {@code atlasFileName} specifies the
+ * `.atlas` file to be loaded for the images used to render the skeleton. The {@code skeletonFileName} specifies either a Skeleton `.json` or
+ * `.skel` file containing the skeleton data.
+ *
+ * After initialization is complete, the provided {@code controller} is invoked as per the {@link SpineController} semantics, to allow
+ * modifying how the skeleton inside the widget is animated and rendered.
+ */
+ public static SpineView loadFromAssets(String atlasFileName, String skeletonFileName, Context context, SpineController controller) {
+ SpineView spineView = new SpineView(context, controller);
+ spineView.loadFromAsset(atlasFileName, skeletonFileName);
+ return spineView;
+ }
+
+ /**
+ * Constructs a new {@link SpineView} from files. The {@code atlasFile} specifies the `.atlas` file to be loaded for the images used to render
+ * the skeleton. The {@code skeletonFile} specifies either a Skeleton `.json` or `.skel` file containing the skeleton data.
+ *
+ * After initialization is complete, the provided {@code SpineController} is invoked as per the {@link SpineController} semantics, to allow
+ * modifying how the skeleton inside the widget is animated and rendered.
+ */
+ public static SpineView loadFromFile(File atlasFile, File skeletonFile, Context context, SpineController controller) {
+ SpineView spineView = new SpineView(context, controller);
+ spineView.loadFromFile(atlasFile, skeletonFile);
+ return spineView;
+ }
+
+ /**
+ * Constructs a new {@link SpineView} from HTTP URLs. The {@code atlasUrl} specifies the `.atlas` url to be loaded for the images used to render
+ * the skeleton. The {@code skeletonUrl} specifies either a Skeleton `.json` or `.skel` url containing the skeleton data.
+ *
+ * After initialization is complete, the provided {@code SpineController} is invoked as per the {@link SpineController} semantics, to allow
+ * modifying how the skeleton inside the widget is animated and rendered.
+ */
+ public static SpineView loadFromHttp(URL atlasUrl, URL skeletonUrl, File targetDirectory, Context context, SpineController controller) {
+ SpineView spineView = new SpineView(context, controller);
+ spineView.loadFromHttp(atlasUrl, skeletonUrl, targetDirectory);
+ return spineView;
+ }
+
+ /**
+ * Constructs a new {@link SpineView} from a {@link AndroidSkeletonDrawable}.
+ *
+ * After initialization is complete, the provided {@code SpineController} is invoked as per the {@link SpineController} semantics, to allow
+ * modifying how the skeleton inside the widget is animated and rendered.
+ */
+ public static SpineView loadFromDrawable(AndroidSkeletonDrawable drawable, Context context, SpineController controller) {
+ SpineView spineView = new SpineView(context, controller);
+ spineView.loadFromDrawable(drawable);
+ return spineView;
+ }
+
+ /**
+ * The same as {@link SpineView#loadFromAssets(String, String, Context, SpineController)}, but can be used after
+ * instantiating the view via {@link SpineView#SpineView(Context, SpineController)}.
+ */
+ public void loadFromAsset(String atlasFileName, String skeletonFileName) {
+ loadFrom(() -> AndroidSkeletonDrawable.fromAsset(atlasFileName, skeletonFileName, getContext()));
+ }
+
+ /**
+ * The same as {@link SpineView#loadFromFile(File, File, Context, SpineController)}, but can be used after
+ * instantiating the view via {@link SpineView#SpineView(Context, SpineController)}.
+ */
+ public void loadFromFile(File atlasFile, File skeletonFile) {
+ loadFrom(() -> AndroidSkeletonDrawable.fromFile(atlasFile, skeletonFile));
+ }
+
+ /**
+ * The same as {@link SpineView#loadFromHttp(URL, URL, File, Context, SpineController)}, but can be used after
+ * instantiating the view via {@link SpineView#SpineView(Context, SpineController)}.
+ */
+ public void loadFromHttp(URL atlasUrl, URL skeletonUrl, File targetDirectory) {
+ loadFrom(() -> AndroidSkeletonDrawable.fromHttp(atlasUrl, skeletonUrl, targetDirectory));
+ }
+
+ /**
+ * The same as {@link SpineView#loadFromDrawable(AndroidSkeletonDrawable, Context, SpineController)}, but can be used after
+ * instantiating the view via {@link SpineView#SpineView(Context, SpineController)}.
+ */
+ public void loadFromDrawable(AndroidSkeletonDrawable drawable) {
+ loadFrom(() -> drawable);
+ }
+
+ /**
+ * Get the {@link SpineController}
+ */
+ public SpineController getController() {
+ return controller;
+ }
+
+ /**
+ * Set the {@link SpineController}. Only do this if you use {@link SpineView#SpineView(Context, AttributeSet)},
+ * {@link SpineView#SpineView(Context, AttributeSet, int)}, or create the {@link SpineView} in an XML layout.
+ */
+ public void setController(SpineController controller) {
+ this.controller = controller;
+ }
+
+ /**
+ * Get the {@link Alignment} used to align the {@link Skeleton} inside the view.
+ * The default is {@link Alignment#CENTER}
+ */
+ public Alignment getAlignment() {
+ return alignment;
+ }
+
+ /**
+ * Set the {@link Alignment}.
+ */
+ public void setAlignment(Alignment alignment) {
+ this.alignment = alignment;
+ updateCanvasTransform();
+ }
+
+ /**
+ * Get the {@link ContentMode} used to fit the {@link Skeleton} inside the view.
+ * The default is {@link ContentMode#FIT}.
+ */
+ public ContentMode getContentMode() {
+ return contentMode;
+ }
+
+ /**
+ * Set the {@link ContentMode}.
+ */
+ public void setContentMode(ContentMode contentMode) {
+ this.contentMode = contentMode;
+ updateCanvasTransform();
+ }
+
+ /**
+ * Get the {@link BoundsProvider} used to compute the bounds of the {@link Skeleton} inside the view.
+ * The default is {@link SetupPoseBounds}.
+ */
+ public BoundsProvider getBoundsProvider() {
+ return boundsProvider;
+ }
+
+ /**
+ * Set the {@link BoundsProvider}.
+ */
+ public void setBoundsProvider(BoundsProvider boundsProvider) {
+ this.boundsProvider = boundsProvider;
+ updateCanvasTransform();
+ }
+
+ /**
+ * Check if rendering is enabled.
+ */
+ public Boolean isRendering() {
+ return rendering;
+ }
+
+ /**
+ * Set to disable or enable rendering. Disable it when the spine view is out of bounds and you want to preserve CPU/GPU resources.
+ */
+ public void setRendering(Boolean rendering) {
+ this.rendering = rendering;
+ }
+
+ private void loadFrom(AndroidSkeletonDrawableLoader loader) {
+ Handler mainHandler = new Handler(Looper.getMainLooper());
+ Thread backgroundThread = new Thread(() -> {
+ final AndroidSkeletonDrawable skeletonDrawable = loader.load();
+ mainHandler.post(() -> {
+ computedBounds = boundsProvider.computeBounds(skeletonDrawable);
+ updateCanvasTransform();
+
+ controller.init(skeletonDrawable);
+ Choreographer.getInstance().postFrameCallback(SpineView.this);
+ });
+ });
+ backgroundThread.start();
+ }
+
+ @Override
+ public void onDraw (@NonNull Canvas canvas) {
+ super.onDraw(canvas);
+ if (controller == null || !controller.isInitialized() || !rendering) {
+ return;
+ }
+
+ if (controller.isPlaying()) {
+ controller.callOnBeforeUpdateWorldTransforms();
+ controller.getDrawable().update(delta);
+ controller.callOnAfterUpdateWorldTransforms();
+ }
+
+ canvas.save();
+
+ canvas.translate(offsetX, offsetY);
+ canvas.scale(scaleX, scaleY * -1);
+ canvas.translate(x, y);
+
+ controller.callOnBeforePaint(canvas);
+ Array 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);
+ }
+}
diff --git a/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/Alignment.java b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/Alignment.java
new file mode 100644
index 000000000..ba2a497ce
--- /dev/null
+++ b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/Alignment.java
@@ -0,0 +1,61 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android.bounds;
+
+/**
+ * How a view should be aligned within another view.
+ */
+public enum Alignment {
+ TOP_LEFT(-1.0f, -1.0f),
+ TOP_CENTER(0.0f, -1.0f),
+ TOP_RIGHT(1.0f, -1.0f),
+ CENTER_LEFT(-1.0f, 0.0f),
+ CENTER(0.0f, 0.0f),
+ CENTER_RIGHT(1.0f, 0.0f),
+ BOTTOM_LEFT(-1.0f, 1.0f),
+ BOTTOM_CENTER(0.0f, 1.0f),
+ BOTTOM_RIGHT(1.0f, 1.0f);
+
+ private final float x;
+ private final float y;
+
+ Alignment(float x, float y) {
+ this.x = x;
+ this.y = y;
+ }
+
+ public float getX() {
+ return x;
+ }
+
+ public float getY() {
+ return y;
+ }
+}
diff --git a/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/Bounds.java b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/Bounds.java
new file mode 100644
index 000000000..b3b8502d8
--- /dev/null
+++ b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/Bounds.java
@@ -0,0 +1,104 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android.bounds;
+
+import com.badlogic.gdx.math.Vector2;
+import com.badlogic.gdx.utils.FloatArray;
+import com.esotericsoftware.spine.Skeleton;
+
+/**
+ * Bounds denoted by the top left corner coordinates {@code x} and {@code y}
+ * and the {@code width} and {@code height}.
+ */
+public class Bounds {
+ private double x;
+ private double y;
+ private double width;
+ private double height;
+
+ public Bounds() {
+ this.x = 0;
+ this.y = 0;
+ this.width = 0;
+ this.height = 0;
+ }
+
+ public Bounds(double x, double y, double width, double height) {
+ this.x = x;
+ this.y = y;
+ this.width = width;
+ this.height = height;
+ }
+
+ public Bounds(Skeleton skeleton) {
+ Vector2 offset = new Vector2(0, 0);
+ Vector2 size = new Vector2(0, 0);
+ FloatArray floatArray = new FloatArray();
+
+ skeleton.getBounds(offset, size, floatArray);
+
+ x = offset.x;
+ y = offset.y;
+ width = size.x;
+ height = size.y;
+ }
+
+ public double getX() {
+ return x;
+ }
+
+ public void setX(double x) {
+ this.x = x;
+ }
+
+ public double getY() {
+ return y;
+ }
+
+ public void setY(double y) {
+ this.y = y;
+ }
+
+ public double getWidth() {
+ return width;
+ }
+
+ public void setWidth(double width) {
+ this.width = width;
+ }
+
+ public double getHeight() {
+ return height;
+ }
+
+ public void setHeight(double height) {
+ this.height = height;
+ }
+}
diff --git a/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/BoundsProvider.java b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/BoundsProvider.java
new file mode 100644
index 000000000..2a59c8666
--- /dev/null
+++ b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/BoundsProvider.java
@@ -0,0 +1,40 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android.bounds;
+
+import com.esotericsoftware.spine.android.AndroidSkeletonDrawable;
+
+/**
+ * A {@link BoundsProvider} that calculates the bounding box of the skeleton based on the visible
+ * attachments in the setup pose.
+ */
+public interface BoundsProvider {
+ Bounds computeBounds(AndroidSkeletonDrawable drawable);
+}
diff --git a/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/ContentMode.java b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/ContentMode.java
new file mode 100644
index 000000000..67d239f51
--- /dev/null
+++ b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/ContentMode.java
@@ -0,0 +1,44 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android.bounds;
+
+/**
+ * How a view should be inscribed into another view.
+ */
+public enum ContentMode {
+ /**
+ * As large as possible while still containing the source view entirely within the target view.
+ */
+ FIT,
+ /**
+ * Fill the target view by distorting the source's aspect ratio.
+ */
+ FILL
+}
diff --git a/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/RawBounds.java b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/RawBounds.java
new file mode 100644
index 000000000..272656c62
--- /dev/null
+++ b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/RawBounds.java
@@ -0,0 +1,54 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android.bounds;
+
+import com.esotericsoftware.spine.android.AndroidSkeletonDrawable;
+
+/**
+ * A {@link BoundsProvider} that returns fixed bounds.
+ */
+public class RawBounds implements BoundsProvider {
+ final Double x;
+ final Double y;
+ final Double width;
+ final Double height;
+
+ public RawBounds(Double x, Double y, Double width, Double height) {
+ this.x = x;
+ this.y = y;
+ this.width = width;
+ this.height = height;
+ }
+
+ @Override
+ public Bounds computeBounds(AndroidSkeletonDrawable drawable) {
+ return new Bounds(x, y, width, height);
+ }
+}
diff --git a/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/SetupPoseBounds.java b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/SetupPoseBounds.java
new file mode 100644
index 000000000..de1523737
--- /dev/null
+++ b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/SetupPoseBounds.java
@@ -0,0 +1,44 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android.bounds;
+
+import com.esotericsoftware.spine.android.AndroidSkeletonDrawable;
+
+/**
+ * A {@link BoundsProvider} that calculates the bounding box of the skeleton based on the visible
+ * attachments in the setup pose.
+ */
+public class SetupPoseBounds implements BoundsProvider {
+
+ @Override
+ public Bounds computeBounds(AndroidSkeletonDrawable drawable) {
+ return new Bounds(drawable.getSkeleton());
+ }
+}
diff --git a/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/SkinAndAnimationBounds.java b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/SkinAndAnimationBounds.java
new file mode 100644
index 000000000..5a3355fcd
--- /dev/null
+++ b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/bounds/SkinAndAnimationBounds.java
@@ -0,0 +1,122 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android.bounds;
+
+import com.esotericsoftware.spine.Animation;
+import com.esotericsoftware.spine.SkeletonData;
+import com.esotericsoftware.spine.Skin;
+import com.esotericsoftware.spine.android.AndroidSkeletonDrawable;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A {@link BoundsProvider} that calculates the bounding box needed for a combination of skins
+ * and an animation.
+ */
+public class SkinAndAnimationBounds implements BoundsProvider {
+ private final List 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 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 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);
+ }
+}
diff --git a/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/callbacks/AndroidSkeletonDrawableLoader.java b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/callbacks/AndroidSkeletonDrawableLoader.java
new file mode 100644
index 000000000..87626810d
--- /dev/null
+++ b/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();
+}
diff --git a/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/callbacks/SpineControllerAfterPaintCallback.java b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/callbacks/SpineControllerAfterPaintCallback.java
new file mode 100644
index 000000000..1864780b4
--- /dev/null
+++ b/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 commands);
+}
diff --git a/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/callbacks/SpineControllerBeforePaintCallback.java b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/callbacks/SpineControllerBeforePaintCallback.java
new file mode 100644
index 000000000..ad622c07d
--- /dev/null
+++ b/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);
+}
diff --git a/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/callbacks/SpineControllerCallback.java b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/callbacks/SpineControllerCallback.java
new file mode 100644
index 000000000..75d3b282f
--- /dev/null
+++ b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/callbacks/SpineControllerCallback.java
@@ -0,0 +1,37 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android.callbacks;
+
+import com.esotericsoftware.spine.android.SpineController;
+
+@FunctionalInterface
+public interface SpineControllerCallback {
+ void execute (SpineController controller);
+}
diff --git a/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/utils/HttpUtils.java b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/utils/HttpUtils.java
new file mode 100644
index 000000000..0f442b43b
--- /dev/null
+++ b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/utils/HttpUtils.java
@@ -0,0 +1,113 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android.utils;
+
+import android.os.Build;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.file.Files;
+
+/**
+ * Helper to load http resources.
+ */
+public class HttpUtils {
+ /**
+ * Download a file from an url into a target directory. It keeps the name from the {@code url}.
+ * This should NOT be executed on the main run loop.
+ */
+ public static File downloadFrom(URL url, File targetDirectory) throws RuntimeException {
+ HttpURLConnection urlConnection = null;
+ InputStream inputStream = null;
+ OutputStream outputStream = null;
+
+ try {
+ urlConnection = (HttpURLConnection) url.openConnection();
+ urlConnection.connect();
+
+ if (urlConnection.getResponseCode() != HttpURLConnection.HTTP_OK) {
+ throw new RuntimeException("Failed to connect: HTTP response code " + urlConnection.getResponseCode());
+ }
+
+ inputStream = new BufferedInputStream(urlConnection.getInputStream());
+
+ String atlasUrlPath = url.getPath();
+ String fileName = atlasUrlPath.substring(atlasUrlPath.lastIndexOf('/') + 1);
+ File file = new File(targetDirectory, fileName);
+
+ // Create an OutputStream to write to the file
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ outputStream = Files.newOutputStream(file.toPath());
+ } else {
+ //noinspection IOStreamConstructor
+ outputStream = new FileOutputStream(file);
+ }
+
+ byte[] buffer = new byte[1024];
+ int bytesRead;
+
+ // Write the input stream to the output stream
+ while ((bytesRead = inputStream.read(buffer)) != -1) {
+ outputStream.write(buffer, 0, bytesRead);
+ }
+ return file;
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ } finally {
+ if (outputStream != null) {
+ try {
+ outputStream.flush();
+ outputStream.close();
+ } catch (IOException e) {
+ // Nothing we can do
+ }
+ }
+
+ if (inputStream != null) {
+ try {
+ inputStream.close();
+ } catch (IOException e) {
+ // Nothing we can do
+ }
+ }
+
+ if (urlConnection != null) {
+ urlConnection.disconnect();
+ }
+ }
+ }
+}
+
diff --git a/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/utils/SkeletonDataUtils.java b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/utils/SkeletonDataUtils.java
new file mode 100644
index 000000000..6395ce22f
--- /dev/null
+++ b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/utils/SkeletonDataUtils.java
@@ -0,0 +1,108 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
+ * otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.android.utils;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+
+import com.badlogic.gdx.files.FileHandle;
+import com.esotericsoftware.spine.SkeletonBinary;
+import com.esotericsoftware.spine.SkeletonData;
+import com.esotericsoftware.spine.SkeletonJson;
+import com.esotericsoftware.spine.SkeletonLoader;
+import com.esotericsoftware.spine.android.AndroidAtlasAttachmentLoader;
+import com.esotericsoftware.spine.android.AndroidTextureAtlas;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+
+/**
+ * Helper to load {@link SkeletonData} from assets.
+ */
+public class SkeletonDataUtils {
+
+ /**
+ * Loads a {@link SkeletonData} from the file {@code skeletonFile} in assets using {@link Context}.
+ * Uses the provided {@link AndroidTextureAtlas} to resolve attachment images.
+ *
+ * Throws a {@link RuntimeException} in case the skeleton data could not be loaded.
+ */
+ public static SkeletonData fromAsset(AndroidTextureAtlas atlas, String skeletonFileName, Context context) {
+ AndroidAtlasAttachmentLoader attachmentLoader = new AndroidAtlasAttachmentLoader(atlas);
+
+ SkeletonLoader skeletonLoader;
+ if (skeletonFileName.endsWith(".json")) {
+ skeletonLoader = new SkeletonJson(attachmentLoader);
+ } else {
+ skeletonLoader = new SkeletonBinary(attachmentLoader);
+ }
+
+ SkeletonData skeletonData;
+
+ AssetManager assetManager = context.getAssets();
+ try (InputStream in = new BufferedInputStream(assetManager.open(skeletonFileName))) {
+ skeletonData = skeletonLoader.readSkeletonData(in);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return skeletonData;
+ }
+
+ /**
+ * Loads a {@link SkeletonData} from the file {@code skeletonFile}. Uses the provided {@link AndroidTextureAtlas} to resolve attachment images.
+ *
+ * Throws a {@link RuntimeException} in case the skeleton data could not be loaded.
+ */
+ public static SkeletonData fromFile(AndroidTextureAtlas atlas, File skeletonFile) {
+ AndroidAtlasAttachmentLoader attachmentLoader = new AndroidAtlasAttachmentLoader(atlas);
+
+ SkeletonLoader skeletonLoader;
+ if (skeletonFile.getPath().endsWith(".json")) {
+ skeletonLoader = new SkeletonJson(attachmentLoader);
+ } else {
+ skeletonLoader = new SkeletonBinary(attachmentLoader);
+ }
+
+ return skeletonLoader.readSkeletonData(new FileHandle(skeletonFile));
+ }
+
+ /**
+ * Loads a {@link SkeletonData} from the URL {@code skeletonURL}. Uses the provided {@link AndroidTextureAtlas} to resolve attachment images.
+ *
+ * Throws a {@link RuntimeException} in case the skeleton data could not be loaded.
+ */
+ public static SkeletonData fromHttp(AndroidTextureAtlas atlas, URL skeletonUrl, File targetDirectory) {
+ File skeletonFile = HttpUtils.downloadFrom(skeletonUrl, targetDirectory);
+ return fromFile(atlas, skeletonFile);
+ }
+}
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
diff --git a/spine-libgdx/build.gradle b/spine-libgdx/build.gradle
index f9480edb8..7f2832c27 100644
--- a/spine-libgdx/build.gradle
+++ b/spine-libgdx/build.gradle
@@ -2,7 +2,7 @@ group = "com.esotericsoftware.spine"
version = "4.2.0"
ext {
- libgdxVersion = "1.12.1"
+ libgdxVersion = "1.12.2-SNAPSHOT"
javaVersion = 8
}
diff --git a/spine-libgdx/settings.gradle b/spine-libgdx/settings.gradle
index 632b7d04f..34b8ed31f 100644
--- a/spine-libgdx/settings.gradle
+++ b/spine-libgdx/settings.gradle
@@ -1,4 +1,4 @@
// includeBuild "../../libgdx"
include ":spine-libgdx"
include ":spine-libgdx-tests"
-include ":spine-skeletonviewer"
\ No newline at end of file
+include ":spine-skeletonviewer"
diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Slot.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Slot.java
index afc1d159e..3d2cce195 100644
--- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Slot.java
+++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Slot.java
@@ -44,7 +44,7 @@
public class Slot {
final SlotData data;
final Bone bone;
- final Color color = new Color();
+ Color color = new Color();
@Null final Color darkColor;
@Null Attachment attachment;
int sequenceIndex;
@@ -95,6 +95,10 @@ public Color getColor () {
return color;
}
+ public void setColor(Color color) {
+ this.color = color;
+ }
+
/** The dark color used to tint the slot's attachment for two color tinting, or null if two color tinting is not used. The dark
* color's alpha is not used. */
public @Null Color getDarkColor () {