OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 package org.chromium.content.browser; |
| 6 |
| 7 import android.content.Context; |
| 8 import android.graphics.Bitmap; |
| 9 import android.graphics.Canvas; |
| 10 import android.graphics.Color; |
| 11 import android.graphics.Paint; |
| 12 import android.graphics.Path; |
| 13 import android.graphics.Path.Direction; |
| 14 import android.graphics.PointF; |
| 15 import android.graphics.PorterDuff.Mode; |
| 16 import android.graphics.PorterDuffXfermode; |
| 17 import android.graphics.Rect; |
| 18 import android.graphics.RectF; |
| 19 import android.graphics.Region.Op; |
| 20 import android.graphics.drawable.Drawable; |
| 21 import android.os.SystemClock; |
| 22 import android.view.GestureDetector; |
| 23 import android.view.MotionEvent; |
| 24 import android.view.View; |
| 25 import android.view.animation.Interpolator; |
| 26 import android.view.animation.OvershootInterpolator; |
| 27 |
| 28 import org.chromium.content.app.AppResource; |
| 29 |
| 30 /** |
| 31 * PopupZoomer is used to show the on-demand link zooming popup. It handles mani
pulation of the |
| 32 * canvas and touch events to display the on-demand zoom magnifier. |
| 33 */ |
| 34 class PopupZoomer extends View { |
| 35 // The padding between the edges of the view and the popup. Note that there
is a mirror |
| 36 // constant in content/renderer/render_view_impl.cc which should be kept in
sync if |
| 37 // this is changed. |
| 38 private static final int ZOOM_BOUNDS_MARGIN = 25; |
| 39 // Time it takes for the animation to finish in ms. |
| 40 private static final long ANIMATION_DURATION = 300; |
| 41 |
| 42 /** |
| 43 * Interface to be implemented to listen for touch events inside the zoomed
area. |
| 44 * The MotionEvent coordinates correspond to original unzoomed view. |
| 45 */ |
| 46 public static interface OnTapListener { |
| 47 public boolean onSingleTap(View v, MotionEvent event); |
| 48 public boolean onLongPress(View v, MotionEvent event); |
| 49 } |
| 50 |
| 51 private OnTapListener mOnTapListener = null; |
| 52 |
| 53 // Cached drawable used to frame the zooming popup. |
| 54 // TODO(tonyg): This should be marked purgeable so that if the system wants
to recover this |
| 55 // memory, we can just reload it from the resource ID next time it is needed
. |
| 56 // See android.graphics.BitmapFactory.Options#inPurgeable |
| 57 private static Drawable sOverlayDrawable; |
| 58 // The padding used for drawing the overlay around the content, instead of d
irectly above it. |
| 59 private static Rect sOverlayPadding; |
| 60 // The radius of the overlay bubble, used for rounding the bitmap to draw un
derneath it. |
| 61 private static float sOverlayCornerRadius; |
| 62 |
| 63 private Interpolator mShowInterpolator = new OvershootInterpolator(); |
| 64 private Interpolator mHideInterpolator = new ReverseInterpolator(mShowInterp
olator); |
| 65 |
| 66 private boolean mAnimating = false; |
| 67 private boolean mShowing = false; |
| 68 private long mAnimationStartTime = 0; |
| 69 |
| 70 // The time that was left for the outwards animation to finish. |
| 71 // This is used in the case that the zoomer is cancelled while it is still a
nimating outwards, |
| 72 // to avoid having it jump to full size then animate closed. |
| 73 private long mTimeLeft = 0; |
| 74 |
| 75 // Available view area after accounting for ZOOM_BOUNDS_MARGIN. |
| 76 private RectF mViewClipRect; |
| 77 |
| 78 // The target rect to be zoomed. |
| 79 private Rect mTargetBounds; |
| 80 |
| 81 // The bitmap to hold the zoomed view. |
| 82 private Bitmap mZoomedBitmap; |
| 83 |
| 84 // How far to shift the canvas after all zooming is done, to keep it inside
the bounds of the |
| 85 // view (including margin). |
| 86 private float mShiftX = 0, mShiftY = 0; |
| 87 // The magnification factor of the popup. It is recomputed once we have mTar
getBounds and |
| 88 // mZoomedBitmap. |
| 89 private float mScale = 1.0f; |
| 90 // The bounds representing the actual zoomed popup. |
| 91 private RectF mClipRect; |
| 92 // The extrusion values are how far the zoomed area (mClipRect) extends from
the touch point. |
| 93 // These values to used to animate the popup. |
| 94 private float mLeftExtrusion, mTopExtrusion, mRightExtrusion, mBottomExtrusi
on; |
| 95 // The last touch point, where the animation will start from. |
| 96 private PointF mTouch = new PointF(); |
| 97 |
| 98 // Since we sometimes overflow the bounds of the mViewClipRect, we need to a
llow scrolling. |
| 99 // Current scroll position. |
| 100 private float mPopupScrollX, mPopupScrollY; |
| 101 // Scroll bounds. |
| 102 private float mMinScrollX, mMaxScrollX; |
| 103 private float mMinScrollY, mMaxScrollY; |
| 104 |
| 105 private GestureDetector mGestureDetector; |
| 106 |
| 107 /** |
| 108 * Gets the drawable that should be used to frame the zooming popup, loading |
| 109 * it from the resource bundle if not already cached. |
| 110 */ |
| 111 protected Drawable getOverlayDrawable() { |
| 112 if (sOverlayDrawable == null) { |
| 113 sOverlayDrawable = loadOverlayDrawable(); |
| 114 sOverlayPadding = new Rect(); |
| 115 sOverlayDrawable.getPadding(sOverlayPadding); |
| 116 } |
| 117 return sOverlayDrawable; |
| 118 } |
| 119 |
| 120 /** |
| 121 * Loads the overlay drawable from the resource bundle. |
| 122 * |
| 123 * @VisibleForTesting |
| 124 */ |
| 125 protected Drawable loadOverlayDrawable() { |
| 126 assert AppResource.DRAWABLE_LINK_PREVIEW_POPUP_OVERLAY != 0; |
| 127 return getContext().getResources().getDrawable( |
| 128 AppResource.DRAWABLE_LINK_PREVIEW_POPUP_OVERLAY); |
| 129 } |
| 130 |
| 131 private static float constrain(float amount, float low, float high) { |
| 132 return amount < low ? low : (amount > high ? high : amount); |
| 133 } |
| 134 |
| 135 private static int constrain(int amount, int low, int high) { |
| 136 return amount < low ? low : (amount > high ? high : amount); |
| 137 } |
| 138 |
| 139 /** |
| 140 * Creates Popupzoomer. |
| 141 * @param context Context to be used. |
| 142 * @param overlayRadiusDimensoinResId Resource to be used to get overlay cor
ner radius. |
| 143 */ |
| 144 public PopupZoomer(Context context, int overlayRadiusDimensoinResId) { |
| 145 super(context); |
| 146 |
| 147 if (overlayRadiusDimensoinResId != 0) { |
| 148 sOverlayCornerRadius = context.getResources().getDimension(overlayRa
diusDimensoinResId); |
| 149 } else { |
| 150 sOverlayCornerRadius = 0; |
| 151 } |
| 152 |
| 153 setVisibility(INVISIBLE); |
| 154 setFocusable(true); |
| 155 setFocusableInTouchMode(true); |
| 156 |
| 157 GestureDetector.SimpleOnGestureListener listener = |
| 158 new GestureDetector.SimpleOnGestureListener() { |
| 159 @Override |
| 160 public boolean onScroll(MotionEvent e1, MotionEvent e2, |
| 161 float distanceX, float distanceY) { |
| 162 if (mAnimating) return true; |
| 163 |
| 164 if (isTouchOutsideArea(e1.getX(), e1.getY())) { |
| 165 hide(true); |
| 166 } else { |
| 167 scroll(distanceX, distanceY); |
| 168 } |
| 169 return true; |
| 170 } |
| 171 |
| 172 @Override |
| 173 public boolean onSingleTapUp(MotionEvent e) { |
| 174 return handleTapOrPress(e, false); |
| 175 } |
| 176 |
| 177 @Override |
| 178 public void onLongPress(MotionEvent e) { |
| 179 handleTapOrPress(e, true); |
| 180 } |
| 181 |
| 182 private boolean handleTapOrPress(MotionEvent e, boolean isLongPr
ess) { |
| 183 if (mAnimating) return true; |
| 184 |
| 185 float x = e.getX(); |
| 186 float y = e.getY(); |
| 187 if (isTouchOutsideArea(x, y)) { |
| 188 // User clicked on area outside the popup. |
| 189 hide(true); |
| 190 } else if (mOnTapListener != null) { |
| 191 PointF converted = convertTouchPoint(x, y); |
| 192 MotionEvent event = MotionEvent.obtainNoHistory(e); |
| 193 event.setLocation(converted.x, converted.y); |
| 194 if (isLongPress) { |
| 195 mOnTapListener.onLongPress(PopupZoomer.this, event); |
| 196 } else { |
| 197 mOnTapListener.onSingleTap(PopupZoomer.this, event); |
| 198 } |
| 199 hide(true); |
| 200 } |
| 201 return true; |
| 202 } |
| 203 }; |
| 204 mGestureDetector = new GestureDetector(context, listener); |
| 205 } |
| 206 |
| 207 /** |
| 208 * Sets the OnTapListener. |
| 209 */ |
| 210 public void setOnTapListener(OnTapListener listener) { |
| 211 mOnTapListener = listener; |
| 212 } |
| 213 |
| 214 /** |
| 215 * Sets the bitmap to be used for the zoomed view. |
| 216 */ |
| 217 public void setBitmap(Bitmap bitmap) { |
| 218 if (mZoomedBitmap != null) { |
| 219 mZoomedBitmap.recycle(); |
| 220 mZoomedBitmap = null; |
| 221 } |
| 222 mZoomedBitmap = bitmap; |
| 223 // Round the corners of the bitmap so it doesn't stick out around the ov
erlay. |
| 224 Canvas canvas = new Canvas(mZoomedBitmap); |
| 225 Path path = new Path(); |
| 226 RectF canvasRect = new RectF(0, 0, canvas.getWidth(), canvas.getHeight()
); |
| 227 path.addRoundRect(canvasRect, sOverlayCornerRadius, sOverlayCornerRadius
, Direction.CCW); |
| 228 canvas.clipPath(path, Op.XOR); |
| 229 Paint clearPaint = new Paint(); |
| 230 clearPaint.setXfermode(new PorterDuffXfermode(Mode.SRC)); |
| 231 clearPaint.setColor(Color.TRANSPARENT); |
| 232 canvas.drawPaint(clearPaint); |
| 233 } |
| 234 |
| 235 private void scroll(float x, float y) { |
| 236 mPopupScrollX = constrain(mPopupScrollX - x, mMinScrollX, mMaxScrollX); |
| 237 mPopupScrollY = constrain(mPopupScrollY - y, mMinScrollY, mMaxScrollY); |
| 238 invalidate(); |
| 239 } |
| 240 |
| 241 private void startAnimation(boolean show) { |
| 242 mAnimating = true; |
| 243 mShowing = show; |
| 244 mTimeLeft = 0; |
| 245 if (show) { |
| 246 setVisibility(VISIBLE); |
| 247 initDimensions(); |
| 248 } else { |
| 249 long endTime = mAnimationStartTime + ANIMATION_DURATION; |
| 250 mTimeLeft = endTime - SystemClock.uptimeMillis(); |
| 251 if (mTimeLeft < 0) mTimeLeft = 0; |
| 252 } |
| 253 mAnimationStartTime = SystemClock.uptimeMillis(); |
| 254 invalidate(); |
| 255 } |
| 256 |
| 257 private void hideImmediately() { |
| 258 mAnimating = false; |
| 259 mShowing = false; |
| 260 mTimeLeft = 0; |
| 261 setVisibility(INVISIBLE); |
| 262 mZoomedBitmap.recycle(); |
| 263 mZoomedBitmap = null; |
| 264 } |
| 265 |
| 266 /** |
| 267 * Returns true if the view is currently being shown (or is animating). |
| 268 */ |
| 269 public boolean isShowing() { |
| 270 return mShowing || mAnimating; |
| 271 } |
| 272 |
| 273 /** |
| 274 * Sets the last touch point (on the unzoomed view). |
| 275 */ |
| 276 public void setLastTouch(float x, float y) { |
| 277 mTouch.x = x; |
| 278 mTouch.y = y; |
| 279 } |
| 280 |
| 281 private void setTargetBounds(Rect rect) { |
| 282 mViewClipRect = new RectF(ZOOM_BOUNDS_MARGIN, |
| 283 ZOOM_BOUNDS_MARGIN, |
| 284 getWidth() - ZOOM_BOUNDS_MARGIN, |
| 285 getHeight() - ZOOM_BOUNDS_MARGIN); |
| 286 mTargetBounds = rect; |
| 287 } |
| 288 |
| 289 private void initDimensions() { |
| 290 if (mTargetBounds == null || mTouch == null) return; |
| 291 |
| 292 // Compute the final zoom scale. |
| 293 mScale = (float) mZoomedBitmap.getWidth() / mTargetBounds.width(); |
| 294 |
| 295 float l = mTouch.x - mScale * (mTouch.x - mTargetBounds.left); |
| 296 float t = mTouch.y - mScale * (mTouch.y - mTargetBounds.top); |
| 297 float r = l + mZoomedBitmap.getWidth(); |
| 298 float b = t + mZoomedBitmap.getHeight(); |
| 299 mClipRect = new RectF(l, t, r, b); |
| 300 int width = getWidth(); |
| 301 int height = getHeight(); |
| 302 |
| 303 // Ensure it stays inside the bounds of the view. First shift it around
to see if it |
| 304 // can fully fit in the view, then clip it to the padding section of the
view to |
| 305 // ensure no overflow. |
| 306 mShiftX = 0; |
| 307 mShiftY = 0; |
| 308 |
| 309 // Right now this has the happy coincidence of showing the leftmost port
ion |
| 310 // of a scaled up bitmap, which usually has the text in it. When we wan
t to support |
| 311 // RTL languages, we can conditionally switch the order of this check to
push it |
| 312 // to the left instead of right. |
| 313 if (mClipRect.left < ZOOM_BOUNDS_MARGIN) { |
| 314 mShiftX = ZOOM_BOUNDS_MARGIN - mClipRect.left; |
| 315 mClipRect.left += mShiftX; |
| 316 mClipRect.right += mShiftX; |
| 317 } else if (mClipRect.right > width - ZOOM_BOUNDS_MARGIN) { |
| 318 mShiftX = (width - ZOOM_BOUNDS_MARGIN - mClipRect.right); |
| 319 mClipRect.right += mShiftX; |
| 320 mClipRect.left += mShiftX; |
| 321 } |
| 322 if (mClipRect.top < ZOOM_BOUNDS_MARGIN) { |
| 323 mShiftY = ZOOM_BOUNDS_MARGIN - mClipRect.top; |
| 324 mClipRect.top += mShiftY; |
| 325 mClipRect.bottom += mShiftY; |
| 326 } else if (mClipRect.bottom > height - ZOOM_BOUNDS_MARGIN) { |
| 327 mShiftY = height - ZOOM_BOUNDS_MARGIN - mClipRect.bottom; |
| 328 mClipRect.bottom += mShiftY; |
| 329 mClipRect.top += mShiftY; |
| 330 } |
| 331 |
| 332 // Allow enough scrolling to get to the entire bitmap that may be clippe
d inside the |
| 333 // bounds of the view. |
| 334 mMinScrollX = mMaxScrollX = mMinScrollY = mMaxScrollY = 0; |
| 335 if (mViewClipRect.right + mShiftX < mClipRect.right) { |
| 336 mMinScrollX = mViewClipRect.right - mClipRect.right; |
| 337 } |
| 338 if (mViewClipRect.left + mShiftX > mClipRect.left) { |
| 339 mMaxScrollX = mViewClipRect.left - mClipRect.left; |
| 340 } |
| 341 if (mViewClipRect.top + mShiftY > mClipRect.top) { |
| 342 mMaxScrollY = mViewClipRect.top - mClipRect.top; |
| 343 } |
| 344 if (mViewClipRect.bottom + mShiftY < mClipRect.bottom) { |
| 345 mMinScrollY = mViewClipRect.bottom - mClipRect.bottom; |
| 346 } |
| 347 // Now that we know how much we need to scroll, we can intersect with mV
iewClipRect. |
| 348 mClipRect.intersect(mViewClipRect); |
| 349 |
| 350 mLeftExtrusion = mTouch.x - mClipRect.left; |
| 351 mRightExtrusion = mClipRect.right - mTouch.x; |
| 352 mTopExtrusion = mTouch.y - mClipRect.top; |
| 353 mBottomExtrusion = mClipRect.bottom - mTouch.y; |
| 354 |
| 355 // Set an initial scroll position to take touch point into account. |
| 356 float percentX = |
| 357 (mTouch.x - mTargetBounds.centerX()) / (mTargetBounds.width() /
2.f) + .5f; |
| 358 float percentY = |
| 359 (mTouch.y - mTargetBounds.centerY()) / (mTargetBounds.height() /
2.f) + .5f; |
| 360 |
| 361 float scrollWidth = mMaxScrollX - mMinScrollX; |
| 362 float scrollHeight = mMaxScrollY - mMinScrollY; |
| 363 mPopupScrollX = scrollWidth * percentX * -1f; |
| 364 mPopupScrollY = scrollHeight * percentY * -1f; |
| 365 // Constrain initial scroll position within allowed bounds. |
| 366 mPopupScrollX = constrain(mPopupScrollX, mMinScrollX, mMaxScrollX); |
| 367 mPopupScrollY = constrain(mPopupScrollY, mMinScrollY, mMaxScrollY); |
| 368 } |
| 369 |
| 370 @Override |
| 371 protected void onDraw(Canvas canvas) { |
| 372 if (!isShowing() || mZoomedBitmap == null) return; |
| 373 canvas.save(); |
| 374 // Calculate the elapsed fraction of animation. |
| 375 float time = (SystemClock.uptimeMillis() - mAnimationStartTime + mTimeLe
ft) / |
| 376 ((float) ANIMATION_DURATION); |
| 377 time = constrain(time, 0, 1); |
| 378 if (time >= 1) { |
| 379 mAnimating = false; |
| 380 if (!isShowing()) { |
| 381 hideImmediately(); |
| 382 return; |
| 383 } |
| 384 } else { |
| 385 invalidate(); |
| 386 } |
| 387 |
| 388 // Fraction of the animation to actally show. |
| 389 float fractionAnimation; |
| 390 if (mShowing) { |
| 391 fractionAnimation = mShowInterpolator.getInterpolation(time); |
| 392 } else { |
| 393 fractionAnimation = mHideInterpolator.getInterpolation(time); |
| 394 } |
| 395 |
| 396 // Draw a faded color over the entire view to fade out the original cont
ent, increasing |
| 397 // the alpha value as fractionAnimation increases. |
| 398 // TODO(nileshagrawal): We should use time here instead of fractionAnima
tion |
| 399 // as fractionAnimaton is interpolated and can go over 1. |
| 400 canvas.drawARGB((int) (80 * fractionAnimation), 0, 0, 0); |
| 401 canvas.save(); |
| 402 |
| 403 // Since we want the content to appear directly above its counterpart we
need to make |
| 404 // sure that it starts out at exactly the same size as it appears in the
page, |
| 405 // i.e. scale grows from 1/mScale to 1. Note that extrusion values are a
lready zoomed |
| 406 // with mScale. |
| 407 float scale = fractionAnimation * (mScale - 1.0f) / mScale + 1.0f / mSca
le; |
| 408 |
| 409 // Since we want the content to appear directly above its counterpart on
the |
| 410 // page, we need to remove the mShiftX/Y effect at the beginning of the
animation. |
| 411 // The unshifting decreases with the animation. |
| 412 float unshiftX = - mShiftX * (1.0f - fractionAnimation) / mScale; |
| 413 float unshiftY = - mShiftY * (1.0f - fractionAnimation) / mScale; |
| 414 |
| 415 // Compute the rect to show. |
| 416 RectF rect = new RectF(); |
| 417 rect.left = mTouch.x - mLeftExtrusion * scale + unshiftX; |
| 418 rect.top = mTouch.y - mTopExtrusion * scale + unshiftY; |
| 419 rect.right = mTouch.x + mRightExtrusion * scale + unshiftX; |
| 420 rect.bottom = mTouch.y + mBottomExtrusion * scale + unshiftY; |
| 421 canvas.clipRect(rect); |
| 422 |
| 423 // Since the canvas transform APIs all pre-concat the transformations, t
his is done in |
| 424 // reverse order. The canvas is first scaled up, then shifted the approp
riate amount of |
| 425 // pixels. |
| 426 canvas.scale(scale, scale, rect.left, rect.top); |
| 427 canvas.translate(mPopupScrollX, mPopupScrollY); |
| 428 canvas.drawBitmap(mZoomedBitmap, rect.left, rect.top, null); |
| 429 canvas.restore(); |
| 430 Drawable overlayNineTile = getOverlayDrawable(); |
| 431 overlayNineTile.setBounds((int) rect.left - sOverlayPadding.left, |
| 432 (int) rect.top - sOverlayPadding.top, |
| 433 (int) rect.right + sOverlayPadding.right, |
| 434 (int) rect.bottom + sOverlayPadding.bottom); |
| 435 // TODO(nileshagrawal): We should use time here instead of fractionAnima
tion |
| 436 // as fractionAnimaton is interpolated and can go over 1. |
| 437 int alpha = constrain((int) (fractionAnimation * 255), 0, 255); |
| 438 overlayNineTile.setAlpha(alpha); |
| 439 overlayNineTile.draw(canvas); |
| 440 canvas.restore(); |
| 441 } |
| 442 |
| 443 /** |
| 444 * Show the PopupZoomer view with given target bounds. |
| 445 */ |
| 446 public void show(Rect rect){ |
| 447 if (mShowing || mZoomedBitmap == null) return; |
| 448 |
| 449 setTargetBounds(rect); |
| 450 startAnimation(true); |
| 451 } |
| 452 |
| 453 /** |
| 454 * Hide the PopupZoomer view. |
| 455 * @param animation true if hide with animation. |
| 456 */ |
| 457 public void hide(boolean animation){ |
| 458 if (!mShowing) return; |
| 459 |
| 460 if (animation) { |
| 461 startAnimation(false); |
| 462 } else { |
| 463 hideImmediately(); |
| 464 } |
| 465 } |
| 466 |
| 467 /** |
| 468 * Converts the coordinates to a point on the original un-zoomed view. |
| 469 */ |
| 470 private PointF convertTouchPoint(float x, float y) { |
| 471 x -= mShiftX; |
| 472 y -= mShiftY; |
| 473 x = mTouch.x + (x - mTouch.x - mPopupScrollX) / mScale; |
| 474 y = mTouch.y + (y - mTouch.y - mPopupScrollY) / mScale; |
| 475 return new PointF(x, y); |
| 476 } |
| 477 |
| 478 /** |
| 479 * Returns true if the point is inside the final drawable area for this popu
p zoomer. |
| 480 */ |
| 481 private boolean isTouchOutsideArea(float x, float y) { |
| 482 return !mClipRect.contains(x, y); |
| 483 } |
| 484 |
| 485 @Override |
| 486 public boolean onTouchEvent(MotionEvent event) { |
| 487 mGestureDetector.onTouchEvent(event); |
| 488 return true; |
| 489 } |
| 490 |
| 491 private static class ReverseInterpolator implements Interpolator { |
| 492 private Interpolator mInterpolator; |
| 493 |
| 494 public ReverseInterpolator(Interpolator i) { |
| 495 mInterpolator = i; |
| 496 } |
| 497 |
| 498 @Override |
| 499 public float getInterpolation(float input) { |
| 500 input = 1.0f - input; |
| 501 if (mInterpolator == null) return input; |
| 502 return mInterpolator.getInterpolation(input); |
| 503 } |
| 504 } |
| 505 } |
OLD | NEW |