|
1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- |
|
2 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
3 * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
|
4 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
5 |
|
6 package org.mozilla.gecko.animation; |
|
7 |
|
8 import android.support.v4.view.ViewCompat; |
|
9 import android.os.Build; |
|
10 import android.os.Handler; |
|
11 import android.view.Choreographer; |
|
12 import android.view.View; |
|
13 import android.view.ViewGroup; |
|
14 import android.view.ViewTreeObserver; |
|
15 import android.view.animation.AnimationUtils; |
|
16 import android.view.animation.DecelerateInterpolator; |
|
17 import android.view.animation.Interpolator; |
|
18 |
|
19 import java.util.ArrayList; |
|
20 import java.util.List; |
|
21 |
|
22 public class PropertyAnimator implements Runnable { |
|
23 private static final String LOGTAG = "GeckoPropertyAnimator"; |
|
24 |
|
25 public static enum Property { |
|
26 ALPHA, |
|
27 TRANSLATION_X, |
|
28 TRANSLATION_Y, |
|
29 SCROLL_X, |
|
30 SCROLL_Y, |
|
31 WIDTH, |
|
32 HEIGHT |
|
33 } |
|
34 |
|
35 private class ElementHolder { |
|
36 View view; |
|
37 AnimatorProxy proxy; |
|
38 Property property; |
|
39 float from; |
|
40 float to; |
|
41 } |
|
42 |
|
43 public static interface PropertyAnimationListener { |
|
44 public void onPropertyAnimationStart(); |
|
45 public void onPropertyAnimationEnd(); |
|
46 } |
|
47 |
|
48 private Interpolator mInterpolator; |
|
49 private long mStartTime; |
|
50 private long mDuration; |
|
51 private float mDurationReciprocal; |
|
52 private List<ElementHolder> mElementsList; |
|
53 private List<PropertyAnimationListener> mListeners; |
|
54 private FramePoster mFramePoster; |
|
55 private boolean mUseHardwareLayer; |
|
56 |
|
57 public PropertyAnimator(long duration) { |
|
58 this(duration, new DecelerateInterpolator()); |
|
59 } |
|
60 |
|
61 public PropertyAnimator(long duration, Interpolator interpolator) { |
|
62 mDuration = duration; |
|
63 mDurationReciprocal = 1.0f / (float) mDuration; |
|
64 mInterpolator = interpolator; |
|
65 mElementsList = new ArrayList<ElementHolder>(); |
|
66 mFramePoster = FramePoster.create(this); |
|
67 mUseHardwareLayer = true; |
|
68 mListeners = null; |
|
69 } |
|
70 |
|
71 public void setUseHardwareLayer(boolean useHardwareLayer) { |
|
72 mUseHardwareLayer = useHardwareLayer; |
|
73 } |
|
74 |
|
75 public void attach(View view, Property property, float to) { |
|
76 ElementHolder element = new ElementHolder(); |
|
77 |
|
78 element.view = view; |
|
79 element.proxy = AnimatorProxy.create(view); |
|
80 element.property = property; |
|
81 element.to = to; |
|
82 |
|
83 mElementsList.add(element); |
|
84 } |
|
85 |
|
86 public void addPropertyAnimationListener(PropertyAnimationListener listener) { |
|
87 if (mListeners == null) { |
|
88 mListeners = new ArrayList<PropertyAnimationListener>(); |
|
89 } |
|
90 |
|
91 mListeners.add(listener); |
|
92 } |
|
93 |
|
94 public long getDuration() { |
|
95 return mDuration; |
|
96 } |
|
97 |
|
98 public long getRemainingTime() { |
|
99 int timePassed = (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime); |
|
100 return mDuration - timePassed; |
|
101 } |
|
102 |
|
103 @Override |
|
104 public void run() { |
|
105 int timePassed = (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime); |
|
106 if (timePassed >= mDuration) { |
|
107 stop(); |
|
108 return; |
|
109 } |
|
110 |
|
111 float interpolation = mInterpolator.getInterpolation(timePassed * mDurationReciprocal); |
|
112 |
|
113 for (ElementHolder element : mElementsList) { |
|
114 float delta = element.from + ((element.to - element.from) * interpolation); |
|
115 invalidate(element, delta); |
|
116 } |
|
117 |
|
118 mFramePoster.postNextAnimationFrame(); |
|
119 } |
|
120 |
|
121 public void start() { |
|
122 if (mDuration == 0) { |
|
123 return; |
|
124 } |
|
125 |
|
126 mStartTime = AnimationUtils.currentAnimationTimeMillis(); |
|
127 |
|
128 // Fix the from value based on current position and property |
|
129 for (ElementHolder element : mElementsList) { |
|
130 if (element.property == Property.ALPHA) |
|
131 element.from = element.proxy.getAlpha(); |
|
132 else if (element.property == Property.TRANSLATION_Y) |
|
133 element.from = element.proxy.getTranslationY(); |
|
134 else if (element.property == Property.TRANSLATION_X) |
|
135 element.from = element.proxy.getTranslationX(); |
|
136 else if (element.property == Property.SCROLL_Y) |
|
137 element.from = element.proxy.getScrollY(); |
|
138 else if (element.property == Property.SCROLL_X) |
|
139 element.from = element.proxy.getScrollX(); |
|
140 else if (element.property == Property.WIDTH) |
|
141 element.from = element.proxy.getWidth(); |
|
142 else if (element.property == Property.HEIGHT) |
|
143 element.from = element.proxy.getHeight(); |
|
144 |
|
145 ViewCompat.setHasTransientState(element.view, true); |
|
146 |
|
147 if (shouldEnableHardwareLayer(element)) |
|
148 element.view.setLayerType(View.LAYER_TYPE_HARDWARE, null); |
|
149 else |
|
150 element.view.setDrawingCacheEnabled(true); |
|
151 } |
|
152 |
|
153 // Get ViewTreeObserver from any of the participant views |
|
154 // in the animation. |
|
155 final ViewTreeObserver treeObserver; |
|
156 if (mElementsList.size() > 0) { |
|
157 treeObserver = mElementsList.get(0).view.getViewTreeObserver(); |
|
158 } else { |
|
159 treeObserver = null; |
|
160 } |
|
161 |
|
162 // Try to start animation after any on-going layout round |
|
163 // in the current view tree. OnPreDrawListener seems broken |
|
164 // on pre-Honeycomb devices, start animation immediatelly |
|
165 // in this case. |
|
166 if (Build.VERSION.SDK_INT >= 11 && treeObserver != null && treeObserver.isAlive()) { |
|
167 treeObserver.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { |
|
168 @Override |
|
169 public boolean onPreDraw() { |
|
170 if (treeObserver.isAlive()) { |
|
171 treeObserver.removeOnPreDrawListener(this); |
|
172 } |
|
173 |
|
174 mFramePoster.postFirstAnimationFrame(); |
|
175 return true; |
|
176 } |
|
177 }); |
|
178 } else { |
|
179 mFramePoster.postFirstAnimationFrame(); |
|
180 } |
|
181 |
|
182 if (mListeners != null) { |
|
183 for (PropertyAnimationListener listener : mListeners) { |
|
184 listener.onPropertyAnimationStart(); |
|
185 } |
|
186 } |
|
187 } |
|
188 |
|
189 |
|
190 /** |
|
191 * Stop the animation, optionally snapping to the end position. |
|
192 * onPropertyAnimationEnd is only called when snapping to the end position. |
|
193 */ |
|
194 public void stop(boolean snapToEndPosition) { |
|
195 mFramePoster.cancelAnimationFrame(); |
|
196 |
|
197 // Make sure to snap to the end position. |
|
198 for (ElementHolder element : mElementsList) { |
|
199 if (snapToEndPosition) |
|
200 invalidate(element, element.to); |
|
201 |
|
202 ViewCompat.setHasTransientState(element.view, false); |
|
203 |
|
204 if (shouldEnableHardwareLayer(element)) |
|
205 element.view.setLayerType(View.LAYER_TYPE_NONE, null); |
|
206 else |
|
207 element.view.setDrawingCacheEnabled(false); |
|
208 } |
|
209 |
|
210 mElementsList.clear(); |
|
211 |
|
212 if (mListeners != null) { |
|
213 if (snapToEndPosition) { |
|
214 for (PropertyAnimationListener listener : mListeners) { |
|
215 listener.onPropertyAnimationEnd(); |
|
216 } |
|
217 } |
|
218 |
|
219 mListeners.clear(); |
|
220 mListeners = null; |
|
221 } |
|
222 } |
|
223 |
|
224 public void stop() { |
|
225 stop(true); |
|
226 } |
|
227 |
|
228 private boolean shouldEnableHardwareLayer(ElementHolder element) { |
|
229 if (!mUseHardwareLayer) |
|
230 return false; |
|
231 |
|
232 if (Build.VERSION.SDK_INT < 11) |
|
233 return false; |
|
234 |
|
235 if (!(element.view instanceof ViewGroup)) |
|
236 return false; |
|
237 |
|
238 if (element.property == Property.ALPHA || |
|
239 element.property == Property.TRANSLATION_Y || |
|
240 element.property == Property.TRANSLATION_X) |
|
241 return true; |
|
242 |
|
243 return false; |
|
244 } |
|
245 |
|
246 private void invalidate(final ElementHolder element, final float delta) { |
|
247 final View view = element.view; |
|
248 |
|
249 // check to see if the view was detached between the check above and this code |
|
250 // getting run on the UI thread. |
|
251 if (view.getHandler() == null) |
|
252 return; |
|
253 |
|
254 if (element.property == Property.ALPHA) |
|
255 element.proxy.setAlpha(delta); |
|
256 else if (element.property == Property.TRANSLATION_Y) |
|
257 element.proxy.setTranslationY(delta); |
|
258 else if (element.property == Property.TRANSLATION_X) |
|
259 element.proxy.setTranslationX(delta); |
|
260 else if (element.property == Property.SCROLL_Y) |
|
261 element.proxy.scrollTo(element.proxy.getScrollX(), (int) delta); |
|
262 else if (element.property == Property.SCROLL_X) |
|
263 element.proxy.scrollTo((int) delta, element.proxy.getScrollY()); |
|
264 else if (element.property == Property.WIDTH) |
|
265 element.proxy.setWidth((int) delta); |
|
266 else if (element.property == Property.HEIGHT) |
|
267 element.proxy.setHeight((int) delta); |
|
268 } |
|
269 |
|
270 private static abstract class FramePoster { |
|
271 public static FramePoster create(Runnable r) { |
|
272 if (Build.VERSION.SDK_INT >= 16) |
|
273 return new FramePosterPostJB(r); |
|
274 else |
|
275 return new FramePosterPreJB(r); |
|
276 } |
|
277 |
|
278 public abstract void postFirstAnimationFrame(); |
|
279 public abstract void postNextAnimationFrame(); |
|
280 public abstract void cancelAnimationFrame(); |
|
281 } |
|
282 |
|
283 private static class FramePosterPreJB extends FramePoster { |
|
284 // Default refresh rate in ms. |
|
285 private static final int INTERVAL = 10; |
|
286 |
|
287 private Handler mHandler; |
|
288 private Runnable mRunnable; |
|
289 |
|
290 public FramePosterPreJB(Runnable r) { |
|
291 mHandler = new Handler(); |
|
292 mRunnable = r; |
|
293 } |
|
294 |
|
295 @Override |
|
296 public void postFirstAnimationFrame() { |
|
297 mHandler.post(mRunnable); |
|
298 } |
|
299 |
|
300 @Override |
|
301 public void postNextAnimationFrame() { |
|
302 mHandler.postDelayed(mRunnable, INTERVAL); |
|
303 } |
|
304 |
|
305 @Override |
|
306 public void cancelAnimationFrame() { |
|
307 mHandler.removeCallbacks(mRunnable); |
|
308 } |
|
309 } |
|
310 |
|
311 private static class FramePosterPostJB extends FramePoster { |
|
312 private Choreographer mChoreographer; |
|
313 private Choreographer.FrameCallback mCallback; |
|
314 |
|
315 public FramePosterPostJB(final Runnable r) { |
|
316 mChoreographer = Choreographer.getInstance(); |
|
317 |
|
318 mCallback = new Choreographer.FrameCallback() { |
|
319 @Override |
|
320 public void doFrame(long frameTimeNanos) { |
|
321 r.run(); |
|
322 } |
|
323 }; |
|
324 } |
|
325 |
|
326 @Override |
|
327 public void postFirstAnimationFrame() { |
|
328 postNextAnimationFrame(); |
|
329 } |
|
330 |
|
331 @Override |
|
332 public void postNextAnimationFrame() { |
|
333 mChoreographer.postFrameCallback(mCallback); |
|
334 } |
|
335 |
|
336 @Override |
|
337 public void cancelAnimationFrame() { |
|
338 mChoreographer.removeFrameCallback(mCallback); |
|
339 } |
|
340 } |
|
341 } |