Skip to content

Instantly share code, notes, and snippets.

@claudemartin
Last active August 29, 2015 14:25
Show Gist options
  • Select an option

  • Save claudemartin/d2a3e3c0f3ecbc10d695 to your computer and use it in GitHub Desktop.

Select an option

Save claudemartin/d2a3e3c0f3ecbc10d695 to your computer and use it in GitHub Desktop.

Revisions

  1. Claude Martin revised this gist Jul 27, 2015. 1 changed file with 53 additions and 26 deletions.
    79 changes: 53 additions & 26 deletions AnimationLoop.java
    Original file line number Diff line number Diff line change
    @@ -38,7 +38,8 @@
    * 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.
    * 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
    @@ -55,6 +56,13 @@ public final class AnimationLoop {
    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 -> {
    @@ -81,8 +89,8 @@ public AnimationLoop(final Runnable action, final double fps) {
    *
    * @param action
    * The callback that is invoked each time the animation needs to repaint. The action has
    * one single argument, which indicates the current time. The animation loop will
    * {@link #pause} if the action returns <code>false</code>.
    * 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.
    *
    @@ -96,8 +104,8 @@ public AnimationLoop(final LongPredicate action, final double fps) {
    *
    * @param action
    * The callback that is invoked each time the animation needs to repaint. The action has
    * one single argument, which indicates the current time. The animation loop will
    * {@link #pause} if the action returns <code>false</code>.
    * 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
    @@ -121,29 +129,30 @@ public AnimationLoop(final LongPredicate action, final long delay,
    if (this.isStopped)
    return;
    // Now it's not paused and not stopped.
    final long now1 = System.nanoTime();
    if (!action.test(now1)) {
    this.pause();
    return;
    }
    final long now2 = System.nanoTime();
    timeToSleep = (long) (this.delay - (now2 - now1));
    } finally {
    this.lock.unlock();
    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;
    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();
    }
    @@ -157,10 +166,19 @@ public AnimationLoop(final LongPredicate action, final long delay,
    */
    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();
    @@ -176,9 +194,11 @@ public void start() throws IllegalStateException {
    */
    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();
    @@ -207,7 +227,11 @@ public static final class Builder implements Cloneable {
    private long delay = Long.MIN_VALUE;
    private boolean autostart = false;

    /** The action to render one frame. */
    /**
    * 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;
    @@ -222,7 +246,10 @@ public Builder runnable(final Runnable runnable) {
    return this;
    }

    /** The action to render one frame. */
    /**
    * 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);
  2. Claude Martin created this gist Jul 27, 2015.
    295 changes: 295 additions & 0 deletions AnimationLoop.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,295 @@
    /****************************
    *
    * 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.
    *
    * <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;
    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, which indicates the current time. 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, which indicates the current time. 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();
    if (!action.test(now1)) {
    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();
    try {
    if (this.isStopped)
    throw new IllegalStateException("Already stopped.");
    this.isPaused = false;
    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();
    try {
    if (this.isStopped)
    throw new IllegalStateException("Already stopped.");
    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. */
    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. */
    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);
    }
    }
    }

    }