Chromium Code Reviews| 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..e4752ea99c605de0b55afc1ae7d971ad5f4f49b1 |
| --- /dev/null |
| +++ b/content/public/android/java/src/org/chromium/content/browser/PopupZoomer.java |
| @@ -0,0 +1,498 @@ |
| +// 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 |
|
Ted C
2012/08/23 00:19:08
This comment looks like it's capped at 80 chars an
nilesh
2012/08/23 17:04:30
Done.
|
| + * 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; |
| + |
| + public static interface OnTapListener { |
|
Ted C
2012/08/23 00:19:08
simple javadoc for this.
nilesh
2012/08/23 17:04:30
Done.
|
| + public boolean onSingleTap(View v, MotionEvent event); |
| + public boolean onLongPress(View v, MotionEvent event); |
| + } |
|
Ted C
2012/08/23 00:19:08
blank line after the definition.
nilesh
2012/08/23 17:04:30
Done.
|
| + 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); |
| + } |
| + |
| + // Copied from android.util.MathUtils. |
|
Yaron
2012/08/22 23:30:02
I'd remove this whole comment as it's a pretty tri
nilesh
2012/08/23 00:11:37
Done.
|
| + // TODO(wangxianzhu): Should use MathUtils.constrain() once it becomes public. |
| + 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); |
| + } |
| + |
| + public PopupZoomer(Context c) { |
| + super(c); |
| + |
| + // TODO(leandrogracia): restore an assertion for the resource id != 0 |
| + // here after fixing crbug.com/136704 |
|
Yaron
2012/08/22 23:30:02
Can you fulfill this TODO?
nilesh
2012/08/23 00:11:37
Modified the constructor to accept a resource id s
|
| + //assert AppResource.DIMENSION_LINK_PREVIEW_OVERLAY_RADIUS != 0; |
| + if (AppResource.DIMENSION_LINK_PREVIEW_OVERLAY_RADIUS != 0) { |
| + sOverlayCornerRadius = c.getResources().getDimension( |
| + AppResource.DIMENSION_LINK_PREVIEW_OVERLAY_RADIUS); |
| + } 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(c, listener); |
| + } |
| + |
| + public void setOnTapListener(OnTapListener listener) { |
| + mOnTapListener = listener; |
| + } |
| + |
| + public void setBitmap(Bitmap b) { |
|
Ted C
2012/08/23 00:19:08
all public methods should have javadoc
nilesh
2012/08/23 17:04:30
Done.
|
| + if (mZoomedBitmap != null) { |
| + mZoomedBitmap.recycle(); |
| + mZoomedBitmap = null; |
| + } |
| + mZoomedBitmap = b; |
| + // Round the corners of the bitmap so it doesn't stick out |
| + // around the overlay. |
| + Canvas c = new Canvas(mZoomedBitmap); |
|
Ted C
2012/08/23 00:19:08
s/c/canvas
nilesh
2012/08/23 17:04:30
Done.
|
| + Path p = new Path(); |
|
Ted C
2012/08/23 00:19:08
s/p/path
nilesh
2012/08/23 17:04:30
Done.
|
| + RectF canvasRect = new RectF(0, 0, c.getWidth(), c.getHeight()); |
| + p.addRoundRect(canvasRect, |
| + sOverlayCornerRadius, |
|
Ted C
2012/08/23 00:19:08
java indenting of 8 chars instead of c++, although
nilesh
2012/08/23 17:04:30
Done.
|
| + sOverlayCornerRadius, |
| + Direction.CCW); |
| + c.clipPath(p, Op.XOR); |
| + Paint clearPaint = new Paint(); |
| + clearPaint.setXfermode(new PorterDuffXfermode(Mode.SRC)); |
| + clearPaint.setColor(Color.TRANSPARENT); |
| + c.drawPaint(clearPaint); |
| + } |
| + |
| + private boolean scroll(float x, float y) { |
|
Ted C
2012/08/23 00:19:08
no one uses this return value so it can be removed
nilesh
2012/08/23 17:04:30
Done.
|
| + mPopupScrollX = constrain(mPopupScrollX - x, mMinScrollX, mMaxScrollX); |
| + mPopupScrollY = constrain(mPopupScrollY - y, mMinScrollY, mMaxScrollY); |
| + invalidate(); |
| + return true; |
| + } |
| + |
| + 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(); |
| + } |
| + |
| + public void hideImmediately() { |
| + mAnimating = false; |
| + mShowing = false; |
| + mTimeLeft = 0; |
| + setVisibility(INVISIBLE); |
| + mZoomedBitmap.recycle(); |
| + mZoomedBitmap = null; |
| + } |
| + |
| + public boolean isShowing() { |
| + return mShowing || mAnimating; |
| + } |
| + |
| + 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); |
|
Ted C
2012/08/23 00:19:08
space after the cast
nilesh
2012/08/23 17:04:30
Done.
|
| + 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); |
|
Ted C
2012/08/23 00:19:08
space between (int) and (
nilesh
2012/08/23 17:04:30
Done.
|
| + 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); |
| + } |
| + } |
| +} |