Bug 1302150 - Ensure the dynamic toolbar becomes visible when end of page is reached r=kats draft
authorRandall Barker <rbarker@mozilla.com>
Wed, 03 May 2017 15:36:01 -0700 (2017-05-03)
changeset 576517 1db9d56d3e1d8f2c3502907ee337273029adb8f9
parent 576516 06695225f1a64b43b20d7999a039c9d334d2da96
child 628214 b16e17e37a0c57bb240fe42e076301a5f925c81c
push id58381
push userbmo:rbarker@mozilla.com
push dateThu, 11 May 2017 19:43:56 +0000 (2017-05-11)
reviewerskats
bugs1302150
milestone55.0a1
Bug 1302150 - Ensure the dynamic toolbar becomes visible when end of page is reached r=kats MozReview-Commit-ID: 2t1wfwGug6e
gfx/layers/apz/src/APZCTreeManager.cpp
gfx/layers/apz/src/AndroidDynamicToolbarAnimator.cpp
gfx/layers/apz/src/AndroidDynamicToolbarAnimator.h
--- a/gfx/layers/apz/src/APZCTreeManager.cpp
+++ b/gfx/layers/apz/src/APZCTreeManager.cpp
@@ -743,17 +743,26 @@ nsEventStatus
 APZCTreeManager::ReceiveInputEvent(InputData& aEvent,
                                    ScrollableLayerGuid* aOutTargetGuid,
                                    uint64_t* aOutInputBlockId)
 {
   APZThreadUtils::AssertOnControllerThread();
 
 #if defined(MOZ_WIDGET_ANDROID)
   MOZ_ASSERT(mToolbarAnimator);
-  nsEventStatus isConsumed = mToolbarAnimator->ReceiveInputEvent(aEvent);
+  ScreenPoint scrollOffset;
+  {
+    MutexAutoLock lock(mTreeLock);
+    RefPtr<AsyncPanZoomController> apzc = FindRootContentOrRootApzc();
+    if (apzc) {
+      scrollOffset = ViewAs<ScreenPixel>(apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::NORMAL),
+                                         PixelCastJustification::ScreenIsParentLayerForRoot);
+    }
+  }
+  nsEventStatus isConsumed = mToolbarAnimator->ReceiveInputEvent(aEvent, scrollOffset);
   // Check if the mToolbarAnimator consumed the event.
   if (isConsumed == nsEventStatus_eConsumeNoDefault) {
     APZCTM_LOG("Dynamic toolbar consumed event");
     return isConsumed;
   }
 #endif // (MOZ_WIDGET_ANDROID)
 
   // Initialize aOutInputBlockId to a sane value, and then later we overwrite
--- a/gfx/layers/apz/src/AndroidDynamicToolbarAnimator.cpp
+++ b/gfx/layers/apz/src/AndroidDynamicToolbarAnimator.cpp
@@ -25,17 +25,17 @@
 
 namespace {
 
 // Internal flags and constants
 static const float   ANIMATION_DURATION = 0.15f; // How many seconds the complete animation should span
 static const int32_t MOVE_TOOLBAR_DOWN  =  1;    // Multiplier to move the toolbar down
 static const int32_t MOVE_TOOLBAR_UP    = -1;    // Multiplier to move the toolbar up
 static const float   SHRINK_FACTOR      = 0.95f; // Amount to shrink the either the full content for small pages or the amount left
-                                                 // See: IsEnoughPageToHideToolbar()
+                                                 // See: PageTooSmallEnsureToolbarVisible()
 } // namespace
 
 namespace mozilla {
 namespace layers {
 
 AndroidDynamicToolbarAnimator::AndroidDynamicToolbarAnimator()
   : mRootLayerTreeId(0)
   // Read/Write Compositor Thread, Read only Controller thread
@@ -49,26 +49,29 @@ AndroidDynamicToolbarAnimator::AndroidDy
   , mControllerResetOnNextMove(false)
   , mControllerStartTouch(0)
   , mControllerPreviousTouch(0)
   , mControllerTotalDistance(0)
   , mControllerMaxToolbarHeight(0)
   , mControllerToolbarHeight(0)
   , mControllerSurfaceHeight(0)
   , mControllerCompositionHeight(0)
+  , mControllerRootScrollY(0.0f)
   , mControllerLastDragDirection(0)
   , mControllerTouchCount(0)
   , mControllerLastEventTimeStamp(0)
   , mControllerState(eNothingPending)
   // Compositor thread only
   , mCompositorShutdown(false)
   , mCompositorAnimationDeferred(false)
   , mCompositorLayersUpdateEnabled(false)
   , mCompositorAnimationStarted(false)
   , mCompositorReceivedFirstPaint(false)
+  , mCompositorWaitForPageResize(false)
+  , mCompositorToolbarShowRequested(false)
   , mCompositorAnimationStyle(eAnimate)
   , mCompositorMaxToolbarHeight(0)
   , mCompositorToolbarHeight(0)
   , mCompositorSurfaceHeight(0)
   , mCompositorAnimationDirection(0)
   , mCompositorAnimationStartHeight(0)
 {}
 
@@ -96,27 +99,33 @@ GetTouchY(MultiTouchInput& multiTouch, S
     *value = multiTouch.mTouches[0].mScreenPoint.y;
     return true;
   }
 
   return false;
 }
 
 nsEventStatus
-AndroidDynamicToolbarAnimator::ReceiveInputEvent(InputData& aEvent)
+AndroidDynamicToolbarAnimator::ReceiveInputEvent(InputData& aEvent, const ScreenPoint& aScrollOffset)
 {
   MOZ_ASSERT(APZThreadUtils::IsControllerThread());
 
+  mControllerRootScrollY = aScrollOffset.y;
+
   // Only process and adjust touch events. Wheel events (aka scroll events) are adjusted in the NativePanZoomController
   if (aEvent.mInputType != MULTITOUCH_INPUT) {
     return nsEventStatus_eIgnore;
   }
 
   MultiTouchInput& multiTouch = aEvent.AsMultiTouchInput();
-  ScreenIntCoord currentTouch = 0;
+
+  if (PageTooSmallEnsureToolbarVisible()) {
+    TranslateTouchEvent(multiTouch);
+    return nsEventStatus_eIgnore;
+  }
 
   switch (multiTouch.mType) {
   case MultiTouchInput::MULTITOUCH_START:
     mControllerTouchCount = multiTouch.mTouches.Length();
     break;
   case MultiTouchInput::MULTITOUCH_END:
   case MultiTouchInput::MULTITOUCH_CANCEL:
     mControllerTouchCount -= multiTouch.mTouches.Length();
@@ -124,59 +133,66 @@ AndroidDynamicToolbarAnimator::ReceiveIn
   default:
     break;
   }
 
   if (mControllerTouchCount > 1) {
     mControllerResetOnNextMove = true;
   }
 
+  ScreenIntCoord currentTouch = 0;
+
   if (mPinnedFlags || !GetTouchY(multiTouch, &currentTouch)) {
     TranslateTouchEvent(multiTouch);
     return nsEventStatus_eIgnore;
   }
 
   // Only the return value from ProcessTouchDelta should
   // change status to nsEventStatus_eConsumeNoDefault
   nsEventStatus status = nsEventStatus_eIgnore;
 
   const StaticToolbarState currentToolbarState = mToolbarState;
   switch (multiTouch.mType) {
   case MultiTouchInput::MULTITOUCH_START:
     mControllerCancelTouchTracking = false;
     mControllerStartTouch = mControllerPreviousTouch = currentTouch;
-    if (currentToolbarState == eToolbarAnimating) {
+    // We don't want to stop the animation if we are near the bottom of the page.
+    if (!ScrollOffsetNearBottom() && (currentToolbarState == eToolbarAnimating)) {
       StopCompositorAnimation();
     }
     break;
   case MultiTouchInput::MULTITOUCH_MOVE: {
     CheckForResetOnNextMove(currentTouch);
-
     if ((mControllerState != eAnimationStartPending) &&
         (mControllerState != eAnimationStopPending) &&
         (currentToolbarState != eToolbarAnimating) &&
         !mControllerCancelTouchTracking) {
 
+      // Don't move the toolbar if we are near the page bottom
+      // and the toolbar is not in transition
+      if (ScrollOffsetNearBottom() && !ToolbarInTransition()) {
+        ShowToolbarIfNotVisible(currentToolbarState);
+        break;
+      }
+
       ScreenIntCoord delta = currentTouch - mControllerPreviousTouch;
       mControllerPreviousTouch = currentTouch;
       mControllerTotalDistance += delta;
       if (delta != 0) {
         ScreenIntCoord direction = (delta > 0 ? MOVE_TOOLBAR_DOWN : MOVE_TOOLBAR_UP);
         if (mControllerLastDragDirection && (direction != mControllerLastDragDirection)) {
           mControllerDragChangedDirection = true;
         }
         mControllerLastDragDirection = direction;
       }
-      if (IsEnoughPageToHideToolbar(delta)) {
-        // NOTE: gfxPrefs::ToolbarScrollThreshold() returns a percentage as an intt32_t. So multiply it by 0.01f to convert.
-        const uint32_t dragThreshold = Abs(std::lround(0.01f * gfxPrefs::ToolbarScrollThreshold() * mControllerCompositionHeight));
-        if ((Abs(mControllerTotalDistance.value) > dragThreshold) && (delta != 0)) {
-          mControllerDragThresholdReached = true;
-          status = ProcessTouchDelta(currentToolbarState, delta, multiTouch.mTime);
-        }
+      // NOTE: gfxPrefs::ToolbarScrollThreshold() returns a percentage as an int32_t. So multiply it by 0.01f to convert.
+      const uint32_t dragThreshold = Abs(std::lround(0.01f * gfxPrefs::ToolbarScrollThreshold() * mControllerCompositionHeight));
+      if ((Abs(mControllerTotalDistance.value) > dragThreshold) && (delta != 0)) {
+        mControllerDragThresholdReached = true;
+        status = ProcessTouchDelta(currentToolbarState, delta, multiTouch.mTime);
       }
       mControllerLastEventTimeStamp = multiTouch.mTime;
     }
     break;
   }
   case MultiTouchInput::MULTITOUCH_END:
   case MultiTouchInput::MULTITOUCH_CANCEL:
     // last finger was lifted
@@ -289,25 +305,28 @@ AndroidDynamicToolbarAnimator::ToolbarAn
     break;
   case STATIC_TOOLBAR_READY:
     break;
   case TOOLBAR_HIDDEN:
     // If the toolbar is animating, then it is already unlocked.
     if (mToolbarState != eToolbarAnimating) {
       mToolbarState = eToolbarUnlocked;
       if (mCompositorAnimationDeferred) {
-        StartCompositorAnimation(mCompositorAnimationDirection, mCompositorAnimationStyle, mCompositorToolbarHeight);
+        StartCompositorAnimation(mCompositorAnimationDirection, mCompositorAnimationStyle, mCompositorToolbarHeight, mCompositorWaitForPageResize);
       }
     } else {
       // The compositor is already animating the toolbar so no need to defer.
       mCompositorAnimationDeferred = false;
     }
     break;
   case TOOLBAR_VISIBLE:
-    mToolbarState = eToolbarVisible;
+    // If we are currently animating, let the animation finish.
+    if (mToolbarState != eToolbarAnimating) {
+      mToolbarState = eToolbarVisible;
+    }
     break;
   case TOOLBAR_SHOW:
     break;
   case FIRST_PAINT:
     break;
   case REQUEST_SHOW_TOOLBAR_IMMEDIATELY:
     NotifyControllerPendingAnimation(MOVE_TOOLBAR_DOWN, eImmediate);
     break;
@@ -344,22 +363,27 @@ AndroidDynamicToolbarAnimator::UpdateAni
 
   AsyncCompositionManager* manager = parent->GetCompositionManager(nullptr);
   if (!manager) {
     return false;
   }
 
   if (mCompositorSurfaceHeight != mCompositorCompositionSize.height) {
     // Waiting for the composition to resize
-    return true;
+    if (mCompositorWaitForPageResize && mCompositorAnimationStarted) {
+      mCompositorWaitForPageResize = false;
+    } else {
+      return true;
+    }
   } else if (!mCompositorAnimationStarted) {
     parent->GetAPZCTreeManager()->AdjustScrollForSurfaceShift(ScreenPoint(0.0f, (float)(-mCompositorToolbarHeight)));
     manager->SetFixedLayerMargins(mCompositorToolbarHeight, 0);
     mCompositorAnimationStarted = true;
     mCompositorReceivedFirstPaint = false;
+    mCompositorToolbarShowRequested = false;
     // Reset the start time so the toolbar does not jump on the first animation frame
     mCompositorAnimationStartTimeStamp = aCurrentFrame;
     // Since the delta time for this frame will be zero. Just return, the animation will start on the next frame.
     return true;
   }
 
   bool continueAnimating = true;
 
@@ -375,19 +399,29 @@ AndroidDynamicToolbarAnimator::UpdateAni
     // This animation was started in the future!
     if (deltaTime < 0.0f) {
       deltaTime = 0.0f;
     }
     mCompositorToolbarHeight = mCompositorAnimationStartHeight + ((int32_t)(rate * deltaTime) * mCompositorAnimationDirection);
   }
 
   if ((mCompositorAnimationDirection == MOVE_TOOLBAR_DOWN) && (mCompositorToolbarHeight >= mCompositorMaxToolbarHeight)) {
-    continueAnimating = false;
-    mToolbarState = eToolbarVisible;
-    PostMessage(TOOLBAR_SHOW);
+    // if the toolbar is being animated and the page is at the end, the animation needs to wait for
+    // the page to resize before ending the animation so that the page may be scrolled
+    if (!mCompositorReceivedFirstPaint && mCompositorWaitForPageResize) {
+      continueAnimating = true;
+    } else {
+      continueAnimating = false;
+      mToolbarState = eToolbarVisible;
+    }
+    // Make sure we only send one show request per animation
+    if (!mCompositorToolbarShowRequested) {
+      PostMessage(TOOLBAR_SHOW);
+      mCompositorToolbarShowRequested = true;
+    }
     mCompositorToolbarHeight = mCompositorMaxToolbarHeight;
   } else if ((mCompositorAnimationDirection == MOVE_TOOLBAR_UP) && (mCompositorToolbarHeight <= 0)) {
     continueAnimating = false;
     mToolbarState = eToolbarUnlocked;
     mCompositorToolbarHeight = 0;
   }
 
   if (continueAnimating) {
@@ -555,18 +589,18 @@ AndroidDynamicToolbarAnimator::ProcessTo
     ScreenIntCoord deltaRemainder = 0;
     mControllerToolbarHeight += aDelta;
     if (tryingToHideToolbar && (mControllerToolbarHeight <= 0 )) {
       deltaRemainder = mControllerToolbarHeight;
       mControllerToolbarHeight = 0;
     } else if (!tryingToHideToolbar && (mControllerToolbarHeight >= mControllerMaxToolbarHeight)) {
       deltaRemainder = mControllerToolbarHeight - mControllerMaxToolbarHeight;
       mControllerToolbarHeight = mControllerMaxToolbarHeight;
+      mControllerState = eShowPending;
       PostMessage(TOOLBAR_SHOW);
-      mControllerState = eShowPending;
     }
 
     UpdateCompositorToolbarHeight(mControllerToolbarHeight);
     RequestComposite();
     // If there was no delta left over, the event was completely consumed.
     if (deltaRemainder == 0) {
       status = nsEventStatus_eConsumeNoDefault;
     }
@@ -594,19 +628,17 @@ AndroidDynamicToolbarAnimator::HandleTou
   int32_t direction = mControllerLastDragDirection;
   mControllerLastDragDirection = 0;
   bool isRoot = mControllerScrollingRootContent;
   mControllerScrollingRootContent = false;
   bool dragChangedDirection = mControllerDragChangedDirection;
   mControllerDragChangedDirection = false;
 
   // If the drag direction changed and the toolbar is partially visible, hide in the direction with the least distance to travel.
-  if (dragChangedDirection &&
-      (mControllerToolbarHeight != mControllerMaxToolbarHeight) &&
-      (mControllerToolbarHeight != 0)) {
+  if (dragChangedDirection && ToolbarInTransition()) {
     direction = ((float)mControllerToolbarHeight / (float)mControllerMaxToolbarHeight) < 0.5f ? MOVE_TOOLBAR_UP : MOVE_TOOLBAR_DOWN;
   }
 
   // If the last touch didn't have a drag direction, use start of touch to find direction
   if (!direction) {
     if (mControllerToolbarHeight == mControllerMaxToolbarHeight) {
       direction = MOVE_TOOLBAR_DOWN;
     } else if (mControllerToolbarHeight == 0) {
@@ -635,17 +667,17 @@ AndroidDynamicToolbarAnimator::HandleTou
 
   // Received a UI thread request to show or hide the snapshot during a touch.
   // This overrides the touch event so just return.
   if (cancelTouchTracking) {
     return;
   }
 
   // The drag threshold has not been reach and the toolbar is either completely visible or completely hidden.
-  if (!dragThresholdReached && ((mControllerToolbarHeight == mControllerMaxToolbarHeight) || (mControllerToolbarHeight == 0))) {
+  if (!dragThresholdReached && !ToolbarInTransition()) {
     ShowToolbarIfNotVisible(aCurrentToolbarState);
     return;
   }
 
   // The toolbar is already where it needs to be so just return.
   if (((direction == MOVE_TOOLBAR_DOWN) && (mControllerToolbarHeight == mControllerMaxToolbarHeight)) ||
       ((direction == MOVE_TOOLBAR_UP) && (mControllerToolbarHeight == 0))) {
     ShowToolbarIfNotVisible(aCurrentToolbarState);
@@ -656,29 +688,28 @@ AndroidDynamicToolbarAnimator::HandleTou
   // snapshot toolbar is completely visible before showing, we don't want to enter this if block
   // if the snapshot toolbar isn't completely visible to avoid early return.
   if (!isRoot &&
       ((direction == MOVE_TOOLBAR_UP) && (mControllerToolbarHeight == mControllerMaxToolbarHeight))) {
     ShowToolbarIfNotVisible(aCurrentToolbarState);
     return;
   }
 
-  // The page is either too small or too close to the end to animate
-  if (!IsEnoughPageToHideToolbar(direction)) {
-    if (mControllerToolbarHeight == mControllerMaxToolbarHeight) {
+  if (ScrollOffsetNearBottom()) {
+    if (ToolbarInTransition()) {
+      // Toolbar is partially visible so make it visible since we are near the end of the page
+      direction = MOVE_TOOLBAR_DOWN;
+    } else {
+      // Don't start an animation if near the bottom of page and toolbar is completely visible or hidden
       ShowToolbarIfNotVisible(aCurrentToolbarState);
       return;
-    } else if (mControllerToolbarHeight != 0) {
-      // The snapshot is partially visible but there is not enough page
-      // to hide the snapshot so make it visible by moving it down
-      direction = MOVE_TOOLBAR_DOWN;
     }
   }
 
-  StartCompositorAnimation(direction, eAnimate, mControllerToolbarHeight);
+  StartCompositorAnimation(direction, eAnimate, mControllerToolbarHeight, ScrollOffsetNearBottom());
 }
 
 void
 AndroidDynamicToolbarAnimator::PostMessage(int32_t aMessage) {
   // if the root layer tree id is zero then Initialize() has not been called yet
   // so queue the message until Initialize() is called.
   if (mRootLayerTreeId == 0) {
     QueueMessage(aMessage);
@@ -785,54 +816,59 @@ AndroidDynamicToolbarAnimator::NotifyCon
     // We received a show request but the real toolbar is hidden, so tell it to show now.
     if ((aDirection == MOVE_TOOLBAR_DOWN) && (mToolbarState == eToolbarUnlocked)) {
       PostMessage(TOOLBAR_SHOW);
     }
     return;
   }
 
   // NOTE: StartCompositorAnimation will set mControllerState to eAnimationStartPending
-  StartCompositorAnimation(aDirection, aAnimationStyle, mControllerToolbarHeight);
+  StartCompositorAnimation(aDirection, aAnimationStyle, mControllerToolbarHeight, ScrollOffsetNearBottom());
   MOZ_ASSERT(mControllerState == eAnimationStartPending);
 }
 
 void
-AndroidDynamicToolbarAnimator::StartCompositorAnimation(int32_t aDirection, AnimationStyle aAnimationStyle, ScreenIntCoord aHeight)
+AndroidDynamicToolbarAnimator::StartCompositorAnimation(int32_t aDirection, AnimationStyle aAnimationStyle, ScreenIntCoord aHeight, bool aWaitForPageResize)
 {
   if (!CompositorThreadHolder::IsInCompositorThread()) {
     mControllerState = eAnimationStartPending;
-    CompositorThreadHolder::Loop()->PostTask(NewRunnableMethod<int32_t, AnimationStyle, ScreenIntCoord>(
-      this, &AndroidDynamicToolbarAnimator::StartCompositorAnimation, aDirection, aAnimationStyle, aHeight));
+    CompositorThreadHolder::Loop()->PostTask(NewRunnableMethod<int32_t, AnimationStyle, ScreenIntCoord, bool>(
+      this, &AndroidDynamicToolbarAnimator::StartCompositorAnimation, aDirection, aAnimationStyle, aHeight, aWaitForPageResize));
     return;
   }
 
   MOZ_ASSERT(aDirection == MOVE_TOOLBAR_UP || aDirection == MOVE_TOOLBAR_DOWN);
 
-  const StaticToolbarState currentToolbarState = mToolbarState;
+  const StaticToolbarState initialToolbarState = mToolbarState;
   mCompositorAnimationDirection = aDirection;
   mCompositorAnimationStartHeight = mCompositorToolbarHeight = aHeight;
   mCompositorAnimationStyle = aAnimationStyle;
+  mCompositorWaitForPageResize = aWaitForPageResize;
   // If the snapshot is not unlocked, request the UI thread update the snapshot
   // and defer animation until it has been unlocked
-  if (currentToolbarState != eToolbarUnlocked) {
+  if ((initialToolbarState != eToolbarUnlocked) && (initialToolbarState != eToolbarAnimating)) {
     mCompositorAnimationDeferred = true;
     PostMessage(STATIC_TOOLBAR_NEEDS_UPDATE);
   } else {
-    // Toolbar is unlocked so animation may begin immediately
+    // Toolbar is either unlocked or already animating so animation may begin immediately
     mCompositorAnimationDeferred = false;
     mToolbarState = eToolbarAnimating;
-    mCompositorAnimationStarted = false;
+    if (initialToolbarState != eToolbarAnimating) {
+      mCompositorAnimationStarted = false;
+    }
     // Let the controller know we starting an animation so it may clear the AnimationStartPending flag.
     NotifyControllerAnimationStarted();
     CompositorBridgeParent* parent = CompositorBridgeParent::GetCompositorBridgeParentFromLayersId(mRootLayerTreeId);
     if (parent) {
       mCompositorAnimationStartTimeStamp = parent->GetAPZCTreeManager()->GetFrameTime();
     }
-    // Kick the compositor to start the animation
-    RequestComposite();
+    if (initialToolbarState != eToolbarAnimating) {
+      // Kick the compositor to start the animation if we aren't already animating.
+      RequestComposite();
+    }
   }
 }
 
 void
 AndroidDynamicToolbarAnimator::NotifyControllerAnimationStarted()
 {
   if (!APZThreadUtils::IsControllerThread()) {
     APZThreadUtils::RunOnControllerThread(NewRunnableMethod(this, &AndroidDynamicToolbarAnimator::NotifyControllerAnimationStarted));
@@ -938,50 +974,59 @@ AndroidDynamicToolbarAnimator::UpdateFra
                                                   CSSToScreenScale aScale,
                                                   CSSRect aCssPageRect)
 {
   if (!APZThreadUtils::IsControllerThread()) {
     APZThreadUtils::RunOnControllerThread(NewRunnableMethod<ScreenPoint, CSSToScreenScale, CSSRect>(this, &AndroidDynamicToolbarAnimator::UpdateFrameMetrics, aScrollOffset, aScale, aCssPageRect));
     return;
   }
 
+  mControllerRootScrollY = aScrollOffset.y;
+
   if (mControllerFrameMetrics.Update(aScrollOffset, aScale, aCssPageRect)) {
+    if (FuzzyEqualsMultiplicative(mControllerFrameMetrics.mPageRect.YMost(), mControllerCompositionHeight + mControllerFrameMetrics.mScrollOffset.y) &&
+        (mControllerFrameMetrics.mPageRect.YMost() > (mControllerSurfaceHeight * 2)) &&
+        (mControllerToolbarHeight != mControllerMaxToolbarHeight) &&
+        !mPinnedFlags) {
+      // The end of the page has been reached, the page is twice the height of the visible height,
+      // and the toolbar is not completely visible so animate it into view.
+      StartCompositorAnimation(MOVE_TOOLBAR_DOWN, eAnimate, mControllerToolbarHeight, /* wait for page resize */ true);
+    }
     RefPtr<UiCompositorControllerParent> uiController = UiCompositorControllerParent::GetFromRootLayerTreeId(mRootLayerTreeId);
     MOZ_ASSERT(uiController);
     CompositorThreadHolder::Loop()->PostTask(NewRunnableMethod<ScreenPoint, CSSToScreenScale, CSSRect>(
                                                uiController, &UiCompositorControllerParent::SendRootFrameMetrics,
                                                aScrollOffset, aScale, aCssPageRect));
   }
 }
 
 bool
-AndroidDynamicToolbarAnimator::IsEnoughPageToHideToolbar(ScreenIntCoord delta)
+AndroidDynamicToolbarAnimator::PageTooSmallEnsureToolbarVisible()
 {
   MOZ_ASSERT(APZThreadUtils::IsControllerThread());
-  // The toolbar will only hide if dragging up so ignore positive deltas from dragging down.
-  if (delta >= 0) {
+  // if the page is too small then the toolbar can not be hidden
+  if ((float)mControllerSurfaceHeight >= (mControllerFrameMetrics.mPageRect.YMost() * SHRINK_FACTOR)) {
+    // If the toolbar is partial hidden, show it.
+    if ((mControllerToolbarHeight != mControllerMaxToolbarHeight) && !mPinnedFlags) {
+      StartCompositorAnimation(MOVE_TOOLBAR_DOWN, eImmediate, mControllerToolbarHeight, /* wait for page resize */ true);
+    }
     return true;
   }
 
-  // if the page is 1) too small or 2) too close to the bottom, then the toolbar can not be hidden
-  if (((float)mControllerSurfaceHeight >= (mControllerFrameMetrics.mPageRect.YMost() * SHRINK_FACTOR)) ||
-      ((float)mControllerSurfaceHeight >= ((mControllerFrameMetrics.mPageRect.YMost() - mControllerFrameMetrics.mScrollOffset.y) * SHRINK_FACTOR))) {
-    return false;
-  }
-
-  return true;
+  return false;
 }
 
 void
 AndroidDynamicToolbarAnimator::ShowToolbarIfNotVisible(StaticToolbarState aCurrentToolbarState)
 {
   MOZ_ASSERT(APZThreadUtils::IsControllerThread());
   if ((mControllerToolbarHeight == mControllerMaxToolbarHeight) &&
       (aCurrentToolbarState != eToolbarVisible) &&
       (mControllerState != eShowPending)) {
+    mControllerState = eShowPending;
     PostMessage(TOOLBAR_SHOW);
   }
 }
 
 bool
 AndroidDynamicToolbarAnimator::FrameMetricsState::Update(const ScreenPoint& aScrollOffset,
                                                          const CSSToScreenScale& aScale,
                                                          const CSSRect& aCssPageRect)
@@ -1039,16 +1084,38 @@ AndroidDynamicToolbarAnimator::CheckForR
     mControllerTotalDistance = 0;
     mControllerLastDragDirection = 0;
     mControllerStartTouch = mControllerPreviousTouch = aCurrentTouch;
     mControllerDragThresholdReached = false;
     mControllerResetOnNextMove = false;
   }
 }
 
+bool
+AndroidDynamicToolbarAnimator::ScrollOffsetNearBottom() const
+{
+  MOZ_ASSERT(APZThreadUtils::IsControllerThread());
+  // Twice the toolbar's height is considered near the bottom of the page.
+  if ((mControllerToolbarHeight * 2) >= (mControllerFrameMetrics.mPageRect.YMost() - (mControllerRootScrollY + ScreenCoord(mControllerCompositionHeight)))) {
+    return true;
+  }
+  return false;
+}
+
+bool
+AndroidDynamicToolbarAnimator::ToolbarInTransition()
+{
+  if (APZThreadUtils::IsControllerThread()) {
+    return (mControllerToolbarHeight != mControllerMaxToolbarHeight) && (mControllerToolbarHeight != 0);
+  }
+
+  MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+  return (mCompositorToolbarHeight != mCompositorMaxToolbarHeight) && (mCompositorToolbarHeight != 0);
+}
+
 void
 AndroidDynamicToolbarAnimator::QueueMessage(int32_t aMessage)
 {
   if (!CompositorThreadHolder::IsInCompositorThread()) {
     CompositorThreadHolder::Loop()->PostTask(NewRunnableMethod<int32_t>(this, &AndroidDynamicToolbarAnimator::QueueMessage, aMessage));
     return;
   }
 
--- a/gfx/layers/apz/src/AndroidDynamicToolbarAnimator.h
+++ b/gfx/layers/apz/src/AndroidDynamicToolbarAnimator.h
@@ -53,17 +53,17 @@ public:
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AndroidDynamicToolbarAnimator);
   AndroidDynamicToolbarAnimator();
   void Initialize(uint64_t aRootLayerTreeId);
   // Used to intercept events to determine if the event affects the toolbar.
   // May apply translation to touch events if the toolbar is visible.
   // Returns nsEventStatus_eIgnore when the event is not consumed and
   // nsEventStatus_eConsumeNoDefault when the event was used to translate the
   // toolbar.
-  nsEventStatus ReceiveInputEvent(InputData& aEvent);
+  nsEventStatus ReceiveInputEvent(InputData& aEvent, const ScreenPoint& aScrollOffset);
   void SetMaxToolbarHeight(ScreenIntCoord aHeight);
   // When a pinned reason is set to true, the animator will prevent
   // touch events from altering the height of the toolbar. All pinned
   // reasons must be cleared before touch events will affect the toolbar.
   // Animation requests from the UI thread are still honored even if any
   // pin reason is set. This allows the UI thread to pin the toolbar for
   // full screen and then request the animator hide the toolbar.
   void SetPinned(bool aPinned, int32_t aReason);
@@ -134,31 +134,37 @@ protected:
   // Sends a message to the UI thread. May be called from any thread
   void PostMessage(int32_t aMessage);
   void UpdateCompositorToolbarHeight(ScreenIntCoord aHeight);
   void UpdateControllerToolbarHeight(ScreenIntCoord aHeight, ScreenIntCoord aMaxHeight = -1);
   void UpdateControllerSurfaceHeight(ScreenIntCoord aHeight);
   void UpdateControllerCompositionHeight(ScreenIntCoord aHeight);
   void UpdateFixedLayerMargins();
   void NotifyControllerPendingAnimation(int32_t aDirection, AnimationStyle aStyle);
-  void StartCompositorAnimation(int32_t aDirection, AnimationStyle aStyle, ScreenIntCoord aHeight);
+  void StartCompositorAnimation(int32_t aDirection, AnimationStyle aStyle, ScreenIntCoord aHeight, bool aWaitForPageResize);
   void NotifyControllerAnimationStarted();
   void StopCompositorAnimation();
   void NotifyControllerAnimationStopped(ScreenIntCoord aHeight);
   void RequestComposite();
   void PostToolbarReady();
   void UpdateFrameMetrics(ScreenPoint aScrollOffset,
                           CSSToScreenScale aScale,
                           CSSRect aCssPageRect);
-  bool IsEnoughPageToHideToolbar(ScreenIntCoord delta);
+  // Returns true if the page is too small to animate the toolbar
+  // Also ensures the toolbar is visible.
+  bool PageTooSmallEnsureToolbarVisible();
   void ShowToolbarIfNotVisible(StaticToolbarState aCurrentToolbarState);
   void TranslateTouchEvent(MultiTouchInput& aTouchEvent);
   ScreenIntCoord GetFixedLayerMarginsBottom();
   void NotifyControllerSnapshotFailed();
   void CheckForResetOnNextMove(ScreenIntCoord aCurrentTouch);
+  // Returns true if the page scroll offset is near the bottom.
+  bool ScrollOffsetNearBottom() const;
+  // Returns true if toolbar is not completely visible nor completely hidden.
+  bool ToolbarInTransition();
   void QueueMessage(int32_t aMessage);
 
   // Read only Compositor and Controller threads after Initialize()
   uint64_t mRootLayerTreeId;
 
   // Read/Write Compositor Thread, Read only Controller thread
   Atomic<StaticToolbarState> mToolbarState; // Current toolbar state.
   Atomic<uint32_t> mPinnedFlags;            // The toolbar should not be moved or animated unless no flags are set
@@ -173,16 +179,17 @@ protected:
                                         // mControllerDragThresholdReached, and mControllerLastDragDirection to be reset on next move
   ScreenIntCoord mControllerStartTouch;        // The Y position where the touch started
   ScreenIntCoord mControllerPreviousTouch;     // The previous Y position of the touch
   ScreenIntCoord mControllerTotalDistance;     // Total distance travel during the current touch
   ScreenIntCoord mControllerMaxToolbarHeight;  // Max height of the toolbar
   ScreenIntCoord mControllerToolbarHeight;     // Current height of the toolbar
   ScreenIntCoord mControllerSurfaceHeight;     // Current height of the render surface
   ScreenIntCoord mControllerCompositionHeight; // Current height of the visible page
+  ScreenCoord mControllerRootScrollY;          // Current scroll Y value of the root scroll frame
   int32_t mControllerLastDragDirection;        // Direction of movement of the previous touch move event
   int32_t mControllerTouchCount;               // Counts the number of current touches.
   uint32_t mControllerLastEventTimeStamp;      // Time stamp for the previous touch event received
   ControllerThreadState mControllerState;      // Contains the expected pending state of the mToolbarState
 
   // Contains the values from the last steady state root content FrameMetrics
   struct FrameMetricsState {
     ScreenPoint mScrollOffset;
@@ -192,35 +199,37 @@ protected:
 
     // Returns true if any of the values have changed.
     bool Update(const ScreenPoint& aScrollOffset,
                 const CSSToScreenScale& aScale,
                 const CSSRect& aCssPageRect);
   };
 
   // Controller thread only
-  FrameMetricsState mControllerFrameMetrics;
+  FrameMetricsState mControllerFrameMetrics; // Updated when frame metrics are in a steady state.
 
   class QueuedMessage : public LinkedListElement<QueuedMessage> {
   public:
     explicit QueuedMessage(int32_t aMessage) :
       mMessage(aMessage) {}
     int32_t mMessage;
   private:
     QueuedMessage() = delete;
     QueuedMessage(const QueuedMessage&) = delete;
     QueuedMessage& operator=(const QueuedMessage&) = delete;
   };
 
   // Compositor thread only
-  bool mCompositorShutdown;            // Set to true when the compositor has been shutdown
-  bool mCompositorAnimationDeferred;   // An animation has been deferred until the toolbar is unlocked
-  bool mCompositorLayersUpdateEnabled; // Flag set to true when the UI thread is expecting to be notified when a layer has been updated
-  bool mCompositorAnimationStarted;    // Set to true when the compositor has actually started animating the static snapshot.
-  bool mCompositorReceivedFirstPaint;  // Set to true when a first paint occurs. Used by toolbar animator to detect a new page load.
+  bool mCompositorShutdown;             // Set to true when the compositor has been shutdown
+  bool mCompositorAnimationDeferred;    // An animation has been deferred until the toolbar is unlocked
+  bool mCompositorLayersUpdateEnabled;  // Flag set to true when the UI thread is expecting to be notified when a layer has been updated
+  bool mCompositorAnimationStarted;     // Set to true when the compositor has actually started animating the static snapshot.
+  bool mCompositorReceivedFirstPaint;   // Set to true when a first paint occurs. Used by toolbar animator to detect a new page load.
+  bool mCompositorWaitForPageResize;    // Set to true if the bottom of the page has been reached and the toolbar animator should wait for the page to resize before ending animation.
+  bool mCompositorToolbarShowRequested; // Set to true if the animator has already requested the real toolbar chrome be shown
   AnimationStyle mCompositorAnimationStyle;       // Set to true when the snap should be immediately hidden or shown in the animation update
   ScreenIntCoord mCompositorMaxToolbarHeight;     // Should contain the same value as mControllerMaxToolbarHeight
   ScreenIntCoord mCompositorToolbarHeight;        // This value is only updated by the compositor thread when the mToolbarState == ToolbarAnimating
   ScreenIntCoord mCompositorSurfaceHeight;        // Current height of the render surface
   ScreenIntSize  mCompositorCompositionSize;      // Current size of the visible page
   int32_t mCompositorAnimationDirection;          // Direction the snapshot should be animated
   ScreenIntCoord mCompositorAnimationStartHeight; // The height of the snapshot at the start of an animation
   ScreenIntSize mCompositorToolbarPixelsSize;     // Size of the received toolbar pixels