Bug 839911 - Separate animation out from layer code; r=kats
authorAnthony Jones <ajones@mozilla.com>
Fri, 06 Dec 2013 16:21:39 +1300
changeset 159092 a6008184956d376254fb49fc8f4f227b4f51eb41
parent 159091 edc4afb4bc487a782e1e252b9171d8364480e54f
child 159093 b395bc2b9ba9aa3654a0a4a31ac12aedd0ca813b
push id37185
push userajones@mozilla.com
push dateFri, 06 Dec 2013 03:21:59 +0000
treeherdermozilla-inbound@a6008184956d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskats
bugs839911
milestone28.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 839911 - Separate animation out from layer code; r=kats
gfx/layers/ipc/AsyncPanZoomController.cpp
gfx/layers/ipc/AsyncPanZoomController.h
gfx/tests/gtest/TestAsyncPanZoomController.cpp
--- a/gfx/layers/ipc/AsyncPanZoomController.cpp
+++ b/gfx/layers/ipc/AsyncPanZoomController.cpp
@@ -273,16 +273,67 @@ static TimeStamp sFrameTime;
 static TimeStamp
 GetFrameTime() {
   if (sFrameTime.IsNull()) {
     return TimeStamp::Now();
   }
   return sFrameTime;
 }
 
+class FlingAnimation: public AsyncPanZoomAnimation {
+public:
+  FlingAnimation(AxisX aX, AxisY aY)
+    : AsyncPanZoomAnimation(TimeDuration::FromMilliseconds(gFlingRepaintInterval))
+    , mX(aX)
+    , mY(aY)
+  {}
+  /**
+   * Advances a fling by an interpolated amount based on the passed in |aDelta|.
+   * This should be called whenever sampling the content transform for this
+   * frame. Returns true if the fling animation should be advanced by one frame,
+   * or false if there is no fling or the fling has ended.
+   */
+  virtual bool Sample(FrameMetrics& aFrameMetrics,
+                      const TimeDuration& aDelta);
+
+private:
+  AxisX mX;
+  AxisY mY;
+};
+
+class ZoomAnimation: public AsyncPanZoomAnimation {
+public:
+  ZoomAnimation(CSSPoint aStartOffset, CSSToScreenScale aStartZoom,
+                CSSPoint aEndOffset, CSSToScreenScale aEndZoom)
+    : mStartOffset(aStartOffset)
+    , mStartZoom(aStartZoom)
+    , mEndOffset(aEndOffset)
+    , mEndZoom(aEndZoom)
+  {}
+
+  virtual bool Sample(FrameMetrics& aFrameMetrics,
+                      const TimeDuration& aDelta);
+
+private:
+  TimeDuration mDuration;
+
+  // Old metrics from before we started a zoom animation. This is only valid
+  // when we are in the "ANIMATED_ZOOM" state. This is used so that we can
+  // interpolate between the start and end frames. We only use the
+  // |mViewportScrollOffset| and |mResolution| fields on this.
+  CSSPoint mStartOffset;
+  CSSToScreenScale mStartZoom;
+
+  // Target metrics for a zoom to animation. This is only valid when we are in
+  // the "ANIMATED_ZOOM" state. We only use the |mViewportScrollOffset| and
+  // |mResolution| fields on this.
+  CSSPoint mEndOffset;
+  CSSToScreenScale mEndZoom;
+};
+
 void
 AsyncPanZoomController::SetFrameTime(const TimeStamp& aTime) {
   sFrameTime = aTime;
 }
 
 /*static*/ void
 AsyncPanZoomController::InitializeGlobalState()
 {
@@ -592,22 +643,22 @@ nsEventStatus AsyncPanZoomController::On
     SetState(NOTHING);
     return nsEventStatus_eIgnore;
 
   case PANNING:
   case PANNING_LOCKED_X:
   case PANNING_LOCKED_Y:
     {
       ReentrantMonitorAutoEnter lock(mMonitor);
-      ScheduleComposite();
       RequestContentRepaint();
     }
     mX.EndTouch();
     mY.EndTouch();
     SetState(FLING);
+    StartAnimation(new FlingAnimation(mX, mY));
     return nsEventStatus_eConsumeNoDefault;
 
   case PINCHING:
     SetState(NOTHING);
     // Scale gesture listener should have handled this.
     NS_WARNING("Gesture listener should have handled pinching in OnTouchEnd.");
     return nsEventStatus_eIgnore;
 
@@ -994,52 +1045,51 @@ void AsyncPanZoomController::TrackTouch(
 
   CallDispatchScroll(prevTouchPoint, touchPoint, 0);
 }
 
 ScreenIntPoint& AsyncPanZoomController::GetFirstTouchScreenPoint(const MultiTouchInput& aEvent) {
   return ((SingleTouchData&)aEvent.mTouches[0]).mScreenPoint;
 }
 
-bool AsyncPanZoomController::DoFling(const TimeDuration& aDelta) {
-  if (mState != FLING) {
-    return false;
-  }
-
+bool FlingAnimation::Sample(FrameMetrics& aFrameMetrics,
+                            const TimeDuration& aDelta) {
   bool shouldContinueFlingX = mX.FlingApplyFrictionOrCancel(aDelta),
        shouldContinueFlingY = mY.FlingApplyFrictionOrCancel(aDelta);
   // If we shouldn't continue the fling, let's just stop and repaint.
   if (!shouldContinueFlingX && !shouldContinueFlingY) {
-    SendAsyncScrollEvent();
-    RequestContentRepaint();
-    SetState(NOTHING);
     return false;
   }
 
   CSSPoint overscroll; // overscroll is ignored for flings
   ScreenPoint offset(aDelta.ToMilliseconds() * mX.GetVelocity(),
                      aDelta.ToMilliseconds() * mY.GetVelocity());
 
   // Inversely scale the offset by the resolution (when you're zoomed further in,
   // a larger swipe should move you a shorter distance).
-  CSSPoint cssOffset = offset / mFrameMetrics.mZoom;
-  ScrollBy(CSSPoint::FromUnknownPoint(gfx::Point(
+  CSSPoint cssOffset = offset / aFrameMetrics.mZoom;
+  aFrameMetrics.mScrollOffset += CSSPoint::FromUnknownPoint(gfx::Point(
     mX.AdjustDisplacement(cssOffset.x, overscroll.x),
     mY.AdjustDisplacement(cssOffset.y, overscroll.y)
-  )));
-  TimeDuration timePaintDelta = mPaintThrottler.TimeSinceLastRequest(GetFrameTime());
-  if (timePaintDelta.ToMilliseconds() > gFlingRepaintInterval) {
-    RequestContentRepaint();
-  }
+  ));
 
   return true;
 }
 
+void AsyncPanZoomController::StartAnimation(AsyncPanZoomAnimation* aAnimation)
+{
+  ReentrantMonitorAutoEnter lock(mMonitor);
+  mAnimation = aAnimation;
+  mLastSampleTime = GetFrameTime();
+  ScheduleComposite();
+}
+
 void AsyncPanZoomController::CancelAnimation() {
   SetState(NOTHING);
+  mAnimation = nullptr;
 }
 
 void AsyncPanZoomController::SetCompositorParent(CompositorParent* aCompositorParent) {
   mCompositorParent = aCompositorParent;
 }
 
 void AsyncPanZoomController::ScrollBy(const CSSPoint& aOffset) {
   mFrameMetrics.mScrollOffset += aOffset;
@@ -1236,70 +1286,79 @@ AsyncPanZoomController::FireAsyncScrollO
 {
   if (mCurrentAsyncScrollOffset != mLastAsyncScrollOffset) {
     ReentrantMonitorAutoEnter lock(mMonitor);
     SendAsyncScrollEvent();
   }
   mAsyncScrollTimeoutTask = nullptr;
 }
 
+bool ZoomAnimation::Sample(FrameMetrics& aFrameMetrics,
+                           const TimeDuration& aDelta) {
+  mDuration += aDelta;
+  double animPosition = mDuration / ZOOM_TO_DURATION;
+
+  if (animPosition >= 1.0) {
+    aFrameMetrics.mZoom = mEndZoom;
+    aFrameMetrics.mScrollOffset = mEndOffset;
+    return false;
+  }
+
+  // Sample the zoom at the current time point.  The sampled zoom
+  // will affect the final computed resolution.
+  double sampledPosition = gComputedTimingFunction->GetValue(animPosition);
+
+  // We scale the scrollOffset linearly with sampledPosition, so the zoom
+  // needs to scale inversely to match.
+  aFrameMetrics.mZoom = CSSToScreenScale(1 /
+    (sampledPosition / mEndZoom.scale +
+    (1 - sampledPosition) / mStartZoom.scale));
+
+  aFrameMetrics.mScrollOffset = CSSPoint::FromUnknownPoint(gfx::Point(
+    mEndOffset.x * sampledPosition + mStartOffset.x * (1 - sampledPosition),
+    mEndOffset.y * sampledPosition + mStartOffset.y * (1 - sampledPosition)
+  ));
+
+  return true;
+}
+
+bool AsyncPanZoomController::UpdateAnimation(const TimeStamp& aSampleTime)
+{
+  if (mAnimation) {
+    if (mAnimation->Sample(mFrameMetrics, aSampleTime - mLastSampleTime)) {
+      if (mPaintThrottler.TimeSinceLastRequest(aSampleTime) >
+          mAnimation->mRepaintInterval) {
+        RequestContentRepaint();
+      }
+    } else {
+      mAnimation = nullptr;
+      SetState(NOTHING);
+      SendAsyncScrollEvent();
+      RequestContentRepaint();
+    }
+    mLastSampleTime = aSampleTime;
+    return true;
+  }
+  return false;
+}
+
 bool AsyncPanZoomController::SampleContentTransformForFrame(const TimeStamp& aSampleTime,
                                                             ViewTransform* aNewTransform,
                                                             ScreenPoint& aScrollOffset) {
   // The eventual return value of this function. The compositor needs to know
   // whether or not to advance by a frame as soon as it can. For example, if a
   // fling is happening, it has to keep compositing so that the animation is
   // smooth. If an animation frame is requested, it is the compositor's
   // responsibility to schedule a composite.
   bool requestAnimationFrame = false;
 
   {
     ReentrantMonitorAutoEnter lock(mMonitor);
 
-    switch (mState) {
-    case FLING:
-      // If a fling is currently happening, apply it now. We can pull
-      // the updated metrics afterwards.
-      requestAnimationFrame |= DoFling(aSampleTime - mLastSampleTime);
-      break;
-    case ANIMATING_ZOOM: {
-      double animPosition = (aSampleTime - mAnimationStartTime) / ZOOM_TO_DURATION;
-      if (animPosition > 1.0) {
-        animPosition = 1.0;
-      }
-      // Sample the zoom at the current time point.  The sampled zoom
-      // will affect the final computed resolution.
-      double sampledPosition = gComputedTimingFunction->GetValue(animPosition);
-
-      // We scale the scrollOffset linearly with sampledPosition, so the zoom
-      // needs to scale inversely to match.
-      mFrameMetrics.mZoom = CSSToScreenScale(1 /
-        (sampledPosition / mEndZoomToMetrics.mZoom.scale +
-          (1 - sampledPosition) / mStartZoomToMetrics.mZoom.scale));
-
-      mFrameMetrics.mScrollOffset = CSSPoint::FromUnknownPoint(gfx::Point(
-        mEndZoomToMetrics.mScrollOffset.x * sampledPosition +
-          mStartZoomToMetrics.mScrollOffset.x * (1 - sampledPosition),
-        mEndZoomToMetrics.mScrollOffset.y * sampledPosition +
-          mStartZoomToMetrics.mScrollOffset.y * (1 - sampledPosition)
-      ));
-
-      requestAnimationFrame = true;
-
-      if (aSampleTime - mAnimationStartTime >= ZOOM_TO_DURATION) {
-        SetState(NOTHING);
-        SendAsyncScrollEvent();
-        RequestContentRepaint();
-      }
-
-      break;
-    }
-    default:
-      break;
-    }
+    requestAnimationFrame = UpdateAnimation(aSampleTime);
 
     aScrollOffset = mFrameMetrics.mScrollOffset * mFrameMetrics.mZoom;
     *aNewTransform = GetCurrentAsyncTransform();
 
     LogRendertraceRect("viewport", "red",
       CSSRect(mFrameMetrics.mScrollOffset,
               ScreenSize(mFrameMetrics.mCompositionBounds.Size()) / mFrameMetrics.mZoom));
 
@@ -1328,18 +1387,16 @@ bool AsyncPanZoomController::SampleConte
   else {
     mAsyncScrollTimeoutTask =
       NewRunnableMethod(this, &AsyncPanZoomController::FireAsyncScrollOnTimeout);
     MessageLoop::current()->PostDelayedTask(FROM_HERE,
                                             mAsyncScrollTimeoutTask,
                                             gAsyncScrollTimeout);
   }
 
-  mLastSampleTime = aSampleTime;
-
   return requestAnimationFrame;
 }
 
 ViewTransform AsyncPanZoomController::GetCurrentAsyncTransform() {
   ReentrantMonitorAutoEnter lock(mMonitor);
 
   CSSPoint lastPaintScrollOffset;
   if (mLastContentPaintMetrics.IsScrollable()) {
@@ -1485,48 +1542,49 @@ void AsyncPanZoomController::ZoomToRect(
                            cssPageRect.width,
                            newHeight);
       aRect = aRect.Intersect(cssPageRect);
       targetZoom = CSSToScreenScale(std::min(compositionBounds.width / aRect.width,
                                              compositionBounds.height / aRect.height));
     }
 
     targetZoom.scale = clamped(targetZoom.scale, localMinZoom.scale, localMaxZoom.scale);
-    mEndZoomToMetrics = mFrameMetrics;
-    mEndZoomToMetrics.mZoom = targetZoom;
+    FrameMetrics endZoomToMetrics = mFrameMetrics;
+    endZoomToMetrics.mZoom = targetZoom;
 
     // Adjust the zoomToRect to a sensible position to prevent overscrolling.
-    CSSRect rectAfterZoom = mEndZoomToMetrics.CalculateCompositedRectInCssPixels();
+    CSSRect rectAfterZoom = endZoomToMetrics.CalculateCompositedRectInCssPixels();
 
     // If either of these conditions are met, the page will be
     // overscrolled after zoomed
     if (aRect.y + rectAfterZoom.height > cssPageRect.height) {
       aRect.y = cssPageRect.height - rectAfterZoom.height;
       aRect.y = aRect.y > 0 ? aRect.y : 0;
     }
     if (aRect.x + rectAfterZoom.width > cssPageRect.width) {
       aRect.x = cssPageRect.width - rectAfterZoom.width;
       aRect.x = aRect.x > 0 ? aRect.x : 0;
     }
 
-    mStartZoomToMetrics = mFrameMetrics;
-    mEndZoomToMetrics.mScrollOffset = aRect.TopLeft();
-    mEndZoomToMetrics.mDisplayPort =
-      CalculatePendingDisplayPort(mEndZoomToMetrics,
+    endZoomToMetrics.mScrollOffset = aRect.TopLeft();
+    endZoomToMetrics.mDisplayPort =
+      CalculatePendingDisplayPort(endZoomToMetrics,
                                   gfx::Point(0,0),
                                   gfx::Point(0,0),
                                   0);
 
-    mAnimationStartTime = GetFrameTime();
-
-    ScheduleComposite();
+    StartAnimation(new ZoomAnimation(
+        mFrameMetrics.mScrollOffset,
+        mFrameMetrics.mZoom,
+        endZoomToMetrics.mScrollOffset,
+        endZoomToMetrics.mZoom));
 
     // Schedule a repaint now, so the new displayport will be painted before the
     // animation finishes.
-    ScheduleContentRepaint(mEndZoomToMetrics);
+    ScheduleContentRepaint(endZoomToMetrics);
   }
 }
 
 void AsyncPanZoomController::ContentReceivedTouch(bool aPreventDefault) {
   if (!mFrameMetrics.mMayHaveTouchListeners) {
     mTouchQueue.Clear();
     return;
   }
--- a/gfx/layers/ipc/AsyncPanZoomController.h
+++ b/gfx/layers/ipc/AsyncPanZoomController.h
@@ -25,16 +25,17 @@ namespace mozilla {
 namespace layers {
 
 struct ScrollableLayerGuid;
 class CompositorParent;
 class GestureEventListener;
 class ContainerLayer;
 class ViewTransform;
 class APZCTreeManager;
+class AsyncPanZoomAnimation;
 
 /**
  * Controller for all panning and zooming logic. Any time a user input is
  * detected and it must be processed in some way to affect what the user sees,
  * it goes through here. Listens for any input event from InputData and can
  * optionally handle WidgetGUIEvent-derived touch events, but this must be done
  * on the main thread. Note that this class completely cross-platform.
  *
@@ -149,16 +150,18 @@ public:
    * in the future.
    */
   void PostDelayedTask(Task* aTask, int aDelayMs);
 
   // --------------------------------------------------------------------------
   // These methods must only be called on the compositor thread.
   //
 
+  bool UpdateAnimation(const TimeStamp& aSampleTime);
+
   /**
    * The compositor calls this when it's about to draw pannable/zoomable content
    * and is setting up transforms for compositing the layer tree. This is not
    * idempotent. For example, a fling transform can be applied each time this is
    * called (though not necessarily). |aSampleTime| is the time that this is
    * sampled at; this is used for interpolating animations. Calling this sets a
    * new transform in |aNewTransform| which should be multiplied to the transform
    * in the shadow layer corresponding to this APZC.
@@ -259,16 +262,18 @@ public:
 
   /**
    * Update mFrameMetrics.mScrollOffset to the given offset.
    * This is necessary in cases where a scroll is not caused by user
    * input (for example, a content scrollTo()).
    */
   void UpdateScrollOffset(const CSSPoint& aScrollOffset);
 
+  void StartAnimation(AsyncPanZoomAnimation* aAnimation);
+
   /**
    * Cancels any currently running animation. Note that all this does is set the
    * state of the AsyncPanZoomController back to NOTHING, but it is the
    * animation's responsibility to check this before advancing.
    */
   void CancelAnimation();
 
   /**
@@ -589,26 +594,16 @@ private:
   // the Gecko state, it should be used as a basis for untransformation when
   // sending messages back to Gecko.
   FrameMetrics mLastContentPaintMetrics;
   // The last metrics that we requested a paint for. These are used to make sure
   // that we're not requesting a paint of the same thing that's already drawn.
   // If we don't do this check, we don't get a ShadowLayersUpdated back.
   FrameMetrics mLastPaintRequestMetrics;
 
-  // Old metrics from before we started a zoom animation. This is only valid
-  // when we are in the "ANIMATED_ZOOM" state. This is used so that we can
-  // interpolate between the start and end frames. We only use the
-  // |mViewportScrollOffset| and |mResolution| fields on this.
-  FrameMetrics mStartZoomToMetrics;
-  // Target metrics for a zoom to animation. This is only valid when we are in
-  // the "ANIMATED_ZOOM" state. We only use the |mViewportScrollOffset| and
-  // |mResolution| fields on this.
-  FrameMetrics mEndZoomToMetrics;
-
   nsTArray<MultiTouchInput> mTouchQueue;
 
   CancelableTask* mTouchListenerTimeoutTask;
 
   AxisX mX;
   AxisY mY;
 
   // Most up-to-date constraints on zooming. These should always be reasonable
@@ -619,20 +614,16 @@ private:
   CSSToScreenScale mMaxZoom;
 
   // The last time the compositor has sampled the content transform for this
   // frame.
   TimeStamp mLastSampleTime;
   // The last time a touch event came through on the UI thread.
   uint32_t mLastEventTime;
 
-  // Start time of an animation. This is used for a zoom to animation to mark
-  // the beginning.
-  TimeStamp mAnimationStartTime;
-
   // Stores the previous focus point if there is a pinch gesture happening. Used
   // to allow panning by moving multiple fingers (thus moving the focus point).
   ScreenPoint mLastZoomFocus;
 
   // Stores the state of panning and zooming this frame. This is protected by
   // |mMonitor|; that is, it should be held whenever this is updated.
   PanZoomState mState;
 
@@ -650,16 +641,18 @@ private:
   CancelableTask* mAsyncScrollTimeoutTask;
 
   // Flag used to determine whether or not we should try to enter the
   // WAITING_LISTENERS state. This is used in the case that we are processing a
   // queued up event block. If set, this means that we are handling this queue
   // and we don't want to queue the events back up again.
   bool mHandlingTouchQueue;
 
+  RefPtr<AsyncPanZoomAnimation> mAnimation;
+
   friend class Axis;
 
   /* The functions and members in this section are used to build a tree
    * structure out of APZC instances. This tree can only be walked or
    * manipulated while holding the lock in the associated APZCTreeManager
    * instance.
    */
 public:
@@ -735,12 +728,35 @@ private:
   ScreenRect mVisibleRect;
   /* This is the cumulative CSS transform for all the layers between the parent
    * APZC and this one (not inclusive) */
   gfx3DMatrix mAncestorTransform;
   /* This is the CSS transform for this APZC's layer. */
   gfx3DMatrix mCSSTransform;
 };
 
+class AsyncPanZoomAnimation {
+  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AsyncPanZoomAnimation)
+
+public:
+  AsyncPanZoomAnimation(const TimeDuration& aRepaintInterval =
+                        TimeDuration::Forever())
+    : mRepaintInterval(aRepaintInterval)
+  { }
+
+  virtual ~AsyncPanZoomAnimation()
+  { }
+
+  virtual bool Sample(FrameMetrics& aFrameMetrics,
+                      const TimeDuration& aDelta) = 0;
+
+  /**
+   * Specifies how frequently (at most) we want to do repaints during the
+   * animation sequence. TimeDuration::Forever() will cause it to only repaint
+   * at the end of the animation.
+   */
+  TimeDuration mRepaintInterval;
+};
+
 }
 }
 
 #endif // mozilla_layers_PanZoomController_h
--- a/gfx/tests/gtest/TestAsyncPanZoomController.cpp
+++ b/gfx/tests/gtest/TestAsyncPanZoomController.cpp
@@ -411,17 +411,17 @@ TEST(AsyncPanZoomController, OverScrollP
 
   nsRefPtr<MockContentController> mcc = new NiceMock<MockContentController>();
   nsRefPtr<TestAPZCTreeManager> tm = new TestAPZCTreeManager();
   nsRefPtr<TestAsyncPanZoomController> apzc = new TestAsyncPanZoomController(0, mcc, tm);
 
   apzc->SetFrameMetrics(TestFrameMetrics());
   apzc->NotifyLayersUpdated(TestFrameMetrics(), true);
 
-  EXPECT_CALL(*mcc, SendAsyncScrollDOMEvent(_,_,_)).Times(3);
+  EXPECT_CALL(*mcc, SendAsyncScrollDOMEvent(_,_,_)).Times(4);
   EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(1);
 
   // Pan sufficiently to hit overscroll behavior
   int time = 0;
   int touchStart = 500;
   int touchEnd = 10;
   ScreenPoint pointOut;
   ViewTransform viewTransformOut;