Add spine-android to formatter

This commit is contained in:
Mario Zechner 2024-07-24 20:37:00 +02:00
parent 5de003cb8f
commit 12f11cbbf9
27 changed files with 794 additions and 971 deletions

View File

@ -8,7 +8,8 @@ spotless {
lineEndings 'UNIX' lineEndings 'UNIX'
java { java {
target 'spine-libgdx/**/*.java' target 'spine-libgdx/**/*.java',
'spine-android/**/*.java'
eclipse().configFile('formatters/eclipse-formatter.xml') eclipse().configFile('formatters/eclipse-formatter.xml')
} }

View File

@ -39,40 +39,38 @@ import com.esotericsoftware.spine.android.SpineController;
import com.esotericsoftware.spine.android.SpineView; import com.esotericsoftware.spine.android.SpineView;
public class SimpleAnimationActivity extends AppCompatActivity { public class SimpleAnimationActivity extends AppCompatActivity {
/** @noinspection FieldCanBeLocal*/ /** @noinspection FieldCanBeLocal */
private SpineView spineView; private SpineView spineView;
/** @noinspection FieldCanBeLocal*/ /** @noinspection FieldCanBeLocal */
private SpineController spineController; private SpineController spineController;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate (Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_simple_animation); setContentView(R.layout.activity_simple_animation);
// Set up the toolbar // Set up the toolbar
Toolbar toolbar = findViewById(R.id.toolbar); Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar); setSupportActionBar(toolbar);
if (getSupportActionBar() != null) { if (getSupportActionBar() != null) {
getSupportActionBar().setTitle("Simple Animation"); getSupportActionBar().setTitle("Simple Animation");
getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true); getSupportActionBar().setDisplayShowHomeEnabled(true);
} }
spineView = findViewById(R.id.spineView); spineView = findViewById(R.id.spineView);
spineController = new SpineController( controller -> spineController = new SpineController(controller -> controller.getAnimationState().setAnimation(0, "walk", true));
controller.getAnimationState().setAnimation(0, "walk", true)
);
spineView.setController(spineController); spineView.setController(spineController);
spineView.loadFromAsset("spineboy.atlas","spineboy-pro.json"); spineView.loadFromAsset("spineboy.atlas", "spineboy-pro.json");
} }
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected (MenuItem item) {
if (item.getItemId() == android.R.id.home) { if (item.getItemId() == android.R.id.home) {
finish(); finish();
return true; return true;
} }
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
} }

14
spine-android/publish.sh Executable file
View File

@ -0,0 +1,14 @@
#!/bin/sh
#
# 1. Set up PGP key for signing
# 2. Create ~/.gradle/gradle.properties
# 3. Add
# ossrhUsername=<sonatype-token-user-name>
# ossrhPassword=<sonatype-token>
# signing.gnupg.passphrase=<pgp-key-passphrase>
#
# After publishing via this script, log into https://oss.sonatype.org and release it manually after
# checks pass ("Release & Drop").
set -e
./gradlew publishReleasePublicationToSonaTypeRepository --info

View File

@ -40,7 +40,7 @@ dependencies {
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)
} }
val libraryVersion = "4.2.2-SNAPSHOT" // Update this as needed val libraryVersion = "4.2.3-SNAPSHOT" // Update this as needed
tasks.register<Jar>("sourceJar") { tasks.register<Jar>("sourceJar") {
archiveClassifier.set("sources") archiveClassifier.set("sources")
@ -125,5 +125,6 @@ afterEvaluate {
signing { signing {
useGpgCmd() useGpgCmd()
sign(publishing.publications["release"]) sign(publishing.publications["release"])
sign(tasks.getByName("sourceJar"))
} }
} }

View File

@ -1,3 +1,4 @@
package com.esotericsoftware.android; package com.esotericsoftware.android;
import android.content.Context; import android.content.Context;
@ -10,17 +11,15 @@ import org.junit.runner.RunWith;
import static org.junit.Assert.*; import static org.junit.Assert.*;
/** /** Instrumented test, which will execute on an Android device.
* Instrumented test, which will execute on an Android device.
* *
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a> * @see <a href="http://d.android.com/tools/testing">Testing documentation</a> */
*/
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest { public class ExampleInstrumentedTest {
@Test @Test
public void useAppContext() { public void useAppContext () {
// Context of the app under test. // Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.esotericsoftware.spine.test", appContext.getPackageName()); assertEquals("com.esotericsoftware.spine.test", appContext.getPackageName());
} }
} }

View File

@ -48,129 +48,111 @@ import com.esotericsoftware.spine.android.utils.SkeletonDataUtils;
import java.io.File; import java.io.File;
import java.net.URL; import java.net.URL;
/** /** A {@link AndroidSkeletonDrawable} bundles loading updating updating an {@link AndroidTextureAtlas}, {@link Skeleton}, and
* A {@link AndroidSkeletonDrawable} bundles loading updating updating an {@link AndroidTextureAtlas}, {@link Skeleton}, and {@link AnimationState} * {@link AnimationState} into a single easy-to-use class.
* into a single easy-to-use class.
* *
* Use the {@link AndroidSkeletonDrawable#fromAsset(String, String, Context)}, {@link AndroidSkeletonDrawable#fromFile(File, File)}, * Use the {@link AndroidSkeletonDrawable#fromAsset(String, String, Context)},
* or {@link AndroidSkeletonDrawable#fromHttp(URL, URL, File)} methods to construct a {@link AndroidSkeletonDrawable}. To have * {@link AndroidSkeletonDrawable#fromFile(File, File)}, or {@link AndroidSkeletonDrawable#fromHttp(URL, URL, File)} methods to
* multiple skeleton drawable instances share the same {@link AndroidTextureAtlas} and {@link SkeletonData}, use the constructor. * 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()}, * You can then directly access the {@link AndroidSkeletonDrawable#getAtlas()}, {@link AndroidSkeletonDrawable#getSkeletonData()},
* {@link AndroidSkeletonDrawable#getSkeleton()}, {@link AndroidSkeletonDrawable#getAnimationStateData()}, and {@link AndroidSkeletonDrawable#getAnimationState()} * {@link AndroidSkeletonDrawable#getSkeleton()}, {@link AndroidSkeletonDrawable#getAnimationStateData()}, and
* to query and animate the skeleton. Use the {@link AnimationState} to queue animations on one or more tracks * {@link AndroidSkeletonDrawable#getAnimationState()} to query and animate the skeleton. Use the {@link AnimationState} to queue
* via {@link AnimationState#setAnimation(int, Animation, boolean)} or {@link AnimationState#addAnimation(int, Animation, boolean, float)}. * 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 * To update the {@link AnimationState} and apply it to the {@link Skeleton}, call the
* a delta time in seconds to advance the animations. * {@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)}, * To render the current pose of the {@link Skeleton}, use {@link SkeletonRenderer#render(Skeleton)},
* {@link SkeletonRenderer#renderToBitmap(float, float, int, Skeleton)}, depending on your needs. * {@link SkeletonRenderer#renderToCanvas(Canvas, Array)}, {@link SkeletonRenderer#renderToBitmap(float, float, int, Skeleton)},
*/ * depending on your needs. */
public class AndroidSkeletonDrawable { public class AndroidSkeletonDrawable {
private final AndroidTextureAtlas atlas; private final AndroidTextureAtlas atlas;
private final SkeletonData skeletonData; private final SkeletonData skeletonData;
private final Skeleton skeleton; private final Skeleton skeleton;
private final AnimationStateData animationStateData; private final AnimationStateData animationStateData;
private final AnimationState animationState; private final AnimationState animationState;
/** /** Constructs a new skeleton drawable from the given (possibly shared) {@link AndroidTextureAtlas} and
* Constructs a new skeleton drawable from the given (possibly shared) {@link AndroidTextureAtlas} and {@link SkeletonData}. * {@link SkeletonData}. */
*/ public AndroidSkeletonDrawable (AndroidTextureAtlas atlas, SkeletonData skeletonData) {
public AndroidSkeletonDrawable(AndroidTextureAtlas atlas, SkeletonData skeletonData) { this.atlas = atlas;
this.atlas = atlas; this.skeletonData = skeletonData;
this.skeletonData = skeletonData;
skeleton = new Skeleton(skeletonData); skeleton = new Skeleton(skeletonData);
animationStateData = new AnimationStateData(skeletonData); animationStateData = new AnimationStateData(skeletonData);
animationState = new AnimationState(animationStateData); animationState = new AnimationState(animationStateData);
skeleton.updateWorldTransform(Skeleton.Physics.none); skeleton.updateWorldTransform(Skeleton.Physics.none);
} }
/** /** Updates the {@link AnimationState} using the {@code delta} time given in seconds, applies the animation state to the
* Updates the {@link AnimationState} using the {@code delta} time given in seconds, applies the * {@link Skeleton} and updates the world transforms of the skeleton to calculate its current pose. */
* animation state to the {@link Skeleton} and updates the world transforms of the skeleton public void update (float delta) {
* to calculate its current pose. animationState.update(delta);
*/ animationState.apply(skeleton);
public void update(float delta) {
animationState.update(delta);
animationState.apply(skeleton);
skeleton.update(delta); skeleton.update(delta);
skeleton.updateWorldTransform(Skeleton.Physics.update); skeleton.updateWorldTransform(Skeleton.Physics.update);
} }
/** /** Get the {@link AndroidTextureAtlas} */
* Get the {@link AndroidTextureAtlas} public AndroidTextureAtlas getAtlas () {
*/ return atlas;
public AndroidTextureAtlas getAtlas() { }
return atlas;
}
/** /** Get the {@link Skeleton} */
* Get the {@link Skeleton} public Skeleton getSkeleton () {
*/ return skeleton;
public Skeleton getSkeleton() { }
return skeleton;
}
/** /** Get the {@link SkeletonData} */
* Get the {@link SkeletonData} public SkeletonData getSkeletonData () {
*/ return skeletonData;
public SkeletonData getSkeletonData() { }
return skeletonData;
}
/** /** Get the {@link AnimationStateData} */
* Get the {@link AnimationStateData} public AnimationStateData getAnimationStateData () {
*/ return animationStateData;
public AnimationStateData getAnimationStateData() { }
return animationStateData;
}
/** /** Get the {@link AnimationState} */
* Get the {@link AnimationState} public AnimationState getAnimationState () {
*/ return animationState;
public AnimationState getAnimationState() { }
return animationState;
}
/** /** Constructs a new skeleton drawable from the {@code atlasFileName} and {@code skeletonFileName} from the the apps resources
* Constructs a new skeleton drawable from the {@code atlasFileName} and {@code skeletonFileName} from the the apps resources using {@link Context}. * using {@link Context}.
* *
* Throws an exception in case the data could not be loaded. * Throws an exception in case the data could not be loaded. */
*/ public static AndroidSkeletonDrawable fromAsset (String atlasFileName, String skeletonFileName, Context context) {
public static AndroidSkeletonDrawable fromAsset (String atlasFileName, String skeletonFileName, Context context) { AndroidTextureAtlas atlas = AndroidTextureAtlas.fromAsset(atlasFileName, context);
AndroidTextureAtlas atlas = AndroidTextureAtlas.fromAsset(atlasFileName, context); SkeletonData skeletonData = SkeletonDataUtils.fromAsset(atlas, skeletonFileName, context);
SkeletonData skeletonData = SkeletonDataUtils.fromAsset(atlas, skeletonFileName, context); return new AndroidSkeletonDrawable(atlas, skeletonData);
return new AndroidSkeletonDrawable(atlas, skeletonData); }
}
/** /** Constructs a new skeleton drawable from the {@code atlasFile} and {@code skeletonFile}.
* Constructs a new skeleton drawable from the {@code atlasFile} and {@code skeletonFile}. *
* * Throws an exception in case the data could not be loaded. */
* Throws an exception in case the data could not be loaded. public static AndroidSkeletonDrawable fromFile (File atlasFile, File skeletonFile) {
*/ AndroidTextureAtlas atlas = AndroidTextureAtlas.fromFile(atlasFile);
public static AndroidSkeletonDrawable fromFile (File atlasFile, File skeletonFile) { SkeletonData skeletonData = SkeletonDataUtils.fromFile(atlas, skeletonFile);
AndroidTextureAtlas atlas = AndroidTextureAtlas.fromFile(atlasFile); return new AndroidSkeletonDrawable(atlas, skeletonData);
SkeletonData skeletonData = SkeletonDataUtils.fromFile(atlas, skeletonFile); }
return new AndroidSkeletonDrawable(atlas, skeletonData);
}
/** /** Constructs a new skeleton drawable from the {@code atlasUrl} and {@code skeletonUrl}.
* Constructs a new skeleton drawable from the {@code atlasUrl} and {@code skeletonUrl}. *
* * Throws an exception in case the data could not be loaded. */
* 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);
public static AndroidSkeletonDrawable fromHttp (URL atlasUrl, URL skeletonUrl, File targetDirectory) { SkeletonData skeletonData = SkeletonDataUtils.fromHttp(atlas, skeletonUrl, targetDirectory);
AndroidTextureAtlas atlas = AndroidTextureAtlas.fromHttp(atlasUrl, targetDirectory); return new AndroidSkeletonDrawable(atlas, skeletonData);
SkeletonData skeletonData = SkeletonDataUtils.fromHttp(atlas, skeletonUrl, targetDirectory); }
return new AndroidSkeletonDrawable(atlas, skeletonData);
}
} }

View File

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

View File

@ -53,14 +53,11 @@ import android.graphics.Paint;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.os.Build; import android.os.Build;
/** /** Atlas data loaded from a `.atlas` file and its corresponding `.png` files. For each atlas image, a corresponding
* Atlas data loaded from a `.atlas` file and its corresponding `.png` files. For each atlas image, * {@link Bitmap} and {@link Paint} is constructed, which are used when rendering a skeleton that uses this atlas.
* 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)}, * Use the static methods {@link AndroidTextureAtlas#fromAsset(String, Context)}, {@link AndroidTextureAtlas#fromFile(File)}, and
* and {@link AndroidTextureAtlas#fromHttp(URL, File)} to load an atlas. * {@link AndroidTextureAtlas#fromHttp(URL, File)} to load an atlas. */
*/
public class AndroidTextureAtlas { public class AndroidTextureAtlas {
private interface BitmapLoader { private interface BitmapLoader {
Bitmap load (String path); Bitmap load (String path);
@ -94,10 +91,8 @@ public class AndroidTextureAtlas {
} }
} }
/** /** Returns the first region found with the specified name. This method uses string comparison to find the region, so the
* 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. */
* result should be cached rather than calling this method multiple times.
*/
public @Null AtlasRegion findRegion (String name) { public @Null AtlasRegion findRegion (String name) {
for (int i = 0, n = regions.size; i < n; i++) for (int i = 0, n = regions.size; i < n; i++)
if (regions.get(i).name.equals(name)) return regions.get(i); if (regions.get(i).name.equals(name)) return regions.get(i);
@ -112,12 +107,10 @@ public class AndroidTextureAtlas {
return regions; return regions;
} }
/** /** Loads an {@link AndroidTextureAtlas} from the file {@code atlasFileName} from assets using {@link 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. * Throws a {@link RuntimeException} in case the atlas could not be loaded. */
*/ public static AndroidTextureAtlas fromAsset (String atlasFileName, Context context) {
public static AndroidTextureAtlas fromAsset(String atlasFileName, Context context) {
TextureAtlasData data = new TextureAtlasData(); TextureAtlasData data = new TextureAtlasData();
AssetManager assetManager = context.getAssets(); AssetManager assetManager = context.getAssets();
@ -138,21 +131,19 @@ public class AndroidTextureAtlas {
} }
return new AndroidTextureAtlas(data, path -> { return new AndroidTextureAtlas(data, path -> {
path = path.startsWith("/") ? path.substring(1) : path; path = path.startsWith("/") ? path.substring(1) : path;
try (InputStream in = new BufferedInputStream(assetManager.open(path))) { try (InputStream in = new BufferedInputStream(assetManager.open(path))) {
return BitmapFactory.decodeStream(in); return BitmapFactory.decodeStream(in);
} catch (Throwable t) { } catch (Throwable t) {
throw new RuntimeException(t); throw new RuntimeException(t);
} }
}); });
} }
/** /** Loads an {@link AndroidTextureAtlas} from the file {@code atlasFileName}.
* Loads an {@link AndroidTextureAtlas} from the file {@code atlasFileName}.
* *
* Throws a {@link RuntimeException} in case the atlas could not be loaded. * Throws a {@link RuntimeException} in case the atlas could not be loaded. */
*/ public static AndroidTextureAtlas fromFile (File atlasFile) {
public static AndroidTextureAtlas fromFile(File atlasFile) {
TextureAtlasData data; TextureAtlasData data;
try { try {
data = loadTextureAtlasData(atlasFile); data = loadTextureAtlasData(atlasFile);
@ -169,12 +160,10 @@ public class AndroidTextureAtlas {
}); });
} }
/** /** Loads an {@link AndroidTextureAtlas} from the URL {@code atlasURL}.
* Loads an {@link AndroidTextureAtlas} from the URL {@code atlasURL}.
* *
* Throws a {@link Exception} in case the atlas could not be loaded. * Throws a {@link Exception} in case the atlas could not be loaded. */
*/ public static AndroidTextureAtlas fromHttp (URL atlasUrl, File targetDirectory) {
public static AndroidTextureAtlas fromHttp(URL atlasUrl, File targetDirectory) {
File atlasFile = HttpUtils.downloadFrom(atlasUrl, targetDirectory); File atlasFile = HttpUtils.downloadFrom(atlasUrl, targetDirectory);
TextureAtlasData data; TextureAtlasData data;
try { try {
@ -205,20 +194,20 @@ public class AndroidTextureAtlas {
}); });
} }
private static InputStream inputStream(File file) throws Exception { private static InputStream inputStream (File file) throws Exception {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return Files.newInputStream(file.toPath()); return Files.newInputStream(file.toPath());
} else { } else {
//noinspection IOStreamConstructor // noinspection IOStreamConstructor
return new FileInputStream(file); return new FileInputStream(file);
} }
} }
private static TextureAtlasData loadTextureAtlasData(File atlasFile) { private static TextureAtlasData loadTextureAtlasData (File atlasFile) {
TextureAtlasData data = new TextureAtlasData(); TextureAtlasData data = new TextureAtlasData();
FileHandle inputFile = new FileHandle() { FileHandle inputFile = new FileHandle() {
@Override @Override
public InputStream read() { public InputStream read () {
try { try {
return new FileInputStream(atlasFile); return new FileInputStream(atlasFile);
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {

View File

@ -36,21 +36,19 @@ import android.graphics.RectF;
import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.Array;
import com.esotericsoftware.spine.Bone; import com.esotericsoftware.spine.Bone;
/** /** Renders debug information for a {@link AndroidSkeletonDrawable}, like bone locations, to a {@link Canvas}. See
* Renders debug information for a {@link AndroidSkeletonDrawable}, like bone locations, to a {@link Canvas}. * {@link DebugRenderer#render}. */
* See {@link DebugRenderer#render}.
*/
public class DebugRenderer { public class DebugRenderer {
public void render(AndroidSkeletonDrawable drawable, Canvas canvas, Array<SkeletonRenderer.RenderCommand> commands) { public void render (AndroidSkeletonDrawable drawable, Canvas canvas, Array<SkeletonRenderer.RenderCommand> commands) {
Paint bonePaint = new Paint(); Paint bonePaint = new Paint();
bonePaint.setColor(android.graphics.Color.BLUE); bonePaint.setColor(android.graphics.Color.BLUE);
bonePaint.setStyle(Paint.Style.FILL); bonePaint.setStyle(Paint.Style.FILL);
for (Bone bone : drawable.getSkeleton().getBones()) { for (Bone bone : drawable.getSkeleton().getBones()) {
float x = bone.getWorldX(); float x = bone.getWorldX();
float y = bone.getWorldY(); float y = bone.getWorldY();
canvas.drawRect(new RectF(x - 2.5f, y - 2.5f, x + 2.5f, y + 2.5f), bonePaint); canvas.drawRect(new RectF(x - 2.5f, y - 2.5f, x + 2.5f, y + 2.5f), bonePaint);
} }
} }
} }

View File

@ -50,17 +50,13 @@ import android.graphics.Canvas;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.RectF; import android.graphics.RectF;
/** /** Is responsible to transform the {@link Skeleton} with its current pose to {@link SkeletonRenderer.RenderCommand} commands and
* Is responsible to transform the {@link Skeleton} with its current pose to {@link SkeletonRenderer.RenderCommand} commands * render them to a {@link Canvas}. */
* and render them to a {@link Canvas}.
*/
public class SkeletonRenderer { public class SkeletonRenderer {
/** /** Stores the vertices, indices, and atlas page index to be used for rendering one or more attachments of a {@link Skeleton}
* Stores the vertices, indices, and atlas page index to be used for rendering one or more attachments * to a {@link Canvas}. See the implementation of {@link SkeletonRenderer#render(Skeleton)} and
* 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}. */
* {@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 { public static class RenderCommand implements Pool.Poolable {
FloatArray vertices = new FloatArray(32); FloatArray vertices = new FloatArray(32);
FloatArray uvs = new FloatArray(32); FloatArray uvs = new FloatArray(32);
@ -90,10 +86,8 @@ public class SkeletonRenderer {
}; };
private final Array<RenderCommand> commandList = new Array<RenderCommand>(); private final Array<RenderCommand> commandList = new Array<RenderCommand>();
/** /** Created the {@link RenderCommand} commands from the skeletons current pose. */
* Created the {@link RenderCommand} commands from the skeletons current pose. public Array<RenderCommand> render (Skeleton skeleton) {
*/
public Array<RenderCommand> render(Skeleton skeleton) {
Color color = null, skeletonColor = skeleton.getColor(); Color color = null, skeletonColor = skeleton.getColor();
float r = skeletonColor.r, g = skeletonColor.g, b = skeletonColor.b, a = skeletonColor.a; float r = skeletonColor.r, g = skeletonColor.g, b = skeletonColor.b, a = skeletonColor.a;
@ -228,11 +222,9 @@ public class SkeletonRenderer {
return commandList; return commandList;
} }
/** /** Renders the {@link RenderCommand} commands created from the skeleton current pose to the given {@link Canvas}. Does not
* Renders the {@link RenderCommand} commands created from the skeleton current pose to the given {@link Canvas}. * perform any scaling or fitting. */
* Does not perform any scaling or fitting. public void renderToCanvas (Canvas canvas, Array<RenderCommand> commands) {
*/
public void renderToCanvas(Canvas canvas, Array<RenderCommand> commands) {
for (int i = 0; i < commands.size; i++) { for (int i = 0; i < commands.size; i++) {
RenderCommand command = commands.get(i); RenderCommand command = commands.get(i);
@ -241,15 +233,13 @@ public class SkeletonRenderer {
} }
} }
/** /** Renders the {@link Skeleton} with its current pose to a {@link Bitmap}.
* Renders the {@link Skeleton} with its current pose to a {@link Bitmap}.
* *
* @param width The width of the bitmap in pixels. * @param width The width of the bitmap in pixels.
* @param height The height of the bitmap in pixels. * @param height The height of the bitmap in pixels.
* @param bgColor The background color. * @param bgColor The background color.
* @param skeleton The skeleton to render. * @param skeleton The skeleton to render. */
*/ public Bitmap renderToBitmap (float width, float height, int bgColor, Skeleton skeleton) {
public Bitmap renderToBitmap(float width, float height, int bgColor, Skeleton skeleton) {
Vector2 offset = new Vector2(0, 0); Vector2 offset = new Vector2(0, 0);
Vector2 size = new Vector2(0, 0); Vector2 size = new Vector2(0, 0);
FloatArray floatArray = new FloatArray(); FloatArray floatArray = new FloatArray();
@ -259,7 +249,7 @@ public class SkeletonRenderer {
RectF bounds = new RectF(offset.x, offset.y, offset.x + size.x, offset.y + size.y); 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)); 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); Bitmap bitmap = Bitmap.createBitmap((int)width, (int)height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap); Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint(); Paint paint = new Paint();

View File

@ -43,289 +43,245 @@ import com.esotericsoftware.spine.android.callbacks.SpineControllerAfterPaintCal
import com.esotericsoftware.spine.android.callbacks.SpineControllerBeforePaintCallback; import com.esotericsoftware.spine.android.callbacks.SpineControllerBeforePaintCallback;
import com.esotericsoftware.spine.android.callbacks.SpineControllerCallback; import com.esotericsoftware.spine.android.callbacks.SpineControllerCallback;
/** /** Controls how the skeleton of a {@link SpineView} is animated and rendered.
* 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 * Upon initialization of a {@link SpineView}, the provided {@code onInitialized} callback method is called once. This method can
* to set up the initial animation(s) of the skeleton, among other things. * 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, * After initialization is complete, the {@link SpineView} is rendered at the screen refresh rate. In each frame, the
* the {@link AnimationState} is updated and applied to the {@link Skeleton}. * {@link AnimationState} is updated and applied to the {@link Skeleton}.
* *
* Next, the optionally provided method {@code onBeforeUpdateWorldTransforms} is called, which can modify the * Next, the optionally provided method {@code onBeforeUpdateWorldTransforms} is called, which can modify the skeleton before its
* skeleton before its current pose is calculated using {@link Skeleton#updateWorldTransform(Skeleton.Physics)}. After * current pose is calculated using {@link Skeleton#updateWorldTransform(Skeleton.Physics)}. After
* {@link Skeleton#updateWorldTransform(Skeleton.Physics)} has completed, the optional {@code onAfterUpdateWorldTransforms} method is * {@link Skeleton#updateWorldTransform(Skeleton.Physics)} has completed, the optional {@code onAfterUpdateWorldTransforms} method
* called, which can modify the current pose before rendering the skeleton. * 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, * Before the skeleton's current pose is rendered by the {@link SpineView}, the optional {@code onBeforePaint} is called, which
* which allows rendering backgrounds or other objects that should go behind the skeleton on the {@link Canvas}. The * allows rendering backgrounds or other objects that should go behind the skeleton on the {@link Canvas}. The {@link SpineView}
* {@link SpineView} then renders the skeleton's current pose and finally calls the optional {@code onAfterPaint}, which * then renders the skeleton's current pose and finally calls the optional {@code onAfterPaint}, which can render additional
* can render additional objects on top of the skeleton. * objects on top of the skeleton.
* *
* The underlying {@link AndroidTextureAtlas}, {@link SkeletonData}, {@link Skeleton}, {@link AnimationStateData}, {@link AnimationState}, and {@link AndroidSkeletonDrawable} * The underlying {@link AndroidTextureAtlas}, {@link SkeletonData}, {@link Skeleton}, {@link AnimationStateData},
* can be accessed through their respective getters to inspect and/or modify the skeleton and its associated data. Accessing * {@link AnimationState}, and {@link AndroidSkeletonDrawable} can be accessed through their respective getters to inspect and/or
* this data is only allowed if the {@link SpineView} and its data have been initialized and have not been disposed of yet. * 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 * By default, the widget updates and renders the skeleton every frame. The {@code pause} method can be used to pause updating and
* and rendering the skeleton. The {@link SpineController#resume()} method resumes updating and rendering the skeleton. The {@link SpineController#isPlaying()} getter * rendering the skeleton. The {@link SpineController#resume()} method resumes updating and rendering the skeleton. The
* reports the current state. * {@link SpineController#isPlaying()} getter reports the current state. */
*/
public class SpineController { public class SpineController {
/** /** Used to build {@link SpineController} instances. */
* Used to build {@link SpineController} instances. public static class Builder {
* */ private final SpineControllerCallback onInitialized;
public static class Builder { private SpineControllerCallback onBeforeUpdateWorldTransforms;
private final SpineControllerCallback onInitialized; private SpineControllerCallback onAfterUpdateWorldTransforms;
private SpineControllerCallback onBeforeUpdateWorldTransforms; private SpineControllerBeforePaintCallback onBeforePaint;
private SpineControllerCallback onAfterUpdateWorldTransforms; private SpineControllerAfterPaintCallback onAfterPaint;
private SpineControllerBeforePaintCallback onBeforePaint;
private SpineControllerAfterPaintCallback onAfterPaint;
/** /** Instantiate a {@link Builder} used to build a {@link SpineController}, which controls how the skeleton of a
* Instantiate a {@link Builder} used to build a {@link SpineController}, which controls how the skeleton of a {@link SpineView} * {@link SpineView} is animated and rendered. Upon initialization of a {@link SpineView}, the provided
* is animated and rendered. Upon initialization of a {@link SpineView}, the provided {@code onInitialized} callback * {@code onInitialized} callback method is called once. This method can be used to set up the initial animation(s) of the
* method is called once. This method can be used to set up the initial animation(s) of the skeleton, among other things. * skeleton, among other things.
* *
* @param onInitialized Upon initialization of a {@link SpineView}, the provided {@code onInitialized} callback * @param onInitialized Upon initialization of a {@link SpineView}, the provided {@code onInitialized} callback method is
* method is called once. This method can be used to set up the initial animation(s) of the skeleton, * called once. This method can be used to set up the initial animation(s) of the skeleton, among other things. */
* among other things. public Builder (SpineControllerCallback onInitialized) {
*/ this.onInitialized = onInitialized;
public Builder(SpineControllerCallback onInitialized) { }
this.onInitialized = onInitialized;
}
/** /** Sets the {@code onBeforeUpdateWorldTransforms} callback. It is called before the skeleton's current pose is calculated
* 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
* using {@link Skeleton#updateWorldTransform(Skeleton.Physics)}. It can be used to modify the skeleton before the pose calculation. * calculation. */
*/ public Builder setOnBeforeUpdateWorldTransforms (SpineControllerCallback onBeforeUpdateWorldTransforms) {
public Builder setOnBeforeUpdateWorldTransforms(SpineControllerCallback onBeforeUpdateWorldTransforms) { this.onBeforeUpdateWorldTransforms = onBeforeUpdateWorldTransforms;
this.onBeforeUpdateWorldTransforms = onBeforeUpdateWorldTransforms; return this;
return this; }
}
/** /** Sets the {@code onAfterUpdateWorldTransforms} callback. This method is called after the skeleton's current pose is
* Sets the {@code onAfterUpdateWorldTransforms} callback. This method is called after the skeleton's current pose is calculated using * calculated using {@link Skeleton#updateWorldTransform(Skeleton.Physics)}. It can be used to modify the current pose
* {@link Skeleton#updateWorldTransform(Skeleton.Physics)}. It can be used to modify the current pose before rendering the skeleton. * before rendering the skeleton. */
*/ public Builder setOnAfterUpdateWorldTransforms (SpineControllerCallback onAfterUpdateWorldTransforms) {
public Builder setOnAfterUpdateWorldTransforms(SpineControllerCallback onAfterUpdateWorldTransforms) { this.onAfterUpdateWorldTransforms = onAfterUpdateWorldTransforms;
this.onAfterUpdateWorldTransforms = onAfterUpdateWorldTransforms; return this;
return this; }
}
/** /** Sets the {@code onBeforePaint} callback. It is called before the skeleton's current pose is rendered by the
* 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 SpineView}. It allows rendering backgrounds or other objects that should go behind the skeleton on the * {@link Canvas}. */
* {@link Canvas}. public Builder setOnBeforePaint (SpineControllerBeforePaintCallback onBeforePaint) {
*/ this.onBeforePaint = onBeforePaint;
public Builder setOnBeforePaint(SpineControllerBeforePaintCallback onBeforePaint) { return this;
this.onBeforePaint = onBeforePaint; }
return this;
}
/** /** Sets the {@code onAfterPaint} callback. It is called after the skeleton's current pose is rendered by the
* 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. */
* {@link SpineView}. It allows rendering additional objects on top of the skeleton. public Builder setOnAfterPaint (SpineControllerAfterPaintCallback onAfterPaint) {
*/ this.onAfterPaint = onAfterPaint;
public Builder setOnAfterPaint(SpineControllerAfterPaintCallback onAfterPaint) { return this;
this.onAfterPaint = onAfterPaint; }
return this;
}
public SpineController build() { public SpineController build () {
SpineController spineController = new SpineController(onInitialized); SpineController spineController = new SpineController(onInitialized);
spineController.onBeforeUpdateWorldTransforms = onBeforeUpdateWorldTransforms; spineController.onBeforeUpdateWorldTransforms = onBeforeUpdateWorldTransforms;
spineController.onAfterUpdateWorldTransforms = onAfterUpdateWorldTransforms; spineController.onAfterUpdateWorldTransforms = onAfterUpdateWorldTransforms;
spineController.onBeforePaint = onBeforePaint; spineController.onBeforePaint = onBeforePaint;
spineController.onAfterPaint = onAfterPaint; spineController.onAfterPaint = onAfterPaint;
return spineController; return spineController;
} }
} }
private final SpineControllerCallback onInitialized; private final SpineControllerCallback onInitialized;
private @Nullable SpineControllerCallback onBeforeUpdateWorldTransforms; private @Nullable SpineControllerCallback onBeforeUpdateWorldTransforms;
private @Nullable SpineControllerCallback onAfterUpdateWorldTransforms; private @Nullable SpineControllerCallback onAfterUpdateWorldTransforms;
private @Nullable SpineControllerBeforePaintCallback onBeforePaint; private @Nullable SpineControllerBeforePaintCallback onBeforePaint;
private @Nullable SpineControllerAfterPaintCallback onAfterPaint; private @Nullable SpineControllerAfterPaintCallback onAfterPaint;
private AndroidSkeletonDrawable drawable; private AndroidSkeletonDrawable drawable;
private boolean playing = true; private boolean playing = true;
private double offsetX = 0; private double offsetX = 0;
private double offsetY = 0; private double offsetY = 0;
private double scaleX = 1; private double scaleX = 1;
private double scaleY = 1; private double scaleY = 1;
/** /** Instantiate a {@link SpineController}, which controls how the skeleton of a {@link SpineView} is animated and rendered.
* 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
* Upon initialization of a {@link SpineView}, the provided {@code onInitialized} callback method is called once. * can be used to set up the initial animation(s) of the skeleton, among other things.
* 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
* @param onInitialized Upon initialization of a {@link SpineView}, the provided {@code onInitialized} callback * called once. This method can be used to set up the initial animation(s) of the skeleton, among other things. */
* method is called once. This method can be used to set up the initial animation(s) of the skeleton, public SpineController (SpineControllerCallback onInitialized) {
* among other things. this.onInitialized = onInitialized;
*/ }
public SpineController(SpineControllerCallback onInitialized) {
this.onInitialized = onInitialized;
}
protected void init(AndroidSkeletonDrawable drawable) { protected void init (AndroidSkeletonDrawable drawable) {
this.drawable = drawable; this.drawable = drawable;
if (onInitialized != null) { if (onInitialized != null) {
onInitialized.execute(this); onInitialized.execute(this);
} }
} }
/** /** The {@link AndroidTextureAtlas} from which images to render the skeleton are sourced. */
* 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.");
public AndroidTextureAtlas getAtlas() { return drawable.getAtlas();
if (drawable == null) throw new RuntimeException("Controller is not initialized yet."); }
return drawable.getAtlas();
}
/** /** The setup-pose data used by the skeleton. */
* The setup-pose data used by the skeleton. public SkeletonData getSkeletonDate () {
*/ if (drawable == null) throw new RuntimeException("Controller is not initialized yet.");
public SkeletonData getSkeletonDate() { return drawable.getSkeletonData();
if (drawable == null) throw new RuntimeException("Controller is not initialized yet."); }
return drawable.getSkeletonData();
}
/** /** The {@link Skeleton}. */
* The {@link Skeleton}. public Skeleton getSkeleton () {
*/ if (drawable == null) throw new RuntimeException("Controller is not initialized yet.");
public Skeleton getSkeleton() { return drawable.getSkeleton();
if (drawable == null) throw new RuntimeException("Controller is not initialized yet."); }
return drawable.getSkeleton();
}
/** /** The mixing information used by the {@link AnimationState}. */
* The mixing information used by the {@link AnimationState}. public AnimationStateData getAnimationStateData () {
*/ if (drawable == null) throw new RuntimeException("Controller is not initialized yet.");
public AnimationStateData getAnimationStateData() { return drawable.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. */
* The {@link AnimationState} used to manage animations that are being applied to the public AnimationState getAnimationState () {
* skeleton. if (drawable == null) throw new RuntimeException("Controller is not initialized yet.");
*/ return drawable.getAnimationState();
public AnimationState getAnimationState() { }
if (drawable == null) throw new RuntimeException("Controller is not initialized yet.");
return drawable.getAnimationState();
}
/** /** The {@link AndroidSkeletonDrawable}. */
* The {@link AndroidSkeletonDrawable}. public AndroidSkeletonDrawable getDrawable () {
*/ if (drawable == null) throw new RuntimeException("Controller is not initialized yet.");
public AndroidSkeletonDrawable getDrawable() { return drawable;
if (drawable == null) throw new RuntimeException("Controller is not initialized yet."); }
return drawable;
}
/** /** Checks if the {@link SpineView} is initialized. */
* Checks if the {@link SpineView} is initialized. public boolean isInitialized () {
*/ return drawable != null;
public boolean isInitialized() { }
return drawable != null;
}
/** /** Checks if the animation is currently playing. */
* Checks if the animation is currently playing. public boolean isPlaying () {
*/ return playing;
public boolean isPlaying() { }
return playing;
}
/** /** Pauses updating and rendering the skeleton. */
* Pauses updating and rendering the skeleton. public void pause () {
*/ if (playing) {
public void pause() { playing = false;
if (playing) { }
playing = false; }
}
}
/** /** Resumes updating and rendering the skeleton. */
* Resumes updating and rendering the skeleton. public void resume () {
*/ if (!playing) {
public void resume() { playing = true;
if (!playing) { }
playing = true; }
}
}
/** /** Transforms the coordinates given in the {@link SpineView} coordinate system in {@code position} to the skeleton coordinate
* Transforms the coordinates given in the {@link SpineView} coordinate system in {@code position} to * system. See the {@code IKFollowing.kt} example for how to use this to move a bone based on user touch input. */
* the skeleton coordinate system. See the {@code IKFollowing.kt} example for how to use this public Point toSkeletonCoordinates (Point position) {
* to move a bone based on user touch input. int x = position.x;
*/ int y = position.y;
public Point toSkeletonCoordinates(Point position) { return new Point((int)(x / scaleX - offsetX), (int)(y / scaleY - offsetY));
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
* 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
* using {@link Skeleton#updateWorldTransform(Skeleton.Physics)}. It can be used to modify the skeleton before the pose calculation. * calculation. */
*/ public void setOnBeforeUpdateWorldTransforms (@Nullable SpineControllerCallback onBeforeUpdateWorldTransforms) {
public void setOnBeforeUpdateWorldTransforms(@Nullable SpineControllerCallback onBeforeUpdateWorldTransforms) { this.onBeforeUpdateWorldTransforms = onBeforeUpdateWorldTransforms;
this.onBeforeUpdateWorldTransforms = onBeforeUpdateWorldTransforms; }
}
/** /** Sets the {@code onAfterUpdateWorldTransforms} callback. This method is called after the skeleton's current pose is
* Sets the {@code onAfterUpdateWorldTransforms} callback. This method is called after the skeleton's current pose is calculated using * calculated using {@link Skeleton#updateWorldTransform(Skeleton.Physics)}. It can be used to modify the current pose before
* {@link Skeleton#updateWorldTransform(Skeleton.Physics)}. It can be used to modify the current pose before rendering the skeleton. * rendering the skeleton. */
*/ public void setOnAfterUpdateWorldTransforms (@Nullable SpineControllerCallback onAfterUpdateWorldTransforms) {
public void setOnAfterUpdateWorldTransforms(@Nullable SpineControllerCallback onAfterUpdateWorldTransforms) { this.onAfterUpdateWorldTransforms = onAfterUpdateWorldTransforms;
this.onAfterUpdateWorldTransforms = onAfterUpdateWorldTransforms; }
}
/** /** Sets the {@code onBeforePaint} callback. It is called before the skeleton's current pose is rendered by the
* 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 SpineView}. It allows rendering backgrounds or other objects that should go behind the skeleton on the * {@link Canvas}. */
* {@link Canvas}. public void setOnBeforePaint (@Nullable SpineControllerBeforePaintCallback onBeforePaint) {
*/ this.onBeforePaint = onBeforePaint;
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
* 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. */
* {@link SpineView}. It allows rendering additional objects on top of the skeleton. public void setOnAfterPaint (@Nullable SpineControllerAfterPaintCallback onAfterPaint) {
*/ this.onAfterPaint = onAfterPaint;
public void setOnAfterPaint(@Nullable SpineControllerAfterPaintCallback onAfterPaint) { }
this.onAfterPaint = onAfterPaint;
}
protected void setCoordinateTransform(double offsetX, double offsetY, double scaleX, double scaleY) { protected void setCoordinateTransform (double offsetX, double offsetY, double scaleX, double scaleY) {
this.offsetX = offsetX; this.offsetX = offsetX;
this.offsetY = offsetY; this.offsetY = offsetY;
this.scaleX = scaleX; this.scaleX = scaleX;
this.scaleY = scaleY; this.scaleY = scaleY;
} }
protected void callOnBeforeUpdateWorldTransforms() { protected void callOnBeforeUpdateWorldTransforms () {
if (onBeforeUpdateWorldTransforms != null) { if (onBeforeUpdateWorldTransforms != null) {
onBeforeUpdateWorldTransforms.execute(this); onBeforeUpdateWorldTransforms.execute(this);
} }
} }
protected void callOnAfterUpdateWorldTransforms() { protected void callOnAfterUpdateWorldTransforms () {
if (onAfterUpdateWorldTransforms != null) { if (onAfterUpdateWorldTransforms != null) {
onAfterUpdateWorldTransforms.execute(this); onAfterUpdateWorldTransforms.execute(this);
} }
} }
protected void callOnBeforePaint(Canvas canvas) { protected void callOnBeforePaint (Canvas canvas) {
if (onBeforePaint != null) { if (onBeforePaint != null) {
onBeforePaint.execute(this, canvas); onBeforePaint.execute(this, canvas);
} }
} }
protected void callOnAfterPaint(Canvas canvas, Array<SkeletonRenderer.RenderCommand> renderCommands) { protected void callOnAfterPaint (Canvas canvas, Array<SkeletonRenderer.RenderCommand> renderCommands) {
if (onAfterPaint != null) { if (onAfterPaint != null) {
onAfterPaint.execute(this, canvas, renderCommands); onAfterPaint.execute(this, canvas, renderCommands);
} }
} }
} }

View File

@ -51,20 +51,20 @@ import androidx.annotation.NonNull;
import java.io.File; import java.io.File;
import java.net.URL; import java.net.URL;
/** /** A {@link View} to display a Spine skeleton. The skeleton can be loaded from an asset bundle
* A {@link View} to display a Spine skeleton. The skeleton can be loaded from an asset bundle ({@link SpineView#loadFromAssets(String, String, Context, SpineController)}), * ({@link SpineView#loadFromAssets(String, String, Context, SpineController)}), local files
* 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)}). * ({@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 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 * The size of the widget can be derived from the bounds provided by a {@link BoundsProvider}. If the widget is not sized by the
* computed by the {@link BoundsProvider}, the widget will use the computed bounds to fit the skeleton inside the widget's dimensions. * 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 { public class SpineView extends View implements Choreographer.FrameCallback {
/** /** Used to build {@link SpineView} instances. */
* Used to build {@link SpineView} instances.
* */
public static class Builder { public static class Builder {
private final Context context; private final Context context;
private final SpineController controller; private final SpineController controller;
@ -80,90 +80,72 @@ public class SpineView extends View implements Choreographer.FrameCallback {
private Alignment alignment = Alignment.CENTER; private Alignment alignment = Alignment.CENTER;
private ContentMode contentMode = ContentMode.FIT; 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.
* 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}. * @param controller The skeleton displayed by a {@link SpineView} can be controlled via a {@link SpineController}. */
*/ public Builder (Context context, SpineController controller) {
public Builder(Context context, SpineController controller) {
this.context = context; this.context = context;
this.controller = controller; this.controller = controller;
} }
/** /** Loads assets from your app assets for the {@link SpineView} if set. The {@code atlasFileName} specifies the `.atlas`
* Loads assets from your app assets for the {@link SpineView} if set. The {@code atlasFileName} specifies the * file to be loaded for the images used to render the skeleton. The {@code skeletonFileName} specifies either a Skeleton
* `.atlas` file to be loaded for the images used to render the skeleton. The {@code skeletonFileName} specifies either a Skeleton `.json` or * `.json` or `.skel` file containing the skeleton data. */
* `.skel` file containing the skeleton data. public Builder setLoadFromAssets (String atlasFileName, String skeletonFileName) {
*/
public Builder setLoadFromAssets(String atlasFileName, String skeletonFileName) {
this.atlasFileName = atlasFileName; this.atlasFileName = atlasFileName;
this.skeletonFileName = skeletonFileName; this.skeletonFileName = skeletonFileName;
return this; return this;
} }
/** /** Loads assets from files for the {@link SpineView} if set. The {@code atlasFile} specifies the `.atlas` file to be loaded
* 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 * for the images used to render the skeleton. The {@code skeletonFile} specifies either a Skeleton `.json` or `.skel` file
* the skeleton. The {@code skeletonFile} specifies either a Skeleton `.json` or `.skel` file containing the skeleton data. * containing the skeleton data. */
*/ public Builder setLoadFromFile (File atlasFile, File skeletonFile) {
public Builder setLoadFromFile(File atlasFile, File skeletonFile) {
this.atlasFile = atlasFile; this.atlasFile = atlasFile;
this.skeletonFile = skeletonFile; this.skeletonFile = skeletonFile;
return this; return this;
} }
/** /** Loads assets from http for the {@link SpineView} if set. The {@code atlasUrl} specifies the `.atlas` url to be loaded
* 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 * for the images used to render the skeleton. The {@code skeletonUrl} specifies either a Skeleton `.json` or `.skel` url
* the skeleton. The {@code skeletonUrl} specifies either a Skeleton `.json` or `.skel` url containing the skeleton data. * containing the skeleton data. */
*/ public Builder setLoadFromHttp (URL atlasUrl, URL skeletonUrl, File targetDirectory) {
public Builder setLoadFromHttp(URL atlasUrl, URL skeletonUrl, File targetDirectory) {
this.atlasUrl = atlasUrl; this.atlasUrl = atlasUrl;
this.skeletonUrl = skeletonUrl; this.skeletonUrl = skeletonUrl;
this.targetDirectory = targetDirectory; this.targetDirectory = targetDirectory;
return this; return this;
} }
/** /** Uses the {@link AndroidSkeletonDrawable} for the {@link SpineView} if set. */
* Uses the {@link AndroidSkeletonDrawable} for the {@link SpineView} if set. public Builder setLoadFromDrawable (AndroidSkeletonDrawable drawable) {
*/
public Builder setLoadFromDrawable(AndroidSkeletonDrawable drawable) {
this.drawable = drawable; this.drawable = drawable;
return this; return this;
} }
/** /** Get the {@link BoundsProvider} used to compute the bounds of the {@link Skeleton} inside the view. The default is
* Get the {@link BoundsProvider} used to compute the bounds of the {@link Skeleton} inside the view. * {@link SetupPoseBounds}. */
* The default is {@link SetupPoseBounds}. public Builder setBoundsProvider (BoundsProvider boundsProvider) {
*/
public Builder setBoundsProvider(BoundsProvider boundsProvider) {
this.boundsProvider = boundsProvider; this.boundsProvider = boundsProvider;
return this; return this;
} }
/** /** Get the {@link ContentMode} used to fit the {@link Skeleton} inside the view. The default is {@link ContentMode#FIT}. */
* Get the {@link ContentMode} used to fit the {@link Skeleton} inside the view. public Builder setContentMode (ContentMode contentMode) {
* The default is {@link ContentMode#FIT}.
*/
public Builder setContentMode(ContentMode contentMode) {
this.contentMode = contentMode; this.contentMode = contentMode;
return this; return this;
} }
/** /** Set the {@link Alignment} used to align the {@link Skeleton} inside the view. The default is {@link Alignment#CENTER} */
* Set the {@link Alignment} used to align the {@link Skeleton} inside the view. public Builder setAlignment (Alignment alignment) {
* The default is {@link Alignment#CENTER}
*/
public Builder setAlignment(Alignment alignment) {
this.alignment = alignment; this.alignment = alignment;
return this; return this;
} }
/** /** Builds a new {@link SpineView}.
* Builds a new {@link SpineView}.
* *
* After initialization is complete, the provided {@code SpineController} is invoked as per the {@link SpineController} semantics, to allow * After initialization is complete, the provided {@code SpineController} is invoked as per the {@link SpineController}
* modifying how the skeleton inside the widget is animated and rendered. * semantics, to allow modifying how the skeleton inside the widget is animated and rendered. */
*/ public SpineView build () {
public SpineView build() {
SpineView spineView = new SpineView(context, controller); SpineView spineView = new SpineView(context, controller);
spineView.boundsProvider = boundsProvider; spineView.boundsProvider = boundsProvider;
spineView.alignment = alignment; spineView.alignment = alignment;
@ -198,201 +180,162 @@ public class SpineView extends View implements Choreographer.FrameCallback {
private Alignment alignment = Alignment.CENTER; private Alignment alignment = Alignment.CENTER;
private ContentMode contentMode = ContentMode.FIT; private ContentMode contentMode = ContentMode.FIT;
/** /** Constructs a new {@link SpineView}.
* Constructs a new {@link SpineView}.
* *
* After initialization is complete, the provided {@code SpineController} is invoked as per the {@link SpineController} semantics, to allow * After initialization is complete, the provided {@code SpineController} is invoked as per the {@link SpineController}
* modifying how the skeleton inside the widget is animated and rendered. * semantics, to allow modifying how the skeleton inside the widget is animated and rendered. */
*/
public SpineView (Context context, SpineController controller) { public SpineView (Context context, SpineController controller) {
super(context); super(context);
this.controller = controller; this.controller = controller;
} }
/** /** Constructs a new {@link SpineView} without providing a {@link SpineController}, which you need to provide using
* Constructs a new {@link SpineView} without providing a {@link SpineController}, which you need to provide using * {@link SpineView#setController(SpineController)}. */
* {@link SpineView#setController(SpineController)}.
*/
public SpineView (Context context, AttributeSet attrs) { public SpineView (Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
// Set properties by view id // Set properties by view id
} }
/** /** Constructs a new {@link SpineView} without providing a {@link SpineController}, which you need to provide using
* Constructs a new {@link SpineView} without providing a {@link SpineController}, which you need to provide using * {@link SpineView#setController(SpineController)}. */
* {@link SpineView#setController(SpineController)}.
*/
public SpineView (Context context, AttributeSet attrs, int defStyle) { public SpineView (Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle); super(context, attrs, defStyle);
// Set properties by view id // Set properties by view id
} }
/** /** Constructs a new {@link SpineView} from files in your app assets. The {@code atlasFileName} specifies the `.atlas` file to
* Constructs a new {@link SpineView} from files in your app assets. The {@code atlasFileName} specifies the * be loaded for the images used to render the skeleton. The {@code skeletonFileName} specifies either a Skeleton `.json` or
* `.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. * `.skel` file containing the skeleton data.
* *
* After initialization is complete, the provided {@code controller} is invoked as per the {@link SpineController} semantics, to allow * After initialization is complete, the provided {@code controller} is invoked as per the {@link SpineController} semantics,
* modifying how the skeleton inside the widget is animated and rendered. * to allow modifying how the skeleton inside the widget is animated and rendered. */
*/ public static SpineView loadFromAssets (String atlasFileName, String skeletonFileName, Context context,
public static SpineView loadFromAssets(String atlasFileName, String skeletonFileName, Context context, SpineController controller) { SpineController controller) {
SpineView spineView = new SpineView(context, controller); SpineView spineView = new SpineView(context, controller);
spineView.loadFromAsset(atlasFileName, skeletonFileName); spineView.loadFromAsset(atlasFileName, skeletonFileName);
return spineView; return spineView;
} }
/** /** Constructs a new {@link SpineView} from files. The {@code atlasFile} specifies the `.atlas` file to be loaded for the
* Constructs a new {@link SpineView} from files. The {@code atlasFile} specifies the `.atlas` file to be loaded for the images used to render * images used to render the skeleton. The {@code skeletonFile} specifies either a Skeleton `.json` or `.skel` file containing
* the skeleton. The {@code skeletonFile} specifies either a Skeleton `.json` or `.skel` file containing the skeleton data. * the skeleton data.
* *
* After initialization is complete, the provided {@code SpineController} is invoked as per the {@link SpineController} semantics, to allow * After initialization is complete, the provided {@code SpineController} is invoked as per the {@link SpineController}
* modifying how the skeleton inside the widget is animated and rendered. * 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) {
public static SpineView loadFromFile(File atlasFile, File skeletonFile, Context context, SpineController controller) {
SpineView spineView = new SpineView(context, controller); SpineView spineView = new SpineView(context, controller);
spineView.loadFromFile(atlasFile, skeletonFile); spineView.loadFromFile(atlasFile, skeletonFile);
return spineView; return spineView;
} }
/** /** Constructs a new {@link SpineView} from HTTP URLs. The {@code atlasUrl} specifies the `.atlas` url to be loaded for the
* Constructs a new {@link SpineView} from HTTP URLs. The {@code atlasUrl} specifies the `.atlas` url to be loaded for the images used to render * images used to render the skeleton. The {@code skeletonUrl} specifies either a Skeleton `.json` or `.skel` url containing
* the skeleton. The {@code skeletonUrl} specifies either a Skeleton `.json` or `.skel` url containing the skeleton data. * the skeleton data.
* *
* After initialization is complete, the provided {@code SpineController} is invoked as per the {@link SpineController} semantics, to allow * After initialization is complete, the provided {@code SpineController} is invoked as per the {@link SpineController}
* modifying how the skeleton inside the widget is animated and rendered. * 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,
public static SpineView loadFromHttp(URL atlasUrl, URL skeletonUrl, File targetDirectory, Context context, SpineController controller) { SpineController controller) {
SpineView spineView = new SpineView(context, controller); SpineView spineView = new SpineView(context, controller);
spineView.loadFromHttp(atlasUrl, skeletonUrl, targetDirectory); spineView.loadFromHttp(atlasUrl, skeletonUrl, targetDirectory);
return spineView; return spineView;
} }
/** /** Constructs a new {@link SpineView} from a {@link AndroidSkeletonDrawable}.
* 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 * After initialization is complete, the provided {@code SpineController} is invoked as per the {@link SpineController}
* modifying how the skeleton inside the widget is animated and rendered. * semantics, to allow modifying how the skeleton inside the widget is animated and rendered. */
*/ public static SpineView loadFromDrawable (AndroidSkeletonDrawable drawable, Context context, SpineController controller) {
public static SpineView loadFromDrawable(AndroidSkeletonDrawable drawable, Context context, SpineController controller) {
SpineView spineView = new SpineView(context, controller); SpineView spineView = new SpineView(context, controller);
spineView.loadFromDrawable(drawable); spineView.loadFromDrawable(drawable);
return spineView; return spineView;
} }
/** /** The same as {@link SpineView#loadFromAssets(String, String, Context, SpineController)}, but can be used after instantiating
* The same as {@link SpineView#loadFromAssets(String, String, Context, SpineController)}, but can be used after * the view via {@link SpineView#SpineView(Context, SpineController)}. */
* instantiating the view via {@link SpineView#SpineView(Context, SpineController)}. public void loadFromAsset (String atlasFileName, String skeletonFileName) {
*/ loadFrom( () -> AndroidSkeletonDrawable.fromAsset(atlasFileName, skeletonFileName, getContext()));
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
* The same as {@link SpineView#loadFromFile(File, File, Context, SpineController)}, but can be used after * view via {@link SpineView#SpineView(Context, SpineController)}. */
* instantiating the view via {@link SpineView#SpineView(Context, SpineController)}. public void loadFromFile (File atlasFile, File skeletonFile) {
*/ loadFrom( () -> AndroidSkeletonDrawable.fromFile(atlasFile, skeletonFile));
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 same as {@link SpineView#loadFromHttp(URL, URL, File, Context, SpineController)}, but can be used after * the view via {@link SpineView#SpineView(Context, SpineController)}. */
* instantiating the view via {@link SpineView#SpineView(Context, SpineController)}. public void loadFromHttp (URL atlasUrl, URL skeletonUrl, File targetDirectory) {
*/ loadFrom( () -> AndroidSkeletonDrawable.fromHttp(atlasUrl, skeletonUrl, targetDirectory));
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
* The same as {@link SpineView#loadFromDrawable(AndroidSkeletonDrawable, Context, SpineController)}, but can be used after * instantiating the view via {@link SpineView#SpineView(Context, SpineController)}. */
* instantiating the view via {@link SpineView#SpineView(Context, SpineController)}. public void loadFromDrawable (AndroidSkeletonDrawable drawable) {
*/ loadFrom( () -> drawable);
public void loadFromDrawable(AndroidSkeletonDrawable drawable) {
loadFrom(() -> drawable);
} }
/** /** Get the {@link SpineController} */
* Get the {@link SpineController} public SpineController getController () {
*/
public SpineController getController() {
return controller; return controller;
} }
/** /** Set the {@link SpineController}. Only do this if you use {@link SpineView#SpineView(Context, AttributeSet)},
* 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. */
* {@link SpineView#SpineView(Context, AttributeSet, int)}, or create the {@link SpineView} in an XML layout. public void setController (SpineController controller) {
*/
public void setController(SpineController controller) {
this.controller = controller; this.controller = controller;
} }
/** /** Get the {@link Alignment} used to align the {@link Skeleton} inside the view. The default is {@link Alignment#CENTER} */
* Get the {@link Alignment} used to align the {@link Skeleton} inside the view. public Alignment getAlignment () {
* The default is {@link Alignment#CENTER}
*/
public Alignment getAlignment() {
return alignment; return alignment;
} }
/** /** Set the {@link Alignment}. */
* Set the {@link Alignment}. public void setAlignment (Alignment alignment) {
*/
public void setAlignment(Alignment alignment) {
this.alignment = alignment; this.alignment = alignment;
updateCanvasTransform(); updateCanvasTransform();
} }
/** /** Get the {@link ContentMode} used to fit the {@link Skeleton} inside the view. The default is {@link ContentMode#FIT}. */
* Get the {@link ContentMode} used to fit the {@link Skeleton} inside the view. public ContentMode getContentMode () {
* The default is {@link ContentMode#FIT}.
*/
public ContentMode getContentMode() {
return contentMode; return contentMode;
} }
/** /** Set the {@link ContentMode}. */
* Set the {@link ContentMode}. public void setContentMode (ContentMode contentMode) {
*/
public void setContentMode(ContentMode contentMode) {
this.contentMode = contentMode; this.contentMode = contentMode;
updateCanvasTransform(); updateCanvasTransform();
} }
/** /** Get the {@link BoundsProvider} used to compute the bounds of the {@link Skeleton} inside the view. The default is
* Get the {@link BoundsProvider} used to compute the bounds of the {@link Skeleton} inside the view. * {@link SetupPoseBounds}. */
* The default is {@link SetupPoseBounds}. public BoundsProvider getBoundsProvider () {
*/
public BoundsProvider getBoundsProvider() {
return boundsProvider; return boundsProvider;
} }
/** /** Set the {@link BoundsProvider}. */
* Set the {@link BoundsProvider}. public void setBoundsProvider (BoundsProvider boundsProvider) {
*/
public void setBoundsProvider(BoundsProvider boundsProvider) {
this.boundsProvider = boundsProvider; this.boundsProvider = boundsProvider;
updateCanvasTransform(); updateCanvasTransform();
} }
/** /** Check if rendering is enabled. */
* Check if rendering is enabled. public Boolean isRendering () {
*/
public Boolean isRendering() {
return rendering; 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
* Set to disable or enable rendering. Disable it when the spine view is out of bounds and you want to preserve CPU/GPU resources. * resources. */
*/ public void setRendering (Boolean rendering) {
public void setRendering(Boolean rendering) {
this.rendering = rendering; this.rendering = rendering;
} }
private void loadFrom(AndroidSkeletonDrawableLoader loader) { private void loadFrom (AndroidSkeletonDrawableLoader loader) {
Handler mainHandler = new Handler(Looper.getMainLooper()); Handler mainHandler = new Handler(Looper.getMainLooper());
Thread backgroundThread = new Thread(() -> { Thread backgroundThread = new Thread( () -> {
final AndroidSkeletonDrawable skeletonDrawable = loader.load(); final AndroidSkeletonDrawable skeletonDrawable = loader.load();
mainHandler.post(() -> { mainHandler.post( () -> {
computedBounds = boundsProvider.computeBounds(skeletonDrawable); computedBounds = boundsProvider.computeBounds(skeletonDrawable);
updateCanvasTransform(); updateCanvasTransform();
@ -431,28 +374,30 @@ public class SpineView extends View implements Choreographer.FrameCallback {
} }
@Override @Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) { protected void onSizeChanged (int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh); super.onSizeChanged(w, h, oldw, oldh);
updateCanvasTransform(); updateCanvasTransform();
} }
private void updateCanvasTransform() { private void updateCanvasTransform () {
if (controller == null) { if (controller == null) {
return; return;
} }
x = (float) (-computedBounds.getX() - computedBounds.getWidth() / 2.0 - (alignment.getX() * computedBounds.getWidth() / 2.0)); x = (float)(-computedBounds.getX() - computedBounds.getWidth() / 2.0
y = (float) (-computedBounds.getY() - computedBounds.getHeight() / 2.0 - (alignment.getY() * computedBounds.getHeight() / 2.0)); - (alignment.getX() * computedBounds.getWidth() / 2.0));
y = (float)(-computedBounds.getY() - computedBounds.getHeight() / 2.0
- (alignment.getY() * computedBounds.getHeight() / 2.0));
switch (contentMode) { switch (contentMode) {
case FIT: case FIT:
scaleX = scaleY = (float) Math.min(getWidth() / computedBounds.getWidth(), getHeight() / computedBounds.getHeight()); scaleX = scaleY = (float)Math.min(getWidth() / computedBounds.getWidth(), getHeight() / computedBounds.getHeight());
break; break;
case FILL: case FILL:
scaleX = scaleY = (float) Math.max(getWidth() / computedBounds.getWidth(), getHeight() / computedBounds.getHeight()); scaleX = scaleY = (float)Math.max(getWidth() / computedBounds.getWidth(), getHeight() / computedBounds.getHeight());
break; break;
} }
offsetX = (float) (getWidth() / 2.0 + (alignment.getX() * getWidth() / 2.0)); offsetX = (float)(getWidth() / 2.0 + (alignment.getX() * getWidth() / 2.0));
offsetY = (float) (getHeight() / 2.0 + (alignment.getY() * getHeight() / 2.0)); offsetY = (float)(getHeight() / 2.0 + (alignment.getY() * getHeight() / 2.0));
controller.setCoordinateTransform(x + offsetX / scaleX, y + offsetY / scaleY, scaleX, scaleY); controller.setCoordinateTransform(x + offsetX / scaleX, y + offsetY / scaleY, scaleX, scaleY);
} }

View File

@ -29,33 +29,24 @@
package com.esotericsoftware.spine.android.bounds; package com.esotericsoftware.spine.android.bounds;
/** /** How a view should be aligned within another view. */
* How a view should be aligned within another view.
*/
public enum Alignment { public enum Alignment {
TOP_LEFT(-1.0f, -1.0f), 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,
TOP_CENTER(0.0f, -1.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);
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 x;
private final float y; private final float y;
Alignment(float x, float y) { Alignment (float x, float y) {
this.x = x; this.x = x;
this.y = y; this.y = y;
} }
public float getX() { public float getX () {
return x; return x;
} }
public float getY() { public float getY () {
return y; return y;
} }
} }

View File

@ -33,72 +33,69 @@ import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.FloatArray; import com.badlogic.gdx.utils.FloatArray;
import com.esotericsoftware.spine.Skeleton; 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}. */
* Bounds denoted by the top left corner coordinates {@code x} and {@code y}
* and the {@code width} and {@code height}.
*/
public class Bounds { public class Bounds {
private double x; private double x;
private double y; private double y;
private double width; private double width;
private double height; private double height;
public Bounds() { public Bounds () {
this.x = 0; this.x = 0;
this.y = 0; this.y = 0;
this.width = 0; this.width = 0;
this.height = 0; this.height = 0;
} }
public Bounds(double x, double y, double width, double height) { public Bounds (double x, double y, double width, double height) {
this.x = x; this.x = x;
this.y = y; this.y = y;
this.width = width; this.width = width;
this.height = height; this.height = height;
} }
public Bounds(Skeleton skeleton) { public Bounds (Skeleton skeleton) {
Vector2 offset = new Vector2(0, 0); Vector2 offset = new Vector2(0, 0);
Vector2 size = new Vector2(0, 0); Vector2 size = new Vector2(0, 0);
FloatArray floatArray = new FloatArray(); FloatArray floatArray = new FloatArray();
skeleton.getBounds(offset, size, floatArray); skeleton.getBounds(offset, size, floatArray);
x = offset.x; x = offset.x;
y = offset.y; y = offset.y;
width = size.x; width = size.x;
height = size.y; height = size.y;
} }
public double getX() { public double getX () {
return x; return x;
} }
public void setX(double x) { public void setX (double x) {
this.x = x; this.x = x;
} }
public double getY() { public double getY () {
return y; return y;
} }
public void setY(double y) { public void setY (double y) {
this.y = y; this.y = y;
} }
public double getWidth() { public double getWidth () {
return width; return width;
} }
public void setWidth(double width) { public void setWidth (double width) {
this.width = width; this.width = width;
} }
public double getHeight() { public double getHeight () {
return height; return height;
} }
public void setHeight(double height) { public void setHeight (double height) {
this.height = height; this.height = height;
} }
} }

View File

@ -31,10 +31,8 @@ package com.esotericsoftware.spine.android.bounds;
import com.esotericsoftware.spine.android.AndroidSkeletonDrawable; 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
* A {@link BoundsProvider} that calculates the bounding box of the skeleton based on the visible * pose. */
* attachments in the setup pose.
*/
public interface BoundsProvider { public interface BoundsProvider {
Bounds computeBounds(AndroidSkeletonDrawable drawable); Bounds computeBounds (AndroidSkeletonDrawable drawable);
} }

View File

@ -29,16 +29,10 @@
package com.esotericsoftware.spine.android.bounds; package com.esotericsoftware.spine.android.bounds;
/** /** How a view should be inscribed into another view. */
* How a view should be inscribed into another view.
*/
public enum ContentMode { public enum ContentMode {
/** /** As large as possible while still containing the source view entirely within the target view. */
* 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. */
FIT, FILL
/**
* Fill the target view by distorting the source's aspect ratio.
*/
FILL
} }

View File

@ -31,24 +31,22 @@ package com.esotericsoftware.spine.android.bounds;
import com.esotericsoftware.spine.android.AndroidSkeletonDrawable; import com.esotericsoftware.spine.android.AndroidSkeletonDrawable;
/** /** A {@link BoundsProvider} that returns fixed bounds. */
* A {@link BoundsProvider} that returns fixed bounds.
*/
public class RawBounds implements BoundsProvider { public class RawBounds implements BoundsProvider {
final Double x; final Double x;
final Double y; final Double y;
final Double width; final Double width;
final Double height; final Double height;
public RawBounds(Double x, Double y, Double width, Double height) { public RawBounds (Double x, Double y, Double width, Double height) {
this.x = x; this.x = x;
this.y = y; this.y = y;
this.width = width; this.width = width;
this.height = height; this.height = height;
} }
@Override @Override
public Bounds computeBounds(AndroidSkeletonDrawable drawable) { public Bounds computeBounds (AndroidSkeletonDrawable drawable) {
return new Bounds(x, y, width, height); return new Bounds(x, y, width, height);
} }
} }

View File

@ -31,14 +31,12 @@ package com.esotericsoftware.spine.android.bounds;
import com.esotericsoftware.spine.android.AndroidSkeletonDrawable; 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
* A {@link BoundsProvider} that calculates the bounding box of the skeleton based on the visible * pose. */
* attachments in the setup pose.
*/
public class SetupPoseBounds implements BoundsProvider { public class SetupPoseBounds implements BoundsProvider {
@Override @Override
public Bounds computeBounds(AndroidSkeletonDrawable drawable) { public Bounds computeBounds (AndroidSkeletonDrawable drawable) {
return new Bounds(drawable.getSkeleton()); return new Bounds(drawable.getSkeleton());
} }
} }

View File

@ -37,86 +37,75 @@ import com.esotericsoftware.spine.android.AndroidSkeletonDrawable;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
/** /** A {@link BoundsProvider} that calculates the bounding box needed for a combination of skins and an animation. */
* A {@link BoundsProvider} that calculates the bounding box needed for a combination of skins
* and an animation.
*/
public class SkinAndAnimationBounds implements BoundsProvider { public class SkinAndAnimationBounds implements BoundsProvider {
private final List<String> skins; private final List<String> skins;
private final String animation; private final String animation;
private final double stepTime; 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
* Constructs a new provider that will use the given {@code skins} and {@code animation} to calculate * skeleton. If no skins are given, the default skin is used. The {@code stepTime}, given in seconds, defines at what interval
* the bounding box of the skeleton. If no skins are given, the default skin is used. * the bounds should be sampled across the entire animation. */
* The {@code stepTime}, given in seconds, defines at what interval the bounds should be sampled public SkinAndAnimationBounds (List<String> skins, String animation, double stepTime) {
* across the entire animation. this.skins = (skins == null || skins.isEmpty()) ? Collections.singletonList("default") : skins;
*/ this.animation = animation;
public SkinAndAnimationBounds(List<String> skins, String animation, double stepTime) { this.stepTime = 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
* Constructs a new provider that will use the given {@code skins} and {@code animation} to calculate * skeleton. If no skins are given, the default skin is used. The {@code stepTime} has default value 0.1. */
* the bounding box of the skeleton. If no skins are given, the default skin is used. public SkinAndAnimationBounds (List<String> skins, String animation) {
* The {@code stepTime} has default value 0.1. this(skins, animation, 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
* Constructs a new provider that will use the given {@code skins} and {@code animation} to calculate * skeleton. The default skin is used. The {@code stepTime} has default value 0.1. */
* 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);
public SkinAndAnimationBounds(String animation) { }
this(Collections.emptyList(), animation, 0.1);
}
@Override @Override
public Bounds computeBounds(AndroidSkeletonDrawable drawable) { public Bounds computeBounds (AndroidSkeletonDrawable drawable) {
SkeletonData data = drawable.getSkeletonData(); SkeletonData data = drawable.getSkeletonData();
Skin oldSkin = drawable.getSkeleton().getSkin(); Skin oldSkin = drawable.getSkeleton().getSkin();
Skin customSkin = new Skin("custom-skin"); Skin customSkin = new Skin("custom-skin");
for (String skinName : skins) { for (String skinName : skins) {
Skin skin = data.findSkin(skinName); Skin skin = data.findSkin(skinName);
if (skin == null) continue; if (skin == null) continue;
customSkin.addSkin(skin); customSkin.addSkin(skin);
} }
drawable.getSkeleton().setSkin(customSkin); drawable.getSkeleton().setSkin(customSkin);
drawable.getSkeleton().setToSetupPose(); drawable.getSkeleton().setToSetupPose();
Animation animation = (this.animation != null) ? data.findAnimation(this.animation) : null; Animation animation = (this.animation != null) ? data.findAnimation(this.animation) : null;
double minX = Double.POSITIVE_INFINITY; double minX = Double.POSITIVE_INFINITY;
double minY = Double.POSITIVE_INFINITY; double minY = Double.POSITIVE_INFINITY;
double maxX = Double.NEGATIVE_INFINITY; double maxX = Double.NEGATIVE_INFINITY;
double maxY = Double.NEGATIVE_INFINITY; double maxY = Double.NEGATIVE_INFINITY;
if (animation == null) { if (animation == null) {
Bounds bounds = new Bounds(drawable.getSkeleton()); Bounds bounds = new Bounds(drawable.getSkeleton());
minX = bounds.getX(); minX = bounds.getX();
minY = bounds.getY(); minY = bounds.getY();
maxX = minX + bounds.getWidth(); maxX = minX + bounds.getWidth();
maxY = minY + bounds.getHeight(); maxY = minY + bounds.getHeight();
} else { } else {
drawable.getAnimationState().setAnimation(0, animation, false); drawable.getAnimationState().setAnimation(0, animation, false);
int steps = (int) Math.max( (animation.getDuration() / stepTime), 1.0); int steps = (int)Math.max((animation.getDuration() / stepTime), 1.0);
for (int i = 0; i < steps; i++) { for (int i = 0; i < steps; i++) {
drawable.update(i > 0 ? (float) stepTime : 0); drawable.update(i > 0 ? (float)stepTime : 0);
Bounds bounds = new Bounds(drawable.getSkeleton()); Bounds bounds = new Bounds(drawable.getSkeleton());
minX = Math.min(minX, bounds.getX()); minX = Math.min(minX, bounds.getX());
minY = Math.min(minY, bounds.getY()); minY = Math.min(minY, bounds.getY());
maxX = Math.max(maxX, minX + bounds.getWidth()); maxX = Math.max(maxX, minX + bounds.getWidth());
maxY = Math.max(maxY, minY + bounds.getHeight()); maxY = Math.max(maxY, minY + bounds.getHeight());
} }
} }
drawable.getSkeleton().setSkin("default"); drawable.getSkeleton().setSkin("default");
drawable.getAnimationState().clearTracks(); drawable.getAnimationState().clearTracks();
if (oldSkin != null) drawable.getSkeleton().setSkin(oldSkin); if (oldSkin != null) drawable.getSkeleton().setSkin(oldSkin);
drawable.getSkeleton().setToSetupPose(); drawable.getSkeleton().setToSetupPose();
drawable.update(0); drawable.update(0);
return new Bounds(minX, minY, maxX - minX, maxY - minY); return new Bounds(minX, minY, maxX - minX, maxY - minY);
} }
} }

View File

@ -33,5 +33,5 @@ import com.esotericsoftware.spine.android.AndroidSkeletonDrawable;
@FunctionalInterface @FunctionalInterface
public interface AndroidSkeletonDrawableLoader { public interface AndroidSkeletonDrawableLoader {
AndroidSkeletonDrawable load(); AndroidSkeletonDrawable load ();
} }

View File

@ -39,5 +39,5 @@ import java.util.List;
@FunctionalInterface @FunctionalInterface
public interface SpineControllerAfterPaintCallback { public interface SpineControllerAfterPaintCallback {
void execute (SpineController controller, Canvas canvas, Array<SkeletonRenderer.RenderCommand> commands); void execute (SpineController controller, Canvas canvas, Array<SkeletonRenderer.RenderCommand> commands);
} }

View File

@ -38,5 +38,5 @@ import java.util.List;
@FunctionalInterface @FunctionalInterface
public interface SpineControllerBeforePaintCallback { public interface SpineControllerBeforePaintCallback {
void execute (SpineController controller, Canvas canvas); void execute (SpineController controller, Canvas canvas);
} }

View File

@ -33,5 +33,5 @@ import com.esotericsoftware.spine.android.SpineController;
@FunctionalInterface @FunctionalInterface
public interface SpineControllerCallback { public interface SpineControllerCallback {
void execute (SpineController controller); void execute (SpineController controller);
} }

View File

@ -41,73 +41,68 @@ import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.nio.file.Files; import java.nio.file.Files;
/** /** Helper to load http resources. */
* Helper to load http resources.
*/
public class HttpUtils { 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
* Download a file from an url into a target directory. It keeps the name from the {@code url}. * the main run loop. */
* This should NOT be executed on the main run loop. public static File downloadFrom (URL url, File targetDirectory) throws RuntimeException {
*/ HttpURLConnection urlConnection = null;
public static File downloadFrom(URL url, File targetDirectory) throws RuntimeException { InputStream inputStream = null;
HttpURLConnection urlConnection = null; OutputStream outputStream = null;
InputStream inputStream = null;
OutputStream outputStream = null;
try { try {
urlConnection = (HttpURLConnection) url.openConnection(); urlConnection = (HttpURLConnection)url.openConnection();
urlConnection.connect(); urlConnection.connect();
if (urlConnection.getResponseCode() != HttpURLConnection.HTTP_OK) { if (urlConnection.getResponseCode() != HttpURLConnection.HTTP_OK) {
throw new RuntimeException("Failed to connect: HTTP response code " + urlConnection.getResponseCode()); throw new RuntimeException("Failed to connect: HTTP response code " + urlConnection.getResponseCode());
} }
inputStream = new BufferedInputStream(urlConnection.getInputStream()); inputStream = new BufferedInputStream(urlConnection.getInputStream());
String atlasUrlPath = url.getPath(); String atlasUrlPath = url.getPath();
String fileName = atlasUrlPath.substring(atlasUrlPath.lastIndexOf('/') + 1); String fileName = atlasUrlPath.substring(atlasUrlPath.lastIndexOf('/') + 1);
File file = new File(targetDirectory, fileName); File file = new File(targetDirectory, fileName);
// Create an OutputStream to write to the file // Create an OutputStream to write to the file
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
outputStream = Files.newOutputStream(file.toPath()); outputStream = Files.newOutputStream(file.toPath());
} else { } else {
//noinspection IOStreamConstructor // noinspection IOStreamConstructor
outputStream = new FileOutputStream(file); outputStream = new FileOutputStream(file);
} }
byte[] buffer = new byte[1024]; byte[] buffer = new byte[1024];
int bytesRead; int bytesRead;
// Write the input stream to the output stream // Write the input stream to the output stream
while ((bytesRead = inputStream.read(buffer)) != -1) { while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead); outputStream.write(buffer, 0, bytesRead);
} }
return file; return file;
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} finally { } finally {
if (outputStream != null) { if (outputStream != null) {
try { try {
outputStream.flush(); outputStream.flush();
outputStream.close(); outputStream.close();
} catch (IOException e) { } catch (IOException e) {
// Nothing we can do // Nothing we can do
} }
} }
if (inputStream != null) { if (inputStream != null) {
try { try {
inputStream.close(); inputStream.close();
} catch (IOException e) { } catch (IOException e) {
// Nothing we can do // Nothing we can do
} }
} }
if (urlConnection != null) { if (urlConnection != null) {
urlConnection.disconnect(); urlConnection.disconnect();
} }
} }
} }
} }

View File

@ -46,63 +46,57 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URL; import java.net.URL;
/** /** Helper to load {@link SkeletonData} from assets. */
* Helper to load {@link SkeletonData} from assets.
*/
public class SkeletonDataUtils { public class SkeletonDataUtils {
/** /** Loads a {@link SkeletonData} from the file {@code skeletonFile} in assets using {@link Context}. Uses the provided
* Loads a {@link SkeletonData} from the file {@code skeletonFile} in assets using {@link Context}. * {@link AndroidTextureAtlas} to resolve attachment images.
* Uses the provided {@link AndroidTextureAtlas} to resolve attachment images. *
* * Throws a {@link RuntimeException} in case the skeleton data could not be loaded. */
* 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);
public static SkeletonData fromAsset(AndroidTextureAtlas atlas, String skeletonFileName, Context context) {
AndroidAtlasAttachmentLoader attachmentLoader = new AndroidAtlasAttachmentLoader(atlas);
SkeletonLoader skeletonLoader; SkeletonLoader skeletonLoader;
if (skeletonFileName.endsWith(".json")) { if (skeletonFileName.endsWith(".json")) {
skeletonLoader = new SkeletonJson(attachmentLoader); skeletonLoader = new SkeletonJson(attachmentLoader);
} else { } else {
skeletonLoader = new SkeletonBinary(attachmentLoader); skeletonLoader = new SkeletonBinary(attachmentLoader);
} }
SkeletonData skeletonData; SkeletonData skeletonData;
AssetManager assetManager = context.getAssets(); AssetManager assetManager = context.getAssets();
try (InputStream in = new BufferedInputStream(assetManager.open(skeletonFileName))) { try (InputStream in = new BufferedInputStream(assetManager.open(skeletonFileName))) {
skeletonData = skeletonLoader.readSkeletonData(in); skeletonData = skeletonLoader.readSkeletonData(in);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
return skeletonData; return skeletonData;
} }
/** /** Loads a {@link SkeletonData} from the file {@code skeletonFile}. Uses the provided {@link AndroidTextureAtlas} to resolve
* Loads a {@link SkeletonData} from the file {@code skeletonFile}. Uses the provided {@link AndroidTextureAtlas} to resolve attachment images. * attachment images.
* *
* Throws a {@link RuntimeException} in case the skeleton data could not be loaded. * Throws a {@link RuntimeException} in case the skeleton data could not be loaded. */
*/ public static SkeletonData fromFile (AndroidTextureAtlas atlas, File skeletonFile) {
public static SkeletonData fromFile(AndroidTextureAtlas atlas, File skeletonFile) { AndroidAtlasAttachmentLoader attachmentLoader = new AndroidAtlasAttachmentLoader(atlas);
AndroidAtlasAttachmentLoader attachmentLoader = new AndroidAtlasAttachmentLoader(atlas);
SkeletonLoader skeletonLoader; SkeletonLoader skeletonLoader;
if (skeletonFile.getPath().endsWith(".json")) { if (skeletonFile.getPath().endsWith(".json")) {
skeletonLoader = new SkeletonJson(attachmentLoader); skeletonLoader = new SkeletonJson(attachmentLoader);
} else { } else {
skeletonLoader = new SkeletonBinary(attachmentLoader); skeletonLoader = new SkeletonBinary(attachmentLoader);
} }
return skeletonLoader.readSkeletonData(new FileHandle(skeletonFile)); return skeletonLoader.readSkeletonData(new FileHandle(skeletonFile));
} }
/** /** Loads a {@link SkeletonData} from the URL {@code skeletonURL}. Uses the provided {@link AndroidTextureAtlas} to resolve
* Loads a {@link SkeletonData} from the URL {@code skeletonURL}. Uses the provided {@link AndroidTextureAtlas} to resolve attachment images. * attachment images.
* *
* Throws a {@link RuntimeException} in case the skeleton data could not be loaded. * Throws a {@link RuntimeException} in case the skeleton data could not be loaded. */
*/ public static SkeletonData fromHttp (AndroidTextureAtlas atlas, URL skeletonUrl, File targetDirectory) {
public static SkeletonData fromHttp(AndroidTextureAtlas atlas, URL skeletonUrl, File targetDirectory) { File skeletonFile = HttpUtils.downloadFrom(skeletonUrl, targetDirectory);
File skeletonFile = HttpUtils.downloadFrom(skeletonUrl, targetDirectory); return fromFile(atlas, skeletonFile);
return fromFile(atlas, skeletonFile); }
}
} }

View File

@ -1,17 +1,16 @@
package com.esotericsoftware.android; package com.esotericsoftware.android;
import org.junit.Test; import org.junit.Test;
import static org.junit.Assert.*; import static org.junit.Assert.*;
/** /** Example local unit test, which will execute on the development machine (host).
* Example local unit test, which will execute on the development machine (host).
* *
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a> * @see <a href="http://d.android.com/tools/testing">Testing documentation</a> */
*/
public class ExampleUnitTest { public class ExampleUnitTest {
@Test @Test
public void addition_isCorrect() { public void addition_isCorrect () {
assertEquals(4, 2 + 2); assertEquals(4, 2 + 2);
} }
} }

View File

@ -95,7 +95,7 @@ public class Slot {
return color; return color;
} }
public void setColor(Color color) { public void setColor (Color color) {
this.color = color; this.color = color;
} }