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.graphics)
implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3) implementation(libs.androidx.material3)
implementation(project(":spine-android"))
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)
@ -67,4 +67,10 @@ dependencies {
androidTestImplementation(libs.androidx.ui.test.junit4) androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest) 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:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.SpineAndroidExamples" android:theme="@style/Theme.SpineAndroidExamples"
tools:targetApi="31"> tools:targetApi="34">
<activity <activity
android:name=".MainActivity" android:name="MainActivity"
android:exported="true" android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.SpineAndroidExamples"> android:theme="@style/Theme.SpineAndroidExamples">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <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 android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface 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.runtime.Composable
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import com.esotericsoftware.spine.android.SpineController
import com.esotericsoftware.spine.android.SpineView import com.esotericsoftware.spine.android.SpineView
import com.esotericsoftware.spine.ui.theme.SpineAndroidExamplesTheme
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -42,10 +39,20 @@ fun AppContent() {
@Composable @Composable
fun SpineViewComposable(modifier: Modifier = Modifier.fillMaxSize()) { fun SpineViewComposable(modifier: Modifier = Modifier.fillMaxSize()) {
val context = LocalContext.current
AndroidView( AndroidView(
factory = { ctx -> factory = { ctx ->
SpineView(ctx).apply { 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 modifier = modifier

View File

@ -16,6 +16,9 @@ dependencyResolutionManagement {
repositories { repositories {
google() google()
mavenCentral() 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")) substitute(module("com.esotericsoftware.spine:spine-libgdx")).using(project(":spine-libgdx"))
} }
} }
includeBuild("../../libgdx") { //includeBuild("../../libgdx") {
dependencySubstitution { // dependencySubstitution {
substitute(module("com.badlogicgames.gdx:gdx")).using(project(":gdx")) // substitute(module("com.badlogicgames.gdx:gdx")).using(project(":gdx"))
} // }
} //}
include(":app") include(":app")
include(":spine-android") include(":spine-android")

View File

@ -32,7 +32,7 @@ dependencies {
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
implementation(libs.material) 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") implementation("com.esotericsoftware.spine:spine-libgdx:4.2.0")
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.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; package com.esotericsoftware.spine.android;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URL;
import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion; 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.Array;
import com.badlogic.gdx.utils.Null; import com.badlogic.gdx.utils.Null;
import android.content.Context;
import android.content.res.AssetManager; import android.content.res.AssetManager;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import kotlin.NotImplementedError;
public class AndroidTextureAtlas { public class AndroidTextureAtlas {
private static interface BitmapLoader { private static interface BitmapLoader {
Bitmap load (String path); Bitmap load (String path);
@ -92,35 +97,41 @@ public class AndroidTextureAtlas {
return regions; return regions;
} }
static public AndroidTextureAtlas loadFromAssets (String atlasFile, AssetManager assetManager) { static public AndroidTextureAtlas fromAsset(String atlasFileName, Context context) {
TextureAtlasData data = new TextureAtlasData(); TextureAtlasData data = new TextureAtlasData();
AssetManager assetManager = context.getAssets();
try { try {
FileHandle inputFile = new FileHandle() { FileHandle inputFile = new FileHandle() {
@Override @Override
public InputStream read () { public InputStream read () {
try { try {
return assetManager.open(atlasFile); return assetManager.open(atlasFileName);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
}; };
data.load(inputFile, new FileHandle(atlasFile).parent(), false); data.load(inputFile, new FileHandle(atlasFileName).parent(), false);
} catch (Throwable t) { } catch (Throwable t) {
throw new RuntimeException(t); throw new RuntimeException(t);
} }
return new AndroidTextureAtlas(data, new BitmapLoader() { return new AndroidTextureAtlas(data, path -> {
@Override path = path.startsWith("/") ? path.substring(1) : path;
public Bitmap load (String path) { try (InputStream in = new BufferedInputStream(assetManager.open(path))) {
path = path.startsWith("/") ? path.substring(1) : path; return BitmapFactory.decodeStream(in);
try (InputStream in = new BufferedInputStream(assetManager.open(path))) { } catch (Throwable t) {
return BitmapFactory.decodeStream(in); throw new RuntimeException(t);
} 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; package com.esotericsoftware.spine.android;
import java.io.BufferedInputStream; import com.esotericsoftware.spine.android.utils.AndroidSkeletonDrawableLoader;
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 android.content.Context; import android.content.Context;
import android.content.res.AssetManager;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Color; import android.os.Handler;
import android.graphics.Paint; import android.os.Looper;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.Choreographer; import android.view.Choreographer;
import android.view.View; import android.view.View;
import androidx.annotation.NonNull;
import java.io.File;
import java.net.URL;
public class SpineView extends View implements Choreographer.FrameCallback { public class SpineView extends View implements Choreographer.FrameCallback {
private long lastTime = 0; private long lastTime = 0;
private float delta = 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(); SkeletonRenderer renderer = new SkeletonRenderer();
SpineController controller;
public SpineView (Context context) { public SpineView (Context context) {
super(context); super(context);
init();
} }
public SpineView (Context context, AttributeSet attrs) { public SpineView (Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
init();
} }
public SpineView (Context context, AttributeSet attrs, int defStyle) { public SpineView (Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle); super(context, attrs, defStyle);
init();
} }
private void loadSkeleton () { public void loadFromAsset(String atlasFileName, String skeletonFileName, SpineController controller) {
String skel = "spineboy-pro.skel"; this.controller = controller;
String atlasFile = "spineboy.atlas"; loadFrom(() -> AndroidSkeletonDrawable.fromAsset(atlasFileName, skeletonFileName, getContext()));
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);
}
} }
private void init () { public void loadFromFile(File atlasFile, File skeletonFile, SpineController controller) {
textPaint = new Paint(); this.controller = controller;
textPaint.setColor(Color.WHITE); // Set the color of the paint loadFrom(() -> AndroidSkeletonDrawable.fromFile(atlasFile, skeletonFile));
textPaint.setTextSize(48); }
Choreographer.getInstance().postFrameCallback(this);
loadSkeleton(); public void loadFromHttp(URL atlasUrl, URL skeletonUrl, SpineController controller) {
this.controller = controller;
loadFrom(() -> AndroidSkeletonDrawable.fromHttp(atlasUrl, skeletonUrl));
}
for (int i = 0; i < instances; i++) { private void loadFrom(AndroidSkeletonDrawableLoader loader) {
Skeleton skeleton = new Skeleton(data); Handler mainHandler = new Handler(Looper.getMainLooper());
skeleton.setScaleY(-1); Thread backgroundThread = new Thread(() -> {
skeleton.setToSetupPose(); final AndroidSkeletonDrawable skeletonDrawable = loader.load();
skeletons.add(skeleton); mainHandler.post(() -> {
controller.init(skeletonDrawable);
AnimationStateData stateData = new AnimationStateData(data); Choreographer.getInstance().postFrameCallback(SpineView.this);
stateData.setDefaultMix(0.2f); });
AnimationState state = new AnimationState(stateData); });
state.setAnimation(0, "hoverboard", true); backgroundThread.start();
states.add(state);
if (i == 0) {
coords[i] = new Vector2(500, 1000);
} else {
coords[i] = new Vector2(MathUtils.random(1000), MathUtils.random(3000));
}
}
} }
@Override @Override
public void onDraw (Canvas canvas) { public void onDraw (@NonNull Canvas canvas) {
super.onDraw(canvas); super.onDraw(canvas);
if (!controller.isInitialized()) {
for (int i = 0; i < instances; i++) { return;
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);
} }
canvas.drawText(delta * 1000 + " ms", 100, 100, textPaint); controller.getDrawable().update(delta);
canvas.drawText(instances + " instances", 100, 150, textPaint);
// TODO: Calculate scaling + position
renderer.render(canvas, controller.getSkeleton(), 500f, 1000f);
} }
// Choreographer.FrameCallback
@Override @Override
public void doFrame (long frameTimeNanos) { public void doFrame (long frameTimeNanos) {
if (lastTime != 0) delta = (frameTimeNanos - lastTime) / 1e9f; 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" version = "4.2.0"
ext { ext {
libgdxVersion = "1.12.1" libgdxVersion = "1.12.2-SNAPSHOT"
javaVersion = 8 javaVersion = 8
} }