Index: content/public/android/java/src/org/chromium/content/browser/PopupZoomer.java |
diff --git a/content/public/android/java/src/org/chromium/content/browser/PopupZoomer.java b/content/public/android/java/src/org/chromium/content/browser/PopupZoomer.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..79c529ce35c9fda0077b0a33ca6cba3d8d0bf35c |
--- /dev/null |
+++ b/content/public/android/java/src/org/chromium/content/browser/PopupZoomer.java |
@@ -0,0 +1,505 @@ |
+// Copyright (c) 2012 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+package org.chromium.content.browser; |
+ |
+import android.content.Context; |
+import android.graphics.Bitmap; |
+import android.graphics.Canvas; |
+import android.graphics.Color; |
+import android.graphics.Paint; |
+import android.graphics.Path; |
+import android.graphics.Path.Direction; |
+import android.graphics.PointF; |
+import android.graphics.PorterDuff.Mode; |
+import android.graphics.PorterDuffXfermode; |
+import android.graphics.Rect; |
+import android.graphics.RectF; |
+import android.graphics.Region.Op; |
+import android.graphics.drawable.Drawable; |
+import android.os.SystemClock; |
+import android.view.GestureDetector; |
+import android.view.MotionEvent; |
+import android.view.View; |
+import android.view.animation.Interpolator; |
+import android.view.animation.OvershootInterpolator; |
+ |
+import org.chromium.content.app.AppResource; |
+ |
+/** |
+ * PopupZoomer is used to show the on-demand link zooming popup. It handles manipulation of the |
+ * canvas and touch events to display the on-demand zoom magnifier. |
+ */ |
+class PopupZoomer extends View { |
+ // The padding between the edges of the view and the popup. Note that there is a mirror |
+ // constant in content/renderer/render_view_impl.cc which should be kept in sync if |
+ // this is changed. |
+ private static final int ZOOM_BOUNDS_MARGIN = 25; |
+ // Time it takes for the animation to finish in ms. |
+ private static final long ANIMATION_DURATION = 300; |
+ |
+ /** |
+ * Interface to be implemented to listen for touch events inside the zoomed area. |
+ * The MotionEvent coordinates correspond to original unzoomed view. |
+ */ |
+ public static interface OnTapListener { |
+ public boolean onSingleTap(View v, MotionEvent event); |
+ public boolean onLongPress(View v, MotionEvent event); |
+ } |
+ |
+ private OnTapListener mOnTapListener = null; |
+ |
+ // Cached drawable used to frame the zooming popup. |
+ // TODO(tonyg): This should be marked purgeable so that if the system wants to recover this |
+ // memory, we can just reload it from the resource ID next time it is needed. |
+ // See android.graphics.BitmapFactory.Options#inPurgeable |
+ private static Drawable sOverlayDrawable; |
+ // The padding used for drawing the overlay around the content, instead of directly above it. |
+ private static Rect sOverlayPadding; |
+ // The radius of the overlay bubble, used for rounding the bitmap to draw underneath it. |
+ private static float sOverlayCornerRadius; |
+ |
+ private Interpolator mShowInterpolator = new OvershootInterpolator(); |
+ private Interpolator mHideInterpolator = new ReverseInterpolator(mShowInterpolator); |
+ |
+ private boolean mAnimating = false; |
+ private boolean mShowing = false; |
+ private long mAnimationStartTime = 0; |
+ |
+ // The time that was left for the outwards animation to finish. |
+ // This is used in the case that the zoomer is cancelled while it is still animating outwards, |
+ // to avoid having it jump to full size then animate closed. |
+ private long mTimeLeft = 0; |
+ |
+ // Available view area after accounting for ZOOM_BOUNDS_MARGIN. |
+ private RectF mViewClipRect; |
+ |
+ // The target rect to be zoomed. |
+ private Rect mTargetBounds; |
+ |
+ // The bitmap to hold the zoomed view. |
+ private Bitmap mZoomedBitmap; |
+ |
+ // How far to shift the canvas after all zooming is done, to keep it inside the bounds of the |
+ // view (including margin). |
+ private float mShiftX = 0, mShiftY = 0; |
+ // The magnification factor of the popup. It is recomputed once we have mTargetBounds and |
+ // mZoomedBitmap. |
+ private float mScale = 1.0f; |
+ // The bounds representing the actual zoomed popup. |
+ private RectF mClipRect; |
+ // The extrusion values are how far the zoomed area (mClipRect) extends from the touch point. |
+ // These values to used to animate the popup. |
+ private float mLeftExtrusion, mTopExtrusion, mRightExtrusion, mBottomExtrusion; |
+ // The last touch point, where the animation will start from. |
+ private PointF mTouch = new PointF(); |
+ |
+ // Since we sometimes overflow the bounds of the mViewClipRect, we need to allow scrolling. |
+ // Current scroll position. |
+ private float mPopupScrollX, mPopupScrollY; |
+ // Scroll bounds. |
+ private float mMinScrollX, mMaxScrollX; |
+ private float mMinScrollY, mMaxScrollY; |
+ |
+ private GestureDetector mGestureDetector; |
+ |
+ /** |
+ * Gets the drawable that should be used to frame the zooming popup, loading |
+ * it from the resource bundle if not already cached. |
+ */ |
+ protected Drawable getOverlayDrawable() { |
+ if (sOverlayDrawable == null) { |
+ sOverlayDrawable = loadOverlayDrawable(); |
+ sOverlayPadding = new Rect(); |
+ sOverlayDrawable.getPadding(sOverlayPadding); |
+ } |
+ return sOverlayDrawable; |
+ } |
+ |
+ /** |
+ * Loads the overlay drawable from the resource bundle. |
+ * |
+ * @VisibleForTesting |
+ */ |
+ protected Drawable loadOverlayDrawable() { |
+ assert AppResource.DRAWABLE_LINK_PREVIEW_POPUP_OVERLAY != 0; |
+ return getContext().getResources().getDrawable( |
+ AppResource.DRAWABLE_LINK_PREVIEW_POPUP_OVERLAY); |
+ } |
+ |
+ private static float constrain(float amount, float low, float high) { |
+ return amount < low ? low : (amount > high ? high : amount); |
+ } |
+ |
+ private static int constrain(int amount, int low, int high) { |
+ return amount < low ? low : (amount > high ? high : amount); |
+ } |
+ |
+ /** |
+ * Creates Popupzoomer. |
+ * @param context Context to be used. |
+ * @param overlayRadiusDimensoinResId Resource to be used to get overlay corner radius. |
+ */ |
+ public PopupZoomer(Context context, int overlayRadiusDimensoinResId) { |
+ super(context); |
+ |
+ if (overlayRadiusDimensoinResId != 0) { |
+ sOverlayCornerRadius = context.getResources().getDimension(overlayRadiusDimensoinResId); |
+ } else { |
+ sOverlayCornerRadius = 0; |
+ } |
+ |
+ setVisibility(INVISIBLE); |
+ setFocusable(true); |
+ setFocusableInTouchMode(true); |
+ |
+ GestureDetector.SimpleOnGestureListener listener = |
+ new GestureDetector.SimpleOnGestureListener() { |
+ @Override |
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, |
+ float distanceX, float distanceY) { |
+ if (mAnimating) return true; |
+ |
+ if (isTouchOutsideArea(e1.getX(), e1.getY())) { |
+ hide(true); |
+ } else { |
+ scroll(distanceX, distanceY); |
+ } |
+ return true; |
+ } |
+ |
+ @Override |
+ public boolean onSingleTapUp(MotionEvent e) { |
+ return handleTapOrPress(e, false); |
+ } |
+ |
+ @Override |
+ public void onLongPress(MotionEvent e) { |
+ handleTapOrPress(e, true); |
+ } |
+ |
+ private boolean handleTapOrPress(MotionEvent e, boolean isLongPress) { |
+ if (mAnimating) return true; |
+ |
+ float x = e.getX(); |
+ float y = e.getY(); |
+ if (isTouchOutsideArea(x, y)) { |
+ // User clicked on area outside the popup. |
+ hide(true); |
+ } else if (mOnTapListener != null) { |
+ PointF converted = convertTouchPoint(x, y); |
+ MotionEvent event = MotionEvent.obtainNoHistory(e); |
+ event.setLocation(converted.x, converted.y); |
+ if (isLongPress) { |
+ mOnTapListener.onLongPress(PopupZoomer.this, event); |
+ } else { |
+ mOnTapListener.onSingleTap(PopupZoomer.this, event); |
+ } |
+ hide(true); |
+ } |
+ return true; |
+ } |
+ }; |
+ mGestureDetector = new GestureDetector(context, listener); |
+ } |
+ |
+ /** |
+ * Sets the OnTapListener. |
+ */ |
+ public void setOnTapListener(OnTapListener listener) { |
+ mOnTapListener = listener; |
+ } |
+ |
+ /** |
+ * Sets the bitmap to be used for the zoomed view. |
+ */ |
+ public void setBitmap(Bitmap bitmap) { |
+ if (mZoomedBitmap != null) { |
+ mZoomedBitmap.recycle(); |
+ mZoomedBitmap = null; |
+ } |
+ mZoomedBitmap = bitmap; |
+ // Round the corners of the bitmap so it doesn't stick out around the overlay. |
+ Canvas canvas = new Canvas(mZoomedBitmap); |
+ Path path = new Path(); |
+ RectF canvasRect = new RectF(0, 0, canvas.getWidth(), canvas.getHeight()); |
+ path.addRoundRect(canvasRect, sOverlayCornerRadius, sOverlayCornerRadius, Direction.CCW); |
+ canvas.clipPath(path, Op.XOR); |
+ Paint clearPaint = new Paint(); |
+ clearPaint.setXfermode(new PorterDuffXfermode(Mode.SRC)); |
+ clearPaint.setColor(Color.TRANSPARENT); |
+ canvas.drawPaint(clearPaint); |
+ } |
+ |
+ private void scroll(float x, float y) { |
+ mPopupScrollX = constrain(mPopupScrollX - x, mMinScrollX, mMaxScrollX); |
+ mPopupScrollY = constrain(mPopupScrollY - y, mMinScrollY, mMaxScrollY); |
+ invalidate(); |
+ } |
+ |
+ private void startAnimation(boolean show) { |
+ mAnimating = true; |
+ mShowing = show; |
+ mTimeLeft = 0; |
+ if (show) { |
+ setVisibility(VISIBLE); |
+ initDimensions(); |
+ } else { |
+ long endTime = mAnimationStartTime + ANIMATION_DURATION; |
+ mTimeLeft = endTime - SystemClock.uptimeMillis(); |
+ if (mTimeLeft < 0) mTimeLeft = 0; |
+ } |
+ mAnimationStartTime = SystemClock.uptimeMillis(); |
+ invalidate(); |
+ } |
+ |
+ private void hideImmediately() { |
+ mAnimating = false; |
+ mShowing = false; |
+ mTimeLeft = 0; |
+ setVisibility(INVISIBLE); |
+ mZoomedBitmap.recycle(); |
+ mZoomedBitmap = null; |
+ } |
+ |
+ /** |
+ * Returns true if the view is currently being shown (or is animating). |
+ */ |
+ public boolean isShowing() { |
+ return mShowing || mAnimating; |
+ } |
+ |
+ /** |
+ * Sets the last touch point (on the unzoomed view). |
+ */ |
+ public void setLastTouch(float x, float y) { |
+ mTouch.x = x; |
+ mTouch.y = y; |
+ } |
+ |
+ private void setTargetBounds(Rect rect) { |
+ mViewClipRect = new RectF(ZOOM_BOUNDS_MARGIN, |
+ ZOOM_BOUNDS_MARGIN, |
+ getWidth() - ZOOM_BOUNDS_MARGIN, |
+ getHeight() - ZOOM_BOUNDS_MARGIN); |
+ mTargetBounds = rect; |
+ } |
+ |
+ private void initDimensions() { |
+ if (mTargetBounds == null || mTouch == null) return; |
+ |
+ // Compute the final zoom scale. |
+ mScale = (float) mZoomedBitmap.getWidth() / mTargetBounds.width(); |
+ |
+ float l = mTouch.x - mScale * (mTouch.x - mTargetBounds.left); |
+ float t = mTouch.y - mScale * (mTouch.y - mTargetBounds.top); |
+ float r = l + mZoomedBitmap.getWidth(); |
+ float b = t + mZoomedBitmap.getHeight(); |
+ mClipRect = new RectF(l, t, r, b); |
+ int width = getWidth(); |
+ int height = getHeight(); |
+ |
+ // Ensure it stays inside the bounds of the view. First shift it around to see if it |
+ // can fully fit in the view, then clip it to the padding section of the view to |
+ // ensure no overflow. |
+ mShiftX = 0; |
+ mShiftY = 0; |
+ |
+ // Right now this has the happy coincidence of showing the leftmost portion |
+ // of a scaled up bitmap, which usually has the text in it. When we want to support |
+ // RTL languages, we can conditionally switch the order of this check to push it |
+ // to the left instead of right. |
+ if (mClipRect.left < ZOOM_BOUNDS_MARGIN) { |
+ mShiftX = ZOOM_BOUNDS_MARGIN - mClipRect.left; |
+ mClipRect.left += mShiftX; |
+ mClipRect.right += mShiftX; |
+ } else if (mClipRect.right > width - ZOOM_BOUNDS_MARGIN) { |
+ mShiftX = (width - ZOOM_BOUNDS_MARGIN - mClipRect.right); |
+ mClipRect.right += mShiftX; |
+ mClipRect.left += mShiftX; |
+ } |
+ if (mClipRect.top < ZOOM_BOUNDS_MARGIN) { |
+ mShiftY = ZOOM_BOUNDS_MARGIN - mClipRect.top; |
+ mClipRect.top += mShiftY; |
+ mClipRect.bottom += mShiftY; |
+ } else if (mClipRect.bottom > height - ZOOM_BOUNDS_MARGIN) { |
+ mShiftY = height - ZOOM_BOUNDS_MARGIN - mClipRect.bottom; |
+ mClipRect.bottom += mShiftY; |
+ mClipRect.top += mShiftY; |
+ } |
+ |
+ // Allow enough scrolling to get to the entire bitmap that may be clipped inside the |
+ // bounds of the view. |
+ mMinScrollX = mMaxScrollX = mMinScrollY = mMaxScrollY = 0; |
+ if (mViewClipRect.right + mShiftX < mClipRect.right) { |
+ mMinScrollX = mViewClipRect.right - mClipRect.right; |
+ } |
+ if (mViewClipRect.left + mShiftX > mClipRect.left) { |
+ mMaxScrollX = mViewClipRect.left - mClipRect.left; |
+ } |
+ if (mViewClipRect.top + mShiftY > mClipRect.top) { |
+ mMaxScrollY = mViewClipRect.top - mClipRect.top; |
+ } |
+ if (mViewClipRect.bottom + mShiftY < mClipRect.bottom) { |
+ mMinScrollY = mViewClipRect.bottom - mClipRect.bottom; |
+ } |
+ // Now that we know how much we need to scroll, we can intersect with mViewClipRect. |
+ mClipRect.intersect(mViewClipRect); |
+ |
+ mLeftExtrusion = mTouch.x - mClipRect.left; |
+ mRightExtrusion = mClipRect.right - mTouch.x; |
+ mTopExtrusion = mTouch.y - mClipRect.top; |
+ mBottomExtrusion = mClipRect.bottom - mTouch.y; |
+ |
+ // Set an initial scroll position to take touch point into account. |
+ float percentX = |
+ (mTouch.x - mTargetBounds.centerX()) / (mTargetBounds.width() / 2.f) + .5f; |
+ float percentY = |
+ (mTouch.y - mTargetBounds.centerY()) / (mTargetBounds.height() / 2.f) + .5f; |
+ |
+ float scrollWidth = mMaxScrollX - mMinScrollX; |
+ float scrollHeight = mMaxScrollY - mMinScrollY; |
+ mPopupScrollX = scrollWidth * percentX * -1f; |
+ mPopupScrollY = scrollHeight * percentY * -1f; |
+ // Constrain initial scroll position within allowed bounds. |
+ mPopupScrollX = constrain(mPopupScrollX, mMinScrollX, mMaxScrollX); |
+ mPopupScrollY = constrain(mPopupScrollY, mMinScrollY, mMaxScrollY); |
+ } |
+ |
+ @Override |
+ protected void onDraw(Canvas canvas) { |
+ if (!isShowing() || mZoomedBitmap == null) return; |
+ canvas.save(); |
+ // Calculate the elapsed fraction of animation. |
+ float time = (SystemClock.uptimeMillis() - mAnimationStartTime + mTimeLeft) / |
+ ((float) ANIMATION_DURATION); |
+ time = constrain(time, 0, 1); |
+ if (time >= 1) { |
+ mAnimating = false; |
+ if (!isShowing()) { |
+ hideImmediately(); |
+ return; |
+ } |
+ } else { |
+ invalidate(); |
+ } |
+ |
+ // Fraction of the animation to actally show. |
+ float fractionAnimation; |
+ if (mShowing) { |
+ fractionAnimation = mShowInterpolator.getInterpolation(time); |
+ } else { |
+ fractionAnimation = mHideInterpolator.getInterpolation(time); |
+ } |
+ |
+ // Draw a faded color over the entire view to fade out the original content, increasing |
+ // the alpha value as fractionAnimation increases. |
+ // TODO(nileshagrawal): We should use time here instead of fractionAnimation |
+ // as fractionAnimaton is interpolated and can go over 1. |
+ canvas.drawARGB((int) (80 * fractionAnimation), 0, 0, 0); |
+ canvas.save(); |
+ |
+ // Since we want the content to appear directly above its counterpart we need to make |
+ // sure that it starts out at exactly the same size as it appears in the page, |
+ // i.e. scale grows from 1/mScale to 1. Note that extrusion values are already zoomed |
+ // with mScale. |
+ float scale = fractionAnimation * (mScale - 1.0f) / mScale + 1.0f / mScale; |
+ |
+ // Since we want the content to appear directly above its counterpart on the |
+ // page, we need to remove the mShiftX/Y effect at the beginning of the animation. |
+ // The unshifting decreases with the animation. |
+ float unshiftX = - mShiftX * (1.0f - fractionAnimation) / mScale; |
+ float unshiftY = - mShiftY * (1.0f - fractionAnimation) / mScale; |
+ |
+ // Compute the rect to show. |
+ RectF rect = new RectF(); |
+ rect.left = mTouch.x - mLeftExtrusion * scale + unshiftX; |
+ rect.top = mTouch.y - mTopExtrusion * scale + unshiftY; |
+ rect.right = mTouch.x + mRightExtrusion * scale + unshiftX; |
+ rect.bottom = mTouch.y + mBottomExtrusion * scale + unshiftY; |
+ canvas.clipRect(rect); |
+ |
+ // Since the canvas transform APIs all pre-concat the transformations, this is done in |
+ // reverse order. The canvas is first scaled up, then shifted the appropriate amount of |
+ // pixels. |
+ canvas.scale(scale, scale, rect.left, rect.top); |
+ canvas.translate(mPopupScrollX, mPopupScrollY); |
+ canvas.drawBitmap(mZoomedBitmap, rect.left, rect.top, null); |
+ canvas.restore(); |
+ Drawable overlayNineTile = getOverlayDrawable(); |
+ overlayNineTile.setBounds((int) rect.left - sOverlayPadding.left, |
+ (int) rect.top - sOverlayPadding.top, |
+ (int) rect.right + sOverlayPadding.right, |
+ (int) rect.bottom + sOverlayPadding.bottom); |
+ // TODO(nileshagrawal): We should use time here instead of fractionAnimation |
+ // as fractionAnimaton is interpolated and can go over 1. |
+ int alpha = constrain((int) (fractionAnimation * 255), 0, 255); |
+ overlayNineTile.setAlpha(alpha); |
+ overlayNineTile.draw(canvas); |
+ canvas.restore(); |
+ } |
+ |
+ /** |
+ * Show the PopupZoomer view with given target bounds. |
+ */ |
+ public void show(Rect rect){ |
+ if (mShowing || mZoomedBitmap == null) return; |
+ |
+ setTargetBounds(rect); |
+ startAnimation(true); |
+ } |
+ |
+ /** |
+ * Hide the PopupZoomer view. |
+ * @param animation true if hide with animation. |
+ */ |
+ public void hide(boolean animation){ |
+ if (!mShowing) return; |
+ |
+ if (animation) { |
+ startAnimation(false); |
+ } else { |
+ hideImmediately(); |
+ } |
+ } |
+ |
+ /** |
+ * Converts the coordinates to a point on the original un-zoomed view. |
+ */ |
+ private PointF convertTouchPoint(float x, float y) { |
+ x -= mShiftX; |
+ y -= mShiftY; |
+ x = mTouch.x + (x - mTouch.x - mPopupScrollX) / mScale; |
+ y = mTouch.y + (y - mTouch.y - mPopupScrollY) / mScale; |
+ return new PointF(x, y); |
+ } |
+ |
+ /** |
+ * Returns true if the point is inside the final drawable area for this popup zoomer. |
+ */ |
+ private boolean isTouchOutsideArea(float x, float y) { |
+ return !mClipRect.contains(x, y); |
+ } |
+ |
+ @Override |
+ public boolean onTouchEvent(MotionEvent event) { |
+ mGestureDetector.onTouchEvent(event); |
+ return true; |
+ } |
+ |
+ private static class ReverseInterpolator implements Interpolator { |
+ private Interpolator mInterpolator; |
+ |
+ public ReverseInterpolator(Interpolator i) { |
+ mInterpolator = i; |
+ } |
+ |
+ @Override |
+ public float getInterpolation(float input) { |
+ input = 1.0f - input; |
+ if (mInterpolator == null) return input; |
+ return mInterpolator.getInterpolation(input); |
+ } |
+ } |
+} |