Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(861)

Unified Diff: content/public/android/java/src/org/chromium/content/browser/PopupZoomer.java

Issue 10828427: Add a view to show magnified link preview on Andrdoid. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Nit fixed. Created 8 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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);
+ }
+ }
+}

Powered by Google App Engine
This is Rietveld 408576698