package com.github.czyzby.lml.scene2d.ui.reflected;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.scenes.scene2d.ui.Image;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.scenes.scene2d.utils.Drawable;
import com.badlogic.gdx.utils.Array;
import com.github.czyzby.kiwi.util.gdx.collection.GdxArrays;
/** Extends the default {@link Image} with animation functionality. Allows to display multiple {@link Drawable}s one
* after another in the selected manner. The order of images can be modified with {@link #setBackwards(boolean)} and
* {@link #setBouncing(boolean)}. If you want the animation to play once and then make the widget work as a regular
* image, use {@link #setPlayOnce(boolean)}. You can affect the speed of animation with {@link #setDelay(float)} and
* {@link #setMaxDelay(float)} methods. All standard {@link Image} methods are supported. Does NOT use
* {@link com.badlogic.gdx.graphics.g2d.Animation} internally, as it relies on texture regions rather than drawables.
*
* <p>
* This class supports pretty simple animations, without frames with different delays etc. If you need complex
* animations, you might prefer using actions API or a custom actor implementation.
*
* @author MJ
* @see Image */
public class AnimatedImage extends Image {
private Array<Drawable> frames;
private int currentFrame;
private float lastUpdate;
private float delay = 0.25f;
private float maxDelay = 1f;
private boolean bouncing;
private boolean backwards;
private boolean playOnce;
/** @param skin contains drawables.
* @param frameNames will be converted to drawables from the skin and stored in an {@link Array} used internally by
* the image. All frame names have to be valid and present in the skin. */
public AnimatedImage(final Skin skin, final String... frameNames) {
this(toFrames(skin, frameNames));
}
/** @param frames will be used to construct an {@link Array} of animations's frames. Cannot contain nulls. */
public AnimatedImage(final Drawable... frames) {
this(GdxArrays.newArray(frames));
}
/** @param frames if not an {@link Array}, will be converted to {@link Array} and used as animation's frames. Cannot
* contain nulls. */
public AnimatedImage(final Iterable<Drawable> frames) {
this(toArray(frames));
}
/** @param frames will be displayed in the chosen manner. Will be used internally by the image. Can be modified
* externally, but make sure to call {@link #validateCurrentFrame()} after any modification. Cannot
* contain nulls. */
public AnimatedImage(final Array<Drawable> frames) {
this.frames = frames;
if (GdxArrays.isNotEmpty(frames)) {
setDrawable(frames.first());
} else if (frames == null) {
this.frames = GdxArrays.newArray();
}
}
protected static Array<Drawable> toFrames(final Skin skin, final String[] frameNames) {
final Array<Drawable> frames = GdxArrays.newArray();
for (final String frame : frameNames) {
frames.add(skin.getDrawable(frame));
}
return frames;
}
protected static Array<Drawable> toArray(final Iterable<Drawable> frames) {
return frames instanceof Array<?> ? (Array<Drawable>) frames : GdxArrays.newArray(frames);
}
@Override
public void act(final float delta) {
if (frames.size > 1) {
lastUpdate += Math.min(delta, maxDelay);
while (lastUpdate >= delay) {
lastUpdate -= delay;
updateFrame();
}
}
super.act(delta);
}
private void updateFrame() {
if (backwards) {
if (--currentFrame < 0) {
if (playOnce) {
currentFrame = 0;
final Drawable frame = frames.get(currentFrame);
frames = GdxArrays.newArray(frame);
} else if (bouncing) {
currentFrame = Math.min(1, frames.size - 1);
backwards = false;
} else {
currentFrame = frames.size - 1;
}
}
} else if (++currentFrame >= frames.size) {
if (playOnce) {
final Drawable frame = frames.get(frames.size - 1);
currentFrame = 0;
frames = GdxArrays.newArray(frame);
} else if (bouncing) {
currentFrame = Math.max(0, frames.size - 2);
backwards = true;
} else {
currentFrame = 0;
}
}
setDrawable(frames.get(currentFrame));
}
/** @param backwards if true, frames will be iterated over from the end. Note that if the animation is bouncing,
* this setting will be overridden. Note that current frame index points to 0 (first) frame at the
* beginning; if you want to start with the last frame, make sure to {@link #setCurrentFrame(int)}. */
public void setBackwards(final boolean backwards) {
this.backwards = backwards;
}
/** @return if true, frames are currently iterated from the end. This value might change if the animation is
* bouncing. */
public boolean isBackwards() {
return backwards;
}
/** @param bouncing if true, the animation will go from the beginning to the end and then from the end to the
* beginning - over and over, in an endless loop. */
public void setBouncing(final boolean bouncing) {
this.bouncing = bouncing;
}
/** @return if true, the animation will go from the beginning to the end and then from the end to the beginning -
* over and over, in an endless loop. {@code #isBackwards()} reports the current direction of iteration. */
public boolean isBouncing() {
return bouncing;
}
/** @param frameId number of the current frame. Index of the drawable in frames array. Can be invalid: will be
* clamped. */
public void setCurrentFrame(final int frameId) {
if (GdxArrays.isNotEmpty(frames)) {
currentFrame = MathUtils.clamp(frameId, 0, frames.size - 1);
setDrawable(frames.get(currentFrame));
}
}
/** Validates current frame index and corrects it, if necessary. Call this method after modifying internal frames
* array. */
public void validateCurrentFrame() {
setCurrentFrame(currentFrame);
}
/** @return index of the currently drawn drawable in the frames array. */
public int getCurrentFrame() {
return currentFrame;
}
/** @return currently used internal array of displayed drawables. Can be shared with other images. Can be modified,
* but make sure to call {@link #validateCurrentFrame()} after modification. Cannot contain nulls. */
public Array<Drawable> getFrames() {
return frames;
}
/** @param frames will become the internally used array of frames displayed by this image. Cannot contain nulls. Can
* be shared with other instances. */
public void setFrames(final Array<Drawable> frames) {
this.frames = frames;
}
/** @param delay the minimum time before the frame is changed to the next one. In seconds. Defaults to 0.2f. If
* higher than {@link #getMaxDelay()}, will replace current max delay value. */
public void setDelay(final float delay) {
this.delay = delay;
maxDelay = Math.max(maxDelay, delay);
}
/** @return the minimum time before the frame is changed to the next one. In seconds. */
public float getDelay() {
return delay;
}
/** @param maxDelay current frame index will not be updated more than roughly (maxDelay / delay) frames on a single
* {@link #act(float)} call. In seconds. Defaults to 1f. Prevents the widget from lagging during long
* rendering delays. */
public void setMaxDelay(final float maxDelay) {
this.maxDelay = maxDelay;
}
/** @return current frame index will be updated by no more than (min(delta, maxDelay) / delay) frames. In
* seconds. */
public float getMaxDelay() {
return maxDelay;
}
/** @param playOnce if true, this image will replace its internal frames array with a new array containing 1
* element: the last frame in the animation. After reaching the last frame, this widget will behave like
* a regular image. */
public void setPlayOnce(final boolean playOnce) {
this.playOnce = playOnce;
}
/** @return if true, this image will replace its internal frames array with a new array containing 1 element: the
* last frame in the animation. After reaching the last frame, this widget will behave like a regular
* image. */
public boolean isPlayedOnce() {
return playOnce;
}
}