diff options
author | Tomaž Vajngerl <tomaz.vajngerl@collabora.com> | 2014-09-24 15:19:58 +0200 |
---|---|---|
committer | Tomaž Vajngerl <tomaz.vajngerl@collabora.com> | 2014-09-24 20:43:04 +0200 |
commit | df433a70cd2fe564a4d046a0bbb1e90292978184 (patch) | |
tree | 3e9e9f50897938a8b030fac7632b95def735ef5c /android | |
parent | 0455b3d4b874db06a205d1133f48bcd323665911 (diff) |
android: upgrade PanZoomController - add configurable zoom limits
Change-Id: I19815f58af4d060cffe515829a2a5472d32bf83c
Diffstat (limited to 'android')
7 files changed, 289 insertions, 171 deletions
diff --git a/android/experimental/LOAndroid3/src/java/org/libreoffice/LibreOfficeMainActivity.java b/android/experimental/LOAndroid3/src/java/org/libreoffice/LibreOfficeMainActivity.java index 35a320d4404d..810ff26046ac 100644 --- a/android/experimental/LOAndroid3/src/java/org/libreoffice/LibreOfficeMainActivity.java +++ b/android/experimental/LOAndroid3/src/java/org/libreoffice/LibreOfficeMainActivity.java @@ -118,6 +118,7 @@ public class LibreOfficeMainActivity extends Activity { } mLayerController = new LayerController(this); + mLayerController.setAllowZoom(true); mLayerClient = new GeckoLayerClient(this); mLayerController.setLayerClient(mLayerClient); mGeckoLayout.addView(mLayerController.getView(), 0); diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/DisplayPortCalculator.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/DisplayPortCalculator.java index a255974b5481..88507e5d1056 100644 --- a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/DisplayPortCalculator.java +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/DisplayPortCalculator.java @@ -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_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) { return sStrategy.calculate(metrics, (velocity == null ? ZERO_VELOCITY : velocity)); diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java index ce047eefe6df..e43a308fae46 100644 --- a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java @@ -172,7 +172,7 @@ public class GeckoLayerClient implements LayerView.Listener { // Don't adjust page size when zooming unless zoom levels are // approximately equal. if (FloatUtils.fuzzyEquals(mLayerController.getZoomFactor(), mGeckoViewport.getZoomFactor())) { - mLayerController.setPageSize(mGeckoViewport.getPageSize()); + mLayerController.setPageSize(mGeckoViewport.getPageSize(), mGeckoViewport.getPageSize()); } } else { mLayerController.setViewportMetrics(mGeckoViewport); @@ -254,7 +254,7 @@ public class GeckoLayerClient implements LayerView.Listener { float ourZoom = mLayerController.getZoomFactor(); pageWidth = pageWidth * 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 // 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 @@ -296,13 +296,13 @@ public class GeckoLayerClient implements LayerView.Listener { } @Override - public void compositionResumeRequested() { + public void compositionResumeRequested(int width, int height) { } @Override public void surfaceChanged(int width, int height) { - compositionResumeRequested(); + compositionResumeRequested(width, height); renderRequested(); } diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/LayerController.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/LayerController.java index 01559a12cd1f..277ed42a6982 100644 --- a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/LayerController.java +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/LayerController.java @@ -1,40 +1,7 @@ /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * 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 ***** */ + * 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/. */ package org.mozilla.gecko.gfx; @@ -42,6 +9,7 @@ import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.graphics.Color; import android.graphics.PointF; import android.graphics.RectF; import android.view.GestureDetector; @@ -49,8 +17,6 @@ import android.view.GestureDetector; import org.mozilla.gecko.ui.PanZoomController; 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 * zooming natively by delegating to a panning/zooming controller. Touch events can be dispatched @@ -87,12 +53,15 @@ public class LayerController { private GeckoLayerClient mLayerClient; /* The layer client. */ /* The new color for the checkerboard. */ - private int mCheckerboardColor; + private int mCheckerboardColor = Color.WHITE; 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) { mContext = context; @@ -124,6 +93,10 @@ public class LayerController { return mViewportMetrics.getViewport(); } + public RectF getCssViewport() { + return mViewportMetrics.getCssViewport(); + } + public FloatSize getViewportSize() { return mViewportMetrics.getSize(); } @@ -132,6 +105,10 @@ public class LayerController { return mViewportMetrics.getPageSize(); } + public FloatSize getCssPageSize() { + return mViewportMetrics.getCssPageSize(); + } + public PointF getOrigin() { return mViewportMetrics.getOrigin(); } @@ -189,12 +166,12 @@ public class LayerController { } /** Sets the current page size. You must hold the monitor while calling this. */ - public void setPageSize(FloatSize size) { - if (mViewportMetrics.getPageSize().fuzzyEquals(size)) + public void setPageSize(FloatSize size, FloatSize cssSize) { + if (mViewportMetrics.getCssPageSize().equals(cssSize)) return; ViewportMetrics viewportMetrics = new ViewportMetrics(mViewportMetrics); - viewportMetrics.setPageSize(size, size); + viewportMetrics.setPageSize(size, cssSize); mViewportMetrics = new ImmutableViewportMetrics(viewportMetrics); // Page size is owned by the layer client, so no need to notify it of @@ -294,8 +271,9 @@ public class LayerController { * correct. */ public PointF convertViewPointToLayerPoint(PointF viewPoint) { - if (mRootLayer == null) + if (mLayerClient == null) { return null; + } ImmutableViewportMetrics viewportMetrics = mViewportMetrics; PointF origin = viewportMetrics.getOrigin(); @@ -337,5 +315,41 @@ public class LayerController { mCheckerboardColor = newColor; 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; + } +} diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/LayerView.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/LayerView.java index f981667486be..9c6a61698926 100644 --- a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/LayerView.java +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/LayerView.java @@ -1,40 +1,7 @@ /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * 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 ***** */ + * 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/. */ package org.mozilla.gecko.gfx; @@ -42,7 +9,6 @@ package org.mozilla.gecko.gfx; import android.content.Context; import android.graphics.Bitmap; import android.graphics.PixelFormat; -import android.opengl.GLSurfaceView; import android.util.Log; import android.view.KeyEvent; import android.view.MotionEvent; @@ -227,7 +193,7 @@ public class LayerView extends SurfaceView implements SurfaceHolder.Callback { } - public GLSurfaceView.Renderer getRenderer() { + public LayerRenderer getRenderer() { return mRenderer; } @@ -235,7 +201,7 @@ public class LayerView extends SurfaceView implements SurfaceHolder.Callback { mListener = listener; } - public synchronized GLController getGLController() { + public GLController getGLController() { return mGLController; } @@ -288,7 +254,7 @@ public class LayerView extends SurfaceView implements SurfaceHolder.Callback { public interface Listener { void renderRequested(); void compositionPauseRequested(); - void compositionResumeRequested(); + void compositionResumeRequested(int width, int height); void surfaceChanged(int width, int height); } diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/TouchEventHandler.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/TouchEventHandler.java index d972ef36eab7..78a141ee0343 100644 --- a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/TouchEventHandler.java +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/TouchEventHandler.java @@ -10,8 +10,8 @@ import android.os.SystemClock; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View.OnTouchListener; -import android.view.ViewConfiguration; +import org.mozilla.gecko.ui.PanZoomController; import org.mozilla.gecko.ui.SimpleScaleGestureDetector; import java.util.LinkedList; @@ -41,18 +41,24 @@ import java.util.Queue; * 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 * 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 { private static final String LOGTAG = "GeckoTouchEventHandler"; // The time limit for listeners to respond with preventDefault on touchevents // 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 LayerController mController; private final GestureDetector mGestureDetector; 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 // notification @@ -119,15 +125,16 @@ public final class TouchEventHandler { TouchEventHandler(Context context, LayerView view, LayerController controller) { mView = view; - mController = controller; mEventQueue = new LinkedList<MotionEvent>(); mGestureDetector = new GestureDetector(context, controller.getGestureListener()); mScaleGestureDetector = new SimpleScaleGestureDetector(controller.getScaleGestureListener()); + mPanZoomController = controller.getPanZoomController(); mListenerTimeoutProcessor = new ListenerTimeoutProcessor(); mDispatchEvents = true; - mGestureDetector.setOnDoubleTapListener(controller.getDoubleTapListener()); + mDoubleTapListener = controller.getDoubleTapListener(); + setDoubleTapEnabled(true); } /* This function MUST be called on the UI thread */ @@ -142,7 +149,18 @@ public final class TouchEventHandler { if (isDownEvent(event)) { // this is the start of a new block of events! whee! 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 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 // we dispatch these events if we don't get a default-prevented notification mView.postDelayed(mListenerTimeoutProcessor, EVENT_LISTENER_TIMEOUT); @@ -164,6 +182,8 @@ public final class TouchEventHandler { mEventQueue.add(MotionEvent.obtain(event)); } else if (mDispatchEvents) { dispatchEvent(event); + } else if (touchFinished(event)) { + mPanZoomController.preventedTouchFinished(); } // notify gecko of the event @@ -193,6 +213,11 @@ public final class TouchEventHandler { } /* 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. */ public void setWaitForTouchListeners(boolean aValue) { mWaitForTouchListeners = aValue; } @@ -207,18 +232,32 @@ public final class TouchEventHandler { 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. */ private void dispatchEvent(MotionEvent event) { if (mGestureDetector.onTouchEvent(event)) { - return; + // 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; + } } mScaleGestureDetector.onTouchEvent(event); if (mScaleGestureDetector.isInProgress()) { return; } - mController.getPanZoomController().onTouchEvent(event); + mPanZoomController.onTouchEvent(event); } /** @@ -244,6 +283,8 @@ public final class TouchEventHandler { // default-prevented. if (allowDefaultAction) { dispatchEvent(event); + } else if (touchFinished(event)) { + mPanZoomController.preventedTouchFinished(); } event = mEventQueue.peek(); if (event == null) { @@ -259,6 +300,7 @@ public final class TouchEventHandler { if (isDownEvent(event)) { // we have finished processing the block we were interested in. // now we wait for the next call to processEventBlock + mPanZoomController.waitingForTouchListeners(event); break; } // pop the event we peeked above, as it is still part of the block and diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/PanZoomController.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/PanZoomController.java index acded9c4e11f..8f81b5d77952 100644 --- a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/PanZoomController.java +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/PanZoomController.java @@ -1,40 +1,7 @@ /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * 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 ***** */ + * 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/. */ package org.mozilla.gecko.ui; @@ -52,6 +19,8 @@ import org.mozilla.gecko.gfx.LayerController; import org.mozilla.gecko.gfx.ViewportMetrics; import org.mozilla.gecko.util.FloatUtils; +import java.util.Arrays; +import java.util.StringTokenizer; import java.util.Timer; import java.util.TimerTask; @@ -85,7 +54,7 @@ public class PanZoomController private static final float MAX_ZOOM = 4.0f; /* 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.10211f, /* 1 */ 0.19864f, /* 2 */ @@ -115,7 +84,11 @@ public class PanZoomController 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 */ 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; @@ -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) { switch (event.getAction() & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: return onTouchStart(event); @@ -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. */ public void pageSizeUpdated() { if (mState == PanZoomState.NOTHING) { @@ -222,10 +236,16 @@ public class PanZoomController switch (mState) { 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 BOUNCE: case NOTHING: + case WAITING_LISTENERS: startTouch(event.getX(0), event.getY(0), event.getEventTime()); return false; case TOUCHING: @@ -244,11 +264,16 @@ public class PanZoomController private boolean onTouchMove(MotionEvent event) { switch (mState) { - case NOTHING: case FLING: case BOUNCE: + case WAITING_LISTENERS: // should never happen 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; case TOUCHING: @@ -274,7 +299,6 @@ public class PanZoomController track(event); return true; - case ANIMATED_ZOOM: case PINCHING: // scale gesture listener will handle this return false; @@ -286,12 +310,18 @@ public class PanZoomController private boolean onTouchEnd(MotionEvent event) { switch (mState) { - case NOTHING: case FLING: case BOUNCE: + case WAITING_LISTENERS: // should never happen 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; + case TOUCHING: mState = PanZoomState.NOTHING; // the switch into TOUCHING might have happened while the page was @@ -299,6 +329,7 @@ public class PanZoomController // was the case bounce(); return false; + case PANNING: case PANNING_LOCKED: case PANNING_HOLD: @@ -306,19 +337,28 @@ public class PanZoomController mState = PanZoomState.FLING; fling(); return true; + case PINCHING: mState = PanZoomState.NOTHING; return true; - case ANIMATED_ZOOM: - return false; } Log.e(LOGTAG, "Unhandled case " + mState + " in onTouchEnd"); return false; } 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(); + mState = PanZoomState.NOTHING; // ensure we snap back if we're overscrolled bounce(); return false; @@ -423,16 +463,17 @@ public class PanZoomController return; } - mState = PanZoomState.BOUNCE; - // set the animation target *after* setting state BOUNCE, so that - // the getRedrawHint() is returning false and we don't clobber the display - // port we set as a result of this animation target call. + // At this point we have already set mState to BOUNCE or ANIMATED_ZOOM, so + // getRedrawHint() is returning false. This means we can safely call + // setAnimationTarget to set the new final display port and not have it get + // clobbered by display ports from intermediate animation frames. mController.setAnimationTarget(metrics); startAnimationTimer(new BounceRunnable(bounceStartMetrics, metrics)); } /* Performs a bounce-back animation to the nearest valid viewport metrics. */ private void bounce() { + mState = PanZoomState.BOUNCE; bounce(getValidViewportMetrics()); } @@ -477,14 +518,14 @@ public class PanZoomController return getVelocity() < STOPPED_THRESHOLD; } - PointF getDisplacement() { + PointF resetDisplacement() { return new PointF(mX.resetDisplacement(), mY.resetDisplacement()); } private void updatePosition() { mX.displace(); mY.displace(); - PointF displacement = getDisplacement(); + PointF displacement = resetDisplacement(); if (FloatUtils.fuzzyEquals(displacement.x, 0.0f) && FloatUtils.fuzzyEquals(displacement.y, 0.0f)) { return; } @@ -542,13 +583,13 @@ public class PanZoomController * animation by setting the state to PanZoomState.NOTHING. Handle this case and bail * out. */ - if (mState != PanZoomState.BOUNCE) { + if (!(mState == PanZoomState.BOUNCE || mState == PanZoomState.ANIMATED_ZOOM)) { finishAnimation(); return; } /* Perform the next frame of the bounce-back animation. */ - if (mBounceFrame < EASE_OUT_ANIMATION_FRAMES.length) { + if (mBounceFrame < ZOOM_ANIMATION_FRAMES.length) { advanceBounce(); return; } @@ -562,7 +603,7 @@ public class PanZoomController /* Performs one frame of a bounce animation. */ private void advanceBounce() { synchronized (mController) { - float t = EASE_OUT_ANIMATION_FRAMES[mBounceFrame]; + float t = ZOOM_ANIMATION_FRAMES[mBounceFrame]; ViewportMetrics newMetrics = mBounceStartMetrics.interpolate(mBounceEndMetrics, t); mController.setViewportMetrics(newMetrics); mController.notifyLayerClientOfGeometryChange(); @@ -655,19 +696,37 @@ public class PanZoomController float focusX = viewport.width() / 2.0f; float focusY = viewport.height() / 2.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; minZoomFactor = Math.max(minZoomFactor, zoomFactor * scaleFactor); - focusX = 0.0f; + if (viewport.width() > pageSize.width) + focusX = 0.0f; } - if (viewport.height() > pageSize.height && pageSize.height > 0) { + if (pageSize.height > 0) { float scaleFactor = viewport.height() / pageSize.height; minZoomFactor = Math.max(minZoomFactor, zoomFactor * scaleFactor); - focusY = 0.0f; + if (viewport.height() > pageSize.height) + 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, // 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 @@ -675,9 +734,9 @@ public class PanZoomController // after applying the scale PointF center = new PointF(focusX, focusY); 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); - viewportMetrics.scaleTo(MAX_ZOOM, center); + viewportMetrics.scaleTo(maxZoomFactor, center); } /* Now we pan to the right origin. */ @@ -717,9 +776,11 @@ public class PanZoomController if (mState == PanZoomState.ANIMATED_ZOOM) return false; + if (!mController.getAllowZoom()) + return false; + mState = PanZoomState.PINCHING; mLastZoomFocus = new PointF(detector.getFocusX(), detector.getFocusY()); - cancelTouch(); return true; @@ -729,7 +790,8 @@ public class PanZoomController public boolean onScale(SimpleScaleGestureDetector detector) { Log.d(LOGTAG, "onScale in state " + mState); - if (mState == PanZoomState.ANIMATED_ZOOM) + + if (mState != PanZoomState.PINCHING) return false; float prevSpan = detector.getPreviousSpan(); @@ -752,13 +814,31 @@ public class PanZoomController synchronized (mController) { float newZoomFactor = mController.getZoomFactor() * spanRatio; - if (newZoomFactor >= MAX_ZOOM) { - // apply resistance when zooming past MAX_ZOOM, - // such that it asymptotically reaches MAX_ZOOM + 1.0 + float minZoomFactor = 0.0f; + float maxZoomFactor = MAX_ZOOM; + + 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 - 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); - newZoomFactor = MAX_ZOOM + excessZoom; + newZoomFactor = maxZoomFactor + excessZoom; } mController.scrollBy(new PointF(mLastZoomFocus.x - detector.getFocusX(), @@ -807,23 +887,37 @@ public class PanZoomController } @Override - public boolean onDown(MotionEvent motionEvent) { - return false; + public boolean onSingleTapUp(MotionEvent motionEvent) { + // When zooming is enabled, wait to see if there's a double-tap. + if (mController.getAllowZoom()) + return false; + return true; } @Override public boolean onSingleTapConfirmed(MotionEvent motionEvent) { + // When zooming is disabled, we handle this in onSingleTapUp. + if (!mController.getAllowZoom()) + return false; return true; } @Override public boolean onDoubleTap(MotionEvent motionEvent) { + if (!mController.getAllowZoom()) + return false; 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) { mState = PanZoomState.ANIMATED_ZOOM; final float startZoom = mController.getZoomFactor(); @@ -849,10 +943,11 @@ public class PanZoomController zoomToRect.right = zoomToRect.left + newWidth; } - float finalZoom = viewport.width() * startZoom / zoomToRect.width(); + float finalZoom = viewport.width() / zoomToRect.width(); 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)); // 2. now run getValidViewportMetrics on it, so that the target viewport is |