2
* Copyright (c) 2005-2010 Trident Kirill Grouchnikov. All Rights Reserved.
4
* Redistribution and use in source and binary forms, with or without
5
* modification, are permitted provided that the following conditions are met:
7
* o Redistributions of source code must retain the above copyright notice,
8
* this list of conditions and the following disclaimer.
10
* o Redistributions in binary form must reproduce the above copyright notice,
11
* this list of conditions and the following disclaimer in the documentation
12
* and/or other materials provided with the distribution.
14
* o Neither the name of Trident Kirill Grouchnikov nor the names of
15
* its contributors may be used to endorse or promote products derived
16
* from this software without specific prior written permission.
18
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
20
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
22
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
23
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
25
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
26
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
27
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
28
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
package org.pushingpixels.trident;
34
import org.pushingpixels.trident.TimelineEngine.FullObjectID;
35
import org.pushingpixels.trident.TimelineEngine.TimelineOperationKind;
36
import org.pushingpixels.trident.TimelinePropertyBuilder.AbstractFieldInfo;
37
import org.pushingpixels.trident.callback.*;
38
import org.pushingpixels.trident.ease.Linear;
39
import org.pushingpixels.trident.ease.TimelineEase;
40
import org.pushingpixels.trident.interpolator.KeyFrames;
42
public class Timeline implements TimelineScenario.TimelineScenarioActor {
45
Comparable<?> secondaryId;
47
FullObjectID fullObjectID;
59
RepeatBehavior repeatBehavior;
61
UIToolkitHandler uiToolkitHandler;
67
List<AbstractFieldInfo> propertiesToInterpolate;
70
* Is used to create unique value for the {@link #id} field.
82
float durationFraction;
87
float timelinePosition;
92
* Indication whether the looping timeline should stop at reaching the end
93
* of the cycle. Relevant only when {@link #isLooping} is <code>true</code>.
95
boolean toCancelAtCycleBreak;
97
Stack<TimelineState> stateStack;
101
private int doneCount;
103
public enum RepeatBehavior {
107
public enum TimelineState {
108
IDLE(false), READY(false), PLAYING_FORWARD(true), PLAYING_REVERSE(true), SUSPENDED(
109
false), CANCELLED(false), DONE(false);
111
private boolean isActive;
113
private TimelineState(boolean isActive) {
114
this.isActive = isActive;
118
private class Setter extends TimelineCallbackAdapter {
120
public void onTimelineStateChanged(TimelineState oldState,
121
TimelineState newState, float durationFraction,
122
float timelinePosition) {
123
if (newState == TimelineState.READY) {
124
for (AbstractFieldInfo fInfo : propertiesToInterpolate) {
125
// check whether the object is in the ready state
126
if ((uiToolkitHandler != null)
127
&& !uiToolkitHandler.isInReadyState(fInfo.object))
133
// Fix for issue 5 - update field values only when
134
// either old or new state (or both) are active. Otherwise
135
// it's a transition between inactive states (such as from
136
// DONE to IDLE) that shouldn't trigger the property changes
137
if (oldState.isActive || newState.isActive) {
138
for (AbstractFieldInfo fInfo : propertiesToInterpolate) {
139
// check whether the object is in the ready state
140
if ((uiToolkitHandler != null)
141
&& !uiToolkitHandler.isInReadyState(fInfo.object))
143
fInfo.updateFieldValue(timelinePosition);
149
public void onTimelinePulse(float durationFraction,
150
float timelinePosition) {
151
for (AbstractFieldInfo fInfo : propertiesToInterpolate) {
152
// check whether the object is in the ready state
153
if ((uiToolkitHandler != null)
154
&& !uiToolkitHandler.isInReadyState(fInfo.object))
156
// System.err.println("Timeline @" + Timeline.this.hashCode()
157
// + " at position " + timelinePosition);
158
fInfo.updateFieldValue(timelinePosition);
164
private class UISetter extends Setter {
167
class Chain implements TimelineCallback {
168
private List<TimelineCallback> callbacks;
170
public Chain(TimelineCallback... callbacks) {
171
this.callbacks = new ArrayList<TimelineCallback>();
172
for (TimelineCallback callback : callbacks)
173
this.callbacks.add(callback);
176
public void addCallback(TimelineCallback callback) {
177
this.callbacks.add(callback);
180
public void removeCallback(TimelineCallback callback) {
181
this.callbacks.remove(callback);
185
public void onTimelineStateChanged(final TimelineState oldState,
186
final TimelineState newState, final float durationFraction,
187
final float timelinePosition) {
188
if ((uiToolkitHandler != null)
189
&& !uiToolkitHandler.isInReadyState(mainObject))
191
for (int i = this.callbacks.size() - 1; i >= 0; i--) {
192
final TimelineCallback callback = this.callbacks.get(i);
193
// special handling for chained callbacks not running on UI
195
boolean shouldRunOnUIThread = false;
196
Class<?> clazz = callback.getClass();
197
while ((clazz != null) && !shouldRunOnUIThread) {
198
shouldRunOnUIThread = clazz
199
.isAnnotationPresent(RunOnUIThread.class);
200
clazz = clazz.getSuperclass();
202
if (shouldRunOnUIThread
203
&& (Timeline.this.uiToolkitHandler != null)) {
204
Timeline.this.uiToolkitHandler.runOnUIThread(mainObject,
208
callback.onTimelineStateChanged(oldState,
209
newState, durationFraction,
214
callback.onTimelineStateChanged(oldState, newState,
215
durationFraction, timelinePosition);
221
public void onTimelinePulse(final float durationFraction,
222
final float timelinePosition) {
223
if ((uiToolkitHandler != null)
224
&& !uiToolkitHandler.isInReadyState(mainObject))
226
for (int i = this.callbacks.size() - 1; i >= 0; i--) {
227
final TimelineCallback callback = this.callbacks.get(i);
228
// special handling for chained callbacks not running on UI
230
boolean shouldRunOnUIThread = false;
231
Class<?> clazz = callback.getClass();
232
while ((clazz != null) && !shouldRunOnUIThread) {
233
shouldRunOnUIThread = clazz
234
.isAnnotationPresent(RunOnUIThread.class);
235
clazz = clazz.getSuperclass();
237
if (shouldRunOnUIThread
238
&& (Timeline.this.uiToolkitHandler != null)) {
239
Timeline.this.uiToolkitHandler.runOnUIThread(mainObject,
243
if (Timeline.this.getState() == TimelineState.CANCELLED)
245
// System.err.println("Timeline @"
246
// + Timeline.this.hashCode());
247
callback.onTimelinePulse(durationFraction,
252
// System.err.println("Timeline @" +
253
// Timeline.this.hashCode());
255
.onTimelinePulse(durationFraction, timelinePosition);
265
public Timeline(Object mainTimelineObject) {
266
this.mainObject = mainTimelineObject;
268
for (UIToolkitHandler uiToolkitHandler : TridentConfig.getInstance()
269
.getUIToolkitHandlers()) {
270
if (uiToolkitHandler.isHandlerFor(mainTimelineObject)) {
271
this.uiToolkitHandler = uiToolkitHandler;
276
// if the main timeline object is handled by a UI toolkit handler,
277
// the setters registered with the different addProperty
278
// APIs need to run with the matching threading policy
279
TimelineCallback setterCallback = (this.uiToolkitHandler != null) ? new UISetter()
281
this.callback = new Chain(setterCallback);
284
this.propertiesToInterpolate = new ArrayList<AbstractFieldInfo>();
285
this.id = Timeline.getId();
286
// this.loopsToLive = -1;
288
this.stateStack = new Stack<TimelineState>();
289
this.stateStack.push(TimelineState.IDLE);
292
this.ease = new Linear();
295
public final void setSecondaryID(Comparable<?> secondaryId) {
296
if (this.getState() != TimelineState.IDLE) {
297
throw new IllegalArgumentException(
298
"Cannot change state of non-idle timeline ["
299
+ this.toString() + "]");
301
this.secondaryId = secondaryId;
304
public final void setDuration(long durationMs) {
305
if (this.getState() != TimelineState.IDLE) {
306
throw new IllegalArgumentException(
307
"Cannot change state of non-idle timeline ["
308
+ this.toString() + "]");
310
this.duration = durationMs;
313
public final void setInitialDelay(long initialDelay) {
314
if (this.getState() != TimelineState.IDLE) {
315
throw new IllegalArgumentException(
316
"Cannot change state of non-idle timeline ["
317
+ this.toString() + "]");
319
this.initialDelay = initialDelay;
322
public final void setCycleDelay(long cycleDelay) {
323
if (this.getState() != TimelineState.IDLE) {
324
throw new IllegalArgumentException(
325
"Cannot change state of non-idle timeline ["
326
+ this.toString() + "]");
328
this.cycleDelay = cycleDelay;
331
public final void addCallback(TimelineCallback callback) {
332
if (this.getState() != TimelineState.IDLE) {
333
throw new IllegalArgumentException(
334
"Cannot change state of non-idle timeline ["
335
+ this.toString() + "]");
337
this.callback.addCallback(callback);
340
public final void removeCallback(TimelineCallback callback) {
341
if (this.getState() != TimelineState.IDLE) {
342
throw new IllegalArgumentException(
343
"Cannot change state of non-idle timeline ["
344
+ this.toString() + "]");
346
this.callback.removeCallback(callback);
349
public static <T> TimelinePropertyBuilder<T> property(String propertyName) {
350
return new TimelinePropertyBuilder<T>(propertyName);
353
public final <T> void addPropertyToInterpolate(
354
TimelinePropertyBuilder<T> propertyBuilder) {
355
this.propertiesToInterpolate.add(propertyBuilder.getFieldInfo(this));
358
public final <T> void addPropertyToInterpolate(String propName,
359
KeyFrames<T> keyFrames) {
360
this.addPropertyToInterpolate(Timeline.<T> property(propName)
361
.goingThrough(keyFrames));
364
public final <T> void addPropertyToInterpolate(String propName, T from, T to) {
365
this.addPropertyToInterpolate(Timeline.<T> property(propName)
371
this.playSkipping(0);
374
public void playSkipping(final long msToSkip) {
375
if ((this.initialDelay + this.duration) < msToSkip) {
376
throw new IllegalArgumentException(
377
"Required skip longer than initial delay + duration");
379
TimelineEngine.getInstance().runTimelineOperation(this,
380
TimelineOperationKind.PLAY, new Runnable() {
383
Timeline.this.isLooping = false;
384
TimelineEngine.getInstance().play(Timeline.this, false,
390
public void playReverse() {
391
playReverseSkipping(0);
394
public void playReverseSkipping(final long msToSkip) {
395
if ((this.initialDelay + this.duration) < msToSkip) {
396
throw new IllegalArgumentException(
397
"Required skip longer than initial delay + duration");
399
TimelineEngine.getInstance().runTimelineOperation(this,
400
TimelineOperationKind.PLAY, new Runnable() {
403
Timeline.this.isLooping = false;
404
TimelineEngine.getInstance().playReverse(Timeline.this,
410
public void replay() {
411
TimelineEngine.getInstance().runTimelineOperation(this,
412
TimelineOperationKind.PLAY, new Runnable() {
415
Timeline.this.isLooping = false;
416
TimelineEngine.getInstance().play(Timeline.this, true,
422
public void replayReverse() {
423
TimelineEngine.getInstance().runTimelineOperation(this,
424
TimelineOperationKind.PLAY, new Runnable() {
427
Timeline.this.isLooping = false;
428
TimelineEngine.getInstance().playReverse(Timeline.this,
434
public void playLoop(RepeatBehavior repeatBehavior) {
435
this.playLoop(-1, repeatBehavior);
438
public void playLoopSkipping(RepeatBehavior repeatBehavior,
439
final long msToSkip) {
440
this.playLoopSkipping(-1, repeatBehavior, msToSkip);
443
public void playLoop(int loopCount, RepeatBehavior repeatBehavior) {
444
this.playLoopSkipping(loopCount, repeatBehavior, 0);
447
public void playLoopSkipping(final int loopCount,
448
final RepeatBehavior repeatBehavior, final long msToSkip) {
449
if ((this.initialDelay + this.duration) < msToSkip) {
450
throw new IllegalArgumentException(
451
"Required skip longer than initial delay + duration");
453
TimelineEngine.getInstance().runTimelineOperation(this,
454
TimelineOperationKind.PLAY, new Runnable() {
457
Timeline.this.isLooping = true;
458
Timeline.this.repeatCount = loopCount;
459
Timeline.this.repeatBehavior = repeatBehavior;
460
TimelineEngine.getInstance().playLoop(Timeline.this,
467
* Cancels this timeline. The timeline transitions to the
468
* {@link TimelineState#CANCELLED} state, preserving its current timeline
469
* position. After application callbacks and field interpolations are done
470
* on the {@link TimelineState#CANCELLED} state, the timeline transitions to
471
* the {@link TimelineState#IDLE} state. Application callbacks and field
472
* interpolations are done on this state as well.
477
public void cancel() {
478
TimelineEngine.getInstance().runTimelineOperation(this,
479
TimelineOperationKind.CANCEL, null);
483
* Ends this timeline. The timeline transitions to the
484
* {@link TimelineState#DONE} state, with the timeline position set to 0.0
485
* or 1.0 - based on the direction of the timeline. After application
486
* callbacks and field interpolations are done on the
487
* {@link TimelineState#DONE} state, the timeline transitions to the
488
* {@link TimelineState#IDLE} state. Application callbacks and field
489
* interpolations are done on this state as well.
495
TimelineEngine.getInstance().runTimelineOperation(this,
496
TimelineOperationKind.END, null);
500
* Aborts this timeline. The timeline transitions to the
501
* {@link TimelineState#IDLE} state. No application callbacks or field
502
* interpolations are done.
507
public void abort() {
508
TimelineEngine.getInstance().runTimelineOperation(this,
509
TimelineOperationKind.ABORT, null);
512
public void suspend() {
513
TimelineEngine.getInstance().runTimelineOperation(this,
514
TimelineOperationKind.SUSPEND, null);
517
public void resume() {
518
TimelineEngine.getInstance().runTimelineOperation(this,
519
TimelineOperationKind.RESUME, null);
523
* Requests that the specified timeline should stop at the end of the cycle.
524
* This method should be called only on looping timelines.
526
public void cancelAtCycleBreak() {
528
throw new IllegalArgumentException(
529
"Can only be called on looping timelines");
530
this.toCancelAtCycleBreak = true;
534
* Returns a unique ID.
538
protected static synchronized long getId() {
542
public final float getTimelinePosition() {
543
return this.timelinePosition;
546
public final float getDurationFraction() {
547
return this.durationFraction;
550
public final TimelineState getState() {
551
return this.stateStack.peek();
554
public final void setEase(TimelineEase ease) {
555
if (this.getState() != TimelineState.IDLE) {
556
throw new IllegalArgumentException(
557
"Cannot change state of non-idle timeline");
563
public boolean isDone() {
564
return (this.doneCount > 0);
568
public boolean supportsReplay() {
573
public void resetDoneFlag() {
578
public String toString() {
579
StringBuffer res = new StringBuffer();
580
if (this.name != null) {
581
res.append(this.name);
583
if (this.mainObject != null) {
584
res.append(":" + this.mainObject.getClass().getName());
586
if (this.secondaryId != null) {
587
res.append(":" + this.secondaryId.toString());
590
res.append(" " + this.getState().name());
591
res.append(":" + this.timelinePosition);
593
return res.toString();
596
void replaceState(TimelineState state) {
597
this.stateStack.pop();
598
this.pushState(state);
601
void pushState(TimelineState state) {
602
if (state == TimelineState.DONE)
604
this.stateStack.add(state);
607
TimelineState popState() {
608
return this.stateStack.pop();
611
public final long getDuration() {
612
return this.duration;
615
public String getName() {
619
public void setName(String name) {
623
public Object getMainObject() {
624
return this.mainObject;
b'\\ No newline at end of file'