Add more documentation

This commit is contained in:
Denis Andrasec 2024-07-19 15:56:40 +02:00
parent ae5ae1e1a1
commit 42be887994
16 changed files with 262 additions and 48 deletions

View File

@ -112,11 +112,11 @@ fun DressUp(nav: NavHostController) {
skeleton.setToSetupPose()
skeleton.update(0f)
skeleton.updateWorldTransform(Skeleton.Physics.update)
skinImages[skin.getName()] = drawable.renderToBitmap(
renderer,
skinImages[skin.getName()] = renderer.renderToBitmap(
with(localDensity) { thumbnailSize.dp.toPx() },
with(localDensity) { thumbnailSize.dp.toPx() },
0xffffffff.toInt()
0xffffffff.toInt(),
skeleton,
).asImageBitmap()
selectedSkins[skin.getName()] = false
}

View File

@ -7,7 +7,9 @@ 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;
@ -17,6 +19,25 @@ 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;
@ -29,6 +50,9 @@ public class AndroidSkeletonDrawable {
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;
@ -40,6 +64,11 @@ public class AndroidSkeletonDrawable {
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);
@ -48,71 +77,71 @@ public class AndroidSkeletonDrawable {
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);
}
public Bitmap renderToBitmap(SkeletonRenderer renderer, float width, float height, int bgColor) {
Vector2 offset = new Vector2(0, 0);
Vector2 size = new Vector2(0, 0);
FloatArray floatArray = new FloatArray();
getSkeleton().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));
renderer.render(canvas, renderer.render(skeleton));
return bitmap;
}
}

View File

@ -40,6 +40,10 @@ import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Shader;
/**
* A class holding an {@link Bitmap} of an {@link AndroidTextureAtlas} page image with it's associated
* blend modes and paints.
*/
public class AndroidTexture extends Texture {
private Bitmap bitmap;
private ObjectMap<BlendMode, Paint> paints = new ObjectMap<>();

View File

@ -49,16 +49,25 @@ 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 static interface BitmapLoader {
private interface BitmapLoader {
Bitmap load (String path);
}
private Array<AndroidTexture> textures = new Array<>();
private Array<AtlasRegion> regions = new Array<>();
private final Array<AndroidTexture> textures = new Array<>();
private final Array<AtlasRegion> regions = new Array<>();
private AndroidTextureAtlas (TextureAtlasData data, BitmapLoader bitmapLoader) {
for (TextureAtlasData.Page page : data.getPages()) {
@ -85,8 +94,10 @@ public class AndroidTextureAtlas {
}
}
/** 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. */
/**
* 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);
@ -101,7 +112,12 @@ public class AndroidTextureAtlas {
return regions;
}
static public AndroidTextureAtlas fromAsset(String atlasFileName, Context context) {
/**
* 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();
@ -131,7 +147,12 @@ public class AndroidTextureAtlas {
});
}
static public AndroidTextureAtlas fromFile(File atlasFile) {
/**
* 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);
@ -148,7 +169,12 @@ public class AndroidTextureAtlas {
});
}
static public AndroidTextureAtlas fromHttp(URL atlasUrl, File targetDirectory) {
/**
* 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 {
@ -188,7 +214,7 @@ public class AndroidTextureAtlas {
}
}
static private TextureAtlasData loadTextureAtlasData(File atlasFile) {
private static TextureAtlasData loadTextureAtlasData(File atlasFile) {
TextureAtlasData data = new TextureAtlasData();
FileHandle inputFile = new FileHandle() {
@Override

View File

@ -7,6 +7,10 @@ import android.graphics.RectF;
import com.badlogic.gdx.utils.Array;
import com.esotericsoftware.spine.Bone;
/**
* Renders debug information for a {@link AndroidSkeletonDrawable}, like bone locations, to a {@link Canvas}.
* See {@link DebugRenderer#render}.
*/
public class DebugRenderer {
public void render(AndroidSkeletonDrawable drawable, Canvas canvas, Array<SkeletonRenderer.RenderCommand> commands) {

View File

@ -30,6 +30,7 @@
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;
@ -44,9 +45,22 @@ 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);
@ -76,7 +90,10 @@ public class SkeletonRenderer {
};
private final Array<RenderCommand> commandList = new Array<RenderCommand>();
public Array<RenderCommand> render (Skeleton skeleton) {
/**
* Created the {@link RenderCommand} commands from the skeletons current pose.
*/
public Array<RenderCommand> render(Skeleton skeleton) {
Color color = null, skeletonColor = skeleton.getColor();
float r = skeletonColor.r, g = skeletonColor.g, b = skeletonColor.b, a = skeletonColor.a;
@ -211,7 +228,11 @@ public class SkeletonRenderer {
return commandList;
}
public void render (Canvas canvas, Array<RenderCommand> commands) {
/**
* Renders the {@link RenderCommand} commands created from the skeleton current pose to the given {@link Canvas}.
* Does not perform any scaling or fitting.
*/
public void renderToCanvas(Canvas canvas, Array<RenderCommand> commands) {
for (int i = 0; i < commands.size; i++) {
RenderCommand command = commands.get(i);
@ -219,4 +240,42 @@ public class SkeletonRenderer {
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;
}
}

View File

@ -424,7 +424,7 @@ public class SpineView extends View implements Choreographer.FrameCallback {
controller.callOnBeforePaint(canvas);
Array<SkeletonRenderer.RenderCommand> commands = renderer.render(controller.getSkeleton());
renderer.render(canvas, commands);
renderer.renderToCanvas(canvas, commands);
controller.callOnAfterPaint(canvas, commands);
canvas.restore();

View File

@ -1,5 +1,8 @@
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),

View File

@ -4,6 +4,10 @@ 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;

View File

@ -2,6 +2,10 @@ 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);
}

View File

@ -1,6 +1,15 @@
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;
/**
* Fill the target view by distorting the source's aspect ratio.
*/
FILL
}

View File

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

View File

@ -4,6 +4,10 @@ import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.FloatArray;
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

View File

@ -8,22 +8,40 @@ import com.esotericsoftware.spine.android.AndroidSkeletonDrawable;
import java.util.Collections;
import java.util.List;
/**
* A {@link BoundsProvider} that calculates the bounding box needed for a combination of skins
* and an animation.
*/
public class SkinAndAnimationBounds implements BoundsProvider {
private final List<String> skins;
private final String animation;
private final double stepTime;
// Constructor
/**
* Constructs a new provider that will use the given {@code skins} and {@code animation} to calculate
* the bounding box of the skeleton. If no skins are given, the default skin is used.
* The {@code stepTime}, given in seconds, defines at what interval the bounds should be sampled
* across the entire animation.
*/
public SkinAndAnimationBounds(List<String> skins, String animation, double stepTime) {
this.skins = (skins == null || skins.isEmpty()) ? Collections.singletonList("default") : skins;
this.animation = animation;
this.stepTime = stepTime;
}
/**
* Constructs a new provider that will use the given {@code skins} and {@code animation} to calculate
* the bounding box of the skeleton. If no skins are given, the default skin is used.
* The {@code stepTime} has default value 0.1.
*/
public SkinAndAnimationBounds(List<String> skins, String animation) {
this(skins, animation, 0.1);
}
/**
* Constructs a new provider that will use the given {@code skins} and {@code animation} to calculate
* the bounding box of the skeleton. The default skin is used. The {@code stepTime} has default value 0.1.
*/
public SkinAndAnimationBounds(String animation) {
this(Collections.emptyList(), animation, 0.1);
}

View File

@ -12,8 +12,14 @@ 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;

View File

@ -17,8 +17,17 @@ 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);
@ -39,6 +48,12 @@ public class SkeletonDataUtils {
}
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);
@ -52,6 +67,11 @@ public class SkeletonDataUtils {
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);