1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.gallery3d.ui; 18 19 import com.android.gallery3d.R; 20 import com.android.gallery3d.app.GalleryActivity; 21 import com.android.gallery3d.common.Utils; 22 import com.android.gallery3d.data.Path; 23 import com.android.gallery3d.ui.PositionRepository.Position; 24 import com.android.gallery3d.util.GalleryUtils; 25 26 import android.content.Context; 27 import android.graphics.Bitmap; 28 import android.graphics.Color; 29 import android.graphics.RectF; 30 import android.os.Message; 31 import android.os.SystemClock; 32 import android.view.GestureDetector; 33 import android.view.MotionEvent; 34 import android.view.ScaleGestureDetector; 35 import android.widget.Scroller; 36 37 class PositionController { 38 private static final String TAG = "PositionController"; 39 private long mAnimationStartTime = NO_ANIMATION; 40 private static final long NO_ANIMATION = -1; 41 private static final long LAST_ANIMATION = -2; 42 43 private int mAnimationKind; 44 private float mAnimationDuration; 45 private final static int ANIM_KIND_SCROLL = 0; 46 private final static int ANIM_KIND_SCALE = 1; 47 private final static int ANIM_KIND_SNAPBACK = 2; 48 private final static int ANIM_KIND_SLIDE = 3; 49 private final static int ANIM_KIND_ZOOM = 4; 50 private final static int ANIM_KIND_FLING = 5; 51 52 // Animation time in milliseconds. The order must match ANIM_KIND_* above. 53 private final static int ANIM_TIME[] = { 54 0, // ANIM_KIND_SCROLL 55 50, // ANIM_KIND_SCALE 56 600, // ANIM_KIND_SNAPBACK 57 400, // ANIM_KIND_SLIDE 58 300, // ANIM_KIND_ZOOM 59 0, // ANIM_KIND_FLING (the duration is calculated dynamically) 60 }; 61 62 // We try to scale up the image to fill the screen. But in order not to 63 // scale too much for small icons, we limit the max up-scaling factor here. 64 private static final float SCALE_LIMIT = 4; 65 private static final int sHorizontalSlack = GalleryUtils.dpToPixel(12); 66 67 private PhotoView mViewer; 68 private EdgeView mEdgeView; 69 private int mImageW, mImageH; 70 private int mViewW, mViewH; 71 72 // The X, Y are the coordinate on bitmap which shows on the center of 73 // the view. We always keep the mCurrent{X,Y,Scale} sync with the actual 74 // values used currently. 75 private int mCurrentX, mFromX, mToX; 76 private int mCurrentY, mFromY, mToY; 77 private float mCurrentScale, mFromScale, mToScale; 78 79 // The focus point of the scaling gesture (in bitmap coordinates). 80 private int mFocusBitmapX; 81 private int mFocusBitmapY; 82 private boolean mInScale; 83 84 // The minimum and maximum scale we allow. 85 private float mScaleMin, mScaleMax = SCALE_LIMIT; 86 87 // This is used by the fling animation 88 private FlingScroller mScroller; 89 90 // The bound of the stable region, see the comments above 91 // calculateStableBound() for details. 92 private int mBoundLeft, mBoundRight, mBoundTop, mBoundBottom; 93 94 // Assume the image size is the same as view size before we know the actual 95 // size of image. 96 private boolean mUseViewSize = true; 97 98 private RectF mTempRect = new RectF(); 99 private float[] mTempPoints = new float[8]; 100 101 public PositionController(PhotoView viewer, Context context, 102 EdgeView edgeView) { 103 mViewer = viewer; 104 mEdgeView = edgeView; 105 mScroller = new FlingScroller(); 106 } 107 108 public void setImageSize(int width, int height) { 109 110 // If no image available, use view size. 111 if (width == 0 || height == 0) { 112 mUseViewSize = true; 113 mImageW = mViewW; 114 mImageH = mViewH; 115 mCurrentX = mImageW / 2; 116 mCurrentY = mImageH / 2; 117 mCurrentScale = 1; 118 mScaleMin = 1; 119 mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale); 120 return; 121 } 122 123 mUseViewSize = false; 124 125 float ratio = Math.min( 126 (float) mImageW / width, (float) mImageH / height); 127 128 // See the comment above translate() for details. 129 mCurrentX = translate(mCurrentX, mImageW, width, ratio); 130 mCurrentY = translate(mCurrentY, mImageH, height, ratio); 131 mCurrentScale = mCurrentScale * ratio; 132 133 mFromX = translate(mFromX, mImageW, width, ratio); 134 mFromY = translate(mFromY, mImageH, height, ratio); 135 mFromScale = mFromScale * ratio; 136 137 mToX = translate(mToX, mImageW, width, ratio); 138 mToY = translate(mToY, mImageH, height, ratio); 139 mToScale = mToScale * ratio; 140 141 mFocusBitmapX = translate(mFocusBitmapX, mImageW, width, ratio); 142 mFocusBitmapY = translate(mFocusBitmapY, mImageH, height, ratio); 143 144 mImageW = width; 145 mImageH = height; 146 147 mScaleMin = getMinimalScale(mImageW, mImageH); 148 149 // Start animation from the saved position if we have one. 150 Position position = mViewer.retrieveSavedPosition(); 151 if (position != null) { 152 // The animation starts from 240 pixels and centers at the image 153 // at the saved position. 154 float scale = 240f / Math.min(width, height); 155 mCurrentX = Math.round((mViewW / 2f - position.x) / scale) + mImageW / 2; 156 mCurrentY = Math.round((mViewH / 2f - position.y) / scale) + mImageH / 2; 157 mCurrentScale = scale; 158 mViewer.openAnimationStarted(); 159 startSnapback(); 160 } else if (mAnimationStartTime == NO_ANIMATION) { 161 mCurrentScale = Utils.clamp(mCurrentScale, mScaleMin, mScaleMax); 162 } 163 mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale); 164 } 165 166 public void zoomIn(float tapX, float tapY, float targetScale) { 167 if (targetScale > mScaleMax) targetScale = mScaleMax; 168 169 // Convert the tap position to image coordinate 170 int tempX = Math.round((tapX - mViewW / 2) / mCurrentScale + mCurrentX); 171 int tempY = Math.round((tapY - mViewH / 2) / mCurrentScale + mCurrentY); 172 173 calculateStableBound(targetScale); 174 int targetX = Utils.clamp(tempX, mBoundLeft, mBoundRight); 175 int targetY = Utils.clamp(tempY, mBoundTop, mBoundBottom); 176 177 startAnimation(targetX, targetY, targetScale, ANIM_KIND_ZOOM); 178 } 179 180 public void resetToFullView() { 181 startAnimation(mImageW / 2, mImageH / 2, mScaleMin, ANIM_KIND_ZOOM); 182 } 183 184 public float getMinimalScale(int w, int h) { 185 return Math.min(SCALE_LIMIT, 186 Math.min((float) mViewW / w, (float) mViewH / h)); 187 } 188 189 // Translate a coordinate on bitmap if the bitmap size changes. 190 // If the aspect ratio doesn't change, it's easy: 191 // 192 // r = w / w' (= h / h') 193 // x' = x / r 194 // y' = y / r 195 // 196 // However the aspect ratio may change. That happens when the user slides 197 // a image before it's loaded, we don't know the actual aspect ratio, so 198 // we will assume one. When we receive the actual bitmap size, we need to 199 // translate the coordinate from the old bitmap into the new bitmap. 200 // 201 // What we want to do is center the bitmap at the original position. 202 // 203 // ...+--+... 204 // . | | . 205 // . | | . 206 // ...+--+... 207 // 208 // First we scale down the new bitmap by a factor r = min(w/w', h/h'). 209 // Overlay it onto the original bitmap. Now (0, 0) of the old bitmap maps 210 // to (-(w-w'*r)/2 / r, -(h-h'*r)/2 / r) in the new bitmap. So (x, y) of 211 // the old bitmap maps to (x', y') in the new bitmap, where 212 // x' = (x-(w-w'*r)/2) / r = w'/2 + (x-w/2)/r 213 // y' = (y-(h-h'*r)/2) / r = h'/2 + (y-h/2)/r 214 private static int translate(int value, int size, int newSize, float ratio) { 215 return Math.round(newSize / 2f + (value - size / 2f) / ratio); 216 } 217 218 public void setViewSize(int viewW, int viewH) { 219 boolean needLayout = mViewW == 0 || mViewH == 0; 220 221 mViewW = viewW; 222 mViewH = viewH; 223 224 if (mUseViewSize) { 225 mImageW = viewW; 226 mImageH = viewH; 227 mCurrentX = mImageW / 2; 228 mCurrentY = mImageH / 2; 229 mCurrentScale = 1; 230 mScaleMin = 1; 231 mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale); 232 return; 233 } 234 235 // In most cases we want to keep the scaling factor intact when the 236 // view size changes. The cases we want to reset the scaling factor 237 // (to fit the view if possible) are (1) the scaling factor is too 238 // small for the new view size (2) the scaling factor has not been 239 // changed by the user. 240 boolean wasMinScale = (mCurrentScale == mScaleMin); 241 mScaleMin = getMinimalScale(mImageW, mImageH); 242 243 if (needLayout || mCurrentScale < mScaleMin || wasMinScale) { 244 mCurrentX = mImageW / 2; 245 mCurrentY = mImageH / 2; 246 mCurrentScale = mScaleMin; 247 mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale); 248 } 249 } 250 251 public void stopAnimation() { 252 mAnimationStartTime = NO_ANIMATION; 253 } 254 255 public void skipAnimation() { 256 if (mAnimationStartTime == NO_ANIMATION) return; 257 mAnimationStartTime = NO_ANIMATION; 258 mCurrentX = mToX; 259 mCurrentY = mToY; 260 mCurrentScale = mToScale; 261 } 262 263 public void beginScale(float focusX, float focusY) { 264 mInScale = true; 265 mFocusBitmapX = Math.round(mCurrentX + 266 (focusX - mViewW / 2f) / mCurrentScale); 267 mFocusBitmapY = Math.round(mCurrentY + 268 (focusY - mViewH / 2f) / mCurrentScale); 269 } 270 271 public void scaleBy(float s, float focusX, float focusY) { 272 273 // We want to keep the focus point (on the bitmap) the same as when 274 // we begin the scale guesture, that is, 275 // 276 // mCurrentX' + (focusX - mViewW / 2f) / scale = mFocusBitmapX 277 // 278 s *= getTargetScale(); 279 int x = Math.round(mFocusBitmapX - (focusX - mViewW / 2f) / s); 280 int y = Math.round(mFocusBitmapY - (focusY - mViewH / 2f) / s); 281 282 startAnimation(x, y, s, ANIM_KIND_SCALE); 283 } 284 285 public void endScale() { 286 mInScale = false; 287 startSnapbackIfNeeded(); 288 } 289 290 public float getCurrentScale() { 291 return mCurrentScale; 292 } 293 294 public boolean isAtMinimalScale() { 295 return isAlmostEquals(mCurrentScale, mScaleMin); 296 } 297 298 private static boolean isAlmostEquals(float a, float b) { 299 float diff = a - b; 300 return (diff < 0 ? -diff : diff) < 0.02f; 301 } 302 303 public void up() { 304 startSnapback(); 305 } 306 307 // |<--| (1/2) * mImageW 308 // +-------+-------+-------+ 309 // | | | | 310 // | | o | | 311 // | | | | 312 // +-------+-------+-------+ 313 // |<----------| (3/2) * mImageW 314 // Slide in the image from left or right. 315 // Precondition: mCurrentScale = 1 (mView{W|H} == mImage{W|H}). 316 // Sliding from left: mCurrentX = (1/2) * mImageW 317 // right: mCurrentX = (3/2) * mImageW 318 public void startSlideInAnimation(int direction) { 319 int fromX = (direction == PhotoView.TRANS_SLIDE_IN_LEFT) ? 320 mImageW / 2 : 3 * mImageW / 2; 321 mFromX = Math.round(fromX); 322 mFromY = Math.round(mImageH / 2f); 323 mCurrentX = mFromX; 324 mCurrentY = mFromY; 325 startAnimation( 326 mImageW / 2, mImageH / 2, mCurrentScale, ANIM_KIND_SLIDE); 327 } 328 329 public void startHorizontalSlide(int distance) { 330 scrollBy(distance, 0, ANIM_KIND_SLIDE); 331 } 332 333 private void scrollBy(float dx, float dy, int type) { 334 startAnimation(getTargetX() + Math.round(dx / mCurrentScale), 335 getTargetY() + Math.round(dy / mCurrentScale), 336 mCurrentScale, type); 337 } 338 339 public void startScroll(float dx, float dy, boolean hasNext, 340 boolean hasPrev) { 341 int x = getTargetX() + Math.round(dx / mCurrentScale); 342 int y = getTargetY() + Math.round(dy / mCurrentScale); 343 344 calculateStableBound(mCurrentScale); 345 346 // Vertical direction: If we have space to move in the vertical 347 // direction, we show the edge effect when scrolling reaches the edge. 348 if (mBoundTop != mBoundBottom) { 349 if (y < mBoundTop) { 350 mEdgeView.onPull(mBoundTop - y, EdgeView.TOP); 351 } else if (y > mBoundBottom) { 352 mEdgeView.onPull(y - mBoundBottom, EdgeView.BOTTOM); 353 } 354 } 355 356 y = Utils.clamp(y, mBoundTop, mBoundBottom); 357 358 // Horizontal direction: we show the edge effect when the scrolling 359 // tries to go left of the first image or go right of the last image. 360 if (!hasPrev && x < mBoundLeft) { 361 int pixels = Math.round((mBoundLeft - x) * mCurrentScale); 362 mEdgeView.onPull(pixels, EdgeView.LEFT); 363 x = mBoundLeft; 364 } else if (!hasNext && x > mBoundRight) { 365 int pixels = Math.round((x - mBoundRight) * mCurrentScale); 366 mEdgeView.onPull(pixels, EdgeView.RIGHT); 367 x = mBoundRight; 368 } 369 370 startAnimation(x, y, mCurrentScale, ANIM_KIND_SCROLL); 371 } 372 373 public boolean fling(float velocityX, float velocityY) { 374 // We only want to do fling when the picture is zoomed-in. 375 if (mImageW * mCurrentScale <= mViewW && 376 mImageH * mCurrentScale <= mViewH) { 377 return false; 378 } 379 380 calculateStableBound(mCurrentScale); 381 mScroller.fling(mCurrentX, mCurrentY, 382 Math.round(-velocityX / mCurrentScale), 383 Math.round(-velocityY / mCurrentScale), 384 mBoundLeft, mBoundRight, mBoundTop, mBoundBottom); 385 int targetX = mScroller.getFinalX(); 386 int targetY = mScroller.getFinalY(); 387 mAnimationDuration = mScroller.getDuration(); 388 startAnimation(targetX, targetY, mCurrentScale, ANIM_KIND_FLING); 389 return true; 390 } 391 392 private void startAnimation( 393 int targetX, int targetY, float scale, int kind) { 394 if (targetX == mCurrentX && targetY == mCurrentY 395 && scale == mCurrentScale) return; 396 397 mFromX = mCurrentX; 398 mFromY = mCurrentY; 399 mFromScale = mCurrentScale; 400 401 mToX = targetX; 402 mToY = targetY; 403 mToScale = Utils.clamp(scale, 0.6f * mScaleMin, 1.4f * mScaleMax); 404 405 // If the scaled height is smaller than the view height, 406 // force it to be in the center. 407 // (We do for height only, not width, because the user may 408 // want to scroll to the previous/next image.) 409 if (Math.floor(mImageH * mToScale) <= mViewH) { 410 mToY = mImageH / 2; 411 } 412 413 mAnimationStartTime = SystemClock.uptimeMillis(); 414 mAnimationKind = kind; 415 if (mAnimationKind != ANIM_KIND_FLING) { 416 mAnimationDuration = ANIM_TIME[mAnimationKind]; 417 } 418 if (advanceAnimation()) mViewer.invalidate(); 419 } 420 421 // Returns true if redraw is needed. 422 public boolean advanceAnimation() { 423 if (mAnimationStartTime == NO_ANIMATION) { 424 return false; 425 } else if (mAnimationStartTime == LAST_ANIMATION) { 426 mAnimationStartTime = NO_ANIMATION; 427 if (mViewer.isInTransition()) { 428 mViewer.notifyTransitionComplete(); 429 return false; 430 } else { 431 return startSnapbackIfNeeded(); 432 } 433 } 434 435 long now = SystemClock.uptimeMillis(); 436 float progress; 437 if (mAnimationDuration == 0) { 438 progress = 1; 439 } else { 440 progress = (now - mAnimationStartTime) / mAnimationDuration; 441 } 442 443 if (progress >= 1) { 444 progress = 1; 445 mCurrentX = mToX; 446 mCurrentY = mToY; 447 mCurrentScale = mToScale; 448 mAnimationStartTime = LAST_ANIMATION; 449 } else { 450 float f = 1 - progress; 451 switch (mAnimationKind) { 452 case ANIM_KIND_SCROLL: 453 case ANIM_KIND_FLING: 454 progress = 1 - f; // linear 455 break; 456 case ANIM_KIND_SCALE: 457 progress = 1 - f * f; // quadratic 458 break; 459 case ANIM_KIND_SNAPBACK: 460 case ANIM_KIND_ZOOM: 461 case ANIM_KIND_SLIDE: 462 progress = 1 - f * f * f * f * f; // x^5 463 break; 464 } 465 if (mAnimationKind == ANIM_KIND_FLING) { 466 flingInterpolate(progress); 467 } else { 468 linearInterpolate(progress); 469 } 470 } 471 mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale); 472 return true; 473 } 474 475 private void flingInterpolate(float progress) { 476 mScroller.computeScrollOffset(progress); 477 int oldX = mCurrentX; 478 int oldY = mCurrentY; 479 mCurrentX = mScroller.getCurrX(); 480 mCurrentY = mScroller.getCurrY(); 481 482 // Check if we hit the edges; show edge effects if we do. 483 if (oldX > mBoundLeft && mCurrentX == mBoundLeft) { 484 int v = Math.round(-mScroller.getCurrVelocityX() * mCurrentScale); 485 mEdgeView.onAbsorb(v, EdgeView.LEFT); 486 } else if (oldX < mBoundRight && mCurrentX == mBoundRight) { 487 int v = Math.round(mScroller.getCurrVelocityX() * mCurrentScale); 488 mEdgeView.onAbsorb(v, EdgeView.RIGHT); 489 } 490 491 if (oldY > mBoundTop && mCurrentY == mBoundTop) { 492 int v = Math.round(-mScroller.getCurrVelocityY() * mCurrentScale); 493 mEdgeView.onAbsorb(v, EdgeView.TOP); 494 } else if (oldY < mBoundBottom && mCurrentY == mBoundBottom) { 495 int v = Math.round(mScroller.getCurrVelocityY() * mCurrentScale); 496 mEdgeView.onAbsorb(v, EdgeView.BOTTOM); 497 } 498 } 499 500 // Interpolates mCurrent{X,Y,Scale} given the progress in [0, 1]. 501 private void linearInterpolate(float progress) { 502 // To linearly interpolate the position on view coordinates, we do the 503 // following steps: 504 // (1) convert a bitmap position (x, y) to view coordinates: 505 // from: (x - mFromX) * mFromScale + mViewW / 2 506 // to: (x - mToX) * mToScale + mViewW / 2 507 // (2) interpolate between the "from" and "to" coordinates: 508 // (x - mFromX) * mFromScale * (1 - p) + (x - mToX) * mToScale * p 509 // + mViewW / 2 510 // should be equal to 511 // (x - mCurrentX) * mCurrentScale + mViewW / 2 512 // (3) The x-related terms in the above equation can be removed because 513 // mFromScale * (1 - p) + ToScale * p = mCurrentScale 514 // (4) Solve for mCurrentX, we have mCurrentX = 515 // (mFromX * mFromScale * (1 - p) + mToX * mToScale * p) / mCurrentScale 516 float fromX = mFromX * mFromScale; 517 float toX = mToX * mToScale; 518 float currentX = fromX + progress * (toX - fromX); 519 520 float fromY = mFromY * mFromScale; 521 float toY = mToY * mToScale; 522 float currentY = fromY + progress * (toY - fromY); 523 524 mCurrentScale = mFromScale + progress * (mToScale - mFromScale); 525 mCurrentX = Math.round(currentX / mCurrentScale); 526 mCurrentY = Math.round(currentY / mCurrentScale); 527 } 528 529 // Returns true if redraw is needed. 530 private boolean startSnapbackIfNeeded() { 531 if (mAnimationStartTime != NO_ANIMATION) return false; 532 if (mInScale) return false; 533 if (mAnimationKind == ANIM_KIND_SCROLL && mViewer.isDown()) { 534 return false; 535 } 536 return startSnapback(); 537 } 538 539 public boolean startSnapback() { 540 boolean needAnimation = false; 541 float scale = mCurrentScale; 542 543 if (mCurrentScale < mScaleMin || mCurrentScale > mScaleMax) { 544 needAnimation = true; 545 scale = Utils.clamp(mCurrentScale, mScaleMin, mScaleMax); 546 } 547 548 calculateStableBound(scale, sHorizontalSlack); 549 int x = Utils.clamp(mCurrentX, mBoundLeft, mBoundRight); 550 int y = Utils.clamp(mCurrentY, mBoundTop, mBoundBottom); 551 552 if (mCurrentX != x || mCurrentY != y || mCurrentScale != scale) { 553 needAnimation = true; 554 } 555 556 if (needAnimation) { 557 startAnimation(x, y, scale, ANIM_KIND_SNAPBACK); 558 } 559 560 return needAnimation; 561 } 562 563 // Calculates the stable region of mCurrent{X/Y}, where "stable" means 564 // 565 // (1) If the dimension of scaled image >= view dimension, we will not 566 // see black region outside the image (at that dimension). 567 // (2) If the dimension of scaled image < view dimension, we will center 568 // the scaled image. 569 // 570 // We might temporarily go out of this stable during user interaction, 571 // but will "snap back" after user stops interaction. 572 // 573 // The results are stored in mBound{Left/Right/Top/Bottom}. 574 // 575 // An extra parameter "horizontalSlack" (which has the value of 0 usually) 576 // is used to extend the stable region by some pixels on each side 577 // horizontally. 578 private void calculateStableBound(float scale) { 579 calculateStableBound(scale, 0f); 580 } 581 582 private void calculateStableBound(float scale, float horizontalSlack) { 583 // The number of pixels between the center of the view 584 // and the edge when the edge is aligned. 585 mBoundLeft = (int) Math.ceil((mViewW - horizontalSlack) / (2 * scale)); 586 mBoundRight = mImageW - mBoundLeft; 587 mBoundTop = (int) Math.ceil(mViewH / (2 * scale)); 588 mBoundBottom = mImageH - mBoundTop; 589 590 // If the scaled height is smaller than the view height, 591 // force it to be in the center. 592 if (Math.floor(mImageH * scale) <= mViewH) { 593 mBoundTop = mBoundBottom = mImageH / 2; 594 } 595 596 // Same for width 597 if (Math.floor(mImageW * scale) <= mViewW) { 598 mBoundLeft = mBoundRight = mImageW / 2; 599 } 600 } 601 602 private boolean useCurrentValueAsTarget() { 603 return mAnimationStartTime == NO_ANIMATION || 604 mAnimationKind == ANIM_KIND_SNAPBACK || 605 mAnimationKind == ANIM_KIND_FLING; 606 } 607 608 private float getTargetScale() { 609 return useCurrentValueAsTarget() ? mCurrentScale : mToScale; 610 } 611 612 private int getTargetX() { 613 return useCurrentValueAsTarget() ? mCurrentX : mToX; 614 } 615 616 private int getTargetY() { 617 return useCurrentValueAsTarget() ? mCurrentY : mToY; 618 } 619 620 public RectF getImageBounds() { 621 float points[] = mTempPoints; 622 623 /* 624 * (p0,p1)----------(p2,p3) 625 * | | 626 * | | 627 * (p4,p5)----------(p6,p7) 628 */ 629 points[0] = points[4] = -mCurrentX; 630 points[1] = points[3] = -mCurrentY; 631 points[2] = points[6] = mImageW - mCurrentX; 632 points[5] = points[7] = mImageH - mCurrentY; 633 634 RectF rect = mTempRect; 635 rect.set(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, 636 Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY); 637 638 float scale = mCurrentScale; 639 float offsetX = mViewW / 2; 640 float offsetY = mViewH / 2; 641 for (int i = 0; i < 4; ++i) { 642 float x = points[i + i] * scale + offsetX; 643 float y = points[i + i + 1] * scale + offsetY; 644 if (x < rect.left) rect.left = x; 645 if (x > rect.right) rect.right = x; 646 if (y < rect.top) rect.top = y; 647 if (y > rect.bottom) rect.bottom = y; 648 } 649 return rect; 650 } 651 652 public int getImageWidth() { 653 return mImageW; 654 } 655 656 public int getImageHeight() { 657 return mImageH; 658 } 659 660 public boolean isAtLeftEdge() { 661 calculateStableBound(mCurrentScale); 662 return mCurrentX <= mBoundLeft; 663 } 664 665 public boolean isAtRightEdge() { 666 calculateStableBound(mCurrentScale); 667 return mCurrentX >= mBoundRight; 668 } 669 } 670