Bug 1180295 - Introduce the skeleton of a DynamicToolbarAnimator class alongside LayerMarginsAnimator. r=rbarker
authorKartikaya Gupta <kgupta@mozilla.com>
Tue, 18 Aug 2015 14:27:18 -0400
changeset 258259 44e23aca35f328a4685022f4f50369957fc7bba8
parent 258258 2e291287590c8a45960c63b5566d0ea9b6d12e29
child 258260 f8faf8d59274d3bcdf3837b819feee662a787ad4
push id29249
push userryanvm@gmail.com
push dateWed, 19 Aug 2015 11:17:27 +0000
treeherdermozilla-central@706b23a03d1c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrbarker
bugs1180295
milestone43.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1180295 - Introduce the skeleton of a DynamicToolbarAnimator class alongside LayerMarginsAnimator. r=rbarker
mobile/android/base/gfx/DynamicToolbarAnimator.java
mobile/android/base/gfx/GeckoLayerClient.java
mobile/android/base/gfx/LayerView.java
mobile/android/base/moz.build
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/gfx/DynamicToolbarAnimator.java
@@ -0,0 +1,226 @@
+/* -*- 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.gfx;
+
+import org.mozilla.gecko.PrefsHelper;
+import org.mozilla.gecko.util.FloatUtils;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.util.Log;
+import android.view.animation.DecelerateInterpolator;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class DynamicToolbarAnimator {
+    private static final String LOGTAG = "GeckoDynamicToolbarAnimator";
+    private static final String PREF_SCROLL_TOOLBAR_THRESHOLD = "browser.ui.scroll-toolbar-threshold";
+
+    // The duration of the animation in ns
+    private static final long ANIMATION_DURATION = 250000000;
+
+    private final GeckoLayerClient mTarget;
+    private final List<LayerView.DynamicToolbarListener> mListeners;
+
+    /* The translation to be applied to the toolbar UI view. This is the
+     * distance from the default/initial location (at the top of the screen,
+     * visible to the user) to where we want it to be. This variable should
+     * always be between 0 (toolbar fully visible) and the height of the toolbar
+     * (toolbar fully hidden), inclusive.
+     */
+    private float mToolbarTranslation;
+
+    /* The translation to be applied to the LayerView. This is the distance from
+     * the default/initial location (just below the toolbar, with the bottom
+     * extending past the bottom of the screen) to where we want it to be.
+     * This variable should always be between 0 and the height of the toolbar,
+     * inclusive.
+     */
+    private float mLayerViewTranslation;
+
+    /* This stores the maximum translation that can be applied to the toolbar
+     * and layerview when scrolling. This is populated with the height of the
+     * toolbar. */
+    private float mMaxTranslation;
+
+    /* If this boolean is true, scroll changes will not affect translation */
+    private boolean mPinned;
+
+    /* This interpolator is used for the above mentioned animation */
+    private DecelerateInterpolator mInterpolator;
+
+    /* This is the proportion of the viewport rect that needs to be travelled
+     * while scrolling before the translation will start taking effect.
+     */
+    private float SCROLL_TOOLBAR_THRESHOLD = 0.20f;
+    /* The ID of the prefs listener for the scroll-toolbar threshold */
+    private Integer mPrefObserverId;
+
+    /* The task that handles showing/hiding toolbar */
+    private DynamicToolbarAnimationTask mAnimationTask;
+
+    public DynamicToolbarAnimator(GeckoLayerClient aTarget) {
+        mTarget = aTarget;
+        mListeners = new ArrayList<LayerView.DynamicToolbarListener>();
+
+        mInterpolator = new DecelerateInterpolator();
+
+        // Listen to the dynamic toolbar pref
+        mPrefObserverId = PrefsHelper.getPref(PREF_SCROLL_TOOLBAR_THRESHOLD, new PrefsHelper.PrefHandlerBase() {
+            @Override
+            public void prefValue(String pref, int value) {
+                SCROLL_TOOLBAR_THRESHOLD = value / 100.0f;
+            }
+
+            @Override
+            public boolean isObserver() {
+                return true;
+            }
+        });
+    }
+
+    public void destroy() {
+        if (mPrefObserverId != null) {
+            PrefsHelper.removeObserver(mPrefObserverId);
+            mPrefObserverId = null;
+        }
+    }
+
+    public void addTranslationListener(LayerView.DynamicToolbarListener aListener) {
+        mListeners.add(aListener);
+    }
+
+    public void removeTranslationListener(LayerView.DynamicToolbarListener aListener) {
+        mListeners.remove(aListener);
+    }
+
+    private void fireListeners() {
+        for (LayerView.DynamicToolbarListener listener : mListeners) {
+            listener.onTranslationChanged(mToolbarTranslation, mLayerViewTranslation);
+        }
+    }
+
+    public void setMaxTranslation(float maxTranslation) {
+        ThreadUtils.assertOnUiThread();
+        if (maxTranslation < 0) {
+            Log.e(LOGTAG, "Got a negative max-translation value: " + maxTranslation + "; clamping to zero");
+            mMaxTranslation = 0;
+        } else {
+            mMaxTranslation = maxTranslation;
+        }
+    }
+
+    public void setPinned(boolean pinned) {
+        mPinned = pinned;
+    }
+
+    public boolean isPinned() {
+        return mPinned;
+    }
+
+    public void showToolbar(boolean immediately) {
+        animateToolbar(0, immediately);
+    }
+
+    public void hideToolbar(boolean immediately) {
+        animateToolbar(mMaxTranslation, immediately);
+    }
+
+    private void animateToolbar(final float translation, boolean immediately) {
+        ThreadUtils.assertOnUiThread();
+
+        if (mAnimationTask != null) {
+            mTarget.getView().removeRenderTask(mAnimationTask);
+            mAnimationTask = null;
+        }
+
+        Log.v(LOGTAG, "Requested " + (immediately ? "immedate " : "") + "toolbar animation to translation " + translation);
+        if (FloatUtils.fuzzyEquals(mToolbarTranslation, translation)) {
+            // If we're already pretty much in the desired position, don't bother
+            // with a full animation; do an immediate jump
+            immediately = true;
+            Log.v(LOGTAG, "Changing animation to immediate jump");
+        }
+
+        if (immediately) {
+            mToolbarTranslation = translation;
+            fireListeners();
+            resizeViewport();
+            mTarget.getView().requestRender();
+            return;
+        }
+
+        Log.v(LOGTAG, "Kicking off animation...");
+        mAnimationTask = new DynamicToolbarAnimationTask(false, translation);
+        mTarget.getView().postRenderTask(mAnimationTask);
+    }
+
+    private void resizeViewport() {
+        ThreadUtils.assertOnUiThread();
+
+        // The animation is done, now we need to tell gecko to resize to the
+        // proper steady-state layout.
+        synchronized (mTarget.getLock()) {
+            int viewWidth = mTarget.getView().getWidth();
+            int viewHeight = mTarget.getView().getHeight();
+            int viewHeightVisible = viewHeight - Math.round(mMaxTranslation - mToolbarTranslation);
+
+            Log.v(LOGTAG, "Resize viewport to dimensions " + viewWidth + "x" + viewHeightVisible);
+            mTarget.setViewportSize(viewWidth, viewHeightVisible);
+        }
+    }
+
+    class DynamicToolbarAnimationTask extends RenderTask {
+        private final float mStartTranslation;
+        private final float mEndTranslation;
+        private boolean mContinueAnimation;
+
+        public DynamicToolbarAnimationTask(boolean aRunAfter, float aTranslation) {
+            super(aRunAfter);
+            mContinueAnimation = true;
+            mStartTranslation = mToolbarTranslation;
+            mEndTranslation = aTranslation;
+        }
+
+        @Override
+        public boolean internalRun(long timeDelta, long currentFrameStartTime) {
+            if (!mContinueAnimation) {
+                return false;
+            }
+
+            // Calculate the progress (between 0 and 1)
+            final float progress = mInterpolator.getInterpolation(
+                    Math.min(1.0f, (System.nanoTime() - getStartTime())
+                                    / (float)ANIMATION_DURATION));
+
+            // This runs on the compositor thread, so we need to post the
+            // actual work to the UI thread.
+            ThreadUtils.assertNotOnUiThread();
+
+            ThreadUtils.postToUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    // Move the toolbar as per the animation
+                    mToolbarTranslation = FloatUtils.interpolate(mStartTranslation, mEndTranslation, progress);
+                    // Move the LayerView so that there is never a gap between the toolbar
+                    // and the LayerView, because there we will draw garbage.
+                    mLayerViewTranslation = Math.max(mLayerViewTranslation, mToolbarTranslation);
+                    fireListeners();
+
+                    if (progress >= 1.0f) {
+                        resizeViewport();
+                    }
+                }
+            });
+
+            mTarget.getView().requestRender();
+            if (progress >= 1.0f) {
+                mContinueAnimation = false;
+            }
+            return mContinueAnimation;
+        }
+    }
+}
--- a/mobile/android/base/gfx/GeckoLayerClient.java
+++ b/mobile/android/base/gfx/GeckoLayerClient.java
@@ -91,16 +91,17 @@ class GeckoLayerClient implements LayerV
     private LayerView.OnMetricsChangedListener mZoomedViewViewportChangeListener;
 
     private ZoomConstraints mZoomConstraints;
 
     private boolean mGeckoIsReady;
 
     private final PanZoomController mPanZoomController;
     private final LayerMarginsAnimator mMarginsAnimator;
+    private final DynamicToolbarAnimator mToolbarAnimator;
     private final LayerView mView;
 
     /* This flag is true from the time that browser.js detects a first-paint is about to start,
      * to the time that we receive the first-paint composite notification from the compositor.
      * Note that there is a small race condition with this; if there are two paints that both
      * have the first-paint flag set, and the second paint happens concurrently with the
      * composite for the first paint, then this flag may be set to true prematurely. Fixing this
      * is possible but risky; see https://bugzilla.mozilla.org/show_bug.cgi?id=797615#c751
@@ -133,16 +134,17 @@ class GeckoLayerClient implements LayerV
             mViewportMetrics = mViewportMetrics.setIsRTL(tab.getIsRTL());
         }
 
         mFrameMetrics = mViewportMetrics;
 
         mDrawListeners = new ArrayList<DrawListener>();
         mPanZoomController = PanZoomController.Factory.create(this, view, eventDispatcher);
         mMarginsAnimator = new LayerMarginsAnimator(this, view);
+        mToolbarAnimator = new DynamicToolbarAnimator(this);
         mView = view;
         mView.setListener(this);
         mContentDocumentIsDisplayed = true;
     }
 
     public void setOverscrollHandler(final Overscroll listener) {
         mPanZoomController.setOverscrollHandler(listener);
     }
@@ -169,16 +171,17 @@ class GeckoLayerClient implements LayerV
                 mView.getGLController().updateCompositor();
             }
         });
     }
 
     public void destroy() {
         mPanZoomController.destroy();
         mMarginsAnimator.destroy();
+        mToolbarAnimator.destroy();
         mDrawListeners.clear();
     }
 
     /**
      * Returns true if this client is fine with performing a redraw operation or false if it
      * would prefer that the action didn't take place.
      */
     private boolean getRedrawHint() {
@@ -233,16 +236,20 @@ class GeckoLayerClient implements LayerV
     PanZoomController getPanZoomController() {
         return mPanZoomController;
     }
 
     LayerMarginsAnimator getLayerMarginsAnimator() {
         return mMarginsAnimator;
     }
 
+    DynamicToolbarAnimator getDynamicToolbarAnimator() {
+        return mToolbarAnimator;
+    }
+
     /* Informs Gecko that the screen size has changed. */
     private void sendResizeEventIfNecessary(boolean force) {
         DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
 
         IntSize newScreenSize = new IntSize(metrics.widthPixels, metrics.heightPixels);
         IntSize newWindowSize = new IntSize(mView.getWidth(), mView.getHeight());
 
         boolean screenSizeChanged = !mScreenSize.equals(newScreenSize);
--- a/mobile/android/base/gfx/LayerView.java
+++ b/mobile/android/base/gfx/LayerView.java
@@ -50,16 +50,17 @@ import android.widget.FrameLayout;
  * A view rendered by the layer compositor.
  */
 public class LayerView extends FrameLayout implements Tabs.OnTabsChangedListener {
     private static final String LOGTAG = "GeckoLayerView";
 
     private GeckoLayerClient mLayerClient;
     private PanZoomController mPanZoomController;
     private LayerMarginsAnimator mMarginsAnimator;
+    private DynamicToolbarAnimator mToolbarAnimator;
     private final GLController mGLController;
     private InputConnectionHandler mInputConnectionHandler;
     private LayerRenderer mRenderer;
     /* Must be a PAINT_xxx constant */
     private int mPaintState;
     private int mBackgroundColor;
     private FullScreenState mFullScreenState;
 
@@ -125,16 +126,17 @@ public class LayerView extends FrameLayo
     public void initializeView(EventDispatcher eventDispatcher) {
         mLayerClient = new GeckoLayerClient(getContext(), this, eventDispatcher);
         if (mOverscroll != null) {
             mLayerClient.setOverscrollHandler(mOverscroll);
         }
 
         mPanZoomController = mLayerClient.getPanZoomController();
         mMarginsAnimator = mLayerClient.getLayerMarginsAnimator();
+        mToolbarAnimator = mLayerClient.getDynamicToolbarAnimator();
 
         mRenderer = new LayerRenderer(this);
         mInputConnectionHandler = null;
 
         setFocusable(true);
         setFocusableInTouchMode(true);
 
         GeckoAccessibility.setDelegate(this);
@@ -296,16 +298,17 @@ public class LayerView extends FrameLayo
     }
 
     // Don't expose GeckoLayerClient to things outside this package; only expose it as an Object
     GeckoLayerClient getLayerClient() { return mLayerClient; }
     public Object getLayerClientObject() { return mLayerClient; }
 
     public PanZoomController getPanZoomController() { return mPanZoomController; }
     public LayerMarginsAnimator getLayerMarginsAnimator() { return mMarginsAnimator; }
+    public DynamicToolbarAnimator getDynamicToolbarAnimator() { return mToolbarAnimator; }
 
     public ImmutableViewportMetrics getViewportMetrics() {
         return mLayerClient.getViewportMetrics();
     }
 
     public void abortPanning() {
         if (mPanZoomController != null) {
             mPanZoomController.abortPanning();
@@ -660,16 +663,22 @@ public class LayerView extends FrameLayo
     @Override
     public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
         if (msg == Tabs.TabEvents.VIEWPORT_CHANGE && Tabs.getInstance().isSelectedTab(tab) && mLayerClient != null) {
             setZoomConstraints(tab.getZoomConstraints());
             setIsRTL(tab.getIsRTL());
         }
     }
 
+    // Public hooks for dynamic toolbar translation
+
+    public interface DynamicToolbarListener {
+        public void onTranslationChanged(float aToolbarTranslation, float aLayerViewTranslation);
+    }
+
     // Public hooks for listening to metrics changing
 
     public interface OnMetricsChangedListener {
         public void onMetricsChanged(ImmutableViewportMetrics viewport);
         public void onPanZoomStopped();
     }
 
     public void setOnMetricsChangedDynamicToolbarViewportListener(OnMetricsChangedListener listener) {
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -263,16 +263,17 @@ gbjar.sources += [
     'GeckoViewContent.java',
     'gfx/Axis.java',
     'gfx/BitmapUtils.java',
     'gfx/BufferedImage.java',
     'gfx/BufferedImageGLInfo.java',
     'gfx/DisplayPortCalculator.java',
     'gfx/DisplayPortMetrics.java',
     'gfx/DrawTimingQueue.java',
+    'gfx/DynamicToolbarAnimator.java',
     'gfx/FloatSize.java',
     'gfx/FullScreenState.java',
     'gfx/GeckoLayerClient.java',
     'gfx/GLController.java',
     'gfx/ImmutableViewportMetrics.java',
     'gfx/InputConnectionHandler.java',
     'gfx/IntSize.java',
     'gfx/JavaPanZoomController.java',