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