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