1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/animation/PropertyAnimator.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,341 @@ 1.4 +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- 1.5 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.7 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +package org.mozilla.gecko.animation; 1.10 + 1.11 +import android.support.v4.view.ViewCompat; 1.12 +import android.os.Build; 1.13 +import android.os.Handler; 1.14 +import android.view.Choreographer; 1.15 +import android.view.View; 1.16 +import android.view.ViewGroup; 1.17 +import android.view.ViewTreeObserver; 1.18 +import android.view.animation.AnimationUtils; 1.19 +import android.view.animation.DecelerateInterpolator; 1.20 +import android.view.animation.Interpolator; 1.21 + 1.22 +import java.util.ArrayList; 1.23 +import java.util.List; 1.24 + 1.25 +public class PropertyAnimator implements Runnable { 1.26 + private static final String LOGTAG = "GeckoPropertyAnimator"; 1.27 + 1.28 + public static enum Property { 1.29 + ALPHA, 1.30 + TRANSLATION_X, 1.31 + TRANSLATION_Y, 1.32 + SCROLL_X, 1.33 + SCROLL_Y, 1.34 + WIDTH, 1.35 + HEIGHT 1.36 + } 1.37 + 1.38 + private class ElementHolder { 1.39 + View view; 1.40 + AnimatorProxy proxy; 1.41 + Property property; 1.42 + float from; 1.43 + float to; 1.44 + } 1.45 + 1.46 + public static interface PropertyAnimationListener { 1.47 + public void onPropertyAnimationStart(); 1.48 + public void onPropertyAnimationEnd(); 1.49 + } 1.50 + 1.51 + private Interpolator mInterpolator; 1.52 + private long mStartTime; 1.53 + private long mDuration; 1.54 + private float mDurationReciprocal; 1.55 + private List<ElementHolder> mElementsList; 1.56 + private List<PropertyAnimationListener> mListeners; 1.57 + private FramePoster mFramePoster; 1.58 + private boolean mUseHardwareLayer; 1.59 + 1.60 + public PropertyAnimator(long duration) { 1.61 + this(duration, new DecelerateInterpolator()); 1.62 + } 1.63 + 1.64 + public PropertyAnimator(long duration, Interpolator interpolator) { 1.65 + mDuration = duration; 1.66 + mDurationReciprocal = 1.0f / (float) mDuration; 1.67 + mInterpolator = interpolator; 1.68 + mElementsList = new ArrayList<ElementHolder>(); 1.69 + mFramePoster = FramePoster.create(this); 1.70 + mUseHardwareLayer = true; 1.71 + mListeners = null; 1.72 + } 1.73 + 1.74 + public void setUseHardwareLayer(boolean useHardwareLayer) { 1.75 + mUseHardwareLayer = useHardwareLayer; 1.76 + } 1.77 + 1.78 + public void attach(View view, Property property, float to) { 1.79 + ElementHolder element = new ElementHolder(); 1.80 + 1.81 + element.view = view; 1.82 + element.proxy = AnimatorProxy.create(view); 1.83 + element.property = property; 1.84 + element.to = to; 1.85 + 1.86 + mElementsList.add(element); 1.87 + } 1.88 + 1.89 + public void addPropertyAnimationListener(PropertyAnimationListener listener) { 1.90 + if (mListeners == null) { 1.91 + mListeners = new ArrayList<PropertyAnimationListener>(); 1.92 + } 1.93 + 1.94 + mListeners.add(listener); 1.95 + } 1.96 + 1.97 + public long getDuration() { 1.98 + return mDuration; 1.99 + } 1.100 + 1.101 + public long getRemainingTime() { 1.102 + int timePassed = (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime); 1.103 + return mDuration - timePassed; 1.104 + } 1.105 + 1.106 + @Override 1.107 + public void run() { 1.108 + int timePassed = (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime); 1.109 + if (timePassed >= mDuration) { 1.110 + stop(); 1.111 + return; 1.112 + } 1.113 + 1.114 + float interpolation = mInterpolator.getInterpolation(timePassed * mDurationReciprocal); 1.115 + 1.116 + for (ElementHolder element : mElementsList) { 1.117 + float delta = element.from + ((element.to - element.from) * interpolation); 1.118 + invalidate(element, delta); 1.119 + } 1.120 + 1.121 + mFramePoster.postNextAnimationFrame(); 1.122 + } 1.123 + 1.124 + public void start() { 1.125 + if (mDuration == 0) { 1.126 + return; 1.127 + } 1.128 + 1.129 + mStartTime = AnimationUtils.currentAnimationTimeMillis(); 1.130 + 1.131 + // Fix the from value based on current position and property 1.132 + for (ElementHolder element : mElementsList) { 1.133 + if (element.property == Property.ALPHA) 1.134 + element.from = element.proxy.getAlpha(); 1.135 + else if (element.property == Property.TRANSLATION_Y) 1.136 + element.from = element.proxy.getTranslationY(); 1.137 + else if (element.property == Property.TRANSLATION_X) 1.138 + element.from = element.proxy.getTranslationX(); 1.139 + else if (element.property == Property.SCROLL_Y) 1.140 + element.from = element.proxy.getScrollY(); 1.141 + else if (element.property == Property.SCROLL_X) 1.142 + element.from = element.proxy.getScrollX(); 1.143 + else if (element.property == Property.WIDTH) 1.144 + element.from = element.proxy.getWidth(); 1.145 + else if (element.property == Property.HEIGHT) 1.146 + element.from = element.proxy.getHeight(); 1.147 + 1.148 + ViewCompat.setHasTransientState(element.view, true); 1.149 + 1.150 + if (shouldEnableHardwareLayer(element)) 1.151 + element.view.setLayerType(View.LAYER_TYPE_HARDWARE, null); 1.152 + else 1.153 + element.view.setDrawingCacheEnabled(true); 1.154 + } 1.155 + 1.156 + // Get ViewTreeObserver from any of the participant views 1.157 + // in the animation. 1.158 + final ViewTreeObserver treeObserver; 1.159 + if (mElementsList.size() > 0) { 1.160 + treeObserver = mElementsList.get(0).view.getViewTreeObserver(); 1.161 + } else { 1.162 + treeObserver = null; 1.163 + } 1.164 + 1.165 + // Try to start animation after any on-going layout round 1.166 + // in the current view tree. OnPreDrawListener seems broken 1.167 + // on pre-Honeycomb devices, start animation immediatelly 1.168 + // in this case. 1.169 + if (Build.VERSION.SDK_INT >= 11 && treeObserver != null && treeObserver.isAlive()) { 1.170 + treeObserver.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 1.171 + @Override 1.172 + public boolean onPreDraw() { 1.173 + if (treeObserver.isAlive()) { 1.174 + treeObserver.removeOnPreDrawListener(this); 1.175 + } 1.176 + 1.177 + mFramePoster.postFirstAnimationFrame(); 1.178 + return true; 1.179 + } 1.180 + }); 1.181 + } else { 1.182 + mFramePoster.postFirstAnimationFrame(); 1.183 + } 1.184 + 1.185 + if (mListeners != null) { 1.186 + for (PropertyAnimationListener listener : mListeners) { 1.187 + listener.onPropertyAnimationStart(); 1.188 + } 1.189 + } 1.190 + } 1.191 + 1.192 + 1.193 + /** 1.194 + * Stop the animation, optionally snapping to the end position. 1.195 + * onPropertyAnimationEnd is only called when snapping to the end position. 1.196 + */ 1.197 + public void stop(boolean snapToEndPosition) { 1.198 + mFramePoster.cancelAnimationFrame(); 1.199 + 1.200 + // Make sure to snap to the end position. 1.201 + for (ElementHolder element : mElementsList) { 1.202 + if (snapToEndPosition) 1.203 + invalidate(element, element.to); 1.204 + 1.205 + ViewCompat.setHasTransientState(element.view, false); 1.206 + 1.207 + if (shouldEnableHardwareLayer(element)) 1.208 + element.view.setLayerType(View.LAYER_TYPE_NONE, null); 1.209 + else 1.210 + element.view.setDrawingCacheEnabled(false); 1.211 + } 1.212 + 1.213 + mElementsList.clear(); 1.214 + 1.215 + if (mListeners != null) { 1.216 + if (snapToEndPosition) { 1.217 + for (PropertyAnimationListener listener : mListeners) { 1.218 + listener.onPropertyAnimationEnd(); 1.219 + } 1.220 + } 1.221 + 1.222 + mListeners.clear(); 1.223 + mListeners = null; 1.224 + } 1.225 + } 1.226 + 1.227 + public void stop() { 1.228 + stop(true); 1.229 + } 1.230 + 1.231 + private boolean shouldEnableHardwareLayer(ElementHolder element) { 1.232 + if (!mUseHardwareLayer) 1.233 + return false; 1.234 + 1.235 + if (Build.VERSION.SDK_INT < 11) 1.236 + return false; 1.237 + 1.238 + if (!(element.view instanceof ViewGroup)) 1.239 + return false; 1.240 + 1.241 + if (element.property == Property.ALPHA || 1.242 + element.property == Property.TRANSLATION_Y || 1.243 + element.property == Property.TRANSLATION_X) 1.244 + return true; 1.245 + 1.246 + return false; 1.247 + } 1.248 + 1.249 + private void invalidate(final ElementHolder element, final float delta) { 1.250 + final View view = element.view; 1.251 + 1.252 + // check to see if the view was detached between the check above and this code 1.253 + // getting run on the UI thread. 1.254 + if (view.getHandler() == null) 1.255 + return; 1.256 + 1.257 + if (element.property == Property.ALPHA) 1.258 + element.proxy.setAlpha(delta); 1.259 + else if (element.property == Property.TRANSLATION_Y) 1.260 + element.proxy.setTranslationY(delta); 1.261 + else if (element.property == Property.TRANSLATION_X) 1.262 + element.proxy.setTranslationX(delta); 1.263 + else if (element.property == Property.SCROLL_Y) 1.264 + element.proxy.scrollTo(element.proxy.getScrollX(), (int) delta); 1.265 + else if (element.property == Property.SCROLL_X) 1.266 + element.proxy.scrollTo((int) delta, element.proxy.getScrollY()); 1.267 + else if (element.property == Property.WIDTH) 1.268 + element.proxy.setWidth((int) delta); 1.269 + else if (element.property == Property.HEIGHT) 1.270 + element.proxy.setHeight((int) delta); 1.271 + } 1.272 + 1.273 + private static abstract class FramePoster { 1.274 + public static FramePoster create(Runnable r) { 1.275 + if (Build.VERSION.SDK_INT >= 16) 1.276 + return new FramePosterPostJB(r); 1.277 + else 1.278 + return new FramePosterPreJB(r); 1.279 + } 1.280 + 1.281 + public abstract void postFirstAnimationFrame(); 1.282 + public abstract void postNextAnimationFrame(); 1.283 + public abstract void cancelAnimationFrame(); 1.284 + } 1.285 + 1.286 + private static class FramePosterPreJB extends FramePoster { 1.287 + // Default refresh rate in ms. 1.288 + private static final int INTERVAL = 10; 1.289 + 1.290 + private Handler mHandler; 1.291 + private Runnable mRunnable; 1.292 + 1.293 + public FramePosterPreJB(Runnable r) { 1.294 + mHandler = new Handler(); 1.295 + mRunnable = r; 1.296 + } 1.297 + 1.298 + @Override 1.299 + public void postFirstAnimationFrame() { 1.300 + mHandler.post(mRunnable); 1.301 + } 1.302 + 1.303 + @Override 1.304 + public void postNextAnimationFrame() { 1.305 + mHandler.postDelayed(mRunnable, INTERVAL); 1.306 + } 1.307 + 1.308 + @Override 1.309 + public void cancelAnimationFrame() { 1.310 + mHandler.removeCallbacks(mRunnable); 1.311 + } 1.312 + } 1.313 + 1.314 + private static class FramePosterPostJB extends FramePoster { 1.315 + private Choreographer mChoreographer; 1.316 + private Choreographer.FrameCallback mCallback; 1.317 + 1.318 + public FramePosterPostJB(final Runnable r) { 1.319 + mChoreographer = Choreographer.getInstance(); 1.320 + 1.321 + mCallback = new Choreographer.FrameCallback() { 1.322 + @Override 1.323 + public void doFrame(long frameTimeNanos) { 1.324 + r.run(); 1.325 + } 1.326 + }; 1.327 + } 1.328 + 1.329 + @Override 1.330 + public void postFirstAnimationFrame() { 1.331 + postNextAnimationFrame(); 1.332 + } 1.333 + 1.334 + @Override 1.335 + public void postNextAnimationFrame() { 1.336 + mChoreographer.postFrameCallback(mCallback); 1.337 + } 1.338 + 1.339 + @Override 1.340 + public void cancelAnimationFrame() { 1.341 + mChoreographer.removeFrameCallback(mCallback); 1.342 + } 1.343 + } 1.344 +}