diff -r 000000000000 -r 6474c204b198 mobile/android/base/animation/PropertyAnimator.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mobile/android/base/animation/PropertyAnimator.java Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,341 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.animation; + +import android.support.v4.view.ViewCompat; +import android.os.Build; +import android.os.Handler; +import android.view.Choreographer; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.animation.AnimationUtils; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +import java.util.ArrayList; +import java.util.List; + +public class PropertyAnimator implements Runnable { + private static final String LOGTAG = "GeckoPropertyAnimator"; + + public static enum Property { + ALPHA, + TRANSLATION_X, + TRANSLATION_Y, + SCROLL_X, + SCROLL_Y, + WIDTH, + HEIGHT + } + + private class ElementHolder { + View view; + AnimatorProxy proxy; + Property property; + float from; + float to; + } + + public static interface PropertyAnimationListener { + public void onPropertyAnimationStart(); + public void onPropertyAnimationEnd(); + } + + private Interpolator mInterpolator; + private long mStartTime; + private long mDuration; + private float mDurationReciprocal; + private List mElementsList; + private List mListeners; + private FramePoster mFramePoster; + private boolean mUseHardwareLayer; + + public PropertyAnimator(long duration) { + this(duration, new DecelerateInterpolator()); + } + + public PropertyAnimator(long duration, Interpolator interpolator) { + mDuration = duration; + mDurationReciprocal = 1.0f / (float) mDuration; + mInterpolator = interpolator; + mElementsList = new ArrayList(); + mFramePoster = FramePoster.create(this); + mUseHardwareLayer = true; + mListeners = null; + } + + public void setUseHardwareLayer(boolean useHardwareLayer) { + mUseHardwareLayer = useHardwareLayer; + } + + public void attach(View view, Property property, float to) { + ElementHolder element = new ElementHolder(); + + element.view = view; + element.proxy = AnimatorProxy.create(view); + element.property = property; + element.to = to; + + mElementsList.add(element); + } + + public void addPropertyAnimationListener(PropertyAnimationListener listener) { + if (mListeners == null) { + mListeners = new ArrayList(); + } + + mListeners.add(listener); + } + + public long getDuration() { + return mDuration; + } + + public long getRemainingTime() { + int timePassed = (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime); + return mDuration - timePassed; + } + + @Override + public void run() { + int timePassed = (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime); + if (timePassed >= mDuration) { + stop(); + return; + } + + float interpolation = mInterpolator.getInterpolation(timePassed * mDurationReciprocal); + + for (ElementHolder element : mElementsList) { + float delta = element.from + ((element.to - element.from) * interpolation); + invalidate(element, delta); + } + + mFramePoster.postNextAnimationFrame(); + } + + public void start() { + if (mDuration == 0) { + return; + } + + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + + // Fix the from value based on current position and property + for (ElementHolder element : mElementsList) { + if (element.property == Property.ALPHA) + element.from = element.proxy.getAlpha(); + else if (element.property == Property.TRANSLATION_Y) + element.from = element.proxy.getTranslationY(); + else if (element.property == Property.TRANSLATION_X) + element.from = element.proxy.getTranslationX(); + else if (element.property == Property.SCROLL_Y) + element.from = element.proxy.getScrollY(); + else if (element.property == Property.SCROLL_X) + element.from = element.proxy.getScrollX(); + else if (element.property == Property.WIDTH) + element.from = element.proxy.getWidth(); + else if (element.property == Property.HEIGHT) + element.from = element.proxy.getHeight(); + + ViewCompat.setHasTransientState(element.view, true); + + if (shouldEnableHardwareLayer(element)) + element.view.setLayerType(View.LAYER_TYPE_HARDWARE, null); + else + element.view.setDrawingCacheEnabled(true); + } + + // Get ViewTreeObserver from any of the participant views + // in the animation. + final ViewTreeObserver treeObserver; + if (mElementsList.size() > 0) { + treeObserver = mElementsList.get(0).view.getViewTreeObserver(); + } else { + treeObserver = null; + } + + // Try to start animation after any on-going layout round + // in the current view tree. OnPreDrawListener seems broken + // on pre-Honeycomb devices, start animation immediatelly + // in this case. + if (Build.VERSION.SDK_INT >= 11 && treeObserver != null && treeObserver.isAlive()) { + treeObserver.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + if (treeObserver.isAlive()) { + treeObserver.removeOnPreDrawListener(this); + } + + mFramePoster.postFirstAnimationFrame(); + return true; + } + }); + } else { + mFramePoster.postFirstAnimationFrame(); + } + + if (mListeners != null) { + for (PropertyAnimationListener listener : mListeners) { + listener.onPropertyAnimationStart(); + } + } + } + + + /** + * Stop the animation, optionally snapping to the end position. + * onPropertyAnimationEnd is only called when snapping to the end position. + */ + public void stop(boolean snapToEndPosition) { + mFramePoster.cancelAnimationFrame(); + + // Make sure to snap to the end position. + for (ElementHolder element : mElementsList) { + if (snapToEndPosition) + invalidate(element, element.to); + + ViewCompat.setHasTransientState(element.view, false); + + if (shouldEnableHardwareLayer(element)) + element.view.setLayerType(View.LAYER_TYPE_NONE, null); + else + element.view.setDrawingCacheEnabled(false); + } + + mElementsList.clear(); + + if (mListeners != null) { + if (snapToEndPosition) { + for (PropertyAnimationListener listener : mListeners) { + listener.onPropertyAnimationEnd(); + } + } + + mListeners.clear(); + mListeners = null; + } + } + + public void stop() { + stop(true); + } + + private boolean shouldEnableHardwareLayer(ElementHolder element) { + if (!mUseHardwareLayer) + return false; + + if (Build.VERSION.SDK_INT < 11) + return false; + + if (!(element.view instanceof ViewGroup)) + return false; + + if (element.property == Property.ALPHA || + element.property == Property.TRANSLATION_Y || + element.property == Property.TRANSLATION_X) + return true; + + return false; + } + + private void invalidate(final ElementHolder element, final float delta) { + final View view = element.view; + + // check to see if the view was detached between the check above and this code + // getting run on the UI thread. + if (view.getHandler() == null) + return; + + if (element.property == Property.ALPHA) + element.proxy.setAlpha(delta); + else if (element.property == Property.TRANSLATION_Y) + element.proxy.setTranslationY(delta); + else if (element.property == Property.TRANSLATION_X) + element.proxy.setTranslationX(delta); + else if (element.property == Property.SCROLL_Y) + element.proxy.scrollTo(element.proxy.getScrollX(), (int) delta); + else if (element.property == Property.SCROLL_X) + element.proxy.scrollTo((int) delta, element.proxy.getScrollY()); + else if (element.property == Property.WIDTH) + element.proxy.setWidth((int) delta); + else if (element.property == Property.HEIGHT) + element.proxy.setHeight((int) delta); + } + + private static abstract class FramePoster { + public static FramePoster create(Runnable r) { + if (Build.VERSION.SDK_INT >= 16) + return new FramePosterPostJB(r); + else + return new FramePosterPreJB(r); + } + + public abstract void postFirstAnimationFrame(); + public abstract void postNextAnimationFrame(); + public abstract void cancelAnimationFrame(); + } + + private static class FramePosterPreJB extends FramePoster { + // Default refresh rate in ms. + private static final int INTERVAL = 10; + + private Handler mHandler; + private Runnable mRunnable; + + public FramePosterPreJB(Runnable r) { + mHandler = new Handler(); + mRunnable = r; + } + + @Override + public void postFirstAnimationFrame() { + mHandler.post(mRunnable); + } + + @Override + public void postNextAnimationFrame() { + mHandler.postDelayed(mRunnable, INTERVAL); + } + + @Override + public void cancelAnimationFrame() { + mHandler.removeCallbacks(mRunnable); + } + } + + private static class FramePosterPostJB extends FramePoster { + private Choreographer mChoreographer; + private Choreographer.FrameCallback mCallback; + + public FramePosterPostJB(final Runnable r) { + mChoreographer = Choreographer.getInstance(); + + mCallback = new Choreographer.FrameCallback() { + @Override + public void doFrame(long frameTimeNanos) { + r.run(); + } + }; + } + + @Override + public void postFirstAnimationFrame() { + postNextAnimationFrame(); + } + + @Override + public void postNextAnimationFrame() { + mChoreographer.postFrameCallback(mCallback); + } + + @Override + public void cancelAnimationFrame() { + mChoreographer.removeFrameCallback(mCallback); + } + } +}