/** This function must be called from the UI thread. */ public void abortAnimation() { checkMainThread(); // this happens when gecko changes the viewport on us or if the device is rotated. // 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. 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 setState(PanZoomState.NOTHING); // fall through case NOTHING: // Don't do animations here; they're distracting and can cause flashes on page // transitions. synchronized (mTarget.getLock()) { mTarget.setViewportMetrics(getValidViewportMetrics()); } break; } }
public void handleMessage(String event, JSONObject message) { try { if (MESSAGE_ZOOM_RECT.equals(event)) { float x = (float) message.getDouble("x"); float y = (float) message.getDouble("y"); final RectF zoomRect = new RectF(x, y, x + (float) message.getDouble("w"), y + (float) message.getDouble("h")); mTarget.post( new Runnable() { public void run() { animatedZoomTo(zoomRect); } }); } else if (MESSAGE_ZOOM_PAGE.equals(event)) { ImmutableViewportMetrics metrics = getMetrics(); RectF cssPageRect = metrics.getCssPageRect(); RectF viewableRect = metrics.getCssViewport(); float y = viewableRect.top; // attempt to keep zoom keep focused on the center of the viewport float newHeight = viewableRect.height() * cssPageRect.width() / viewableRect.width(); float dh = viewableRect.height() - newHeight; // increase in the height final RectF r = new RectF(0.0f, y + dh / 2, cssPageRect.width(), y + dh / 2 + newHeight); mTarget.post( new Runnable() { public void run() { animatedZoomTo(r); } }); } } catch (Exception e) { Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e); } }
/** This must be called on the UI thread. */ public void pageRectUpdated() { if (mState == PanZoomState.NOTHING) { synchronized (mTarget.getLock()) { ImmutableViewportMetrics validated = getValidViewportMetrics(); if (!getMetrics().fuzzyEquals(validated)) { // page size changed such that we are now in overscroll. snap to the // the nearest valid viewport mTarget.setViewportMetrics(validated); } } } }
private boolean onTouchStart(MotionEvent event) { // user is taking control of movement, so stop // any auto-movement we have going stopAnimationTimer(); switch (mState) { case ANIMATED_ZOOM: // 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 mTarget.setForceRedraw(); // fall through case FLING: case BOUNCE: case NOTHING: case WAITING_LISTENERS: startTouch(event.getX(0), event.getY(0), event.getEventTime()); return false; case TOUCHING: case PANNING: case PANNING_LOCKED: case PANNING_HOLD: case PANNING_HOLD_LOCKED: case PINCHING: Log.e(LOGTAG, "Received impossible touch down while in " + mState); return false; } Log.e(LOGTAG, "Unhandled case " + mState + " in onTouchStart"); return false; }
@Override public boolean onDoubleTap(MotionEvent motionEvent) { if (mTarget.getZoomConstraints().getAllowZoom()) { sendPointToGecko("Gesture:DoubleTap", motionEvent); } return true; }
@Override public boolean onSingleTapConfirmed(MotionEvent motionEvent) { // When zooming is disabled, we handle this in onSingleTapUp. if (mTarget.getZoomConstraints().getAllowZoom()) { sendPointToGecko("Gesture:SingleTap", motionEvent); } return true; }
private void finishAnimation() { checkMainThread(); stopAnimationTimer(); // Force a viewport synchronisation mTarget.setForceRedraw(); }
@Override public boolean onSingleTapUp(MotionEvent motionEvent) { // When zooming is enabled, wait to see if there's a double-tap. if (!mTarget.getZoomConstraints().getAllowZoom()) { sendPointToGecko("Gesture:SingleTap", motionEvent); } // return false because we still want to get the ACTION_UP event that triggers this return false; }
private ImmutableViewportMetrics getValidViewportMetrics( ImmutableViewportMetrics viewportMetrics) { /* First, we adjust the zoom factor so that we can make no overscrolled area visible. */ float zoomFactor = viewportMetrics.zoomFactor; RectF pageRect = viewportMetrics.getPageRect(); RectF viewport = viewportMetrics.getViewport(); float focusX = viewport.width() / 2.0f; float focusY = viewport.height() / 2.0f; float minZoomFactor = 0.0f; float maxZoomFactor = MAX_ZOOM; ZoomConstraints constraints = mTarget.getZoomConstraints(); if (constraints.getMinZoom() > 0) minZoomFactor = constraints.getMinZoom(); if (constraints.getMaxZoom() > 0) maxZoomFactor = constraints.getMaxZoom(); if (!constraints.getAllowZoom()) { // If allowZoom is false, clamp to the default zoom level. maxZoomFactor = minZoomFactor = constraints.getDefaultZoom(); } // Ensure minZoomFactor keeps the page at least as big as the viewport. if (pageRect.width() > 0) { float scaleFactor = viewport.width() / pageRect.width(); minZoomFactor = Math.max(minZoomFactor, zoomFactor * scaleFactor); if (viewport.width() > pageRect.width()) focusX = 0.0f; } if (pageRect.height() > 0) { float scaleFactor = viewport.height() / pageRect.height(); minZoomFactor = Math.max(minZoomFactor, zoomFactor * scaleFactor); if (viewport.height() > pageRect.height()) focusY = 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 // by different scale factors, we end up scrolled to the end on one axis // after applying the scale PointF center = new PointF(focusX, focusY); viewportMetrics = viewportMetrics.scaleTo(minZoomFactor, center); } else if (zoomFactor > maxZoomFactor) { PointF center = new PointF(viewport.width() / 2.0f, viewport.height() / 2.0f); viewportMetrics = viewportMetrics.scaleTo(maxZoomFactor, center); } /* Now we pan to the right origin. */ viewportMetrics = viewportMetrics.clamp(); return viewportMetrics; }
private void updatePosition() { mX.displace(); mY.displace(); PointF displacement = resetDisplacement(); if (FloatUtils.fuzzyEquals(displacement.x, 0.0f) && FloatUtils.fuzzyEquals(displacement.y, 0.0f)) { return; } if (!mSubscroller.scrollBy(displacement)) { synchronized (mTarget.getLock()) { scrollBy(displacement.x, displacement.y); } } }
private void sendPointToGecko(String event, MotionEvent motionEvent) { String json; try { PointF point = new PointF(motionEvent.getX(), motionEvent.getY()); point = mTarget.convertViewPointToLayerPoint(point); if (point == null) { return; } json = PointUtils.toJSON(point).toString(); } catch (Exception e) { Log.e(LOGTAG, "Unable to convert point to JSON for " + event, e); return; } GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(event, json)); }
/* * Zooming */ @Override public boolean onScaleBegin(SimpleScaleGestureDetector detector) { if (mState == PanZoomState.ANIMATED_ZOOM) return false; if (!mTarget.getZoomConstraints().getAllowZoom()) return false; setState(PanZoomState.PINCHING); mLastZoomFocus = new PointF(detector.getFocusX(), detector.getFocusY()); cancelTouch(); GeckoAppShell.sendEventToGecko( GeckoEvent.createNativeGestureEvent( GeckoEvent.ACTION_MAGNIFY_START, mLastZoomFocus, getMetrics().zoomFactor)); return true; }
/* Performs a bounce-back animation to the given viewport metrics. */ private void bounce(ImmutableViewportMetrics metrics) { stopAnimationTimer(); ImmutableViewportMetrics bounceStartMetrics = getMetrics(); if (bounceStartMetrics.fuzzyEquals(metrics)) { setState(PanZoomState.NOTHING); return; } // 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. mTarget.setAnimationTarget(metrics); startAnimationTimer(new BounceRunnable(bounceStartMetrics, metrics)); }
@Override public void onScaleEnd(SimpleScaleGestureDetector detector) { if (mState == PanZoomState.ANIMATED_ZOOM) return; // switch back to the touching state startTouch(detector.getFocusX(), detector.getFocusY(), detector.getEventTime()); // Force a viewport synchronisation mTarget.setForceRedraw(); PointF point = new PointF(detector.getFocusX(), detector.getFocusY()); GeckoEvent event = GeckoEvent.createNativeGestureEvent( GeckoEvent.ACTION_MAGNIFY_END, point, getMetrics().zoomFactor); if (event == null) { return; } GeckoAppShell.sendEventToGecko(event); }
/** * 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. */ private void scaleWithFocus(float zoomFactor, PointF focus) { ImmutableViewportMetrics viewportMetrics = getMetrics(); viewportMetrics = viewportMetrics.scaleTo(zoomFactor, focus); mTarget.setViewportMetrics(viewportMetrics); }
@Override public boolean onScale(SimpleScaleGestureDetector detector) { if (GeckoApp.mAppContext == null || GeckoApp.mAppContext.mDOMFullScreen) return false; if (mState != PanZoomState.PINCHING) return false; float prevSpan = detector.getPreviousSpan(); if (FloatUtils.fuzzyEquals(prevSpan, 0.0f)) { // let's eat this one to avoid setting the new zoom to infinity (bug 711453) return true; } float spanRatio = detector.getCurrentSpan() / prevSpan; /* * Apply edge resistance if we're zoomed out smaller than the page size by scaling the zoom * factor toward 1.0. */ float resistance = Math.min(mX.getEdgeResistance(true), mY.getEdgeResistance(true)); if (spanRatio > 1.0f) spanRatio = 1.0f + (spanRatio - 1.0f) * resistance; else spanRatio = 1.0f - (1.0f - spanRatio) * resistance; synchronized (mTarget.getLock()) { float newZoomFactor = getMetrics().zoomFactor * spanRatio; float minZoomFactor = 0.0f; float maxZoomFactor = MAX_ZOOM; ZoomConstraints constraints = mTarget.getZoomConstraints(); if (constraints.getMinZoom() > 0) minZoomFactor = constraints.getMinZoom(); if (constraints.getMaxZoom() > 0) maxZoomFactor = constraints.getMaxZoom(); if (newZoomFactor < minZoomFactor) { // apply resistance when zooming past minZoomFactor, // such that it asymptotically reaches minZoomFactor / 2.0 // but never exceeds that 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 = maxZoomFactor + excessZoom; } scrollBy(mLastZoomFocus.x - detector.getFocusX(), mLastZoomFocus.y - detector.getFocusY()); PointF focus = new PointF(detector.getFocusX(), detector.getFocusY()); scaleWithFocus(newZoomFactor, focus); } mLastZoomFocus.set(detector.getFocusX(), detector.getFocusY()); GeckoEvent event = GeckoEvent.createNativeGestureEvent( GeckoEvent.ACTION_MAGNIFY, mLastZoomFocus, getMetrics().zoomFactor); GeckoAppShell.sendEventToGecko(event); return true; }
private ImmutableViewportMetrics getMetrics() { return mTarget.getViewportMetrics(); }
private void scrollBy(float dx, float dy) { ImmutableViewportMetrics scrolled = getMetrics().offsetViewportBy(dx, dy); mTarget.setViewportMetrics(scrolled); }