Kaydet (Commit) df433a70 authored tarafından Tomaž Vajngerl's avatar Tomaž Vajngerl

android: upgrade PanZoomController - add configurable zoom limits

Change-Id: I19815f58af4d060cffe515829a2a5472d32bf83c
üst 0455b3d4
...@@ -118,6 +118,7 @@ public class LibreOfficeMainActivity extends Activity { ...@@ -118,6 +118,7 @@ public class LibreOfficeMainActivity extends Activity {
} }
mLayerController = new LayerController(this); mLayerController = new LayerController(this);
mLayerController.setAllowZoom(true);
mLayerClient = new GeckoLayerClient(this); mLayerClient = new GeckoLayerClient(this);
mLayerController.setLayerClient(mLayerClient); mLayerController.setLayerClient(mLayerClient);
mGeckoLayout.addView(mLayerController.getView(), 0); mGeckoLayout.addView(mLayerController.getView(), 0);
......
...@@ -36,7 +36,7 @@ final class DisplayPortCalculator { ...@@ -36,7 +36,7 @@ final class DisplayPortCalculator {
private static final String PREF_DISPLAYPORT_VB_DANGER_Y_INCR = "gfx.displayport.strategy_vb.danger_y_incr"; private static final String PREF_DISPLAYPORT_VB_DANGER_Y_INCR = "gfx.displayport.strategy_vb.danger_y_incr";
private static final String PREF_DISPLAYPORT_PB_VELOCITY_THRESHOLD = "gfx.displayport.strategy_pb.threshold"; private static final String PREF_DISPLAYPORT_PB_VELOCITY_THRESHOLD = "gfx.displayport.strategy_pb.threshold";
private static DisplayPortStrategy sStrategy = new NoMarginStrategy(null); private static DisplayPortStrategy sStrategy = new DynamicResolutionStrategy(null);
static DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) { static DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
return sStrategy.calculate(metrics, (velocity == null ? ZERO_VELOCITY : velocity)); return sStrategy.calculate(metrics, (velocity == null ? ZERO_VELOCITY : velocity));
......
...@@ -172,7 +172,7 @@ public class GeckoLayerClient implements LayerView.Listener { ...@@ -172,7 +172,7 @@ public class GeckoLayerClient implements LayerView.Listener {
// Don't adjust page size when zooming unless zoom levels are // Don't adjust page size when zooming unless zoom levels are
// approximately equal. // approximately equal.
if (FloatUtils.fuzzyEquals(mLayerController.getZoomFactor(), mGeckoViewport.getZoomFactor())) { if (FloatUtils.fuzzyEquals(mLayerController.getZoomFactor(), mGeckoViewport.getZoomFactor())) {
mLayerController.setPageSize(mGeckoViewport.getPageSize()); mLayerController.setPageSize(mGeckoViewport.getPageSize(), mGeckoViewport.getPageSize());
} }
} else { } else {
mLayerController.setViewportMetrics(mGeckoViewport); mLayerController.setViewportMetrics(mGeckoViewport);
...@@ -254,7 +254,7 @@ public class GeckoLayerClient implements LayerView.Listener { ...@@ -254,7 +254,7 @@ public class GeckoLayerClient implements LayerView.Listener {
float ourZoom = mLayerController.getZoomFactor(); float ourZoom = mLayerController.getZoomFactor();
pageWidth = pageWidth * ourZoom / zoom; pageWidth = pageWidth * ourZoom / zoom;
pageHeight = pageHeight * ourZoom /zoom; pageHeight = pageHeight * ourZoom /zoom;
mLayerController.setPageSize(new FloatSize(pageWidth, pageHeight)); mLayerController.setPageSize(new FloatSize(pageWidth, pageHeight), new FloatSize(pageWidth, pageHeight));
// Here the page size of the document has changed, but the document being displayed // Here the page size of the document has changed, but the document being displayed
// is still the same. Therefore, we don't need to send anything to browser.js; any // is still the same. Therefore, we don't need to send anything to browser.js; any
// changes we need to make to the display port will get sent the next time we call // changes we need to make to the display port will get sent the next time we call
...@@ -296,13 +296,13 @@ public class GeckoLayerClient implements LayerView.Listener { ...@@ -296,13 +296,13 @@ public class GeckoLayerClient implements LayerView.Listener {
} }
@Override @Override
public void compositionResumeRequested() { public void compositionResumeRequested(int width, int height) {
} }
@Override @Override
public void surfaceChanged(int width, int height) { public void surfaceChanged(int width, int height) {
compositionResumeRequested(); compositionResumeRequested(width, height);
renderRequested(); renderRequested();
} }
......
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* ***** BEGIN LICENSE BLOCK ***** * This Source Code Form is subject to the terms of the Mozilla Public
* Version: MPL 1.1/GPL 2.0/LGPL 2.1 * 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/. */
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Mozilla Android code.
*
* The Initial Developer of the Original Code is Mozilla Foundation.
* Portions created by the Initial Developer are Copyright (C) 2009-2010
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Patrick Walton <pcwalton@mozilla.com>
* Chris Lord <chrislord.net@gmail.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
package org.mozilla.gecko.gfx; package org.mozilla.gecko.gfx;
...@@ -42,6 +9,7 @@ import android.content.Context; ...@@ -42,6 +9,7 @@ import android.content.Context;
import android.content.res.Resources; import android.content.res.Resources;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.PointF; import android.graphics.PointF;
import android.graphics.RectF; import android.graphics.RectF;
import android.view.GestureDetector; import android.view.GestureDetector;
...@@ -49,8 +17,6 @@ import android.view.GestureDetector; ...@@ -49,8 +17,6 @@ import android.view.GestureDetector;
import org.mozilla.gecko.ui.PanZoomController; import org.mozilla.gecko.ui.PanZoomController;
import org.mozilla.gecko.ui.SimpleScaleGestureDetector; import org.mozilla.gecko.ui.SimpleScaleGestureDetector;
import java.util.regex.Pattern;
/** /**
* The layer controller manages a tile that represents the visible page. It does panning and * The layer controller manages a tile that represents the visible page. It does panning and
* zooming natively by delegating to a panning/zooming controller. Touch events can be dispatched * zooming natively by delegating to a panning/zooming controller. Touch events can be dispatched
...@@ -87,12 +53,15 @@ public class LayerController { ...@@ -87,12 +53,15 @@ public class LayerController {
private GeckoLayerClient mLayerClient; /* The layer client. */ private GeckoLayerClient mLayerClient; /* The layer client. */
/* The new color for the checkerboard. */ /* The new color for the checkerboard. */
private int mCheckerboardColor; private int mCheckerboardColor = Color.WHITE;
private boolean mCheckerboardShouldShowChecks; private boolean mCheckerboardShouldShowChecks;
private boolean mForceRedraw; private boolean mAllowZoom;
private float mDefaultZoom;
private float mMinZoom;
private float mMaxZoom;
private static Pattern sColorPattern; private boolean mForceRedraw;
public LayerController(Context context) { public LayerController(Context context) {
mContext = context; mContext = context;
...@@ -124,6 +93,10 @@ public class LayerController { ...@@ -124,6 +93,10 @@ public class LayerController {
return mViewportMetrics.getViewport(); return mViewportMetrics.getViewport();
} }
public RectF getCssViewport() {
return mViewportMetrics.getCssViewport();
}
public FloatSize getViewportSize() { public FloatSize getViewportSize() {
return mViewportMetrics.getSize(); return mViewportMetrics.getSize();
} }
...@@ -132,6 +105,10 @@ public class LayerController { ...@@ -132,6 +105,10 @@ public class LayerController {
return mViewportMetrics.getPageSize(); return mViewportMetrics.getPageSize();
} }
public FloatSize getCssPageSize() {
return mViewportMetrics.getCssPageSize();
}
public PointF getOrigin() { public PointF getOrigin() {
return mViewportMetrics.getOrigin(); return mViewportMetrics.getOrigin();
} }
...@@ -189,12 +166,12 @@ public class LayerController { ...@@ -189,12 +166,12 @@ public class LayerController {
} }
/** Sets the current page size. You must hold the monitor while calling this. */ /** Sets the current page size. You must hold the monitor while calling this. */
public void setPageSize(FloatSize size) { public void setPageSize(FloatSize size, FloatSize cssSize) {
if (mViewportMetrics.getPageSize().fuzzyEquals(size)) if (mViewportMetrics.getCssPageSize().equals(cssSize))
return; return;
ViewportMetrics viewportMetrics = new ViewportMetrics(mViewportMetrics); ViewportMetrics viewportMetrics = new ViewportMetrics(mViewportMetrics);
viewportMetrics.setPageSize(size, size); viewportMetrics.setPageSize(size, cssSize);
mViewportMetrics = new ImmutableViewportMetrics(viewportMetrics); mViewportMetrics = new ImmutableViewportMetrics(viewportMetrics);
// Page size is owned by the layer client, so no need to notify it of // Page size is owned by the layer client, so no need to notify it of
...@@ -294,8 +271,9 @@ public class LayerController { ...@@ -294,8 +271,9 @@ public class LayerController {
* correct. * correct.
*/ */
public PointF convertViewPointToLayerPoint(PointF viewPoint) { public PointF convertViewPointToLayerPoint(PointF viewPoint) {
if (mRootLayer == null) if (mLayerClient == null) {
return null; return null;
}
ImmutableViewportMetrics viewportMetrics = mViewportMetrics; ImmutableViewportMetrics viewportMetrics = mViewportMetrics;
PointF origin = viewportMetrics.getOrigin(); PointF origin = viewportMetrics.getOrigin();
...@@ -337,5 +315,41 @@ public class LayerController { ...@@ -337,5 +315,41 @@ public class LayerController {
mCheckerboardColor = newColor; mCheckerboardColor = newColor;
mView.requestRender(); mView.requestRender();
} }
}
public void setAllowZoom(final boolean aValue) {
mAllowZoom = aValue;
mView.post(new Runnable() {
public void run() {
mView.getTouchEventHandler().setDoubleTapEnabled(aValue);
}
});
}
public boolean getAllowZoom() {
return mAllowZoom;
}
public void setDefaultZoom(float aValue) {
mDefaultZoom = aValue;
}
public float getDefaultZoom() {
return mDefaultZoom;
}
public void setMinZoom(float aValue) {
mMinZoom = aValue;
}
public float getMinZoom() {
return mMinZoom;
}
public void setMaxZoom(float aValue) {
mMaxZoom = aValue;
}
public float getMaxZoom() {
return mMaxZoom;
}
}
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* ***** BEGIN LICENSE BLOCK ***** * This Source Code Form is subject to the terms of the Mozilla Public
* Version: MPL 1.1/GPL 2.0/LGPL 2.1 * 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/. */
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Mozilla Android code.
*
* The Initial Developer of the Original Code is Mozilla Foundation.
* Portions created by the Initial Developer are Copyright (C) 2009-2010
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Patrick Walton <pcwalton@mozilla.com>
* Arkady Blyakher <rkadyb@mit.edu>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
package org.mozilla.gecko.gfx; package org.mozilla.gecko.gfx;
...@@ -42,7 +9,6 @@ package org.mozilla.gecko.gfx; ...@@ -42,7 +9,6 @@ package org.mozilla.gecko.gfx;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.PixelFormat; import android.graphics.PixelFormat;
import android.opengl.GLSurfaceView;
import android.util.Log; import android.util.Log;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.MotionEvent; import android.view.MotionEvent;
...@@ -227,7 +193,7 @@ public class LayerView extends SurfaceView implements SurfaceHolder.Callback { ...@@ -227,7 +193,7 @@ public class LayerView extends SurfaceView implements SurfaceHolder.Callback {
} }
public GLSurfaceView.Renderer getRenderer() { public LayerRenderer getRenderer() {
return mRenderer; return mRenderer;
} }
...@@ -235,7 +201,7 @@ public class LayerView extends SurfaceView implements SurfaceHolder.Callback { ...@@ -235,7 +201,7 @@ public class LayerView extends SurfaceView implements SurfaceHolder.Callback {
mListener = listener; mListener = listener;
} }
public synchronized GLController getGLController() { public GLController getGLController() {
return mGLController; return mGLController;
} }
...@@ -288,7 +254,7 @@ public class LayerView extends SurfaceView implements SurfaceHolder.Callback { ...@@ -288,7 +254,7 @@ public class LayerView extends SurfaceView implements SurfaceHolder.Callback {
public interface Listener { public interface Listener {
void renderRequested(); void renderRequested();
void compositionPauseRequested(); void compositionPauseRequested();
void compositionResumeRequested(); void compositionResumeRequested(int width, int height);
void surfaceChanged(int width, int height); void surfaceChanged(int width, int height);
} }
......
...@@ -10,8 +10,8 @@ import android.os.SystemClock; ...@@ -10,8 +10,8 @@ import android.os.SystemClock;
import android.view.GestureDetector; import android.view.GestureDetector;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View.OnTouchListener; import android.view.View.OnTouchListener;
import android.view.ViewConfiguration;
import org.mozilla.gecko.ui.PanZoomController;
import org.mozilla.gecko.ui.SimpleScaleGestureDetector; import org.mozilla.gecko.ui.SimpleScaleGestureDetector;
import java.util.LinkedList; import java.util.LinkedList;
...@@ -41,18 +41,24 @@ import java.util.Queue; ...@@ -41,18 +41,24 @@ import java.util.Queue;
* at some point after the first or second event in the block is processed in Gecko. * at some point after the first or second event in the block is processed in Gecko.
* This code assumes we get EXACTLY ONE default-prevented notification for each block * This code assumes we get EXACTLY ONE default-prevented notification for each block
* of events. * of events.
*
* Note that even if all events are default-prevented, we still send specific types
* of notifications to the pan/zoom controller. The notifications are needed
* to respond to user actions a timely manner regardless of default-prevention,
* and fix issues like bug 749384.
*/ */
public final class TouchEventHandler { public final class TouchEventHandler {
private static final String LOGTAG = "GeckoTouchEventHandler"; private static final String LOGTAG = "GeckoTouchEventHandler";
// The time limit for listeners to respond with preventDefault on touchevents // The time limit for listeners to respond with preventDefault on touchevents
// before we begin panning the page // before we begin panning the page
private final int EVENT_LISTENER_TIMEOUT = ViewConfiguration.getLongPressTimeout(); private final int EVENT_LISTENER_TIMEOUT = 200;
private final LayerView mView; private final LayerView mView;
private final LayerController mController;
private final GestureDetector mGestureDetector; private final GestureDetector mGestureDetector;
private final SimpleScaleGestureDetector mScaleGestureDetector; private final SimpleScaleGestureDetector mScaleGestureDetector;
private final PanZoomController mPanZoomController;
private final GestureDetector.OnDoubleTapListener mDoubleTapListener;
// the queue of events that we are holding on to while waiting for a preventDefault // the queue of events that we are holding on to while waiting for a preventDefault
// notification // notification
...@@ -119,15 +125,16 @@ public final class TouchEventHandler { ...@@ -119,15 +125,16 @@ public final class TouchEventHandler {
TouchEventHandler(Context context, LayerView view, LayerController controller) { TouchEventHandler(Context context, LayerView view, LayerController controller) {
mView = view; mView = view;
mController = controller;
mEventQueue = new LinkedList<MotionEvent>(); mEventQueue = new LinkedList<MotionEvent>();
mGestureDetector = new GestureDetector(context, controller.getGestureListener()); mGestureDetector = new GestureDetector(context, controller.getGestureListener());
mScaleGestureDetector = new SimpleScaleGestureDetector(controller.getScaleGestureListener()); mScaleGestureDetector = new SimpleScaleGestureDetector(controller.getScaleGestureListener());
mPanZoomController = controller.getPanZoomController();
mListenerTimeoutProcessor = new ListenerTimeoutProcessor(); mListenerTimeoutProcessor = new ListenerTimeoutProcessor();
mDispatchEvents = true; mDispatchEvents = true;
mGestureDetector.setOnDoubleTapListener(controller.getDoubleTapListener()); mDoubleTapListener = controller.getDoubleTapListener();
setDoubleTapEnabled(true);
} }
/* This function MUST be called on the UI thread */ /* This function MUST be called on the UI thread */
...@@ -142,7 +149,18 @@ public final class TouchEventHandler { ...@@ -142,7 +149,18 @@ public final class TouchEventHandler {
if (isDownEvent(event)) { if (isDownEvent(event)) {
// this is the start of a new block of events! whee! // this is the start of a new block of events! whee!
mHoldInQueue = mWaitForTouchListeners; mHoldInQueue = mWaitForTouchListeners;
// Set mDispatchEvents to true so that we are guaranteed to either queue these
// events or dispatch them. The only time we should not do either is once we've
// heard back from content to preventDefault this block.
mDispatchEvents = true;
if (mHoldInQueue) { if (mHoldInQueue) {
// if the new block we are starting is the current block (i.e. there are no
// other blocks waiting in the queue, then we should let the pan/zoom controller
// know we are waiting for the touch listeners to run
if (mEventQueue.isEmpty()) {
mPanZoomController.waitingForTouchListeners(event);
}
// if we're holding the events in the queue, set the timeout so that // if we're holding the events in the queue, set the timeout so that
// we dispatch these events if we don't get a default-prevented notification // we dispatch these events if we don't get a default-prevented notification
mView.postDelayed(mListenerTimeoutProcessor, EVENT_LISTENER_TIMEOUT); mView.postDelayed(mListenerTimeoutProcessor, EVENT_LISTENER_TIMEOUT);
...@@ -164,6 +182,8 @@ public final class TouchEventHandler { ...@@ -164,6 +182,8 @@ public final class TouchEventHandler {
mEventQueue.add(MotionEvent.obtain(event)); mEventQueue.add(MotionEvent.obtain(event));
} else if (mDispatchEvents) { } else if (mDispatchEvents) {
dispatchEvent(event); dispatchEvent(event);
} else if (touchFinished(event)) {
mPanZoomController.preventedTouchFinished();
} }
// notify gecko of the event // notify gecko of the event
...@@ -192,6 +212,11 @@ public final class TouchEventHandler { ...@@ -192,6 +212,11 @@ public final class TouchEventHandler {
mProcessingBalance--; mProcessingBalance--;
} }
/* This function MUST be called on the UI thread. */
public void setDoubleTapEnabled(boolean aValue) {
mGestureDetector.setOnDoubleTapListener(aValue ? mDoubleTapListener : null);
}
/* This function MUST be called on the UI thread. */ /* This function MUST be called on the UI thread. */
public void setWaitForTouchListeners(boolean aValue) { public void setWaitForTouchListeners(boolean aValue) {
mWaitForTouchListeners = aValue; mWaitForTouchListeners = aValue;
...@@ -207,18 +232,32 @@ public final class TouchEventHandler { ...@@ -207,18 +232,32 @@ public final class TouchEventHandler {
return (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN); return (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN);
} }
private boolean touchFinished(MotionEvent event) {
int action = (event.getAction() & MotionEvent.ACTION_MASK);
return (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL);
}
/** /**
* Dispatch the event to the gesture detectors and the pan/zoom controller. * Dispatch the event to the gesture detectors and the pan/zoom controller.
*/ */
private void dispatchEvent(MotionEvent event) { private void dispatchEvent(MotionEvent event) {
if (mGestureDetector.onTouchEvent(event)) { if (mGestureDetector.onTouchEvent(event)) {
// An up/cancel event should get passed to both detectors, in
// case it comes from a pointer the scale detector is tracking.
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
break;
default:
return; return;
} }
}
mScaleGestureDetector.onTouchEvent(event); mScaleGestureDetector.onTouchEvent(event);
if (mScaleGestureDetector.isInProgress()) { if (mScaleGestureDetector.isInProgress()) {
return; return;
} }
mController.getPanZoomController().onTouchEvent(event); mPanZoomController.onTouchEvent(event);
} }
/** /**
...@@ -244,6 +283,8 @@ public final class TouchEventHandler { ...@@ -244,6 +283,8 @@ public final class TouchEventHandler {
// default-prevented. // default-prevented.
if (allowDefaultAction) { if (allowDefaultAction) {
dispatchEvent(event); dispatchEvent(event);
} else if (touchFinished(event)) {
mPanZoomController.preventedTouchFinished();
} }
event = mEventQueue.peek(); event = mEventQueue.peek();
if (event == null) { if (event == null) {
...@@ -259,6 +300,7 @@ public final class TouchEventHandler { ...@@ -259,6 +300,7 @@ public final class TouchEventHandler {
if (isDownEvent(event)) { if (isDownEvent(event)) {
// we have finished processing the block we were interested in. // we have finished processing the block we were interested in.
// now we wait for the next call to processEventBlock // now we wait for the next call to processEventBlock
mPanZoomController.waitingForTouchListeners(event);
break; break;
} }
// pop the event we peeked above, as it is still part of the block and // pop the event we peeked above, as it is still part of the block and
......
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* ***** BEGIN LICENSE BLOCK ***** * This Source Code Form is subject to the terms of the Mozilla Public
* Version: MPL 1.1/GPL 2.0/LGPL 2.1 * 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/. */
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Mozilla Android code.
*
* The Initial Developer of the Original Code is Mozilla Foundation.
* Portions created by the Initial Developer are Copyright (C) 2009-2012
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Patrick Walton <pcwalton@mozilla.com>
* Kartikaya Gupta <kgupta@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
package org.mozilla.gecko.ui; package org.mozilla.gecko.ui;
...@@ -52,6 +19,8 @@ import org.mozilla.gecko.gfx.LayerController; ...@@ -52,6 +19,8 @@ import org.mozilla.gecko.gfx.LayerController;
import org.mozilla.gecko.gfx.ViewportMetrics; import org.mozilla.gecko.gfx.ViewportMetrics;
import org.mozilla.gecko.util.FloatUtils; import org.mozilla.gecko.util.FloatUtils;
import java.util.Arrays;
import java.util.StringTokenizer;
import java.util.Timer; import java.util.Timer;
import java.util.TimerTask; import java.util.TimerTask;
...@@ -85,7 +54,7 @@ public class PanZoomController ...@@ -85,7 +54,7 @@ public class PanZoomController
private static final float MAX_ZOOM = 4.0f; private static final float MAX_ZOOM = 4.0f;
/* 16 precomputed frames of the _ease-out_ animation from the CSS Transitions specification. */ /* 16 precomputed frames of the _ease-out_ animation from the CSS Transitions specification. */
private static final float[] EASE_OUT_ANIMATION_FRAMES = { private static float[] ZOOM_ANIMATION_FRAMES = new float[] {
0.00000f, /* 0 */ 0.00000f, /* 0 */
0.10211f, /* 1 */ 0.10211f, /* 1 */
0.19864f, /* 2 */ 0.19864f, /* 2 */
...@@ -115,7 +84,11 @@ public class PanZoomController ...@@ -115,7 +84,11 @@ public class PanZoomController
PANNING_HOLD_LOCKED, /* like PANNING_HOLD, but axis lock still in effect */ PANNING_HOLD_LOCKED, /* like PANNING_HOLD, but axis lock still in effect */
PINCHING, /* nth touch-start, where n > 1. this mode allows pan and zoom */ PINCHING, /* nth touch-start, where n > 1. this mode allows pan and zoom */
ANIMATED_ZOOM, /* animated zoom to a new rect */ ANIMATED_ZOOM, /* animated zoom to a new rect */
BOUNCE /* in a bounce animation */ BOUNCE, /* in a bounce animation */
WAITING_LISTENERS, /* a state halfway between NOTHING and TOUCHING - the user has
put a finger down, but we don't yet know if a touch listener has
prevented the default actions yet. we still need to abort animations. */
} }
private final LayerController mController; private final LayerController mController;
...@@ -156,6 +129,23 @@ public class PanZoomController ...@@ -156,6 +129,23 @@ public class PanZoomController
} }
} }
private void setZoomAnimationFrames(String frames) {
try {
if (frames.length() > 0) {
StringTokenizer st = new StringTokenizer(frames, ",");
float[] values = new float[st.countTokens()];
for (int i = 0; i < values.length; i++) {
values[i] = Float.parseFloat(st.nextToken());
}
ZOOM_ANIMATION_FRAMES = values;
}
} catch (NumberFormatException e) {
Log.e(LOGTAG, "Error setting zoom animation frames", e);
} finally {
Log.i(LOGTAG, "Zoom animation frames: " + Arrays.toString(ZOOM_ANIMATION_FRAMES));
}
}
public boolean onTouchEvent(MotionEvent event) { public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction() & MotionEvent.ACTION_MASK) { switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: return onTouchStart(event); case MotionEvent.ACTION_DOWN: return onTouchStart(event);
...@@ -195,6 +185,30 @@ public class PanZoomController ...@@ -195,6 +185,30 @@ public class PanZoomController
} }
} }
/** This function must be called on the UI thread. */
public void waitingForTouchListeners(MotionEvent event) {
checkMainThread();
if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {
// this is the first touch point going down, so we enter the pending state
mSubscroller.cancel();
// seting the state will kill any animations in progress, possibly leaving
// the page in overscroll
mState = PanZoomState.WAITING_LISTENERS;
}
}
/** This function must be called on the UI thread. */
public void preventedTouchFinished() {
checkMainThread();
if (mState == PanZoomState.WAITING_LISTENERS) {
// if we enter here, we just finished a block of events whose default actions
// were prevented by touch listeners. Now there are no touch points left, so
// we need to reset our state and re-bounce because we might be in overscroll
mState = PanZoomState.NOTHING;
bounce();
}
}
/** This must be called on the UI thread. */ /** This must be called on the UI thread. */
public void pageSizeUpdated() { public void pageSizeUpdated() {
if (mState == PanZoomState.NOTHING) { if (mState == PanZoomState.NOTHING) {
...@@ -222,10 +236,16 @@ public class PanZoomController ...@@ -222,10 +236,16 @@ public class PanZoomController
switch (mState) { switch (mState) {
case ANIMATED_ZOOM: case ANIMATED_ZOOM:
return false; // We just interrupted a double-tap animation, so force a redraw in
// case this touchstart is just a tap that doesn't end up triggering
// a redraw
mController.setForceRedraw();
mController.notifyLayerClientOfGeometryChange();
// fall through
case FLING: case FLING:
case BOUNCE: case BOUNCE:
case NOTHING: case NOTHING:
case WAITING_LISTENERS:
startTouch(event.getX(0), event.getY(0), event.getEventTime()); startTouch(event.getX(0), event.getY(0), event.getEventTime());
return false; return false;
case TOUCHING: case TOUCHING:
...@@ -244,11 +264,16 @@ public class PanZoomController ...@@ -244,11 +264,16 @@ public class PanZoomController
private boolean onTouchMove(MotionEvent event) { private boolean onTouchMove(MotionEvent event) {
switch (mState) { switch (mState) {
case NOTHING:
case FLING: case FLING:
case BOUNCE: case BOUNCE:
case WAITING_LISTENERS:
// should never happen // should never happen
Log.e(LOGTAG, "Received impossible touch move while in " + mState); Log.e(LOGTAG, "Received impossible touch move while in " + mState);
// fall through
case ANIMATED_ZOOM:
case NOTHING:
// may happen if user double-taps and drags without lifting after the
// second tap. ignore the move if this happens.
return false; return false;
case TOUCHING: case TOUCHING:
...@@ -274,7 +299,6 @@ public class PanZoomController ...@@ -274,7 +299,6 @@ public class PanZoomController
track(event); track(event);
return true; return true;
case ANIMATED_ZOOM:
case PINCHING: case PINCHING:
// scale gesture listener will handle this // scale gesture listener will handle this
return false; return false;
...@@ -286,12 +310,18 @@ public class PanZoomController ...@@ -286,12 +310,18 @@ public class PanZoomController
private boolean onTouchEnd(MotionEvent event) { private boolean onTouchEnd(MotionEvent event) {
switch (mState) { switch (mState) {
case NOTHING:
case FLING: case FLING:
case BOUNCE: case BOUNCE:
case WAITING_LISTENERS:
// should never happen // should never happen
Log.e(LOGTAG, "Received impossible touch end while in " + mState); Log.e(LOGTAG, "Received impossible touch end while in " + mState);
// fall through
case ANIMATED_ZOOM:
case NOTHING:
// may happen if user double-taps and drags without lifting after the
// second tap. ignore if this happens.
return false; return false;
case TOUCHING: case TOUCHING:
mState = PanZoomState.NOTHING; mState = PanZoomState.NOTHING;
// the switch into TOUCHING might have happened while the page was // the switch into TOUCHING might have happened while the page was
...@@ -299,6 +329,7 @@ public class PanZoomController ...@@ -299,6 +329,7 @@ public class PanZoomController
// was the case // was the case
bounce(); bounce();
return false; return false;
case PANNING: case PANNING:
case PANNING_LOCKED: case PANNING_LOCKED:
case PANNING_HOLD: case PANNING_HOLD:
...@@ -306,19 +337,28 @@ public class PanZoomController ...@@ -306,19 +337,28 @@ public class PanZoomController
mState = PanZoomState.FLING; mState = PanZoomState.FLING;
fling(); fling();
return true; return true;
case PINCHING: case PINCHING:
mState = PanZoomState.NOTHING; mState = PanZoomState.NOTHING;
return true; return true;
case ANIMATED_ZOOM:
return false;
} }
Log.e(LOGTAG, "Unhandled case " + mState + " in onTouchEnd"); Log.e(LOGTAG, "Unhandled case " + mState + " in onTouchEnd");
return false; return false;
} }
private boolean onTouchCancel(MotionEvent event) { private boolean onTouchCancel(MotionEvent event) {
mState = PanZoomState.NOTHING; if (mState == PanZoomState.WAITING_LISTENERS) {
// we might get a cancel event from the TouchEventHandler while in the
// WAITING_LISTENERS state if the touch listeners prevent-default the
// block of events. at this point being in WAITING_LISTENERS is equivalent
// to being in NOTHING with the exception of possibly being in overscroll.
// so here we don't want to do anything right now; the overscroll will be
// corrected in preventedTouchFinished().
return false;
}
cancelTouch(); cancelTouch();
mState = PanZoomState.NOTHING;
// ensure we snap back if we're overscrolled // ensure we snap back if we're overscrolled
bounce(); bounce();
return false; return false;
...@@ -423,16 +463,17 @@ public class PanZoomController ...@@ -423,16 +463,17 @@ public class PanZoomController
return; return;
} }
mState = PanZoomState.BOUNCE; // At this point we have already set mState to BOUNCE or ANIMATED_ZOOM, so
// set the animation target *after* setting state BOUNCE, so that // getRedrawHint() is returning false. This means we can safely call
// the getRedrawHint() is returning false and we don't clobber the display // setAnimationTarget to set the new final display port and not have it get
// port we set as a result of this animation target call. // clobbered by display ports from intermediate animation frames.
mController.setAnimationTarget(metrics); mController.setAnimationTarget(metrics);
startAnimationTimer(new BounceRunnable(bounceStartMetrics, metrics)); startAnimationTimer(new BounceRunnable(bounceStartMetrics, metrics));
} }
/* Performs a bounce-back animation to the nearest valid viewport metrics. */ /* Performs a bounce-back animation to the nearest valid viewport metrics. */
private void bounce() { private void bounce() {
mState = PanZoomState.BOUNCE;
bounce(getValidViewportMetrics()); bounce(getValidViewportMetrics());
} }
...@@ -477,14 +518,14 @@ public class PanZoomController ...@@ -477,14 +518,14 @@ public class PanZoomController
return getVelocity() < STOPPED_THRESHOLD; return getVelocity() < STOPPED_THRESHOLD;
} }
PointF getDisplacement() { PointF resetDisplacement() {
return new PointF(mX.resetDisplacement(), mY.resetDisplacement()); return new PointF(mX.resetDisplacement(), mY.resetDisplacement());
} }
private void updatePosition() { private void updatePosition() {
mX.displace(); mX.displace();
mY.displace(); mY.displace();
PointF displacement = getDisplacement(); PointF displacement = resetDisplacement();
if (FloatUtils.fuzzyEquals(displacement.x, 0.0f) && FloatUtils.fuzzyEquals(displacement.y, 0.0f)) { if (FloatUtils.fuzzyEquals(displacement.x, 0.0f) && FloatUtils.fuzzyEquals(displacement.y, 0.0f)) {
return; return;
} }
...@@ -542,13 +583,13 @@ public class PanZoomController ...@@ -542,13 +583,13 @@ public class PanZoomController
* animation by setting the state to PanZoomState.NOTHING. Handle this case and bail * animation by setting the state to PanZoomState.NOTHING. Handle this case and bail
* out. * out.
*/ */
if (mState != PanZoomState.BOUNCE) { if (!(mState == PanZoomState.BOUNCE || mState == PanZoomState.ANIMATED_ZOOM)) {
finishAnimation(); finishAnimation();
return; return;
} }
/* Perform the next frame of the bounce-back animation. */ /* Perform the next frame of the bounce-back animation. */
if (mBounceFrame < EASE_OUT_ANIMATION_FRAMES.length) { if (mBounceFrame < ZOOM_ANIMATION_FRAMES.length) {
advanceBounce(); advanceBounce();
return; return;
} }
...@@ -562,7 +603,7 @@ public class PanZoomController ...@@ -562,7 +603,7 @@ public class PanZoomController
/* Performs one frame of a bounce animation. */ /* Performs one frame of a bounce animation. */
private void advanceBounce() { private void advanceBounce() {
synchronized (mController) { synchronized (mController) {
float t = EASE_OUT_ANIMATION_FRAMES[mBounceFrame]; float t = ZOOM_ANIMATION_FRAMES[mBounceFrame];
ViewportMetrics newMetrics = mBounceStartMetrics.interpolate(mBounceEndMetrics, t); ViewportMetrics newMetrics = mBounceStartMetrics.interpolate(mBounceEndMetrics, t);
mController.setViewportMetrics(newMetrics); mController.setViewportMetrics(newMetrics);
mController.notifyLayerClientOfGeometryChange(); mController.notifyLayerClientOfGeometryChange();
...@@ -655,19 +696,37 @@ public class PanZoomController ...@@ -655,19 +696,37 @@ public class PanZoomController
float focusX = viewport.width() / 2.0f; float focusX = viewport.width() / 2.0f;
float focusY = viewport.height() / 2.0f; float focusY = viewport.height() / 2.0f;
float minZoomFactor = 0.0f; float minZoomFactor = 0.0f;
if (viewport.width() > pageSize.width && pageSize.width > 0) { float maxZoomFactor = MAX_ZOOM;
if (mController.getMinZoom() > 0)
minZoomFactor = mController.getMinZoom();
if (mController.getMaxZoom() > 0)
maxZoomFactor = mController.getMaxZoom();
if (!mController.getAllowZoom()) {
// If allowZoom is false, clamp to the default zoom level.
maxZoomFactor = minZoomFactor = mController.getDefaultZoom();
}
// Ensure minZoomFactor keeps the page at least as big as the viewport.
if (pageSize.width > 0) {
float scaleFactor = viewport.width() / pageSize.width; float scaleFactor = viewport.width() / pageSize.width;
minZoomFactor = Math.max(minZoomFactor, zoomFactor * scaleFactor); minZoomFactor = Math.max(minZoomFactor, zoomFactor * scaleFactor);
if (viewport.width() > pageSize.width)
focusX = 0.0f; focusX = 0.0f;
} }
if (viewport.height() > pageSize.height && pageSize.height > 0) { if (pageSize.height > 0) {
float scaleFactor = viewport.height() / pageSize.height; float scaleFactor = viewport.height() / pageSize.height;
minZoomFactor = Math.max(minZoomFactor, zoomFactor * scaleFactor); minZoomFactor = Math.max(minZoomFactor, zoomFactor * scaleFactor);
if (viewport.height() > pageSize.height)
focusY = 0.0f; focusY = 0.0f;
} }
if (!FloatUtils.fuzzyEquals(minZoomFactor, 0.0f)) { maxZoomFactor = Math.max(maxZoomFactor, minZoomFactor);
if (zoomFactor < minZoomFactor) {
// if one (or both) of the page dimensions is smaller than the viewport, // if one (or both) of the page dimensions is smaller than the viewport,
// zoom using the top/left as the focus on that axis. this prevents the // zoom using the top/left as the focus on that axis. this prevents the
// scenario where, if both dimensions are smaller than the viewport, but // scenario where, if both dimensions are smaller than the viewport, but
...@@ -675,9 +734,9 @@ public class PanZoomController ...@@ -675,9 +734,9 @@ public class PanZoomController
// after applying the scale // after applying the scale
PointF center = new PointF(focusX, focusY); PointF center = new PointF(focusX, focusY);
viewportMetrics.scaleTo(minZoomFactor, center); viewportMetrics.scaleTo(minZoomFactor, center);
} else if (zoomFactor > MAX_ZOOM) { } else if (zoomFactor > maxZoomFactor) {
PointF center = new PointF(viewport.width() / 2.0f, viewport.height() / 2.0f); PointF center = new PointF(viewport.width() / 2.0f, viewport.height() / 2.0f);
viewportMetrics.scaleTo(MAX_ZOOM, center); viewportMetrics.scaleTo(maxZoomFactor, center);
} }
/* Now we pan to the right origin. */ /* Now we pan to the right origin. */
...@@ -717,9 +776,11 @@ public class PanZoomController ...@@ -717,9 +776,11 @@ public class PanZoomController
if (mState == PanZoomState.ANIMATED_ZOOM) if (mState == PanZoomState.ANIMATED_ZOOM)
return false; return false;
if (!mController.getAllowZoom())
return false;
mState = PanZoomState.PINCHING; mState = PanZoomState.PINCHING;
mLastZoomFocus = new PointF(detector.getFocusX(), detector.getFocusY()); mLastZoomFocus = new PointF(detector.getFocusX(), detector.getFocusY());
cancelTouch(); cancelTouch();
return true; return true;
...@@ -729,7 +790,8 @@ public class PanZoomController ...@@ -729,7 +790,8 @@ public class PanZoomController
public boolean onScale(SimpleScaleGestureDetector detector) { public boolean onScale(SimpleScaleGestureDetector detector) {
Log.d(LOGTAG, "onScale in state " + mState); Log.d(LOGTAG, "onScale in state " + mState);
if (mState == PanZoomState.ANIMATED_ZOOM)
if (mState != PanZoomState.PINCHING)
return false; return false;
float prevSpan = detector.getPreviousSpan(); float prevSpan = detector.getPreviousSpan();
...@@ -752,13 +814,31 @@ public class PanZoomController ...@@ -752,13 +814,31 @@ public class PanZoomController
synchronized (mController) { synchronized (mController) {
float newZoomFactor = mController.getZoomFactor() * spanRatio; float newZoomFactor = mController.getZoomFactor() * spanRatio;
if (newZoomFactor >= MAX_ZOOM) { float minZoomFactor = 0.0f;
// apply resistance when zooming past MAX_ZOOM, float maxZoomFactor = MAX_ZOOM;
// such that it asymptotically reaches MAX_ZOOM + 1.0
if (mController.getMinZoom() > 0)
minZoomFactor = mController.getMinZoom();
if (mController.getMaxZoom() > 0)
maxZoomFactor = mController.getMaxZoom();
if (newZoomFactor < minZoomFactor) {
// apply resistance when zooming past minZoomFactor,
// such that it asymptotically reaches minZoomFactor / 2.0
// but never exceeds that // but never exceeds that
float excessZoom = newZoomFactor - MAX_ZOOM; final float rate = 0.5f; // controls how quickly we approach the limit
float excessZoom = minZoomFactor - newZoomFactor;
excessZoom = 1.0f - (float)Math.exp(-excessZoom * rate);
newZoomFactor = minZoomFactor * (1.0f - excessZoom / 2.0f);
}
if (newZoomFactor > maxZoomFactor) {
// apply resistance when zooming past maxZoomFactor,
// such that it asymptotically reaches maxZoomFactor + 1.0
// but never exceeds that
float excessZoom = newZoomFactor - maxZoomFactor;
excessZoom = 1.0f - (float)Math.exp(-excessZoom); excessZoom = 1.0f - (float)Math.exp(-excessZoom);
newZoomFactor = MAX_ZOOM + excessZoom; newZoomFactor = maxZoomFactor + excessZoom;
} }
mController.scrollBy(new PointF(mLastZoomFocus.x - detector.getFocusX(), mController.scrollBy(new PointF(mLastZoomFocus.x - detector.getFocusX(),
...@@ -807,23 +887,37 @@ public class PanZoomController ...@@ -807,23 +887,37 @@ public class PanZoomController
} }
@Override @Override
public boolean onDown(MotionEvent motionEvent) { public boolean onSingleTapUp(MotionEvent motionEvent) {
// When zooming is enabled, wait to see if there's a double-tap.
if (mController.getAllowZoom())
return false; return false;
return true;
} }
@Override @Override
public boolean onSingleTapConfirmed(MotionEvent motionEvent) { public boolean onSingleTapConfirmed(MotionEvent motionEvent) {
// When zooming is disabled, we handle this in onSingleTapUp.
if (!mController.getAllowZoom())
return false;
return true; return true;
} }
@Override @Override
public boolean onDoubleTap(MotionEvent motionEvent) { public boolean onDoubleTap(MotionEvent motionEvent) {
if (!mController.getAllowZoom())
return false;
return true; return true;
} }
public void cancelTouch() { private void cancelTouch() {
} }
/**
* Zoom to a specified rect IN CSS PIXELS.
*
* While we usually use device pixels, @zoomToRect must be specified in CSS
* pixels.
*/
private boolean animatedZoomTo(RectF zoomToRect) { private boolean animatedZoomTo(RectF zoomToRect) {
mState = PanZoomState.ANIMATED_ZOOM; mState = PanZoomState.ANIMATED_ZOOM;
final float startZoom = mController.getZoomFactor(); final float startZoom = mController.getZoomFactor();
...@@ -849,10 +943,11 @@ public class PanZoomController ...@@ -849,10 +943,11 @@ public class PanZoomController
zoomToRect.right = zoomToRect.left + newWidth; zoomToRect.right = zoomToRect.left + newWidth;
} }
float finalZoom = viewport.width() * startZoom / zoomToRect.width(); float finalZoom = viewport.width() / zoomToRect.width();
ViewportMetrics finalMetrics = new ViewportMetrics(mController.getViewportMetrics()); ViewportMetrics finalMetrics = new ViewportMetrics(mController.getViewportMetrics());
finalMetrics.setOrigin(new PointF(zoomToRect.left, zoomToRect.top)); finalMetrics.setOrigin(new PointF(zoomToRect.left * finalMetrics.getZoomFactor(),
zoomToRect.top * finalMetrics.getZoomFactor()));
finalMetrics.scaleTo(finalZoom, new PointF(0.0f, 0.0f)); finalMetrics.scaleTo(finalZoom, new PointF(0.0f, 0.0f));
// 2. now run getValidViewportMetrics on it, so that the target viewport is // 2. now run getValidViewportMetrics on it, so that the target viewport is
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment