Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
A very simple animation loop that can be paused and stopped.
/****************************
*
* The MIT License (MIT)
*
* Copyright (c) 2015 Claude Martin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
****************************/
package ch.claude_martin.util;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.LongConsumer;
import java.util.function.LongPredicate;
/**
* A very simple animation loop that can be paused and stopped. This allows timing control for
* animations. The action is executed in a thread that is created on construction.
* <code>System.nanoTime()</code> is used as the high-resolution time source. However, the actual
* number of frames per second will not be precise at all. The given action callback to render a
* single frame will get the approximate time since the first start in nanoseconds.
*
* <p>
* The created thread will have a hard reference to the animation loop. An instance will not be
* garbage collected until it is {@link #stop() stopped}.
*
* @author Claude Martin
*
*/
public final class AnimationLoop {
private static final AtomicInteger counter = new AtomicInteger();
private final Lock lock = new ReentrantLock();
private boolean isPaused = true;
private boolean isStopped = false;
private final Condition running = this.lock.newCondition();
/** Time between frames in nanoseconds. */
private final double delay;
/** Start of the animation. */
private long timeStarted = -1;
/** When the current pause was started, or -1. */
private long timePaused = -1;
/** Total duration (nanos) of all pauses after the first start. */
private long durationOfPauses = 0;
private final Thread thread;
final static ThreadFactory DEFAULT_THREAD_FACTORY = r -> {
final Thread t = new Thread(r);
t.setName(AnimationLoop.class.getSimpleName() + "-" + counter.getAndIncrement());
t.setDaemon(true);
return t;
};
/**
* Creates a new animation loop from a {@link Runnable} with a default thread factory, creating
* daemon threads. The loop is paused and needs to be {@link #start() started}.
*/
public AnimationLoop(final Runnable action, final double fps) {
this(time -> {
action.run();
return true;
}, fps);
}
/**
* Creates a new animation loop with a default thread factory. The loop is paused and needs to be
* {@link #start() started}.
*
* @param action
* The callback that is invoked each time the animation needs to repaint. The action has
* one single argument, the nanoseconds of unpaused animation since the first start. The
* animation loop will {@link #pause} if the action returns <code>false</code>.
* @param fps
* Frames per second. This will regulate the timeout before each new animation step.
*
*/
public AnimationLoop(final LongPredicate action, final double fps) {
this(action, (long) (1_000_000_000 / fps), DEFAULT_THREAD_FACTORY);
}
/**
* Creates a new animation loop. The loop is paused and needs to be {@link #start() started}.
*
* @param action
* The callback that is invoked each time the animation needs to repaint. The action has
* one single argument, the nanoseconds of unpaused animation since the first start. The
* animation loop will {@link #pause} if the action returns <code>false</code>.
* @param delay
* The delay between frames in nano seconds.
* @param threadFactory
* A thread factory for the thread used by this animation loop.
*/
public AnimationLoop(final LongPredicate action, final long delay,
final ThreadFactory threadFactory) {
this.delay = delay;
this.thread = threadFactory.newThread(() -> {
while (!this.isStopped) {
final long timeToSleep;
this.lock.lock();
try {
while (this.isPaused && !this.isStopped)
try {
this.running.await();
} catch (final InterruptedException e) {
this.stop();
return;
}
if (this.isStopped)
return;
// Now it's not paused and not stopped.
final long now1 = System.nanoTime();
final long param = now1 - this.timeStarted - this.durationOfPauses;
if (!action.test(param)) {
this.pause();
return;
}
final long now2 = System.nanoTime();
timeToSleep = (long) (this.delay - (now2 - now1));
} finally {
this.lock.unlock();
}
try {
if (timeToSleep > 0) {
final int ns = (int) (timeToSleep % 1_000_000);
final long ms = (timeToSleep - ns) / 1_000_000;
Thread.sleep(ms, ns);
}
} catch (final InterruptedException e) {
this.stop();
return;
}
}
});
// The thread will start and then sleep until start() is invoked from any thread.
this.thread.start();
}
/**
* Starts the animation loop. Calling this method when it's already started has no side effects,
* but it could slow down animation.
*
* @throws IllegalStateException
* If the loop is already stopped.
*/
public void start() throws IllegalStateException {
this.lock.lock();
assert this.isPaused || this.timePaused == -1;
try {
if (this.isStopped)
throw new IllegalStateException("Already stopped.");
if (!this.isPaused)
return;
this.isPaused = false;
final long now = System.nanoTime();
if (this.timeStarted == -1)
this.timeStarted = now;
else
this.durationOfPauses += now - this.timePaused;
this.timePaused = -1;
this.running.signalAll();
} finally {
this.lock.unlock();
}
}
/**
* Pause the animation. The current frame will be rendered and execution is then paused. Calling
* this method when it's already paused has no side effects.
*
* @throws IllegalStateException
* If the loop is already stopped.
*/
public void pause() throws IllegalStateException {
this.lock.lock();
assert this.isPaused || this.timePaused == -1;
try {
if (this.isStopped)
throw new IllegalStateException("Already stopped.");
this.timePaused = System.nanoTime();
this.isPaused = true;
} finally {
this.lock.unlock();
}
}
/**
* Permanently stops this animation loop. It then can not be started again.
*/
public void stop() {
this.lock.lock();
try {
this.isStopped = true;
// Signal needed if loop is waiting:
if (this.isPaused)
this.running.signalAll();
} finally {
this.lock.unlock();
}
}
/** A builder for an animation loop. */
public static final class Builder implements Cloneable {
private LongPredicate action = null;
private ThreadFactory threadFactory = DEFAULT_THREAD_FACTORY;
private long delay = Long.MIN_VALUE;
private boolean autostart = false;
/**
* The action to render one frame. The action has one single argument, the nanoseconds of
* unpaused animation since the first start. The animation loop will
* {@link AnimationLoop#pause() pause} if the action returns <code>false</code>
*/
public Builder action(@SuppressWarnings("hiding") final LongPredicate action) {
this.action = action;
return this;
}
/** The action to render one frame. */
public Builder runnable(final Runnable runnable) {
this.action = time -> {
runnable.run();
return true;
};
return this;
}
/**
* The action to render one frame. The action has one single argument, the nanoseconds of
* unpaused animation since the first start.
*/
public Builder consumer(final LongConsumer consumer) {
this.action = time -> {
consumer.accept(time);
return true;
};
return this;
}
/** Delay between frames in nano seconds. */
public Builder delay(final long nanos) {
this.delay = nanos;
return this;
}
/** Delay between frames in any time unit. */
public Builder delay(final long duration, final TimeUnit unit) {
this.delay = unit.toNanos(duration);
return this;
}
/** Frames per second (instead of delay in nano seconds). */
public Builder fps(final double fps) {
this.delay = (long) (1_000_000_000 / fps);
return this;
}
/**
* Set the thread factory for the anmation loop.
* <p>
* Default value: A default thread factory that creats daemon threads.
*/
public Builder threadFactory(final ThreadFactory factory) {
this.threadFactory = factory;
return this;
}
/**
* Set autostart. The animation loop will not start unless this is set to true or
* <code>start()</code> is invoked.
* <p>
* Default value: <code>false</code>
*/
public Builder autostart(final boolean auto) {
this.autostart = auto;
return this;
}
/** Builds the animation loop. */
public AnimationLoop build() {
if (this.action == null)
throw new IllegalStateException("no action");
if (this.delay < 0)
throw new IllegalStateException("no delay");
final AnimationLoop al = new AnimationLoop(this.action, this.delay, this.threadFactory);
if (this.autostart)
al.start();
return al;
}
@Override
public Builder clone() {
try {
return (Builder) super.clone();
} catch (final CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.