Bug 892684 - Implement axis locking in AsyncPanZoomController [r=kats,botond]

This commit is contained in:
Matt Brubeck 2013-10-02 18:03:04 -07:00
parent f58ece2dd9
commit 1403178461
6 changed files with 177 additions and 16 deletions

View file

@ -42,6 +42,9 @@ pref("gfx.azpc.fling_repaint_interval", "50"); // prefer 20 fps
pref("gfx.axis.fling_friction", "0.002");
pref("gfx.axis.fling_stopped_threshold", "0.2");
// 0 = free, 1 = standard, 2 = sticky
pref("apzc.axis_lock_mode", 1);
// Enable Microsoft TSF support by default for imes.
pref("intl.enable_tsf_support", true);

View file

@ -21,6 +21,7 @@
#include "gfxTypes.h" // for gfxFloat
#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc
#include "mozilla/ClearOnShutdown.h" // for ClearOnShutdown
#include "mozilla/Constants.h" // for M_PI
#include "mozilla/EventForwards.h" // for nsEventStatus_*
#include "mozilla/Preferences.h" // for Preferences
#include "mozilla/ReentrantMonitor.h" // for ReentrantMonitorAutoEnter, etc
@ -64,7 +65,27 @@ namespace layers {
*/
static float gTouchStartTolerance = 1.0f/16.0f;
static const float EPSILON = 0.0001;
static const float EPSILON = 0.0001f;
/**
* Angle from axis within which we stay axis-locked
*/
static const double AXIS_LOCK_ANGLE = M_PI / 6.0; // 30 degrees
/**
* The distance in inches the user must pan before axis lock can be broken
*/
static const float AXIS_BREAKOUT_THRESHOLD = 1.0f/32.0f;
/**
* The angle at which axis lock can be broken
*/
static const double AXIS_BREAKOUT_ANGLE = M_PI / 8.0; // 22.5 degrees
/**
* The preferred axis locking style. See AxisLockMode for possible values.
*/
static int32_t gAxisLockMode = 0;
/**
* Maximum amount of time while panning before sending a viewport change. This
@ -153,6 +174,23 @@ static int gAsyncScrollTimeout = 300;
*/
static bool gAsyncZoomDisabled = false;
/**
* Is aAngle within the given threshold of the horizontal axis?
* @param aAngle an angle in radians in the range [0, pi]
* @param aThreshold an angle in radians in the range [0, pi/2]
*/
static bool IsCloseToHorizontal(float aAngle, float aThreshold)
{
return (aAngle < aThreshold || aAngle > (M_PI - aThreshold));
}
// As above, but for the vertical axis.
static bool IsCloseToVertical(float aAngle, float aThreshold)
{
return (fabs(aAngle - (M_PI / 2)) < aThreshold);
}
static TimeStamp sFrameTime;
static TimeStamp
@ -191,6 +229,7 @@ AsyncPanZoomController::InitializeGlobalState()
Preferences::AddIntVarCache(&gAsyncScrollThrottleTime, "apzc.asyncscroll.throttle", gAsyncScrollThrottleTime);
Preferences::AddIntVarCache(&gAsyncScrollTimeout, "apzc.asyncscroll.timeout", gAsyncScrollTimeout);
Preferences::AddBoolVarCache(&gAsyncZoomDisabled, "apzc.asynczoom.disabled", gAsyncZoomDisabled);
Preferences::AddIntVarCache(&gAxisLockMode, "apzc.axis_lock_mode", gAxisLockMode);
gComputedTimingFunction = new ComputedTimingFunction();
gComputedTimingFunction->Init(
@ -272,6 +311,11 @@ AsyncPanZoomController::GetTouchStartTolerance()
return gTouchStartTolerance;
}
/* static */AsyncPanZoomController::AxisLockMode AsyncPanZoomController::GetAxisLockMode()
{
return static_cast<AxisLockMode>(gAxisLockMode);
}
static CSSPoint
WidgetSpaceToCompensatedViewportSpace(const ScreenPoint& aPoint,
const CSSToScreenScale& aCurrentZoom)
@ -295,7 +339,7 @@ nsEventStatus AsyncPanZoomController::ReceiveInputEvent(const InputData& aEvent)
// responding in a timely fashion, this only introduces a nearly constant few
// hundred ms of lag.
if (mFrameMetrics.mMayHaveTouchListeners && aEvent.mInputType == MULTITOUCH_INPUT &&
(mState == NOTHING || mState == TOUCHING || mState == PANNING)) {
(mState == NOTHING || mState == TOUCHING || IsPanningState(mState))) {
const MultiTouchInput& multiTouchInput = aEvent.AsMultiTouchInput();
if (multiTouchInput.mType == MultiTouchInput::MULTITOUCH_START) {
SetState(WAITING_LISTENERS);
@ -413,6 +457,8 @@ nsEventStatus AsyncPanZoomController::OnTouchStart(const MultiTouchInput& aEvent
break;
case TOUCHING:
case PANNING:
case PANNING_LOCKED_X:
case PANNING_LOCKED_Y:
case PINCHING:
case WAITING_LISTENERS:
NS_WARNING("Received impossible touch in OnTouchStart");
@ -452,6 +498,8 @@ nsEventStatus AsyncPanZoomController::OnTouchMove(const MultiTouchInput& aEvent)
}
case PANNING:
case PANNING_LOCKED_X:
case PANNING_LOCKED_Y:
TrackTouch(aEvent);
return nsEventStatus_eConsumeNoDefault;
@ -495,6 +543,8 @@ nsEventStatus AsyncPanZoomController::OnTouchEnd(const MultiTouchInput& aEvent)
return nsEventStatus_eIgnore;
case PANNING:
case PANNING_LOCKED_X:
case PANNING_LOCKED_Y:
{
ReentrantMonitorAutoEnter lock(mMonitor);
ScheduleComposite();
@ -704,13 +754,37 @@ const gfx::Point AsyncPanZoomController::GetAccelerationVector() {
}
void AsyncPanZoomController::StartPanning(const MultiTouchInput& aEvent) {
float dx = mX.PanDistance(),
dy = mY.PanDistance();
ReentrantMonitorAutoEnter lock(mMonitor);
ScreenIntPoint point = GetFirstTouchScreenPoint(aEvent);
float dx = mX.PanDistance(point.x);
float dy = mY.PanDistance(point.y);
// When the touch move breaks through the pan threshold, reposition the touch down origin
// so the page won't jump when we start panning.
mX.StartTouch(point.x);
mY.StartTouch(point.y);
mLastEventTime = aEvent.mTime;
if (GetAxisLockMode() == FREE) {
SetState(PANNING);
return;
}
double angle = atan2(dy, dx); // range [-pi, pi]
angle = fabs(angle); // range [0, pi]
if (!mX.Scrollable() || !mY.Scrollable()) {
SetState(PANNING);
} else if (IsCloseToHorizontal(angle, AXIS_LOCK_ANGLE)) {
mY.SetScrollingDisabled(true);
SetState(PANNING_LOCKED_X);
} else if (IsCloseToVertical(angle, AXIS_LOCK_ANGLE)) {
mX.SetScrollingDisabled(true);
SetState(PANNING_LOCKED_Y);
} else {
SetState(PANNING);
}
}
void AsyncPanZoomController::UpdateWithTouchAtDevicePoint(const MultiTouchInput& aEvent) {
@ -775,6 +849,32 @@ void AsyncPanZoomController::TrackTouch(const MultiTouchInput& aEvent) {
return;
}
// If we're axis-locked, check if the user is trying to break the lock
if (GetAxisLockMode() == STICKY) {
ScreenIntPoint point = GetFirstTouchScreenPoint(aEvent);
float dx = mX.PanDistance(point.x);
float dy = mY.PanDistance(point.y);
double angle = atan2(dy, dx); // range [-pi, pi]
angle = fabs(angle); // range [0, pi]
float breakThreshold = AXIS_BREAKOUT_THRESHOLD * APZCTreeManager::GetDPI();
if (fabs(dx) > breakThreshold || fabs(dy) > breakThreshold) {
if (mState == PANNING_LOCKED_X) {
if (!IsCloseToHorizontal(angle, AXIS_BREAKOUT_ANGLE)) {
mY.SetScrollingDisabled(false);
SetState(PANNING);
}
} else if (mState == PANNING_LOCKED_Y) {
if (!IsCloseToVertical(angle, AXIS_BREAKOUT_ANGLE)) {
mX.SetScrollingDisabled(false);
SetState(PANNING);
}
}
}
}
UpdateWithTouchAtDevicePoint(aEvent);
AttemptScroll(prevTouchPoint, touchPoint);
@ -1361,14 +1461,18 @@ void AsyncPanZoomController::SetState(PanZoomState aNewState) {
}
if (mGeckoContentController) {
if (oldState == PANNING && aNewState != PANNING) {
if (IsPanningState(oldState) && !IsPanningState(aNewState)) {
mGeckoContentController->HandlePanEnd();
} else if (oldState != PANNING && aNewState == PANNING) {
} else if (!IsPanningState(oldState) && IsPanningState(aNewState)) {
mGeckoContentController->HandlePanBegin();
}
}
}
bool AsyncPanZoomController::IsPanningState(PanZoomState aState) {
return (aState == PANNING || aState == PANNING_LOCKED_X || aState == PANNING_LOCKED_Y);
}
void AsyncPanZoomController::TimeoutTouchListeners() {
mTouchListenerTimeoutTask = nullptr;
ContentReceivedTouch(false);

View file

@ -484,7 +484,11 @@ private:
NOTHING, /* no touch-start events received */
FLING, /* all touches removed, but we're still scrolling page */
TOUCHING, /* one touch-start event received */
PANNING, /* panning the frame */
PANNING_LOCKED_X, /* touch-start followed by move (i.e. panning with axis lock) X axis */
PANNING_LOCKED_Y, /* as above for Y axis */
PINCHING, /* nth touch-start, where n > 1. this mode allows pan and zoom */
ANIMATING_ZOOM, /* animated zoom to a new rect */
WAITING_LISTENERS, /* a state halfway between NOTHING and TOUCHING - the user has
@ -492,6 +496,14 @@ private:
prevented the default actions yet. we still need to abort animations. */
};
enum AxisLockMode {
FREE, /* No locking at all */
STANDARD, /* Default axis locking mode that remains locked until pan ends*/
STICKY, /* Allow lock to be broken, with hysteresis */
};
static AxisLockMode GetAxisLockMode();
/**
* Helper to set the current state. Holds the monitor before actually setting
* it. If the monitor is already held by the current thread, it is safe to
@ -499,6 +511,8 @@ private:
*/
void SetState(PanZoomState aState);
bool IsPanningState(PanZoomState mState);
uint64_t mLayersId;
nsRefPtr<CompositorParent> mCompositorParent;
TaskThrottler mPaintThrottler;

View file

@ -1,5 +1,5 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set sw=4 ts=8 et tw=80 : */
/* vim: set sw=2 ts=8 et tw=80 : */
/* 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/. */
@ -103,13 +103,14 @@ Axis::Axis(AsyncPanZoomController* aAsyncPanZoomController)
: mPos(0),
mVelocity(0.0f),
mAcceleration(0),
mScrollingDisabled(false),
mAsyncPanZoomController(aAsyncPanZoomController)
{
InitAxisPrefs();
}
void Axis::UpdateWithTouchAtDevicePoint(int32_t aPos, const TimeDuration& aTimeDelta) {
float newVelocity = (mPos - aPos) / aTimeDelta.ToMilliseconds();
float newVelocity = mScrollingDisabled ? 0 : (mPos - aPos) / aTimeDelta.ToMilliseconds();
bool curVelocityBelowThreshold = fabsf(newVelocity) < gVelocityThreshold;
bool directionChange = (mVelocity > 0) != (newVelocity > 0);
@ -133,9 +134,15 @@ void Axis::UpdateWithTouchAtDevicePoint(int32_t aPos, const TimeDuration& aTimeD
void Axis::StartTouch(int32_t aPos) {
mStartPos = aPos;
mPos = aPos;
mScrollingDisabled = false;
}
float Axis::AdjustDisplacement(float aDisplacement, float& aOverscrollAmountOut) {
if (mScrollingDisabled) {
aOverscrollAmountOut = 0;
return 0;
}
if (fabsf(mVelocity) < gVelocityThreshold) {
mAcceleration = 0;
}
@ -159,6 +166,10 @@ float Axis::PanDistance() {
return fabsf(mPos - mStartPos);
}
float Axis::PanDistance(float aPos) {
return fabsf(aPos - mStartPos);
}
void Axis::EndTouch() {
mAcceleration++;
@ -182,6 +193,13 @@ void Axis::CancelTouch() {
}
}
bool Axis::Scrollable() {
if (mScrollingDisabled) {
return false;
}
return GetCompositionLength() < GetPageLength();
}
bool Axis::FlingApplyFrictionOrCancel(const TimeDuration& aDelta) {
if (fabsf(mVelocity) <= gFlingStoppedThreshold) {
// If the velocity is very low, just set it to 0 and stop the fling,
@ -284,7 +302,7 @@ float Axis::ScaleWillOverscrollAmount(ScreenToScreenScale aScale, float aFocus)
}
float Axis::GetVelocity() {
return mVelocity;
return mScrollingDisabled ? 0 : mVelocity;
}
float Axis::GetAccelerationFactor() {

View file

@ -1,5 +1,5 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set sw=4 ts=8 et tw=80 : */
/* vim: set sw=2 ts=8 et tw=80 : */
/* 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/. */
@ -69,10 +69,11 @@ public:
/**
* Takes a requested displacement to the position of this axis, and adjusts
* it to account for acceleration (which might increase the displacement)
* and overscroll (which might decrease the displacement; this is to prevent
* the viewport from overscrolling the page rect). If overscroll ocurred,
* its amount is written to |aOverscrollAmountOut|.
* it to account for acceleration (which might increase the displacement),
* overscroll (which might decrease the displacement; this is to prevent the
* viewport from overscrolling the page rect), and axis locking (which might
* prevent any displacement from happening). If overscroll ocurred, its amount
* is written to |aOverscrollAmountOut|.
* The adjusted displacement is returned.
*/
float AdjustDisplacement(float aDisplacement, float& aOverscrollAmountOut);
@ -84,6 +85,12 @@ public:
*/
float PanDistance();
/**
* Gets the distance between the starting position of the touch supplied in
* startTouch() and the supplied position.
*/
float PanDistance(float aPos);
/**
* Applies friction during a fling, or cancels the fling if the velocity is
* too low. Returns true if the fling should continue to another frame, or
@ -92,6 +99,14 @@ public:
*/
bool FlingApplyFrictionOrCancel(const TimeDuration& aDelta);
/*
* Returns true if the page is zoomed in to some degree along this axis such that scrolling is
* possible and this axis has not been scroll locked while panning. Otherwise, returns false.
*/
bool Scrollable();
void SetScrollingDisabled(bool aDisabled) { mScrollingDisabled = aDisabled; }
/**
* Gets the overscroll state of the axis in its current position.
*/
@ -184,6 +199,7 @@ protected:
// they are flinging multiple times in a row very quickly, probably trying to
// reach one of the extremes of the page.
int32_t mAcceleration;
bool mScrollingDisabled; // Whether movement on this axis is locked.
AsyncPanZoomController* mAsyncPanZoomController;
nsTArray<float> mVelocityQueue;
};

View file

@ -273,6 +273,12 @@ pref("media.video_stats.enabled", true);
// Whether to enable the audio writing APIs on the audio element
pref("media.audio_data.enabled", true);
// Whether to lock touch scrolling to one axis at a time
// 0 = FREE (No locking at all)
// 1 = STANDARD (Once locked, remain locked until scrolling ends)
// 2 = STICKY (Allow lock to be broken, with hysteresis)
pref("apzc.axis_lock_mode", 0);
#ifdef XP_MACOSX
// Whether to run in native HiDPI mode on machines with "Retina"/HiDPI display;
// <= 0 : hidpi mode disabled, display will just use pixel-based upscaling