1 /*
2 * Copyright 2008 Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16 //--------------------------------------
17 // utgb-core Project
18 //
19 // Animation.java
20 // Since: Jun 13, 2010
21 //
22 //--------------------------------------
23 package org.utgenome.gwt.utgb.client.ui;
24
25 import java.util.ArrayList;
26 import java.util.List;
27
28 import com.google.gwt.core.client.Duration;
29 import com.google.gwt.user.client.Timer;
30
31 /**
32 * A modified version of the animation support class included in GWT. An {@link Animation} is a continuous event that
33 * updates progressively over time at a non-fixed frame rate.
34 */
35 public abstract class Animation {
36
37 protected Animation() {
38 this(25);
39 }
40
41 protected Animation(int default_frame_delay) {
42 DEFAULT_FRAME_DELAY = default_frame_delay;
43 }
44
45 /**
46 * The default time in milliseconds between frames.
47 */
48 private final int DEFAULT_FRAME_DELAY;
49
50 /**
51 * The {@link Animation Animations} that are currently in progress.
52 */
53 private static List<Animation> animations = null;
54
55 /**
56 * The {@link Timer} that applies the animations.
57 */
58 private static Timer animationTimer = null;
59
60 /**
61 * Update all {@link Animation Animations}.
62 */
63 private void updateAnimations() {
64 // Duplicate the animations list in case it changes as we iterate over it
65 Animation[] curAnimations = new Animation[animations.size()];
66 curAnimations = animations.toArray(curAnimations);
67
68 // Iterator through the animations
69 double curTime = Duration.currentTimeMillis();
70 for (Animation animation : curAnimations) {
71 if (animation.running && animation.update(curTime)) {
72 // We can't just remove the animation at the index, because calling
73 // animation.update may have the side effect of canceling this
74 // animation, running new animations, or canceling other animations.
75 animations.remove(animation);
76 }
77 }
78
79 // Reschedule the timer
80 if (animations.size() > 0) {
81 animationTimer.schedule(DEFAULT_FRAME_DELAY);
82 }
83 }
84
85 /**
86 * The duration of the {@link Animation} in milliseconds.
87 */
88 private int duration = -1;
89
90 /**
91 * Is the {@link Animation} running, even if {@link #onStart()} has not yet been called.
92 */
93 private boolean running = false;
94
95 /**
96 * Has the {@link Animation} actually started.
97 */
98 private boolean started = false;
99
100 /**
101 * The start time of the {@link Animation}.
102 */
103 private double startTime = -1;
104
105 /**
106 * Immediately cancel this animation. If the animation is running or is scheduled to run, {@link #onCancel()} will
107 * be called.
108 */
109 public void cancel() {
110 // Ignore if the animation is not currently running
111 if (!running) {
112 return;
113 }
114
115 animations.remove(this);
116 onCancel();
117 started = false;
118 running = false;
119 }
120
121 /**
122 * Immediately run this animation. If the animation is already running, it will be canceled first.
123 *
124 * @param duration
125 * the duration of the animation in milliseconds
126 */
127 public void run(int duration) {
128 run(duration, Duration.currentTimeMillis());
129 }
130
131 /**
132 * Run this animation at the given startTime. If the startTime has already passed, the animation will be synchronize
133 * as if it started at the specified start time. If the animation is already running, it will be canceled first.
134 *
135 * @param duration
136 * the duration of the animation in milliseconds
137 * @param startTime
138 * the synchronized start time in milliseconds
139 */
140 public void run(int duration, double startTime) {
141 // Cancel the animation if it is running
142 cancel();
143
144 // Save the duration and startTime
145 this.running = true;
146 this.duration = duration;
147 this.startTime = startTime;
148
149 // Start synchronously if start time has passed
150 if (update(Duration.currentTimeMillis())) {
151 return;
152 }
153
154 // Add to the list of animations
155 if (animations == null) {
156 animations = new ArrayList<Animation>();
157 animationTimer = new Timer() {
158 @Override
159 public void run() {
160 updateAnimations();
161 }
162 };
163 }
164 animations.add(this);
165
166 // Restart the timer if there is the only animation
167 if (animations.size() == 1) {
168 animationTimer.schedule(DEFAULT_FRAME_DELAY);
169 }
170 }
171
172 /**
173 * Interpolate the linear progress into a more natural easing function.
174 *
175 * Depending on the {@link Animation}, the return value of this method can be less than 0.0 or greater than 1.0.
176 *
177 * @param progress
178 * the linear progress, between 0.0 and 1.0
179 * @return the interpolated progress
180 */
181 protected double interpolate(double progress) {
182 return (1 + Math.cos(Math.PI + progress * Math.PI)) / 2;
183 }
184
185 /**
186 * Called immediately after the animation is canceled. The default implementation of this method calls
187 * {@link #onComplete()} only if the animation has actually started running.
188 */
189 protected void onCancel() {
190 if (started) {
191 onComplete();
192 }
193 }
194
195 /**
196 * Called immediately after the animation completes.
197 */
198 protected void onComplete() {
199 onUpdate(interpolate(1.0));
200 }
201
202 /**
203 * Called immediately before the animation starts.
204 */
205 protected void onStart() {
206 onUpdate(interpolate(0.0));
207 }
208
209 /**
210 * Called when the animation should be updated.
211 *
212 * The value of progress is between 0.0 and 1.0 inclusively (unless you override the {@link #interpolate(double)}
213 * method to provide a wider range of values). You can override {@link #onStart()} and {@link #onComplete()} to
214 * perform setup and tear down procedures.
215 */
216 protected abstract void onUpdate(double progress);
217
218 /**
219 * Update the {@link Animation}.
220 *
221 * @param curTime
222 * the current time
223 * @return true if the animation is complete, false if still running
224 */
225 private boolean update(double curTime) {
226 boolean finished = curTime >= startTime + duration;
227 if (started && !finished) {
228 // Animation is in progress.
229 double progress = (curTime - startTime) / duration;
230 onUpdate(interpolate(progress));
231 return false;
232 }
233 if (!started && curTime >= startTime) {
234 // Start the animation.
235 started = true;
236 onStart();
237 // Intentional fall through to possibly end the animation.
238 }
239 if (finished) {
240 // Animation is complete.
241 onComplete();
242 started = false;
243 running = false;
244 return true;
245 }
246 return false;
247 }
248 }