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