From 49ffe20e5e5f83d32fb4588ae64d815e851809be Mon Sep 17 00:00:00 2001 From: Botond Ballo Date: Mon, 10 Mar 2014 20:04:44 -0400 Subject: [PATCH] Bug 965871 - Implement overscroll handoff for flings. r=kats --- gfx/layers/composite/APZCTreeManager.cpp | 140 ++++++++++++++++++---- gfx/layers/composite/APZCTreeManager.h | 14 +++ gfx/layers/ipc/AsyncPanZoomController.cpp | 85 +++++++++++-- gfx/layers/ipc/AsyncPanZoomController.h | 8 ++ gfx/layers/ipc/Axis.cpp | 4 + gfx/layers/ipc/Axis.h | 10 ++ 6 files changed, 228 insertions(+), 33 deletions(-) diff --git a/gfx/layers/composite/APZCTreeManager.cpp b/gfx/layers/composite/APZCTreeManager.cpp index 4558adc13a5e..321b765c669c 100644 --- a/gfx/layers/composite/APZCTreeManager.cpp +++ b/gfx/layers/composite/APZCTreeManager.cpp @@ -403,7 +403,7 @@ APZCTreeManager::ReceiveInputEvent(const InputData& aEvent, // then null it out so we don't keep a dangling reference and leak things. if (mTouchCount == 0) { mApzcForInputBlock = nullptr; - mOverscrollHandoffChain.clear(); + ClearOverscrollHandoffChain(); } } break; @@ -517,7 +517,7 @@ APZCTreeManager::ProcessTouchEvent(WidgetTouchEvent& aEvent, } if (mTouchCount == 0) { mApzcForInputBlock = nullptr; - mOverscrollHandoffChain.clear(); + ClearOverscrollHandoffChain(); } } return ret; @@ -677,18 +677,58 @@ APZCTreeManager::ClearTree() mRootApzc = nullptr; } +/** + * Transform a displacement from the screen coordinates of a source APZC to + * the screen coordinates of a target APZC. + * @param aTreeManager the tree manager for the APZC tree containing |aSource| + * and |aTarget| + * @param aSource the source APZC + * @param aTarget the target APZC + * @param aStartPoint the start point of the displacement + * @param aEndPoint the end point of the displacement + */ +static void +TransformDisplacement(APZCTreeManager* aTreeManager, + AsyncPanZoomController* aSource, + AsyncPanZoomController* aTarget, + ScreenPoint& aStartPoint, + ScreenPoint& aEndPoint) { + gfx3DMatrix transformToApzc; + gfx3DMatrix transformToGecko; // ignored + + // Convert start and end points to untransformed screen coordinates. + aTreeManager->GetInputTransforms(aSource, transformToApzc, transformToGecko); + ApplyTransform(&aStartPoint, transformToApzc.Inverse()); + ApplyTransform(&aEndPoint, transformToApzc.Inverse()); + + // Convert start and end points to aTarget's transformed screen coordinates. + aTreeManager->GetInputTransforms(aTarget, transformToApzc, transformToGecko); + ApplyTransform(&aStartPoint, transformToApzc); + ApplyTransform(&aEndPoint, transformToApzc); +} + void APZCTreeManager::DispatchScroll(AsyncPanZoomController* aPrev, ScreenPoint aStartPoint, ScreenPoint aEndPoint, uint32_t aOverscrollHandoffChainIndex) { - // If we have reached the end of the overscroll handoff chain, there is - // nothing more to scroll, so we ignore the rest of the pan gesture. - if (aOverscrollHandoffChainIndex >= mOverscrollHandoffChain.length()) { - // Nothing more to scroll - ignore the rest of the pan gesture. - return; + nsRefPtr next; + { + // Grab tree lock to protect mOverscrollHandoffChain from concurrent + // access from the input and compositor threads. + // Release it before calling TransformDisplacement() as that grabs the + // lock itself. + MonitorAutoLock lock(mTreeLock); + + // If we have reached the end of the overscroll handoff chain, there is + // nothing more to scroll, so we ignore the rest of the pan gesture. + if (aOverscrollHandoffChainIndex >= mOverscrollHandoffChain.length()) { + // Nothing more to scroll - ignore the rest of the pan gesture. + return; + } + + next = mOverscrollHandoffChain[aOverscrollHandoffChainIndex]; } - nsRefPtr next = mOverscrollHandoffChain[aOverscrollHandoffChainIndex]; if (next == nullptr) return; @@ -698,18 +738,7 @@ APZCTreeManager::DispatchScroll(AsyncPanZoomController* aPrev, ScreenPoint aStar // scroll grabbing to grab the scroll from it), don't bother doing the // transformations in that case. if (next != aPrev) { - gfx3DMatrix transformToApzc; - gfx3DMatrix transformToGecko; // ignored - - // Convert start and end points to untransformed screen coordinates. - GetInputTransforms(aPrev, transformToApzc, transformToGecko); - ApplyTransform(&aStartPoint, transformToApzc.Inverse()); - ApplyTransform(&aEndPoint, transformToApzc.Inverse()); - - // Convert start and end points to next's transformed screen coordinates. - GetInputTransforms(next, transformToApzc, transformToGecko); - ApplyTransform(&aStartPoint, transformToApzc); - ApplyTransform(&aEndPoint, transformToApzc); + TransformDisplacement(this, aPrev, next, aStartPoint, aEndPoint); } // Scroll |next|. If this causes overscroll, it will call DispatchScroll() @@ -717,9 +746,69 @@ APZCTreeManager::DispatchScroll(AsyncPanZoomController* aPrev, ScreenPoint aStar next->AttemptScroll(aStartPoint, aEndPoint, aOverscrollHandoffChainIndex); } +void +APZCTreeManager::HandOffFling(AsyncPanZoomController* aPrev, ScreenPoint aVelocity) +{ + // Build the overscroll handoff chain. This is necessary because it is + // otherwise built on touch-start and cleared on touch-end, and a fling + // happens after touch-end. Note that, unlike DispatchScroll() which is + // called on every touch-move during overscroll panning, + // HandleFlingOverscroll() is only called once during a fling handoff, + // so it's not worth trying to avoid building the handoff chain here. + BuildOverscrollHandoffChain(aPrev); + + nsRefPtr next; // will be used outside monitor block + { + // Grab tree lock to protect mOverscrollHandoffChain from concurrent + // access from the input and compositor threads. + // Release it before calling GetInputTransforms() as that grabs the + // lock itself. + MonitorAutoLock lock(mTreeLock); + + // Find |aPrev| in the handoff chain. + uint32_t i; + for (i = 0; i < mOverscrollHandoffChain.length(); ++i) { + if (mOverscrollHandoffChain[i] == aPrev) { + break; + } + } + + // Get the next APZC in the handoff chain, if any. + if (i + 1 < mOverscrollHandoffChain.length()) { + next = mOverscrollHandoffChain[i + 1]; + } + + // Clear the handoff chain so we don't maintain references to APZCs + // unnecessarily. + mOverscrollHandoffChain.clear(); + } + + // Nothing to hand off fling to. + if (next == nullptr) { + return; + } + + // The fling's velocity needs to be transformed from the screen coordinates + // of |aPrev| to the screen coordinates of |next|. To transform a velocity + // correctly, we need to convert it to a displacement. For now, we do this + // by anchoring it to a start point of (0, 0). + // TODO: For this to be correct in the presence of 3D transforms, we should + // use the end point of the touch that started the fling as the start point + // rather than (0, 0). + ScreenPoint startPoint; // (0, 0) + ScreenPoint endPoint = startPoint + aVelocity; + TransformDisplacement(this, aPrev, next, startPoint, endPoint); + ScreenPoint transformedVelocity = endPoint - startPoint; + + // Tell |next| to start a fling with the transformed velocity. + next->TakeOverFling(transformedVelocity); +} + + bool APZCTreeManager::FlushRepaintsForOverscrollHandoffChain() { + MonitorAutoLock lock(mTreeLock); // to access mOverscrollHandoffChain if (mOverscrollHandoffChain.length() == 0) { return false; } @@ -799,6 +888,10 @@ APZCTreeManager::BuildOverscrollHandoffChain(const nsRefPtr parent chain. @@ -822,6 +915,13 @@ APZCTreeManager::BuildOverscrollHandoffChain(const nsRefPtr mRootApzc; diff --git a/gfx/layers/ipc/AsyncPanZoomController.cpp b/gfx/layers/ipc/AsyncPanZoomController.cpp index 015b381ca5c7..28eac46f8815 100644 --- a/gfx/layers/ipc/AsyncPanZoomController.cpp +++ b/gfx/layers/ipc/AsyncPanZoomController.cpp @@ -346,10 +346,9 @@ GetFrameTime() { class FlingAnimation: public AsyncPanZoomAnimation { public: - FlingAnimation(AxisX& aX, AxisY& aY) + FlingAnimation(AsyncPanZoomController& aApzc) : AsyncPanZoomAnimation(TimeDuration::FromMilliseconds(gFlingRepaintInterval)) - , mX(aX) - , mY(aY) + , mApzc(aApzc) {} /** * Advances a fling by an interpolated amount based on the passed in |aDelta|. @@ -361,8 +360,7 @@ public: const TimeDuration& aDelta); private: - AxisX& mX; - AxisY& mY; + AsyncPanZoomController& mApzc; }; class ZoomAnimation: public AsyncPanZoomAnimation { @@ -772,7 +770,7 @@ nsEventStatus AsyncPanZoomController::OnTouchEnd(const MultiTouchInput& aEvent) mX.EndTouch(); mY.EndTouch(); SetState(FLING); - StartAnimation(new FlingAnimation(mX, mY)); + StartAnimation(new FlingAnimation(*this)); return nsEventStatus_eConsumeNoDefault; case PINCHING: @@ -1189,6 +1187,15 @@ void AsyncPanZoomController::AttemptScroll(const ScreenPoint& aStartPoint, } } +void AsyncPanZoomController::TakeOverFling(ScreenPoint aVelocity) { + // We may have a pre-existing velocity for whatever reason (for example, + // a previously handed off fling). We don't want to clobber that. + mX.SetVelocity(mX.GetVelocity() + aVelocity.x); + mY.SetVelocity(mY.GetVelocity() + aVelocity.y); + SetState(FLING); + StartAnimation(new FlingAnimation(*this)); +} + void AsyncPanZoomController::CallDispatchScroll(const ScreenPoint& aStartPoint, const ScreenPoint& aEndPoint, uint32_t aOverscrollHandoffChainIndex) { // Make a local copy of the tree manager pointer and check if it's not @@ -1248,25 +1255,77 @@ ScreenIntPoint& AsyncPanZoomController::GetFirstTouchScreenPoint(const MultiTouc bool FlingAnimation::Sample(FrameMetrics& aFrameMetrics, const TimeDuration& aDelta) { - bool shouldContinueFlingX = mX.FlingApplyFrictionOrCancel(aDelta), - shouldContinueFlingY = mY.FlingApplyFrictionOrCancel(aDelta); + + // If the fling is handed off to our APZC from a child, on the first call to + // Sample() aDelta might be negative because it's computed as the sample time + // from SampleContentTransformForFrame() minus our APZC's mLastSampleTime + // which is the time the child handed off the fling from its call to + // SampleContentTransformForFrame() with the same sample time. If we allow + // the negative aDelta to be processed, it will yield a displacement in the + // direction opposite to the fling, which can cause us to overscroll and + // hand off the fling to _our_ parent, which effectively kills the fling. + if (aDelta.ToMilliseconds() <= 0) { + return true; + } + + bool shouldContinueFlingX = mApzc.mX.FlingApplyFrictionOrCancel(aDelta), + shouldContinueFlingY = mApzc.mY.FlingApplyFrictionOrCancel(aDelta); // If we shouldn't continue the fling, let's just stop and repaint. if (!shouldContinueFlingX && !shouldContinueFlingY) { return false; } - CSSPoint overscroll; // overscroll is ignored for flings - ScreenPoint offset(aDelta.ToMilliseconds() * mX.GetVelocity(), - aDelta.ToMilliseconds() * mY.GetVelocity()); + // AdjustDisplacement() zeroes out the Axis velocity if we're in overscroll. + // Since we need to hand off the velocity to the tree manager in such a case, + // we save it here. Would be ScreenVector instead of ScreenPoint if we had + // vector classes. + ScreenPoint velocity(mApzc.mX.GetVelocity(), mApzc.mY.GetVelocity()); + + ScreenPoint offset = velocity * aDelta.ToMilliseconds(); // Inversely scale the offset by the resolution (when you're zoomed further in, // the same swipe should move you a shorter distance). CSSPoint cssOffset = offset / aFrameMetrics.mZoom; + CSSPoint overscroll; aFrameMetrics.mScrollOffset += CSSPoint( - mX.AdjustDisplacement(cssOffset.x, overscroll.x), - mY.AdjustDisplacement(cssOffset.y, overscroll.y) + mApzc.mX.AdjustDisplacement(cssOffset.x, overscroll.x), + mApzc.mY.AdjustDisplacement(cssOffset.y, overscroll.y) ); + // If the fling has caused us to reach the end of our scroll range, hand + // off the fling to the next APZC in the overscroll handoff chain. + if (!IsZero(overscroll)) { + // We may have reached the end of the scroll range along one axis but + // not the other. In such a case we only want to hand off the relevant + // component of the fling. + if (FuzzyEqualsMultiplicative(overscroll.x, 0.0f)) { + velocity.x = 0; + } else if (FuzzyEqualsMultiplicative(overscroll.y, 0.0f)) { + velocity.y = 0; + } + + // To hand off the fling, we call APZCTreeManager::HandleFlingOverscroll() + // which starts a new fling in the next APZC in the handoff chain with + // the same velocity. For simplicity, the actual overscroll of the current + // sample is discarded rather than being handed off. The compositor should + // sample animations sufficiently frequently that this is not noticeable. + + // Make a local copy of the tree manager pointer and check if it's not + // null before calling HandleFlingOverscroll(). This is necessary because + // Destroy(), which nulls out mTreeManager, could be called concurrently. + APZCTreeManager* treeManagerLocal = mApzc.mTreeManager; + if (treeManagerLocal) { + // APZC is holding mMonitor, so directly calling HandleFlingOverscroll() + // (which acquires the tree lock) would violate the lock ordering. Instead + // we schedule HandleFlingOverscroll() to be called after mMonitor is + // released. + mDeferredTasks.append(NewRunnableMethod(treeManagerLocal, + &APZCTreeManager::HandOffFling, + &mApzc, + velocity)); + } + } + return true; } diff --git a/gfx/layers/ipc/AsyncPanZoomController.h b/gfx/layers/ipc/AsyncPanZoomController.h index 09716792403d..943c0987b3ac 100644 --- a/gfx/layers/ipc/AsyncPanZoomController.h +++ b/gfx/layers/ipc/AsyncPanZoomController.h @@ -40,6 +40,7 @@ class PCompositorParent; class ViewTransform; class APZCTreeManager; class AsyncPanZoomAnimation; +class FlingAnimation; /** * Controller for all panning and zooming logic. Any time a user input is @@ -301,6 +302,12 @@ public: void AttemptScroll(const ScreenPoint& aStartPoint, const ScreenPoint& aEndPoint, uint32_t aOverscrollHandoffChainIndex = 0); + /** + * Take over a fling with the given velocity from another APZC. Used for + * during overscroll handoff for a fling. + */ + void TakeOverFling(ScreenPoint aVelocity); + /** * Returns allowed touch behavior for the given point on the scrollable layer. * Internally performs a kind of hit testing based on the regions constructed @@ -748,6 +755,7 @@ private: RefPtr mAnimation; friend class Axis; + friend class FlingAnimation; /* 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 diff --git a/gfx/layers/ipc/Axis.cpp b/gfx/layers/ipc/Axis.cpp index 9950008c64fd..b25760f5ba51 100644 --- a/gfx/layers/ipc/Axis.cpp +++ b/gfx/layers/ipc/Axis.cpp @@ -254,6 +254,10 @@ float Axis::GetVelocity() { return mAxisLocked ? 0 : mVelocity; } +void Axis::SetVelocity(float aVelocity) { + mVelocity = aVelocity; +} + float Axis::GetCompositionEnd() { return GetOrigin() + GetCompositionLength(); } diff --git a/gfx/layers/ipc/Axis.h b/gfx/layers/ipc/Axis.h index b757f5ed52a9..dd544da07cfd 100644 --- a/gfx/layers/ipc/Axis.h +++ b/gfx/layers/ipc/Axis.h @@ -128,6 +128,16 @@ public: */ float GetVelocity(); + /** + * Sets the raw velocity of this axis at this moment. + * Intended to be called only when the axis "takes over" a velocity from + * another APZC, in which case there are no touch points available to call + * UpdateWithTouchAtDevicePoint. In other circumstances, + * UpdateWithTouchAtDevicePoint should be used and the velocity calculated + * there. + */ + void SetVelocity(float aVelocity); + /** * Gets the overscroll state of the axis given an additional displacement. * That is to say, if the given displacement is applied, this will tell you