All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.pushingpixels.trident.TimelineEngine Maven / Gradle / Ivy

There is a newer version: 4.5.0
Show newest version
/*
 * Copyright (c) 2005-2018 Trident Kirill Grouchnikov. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  o Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 *  o Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 *  o Neither the name of Trident Kirill Grouchnikov nor the names of
 *    its contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.pushingpixels.trident;

import org.pushingpixels.trident.Timeline.TimelineState;
import org.pushingpixels.trident.TimelineScenario.TimelineScenarioState;
import org.pushingpixels.trident.callback.RunOnUIThread;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * The Trident timeline engine. This is the main entry point to play {@link Timeline}s and
 * {@link TimelineScenario}s. Use the {@link #getInstance()} method to get the timeline engine.
 * 
 * @author Kirill Grouchnikov
 */
class TimelineEngine {
    /**
     * Debug mode indicator. Set to true to print trace messages to console
     * during development.
     */
    public static boolean DEBUG_MODE = false;

    /**
     * Single instance of this class.
     */
    private static TimelineEngine instance;

    /**
     * All currently running timelines.
     */
    private Set runningTimelines;

    enum TimelineOperationKind {
        PLAY, CANCEL, RESUME, SUSPEND, ABORT, END
    }

    class TimelineOperation {
        public TimelineOperationKind operationKind;

        Runnable operationRunnable;

        public TimelineOperation(TimelineOperationKind operationKind, Runnable operationRunnable) {
            this.operationKind = operationKind;
            this.operationRunnable = operationRunnable;
        }
    }

    private Set runningScenarios;

    long lastIterationTimeStamp;

    /**
     * Identifies a main object and an optional secondary ID.
     * 
     * @author Kirill Grouchnikov
     */
    static class FullObjectID {
        /**
         * Main object for the timeline.
         */
        public Object mainObj;

        /**
         * ID to distinguish between different sub-components of {@link #mainObj}. For example, the
         * tabbed pane uses this field to make tab-specific animations.
         */
        @SuppressWarnings("unchecked")
        public Comparable subID;

        /**
         * Creates a new object ID.
         * 
         * @param mainObj
         *            The main object.
         * @param subID
         *            ID to distinguish between different sub-components of mainObj.
         *            Can be null.
         */
        @SuppressWarnings("unchecked")
        public FullObjectID(Object mainObj, Comparable subID) {
            this.mainObj = mainObj;
            this.subID = subID;
        }

        @Override
        public int hashCode() {
            int result = this.mainObj.hashCode();
            if (this.subID != null)
                result &= (this.subID.hashCode());
            return result;
        }

        @Override
        @SuppressWarnings("unchecked")
        public boolean equals(Object obj) {
            if (obj instanceof FullObjectID) {
                FullObjectID cid = (FullObjectID) obj;
                try {
                    boolean result = (this.mainObj == cid.mainObj);
                    if (this.subID == null) {
                        result = result && (cid.subID == null);
                    } else {
                        result = result && (this.subID.compareTo(cid.subID) == 0);
                    }
                    return result;
                } catch (Exception exc) {
                    return false;
                }
            }
            return false;
        }

        @Override
        public String toString() {
            return this.mainObj.getClass().getSimpleName() + ":" + this.subID;
        }
    }

    /**
     * The timeline thread.
     */
    TridentAnimationThread animatorThread;

    private BlockingQueue callbackQueue;

    private TimelineCallbackThread callbackThread;

    class TridentAnimationThread extends Thread {
        public TridentAnimationThread() {
            super();
            this.setName("Trident pulse source thread");
            this.setDaemon(true);
        }

        @Override
        public final void run() {
            TridentConfig.PulseSource pulseSource = TridentConfig.getInstance().getPulseSource();
            lastIterationTimeStamp = System.currentTimeMillis();
            while (true) {
                pulseSource.waitUntilNextPulse();
                updateTimelines();
                // engine.currLoopId++;
            }
        }

        @Override
        public void interrupt() {
            System.err.println("Interrupted");
            super.interrupt();
        }
    }

    private class TimelineCallbackThread extends Thread {
        public TimelineCallbackThread() {
            super();
            this.setName("Trident callback thread");
            this.setDaemon(true);
        }

        @Override
        public void run() {
            while (true) {
                try {
                    Runnable runnable = callbackQueue.take();
                    runnable.run();
                } catch (Throwable t) {
                    t.printStackTrace();
                }
            }
        }
    }

    /**
     * Simple constructor. Defined private for singleton.
     * 
     * @see #getInstance()
     */
    private TimelineEngine() {
        this.runningTimelines = new HashSet();
        this.runningScenarios = new HashSet();

        this.callbackQueue = new LinkedBlockingQueue();
        this.callbackThread = this.getCallbackThread();
    }

    /**
     * Gets singleton instance.
     * 
     * @return Singleton instance.
     */
    public synchronized static TimelineEngine getInstance() {
        if (TimelineEngine.instance == null) {
            TimelineEngine.instance = new TimelineEngine();
        }
        return TimelineEngine.instance;
    }

    /**
     * Updates all timelines that are currently registered with this tracker.
     */
    void updateTimelines() {
        synchronized (LOCK) {
            if ((this.runningTimelines.size() == 0) && (this.runningScenarios.size() == 0)) {
                this.lastIterationTimeStamp = System.currentTimeMillis();
                return;
            }

            long passedSinceLastIteration = (System.currentTimeMillis()
                    - this.lastIterationTimeStamp);
            if (passedSinceLastIteration < 0) {
                // ???
                passedSinceLastIteration = 0;
            }
            if (DEBUG_MODE) {
                System.out.println(
                        "Elapsed since last iteration: " + passedSinceLastIteration + "ms");
            }

            // System.err.println("Periodic update on "
            // + this.runningTimelines.size() + " timelines; "
            // + passedSinceLastIteration + " ms passed since last");
            // for (Timeline t : runningTimelines) {
            // if (t.mainObject != null
            // && t.mainObject.getClass().getName().indexOf(
            // "ProgressBar") >= 0) {
            // continue;
            // }
            // System.err.println("\tTimeline @"
            // + t.hashCode()
            // + " ["
            // + t.getName()
            // + "] on "
            // + (t.mainObject == null ? "null" : t.mainObject
            // .getClass().getName()));
            // }
            for (Iterator itTimeline = this.runningTimelines.iterator(); itTimeline
                    .hasNext();) {
                Timeline timeline = itTimeline.next();
                if (timeline.getState() == TimelineState.SUSPENDED)
                    continue;

                boolean timelineWasInReadyState = false;
                if (timeline.getState() == TimelineState.READY) {
                    if ((timeline.timeUntilPlay - passedSinceLastIteration) > 0) {
                        // still needs to wait in the READY state
                        timeline.timeUntilPlay -= passedSinceLastIteration;
                        continue;
                    }

                    // can go from READY to PLAYING
                    timelineWasInReadyState = true;
                    timeline.popState();
                    if (DEBUG_MODE) {
                        System.out.println("Moving " + timeline.id + " to READY");
                    }
                    this.callbackCallTimelineStateChanged(timeline, TimelineState.READY);
                }

                boolean hasEnded = false;
                if (DEBUG_MODE) {
                    System.out.println("Processing " + timeline.id + "["
                            + timeline.mainObject.getClass().getSimpleName() + "] from "
                            + timeline.durationFraction + ". Callback - "
                            + (timeline.callbackChain == null ? "no" : "yes"));
                }
                // Component comp = entry.getKey();

                // at this point, the timeline must be playing
                switch (timeline.getState()) {
                case PLAYING_FORWARD:
                    if (!timelineWasInReadyState) {
                        timeline.durationFraction = timeline.durationFraction
                                + (float) passedSinceLastIteration / (float) timeline.duration;
                    }
                    timeline.timelinePosition = timeline.ease.map(timeline.durationFraction);
                    if (DEBUG_MODE) {
                        System.out.println("Timeline " + timeline.id + " position: "
                                + ((long) (timeline.durationFraction * timeline.duration)) + "/"
                                + timeline.duration + " = " + timeline.durationFraction);
                    }
                    if (timeline.durationFraction > 1.0f) {
                        timeline.durationFraction = 1.0f;
                        timeline.timelinePosition = 1.0f;
                        if (timeline.isLooping) {
                            boolean stopLoopingAnimation = timeline.toCancelAtCycleBreak;
                            int loopsToLive = timeline.repeatCount;
                            if (loopsToLive > 0) {
                                loopsToLive--;
                                stopLoopingAnimation = stopLoopingAnimation || (loopsToLive == 0);
                                timeline.repeatCount = loopsToLive;
                            }
                            if (stopLoopingAnimation) {
                                // end looping animation
                                hasEnded = true;
                                itTimeline.remove();
                            } else {
                                if (timeline.repeatBehavior == Timeline.RepeatBehavior.REVERSE) {
                                    timeline.replaceState(TimelineState.PLAYING_REVERSE);
                                    if (timeline.cycleDelay > 0) {
                                        timeline.pushState(TimelineState.READY);
                                        timeline.timeUntilPlay = timeline.cycleDelay;
                                    }
                                    this.callbackCallTimelineStateChanged(timeline,
                                            TimelineState.PLAYING_FORWARD);
                                } else {
                                    timeline.durationFraction = 0.0f;
                                    timeline.timelinePosition = 0.0f;
                                    if (timeline.cycleDelay > 0) {
                                        timeline.pushState(TimelineState.READY);
                                        timeline.timeUntilPlay = timeline.cycleDelay;
                                        this.callbackCallTimelineStateChanged(timeline,
                                                TimelineState.PLAYING_FORWARD);
                                    } else {
                                        // it's still playing forward, but lets
                                        // the app code know
                                        // that the new loop has begun
                                        this.callbackCallTimelineStateChanged(timeline,
                                                TimelineState.PLAYING_FORWARD);
                                    }
                                }
                            }
                        } else {
                            hasEnded = true;
                            itTimeline.remove();
                        }
                    }
                    break;
                case PLAYING_REVERSE:
                    if (!timelineWasInReadyState) {
                        timeline.durationFraction = timeline.durationFraction
                                - (float) passedSinceLastIteration / (float) timeline.duration;
                    }
                    timeline.timelinePosition = timeline.ease.map(timeline.durationFraction);
                    // state.timelinePosition = state.timelinePosition
                    // - stepFactor
                    // * state.fadeStep.getNextStep(state.timelineKind,
                    // state.timelinePosition,
                    // state.isPlayingForward, state.isLooping);
                    if (DEBUG_MODE) {
                        System.out.println("Timeline position: "
                                + ((long) (timeline.durationFraction * timeline.duration)) + "/"
                                + timeline.duration + " = " + timeline.durationFraction);
                    }
                    if (timeline.durationFraction < 0) {
                        timeline.durationFraction = 0.0f;
                        timeline.timelinePosition = 0.0f;
                        if (timeline.isLooping) {
                            boolean stopLoopingAnimation = timeline.toCancelAtCycleBreak;
                            int loopsToLive = timeline.repeatCount;
                            if (loopsToLive > 0) {
                                loopsToLive--;
                                stopLoopingAnimation = stopLoopingAnimation || (loopsToLive == 0);
                                timeline.repeatCount = loopsToLive;
                            }
                            if (stopLoopingAnimation) {
                                // end looping animation
                                hasEnded = true;
                                itTimeline.remove();
                            } else {
                                timeline.replaceState(TimelineState.PLAYING_FORWARD);
                                if (timeline.cycleDelay > 0) {
                                    timeline.pushState(TimelineState.READY);
                                    timeline.timeUntilPlay = timeline.cycleDelay;
                                }
                                this.callbackCallTimelineStateChanged(timeline,
                                        TimelineState.PLAYING_REVERSE);
                            }
                        } else {
                            hasEnded = true;
                            itTimeline.remove();
                        }
                    }
                    break;
                default:
                    throw new IllegalStateException(
                            "Timeline cannot be in " + timeline.getState() + " state");
                }
                if (hasEnded) {
                    if (DEBUG_MODE) {
                        System.out.println("Ending " + timeline.id + " on "
                        // + timeline.timelineKind.toString()
                                + " in state " + timeline.getState().name() + " at position "
                                + timeline.durationFraction);
                    }
                    TimelineState oldState = timeline.getState();
                    timeline.replaceState(TimelineState.DONE);
                    this.callbackCallTimelineStateChanged(timeline, oldState);
                    timeline.popState();
                    if (timeline.getState() != TimelineState.IDLE) {
                        throw new IllegalStateException("Timeline should be IDLE at this point");
                    }
                    this.callbackCallTimelineStateChanged(timeline, TimelineState.DONE);
                } else {
                    if (DEBUG_MODE) {
                        System.out.println("Calling " + timeline.id + " on "
                        // + timeline.timelineKind.toString() + " at "
                                + timeline.durationFraction);
                    }
                    this.callbackCallTimelinePulse(timeline);
                }
            }

            if (this.runningScenarios.size() > 0) {
                // System.err.println(Thread.currentThread().getName()
                // + " : updating");
                for (Iterator it = this.runningScenarios.iterator(); it
                        .hasNext();) {
                    TimelineScenario scenario = it.next();
                    if (scenario.state == TimelineScenarioState.DONE) {
                        it.remove();
                        this.callbackCallTimelineScenarioEnded(scenario);
                        continue;
                    }
                    Set readyActors = scenario
                            .getReadyActors();
                    if (readyActors != null) {
                        // if (readyActors.size() > 0)
                        // System.out.println("Scenario : " + scenario.state +
                        // ":"
                        // + readyActors.size());
                        for (TimelineScenario.TimelineScenarioActor readyActor : readyActors) {
                            readyActor.play();
                        }
                    }
                }
            }
            // System.err.println("Periodic update done");

            // this.nothingTracked = (this.runningTimelines.size() == 0);
            this.lastIterationTimeStamp = System.currentTimeMillis();
        }
    }

    private void callbackCallTimelineStateChanged(final Timeline timeline,
            final TimelineState oldState) {
        final TimelineState newState = timeline.getState();
        final float durationFraction = timeline.durationFraction;
        final float timelinePosition = timeline.timelinePosition;
        Runnable callbackRunnable = () -> {
            boolean shouldRunOnUIThread = false;
            Class clazz = timeline.callbackChain.getClass();
            while ((clazz != null) && !shouldRunOnUIThread) {
                shouldRunOnUIThread = clazz.isAnnotationPresent(RunOnUIThread.class);
                clazz = clazz.getSuperclass();
            }
            if (shouldRunOnUIThread && (timeline.uiToolkitHandler != null)) {
                if (DEBUG_MODE) {
                    System.out.println("Scheduling callback state change from " + oldState.name()
                            + " to " + newState.name() + " on timeline " + timeline.id);
                }
                // System.out.println("Will update from " + oldState + " to " + newState);
                timeline.uiToolkitHandler.runOnUIThread(timeline.mainObject,
                        () -> timeline.callbackChain.onTimelineStateChanged(oldState, newState,
                                durationFraction, timelinePosition));
            } else {
                if (DEBUG_MODE) {
                    System.out.println("Calling callback state change from " + oldState.name()
                            + " to " + newState.name() + " on timeline " + timeline.id);
                }
                // System.out.println("Updating from " + oldState + " to " + newState);
                timeline.callbackChain.onTimelineStateChanged(oldState, newState, durationFraction,
                        timelinePosition);
            }
        };
        if (DEBUG_MODE) {
            System.out.println("Scheduling callback runnable for " + oldState.name() + " to "
                    + newState.name() + " on timeline " + timeline.id);
        }
        this.callbackQueue.add(callbackRunnable);
    }

    private void callbackCallTimelinePulse(final Timeline timeline) {
        final float durationFraction = timeline.durationFraction;
        final float timelinePosition = timeline.timelinePosition;
        Runnable callbackRunnable = () -> {
            boolean shouldRunOnUIThread = false;
            Class clazz = timeline.callbackChain.getClass();
            while ((clazz != null) && !shouldRunOnUIThread) {
                shouldRunOnUIThread = clazz.isAnnotationPresent(RunOnUIThread.class);
                clazz = clazz.getSuperclass();
            }
            if (shouldRunOnUIThread && (timeline.uiToolkitHandler != null)) {
                timeline.uiToolkitHandler.runOnUIThread(timeline.mainObject,
                        () -> timeline.callbackChain.onTimelinePulse(durationFraction,
                                timelinePosition));
            } else {
                timeline.callbackChain.onTimelinePulse(durationFraction, timelinePosition);
            }
        };
        this.callbackQueue.add(callbackRunnable);
    }

    private void callbackCallTimelineScenarioEnded(final TimelineScenario timelineScenario) {
        Runnable callbackRunnable = () -> timelineScenario.callback.onTimelineScenarioDone();
        this.callbackQueue.offer(callbackRunnable);
    }

    /**
     * Returns an existing running timeline that matches the specified parameters.
     * 
     * @param timeline
     *            Timeline.
     * @return An existing running timeline that matches the specified parameters.
     */
    private Timeline getRunningTimeline(Timeline timeline) {
        synchronized (LOCK) {
            if (this.runningTimelines.contains(timeline))
                return timeline;
            return null;
        }
    }

    /**
     * Adds the specified timeline.
     * 
     * @param timeline
     *            Timeline to add.
     */
    private void addTimeline(Timeline timeline) {
        synchronized (LOCK) {
            FullObjectID cid = new FullObjectID(timeline.mainObject, timeline.secondaryId);
            timeline.fullObjectID = cid;
            this.runningTimelines.add(timeline);
            // this.nothingTracked = false;
            if (DEBUG_MODE) {
                System.out.println(
                        "Added (" + timeline.id + ") on [" + timeline.fullObjectID + "]. State - "
                        // + timeline.timelineKind.toString() + " with state "
                                + timeline.getState().name() + ". Callback - "
                                + (timeline.callbackChain == null ? "no" : "yes"));
            }
        }
    }

    void play(Timeline timeline, boolean reset, long msToSkip) {
        synchronized (LOCK) {
            getAnimatorThread();

            // see if it's already tracked
            Timeline existing = this.getRunningTimeline(timeline);
            if (existing == null) {
                TimelineState oldState = timeline.getState();
                timeline.timeUntilPlay = timeline.initialDelay - msToSkip;
                if (timeline.timeUntilPlay < 0) {
                    timeline.durationFraction = (float) -timeline.timeUntilPlay
                            / (float) timeline.duration;
                    timeline.timelinePosition = timeline.ease.map(timeline.durationFraction);
                    timeline.timeUntilPlay = 0;
                } else {
                    timeline.durationFraction = 0.0f;
                    timeline.timelinePosition = 0.0f;
                }
                timeline.pushState(TimelineState.PLAYING_FORWARD);
                timeline.pushState(TimelineState.READY);
                this.addTimeline(timeline);

                this.callbackCallTimelineStateChanged(timeline, oldState);
            } else {
                TimelineState oldState = existing.getState();
                if (oldState == TimelineState.READY) {
                    // the timeline remains READY, but after that it will be
                    // PLAYING_FORWARD
                    existing.popState();
                    existing.replaceState(TimelineState.PLAYING_FORWARD);
                    existing.pushState(TimelineState.READY);
                } else {
                    // change the timeline state
                    existing.replaceState(TimelineState.PLAYING_FORWARD);
                    if (oldState != existing.getState()) {
                        this.callbackCallTimelineStateChanged(timeline, oldState);
                    }
                }
                if (reset) {
                    existing.durationFraction = 0.0f;
                    existing.timelinePosition = 0.0f;
                    this.callbackCallTimelinePulse(existing);
                }
            }
        }
    }

    void playScenario(TimelineScenario scenario) {
        synchronized (LOCK) {
            getAnimatorThread();
            Set readyActors = scenario.getReadyActors();

            // System.err.println(Thread.currentThread().getName() +
            // " : adding");
            this.runningScenarios.add(scenario);
            for (TimelineScenario.TimelineScenarioActor readyActor : readyActors) {
                readyActor.play();
            }
        }
    }

    void playReverse(Timeline timeline, boolean reset, long msToSkip) {
        synchronized (LOCK) {
            getAnimatorThread();
            if (timeline.isLooping) {
                throw new IllegalArgumentException("Timeline must not be marked as looping");
            }

            // see if it's already tracked
            Timeline existing = this.getRunningTimeline(timeline);
            if (existing == null) {
                TimelineState oldState = timeline.getState();
                timeline.timeUntilPlay = timeline.initialDelay - msToSkip;
                if (timeline.timeUntilPlay < 0) {
                    timeline.durationFraction = 1.0f
                            - (float) -timeline.timeUntilPlay / (float) timeline.duration;
                    timeline.timelinePosition = timeline.ease.map(timeline.durationFraction);
                    timeline.timeUntilPlay = 0;
                } else {
                    timeline.durationFraction = 1.0f;
                    timeline.timelinePosition = 1.0f;
                }
                timeline.pushState(TimelineState.PLAYING_REVERSE);
                timeline.pushState(TimelineState.READY);

                this.addTimeline(timeline);
                this.callbackCallTimelineStateChanged(timeline, oldState);
            } else {
                TimelineState oldState = existing.getState();
                if (oldState == TimelineState.READY) {
                    // the timeline remains READY, but after that it will be
                    // PLAYING_REVERSE
                    existing.popState();
                    existing.replaceState(TimelineState.PLAYING_REVERSE);
                    existing.pushState(TimelineState.READY);
                } else {
                    // change the timeline state
                    existing.replaceState(TimelineState.PLAYING_REVERSE);
                    if (oldState != existing.getState()) {
                        this.callbackCallTimelineStateChanged(timeline, oldState);
                    }
                }
                if (reset) {
                    existing.durationFraction = 1.0f;
                    existing.timelinePosition = 1.0f;
                    this.callbackCallTimelinePulse(existing);
                }
            }
        }
    }

    void playLoop(Timeline timeline, long msToSkip) {
        synchronized (LOCK) {
            getAnimatorThread();
            if (!timeline.isLooping) {
                throw new IllegalArgumentException("Timeline must be marked as looping");
            }

            // see if it's already tracked
            Timeline existing = this.getRunningTimeline(timeline);
            if (existing == null) {
                TimelineState oldState = timeline.getState();
                timeline.timeUntilPlay = timeline.initialDelay - msToSkip;
                if (timeline.timeUntilPlay < 0) {
                    timeline.durationFraction = (float) -timeline.timeUntilPlay
                            / (float) timeline.duration;
                    timeline.timelinePosition = timeline.ease.map(timeline.durationFraction);
                    timeline.timeUntilPlay = 0;
                } else {
                    timeline.durationFraction = 0.0f;
                    timeline.timelinePosition = 0.0f;
                }
                timeline.pushState(TimelineState.PLAYING_FORWARD);
                timeline.pushState(TimelineState.READY);
                timeline.toCancelAtCycleBreak = false;

                this.addTimeline(timeline);
                this.callbackCallTimelineStateChanged(timeline, oldState);
            } else {
                existing.toCancelAtCycleBreak = false;
                existing.repeatCount = timeline.repeatCount;
            }
        }
    }

    /**
     * Stops tracking of all timelines. Note that this function does not stop the timeline
     * engine thread ({@link #animatorThread}) and the timeline callback thread
     * ({@link #callbackThread}).
     */
    public void cancelAllTimelines() {
        synchronized (LOCK) {
            getAnimatorThread();
            for (Timeline timeline : this.runningTimelines) {
                TimelineState oldState = timeline.getState();
                while (timeline.getState() != TimelineState.IDLE)
                    timeline.popState();
                timeline.pushState(TimelineState.CANCELLED);
                this.callbackCallTimelineStateChanged(timeline, oldState);
                timeline.popState();
                this.callbackCallTimelineStateChanged(timeline, TimelineState.CANCELLED);
            }
            this.runningTimelines.clear();
            this.runningScenarios.clear();
        }
    }

    /**
     * Returns an instance of the animator thread.
     * 
     * @return The animator thread.
     */
    private TridentAnimationThread getAnimatorThread() {
        if (this.animatorThread == null) {
            this.animatorThread = new TridentAnimationThread();
            this.animatorThread.start();
        }
        return this.animatorThread;
    }

    /**
     * Returns an instance of the callback thread.
     * 
     * @return The animator thread.
     */
    private TimelineCallbackThread getCallbackThread() {
        if (this.callbackThread == null) {
            this.callbackThread = new TimelineCallbackThread();
            this.callbackThread.start();
        }
        return this.callbackThread;
    }

    /**
     * Cancels the specified timeline instance.
     * 
     * @param timeline
     *            Timeline to cancel.
     */
    private void cancelTimeline(Timeline timeline) {
        getAnimatorThread();
        if (this.runningTimelines.contains(timeline)) {
            this.runningTimelines.remove(timeline);
            TimelineState oldState = timeline.getState();
            while (timeline.getState() != TimelineState.IDLE)
                timeline.popState();
            timeline.pushState(TimelineState.CANCELLED);
            this.callbackCallTimelineStateChanged(timeline, oldState);
            timeline.popState();
            this.callbackCallTimelineStateChanged(timeline, TimelineState.CANCELLED);
        }
    }

    /**
     * Ends the specified timeline instance.
     * 
     * @param timeline
     *            Timeline to end.
     */
    private void endTimeline(Timeline timeline) {
        getAnimatorThread();
        if (this.runningTimelines.contains(timeline)) {
            this.runningTimelines.remove(timeline);
            TimelineState oldState = timeline.getState();
            float endPosition = timeline.timelinePosition;
            while (timeline.getState() != TimelineState.IDLE) {
                TimelineState state = timeline.popState();
                if (state == TimelineState.PLAYING_FORWARD)
                    endPosition = 1.0f;
                if (state == TimelineState.PLAYING_REVERSE)
                    endPosition = 0.0f;
            }
            timeline.durationFraction = endPosition;
            timeline.timelinePosition = endPosition;
            timeline.pushState(TimelineState.DONE);
            this.callbackCallTimelineStateChanged(timeline, oldState);
            timeline.popState();
            this.callbackCallTimelineStateChanged(timeline, TimelineState.DONE);
        }
    }

    /**
     * Cancels the specified timeline instance.
     * 
     * @param timeline
     *            Timeline to cancel.
     */
    private void abortTimeline(Timeline timeline) {
        getAnimatorThread();
        if (this.runningTimelines.contains(timeline)) {
            this.runningTimelines.remove(timeline);
            while (timeline.getState() != TimelineState.IDLE)
                timeline.popState();
        }
    }

    /**
     * Suspends the specified timeline instance.
     * 
     * @param timeline
     *            Timeline to suspend.
     */
    private void suspendTimeline(Timeline timeline) {
        getAnimatorThread();
        if (this.runningTimelines.contains(timeline)) {
            TimelineState oldState = timeline.getState();
            if ((oldState != TimelineState.PLAYING_FORWARD)
                    && (oldState != TimelineState.PLAYING_REVERSE)
                    && (oldState != TimelineState.READY)) {
                return;
            }
            timeline.pushState(TimelineState.SUSPENDED);
            this.callbackCallTimelineStateChanged(timeline, oldState);
        }
    }

    /**
     * Resume the specified timeline instance.
     * 
     * @param timeline
     *            Timeline to resume.
     */
    private void resumeTimeline(Timeline timeline) {
        getAnimatorThread();
        if (this.runningTimelines.contains(timeline)) {
            TimelineState oldState = timeline.getState();
            if (oldState != TimelineState.SUSPENDED)
                return;
            timeline.popState();
            this.callbackCallTimelineStateChanged(timeline, oldState);
        }
    }

    @SuppressWarnings("incomplete-switch")
    void runTimelineOperation(Timeline timeline, TimelineOperationKind operationKind,
            Runnable operationRunnable) {
        synchronized (LOCK) {
            this.getAnimatorThread();
            switch (operationKind) {
            case CANCEL:
                this.cancelTimeline(timeline);
                return;
            case END:
                this.endTimeline(timeline);
                return;
            case RESUME:
                this.resumeTimeline(timeline);
                return;
            case SUSPEND:
                this.suspendTimeline(timeline);
                return;
            case ABORT:
                this.abortTimeline(timeline);
                return;
            }
            operationRunnable.run();
        }
    }

    void runTimelineScenario(TimelineScenario timelineScenario, Runnable timelineScenarioRunnable) {
        synchronized (LOCK) {
            this.getAnimatorThread();
            timelineScenarioRunnable.run();
        }
    }

    static final Object LOCK = new Object();
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy