1 /* 2 * Copyright (C) 2011 Google Inc. 3 * Licensed to The Android Open Source Project. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package com.android.ex.photo.views; 19 20 import android.content.Context; 21 import android.content.res.Resources; 22 import android.graphics.Bitmap; 23 import android.graphics.Canvas; 24 import android.graphics.Matrix; 25 import android.graphics.Paint; 26 import android.graphics.Paint.Style; 27 import android.graphics.Rect; 28 import android.graphics.RectF; 29 import android.graphics.drawable.BitmapDrawable; 30 import android.graphics.drawable.Drawable; 31 import android.support.v4.view.GestureDetectorCompat; 32 import android.support.v4.view.ScaleGestureDetectorCompat; 33 import android.util.AttributeSet; 34 import android.view.GestureDetector.OnDoubleTapListener; 35 import android.view.GestureDetector.OnGestureListener; 36 import android.view.MotionEvent; 37 import android.view.ScaleGestureDetector; 38 import android.view.View; 39 import android.view.ViewConfiguration; 40 41 import com.android.ex.photo.R; 42 import com.android.ex.photo.fragments.PhotoViewFragment.HorizontallyScrollable; 43 44 /** 45 * Layout for the photo list view header. 46 */ 47 public class PhotoView extends View implements OnGestureListener, 48 OnDoubleTapListener, ScaleGestureDetector.OnScaleGestureListener, 49 HorizontallyScrollable { 50 51 public static final int TRANSLATE_NONE = 0; 52 public static final int TRANSLATE_X_ONLY = 1; 53 public static final int TRANSLATE_Y_ONLY = 2; 54 public static final int TRANSLATE_BOTH = 3; 55 56 /** Zoom animation duration; in milliseconds */ 57 private final static long ZOOM_ANIMATION_DURATION = 200L; 58 /** Rotate animation duration; in milliseconds */ 59 private final static long ROTATE_ANIMATION_DURATION = 500L; 60 /** Snap animation duration; in milliseconds */ 61 private static final long SNAP_DURATION = 100L; 62 /** Amount of time to wait before starting snap animation; in milliseconds */ 63 private static final long SNAP_DELAY = 250L; 64 /** By how much to scale the image when double click occurs */ 65 private final static float DOUBLE_TAP_SCALE_FACTOR = 2.0f; 66 /** Amount which can be zoomed in past the maximum scale, and then scaled back */ 67 private final static float SCALE_OVERZOOM_FACTOR = 1.5f; 68 /** Amount of translation needed before starting a snap animation */ 69 private final static float SNAP_THRESHOLD = 20.0f; 70 /** The width & height of the bitmap returned by {@link #getCroppedPhoto()} */ 71 private final static float CROPPED_SIZE = 256.0f; 72 73 /** 74 * Touch slop used to determine if this double tap is valid for starting a scale or should be 75 * ignored. 76 */ 77 private static int sTouchSlopSquare; 78 79 /** If {@code true}, the static values have been initialized */ 80 private static boolean sInitialized; 81 82 // Various dimensions 83 /** Width & height of the crop region */ 84 private static int sCropSize; 85 86 // Bitmaps 87 /** Video icon */ 88 private static Bitmap sVideoImage; 89 /** Video icon */ 90 private static Bitmap sVideoNotReadyImage; 91 92 // Paints 93 /** Paint to partially dim the photo during crop */ 94 private static Paint sCropDimPaint; 95 /** Paint to highlight the cropped portion of the photo */ 96 private static Paint sCropPaint; 97 98 /** The photo to display */ 99 private Drawable mDrawable; 100 /** The matrix used for drawing; this may be {@code null} */ 101 private Matrix mDrawMatrix; 102 /** A matrix to apply the scaling of the photo */ 103 private Matrix mMatrix = new Matrix(); 104 /** The original matrix for this image; used to reset any transformations applied by the user */ 105 private Matrix mOriginalMatrix = new Matrix(); 106 107 /** The fixed height of this view. If {@code -1}, calculate the height */ 108 private int mFixedHeight = -1; 109 /** When {@code true}, the view has been laid out */ 110 private boolean mHaveLayout; 111 /** Whether or not the photo is full-screen */ 112 private boolean mFullScreen; 113 /** Whether or not this is a still image of a video */ 114 private byte[] mVideoBlob; 115 /** Whether or not this is a still image of a video */ 116 private boolean mVideoReady; 117 118 /** Whether or not crop is allowed */ 119 private boolean mAllowCrop; 120 /** The crop region */ 121 private Rect mCropRect = new Rect(); 122 /** Actual crop size; may differ from {@link #sCropSize} if the screen is smaller */ 123 private int mCropSize; 124 /** The maximum amount of scaling to apply to images */ 125 private float mMaxInitialScaleFactor; 126 127 /** Gesture detector */ 128 private GestureDetectorCompat mGestureDetector; 129 /** Gesture detector that detects pinch gestures */ 130 private ScaleGestureDetector mScaleGetureDetector; 131 /** An external click listener */ 132 private OnClickListener mExternalClickListener; 133 /** When {@code true}, allows gestures to scale / pan the image */ 134 private boolean mTransformsEnabled; 135 136 // To support zooming 137 /** When {@code true}, a double tap scales the image by {@link #DOUBLE_TAP_SCALE_FACTOR} */ 138 private boolean mDoubleTapToZoomEnabled = true; 139 /** When {@code true}, prevents scale end gesture from falsely triggering a double click. */ 140 private boolean mDoubleTapDebounce; 141 /** When {@code false}, event is a scale gesture. Otherwise, event is a double touch. */ 142 private boolean mIsDoubleTouch; 143 /** Runnable that scales the image */ 144 private ScaleRunnable mScaleRunnable; 145 /** Minimum scale the image can have. */ 146 private float mMinScale; 147 /** Maximum scale to limit scaling to, 0 means no limit. */ 148 private float mMaxScale; 149 150 // To support translation [i.e. panning] 151 /** Runnable that can move the image */ 152 private TranslateRunnable mTranslateRunnable; 153 private SnapRunnable mSnapRunnable; 154 155 // To support rotation 156 /** The rotate runnable used to animate rotations of the image */ 157 private RotateRunnable mRotateRunnable; 158 /** The current rotation amount, in degrees */ 159 private float mRotation; 160 161 // Convenience fields 162 // These are declared here not because they are important properties of the view. Rather, we 163 // declare them here to avoid object allocation during critical graphics operations; such as 164 // layout or drawing. 165 /** Source (i.e. the photo size) bounds */ 166 private RectF mTempSrc = new RectF(); 167 /** Destination (i.e. the display) bounds. The image is scaled to this size. */ 168 private RectF mTempDst = new RectF(); 169 /** Rectangle to handle translations */ 170 private RectF mTranslateRect = new RectF(); 171 /** Array to store a copy of the matrix values */ 172 private float[] mValues = new float[9]; 173 174 /** 175 * Track whether a double tap event occurred. 176 */ 177 private boolean mDoubleTapOccurred; 178 179 /** 180 * X and Y coordinates for the current down event. Since mDoubleTapOccurred only contains the 181 * information that there was a double tap event, use these to get the secondary tap 182 * information to determine if a user has moved beyond touch slop. 183 */ 184 private float mDownFocusX; 185 private float mDownFocusY; 186 187 /** 188 * Whether the QuickSale gesture is enabled. 189 */ 190 private boolean mQuickScaleEnabled; 191 PhotoView(Context context)192 public PhotoView(Context context) { 193 super(context); 194 initialize(); 195 } 196 PhotoView(Context context, AttributeSet attrs)197 public PhotoView(Context context, AttributeSet attrs) { 198 super(context, attrs); 199 initialize(); 200 } 201 PhotoView(Context context, AttributeSet attrs, int defStyle)202 public PhotoView(Context context, AttributeSet attrs, int defStyle) { 203 super(context, attrs, defStyle); 204 initialize(); 205 } 206 207 @Override onTouchEvent(MotionEvent event)208 public boolean onTouchEvent(MotionEvent event) { 209 if (mScaleGetureDetector == null || mGestureDetector == null) { 210 // We're being destroyed; ignore any touch events 211 return true; 212 } 213 214 mScaleGetureDetector.onTouchEvent(event); 215 mGestureDetector.onTouchEvent(event); 216 final int action = event.getAction(); 217 218 switch (action) { 219 case MotionEvent.ACTION_UP: 220 case MotionEvent.ACTION_CANCEL: 221 if (!mTranslateRunnable.mRunning) { 222 snap(); 223 } 224 break; 225 } 226 227 return true; 228 } 229 230 @Override onDoubleTap(MotionEvent e)231 public boolean onDoubleTap(MotionEvent e) { 232 mDoubleTapOccurred = true; 233 if (!mQuickScaleEnabled) { 234 return scale(e); 235 } 236 return false; 237 } 238 239 @Override onDoubleTapEvent(MotionEvent e)240 public boolean onDoubleTapEvent(MotionEvent e) { 241 final int action = e.getAction(); 242 boolean handled = false; 243 244 switch (action) { 245 case MotionEvent.ACTION_DOWN: 246 if (mQuickScaleEnabled) { 247 mDownFocusX = e.getX(); 248 mDownFocusY = e.getY(); 249 } 250 break; 251 case MotionEvent.ACTION_UP: 252 if (mQuickScaleEnabled) { 253 handled = scale(e); 254 } 255 break; 256 case MotionEvent.ACTION_MOVE: 257 if (mQuickScaleEnabled && mDoubleTapOccurred) { 258 final int deltaX = (int) (e.getX() - mDownFocusX); 259 final int deltaY = (int) (e.getY() - mDownFocusY); 260 int distance = (deltaX * deltaX) + (deltaY * deltaY); 261 if (distance > sTouchSlopSquare) { 262 mDoubleTapOccurred = false; 263 } 264 } 265 break; 266 267 } 268 return handled; 269 } 270 scale(MotionEvent e)271 private boolean scale(MotionEvent e) { 272 boolean handled = false; 273 if (mDoubleTapToZoomEnabled && mTransformsEnabled && mDoubleTapOccurred) { 274 if (!mDoubleTapDebounce) { 275 float currentScale = getScale(); 276 float targetScale; 277 float centerX, centerY; 278 279 // Zoom out if not default scale, otherwise zoom in 280 if (currentScale > mMinScale) { 281 targetScale = mMinScale; 282 float relativeScale = targetScale / currentScale; 283 // Find the apparent origin for scaling that equals this scale and translate 284 centerX = (getWidth() / 2 - relativeScale * mTranslateRect.centerX()) / 285 (1 - relativeScale); 286 centerY = (getHeight() / 2 - relativeScale * mTranslateRect.centerY()) / 287 (1 - relativeScale); 288 } else { 289 targetScale = currentScale * DOUBLE_TAP_SCALE_FACTOR; 290 // Ensure the target scale is within our bounds 291 targetScale = Math.max(mMinScale, targetScale); 292 targetScale = Math.min(mMaxScale, targetScale); 293 float relativeScale = targetScale / currentScale; 294 float widthBuffer = (getWidth() - mTranslateRect.width()) / relativeScale; 295 float heightBuffer = (getHeight() - mTranslateRect.height()) / relativeScale; 296 // Clamp the center if it would result in uneven borders 297 if (mTranslateRect.width() <= widthBuffer * 2) { 298 centerX = mTranslateRect.centerX(); 299 } else { 300 centerX = Math.min(Math.max(mTranslateRect.left + widthBuffer, 301 e.getX()), mTranslateRect.right - widthBuffer); 302 } 303 if (mTranslateRect.height() <= heightBuffer * 2) { 304 centerY = mTranslateRect.centerY(); 305 } else { 306 centerY = Math.min(Math.max(mTranslateRect.top + heightBuffer, 307 e.getY()), mTranslateRect.bottom - heightBuffer); 308 } 309 } 310 311 mScaleRunnable.start(currentScale, targetScale, centerX, centerY); 312 handled = true; 313 } 314 mDoubleTapDebounce = false; 315 } 316 mDoubleTapOccurred = false; 317 return handled; 318 } 319 320 @Override onSingleTapConfirmed(MotionEvent e)321 public boolean onSingleTapConfirmed(MotionEvent e) { 322 if (mExternalClickListener != null && !mIsDoubleTouch) { 323 mExternalClickListener.onClick(this); 324 } 325 mIsDoubleTouch = false; 326 return true; 327 } 328 329 @Override onSingleTapUp(MotionEvent e)330 public boolean onSingleTapUp(MotionEvent e) { 331 return false; 332 } 333 334 @Override onLongPress(MotionEvent e)335 public void onLongPress(MotionEvent e) { 336 } 337 338 @Override onShowPress(MotionEvent e)339 public void onShowPress(MotionEvent e) { 340 } 341 342 @Override onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)343 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { 344 if (mTransformsEnabled && !mScaleRunnable.mRunning) { 345 translate(-distanceX, -distanceY); 346 } 347 return true; 348 } 349 350 @Override onDown(MotionEvent e)351 public boolean onDown(MotionEvent e) { 352 if (mTransformsEnabled) { 353 mTranslateRunnable.stop(); 354 mSnapRunnable.stop(); 355 } 356 return true; 357 } 358 359 @Override onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)360 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { 361 if (mTransformsEnabled && !mScaleRunnable.mRunning) { 362 mTranslateRunnable.start(velocityX, velocityY); 363 } 364 return true; 365 } 366 367 @Override onScale(ScaleGestureDetector detector)368 public boolean onScale(ScaleGestureDetector detector) { 369 if (mTransformsEnabled) { 370 mIsDoubleTouch = false; 371 float currentScale = getScale(); 372 float newScale = currentScale * detector.getScaleFactor(); 373 scale(newScale, detector.getFocusX(), detector.getFocusY()); 374 } 375 return true; 376 } 377 378 @Override onScaleBegin(ScaleGestureDetector detector)379 public boolean onScaleBegin(ScaleGestureDetector detector) { 380 if (mTransformsEnabled) { 381 mScaleRunnable.stop(); 382 mIsDoubleTouch = true; 383 } 384 return true; 385 } 386 387 @Override onScaleEnd(ScaleGestureDetector detector)388 public void onScaleEnd(ScaleGestureDetector detector) { 389 // Scale back to the maximum if over-zoomed 390 float currentScale = getScale(); 391 if (currentScale > mMaxScale) { 392 // The number of times the crop amount pulled in can fit on the screen 393 float marginFit = 1 / (1 - mMaxScale / currentScale); 394 // The (negative) relative maximum distance from an image edge such that when scaled 395 // this far from the edge, all of the image off-screen in that direction is pulled in 396 float relativeDistance = 1 - marginFit; 397 float centerX = getWidth() / 2; 398 float centerY = getHeight() / 2; 399 // This center will pull all of the margin from the lesser side, over will expose trim 400 float maxX = mTranslateRect.left * relativeDistance; 401 float maxY = mTranslateRect.top * relativeDistance; 402 // This center will pull all of the margin from the greater side, over will expose trim 403 float minX = getWidth() * marginFit + mTranslateRect.right * relativeDistance; 404 float minY = getHeight() * marginFit + mTranslateRect.bottom * relativeDistance; 405 // Adjust center according to bounds to avoid bad crop 406 if (minX > maxX) { 407 // Border is inevitable due to small image size, so we split the crop difference 408 centerX = (minX + maxX) / 2; 409 } else { 410 centerX = Math.min(Math.max(minX, centerX), maxX); 411 } 412 if (minY > maxY) { 413 // Border is inevitable due to small image size, so we split the crop difference 414 centerY = (minY + maxY) / 2; 415 } else { 416 centerY = Math.min(Math.max(minY, centerY), maxY); 417 } 418 mScaleRunnable.start(currentScale, mMaxScale, centerX, centerY); 419 } 420 if (mTransformsEnabled && mIsDoubleTouch) { 421 mDoubleTapDebounce = true; 422 resetTransformations(); 423 } 424 } 425 426 @Override setOnClickListener(OnClickListener listener)427 public void setOnClickListener(OnClickListener listener) { 428 mExternalClickListener = listener; 429 } 430 431 @Override interceptMoveLeft(float origX, float origY)432 public boolean interceptMoveLeft(float origX, float origY) { 433 if (!mTransformsEnabled) { 434 // Allow intercept if we're not in transform mode 435 return false; 436 } else if (mTranslateRunnable.mRunning) { 437 // Don't allow touch intercept until we've stopped flinging 438 return true; 439 } else { 440 mMatrix.getValues(mValues); 441 mTranslateRect.set(mTempSrc); 442 mMatrix.mapRect(mTranslateRect); 443 444 final float viewWidth = getWidth(); 445 final float transX = mValues[Matrix.MTRANS_X]; 446 final float drawWidth = mTranslateRect.right - mTranslateRect.left; 447 448 if (!mTransformsEnabled || drawWidth <= viewWidth) { 449 // Allow intercept if not in transform mode or the image is smaller than the view 450 return false; 451 } else if (transX == 0) { 452 // We're at the left-side of the image; allow intercepting movements to the right 453 return false; 454 } else if (viewWidth >= drawWidth + transX) { 455 // We're at the right-side of the image; allow intercepting movements to the left 456 return true; 457 } else { 458 // We're in the middle of the image; don't allow touch intercept 459 return true; 460 } 461 } 462 } 463 464 @Override interceptMoveRight(float origX, float origY)465 public boolean interceptMoveRight(float origX, float origY) { 466 if (!mTransformsEnabled) { 467 // Allow intercept if we're not in transform mode 468 return false; 469 } else if (mTranslateRunnable.mRunning) { 470 // Don't allow touch intercept until we've stopped flinging 471 return true; 472 } else { 473 mMatrix.getValues(mValues); 474 mTranslateRect.set(mTempSrc); 475 mMatrix.mapRect(mTranslateRect); 476 477 final float viewWidth = getWidth(); 478 final float transX = mValues[Matrix.MTRANS_X]; 479 final float drawWidth = mTranslateRect.right - mTranslateRect.left; 480 481 if (!mTransformsEnabled || drawWidth <= viewWidth) { 482 // Allow intercept if not in transform mode or the image is smaller than the view 483 return false; 484 } else if (transX == 0) { 485 // We're at the left-side of the image; allow intercepting movements to the right 486 return true; 487 } else if (viewWidth >= drawWidth + transX) { 488 // We're at the right-side of the image; allow intercepting movements to the left 489 return false; 490 } else { 491 // We're in the middle of the image; don't allow touch intercept 492 return true; 493 } 494 } 495 } 496 497 /** 498 * Free all resources held by this view. 499 * The view is on its way to be collected and will not be reused. 500 */ clear()501 public void clear() { 502 mGestureDetector = null; 503 mScaleGetureDetector = null; 504 mDrawable = null; 505 mScaleRunnable.stop(); 506 mScaleRunnable = null; 507 mTranslateRunnable.stop(); 508 mTranslateRunnable = null; 509 mSnapRunnable.stop(); 510 mSnapRunnable = null; 511 mRotateRunnable.stop(); 512 mRotateRunnable = null; 513 setOnClickListener(null); 514 mExternalClickListener = null; 515 mDoubleTapOccurred = false; 516 } 517 bindDrawable(Drawable drawable)518 public void bindDrawable(Drawable drawable) { 519 boolean changed = false; 520 if (drawable != null && drawable != mDrawable) { 521 // Clear previous state. 522 if (mDrawable != null) { 523 mDrawable.setCallback(null); 524 } 525 526 mDrawable = drawable; 527 528 // Reset mMinScale to ensure the bounds / matrix are recalculated 529 mMinScale = 0f; 530 531 // Set a callback? 532 mDrawable.setCallback(this); 533 534 changed = true; 535 } 536 537 configureBounds(changed); 538 invalidate(); 539 } 540 541 /** 542 * Binds a bitmap to the view. 543 * 544 * @param photoBitmap the bitmap to bind. 545 */ bindPhoto(Bitmap photoBitmap)546 public void bindPhoto(Bitmap photoBitmap) { 547 boolean currentDrawableIsBitmapDrawable = mDrawable instanceof BitmapDrawable; 548 boolean changed = !(currentDrawableIsBitmapDrawable); 549 if (mDrawable != null && currentDrawableIsBitmapDrawable) { 550 final Bitmap drawableBitmap = ((BitmapDrawable) mDrawable).getBitmap(); 551 if (photoBitmap == drawableBitmap) { 552 // setting the same bitmap; do nothing 553 return; 554 } 555 556 changed = photoBitmap != null && 557 (mDrawable.getIntrinsicWidth() != photoBitmap.getWidth() || 558 mDrawable.getIntrinsicHeight() != photoBitmap.getHeight()); 559 560 // Reset mMinScale to ensure the bounds / matrix are recalculated 561 mMinScale = 0f; 562 mDrawable = null; 563 } 564 565 if (mDrawable == null && photoBitmap != null) { 566 mDrawable = new BitmapDrawable(getResources(), photoBitmap); 567 } 568 569 configureBounds(changed); 570 invalidate(); 571 } 572 573 /** 574 * Returns the bound photo data if set. Otherwise, {@code null}. 575 */ getPhoto()576 public Bitmap getPhoto() { 577 if (mDrawable != null && mDrawable instanceof BitmapDrawable) { 578 return ((BitmapDrawable) mDrawable).getBitmap(); 579 } 580 return null; 581 } 582 583 /** 584 * Returns the bound drawable. May be {@code null} if no drawable is bound. 585 */ getDrawable()586 public Drawable getDrawable() { 587 return mDrawable; 588 } 589 590 /** 591 * Gets video data associated with this item. Returns {@code null} if this is not a video. 592 */ getVideoData()593 public byte[] getVideoData() { 594 return mVideoBlob; 595 } 596 597 /** 598 * Returns {@code true} if the photo represents a video. Otherwise, {@code false}. 599 */ isVideo()600 public boolean isVideo() { 601 return mVideoBlob != null; 602 } 603 604 /** 605 * Returns {@code true} if the video is ready to play. Otherwise, {@code false}. 606 */ isVideoReady()607 public boolean isVideoReady() { 608 return mVideoBlob != null && mVideoReady; 609 } 610 611 /** 612 * Returns {@code true} if a photo has been bound. Otherwise, {@code false}. 613 */ isPhotoBound()614 public boolean isPhotoBound() { 615 return mDrawable != null; 616 } 617 618 /** 619 * Hides the photo info portion of the header. As a side effect, this automatically enables 620 * or disables image transformations [eg zoom, pan, etc...] depending upon the value of 621 * fullScreen. If this is not desirable, enable / disable image transformations manually. 622 */ setFullScreen(boolean fullScreen, boolean animate)623 public void setFullScreen(boolean fullScreen, boolean animate) { 624 if (fullScreen != mFullScreen) { 625 mFullScreen = fullScreen; 626 requestLayout(); 627 invalidate(); 628 } 629 } 630 631 /** 632 * Enable or disable cropping of the displayed image. Cropping can only be enabled 633 * <em>before</em> the view has been laid out. Additionally, once cropping has been 634 * enabled, it cannot be disabled. 635 */ enableAllowCrop(boolean allowCrop)636 public void enableAllowCrop(boolean allowCrop) { 637 if (allowCrop && mHaveLayout) { 638 throw new IllegalArgumentException("Cannot set crop after view has been laid out"); 639 } 640 if (!allowCrop && mAllowCrop) { 641 throw new IllegalArgumentException("Cannot unset crop mode"); 642 } 643 mAllowCrop = allowCrop; 644 } 645 646 /** 647 * Gets a bitmap of the cropped region. If cropping is not enabled, returns {@code null}. 648 */ getCroppedPhoto()649 public Bitmap getCroppedPhoto() { 650 if (!mAllowCrop) { 651 return null; 652 } 653 654 final Bitmap croppedBitmap = Bitmap.createBitmap( 655 (int) CROPPED_SIZE, (int) CROPPED_SIZE, Bitmap.Config.ARGB_8888); 656 final Canvas croppedCanvas = new Canvas(croppedBitmap); 657 658 // scale for the final dimensions 659 final int cropWidth = mCropRect.right - mCropRect.left; 660 final float scaleWidth = CROPPED_SIZE / cropWidth; 661 final float scaleHeight = CROPPED_SIZE / cropWidth; 662 663 // translate to the origin & scale 664 final Matrix matrix = new Matrix(mDrawMatrix); 665 matrix.postTranslate(-mCropRect.left, -mCropRect.top); 666 matrix.postScale(scaleWidth, scaleHeight); 667 668 // draw the photo 669 if (mDrawable != null) { 670 croppedCanvas.concat(matrix); 671 mDrawable.draw(croppedCanvas); 672 } 673 return croppedBitmap; 674 } 675 676 /** 677 * Resets the image transformation to its original value. 678 */ resetTransformations()679 public void resetTransformations() { 680 // snap transformations; we don't animate 681 mMatrix.set(mOriginalMatrix); 682 683 // Invalidate the view because if you move off this PhotoView 684 // to another one and come back, you want it to draw from scratch 685 // in case you were zoomed in or translated (since those settings 686 // are not preserved and probably shouldn't be). 687 invalidate(); 688 } 689 690 /** 691 * Rotates the image 90 degrees, clockwise. 692 */ rotateClockwise()693 public void rotateClockwise() { 694 rotate(90, true); 695 } 696 697 /** 698 * Rotates the image 90 degrees, counter clockwise. 699 */ rotateCounterClockwise()700 public void rotateCounterClockwise() { 701 rotate(-90, true); 702 } 703 704 @Override onDraw(Canvas canvas)705 protected void onDraw(Canvas canvas) { 706 super.onDraw(canvas); 707 708 // draw the photo 709 if (mDrawable != null) { 710 int saveCount = canvas.getSaveCount(); 711 canvas.save(); 712 713 if (mDrawMatrix != null) { 714 canvas.concat(mDrawMatrix); 715 } 716 mDrawable.draw(canvas); 717 718 canvas.restoreToCount(saveCount); 719 720 if (mVideoBlob != null) { 721 final Bitmap videoImage = (mVideoReady ? sVideoImage : sVideoNotReadyImage); 722 final int drawLeft = (getWidth() - videoImage.getWidth()) / 2; 723 final int drawTop = (getHeight() - videoImage.getHeight()) / 2; 724 canvas.drawBitmap(videoImage, drawLeft, drawTop, null); 725 } 726 727 // Extract the drawable's bounds (in our own copy, to not alter the image) 728 mTranslateRect.set(mDrawable.getBounds()); 729 if (mDrawMatrix != null) { 730 mDrawMatrix.mapRect(mTranslateRect); 731 } 732 733 if (mAllowCrop) { 734 int previousSaveCount = canvas.getSaveCount(); 735 canvas.drawRect(0, 0, getWidth(), getHeight(), sCropDimPaint); 736 canvas.save(); 737 canvas.clipRect(mCropRect); 738 739 if (mDrawMatrix != null) { 740 canvas.concat(mDrawMatrix); 741 } 742 743 mDrawable.draw(canvas); 744 canvas.restoreToCount(previousSaveCount); 745 canvas.drawRect(mCropRect, sCropPaint); 746 } 747 } 748 } 749 750 @Override onLayout(boolean changed, int left, int top, int right, int bottom)751 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 752 super.onLayout(changed, left, top, right, bottom); 753 mHaveLayout = true; 754 final int layoutWidth = getWidth(); 755 final int layoutHeight = getHeight(); 756 757 if (mAllowCrop) { 758 mCropSize = Math.min(sCropSize, Math.min(layoutWidth, layoutHeight)); 759 final int cropLeft = (layoutWidth - mCropSize) / 2; 760 final int cropTop = (layoutHeight - mCropSize) / 2; 761 final int cropRight = cropLeft + mCropSize; 762 final int cropBottom = cropTop + mCropSize; 763 764 // Create a crop region overlay. We need a separate canvas to be able to "punch 765 // a hole" through to the underlying image. 766 mCropRect.set(cropLeft, cropTop, cropRight, cropBottom); 767 } 768 configureBounds(changed); 769 } 770 771 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)772 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 773 if (mFixedHeight != -1) { 774 super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(mFixedHeight, 775 MeasureSpec.AT_MOST)); 776 setMeasuredDimension(getMeasuredWidth(), mFixedHeight); 777 } else { 778 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 779 } 780 } 781 782 @Override verifyDrawable(Drawable drawable)783 public boolean verifyDrawable(Drawable drawable) { 784 return mDrawable == drawable || super.verifyDrawable(drawable); 785 } 786 787 @Override 788 /** 789 * {@inheritDoc} 790 */ invalidateDrawable(Drawable drawable)791 public void invalidateDrawable(Drawable drawable) { 792 // Only invalidate this view if the passed in drawable is displayed within this view. If 793 // another drawable is passed in, have the parent view handle invalidation. 794 if (mDrawable == drawable) { 795 invalidate(); 796 } else { 797 super.invalidateDrawable(drawable); 798 } 799 } 800 801 /** 802 * Forces a fixed height for this view. 803 * 804 * @param fixedHeight The height. If {@code -1}, use the measured height. 805 */ setFixedHeight(int fixedHeight)806 public void setFixedHeight(int fixedHeight) { 807 final boolean adjustBounds = (fixedHeight != mFixedHeight); 808 mFixedHeight = fixedHeight; 809 setMeasuredDimension(getMeasuredWidth(), mFixedHeight); 810 if (adjustBounds) { 811 configureBounds(true); 812 requestLayout(); 813 } 814 } 815 816 /** 817 * Enable or disable image transformations. When transformations are enabled, this view 818 * consumes all touch events. 819 */ enableImageTransforms(boolean enable)820 public void enableImageTransforms(boolean enable) { 821 mTransformsEnabled = enable; 822 if (!mTransformsEnabled) { 823 resetTransformations(); 824 } 825 } 826 827 /** 828 * Configures the bounds of the photo. The photo will always be scaled to fit center. 829 */ configureBounds(boolean changed)830 private void configureBounds(boolean changed) { 831 if (mDrawable == null || !mHaveLayout) { 832 return; 833 } 834 final int dwidth = mDrawable.getIntrinsicWidth(); 835 final int dheight = mDrawable.getIntrinsicHeight(); 836 837 final int vwidth = getWidth(); 838 final int vheight = getHeight(); 839 840 final boolean fits = (dwidth < 0 || vwidth == dwidth) && 841 (dheight < 0 || vheight == dheight); 842 843 // We need to do the scaling ourself, so have the drawable use its native size. 844 mDrawable.setBounds(0, 0, dwidth, dheight); 845 846 // Create a matrix with the proper transforms 847 if (changed || (mMinScale == 0 && mDrawable != null && mHaveLayout)) { 848 generateMatrix(); 849 generateScale(); 850 } 851 852 if (fits || mMatrix.isIdentity()) { 853 // The bitmap fits exactly, no transform needed. 854 mDrawMatrix = null; 855 } else { 856 mDrawMatrix = mMatrix; 857 } 858 } 859 860 /** 861 * Generates the initial transformation matrix for drawing. Additionally, it sets the 862 * minimum and maximum scale values. 863 */ generateMatrix()864 private void generateMatrix() { 865 final int dwidth = mDrawable.getIntrinsicWidth(); 866 final int dheight = mDrawable.getIntrinsicHeight(); 867 868 final int vwidth = mAllowCrop ? sCropSize : getWidth(); 869 final int vheight = mAllowCrop ? sCropSize : getHeight(); 870 871 final boolean fits = (dwidth < 0 || vwidth == dwidth) && 872 (dheight < 0 || vheight == dheight); 873 874 if (fits && !mAllowCrop) { 875 mMatrix.reset(); 876 } else { 877 // Generate the required transforms for the photo 878 mTempSrc.set(0, 0, dwidth, dheight); 879 if (mAllowCrop) { 880 mTempDst.set(mCropRect); 881 } else { 882 mTempDst.set(0, 0, vwidth, vheight); 883 } 884 RectF scaledDestination = new RectF( 885 (vwidth / 2) - (dwidth * mMaxInitialScaleFactor / 2), 886 (vheight / 2) - (dheight * mMaxInitialScaleFactor / 2), 887 (vwidth / 2) + (dwidth * mMaxInitialScaleFactor / 2), 888 (vheight / 2) + (dheight * mMaxInitialScaleFactor / 2)); 889 if(mTempDst.contains(scaledDestination)) { 890 mMatrix.setRectToRect(mTempSrc, scaledDestination, Matrix.ScaleToFit.CENTER); 891 } else { 892 mMatrix.setRectToRect(mTempSrc, mTempDst, Matrix.ScaleToFit.CENTER); 893 } 894 } 895 mOriginalMatrix.set(mMatrix); 896 } 897 generateScale()898 private void generateScale() { 899 final int dwidth = mDrawable.getIntrinsicWidth(); 900 final int dheight = mDrawable.getIntrinsicHeight(); 901 902 final int vwidth = mAllowCrop ? getCropSize() : getWidth(); 903 final int vheight = mAllowCrop ? getCropSize() : getHeight(); 904 905 if (dwidth < vwidth && dheight < vheight && !mAllowCrop) { 906 mMinScale = 1.0f; 907 } else { 908 mMinScale = getScale(); 909 } 910 mMaxScale = Math.max(mMinScale * 4, 4); 911 } 912 913 /** 914 * @return the size of the crop regions 915 */ getCropSize()916 private int getCropSize() { 917 return mCropSize > 0 ? mCropSize : sCropSize; 918 } 919 920 /** 921 * Returns the currently applied scale factor for the image. 922 * <p> 923 * NOTE: This method overwrites any values stored in {@link #mValues}. 924 */ getScale()925 private float getScale() { 926 mMatrix.getValues(mValues); 927 return mValues[Matrix.MSCALE_X]; 928 } 929 930 /** 931 * Scales the image while keeping the aspect ratio. 932 * 933 * The given scale is capped so that the resulting scale of the image always remains 934 * between {@link #mMinScale} and {@link #mMaxScale}. 935 * 936 * If the image is smaller than the viewable area, it will be centered. 937 * 938 * @param newScale the new scale 939 * @param centerX the center horizontal point around which to scale 940 * @param centerY the center vertical point around which to scale 941 */ scale(float newScale, float centerX, float centerY)942 private void scale(float newScale, float centerX, float centerY) { 943 // rotate back to the original orientation 944 mMatrix.postRotate(-mRotation, getWidth() / 2, getHeight() / 2); 945 946 // ensure that mMinScale <= newScale <= mMaxScale 947 newScale = Math.max(newScale, mMinScale); 948 newScale = Math.min(newScale, mMaxScale * SCALE_OVERZOOM_FACTOR); 949 950 float currentScale = getScale(); 951 float factor = newScale / currentScale; 952 953 // apply the scale factor 954 mMatrix.postScale(factor, factor, centerX, centerY); 955 956 // re-apply any rotation 957 mMatrix.postRotate(mRotation, getWidth() / 2, getHeight() / 2); 958 959 invalidate(); 960 } 961 962 /** 963 * Translates the image. 964 * 965 * This method will not allow the image to be translated outside of the visible area. 966 * 967 * @param tx how many pixels to translate horizontally 968 * @param ty how many pixels to translate vertically 969 * @return result of the translation, represented as either {@link TRANSLATE_NONE}, 970 * {@link TRANSLATE_X_ONLY}, {@link TRANSLATE_Y_ONLY}, or {@link TRANSLATE_BOTH} 971 */ translate(float tx, float ty)972 private int translate(float tx, float ty) { 973 mTranslateRect.set(mTempSrc); 974 mMatrix.mapRect(mTranslateRect); 975 976 final float maxLeft = mAllowCrop ? mCropRect.left : 0.0f; 977 final float maxRight = mAllowCrop ? mCropRect.right : getWidth(); 978 float l = mTranslateRect.left; 979 float r = mTranslateRect.right; 980 981 final float translateX; 982 if (mAllowCrop) { 983 // If we're cropping, allow the image to scroll off the edge of the screen 984 translateX = Math.max(maxLeft - mTranslateRect.right, 985 Math.min(maxRight - mTranslateRect.left, tx)); 986 } else { 987 // Otherwise, ensure the image never leaves the screen 988 if (r - l < maxRight - maxLeft) { 989 translateX = maxLeft + ((maxRight - maxLeft) - (r + l)) / 2; 990 } else { 991 translateX = Math.max(maxRight - r, Math.min(maxLeft - l, tx)); 992 } 993 } 994 995 float maxTop = mAllowCrop ? mCropRect.top: 0.0f; 996 float maxBottom = mAllowCrop ? mCropRect.bottom : getHeight(); 997 float t = mTranslateRect.top; 998 float b = mTranslateRect.bottom; 999 1000 final float translateY; 1001 1002 if (mAllowCrop) { 1003 // If we're cropping, allow the image to scroll off the edge of the screen 1004 translateY = Math.max(maxTop - mTranslateRect.bottom, 1005 Math.min(maxBottom - mTranslateRect.top, ty)); 1006 } else { 1007 // Otherwise, ensure the image never leaves the screen 1008 if (b - t < maxBottom - maxTop) { 1009 translateY = maxTop + ((maxBottom - maxTop) - (b + t)) / 2; 1010 } else { 1011 translateY = Math.max(maxBottom - b, Math.min(maxTop - t, ty)); 1012 } 1013 } 1014 1015 // Do the translation 1016 mMatrix.postTranslate(translateX, translateY); 1017 invalidate(); 1018 1019 boolean didTranslateX = translateX == tx; 1020 boolean didTranslateY = translateY == ty; 1021 if (didTranslateX && didTranslateY) { 1022 return TRANSLATE_BOTH; 1023 } else if (didTranslateX) { 1024 return TRANSLATE_X_ONLY; 1025 } else if (didTranslateY) { 1026 return TRANSLATE_Y_ONLY; 1027 } 1028 return TRANSLATE_NONE; 1029 } 1030 1031 /** 1032 * Snaps the image so it touches all edges of the view. 1033 */ snap()1034 private void snap() { 1035 mTranslateRect.set(mTempSrc); 1036 mMatrix.mapRect(mTranslateRect); 1037 1038 // Determine how much to snap in the horizontal direction [if any] 1039 float maxLeft = mAllowCrop ? mCropRect.left : 0.0f; 1040 float maxRight = mAllowCrop ? mCropRect.right : getWidth(); 1041 float l = mTranslateRect.left; 1042 float r = mTranslateRect.right; 1043 1044 final float translateX; 1045 if (r - l < maxRight - maxLeft) { 1046 // Image is narrower than view; translate to the center of the view 1047 translateX = maxLeft + ((maxRight - maxLeft) - (r + l)) / 2; 1048 } else if (l > maxLeft) { 1049 // Image is off right-edge of screen; bring it into view 1050 translateX = maxLeft - l; 1051 } else if (r < maxRight) { 1052 // Image is off left-edge of screen; bring it into view 1053 translateX = maxRight - r; 1054 } else { 1055 translateX = 0.0f; 1056 } 1057 1058 // Determine how much to snap in the vertical direction [if any] 1059 float maxTop = mAllowCrop ? mCropRect.top : 0.0f; 1060 float maxBottom = mAllowCrop ? mCropRect.bottom : getHeight(); 1061 float t = mTranslateRect.top; 1062 float b = mTranslateRect.bottom; 1063 1064 final float translateY; 1065 if (b - t < maxBottom - maxTop) { 1066 // Image is shorter than view; translate to the bottom edge of the view 1067 translateY = maxTop + ((maxBottom - maxTop) - (b + t)) / 2; 1068 } else if (t > maxTop) { 1069 // Image is off bottom-edge of screen; bring it into view 1070 translateY = maxTop - t; 1071 } else if (b < maxBottom) { 1072 // Image is off top-edge of screen; bring it into view 1073 translateY = maxBottom - b; 1074 } else { 1075 translateY = 0.0f; 1076 } 1077 1078 if (Math.abs(translateX) > SNAP_THRESHOLD || Math.abs(translateY) > SNAP_THRESHOLD) { 1079 mSnapRunnable.start(translateX, translateY); 1080 } else { 1081 mMatrix.postTranslate(translateX, translateY); 1082 invalidate(); 1083 } 1084 } 1085 1086 /** 1087 * Rotates the image, either instantly or gradually 1088 * 1089 * @param degrees how many degrees to rotate the image, positive rotates clockwise 1090 * @param animate if {@code true}, animate during the rotation. Otherwise, snap rotate. 1091 */ rotate(float degrees, boolean animate)1092 private void rotate(float degrees, boolean animate) { 1093 if (animate) { 1094 mRotateRunnable.start(degrees); 1095 } else { 1096 mRotation += degrees; 1097 mMatrix.postRotate(degrees, getWidth() / 2, getHeight() / 2); 1098 invalidate(); 1099 } 1100 } 1101 1102 /** 1103 * Initializes the header and any static values 1104 */ initialize()1105 private void initialize() { 1106 Context context = getContext(); 1107 1108 if (!sInitialized) { 1109 sInitialized = true; 1110 1111 Resources resources = context.getApplicationContext().getResources(); 1112 1113 sCropSize = resources.getDimensionPixelSize(R.dimen.photo_crop_width); 1114 1115 sCropDimPaint = new Paint(); 1116 sCropDimPaint.setAntiAlias(true); 1117 sCropDimPaint.setColor(resources.getColor(R.color.photo_crop_dim_color)); 1118 sCropDimPaint.setStyle(Style.FILL); 1119 1120 sCropPaint = new Paint(); 1121 sCropPaint.setAntiAlias(true); 1122 sCropPaint.setColor(resources.getColor(R.color.photo_crop_highlight_color)); 1123 sCropPaint.setStyle(Style.STROKE); 1124 sCropPaint.setStrokeWidth(resources.getDimension(R.dimen.photo_crop_stroke_width)); 1125 1126 final ViewConfiguration configuration = ViewConfiguration.get(context); 1127 final int touchSlop = configuration.getScaledTouchSlop(); 1128 sTouchSlopSquare = touchSlop * touchSlop; 1129 } 1130 1131 mGestureDetector = new GestureDetectorCompat(context, this, null); 1132 mScaleGetureDetector = new ScaleGestureDetector(context, this); 1133 mQuickScaleEnabled = ScaleGestureDetectorCompat.isQuickScaleEnabled(mScaleGetureDetector); 1134 mScaleRunnable = new ScaleRunnable(this); 1135 mTranslateRunnable = new TranslateRunnable(this); 1136 mSnapRunnable = new SnapRunnable(this); 1137 mRotateRunnable = new RotateRunnable(this); 1138 } 1139 1140 /** 1141 * Runnable that animates an image scale operation. 1142 */ 1143 private static class ScaleRunnable implements Runnable { 1144 1145 private final PhotoView mHeader; 1146 1147 private float mCenterX; 1148 private float mCenterY; 1149 1150 private boolean mZoomingIn; 1151 1152 private float mTargetScale; 1153 private float mStartScale; 1154 private float mVelocity; 1155 private long mStartTime; 1156 1157 private boolean mRunning; 1158 private boolean mStop; 1159 ScaleRunnable(PhotoView header)1160 public ScaleRunnable(PhotoView header) { 1161 mHeader = header; 1162 } 1163 1164 /** 1165 * Starts the animation. There is no target scale bounds check. 1166 */ start(float startScale, float targetScale, float centerX, float centerY)1167 public boolean start(float startScale, float targetScale, float centerX, float centerY) { 1168 if (mRunning) { 1169 return false; 1170 } 1171 1172 mCenterX = centerX; 1173 mCenterY = centerY; 1174 1175 // Ensure the target scale is within the min/max bounds 1176 mTargetScale = targetScale; 1177 mStartTime = System.currentTimeMillis(); 1178 mStartScale = startScale; 1179 mZoomingIn = mTargetScale > mStartScale; 1180 mVelocity = (mTargetScale - mStartScale) / ZOOM_ANIMATION_DURATION; 1181 mRunning = true; 1182 mStop = false; 1183 mHeader.post(this); 1184 return true; 1185 } 1186 1187 /** 1188 * Stops the animation in place. It does not snap the image to its final zoom. 1189 */ stop()1190 public void stop() { 1191 mRunning = false; 1192 mStop = true; 1193 } 1194 1195 @Override run()1196 public void run() { 1197 if (mStop) { 1198 return; 1199 } 1200 1201 // Scale 1202 long now = System.currentTimeMillis(); 1203 long ellapsed = now - mStartTime; 1204 float newScale = (mStartScale + mVelocity * ellapsed); 1205 mHeader.scale(newScale, mCenterX, mCenterY); 1206 1207 // Stop when done 1208 if (newScale == mTargetScale || (mZoomingIn == (newScale > mTargetScale))) { 1209 mHeader.scale(mTargetScale, mCenterX, mCenterY); 1210 stop(); 1211 } 1212 1213 if (!mStop) { 1214 mHeader.post(this); 1215 } 1216 } 1217 } 1218 1219 /** 1220 * Runnable that animates an image translation operation. 1221 */ 1222 private static class TranslateRunnable implements Runnable { 1223 1224 private static final float DECELERATION_RATE = 1000f; 1225 private static final long NEVER = -1L; 1226 1227 private final PhotoView mHeader; 1228 1229 private float mVelocityX; 1230 private float mVelocityY; 1231 1232 private float mDecelerationX; 1233 private float mDecelerationY; 1234 1235 private long mLastRunTime; 1236 private boolean mRunning; 1237 private boolean mStop; 1238 TranslateRunnable(PhotoView header)1239 public TranslateRunnable(PhotoView header) { 1240 mLastRunTime = NEVER; 1241 mHeader = header; 1242 } 1243 1244 /** 1245 * Starts the animation. 1246 */ start(float velocityX, float velocityY)1247 public boolean start(float velocityX, float velocityY) { 1248 if (mRunning) { 1249 return false; 1250 } 1251 mLastRunTime = NEVER; 1252 mVelocityX = velocityX; 1253 mVelocityY = velocityY; 1254 1255 float angle = (float) Math.atan2(mVelocityY, mVelocityX); 1256 mDecelerationX = (float) (DECELERATION_RATE * Math.cos(angle)); 1257 mDecelerationY = (float) (DECELERATION_RATE * Math.sin(angle)); 1258 1259 mStop = false; 1260 mRunning = true; 1261 mHeader.post(this); 1262 return true; 1263 } 1264 1265 /** 1266 * Stops the animation in place. It does not snap the image to its final translation. 1267 */ stop()1268 public void stop() { 1269 mRunning = false; 1270 mStop = true; 1271 } 1272 1273 @Override run()1274 public void run() { 1275 // See if we were told to stop: 1276 if (mStop) { 1277 return; 1278 } 1279 1280 // Translate according to current velocities and time delta: 1281 long now = System.currentTimeMillis(); 1282 float delta = (mLastRunTime != NEVER) ? (now - mLastRunTime) / 1000f : 0f; 1283 final int translateResult = mHeader.translate(mVelocityX * delta, mVelocityY * delta); 1284 mLastRunTime = now; 1285 // Slow down: 1286 float slowDownX = mDecelerationX * delta; 1287 if (Math.abs(mVelocityX) > Math.abs(slowDownX)) { 1288 mVelocityX -= slowDownX; 1289 } else { 1290 mVelocityX = 0f; 1291 } 1292 float slowDownY = mDecelerationY * delta; 1293 if (Math.abs(mVelocityY) > Math.abs(slowDownY)) { 1294 mVelocityY -= slowDownY; 1295 } else { 1296 mVelocityY = 0f; 1297 } 1298 1299 // Stop when done 1300 if ((mVelocityX == 0f && mVelocityY == 0f) 1301 || translateResult == TRANSLATE_NONE) { 1302 stop(); 1303 mHeader.snap(); 1304 } else if (translateResult == TRANSLATE_X_ONLY) { 1305 mDecelerationX = (mVelocityX > 0) ? DECELERATION_RATE : -DECELERATION_RATE; 1306 mDecelerationY = 0; 1307 mVelocityY = 0f; 1308 } else if (translateResult == TRANSLATE_Y_ONLY) { 1309 mDecelerationX = 0; 1310 mDecelerationY = (mVelocityY > 0) ? DECELERATION_RATE : -DECELERATION_RATE; 1311 mVelocityX = 0f; 1312 } 1313 1314 // See if we need to continue flinging: 1315 if (mStop) { 1316 return; 1317 } 1318 mHeader.post(this); 1319 } 1320 } 1321 1322 /** 1323 * Runnable that animates an image translation operation. 1324 */ 1325 private static class SnapRunnable implements Runnable { 1326 1327 private static final long NEVER = -1L; 1328 1329 private final PhotoView mHeader; 1330 1331 private float mTranslateX; 1332 private float mTranslateY; 1333 1334 private long mStartRunTime; 1335 private boolean mRunning; 1336 private boolean mStop; 1337 SnapRunnable(PhotoView header)1338 public SnapRunnable(PhotoView header) { 1339 mStartRunTime = NEVER; 1340 mHeader = header; 1341 } 1342 1343 /** 1344 * Starts the animation. 1345 */ start(float translateX, float translateY)1346 public boolean start(float translateX, float translateY) { 1347 if (mRunning) { 1348 return false; 1349 } 1350 mStartRunTime = NEVER; 1351 mTranslateX = translateX; 1352 mTranslateY = translateY; 1353 mStop = false; 1354 mRunning = true; 1355 mHeader.postDelayed(this, SNAP_DELAY); 1356 return true; 1357 } 1358 1359 /** 1360 * Stops the animation in place. It does not snap the image to its final translation. 1361 */ stop()1362 public void stop() { 1363 mRunning = false; 1364 mStop = true; 1365 } 1366 1367 @Override run()1368 public void run() { 1369 // See if we were told to stop: 1370 if (mStop) { 1371 return; 1372 } 1373 1374 // Translate according to current velocities and time delta: 1375 long now = System.currentTimeMillis(); 1376 float delta = (mStartRunTime != NEVER) ? (now - mStartRunTime) : 0f; 1377 1378 if (mStartRunTime == NEVER) { 1379 mStartRunTime = now; 1380 } 1381 1382 float transX; 1383 float transY; 1384 if (delta >= SNAP_DURATION) { 1385 transX = mTranslateX; 1386 transY = mTranslateY; 1387 } else { 1388 transX = (mTranslateX / (SNAP_DURATION - delta)) * 10f; 1389 transY = (mTranslateY / (SNAP_DURATION - delta)) * 10f; 1390 if (Math.abs(transX) > Math.abs(mTranslateX) || Float.isNaN(transX)) { 1391 transX = mTranslateX; 1392 } 1393 if (Math.abs(transY) > Math.abs(mTranslateY) || Float.isNaN(transY)) { 1394 transY = mTranslateY; 1395 } 1396 } 1397 1398 mHeader.translate(transX, transY); 1399 mTranslateX -= transX; 1400 mTranslateY -= transY; 1401 1402 if (mTranslateX == 0 && mTranslateY == 0) { 1403 stop(); 1404 } 1405 1406 // See if we need to continue flinging: 1407 if (mStop) { 1408 return; 1409 } 1410 mHeader.post(this); 1411 } 1412 } 1413 1414 /** 1415 * Runnable that animates an image rotation operation. 1416 */ 1417 private static class RotateRunnable implements Runnable { 1418 1419 private static final long NEVER = -1L; 1420 1421 private final PhotoView mHeader; 1422 1423 private float mTargetRotation; 1424 private float mAppliedRotation; 1425 private float mVelocity; 1426 private long mLastRuntime; 1427 1428 private boolean mRunning; 1429 private boolean mStop; 1430 RotateRunnable(PhotoView header)1431 public RotateRunnable(PhotoView header) { 1432 mHeader = header; 1433 } 1434 1435 /** 1436 * Starts the animation. 1437 */ start(float rotation)1438 public void start(float rotation) { 1439 if (mRunning) { 1440 return; 1441 } 1442 1443 mTargetRotation = rotation; 1444 mVelocity = mTargetRotation / ROTATE_ANIMATION_DURATION; 1445 mAppliedRotation = 0f; 1446 mLastRuntime = NEVER; 1447 mStop = false; 1448 mRunning = true; 1449 mHeader.post(this); 1450 } 1451 1452 /** 1453 * Stops the animation in place. It does not snap the image to its final rotation. 1454 */ stop()1455 public void stop() { 1456 mRunning = false; 1457 mStop = true; 1458 } 1459 1460 @Override run()1461 public void run() { 1462 if (mStop) { 1463 return; 1464 } 1465 1466 if (mAppliedRotation != mTargetRotation) { 1467 long now = System.currentTimeMillis(); 1468 long delta = mLastRuntime != NEVER ? now - mLastRuntime : 0L; 1469 float rotationAmount = mVelocity * delta; 1470 if (mAppliedRotation < mTargetRotation 1471 && mAppliedRotation + rotationAmount > mTargetRotation 1472 || mAppliedRotation > mTargetRotation 1473 && mAppliedRotation + rotationAmount < mTargetRotation) { 1474 rotationAmount = mTargetRotation - mAppliedRotation; 1475 } 1476 mHeader.rotate(rotationAmount, false); 1477 mAppliedRotation += rotationAmount; 1478 if (mAppliedRotation == mTargetRotation) { 1479 stop(); 1480 } 1481 mLastRuntime = now; 1482 } 1483 1484 if (mStop) { 1485 return; 1486 } 1487 mHeader.post(this); 1488 } 1489 } 1490 setMaxInitialScale(float f)1491 public void setMaxInitialScale(float f) { 1492 mMaxInitialScaleFactor = f; 1493 } 1494 } 1495