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