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

android: extract touch handling into TouchEventHandler

Change-Id: I138f746940bf89349d4662c95427113bff221231
üst b0a1d430
......@@ -5,97 +5,472 @@
package org.mozilla.gecko.gfx;
import android.graphics.PointF;
import android.graphics.RectF;
import android.util.Log;
import org.libreoffice.LOKitShell;
import org.mozilla.gecko.util.FloatUtils;
final class DisplayPortCalculator {
private static final String LOGTAG = "GeckoDisplayPortCalculator";
private static final PointF ZERO_VELOCITY = new PointF(0, 0);
private static final int DEFAULT_DISPLAY_PORT_MARGIN = 300;
private static DisplayPortStrategy sStrategy = new FixedMarginStrategy();
/* If the visible rect is within the danger zone (measured in pixels from each edge of a tile),
* we start aggressively redrawing to minimize checkerboarding. */
private static final int DANGER_ZONE_X = 75;
private static final int DANGER_ZONE_Y = 150;
static DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
return sStrategy.calculate(metrics, (velocity == null ? ZERO_VELOCITY : velocity));
}
static DisplayPortMetrics calculate(ImmutableViewportMetrics metrics) {
float desiredXMargins = 2 * DEFAULT_DISPLAY_PORT_MARGIN;
float desiredYMargins = 2 * DEFAULT_DISPLAY_PORT_MARGIN;
static boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
if (displayPort == null) {
return true;
}
return sStrategy.aboutToCheckerboard(metrics, (velocity == null ? ZERO_VELOCITY : velocity), displayPort);
}
// we need to avoid having a display port that is larger than the page, or we will end up
// painting things outside the page bounds (bug 729169). we simultaneously need to make
// the display port as large as possible so that we redraw less.
/**
* Set the active strategy to use.
* See the gfx.displayport.strategy pref in mobile/android/app/mobile.js to see the
* mapping between ints and strategies.
*/
static void setStrategy(int strategy) {
switch (strategy) {
case 0:
default:
sStrategy = new FixedMarginStrategy();
break;
case 1:
sStrategy = new VelocityBiasStrategy();
break;
case 2:
sStrategy = new DynamicResolutionStrategy();
break;
case 3:
sStrategy = new NoMarginStrategy();
break;
}
Log.i(LOGTAG, "Set strategy " + sStrategy.getClass().getName());
}
private interface DisplayPortStrategy {
/** Calculates a displayport given a viewport and panning velocity. */
public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity);
/** Returns true if a checkerboard is about to be visible and we should not throttle drawing. */
public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort);
}
/**
* Return the dimensions for a rect that has area (width*height) that does not exceed the page size in the
* given metrics object. The area in the returned FloatSize may be less than width*height if the page is
* small, but it will never be larger than width*height.
* Note that this process may change the relative aspect ratio of the given dimensions.
*/
private static FloatSize reshapeForPage(float width, float height, ImmutableViewportMetrics metrics) {
// figure out how much of the desired buffer amount we can actually use on the horizontal axis
float xBufferAmount = Math.min(desiredXMargins, metrics.pageSizeWidth - metrics.getWidth());
float usableWidth = Math.min(width, metrics.pageSizeWidth);
// if we reduced the buffer amount on the horizontal axis, we should take that saved memory and
// use it on the vertical axis
float savedPixels = (desiredXMargins - xBufferAmount) * (metrics.getHeight() + desiredYMargins);
float extraYAmount = (float)Math.floor(savedPixels / (metrics.getWidth() + xBufferAmount));
float yBufferAmount = Math.min(desiredYMargins + extraYAmount, metrics.pageSizeHeight - metrics.getHeight());
float extraUsableHeight = (float)Math.floor(((width - usableWidth) * height) / usableWidth);
float usableHeight = Math.min(height + extraUsableHeight, metrics.pageSizeHeight);
if (usableHeight < height && usableWidth == width) {
// and the reverse - if we shrunk the buffer on the vertical axis we can add it to the horizontal
if (xBufferAmount == desiredXMargins && yBufferAmount < desiredYMargins) {
savedPixels = (desiredYMargins - yBufferAmount) * (metrics.getWidth() + xBufferAmount);
float extraXAmount = (float)Math.floor(savedPixels / (metrics.getHeight() + yBufferAmount));
xBufferAmount = Math.min(xBufferAmount + extraXAmount, metrics.pageSizeWidth - metrics.getWidth());
float extraUsableWidth = (float)Math.floor(((height - usableHeight) * width) / usableHeight);
usableWidth = Math.min(width + extraUsableWidth, metrics.pageSizeWidth);
}
return new FloatSize(usableWidth, usableHeight);
}
/**
* Expand the given rect in all directions by a "danger zone". The size of the danger zone on an axis
* is the size of the view on that axis multiplied by the given multiplier. The expanded rect is then
* clamped to page bounds and returned.
*/
private static RectF expandByDangerZone(RectF rect, float dangerZoneXMultiplier, float dangerZoneYMultiplier, ImmutableViewportMetrics metrics) {
// calculate the danger zone amounts in pixels
float dangerZoneX = metrics.getWidth() * dangerZoneXMultiplier;
float dangerZoneY = metrics.getHeight() * dangerZoneYMultiplier;
rect = RectUtils.expand(rect, dangerZoneX, dangerZoneY);
// clamp to page bounds
if (rect.top < 0) rect.top = 0;
if (rect.left < 0) rect.left = 0;
if (rect.right > metrics.pageSizeWidth) rect.right = metrics.pageSizeWidth;
if (rect.bottom > metrics.pageSizeHeight) rect.bottom = metrics.pageSizeHeight;
return rect;
}
/**
* Adjust the given margins so if they are applied on the viewport in the metrics, the resulting rect
* does not exceed the page bounds. This code will maintain the total margin amount for a given axis;
* it assumes that margins.left + metrics.getWidth() + margins.right is less than or equal to
* metrics.pageSizeWidth; and the same for the y axis.
*/
private static RectF shiftMarginsForPageBounds(RectF margins, ImmutableViewportMetrics metrics) {
// check how much we're overflowing in each direction. note that at most one of leftOverflow
// and rightOverflow can be greater than zero, and at most one of topOverflow and bottomOverflow
// can be greater than zero, because of the assumption described in the method javadoc.
float leftOverflow = margins.left - metrics.viewportRectLeft;
float rightOverflow = margins.right - (metrics.pageSizeWidth - metrics.viewportRectRight);
float topOverflow = margins.top - metrics.viewportRectTop;
float bottomOverflow = margins.bottom - (metrics.pageSizeHeight - metrics.viewportRectBottom);
// if the margins overflow the page bounds, shift them to other side on the same axis
if (leftOverflow > 0) {
margins.left -= leftOverflow;
margins.right += leftOverflow;
} else if (rightOverflow > 0) {
margins.right -= rightOverflow;
margins.left += rightOverflow;
}
if (topOverflow > 0) {
margins.top -= topOverflow;
margins.bottom += topOverflow;
} else if (bottomOverflow > 0) {
margins.bottom -= bottomOverflow;
margins.top += bottomOverflow;
}
return margins;
}
/**
* This class implements the variation where we basically don't bother with a a display port.
*/
private static class NoMarginStrategy implements DisplayPortStrategy {
public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
return new DisplayPortMetrics(metrics.viewportRectLeft,
metrics.viewportRectTop,
metrics.viewportRectRight,
metrics.viewportRectBottom,
metrics.zoomFactor);
}
public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
return true;
}
}
/**
* This class implements the variation where we use a fixed-size margin on the display port.
* The margin is always 300 pixels in all directions, except when we are (a) approaching a page
* boundary, and/or (b) if we are limited by the page size. In these cases we try to maintain
* the area of the display port by (a) shifting the buffer to the other side on the same axis,
* and/or (b) increasing the buffer on the other axis to compensate for the reduced buffer on
* one axis.
*/
private static class FixedMarginStrategy implements DisplayPortStrategy {
// The length of each axis of the display port will be the corresponding view length
// multiplied by this factor.
private static final float SIZE_MULTIPLIER = 1.5f;
// If the visible rect is within the danger zone (measured as a fraction of the view size
// from the edge of the displayport) we start redrawing to minimize checkerboarding.
private static final float DANGER_ZONE_X_MULTIPLIER = 0.10f;
private static final float DANGER_ZONE_Y_MULTIPLIER = 0.20f;
public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER;
float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER;
// we need to avoid having a display port that is larger than the page, or we will end up
// painting things outside the page bounds (bug 729169). we simultaneously need to make
// the display port as large as possible so that we redraw less. reshape the display
// port dimensions to accomplish this.
FloatSize usableSize = reshapeForPage(displayPortWidth, displayPortHeight, metrics);
float horizontalBuffer = usableSize.width - metrics.getWidth();
float verticalBuffer = usableSize.height - metrics.getHeight();
// and now calculate the display port margins based on how much buffer we've decided to use and
// the page bounds, ensuring we use all of the available buffer amounts on one side or the other
// on any given axis. (i.e. if we're scrolled to the top of the page, the vertical buffer is
// entirely below the visible viewport, but if we're halfway down the page, the vertical buffer
// is split).
float leftMargin = Math.min(DEFAULT_DISPLAY_PORT_MARGIN, metrics.viewportRectLeft);
float rightMargin = Math.min(DEFAULT_DISPLAY_PORT_MARGIN, metrics.pageSizeWidth - (metrics.viewportRectLeft + metrics.getWidth()));
if (leftMargin < DEFAULT_DISPLAY_PORT_MARGIN) {
rightMargin = xBufferAmount - leftMargin;
} else if (rightMargin < DEFAULT_DISPLAY_PORT_MARGIN) {
leftMargin = xBufferAmount - rightMargin;
} else if (!FloatUtils.fuzzyEquals(leftMargin + rightMargin, xBufferAmount)) {
float delta = xBufferAmount - leftMargin - rightMargin;
leftMargin += delta / 2;
rightMargin += delta / 2;
}
float topMargin = Math.min(DEFAULT_DISPLAY_PORT_MARGIN, metrics.viewportRectTop);
float bottomMargin = Math.min(DEFAULT_DISPLAY_PORT_MARGIN, metrics.pageSizeHeight - (metrics.viewportRectTop + metrics.getHeight()));
if (topMargin < DEFAULT_DISPLAY_PORT_MARGIN) {
bottomMargin = yBufferAmount - topMargin;
} else if (bottomMargin < DEFAULT_DISPLAY_PORT_MARGIN) {
topMargin = yBufferAmount - bottomMargin;
} else if (!FloatUtils.fuzzyEquals(topMargin + bottomMargin, yBufferAmount)) {
float delta = yBufferAmount - topMargin - bottomMargin;
topMargin += delta / 2;
bottomMargin += delta / 2;
}
RectF margins = new RectF();
margins.left = horizontalBuffer / 2.0f;
margins.right = horizontalBuffer - margins.left;
margins.top = verticalBuffer / 2.0f;
margins.bottom = verticalBuffer - margins.top;
margins = shiftMarginsForPageBounds(margins, metrics);
// note that unless the viewport size changes, or the page dimensions change (either because of
// content changes or zooming), the size of the display port should remain constant. this
// is intentional to avoid re-creating textures and all sorts of other reallocations in the
// draw and composition code.
return new DisplayPortMetrics(metrics.viewportRectLeft - leftMargin,
metrics.viewportRectTop - topMargin,
metrics.viewportRectRight + rightMargin,
metrics.viewportRectBottom + bottomMargin,
return new DisplayPortMetrics(metrics.viewportRectLeft - margins.left,
metrics.viewportRectTop - margins.top,
metrics.viewportRectRight + margins.right,
metrics.viewportRectBottom + margins.bottom,
metrics.zoomFactor);
}
// Returns true if a checkerboard is about to be visible.
static boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, DisplayPortMetrics displayPort) {
if (displayPort == null) {
public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
// Increase the size of the viewport based on the danger zone multiplier (and clamp to page
// boundaries), and intersect it with the current displayport to determine whether we're
// close to checkerboarding.
RectF adjustedViewport = expandByDangerZone(metrics.getViewport(), DANGER_ZONE_X_MULTIPLIER, DANGER_ZONE_Y_MULTIPLIER, metrics);
return !displayPort.contains(adjustedViewport);
}
}
/**
* This class implements the variation with a small fixed-size margin with velocity bias.
* In this variation, the default margins are pretty small relative to the view size, but
* they are affected by the panning velocity. Specifically, if we are panning on one axis,
* we remove the margins on the other axis because we are likely axis-locked. Also once
* we are panning in one direction above a certain threshold velocity, we shift the buffer
* so that it is entirely in the direction of the pan.
*/
private static class VelocityBiasStrategy implements DisplayPortStrategy {
// The length of each axis of the display port will be the corresponding view length
// multiplied by this factor.
private static final float SIZE_MULTIPLIER = 1.2f;
// The velocity above which we apply the velocity bias
private static final float VELOCITY_THRESHOLD = LOKitShell.getDpi() / 32f;
public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER;
float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER;
// but if we're panning on one axis, set the margins for the other axis to zero since we are likely
// axis locked and won't be displaying that extra area.
if (Math.abs(velocity.x) > VELOCITY_THRESHOLD && FloatUtils.fuzzyEquals(velocity.y, 0)) {
displayPortHeight = metrics.getHeight();
} else if (Math.abs(velocity.y) > VELOCITY_THRESHOLD && FloatUtils.fuzzyEquals(velocity.x, 0)) {
displayPortWidth = metrics.getWidth();
}
// we need to avoid having a display port that is larger than the page, or we will end up
// painting things outside the page bounds (bug 729169).
displayPortWidth = Math.min(displayPortWidth, metrics.pageSizeWidth);
displayPortHeight = Math.min(displayPortHeight, metrics.pageSizeHeight);
float horizontalBuffer = displayPortWidth - metrics.getWidth();
float verticalBuffer = displayPortHeight - metrics.getHeight();
// if we're panning above the VELOCITY_THRESHOLD on an axis, apply the margin so that it
// is entirely in the direction of panning. Otherwise, split the margin evenly on both sides of
// the display port.
RectF margins = new RectF();
if (velocity.x > VELOCITY_THRESHOLD) {
margins.right = horizontalBuffer;
} else if (velocity.x < -VELOCITY_THRESHOLD) {
margins.left = horizontalBuffer;
} else {
margins.left = horizontalBuffer / 2.0f;
margins.right = horizontalBuffer - margins.left;
}
if (velocity.y > VELOCITY_THRESHOLD) {
margins.bottom = verticalBuffer;
} else if (velocity.y < -VELOCITY_THRESHOLD) {
margins.top = verticalBuffer;
} else {
margins.top = verticalBuffer / 2.0f;
margins.bottom = verticalBuffer - margins.top;
}
// and finally shift the margins to account for page bounds
margins = shiftMarginsForPageBounds(margins, metrics);
return new DisplayPortMetrics(metrics.viewportRectLeft - margins.left,
metrics.viewportRectTop - margins.top,
metrics.viewportRectRight + margins.right,
metrics.viewportRectBottom + margins.bottom,
metrics.zoomFactor);
}
public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
// Since we have such a small margin, we want to be drawing more aggressively. At the start of a
// pan the velocity is going to be large so we're almost certainly going to go into checkerboard
// on every frame, so drawing all the time seems like the right thing. At the end of the pan we
// want to re-center the displayport and draw stuff on all sides, so again we don't want to throttle
// there. When we're not panning we're not drawing anyway so it doesn't make a difference there.
return true;
}
}
/**
* This class implements the variation where we draw more of the page at low resolution while panning.
* In this variation, as we pan faster, we increase the page area we are drawing, but reduce the draw
* resolution to compensate. This results in the same device-pixel area drawn; the compositor then
* scales this up to the viewport zoom level. This results in a large area of the page drawn but it
* looks blurry. The assumption is that drawing extra that we never display is better than checkerboarding,
* where we draw less but never even show it on the screen.
*/
private static class DynamicResolutionStrategy implements DisplayPortStrategy {
// The length of each axis of the display port will be the corresponding view length
// multiplied by this factor.
private static final float SIZE_MULTIPLIER = 1.5f;
// The velocity above which we start zooming out the display port to keep up
// with the panning.
private static final float VELOCITY_EXPANSION_THRESHOLD = LOKitShell.getDpi() / 16f;
// How much we increase the display port based on velocity. Assuming no friction and
// splitting (see below), this should be be the number of frames (@60fps) between us
// calculating the display port and the draw of the *next* display port getting composited
// and displayed on the screen. This is because the timeline looks like this:
// Java: pan pan pan pan pan pan ! pan pan pan pan pan pan !
// Gecko: \-> draw -> composite / \-> draw -> composite /
// The display port calculated on the first "pan" gets composited to the screen at the
// first exclamation mark, and remains on the screen until the second exclamation mark.
// In order to avoid checkerboarding, that display port must be able to contain all of
// the panning until the second exclamation mark, which encompasses two entire draw/composite
// cycles.
// If we take into account friction, our velocity multiplier should be reduced as the
// amount of pan will decrease each time. If we take into account display port splitting,
// it should be increased as the splitting means some of the display port will be used to
// draw in the opposite direction of the velocity. For now I'm assuming these two cancel
// each other out.
private static final float VELOCITY_MULTIPLIER = 60.0f;
// The following constants adjust how biased the display port is in the direction of panning.
// When panning fast (above the FAST_THRESHOLD) we use the fast split factor to split the
// display port "buffer" area, otherwise we use the slow split factor. This is based on the
// assumption that if the user is panning fast, they are less likely to reverse directions
// and go backwards, so we should spend more of our display port buffer in the direction of
// panning.
private static final float VELOCITY_FAST_THRESHOLD = VELOCITY_EXPANSION_THRESHOLD * 2.0f;
private static final float FAST_SPLIT_FACTOR = 0.95f;
private static final float SLOW_SPLIT_FACTOR = 0.8f;
// The following constants are used for viewport prediction; we use them to estimate where
// the viewport will be soon and whether or not we should trigger a draw right now. "soon"
// in the previous sentence really refers to the amount of time it would take to draw and
// composite from the point at which we do the calculation, and that is not really a known
// quantity. The velocity multiplier is how much we multiply the velocity by; it has the
// same caveats as the VELOCITY_MULTIPLIER above except that it only needs to take into account
// one draw/composite cycle instead of two. The danger zone multiplier is a multiplier of the
// viewport size that we use as an extra "danger zone" around the viewport; if this danger
// zone falls outside the display port then we are approaching the point at which we will
// checkerboard, and hence should start drawing. Note that if DANGER_ZONE_MULTIPLIER is
// greater than (SIZE_MULTIPLIER - 1.0f), then at zero velocity we will always be in the
// danger zone, and thus will be constantly drawing.
private static final float PREDICTION_VELOCITY_MULTIPLIER = 30.0f;
private static final float DANGER_ZONE_MULTIPLIER = 0.20f; // must be less than (SIZE_MULTIPLIER - 1.0f)
public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER;
float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER;
// for resolution calculation purposes, we need to know what the adjusted display port dimensions
// would be if we had zero velocity, so calculate that here before we increase the display port
// based on velocity.
FloatSize reshapedSize = reshapeForPage(displayPortWidth, displayPortHeight, metrics);
// increase displayPortWidth and displayPortHeight based on the velocity, but maintaining their
// relative aspect ratio.
if (velocity.length() > VELOCITY_EXPANSION_THRESHOLD) {
float velocityFactor = Math.max(Math.abs(velocity.x) / displayPortWidth,
Math.abs(velocity.y) / displayPortHeight);
velocityFactor *= VELOCITY_MULTIPLIER;
displayPortWidth += (displayPortWidth * velocityFactor);
displayPortHeight += (displayPortHeight * velocityFactor);
}
// Increase the size of the viewport (and clamp to page boundaries), and
// intersect it with the tile's displayport to determine whether we're
// at this point, displayPortWidth and displayPortHeight are how much of the page (in device pixels)
// we want to be rendered by Gecko. Note here "device pixels" is equivalent to CSS pixels multiplied
// by metrics.zoomFactor
// we need to avoid having a display port that is larger than the page, or we will end up
// painting things outside the page bounds (bug 729169). we simultaneously need to make
// the display port as large as possible so that we redraw less. reshape the display
// port dimensions to accomplish this. this may change the aspect ratio of the display port,
// but we are assuming that this is desirable because the advantages from pre-drawing will
// outweigh the disadvantages from any buffer reallocations that might occur.
FloatSize usableSize = reshapeForPage(displayPortWidth, displayPortHeight, metrics);
float horizontalBuffer = usableSize.width - metrics.getWidth();
float verticalBuffer = usableSize.height - metrics.getHeight();
// at this point, horizontalBuffer and verticalBuffer are the dimensions of the buffer area we have.
// the buffer area is the off-screen area that is part of the display port and will be pre-drawn in case
// the user scrolls there. we now need to split the buffer area on each axis so that we know
// what the exact margins on each side will be. first we split the buffer amount based on the direction
// we're moving, so that we have a larger buffer in the direction of travel.
RectF margins = new RectF();
margins.left = splitBufferByVelocity(horizontalBuffer, velocity.x);
margins.right = horizontalBuffer - margins.left;
margins.top = splitBufferByVelocity(verticalBuffer, velocity.y);
margins.bottom = verticalBuffer - margins.top;
// then, we account for running into the page bounds - so that if we hit the top of the page, we need
// to drop the top margin and move that amount to the bottom margin.
margins = shiftMarginsForPageBounds(margins, metrics);
// finally, we calculate the resolution we want to render the display port area at. We do this
// so that as we expand the display port area (because of velocity), we reduce the resolution of
// the painted area so as to maintain the size of the buffer Gecko is painting into. we calculate
// the reduction in resolution by comparing the display port size with and without the velocity
// changes applied.
// this effectively means that as we pan faster and faster, the display port grows, but we paint
// at lower resolutions. this paints more area to reduce checkerboard at the cost of increasing
// compositor-scaling and blurriness. Once we stop panning, the blurriness must be entirely gone.
// Note that usable* could be less than base* if we are pinch-zoomed out into overscroll, so we
// clamp it to make sure this doesn't increase our display resolution past metrics.zoomFactor.
float scaleFactor = Math.min(reshapedSize.width / usableSize.width, reshapedSize.height / usableSize.height);
float displayResolution = metrics.zoomFactor * Math.min(1.0f, scaleFactor);
DisplayPortMetrics dpMetrics = new DisplayPortMetrics(
metrics.viewportRectLeft - margins.left,
metrics.viewportRectTop - margins.top,
metrics.viewportRectRight + margins.right,
metrics.viewportRectBottom + margins.bottom,
displayResolution);
return dpMetrics;
}
/**
* Split the given buffer amount into two based on the velocity.
* Given an amount of total usable buffer on an axis, this will
* return the amount that should be used on the left/top side of
* the axis (the side which a negative velocity vector corresponds
* to).
*/
private float splitBufferByVelocity(float amount, float velocity) {
// if no velocity, so split evenly
if (FloatUtils.fuzzyEquals(velocity, 0)) {
return amount / 2.0f;
}
// if we're moving quickly, assign more of the amount in that direction
// since is is less likely that we will reverse direction immediately
if (velocity < -VELOCITY_FAST_THRESHOLD) {
return amount * FAST_SPLIT_FACTOR;
}
if (velocity > VELOCITY_FAST_THRESHOLD) {
return amount * (1.0f - FAST_SPLIT_FACTOR);
}
// if we're moving slowly, then assign less of the amount in that direction
if (velocity < 0) {
return amount * SLOW_SPLIT_FACTOR;
} else {
return amount * (1.0f - SLOW_SPLIT_FACTOR);
}
}
public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
// Expand the viewport based on our velocity (and clamp it to page boundaries).
// Then intersect it with the last-requested displayport to determine whether we're
// close to checkerboarding.
FloatSize pageSize = metrics.getPageSize();
RectF adjustedViewport = RectUtils.expand(metrics.getViewport(), DANGER_ZONE_X, DANGER_ZONE_Y);
if (adjustedViewport.top < 0) adjustedViewport.top = 0;
if (adjustedViewport.left < 0) adjustedViewport.left = 0;
if (adjustedViewport.right > pageSize.width) adjustedViewport.right = pageSize.width;
if (adjustedViewport.bottom > pageSize.height) adjustedViewport.bottom = pageSize.height;
return !displayPort.contains(adjustedViewport);
RectF predictedViewport = metrics.getViewport();
// first we expand the viewport in the direction we're moving based on some
// multiple of the current velocity.
if (velocity.length() > 0) {
if (velocity.x < 0) {
predictedViewport.left += velocity.x * PREDICTION_VELOCITY_MULTIPLIER;
} else if (velocity.x > 0) {
predictedViewport.right += velocity.x * PREDICTION_VELOCITY_MULTIPLIER;
}
if (velocity.y < 0) {
predictedViewport.top += velocity.y * PREDICTION_VELOCITY_MULTIPLIER;
} else if (velocity.y > 0) {
predictedViewport.bottom += velocity.y * PREDICTION_VELOCITY_MULTIPLIER;
}
}
// then we expand the viewport evenly in all directions just to have an extra
// safety zone. this also clamps it to page bounds.
predictedViewport = expandByDangerZone(predictedViewport, DANGER_ZONE_MULTIPLIER, DANGER_ZONE_MULTIPLIER, metrics);
return !displayPort.contains(predictedViewport);
}
}
}
......@@ -212,7 +212,7 @@ public class GeckoLayerClient {
mLayerController.getView().postDelayed(new AdjustRunnable(), MIN_VIEWPORT_CHANGE_DELAY - timeDelta);
mPendingViewportAdjust = true;
} else {
adjustViewport();
adjustViewport(null);
}
}
......@@ -220,15 +220,21 @@ public class GeckoLayerClient {
mViewportSizeChanged = true;
}
private void adjustViewport() {
ViewportMetrics viewportMetrics =
new ViewportMetrics(mLayerController.getViewportMetrics());
void adjustViewport(DisplayPortMetrics displayPort) {
ImmutableViewportMetrics metrics = mLayerController.getViewportMetrics();
viewportMetrics.setViewport(viewportMetrics.getClampedViewport());
ViewportMetrics clampedMetrics = new ViewportMetrics(metrics);
clampedMetrics.setViewport(clampedMetrics.getClampedViewport());
mDisplayPort = DisplayPortCalculator.calculate(mLayerController.getViewportMetrics());
if (displayPort == null) {
displayPort = DisplayPortCalculator.calculate(metrics,
mLayerController.getPanZoomController().getVelocityVector());
}
mDisplayPort = displayPort;
mGeckoViewport = clampedMetrics;
LOKitShell.sendEvent(LOEvent.viewport(viewportMetrics));
LOKitShell.sendEvent(LOEvent.viewport(clampedMetrics));
if (mViewportSizeChanged) {
mViewportSizeChanged = false;
LOKitShell.viewSizeChanged();
......@@ -240,7 +246,7 @@ public class GeckoLayerClient {
public void geometryChanged() {
sendResizeEventIfNecessary(false);
if (mLayerController.getRedrawHint())
adjustViewport();
adjustViewport(null);
}
public ViewportMetrics getGeckoViewportMetrics() {
......@@ -267,7 +273,7 @@ public class GeckoLayerClient {
private class AdjustRunnable implements Runnable {
public void run() {
mPendingViewportAdjust = false;
adjustViewport();
adjustViewport(null);
}
}
}
\ No newline at end of file
......@@ -43,18 +43,12 @@ import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View.OnTouchListener;
import org.mozilla.gecko.ui.PanZoomController;
import org.mozilla.gecko.ui.SimpleScaleGestureDetector;
import java.util.Timer;
import java.util.TimerTask;
import java.util.regex.Pattern;
/**
......@@ -84,42 +78,20 @@ public class LayerController {
* fields. */
private volatile ImmutableViewportMetrics mViewportMetrics; /* The current viewport metrics. */
private boolean mWaitForTouchListeners;
private PanZoomController mPanZoomController;
/*
* The panning and zooming controller, which interprets pan and zoom gestures for us and
* updates our visible rect appropriately.
*/
private PanZoomController mPanZoomController;
private OnTouchListener mOnTouchListener; /* The touch listener. */
private GeckoLayerClient mLayerClient; /* The layer client. */
/* The new color for the checkerboard. */
private int mCheckerboardColor;
private boolean mCheckerboardShouldShowChecks = true;
private boolean mCheckerboardShouldShowChecks;
private boolean mForceRedraw;
/* The extra area on the sides of the page that we want to buffer to help with
* smooth, asynchronous scrolling. Depending on a device's support for NPOT
* textures, this may be rounded up to the nearest power of two.
*/
public static final IntSize MIN_BUFFER = new IntSize(512, 1024);
/* If the visible rect is within the danger zone (measured in pixels from each edge of a tile),
* we start aggressively redrawing to minimize checkerboarding. */
private static final int DANGER_ZONE_X = 75;
private static final int DANGER_ZONE_Y = 150;
/* The time limit for pages to respond with preventDefault on touchevents
* before we begin panning the page */
private int mTimeout = 200;
private boolean allowDefaultActions = true;
private Timer allowDefaultTimer = null;
private PointF initialTouchLocation = null;
private static Pattern sColorPattern;
public LayerController(Context context) {
......@@ -129,9 +101,7 @@ public class LayerController {
mViewportMetrics = new ImmutableViewportMetrics(new ViewportMetrics());
mPanZoomController = new PanZoomController(this);
mView = new LayerView(context, this);
}
public void onDestroy() {
mCheckerboardShouldShowChecks = true;
}
public void setRoot(Layer layer) { mRootLayer = layer; }
......@@ -197,50 +167,13 @@ public class LayerController {
* result in an infinite loop.
*/
public void setViewportSize(FloatSize size) {
// Resize the viewport, and modify its zoom factor so that the page retains proportionally
// zoomed relative to the screen.
ViewportMetrics viewportMetrics = new ViewportMetrics(mViewportMetrics);
float oldHeight = viewportMetrics.getSize().height;
float oldWidth = viewportMetrics.getSize().width;
float oldZoomFactor = viewportMetrics.getZoomFactor();
viewportMetrics.setSize(size);
// if the viewport got larger (presumably because the vkb went away), and the page
// is smaller than the new viewport size, increase the page size so that the panzoomcontroller
// doesn't zoom in to make it fit (bug 718270). this page size change is in anticipation of
// gecko increasing the page size to match the new viewport size, which will happen the next
// time we get a draw update.
if (size.width >= oldWidth && size.height >= oldHeight) {
FloatSize pageSize = viewportMetrics.getPageSize();
if (pageSize.width < size.width || pageSize.height < size.height) {
FloatSize actualPageSize = new FloatSize(Math.max(pageSize.width, size.width),
Math.max(pageSize.height, size.height));
viewportMetrics.setPageSize(actualPageSize, actualPageSize);
}
}
// For rotations, we want the focus point to be at the top left.
boolean rotation = (size.width > oldWidth && size.height < oldHeight) ||
(size.width < oldWidth && size.height > oldHeight);
PointF newFocus;
if (rotation) {
newFocus = new PointF(0, 0);
} else {
newFocus = new PointF(size.width / 2.0f, size.height / 2.0f);
}
float newZoomFactor = size.width * oldZoomFactor / oldWidth;
viewportMetrics.scaleTo(newZoomFactor, newFocus);
mViewportMetrics = new ImmutableViewportMetrics(viewportMetrics);
setForceRedraw();
if (mLayerClient != null) {
mLayerClient.viewportSizeChanged();
notifyLayerClientOfGeometryChange();
}
mPanZoomController.abortAnimation();
mView.requestRender();
}
/** Scrolls the viewport by the given offset. You must hold the monitor while calling this. */
......@@ -283,10 +216,21 @@ public class LayerController {
*/
public void setViewportMetrics(ViewportMetrics viewport) {
mViewportMetrics = new ImmutableViewportMetrics(viewport);
Log.d(LOGTAG, "setViewportMetrics: " + mViewportMetrics);
mView.requestRender();
}
public void setAnimationTarget(ViewportMetrics viewport) {
if (mLayerClient != null) {
// We know what the final viewport of the animation is going to be, so
// immediately request a draw of that area by setting the display port
// accordingly. This way we should have the content pre-rendered by the
// time the animation is done.
ImmutableViewportMetrics metrics = new ImmutableViewportMetrics(viewport);
DisplayPortMetrics displayPort = DisplayPortCalculator.calculate(metrics, null);
mLayerClient.adjustViewport(displayPort);
}
}
/**
* Scales the viewport, keeping the given focus point in the same place before and after the
* scale operation. You must hold the monitor while calling this.
......@@ -295,7 +239,6 @@ public class LayerController {
ViewportMetrics viewportMetrics = new ViewportMetrics(mViewportMetrics);
viewportMetrics.scaleTo(zoomFactor, focus);
mViewportMetrics = new ImmutableViewportMetrics(viewportMetrics);
Log.d(LOGTAG, "scaleWithFocus: " + mViewportMetrics + "; zf=" + zoomFactor);
// We assume the zoom level will only be modified by the
// PanZoomController, so no need to notify it of this change.
......@@ -305,10 +248,6 @@ public class LayerController {
public boolean post(Runnable action) { return mView.post(action); }
public void setOnTouchListener(OnTouchListener onTouchListener) {
mOnTouchListener = onTouchListener;
}
/**
* The view as well as the controller itself use this method to notify the layer client that
* the geometry changed.
......@@ -343,119 +282,38 @@ public class LayerController {
return false;
}
return aboutToCheckerboard();
}
// Returns true if a checkerboard is about to be visible.
private boolean aboutToCheckerboard() {
// Increase the size of the viewport (and clamp to page boundaries), and
// intersect it with the tile's displayport to determine whether we're
// close to checkerboarding.
FloatSize pageSize = getPageSize();
RectF adjustedViewport = RectUtils.expand(getViewport(), DANGER_ZONE_X, DANGER_ZONE_Y);
if (adjustedViewport.top < 0) adjustedViewport.top = 0;
if (adjustedViewport.left < 0) adjustedViewport.left = 0;
if (adjustedViewport.right > pageSize.width) adjustedViewport.right = pageSize.width;
if (adjustedViewport.bottom > pageSize.height) adjustedViewport.bottom = pageSize.height;
DisplayPortMetrics displayPort = (mLayerClient == null ? new DisplayPortMetrics() : mLayerClient.getDisplayPort());
return !displayPort.contains(adjustedViewport);
return DisplayPortCalculator.aboutToCheckerboard(mViewportMetrics,
mPanZoomController.getVelocityVector(), mLayerClient.getDisplayPort());
}
/**
* Converts a point from layer view coordinates to layer coordinates. In other words, given a
* point measured in pixels from the top left corner of the layer view, returns the point in
* pixels measured from the top left corner of the root layer, in the coordinate system of the
* layer itself. This method is used by the viewport controller as part of the process of
* translating touch events to Gecko's coordinate system.
* pixels measured from the last scroll position we sent to Gecko, in CSS pixels. Assuming the
* events being sent to Gecko are processed in FIFO order, this calculation should always be
* correct.
*/
public PointF convertViewPointToLayerPoint(PointF viewPoint) {
if (mRootLayer == null)
return null;
ImmutableViewportMetrics viewportMetrics = mViewportMetrics;
// Undo the transforms.
PointF origin = viewportMetrics.getOrigin();
PointF newPoint = new PointF(origin.x, origin.y);
float zoom = viewportMetrics.zoomFactor;
viewPoint.x /= zoom;
viewPoint.y /= zoom;
newPoint.offset(viewPoint.x, viewPoint.y);
Rect rootPosition = mRootLayer.getPosition();
newPoint.offset(-rootPosition.left, -rootPosition.top);
return newPoint;
}
/*
* Gesture detection. This is handled only at a high level in this class; we dispatch to the
* pan/zoom controller to do the dirty work.
*/
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
PointF point = new PointF(event.getX(), event.getY());
// this will only match the first touchstart in a series
if ((action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {
initialTouchLocation = point;
allowDefaultActions = !mWaitForTouchListeners;
// if we have a timer, this may be a double tap,
// cancel the current timer but don't clear the event queue
if (allowDefaultTimer != null) {
allowDefaultTimer.cancel();
} else {
// if we don't have a timer, make sure we remove any old events
mView.clearEventQueue();
}
allowDefaultTimer = new Timer();
allowDefaultTimer.schedule(new TimerTask() {
public void run() {
post(new Runnable() {
public void run() {
preventPanning(false);
}
});
}
}, mTimeout);
}
// After the initial touch, ignore touch moves until they exceed a minimum distance.
if (initialTouchLocation != null && (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_MOVE) {
if (PointUtils.subtract(point, initialTouchLocation).length() > PanZoomController.PAN_THRESHOLD) {
initialTouchLocation = null;
} else {
return !allowDefaultActions;
}
}
// send the event to content
if (mOnTouchListener != null)
mOnTouchListener.onTouch(mView, event);
return !allowDefaultActions;
}
public void preventPanning(boolean aValue) {
if (allowDefaultTimer != null) {
allowDefaultTimer.cancel();
allowDefaultTimer = null;
}
if (aValue == allowDefaultActions) {
allowDefaultActions = !aValue;
if (aValue) {
mView.clearEventQueue();
mPanZoomController.cancelTouch();
} else {
mView.processEventQueue();
}
}
}
public void setWaitForTouchListeners(boolean aValue) {
mWaitForTouchListeners = aValue;
ViewportMetrics geckoViewport = mLayerClient.getGeckoViewportMetrics();
PointF geckoOrigin = geckoViewport.getOrigin();
float geckoZoom = geckoViewport.getZoomFactor();
// viewPoint + origin gives the coordinate in device pixels from the top-left corner of the page.
// Divided by zoom, this gives us the coordinate in CSS pixels from the top-left corner of the page.
// geckoOrigin / geckoZoom is where Gecko thinks it is (scrollTo position) in CSS pixels from
// the top-left corner of the page. Subtracting the two gives us the offset of the viewPoint from
// the current Gecko coordinate in CSS pixels.
PointF layerPoint = new PointF(
((viewPoint.x + origin.x) / zoom) - (geckoOrigin.x / geckoZoom),
((viewPoint.y + origin.y) / zoom) - (geckoOrigin.y / geckoZoom));
return layerPoint;
}
/** Retrieves whether we should show checkerboard checks or not. */
......
......@@ -44,7 +44,6 @@ import android.graphics.Bitmap;
import android.graphics.PixelFormat;
import android.opengl.GLSurfaceView;
import android.util.Log;
import android.view.GestureDetector;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
......@@ -53,10 +52,8 @@ import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import org.libreoffice.LibreOfficeMainActivity;
import org.mozilla.gecko.ui.SimpleScaleGestureDetector;
import java.nio.IntBuffer;
import java.util.LinkedList;
/**
* A view rendered by the layer compositor.
......@@ -67,18 +64,16 @@ import java.util.LinkedList;
* Note that LayerView is accessed by Robocop via reflection.
*/
public class LayerView extends SurfaceView implements SurfaceHolder.Callback {
private static String LOGTAG = "GeckoLayerView";
private Context mContext;
private LayerController mController;
private TouchEventHandler mTouchEventHandler;
private GLController mGLController;
private InputConnectionHandler mInputConnectionHandler;
private LayerRenderer mRenderer;
private GestureDetector mGestureDetector;
private SimpleScaleGestureDetector mScaleGestureDetector;
private long mRenderTime;
private boolean mRenderTimeReset;
private static String LOGTAG = "GeckoLayerView";
/* List of events to be processed if the page does not prevent them. Should only be touched on the main thread */
private LinkedList<MotionEvent> mEventQueue = new LinkedList<MotionEvent>();
/* Must be a PAINT_xxx constant */
private int mPaintState = PAINT_NONE;
......@@ -101,11 +96,8 @@ public class LayerView extends SurfaceView implements SurfaceHolder.Callback {
mGLController = new GLController(this);
mContext = context;
mController = controller;
mTouchEventHandler = new TouchEventHandler(context, this, mController);
mRenderer = new LayerRenderer(this);
mGestureDetector = new GestureDetector(context, controller.getGestureListener());
mScaleGestureDetector =
new SimpleScaleGestureDetector(controller.getScaleGestureListener());
mGestureDetector.setOnDoubleTapListener(controller.getDoubleTapListener());
mInputConnectionHandler = null;
setFocusable(true);
......@@ -114,43 +106,13 @@ public class LayerView extends SurfaceView implements SurfaceHolder.Callback {
createGLThread();
}
private void addToEventQueue(MotionEvent event) {
MotionEvent copy = MotionEvent.obtain(event);
mEventQueue.add(copy);
}
public void processEventQueue() {
MotionEvent event = mEventQueue.poll();
while(event != null) {
processEvent(event);
event = mEventQueue.poll();
}
}
public void clearEventQueue() {
mEventQueue.clear();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mController.onTouchEvent(event)) {
addToEventQueue(event);
return true;
}
return processEvent(event);
}
private boolean processEvent(MotionEvent event) {
if (mGestureDetector.onTouchEvent(event))
return true;
mScaleGestureDetector.onTouchEvent(event);
if (mScaleGestureDetector.isInProgress())
return true;
mController.getPanZoomController().onTouchEvent(event);
return true;
return mTouchEventHandler.handleEvent(event);
}
public LayerController getController() { return mController; }
public TouchEventHandler getTouchEventHandler() { return mTouchEventHandler; }
/** The LayerRenderer calls this to indicate that the window has changed size. */
public void setViewportSize(IntSize size) {
......@@ -215,11 +177,6 @@ public class LayerView extends SurfaceView implements SurfaceHolder.Callback {
}
}
public void changeCheckerboardBitmap(Bitmap bitmap) {
mRenderer.resetCheckerboard();
mRenderer.setCheckerboardBitmap(bitmap);
}
public void addLayer(Layer layer) {
mRenderer.addLayer(layer);
}
......@@ -390,4 +347,9 @@ public class LayerView extends SurfaceView implements SurfaceHolder.Callback {
super(e);
}
}
public void changeCheckerboardBitmap(Bitmap bitmap) {
mRenderer.resetCheckerboard();
mRenderer.setCheckerboardBitmap(bitmap);
}
}
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* 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;
import android.content.Context;
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.SimpleScaleGestureDetector;
import java.util.LinkedList;
import java.util.Queue;
/**
* This class handles incoming touch events from the user and sends them to
* listeners in Gecko and/or performs the "default action" (asynchronous pan/zoom
* behaviour. EVERYTHING IN THIS CLASS MUST RUN ON THE UI THREAD.
*
* In the following code/comments, a "block" of events refers to a contiguous
* sequence of events that starts with a DOWN or POINTER_DOWN and goes up to
* but not including the next DOWN or POINTER_DOWN event.
*
* "Dispatching" an event refers to performing the default actions for the event,
* which at our level of abstraction just means sending it off to the gesture
* detectors and the pan/zoom controller.
*
* If an event is "default-prevented" that means one or more listeners in Gecko
* has called preventDefault() on the event, which means that the default action
* for that event should not occur. Usually we care about a "block" of events being
* default-prevented, which means that the DOWN/POINTER_DOWN event that started
* the block, or the first MOVE event following that, were prevent-defaulted.
*
* A "default-prevented notification" is when we here in Java-land receive a notification
* from gecko as to whether or not a block of events was default-prevented. This happens
* 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.
*/
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 LayerView mView;
private final LayerController mController;
private final GestureDetector mGestureDetector;
private final SimpleScaleGestureDetector mScaleGestureDetector;
// the queue of events that we are holding on to while waiting for a preventDefault
// notification
private final Queue<MotionEvent> mEventQueue;
private final ListenerTimeoutProcessor mListenerTimeoutProcessor;
// the listener we use to notify gecko of touch events
private OnTouchListener mOnTouchListener;
// whether or not we should wait for touch listeners to respond (this state is
// per-tab and is updated when we switch tabs).
private boolean mWaitForTouchListeners;
// true if we should hold incoming events in our queue. this is re-set for every
// block of events, this is cleared once we find out if the block has been
// default-prevented or not (or we time out waiting for that).
private boolean mHoldInQueue;
// true if we should dispatch incoming events to the gesture detector and the pan/zoom
// controller. if this is false, then the current block of events has been
// default-prevented, and we should not dispatch these events (although we'll still send
// them to gecko listeners).
private boolean mDispatchEvents;
// this next variable requires some explanation. strap yourself in.
//
// for each block of events, we do two things: (1) send the events to gecko and expect
// exactly one default-prevented notification in return, and (2) kick off a delayed
// ListenerTimeoutProcessor that triggers in case we don't hear from the listener in
// a timely fashion.
// since events are constantly coming in, we need to be able to handle more than one
// block of events in the queue.
//
// this means that there are ordering restrictions on these that we can take advantage of,
// and need to abide by. blocks of events in the queue will always be in the order that
// the user generated them. default-prevented notifications we get from gecko will be in
// the same order as the blocks of events in the queue. the ListenerTimeoutProcessors that
// have been posted will also fire in the same order as the blocks of events in the queue.
// HOWEVER, we may get multiple default-prevented notifications interleaved with multiple
// ListenerTimeoutProcessor firings, and that interleaving is not predictable.
//
// therefore, we need to make sure that for each block of events, we process the queued
// events exactly once, either when we get the default-prevented notification, or when the
// timeout expires (whichever happens first). there is no way to associate the
// default-prevented notification with a particular block of events other than via ordering,
//
// so what we do to accomplish this is to track a "processing balance", which is the number
// of default-prevented notifications that we have received, minus the number of ListenerTimeoutProcessors
// that have fired. (think "balance" as in teeter-totter balance). this value is:
// - zero when we are in a state where the next default-prevented notification we expect
// to receive and the next ListenerTimeoutProcessor we expect to fire both correspond to
// the next block of events in the queue.
// - positive when we are in a state where we have received more default-prevented notifications
// than ListenerTimeoutProcessors. This means that the next default-prevented notification
// does correspond to the block at the head of the queue, but the next n ListenerTimeoutProcessors
// need to be ignored as they are for blocks we have already processed. (n is the absolute value
// of the balance.)
// - negative when we are in a state where we have received more ListenerTimeoutProcessors than
// default-prevented notifications. This means that the next ListenerTimeoutProcessor that
// we receive does correspond to the block at the head of the queue, but the next n
// default-prevented notifications need to be ignored as they are for blocks we have already
// processed. (n is the absolute value of the balance.)
private int mProcessingBalance;
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());
mListenerTimeoutProcessor = new ListenerTimeoutProcessor();
mDispatchEvents = true;
mGestureDetector.setOnDoubleTapListener(controller.getDoubleTapListener());
}
/* This function MUST be called on the UI thread */
public boolean handleEvent(MotionEvent event) {
// if we don't have gecko listeners, just dispatch the event
// and be done with it, no extra work needed.
if (mOnTouchListener == null) {
dispatchEvent(event);
return true;
}
if (isDownEvent(event)) {
// this is the start of a new block of events! whee!
mHoldInQueue = mWaitForTouchListeners;
if (mHoldInQueue) {
// 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);
} else {
// if we're not holding these events, then we still need to pretend like
// we did and had a ListenerTimeoutProcessor fire so that when we get
// the default-prevented notification for this block, it doesn't accidentally
// act upon some other block
mProcessingBalance++;
}
}
// if we need to hold the events, add it to the queue. if we need to dispatch
// it directly, do that. it is possible that both mHoldInQueue and mDispatchEvents
// are false, in which case we are processing a block of events that we know
// has been default-prevented. in that case we don't keep the events as we don't
// need them (but we still pass them to the gecko listener).
if (mHoldInQueue) {
mEventQueue.add(MotionEvent.obtain(event));
} else if (mDispatchEvents) {
dispatchEvent(event);
}
// notify gecko of the event
mOnTouchListener.onTouch(mView, event);
return true;
}
/**
* This function is how gecko sends us a default-prevented notification. It is called
* once gecko knows definitively whether the block of events has had preventDefault
* called on it (either on the initial down event that starts the block, or on
* the first event following that down event).
*
* This function MUST be called on the UI thread.
*/
public void handleEventListenerAction(boolean allowDefaultAction) {
if (mProcessingBalance > 0) {
// this event listener that triggered this took too long, and the corresponding
// ListenerTimeoutProcessor runnable already ran for the event in question. the
// block of events this is for has already been processed, so we don't need to
// do anything here.
} else {
processEventBlock(allowDefaultAction);
}
mProcessingBalance--;
}
/* This function MUST be called on the UI thread. */
public void setWaitForTouchListeners(boolean aValue) {
mWaitForTouchListeners = aValue;
}
/* This function MUST be called on the UI thread. */
public void setOnTouchListener(OnTouchListener onTouchListener) {
mOnTouchListener = onTouchListener;
}
private boolean isDownEvent(MotionEvent event) {
int action = (event.getAction() & MotionEvent.ACTION_MASK);
return (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN);
}
/**
* Dispatch the event to the gesture detectors and the pan/zoom controller.
*/
private void dispatchEvent(MotionEvent event) {
if (mGestureDetector.onTouchEvent(event)) {
return;
}
mScaleGestureDetector.onTouchEvent(event);
if (mScaleGestureDetector.isInProgress()) {
return;
}
mController.getPanZoomController().onTouchEvent(event);
}
/**
* Process the block of events at the head of the queue now that we know
* whether it has been default-prevented or not.
*/
private void processEventBlock(boolean allowDefaultAction) {
if (!allowDefaultAction) {
// if the block has been default-prevented, cancel whatever stuff we had in
// progress in the gesture detector and pan zoom controller
long now = SystemClock.uptimeMillis();
dispatchEvent(MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0));
}
// the odd loop condition is because the first event in the queue will
// always be a DOWN or POINTER_DOWN event, and we want to process all
// the events in the queue starting at that one, up to but not including
// the next DOWN or POINTER_DOWN event.
MotionEvent event = mEventQueue.poll();
while (true) {
// for each event we process, only dispatch it if the block hasn't been
// default-prevented.
if (allowDefaultAction) {
dispatchEvent(event);
}
event = mEventQueue.peek();
if (event == null) {
// we have processed the backlog of events, and are all caught up.
// now we can set clear the hold flag and set the dispatch flag so
// that the handleEvent() function can do the right thing for all
// remaining events in this block (which is still ongoing) without
// having to put them in the queue.
mHoldInQueue = false;
mDispatchEvents = allowDefaultAction;
break;
}
if (isDownEvent(event)) {
// we have finished processing the block we were interested in.
// now we wait for the next call to processEventBlock
break;
}
// pop the event we peeked above, as it is still part of the block and
// we want to keep processing
mEventQueue.remove();
}
}
private class ListenerTimeoutProcessor implements Runnable {
/* This MUST be run on the UI thread */
public void run() {
if (mProcessingBalance < 0) {
// gecko already responded with default-prevented notification, and so
// the block of events this ListenerTimeoutProcessor corresponds to have
// already been removed from the queue.
} else {
processEventBlock(true);
}
mProcessingBalance++;
}
}
}
......@@ -114,7 +114,8 @@ public class PanZoomController
* similar to TOUCHING but after starting a pan */
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 */
ANIMATED_ZOOM, /* animated zoom to a new rect */
BOUNCE /* in a bounce animation */
}
private final LayerController mController;
......@@ -172,16 +173,32 @@ public class PanZoomController
// if that's the case, abort any animation in progress and re-zoom so that the page
// snaps to edges. for other cases (where the user's finger(s) are down) don't do
// anything special.
if (mState == PanZoomState.FLING) {
switch (mState) {
case FLING:
mX.stopFling();
mY.stopFling();
// fall through
case BOUNCE:
case ANIMATED_ZOOM:
// the zoom that's in progress likely makes no sense any more (such as if
// the screen orientation changed) so abort it
mState = PanZoomState.NOTHING;
// fall through
case NOTHING:
// Don't do animations here; they're distracting and can cause flashes on page
// transitions.
synchronized (mController) {
mController.setViewportMetrics(getValidViewportMetrics());
mController.notifyLayerClientOfGeometryChange();
}
break;
}
}
/** This must be called on the UI thread. */
public void pageSizeUpdated() {
if (mState == PanZoomState.NOTHING) {
synchronized (mController) {
ViewportMetrics validated = getValidViewportMetrics();
if (! (new ViewportMetrics(mController.getViewportMetrics())).fuzzyEquals(validated)) {
// page size changed such that we are now in overscroll. snap to the
......@@ -191,6 +208,7 @@ public class PanZoomController
}
}
}
}
/*
* Panning/scrolling
......@@ -206,6 +224,7 @@ public class PanZoomController
case ANIMATED_ZOOM:
return false;
case FLING:
case BOUNCE:
case NOTHING:
startTouch(event.getX(0), event.getY(0), event.getEventTime());
return false;
......@@ -227,6 +246,7 @@ public class PanZoomController
switch (mState) {
case NOTHING:
case FLING:
case BOUNCE:
// should never happen
Log.e(LOGTAG, "Received impossible touch move while in " + mState);
return false;
......@@ -268,6 +288,7 @@ public class PanZoomController
switch (mState) {
case NOTHING:
case FLING:
case BOUNCE:
// should never happen
Log.e(LOGTAG, "Received impossible touch end while in " + mState);
return false;
......@@ -297,6 +318,7 @@ public class PanZoomController
private boolean onTouchCancel(MotionEvent event) {
mState = PanZoomState.NOTHING;
cancelTouch();
// ensure we snap back if we're overscrolled
bounce();
return false;
......@@ -401,8 +423,11 @@ public class PanZoomController
return;
}
mState = PanZoomState.FLING;
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.
mController.setAnimationTarget(metrics);
startAnimationTimer(new BounceRunnable(bounceStartMetrics, metrics));
}
......@@ -444,6 +469,10 @@ public class PanZoomController
return FloatMath.sqrt(xvel * xvel + yvel * yvel);
}
public PointF getVelocityVector() {
return new PointF(mX.getRealVelocity(), mY.getRealVelocity());
}
private boolean stopped() {
return getVelocity() < STOPPED_THRESHOLD;
}
......@@ -456,6 +485,9 @@ public class PanZoomController
mX.displace();
mY.displace();
PointF displacement = getDisplacement();
if (FloatUtils.fuzzyEquals(displacement.x, 0.0f) && FloatUtils.fuzzyEquals(displacement.y, 0.0f)) {
return;
}
if (! mSubscroller.scrollBy(displacement)) {
synchronized (mController) {
mController.scrollBy(displacement);
......@@ -510,7 +542,7 @@ public class PanZoomController
* animation by setting the state to PanZoomState.NOTHING. Handle this case and bail
* out.
*/
if (mState != PanZoomState.FLING) {
if (mState != PanZoomState.BOUNCE) {
finishAnimation();
return;
}
......@@ -756,7 +788,18 @@ public class PanZoomController
}
public boolean getRedrawHint() {
return (mState != PanZoomState.PINCHING && mState != PanZoomState.ANIMATED_ZOOM);
switch (mState) {
case PINCHING:
case ANIMATED_ZOOM:
case BOUNCE:
// don't redraw during these because the zoom is (or might be, in the case
// of BOUNCE) be changing rapidly and gecko will have to redraw the entire
// display port area. we trigger a force-redraw upon exiting these states.
return false;
default:
// allow redrawing in other states
return true;
}
}
@Override
......
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