Load Atlas/Skeleton from assets + Add basic classes (#2570)

- Add load methods to load atlas and skeleton from assets.
- Add basic classes for next steps (SpineController etc)
This commit is contained in:
Denis Andrašec 2024-07-03 14:32:48 +00:00 committed by GitHub
parent 603f181c79
commit ceb9ae13f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 9044 additions and 109 deletions

View File

@ -59,7 +59,7 @@ dependencies {
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(project(":spine-android"))
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
@ -67,4 +67,10 @@ dependencies {
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
implementation(project(":spine-android"))
// TODO Check if we really need to import `spine-libgdx` in addition to `spine-android`?
implementation("com.badlogicgames.gdx:gdx:1.12.2-SNAPSHOT")
implementation("com.esotericsoftware.spine:spine-libgdx:4.2.0")
}

View File

@ -11,11 +11,10 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.SpineAndroidExamples"
tools:targetApi="31">
tools:targetApi="34">
<activity
android:name=".MainActivity"
android:name="MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.SpineAndroidExamples">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

File diff suppressed because it is too large Load Diff

View File

@ -3,19 +3,16 @@ package com.esotericsoftware.spine
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import com.esotericsoftware.spine.ui.theme.SpineAndroidExamplesTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import com.esotericsoftware.spine.android.SpineController
import com.esotericsoftware.spine.android.SpineView
import com.esotericsoftware.spine.ui.theme.SpineAndroidExamplesTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@ -42,10 +39,20 @@ fun AppContent() {
@Composable
fun SpineViewComposable(modifier: Modifier = Modifier.fillMaxSize()) {
val context = LocalContext.current
AndroidView(
factory = { ctx ->
SpineView(ctx).apply {
loadFromAsset(
"spineboy.atlas",
"spineboy-pro.json",
SpineController {
it.skeleton.scaleY = -1f
it.skeleton.setToSetupPose()
it.animationStateData.defaultMix = 0.2f
it.animationState.setAnimation(0, "hoverboard", true)
}
)
}
},
modifier = modifier

View File

@ -16,6 +16,9 @@ dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven {
url = uri("https://oss.sonatype.org/content/repositories/snapshots")
}
}
}
@ -25,10 +28,10 @@ includeBuild("../spine-libgdx") {
substitute(module("com.esotericsoftware.spine:spine-libgdx")).using(project(":spine-libgdx"))
}
}
includeBuild("../../libgdx") {
dependencySubstitution {
substitute(module("com.badlogicgames.gdx:gdx")).using(project(":gdx"))
}
}
//includeBuild("../../libgdx") {
// dependencySubstitution {
// substitute(module("com.badlogicgames.gdx:gdx")).using(project(":gdx"))
// }
//}
include(":app")
include(":spine-android")

View File

@ -32,7 +32,7 @@ dependencies {
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation("com.badlogicgames.gdx:gdx:1.12.1")
implementation("com.badlogicgames.gdx:gdx:1.12.2-SNAPSHOT")
implementation("com.esotericsoftware.spine:spine-libgdx:4.2.0")
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)

View File

@ -0,0 +1,84 @@
package com.esotericsoftware.spine.android;
import android.content.Context;
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;
import kotlin.NotImplementedError;
public class AndroidSkeletonDrawable {
private final AndroidTextureAtlas atlas;
private final SkeletonData skeletonData;
private final Skeleton skeleton;
private final AnimationStateData animationStateData;
private final AnimationState animationState;
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);
}
public void update(float delta) {
animationState.update(delta);
animationState.apply(skeleton);
skeleton.update(delta);
skeleton.updateWorldTransform(Skeleton.Physics.update);
}
public AndroidTextureAtlas getAtlas() {
return atlas;
}
public Skeleton getSkeleton() {
return skeleton;
}
public SkeletonData getSkeletonData() {
return skeletonData;
}
public AnimationStateData getAnimationStateData() {
return animationStateData;
}
public AnimationState getAnimationState() {
return animationState;
}
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);
}
public static AndroidSkeletonDrawable fromFile (File atlasFile, File skeletonFile) {
AndroidTextureAtlas atlas = AndroidTextureAtlas.fromFile(atlasFile);
SkeletonData skeletonData = SkeletonDataUtils.fromFile(atlas, skeletonFile);
return new AndroidSkeletonDrawable(atlas, skeletonData);
}
public static AndroidSkeletonDrawable fromHttp (URL atlasUrl, URL skeletonUrl) {
AndroidTextureAtlas atlas = AndroidTextureAtlas.fromHttp(atlasUrl);
SkeletonData skeletonData = SkeletonDataUtils.fromHttp(atlas, skeletonUrl);
return new AndroidSkeletonDrawable(atlas, skeletonData);
}
}

View File

@ -30,8 +30,10 @@
package com.esotericsoftware.spine.android;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
@ -39,10 +41,13 @@ import com.badlogic.gdx.graphics.g2d.TextureAtlas.TextureAtlasData;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Null;
import android.content.Context;
import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import kotlin.NotImplementedError;
public class AndroidTextureAtlas {
private static interface BitmapLoader {
Bitmap load (String path);
@ -92,35 +97,41 @@ public class AndroidTextureAtlas {
return regions;
}
static public AndroidTextureAtlas loadFromAssets (String atlasFile, AssetManager assetManager) {
static public 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(atlasFile);
return assetManager.open(atlasFileName);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
};
data.load(inputFile, new FileHandle(atlasFile).parent(), false);
data.load(inputFile, new FileHandle(atlasFileName).parent(), false);
} catch (Throwable t) {
throw new RuntimeException(t);
}
return new AndroidTextureAtlas(data, new BitmapLoader() {
@Override
public Bitmap load (String path) {
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);
}
}
});
}
static public AndroidTextureAtlas fromFile(File atlasFile) {
throw new NotImplementedError("TODO");
}
static public AndroidTextureAtlas fromHttp(URL atlasUrl) {
throw new NotImplementedError("TODO");
}
}

View File

@ -0,0 +1,69 @@
package com.esotericsoftware.spine.android;
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.SpineControllerCallback;
public class SpineController {
private final SpineControllerCallback onInitialized;
private AndroidSkeletonDrawable drawable;
private boolean playing = true;
public SpineController(SpineControllerCallback onInitialized) {
this.onInitialized = onInitialized;
}
protected void init(AndroidSkeletonDrawable drawable) {
this.drawable = drawable;
onInitialized.execute(this);
}
public AndroidTextureAtlas getAtlas() {
if (drawable == null) throw new RuntimeException("Controller is not initialized yet.");
return drawable.getAtlas();
}
public SkeletonData getSkeletonDate() {
if (drawable == null) throw new RuntimeException("Controller is not initialized yet.");
return drawable.getSkeletonData();
}
public Skeleton getSkeleton() {
if (drawable == null) throw new RuntimeException("Controller is not initialized yet.");
return drawable.getSkeleton();
}
public AnimationStateData getAnimationStateData() {
if (drawable == null) throw new RuntimeException("Controller is not initialized yet.");
return drawable.getAnimationStateData();
}
public AnimationState getAnimationState() {
if (drawable == null) throw new RuntimeException("Controller is not initialized yet.");
return drawable.getAnimationState();
}
AndroidSkeletonDrawable getDrawable() {
if (drawable == null) throw new RuntimeException("Controller is not initialized yet.");
return drawable;
}
public boolean isInitialized() {
return drawable != null;
};
public boolean isPlaying() {
return playing;
}
public void pause() {
playing = false;
}
public void resume() {
playing = true;
}
}

View File

@ -29,116 +29,82 @@
package com.esotericsoftware.spine.android;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector2;
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.SkeletonBinary;
import com.esotericsoftware.spine.SkeletonData;
import com.esotericsoftware.spine.android.utils.AndroidSkeletonDrawableLoader;
import android.content.Context;
import android.content.res.AssetManager;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
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;
public class SpineView extends View implements Choreographer.FrameCallback {
private long lastTime = 0;
private float delta = 0;
private Paint textPaint;
int instances = 1;
Vector2[] coords = new Vector2[instances];
AndroidTextureAtlas atlas;
SkeletonData data;
Array<Skeleton> skeletons = new Array<>();
Array<AnimationState> states = new Array<>();
SkeletonRenderer renderer = new SkeletonRenderer();
SpineController controller;
public SpineView (Context context) {
super(context);
init();
}
public SpineView (Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public SpineView (Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void loadSkeleton () {
String skel = "spineboy-pro.skel";
String atlasFile = "spineboy.atlas";
AssetManager assetManager = this.getContext().getAssets();
atlas = AndroidTextureAtlas.loadFromAssets(atlasFile, assetManager);
AndroidAtlasAttachmentLoader attachmentLoader = new AndroidAtlasAttachmentLoader(atlas);
SkeletonBinary binary = new SkeletonBinary(attachmentLoader);
try (InputStream in = new BufferedInputStream(assetManager.open(skel))) {
data = binary.readSkeletonData(in);
} catch (IOException e) {
throw new RuntimeException(e);
}
public void loadFromAsset(String atlasFileName, String skeletonFileName, SpineController controller) {
this.controller = controller;
loadFrom(() -> AndroidSkeletonDrawable.fromAsset(atlasFileName, skeletonFileName, getContext()));
}
private void init () {
textPaint = new Paint();
textPaint.setColor(Color.WHITE); // Set the color of the paint
textPaint.setTextSize(48);
Choreographer.getInstance().postFrameCallback(this);
loadSkeleton();
for (int i = 0; i < instances; i++) {
Skeleton skeleton = new Skeleton(data);
skeleton.setScaleY(-1);
skeleton.setToSetupPose();
skeletons.add(skeleton);
AnimationStateData stateData = new AnimationStateData(data);
stateData.setDefaultMix(0.2f);
AnimationState state = new AnimationState(stateData);
state.setAnimation(0, "hoverboard", true);
states.add(state);
if (i == 0) {
coords[i] = new Vector2(500, 1000);
} else {
coords[i] = new Vector2(MathUtils.random(1000), MathUtils.random(3000));
public void loadFromFile(File atlasFile, File skeletonFile, SpineController controller) {
this.controller = controller;
loadFrom(() -> AndroidSkeletonDrawable.fromFile(atlasFile, skeletonFile));
}
public void loadFromHttp(URL atlasUrl, URL skeletonUrl, SpineController controller) {
this.controller = controller;
loadFrom(() -> AndroidSkeletonDrawable.fromHttp(atlasUrl, skeletonUrl));
}
private void loadFrom(AndroidSkeletonDrawableLoader loader) {
Handler mainHandler = new Handler(Looper.getMainLooper());
Thread backgroundThread = new Thread(() -> {
final AndroidSkeletonDrawable skeletonDrawable = loader.load();
mainHandler.post(() -> {
controller.init(skeletonDrawable);
Choreographer.getInstance().postFrameCallback(SpineView.this);
});
});
backgroundThread.start();
}
@Override
public void onDraw (Canvas canvas) {
public void onDraw (@NonNull Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < instances; i++) {
AnimationState state = states.get(i);
Skeleton skeleton = skeletons.get(i);
state.update(delta);
state.apply(skeleton);
skeleton.update(delta);
skeleton.updateWorldTransform(Skeleton.Physics.update);
renderer.render(canvas, skeleton, coords[i].x, coords[i].y);
if (!controller.isInitialized()) {
return;
}
canvas.drawText(delta * 1000 + " ms", 100, 100, textPaint);
canvas.drawText(instances + " instances", 100, 150, textPaint);
controller.getDrawable().update(delta);
// TODO: Calculate scaling + position
renderer.render(canvas, controller.getSkeleton(), 500f, 1000f);
}
// Choreographer.FrameCallback
@Override
public void doFrame (long frameTimeNanos) {
if (lastTime != 0) delta = (frameTimeNanos - lastTime) / 1e9f;

View File

@ -0,0 +1,8 @@
package com.esotericsoftware.spine.android.utils;
import com.esotericsoftware.spine.android.AndroidSkeletonDrawable;
@FunctionalInterface
public interface AndroidSkeletonDrawableLoader {
AndroidSkeletonDrawable load();
}

View File

@ -0,0 +1,51 @@
package com.esotericsoftware.spine.android.utils;
import android.content.Context;
import android.content.res.AssetManager;
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;
import kotlin.NotImplementedError;
public class SkeletonDataUtils {
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;
}
public static SkeletonData fromFile(AndroidTextureAtlas atlas, File skeletonFile) {
throw new NotImplementedError("TODO");
}
public static SkeletonData fromHttp(AndroidTextureAtlas atlas, URL skeletonUrl) {
throw new NotImplementedError("TODO");
}
}

View File

@ -0,0 +1,8 @@
package com.esotericsoftware.spine.android.utils;
import com.esotericsoftware.spine.android.SpineController;
@FunctionalInterface
public interface SpineControllerCallback {
void execute (SpineController controller);
}

View File

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