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 android.content.Context; 20 import android.graphics.Rect; 21 import android.util.Log; 22 import android.widget.OverScroller; 23 24 import com.android.gallery3d.common.Utils; 25 import com.android.gallery3d.util.GalleryUtils; 26 import com.android.gallery3d.util.RangeArray; 27 import com.android.gallery3d.util.RangeIntArray; 28 import com.android.gallery3d.ui.PhotoView.Size; 29 30 class PositionController { 31 private static final String TAG = "PositionController"; 32 33 public static final int IMAGE_AT_LEFT_EDGE = 1; 34 public static final int IMAGE_AT_RIGHT_EDGE = 2; 35 public static final int IMAGE_AT_TOP_EDGE = 4; 36 public static final int IMAGE_AT_BOTTOM_EDGE = 8; 37 38 public static final int CAPTURE_ANIMATION_TIME = 700; 39 public static final int SNAPBACK_ANIMATION_TIME = 600; 40 41 // Special values for animation time. 42 private static final long NO_ANIMATION = -1; 43 private static final long LAST_ANIMATION = -2; 44 45 private static final int ANIM_KIND_NONE = -1; 46 private static final int ANIM_KIND_SCROLL = 0; 47 private static final int ANIM_KIND_SCALE = 1; 48 private static final int ANIM_KIND_SNAPBACK = 2; 49 private static final int ANIM_KIND_SLIDE = 3; 50 private static final int ANIM_KIND_ZOOM = 4; 51 private static final int ANIM_KIND_OPENING = 5; 52 private static final int ANIM_KIND_FLING = 6; 53 private static final int ANIM_KIND_FLING_X = 7; 54 private static final int ANIM_KIND_DELETE = 8; 55 private static final int ANIM_KIND_CAPTURE = 9; 56 57 // Animation time in milliseconds. The order must match ANIM_KIND_* above. 58 // 59 // The values for ANIM_KIND_FLING_X does't matter because we use 60 // mFilmScroller.isFinished() to decide when to stop. We set it to 0 so it's 61 // faster for Animatable.advanceAnimation() to calculate the progress 62 // (always 1). 63 private static final int ANIM_TIME[] = { 64 0, // ANIM_KIND_SCROLL 65 0, // ANIM_KIND_SCALE 66 SNAPBACK_ANIMATION_TIME, // ANIM_KIND_SNAPBACK 67 400, // ANIM_KIND_SLIDE 68 300, // ANIM_KIND_ZOOM 69 400, // ANIM_KIND_OPENING 70 0, // ANIM_KIND_FLING (the duration is calculated dynamically) 71 0, // ANIM_KIND_FLING_X (see the comment above) 72 0, // ANIM_KIND_DELETE (the duration is calculated dynamically) 73 CAPTURE_ANIMATION_TIME, // ANIM_KIND_CAPTURE 74 }; 75 76 // We try to scale up the image to fill the screen. But in order not to 77 // scale too much for small icons, we limit the max up-scaling factor here. 78 private static final float SCALE_LIMIT = 4; 79 80 // For user's gestures, we give a temporary extra scaling range which goes 81 // above or below the usual scaling limits. 82 private static final float SCALE_MIN_EXTRA = 0.7f; 83 private static final float SCALE_MAX_EXTRA = 1.4f; 84 85 // Setting this true makes the extra scaling range permanent (until this is 86 // set to false again). 87 private boolean mExtraScalingRange = false; 88 89 // Film Mode v.s. Page Mode: in film mode we show smaller pictures. 90 private boolean mFilmMode = false; 91 92 // These are the limits for width / height of the picture in film mode. 93 private static final float FILM_MODE_PORTRAIT_HEIGHT = 0.48f; 94 private static final float FILM_MODE_PORTRAIT_WIDTH = 0.7f; 95 private static final float FILM_MODE_LANDSCAPE_HEIGHT = 0.7f; 96 private static final float FILM_MODE_LANDSCAPE_WIDTH = 0.7f; 97 98 // In addition to the focused box (index == 0). We also keep information 99 // about this many boxes on each side. 100 private static final int BOX_MAX = PhotoView.SCREEN_NAIL_MAX; 101 private static final int[] CENTER_OUT_INDEX = new int[2 * BOX_MAX + 1]; 102 103 private static final int IMAGE_GAP = GalleryUtils.dpToPixel(16); 104 private static final int HORIZONTAL_SLACK = GalleryUtils.dpToPixel(12); 105 106 // These are constants for the delete gesture. 107 private static final int DEFAULT_DELETE_ANIMATION_DURATION = 200; // ms 108 private static final int MAX_DELETE_ANIMATION_DURATION = 400; // ms 109 110 private Listener mListener; 111 private volatile Rect mOpenAnimationRect; 112 113 // Use a large enough value, so we won't see the gray shadown in the beginning. 114 private int mViewW = 1200; 115 private int mViewH = 1200; 116 117 // A scaling guesture is in progress. 118 private boolean mInScale; 119 // The focus point of the scaling gesture, relative to the center of the 120 // picture in bitmap pixels. 121 private float mFocusX, mFocusY; 122 123 // whether there is a previous/next picture. 124 private boolean mHasPrev, mHasNext; 125 126 // This is used by the fling animation (page mode). 127 private FlingScroller mPageScroller; 128 129 // This is used by the fling animation (film mode). 130 private OverScroller mFilmScroller; 131 132 // The bound of the stable region that the focused box can stay, see the 133 // comments above calculateStableBound() for details. 134 private int mBoundLeft, mBoundRight, mBoundTop, mBoundBottom; 135 136 // Constrained frame is a rectangle that the focused box should fit into if 137 // it is constrained. It has two effects: 138 // 139 // (1) In page mode, if the focused box is constrained, scaling for the 140 // focused box is adjusted to fit into the constrained frame, instead of the 141 // whole view. 142 // 143 // (2) In page mode, if the focused box is constrained, the mPlatform's 144 // default center (mDefaultX/Y) is moved to the center of the constrained 145 // frame, instead of the view center. 146 // 147 private Rect mConstrainedFrame = new Rect(); 148 149 // Whether the focused box is constrained. 150 // 151 // Our current program's first call to moveBox() sets constrained = true, so 152 // we set the initial value of this variable to true, and we will not see 153 // see unwanted transition animation. 154 private boolean mConstrained = true; 155 156 // 157 // ___________________________________________________________ 158 // | _____ _____ _____ _____ _____ | 159 // | | | | | | | | | | | | 160 // | | Box | | Box | | Box*| | Box | | Box | | 161 // | |_____|.....|_____|.....|_____|.....|_____|.....|_____| | 162 // | Gap Gap Gap Gap | 163 // |___________________________________________________________| 164 // 165 // <-- Platform --> 166 // 167 // The focused box (Box*) centers at mPlatform's (mCurrentX, mCurrentY) 168 169 private Platform mPlatform = new Platform(); 170 private RangeArray<Box> mBoxes = new RangeArray<Box>(-BOX_MAX, BOX_MAX); 171 // The gap at the right of a Box i is at index i. The gap at the left of a 172 // Box i is at index i - 1. 173 private RangeArray<Gap> mGaps = new RangeArray<Gap>(-BOX_MAX, BOX_MAX - 1); 174 private FilmRatio mFilmRatio = new FilmRatio(); 175 176 // These are only used during moveBox(). 177 private RangeArray<Box> mTempBoxes = new RangeArray<Box>(-BOX_MAX, BOX_MAX); 178 private RangeArray<Gap> mTempGaps = 179 new RangeArray<Gap>(-BOX_MAX, BOX_MAX - 1); 180 181 // The output of the PositionController. Available throught getPosition(). 182 private RangeArray<Rect> mRects = new RangeArray<Rect>(-BOX_MAX, BOX_MAX); 183 184 // The direction of a new picture should appear. New pictures pop from top 185 // if this value is true, or from bottom if this value is false. 186 boolean mPopFromTop; 187 188 public interface Listener { invalidate()189 void invalidate(); isHoldingDown()190 boolean isHoldingDown(); isHoldingDelete()191 boolean isHoldingDelete(); 192 193 // EdgeView onPull(int offset, int direction)194 void onPull(int offset, int direction); onRelease()195 void onRelease(); onAbsorb(int velocity, int direction)196 void onAbsorb(int velocity, int direction); 197 } 198 199 static { 200 // Initialize the CENTER_OUT_INDEX array. 201 // The array maps 0, 1, 2, 3, 4, ..., 2 * BOX_MAX 202 // to 0, 1, -1, 2, -2, ..., BOX_MAX, -BOX_MAX 203 for (int i = 0; i < CENTER_OUT_INDEX.length; i++) { 204 int j = (i + 1) / 2; 205 if ((i & 1) == 0) j = -j; 206 CENTER_OUT_INDEX[i] = j; 207 } 208 } 209 PositionController(Context context, Listener listener)210 public PositionController(Context context, Listener listener) { 211 mListener = listener; 212 mPageScroller = new FlingScroller(); 213 mFilmScroller = new OverScroller(context, 214 null /* default interpolator */, false /* no flywheel */); 215 216 // Initialize the areas. 217 initPlatform(); 218 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 219 mBoxes.put(i, new Box()); 220 initBox(i); 221 mRects.put(i, new Rect()); 222 } 223 for (int i = -BOX_MAX; i < BOX_MAX; i++) { 224 mGaps.put(i, new Gap()); 225 initGap(i); 226 } 227 } 228 setOpenAnimationRect(Rect r)229 public void setOpenAnimationRect(Rect r) { 230 mOpenAnimationRect = r; 231 } 232 setViewSize(int viewW, int viewH)233 public void setViewSize(int viewW, int viewH) { 234 if (viewW == mViewW && viewH == mViewH) return; 235 236 boolean wasMinimal = isAtMinimalScale(); 237 238 mViewW = viewW; 239 mViewH = viewH; 240 initPlatform(); 241 242 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 243 setBoxSize(i, viewW, viewH, true); 244 } 245 246 updateScaleAndGapLimit(); 247 248 // If the focused box was at minimal scale, we try to make it the 249 // minimal scale under the new view size. 250 if (wasMinimal) { 251 Box b = mBoxes.get(0); 252 b.mCurrentScale = b.mScaleMin; 253 } 254 255 // If we have the opening animation, do it. Otherwise go directly to the 256 // right position. 257 if (!startOpeningAnimationIfNeeded()) { 258 skipToFinalPosition(); 259 } 260 } 261 setConstrainedFrame(Rect cFrame)262 public void setConstrainedFrame(Rect cFrame) { 263 if (mConstrainedFrame.equals(cFrame)) return; 264 mConstrainedFrame.set(cFrame); 265 mPlatform.updateDefaultXY(); 266 updateScaleAndGapLimit(); 267 snapAndRedraw(); 268 } 269 forceImageSize(int index, Size s)270 public void forceImageSize(int index, Size s) { 271 if (s.width == 0 || s.height == 0) return; 272 Box b = mBoxes.get(index); 273 b.mImageW = s.width; 274 b.mImageH = s.height; 275 return; 276 } 277 setImageSize(int index, Size s, Rect cFrame)278 public void setImageSize(int index, Size s, Rect cFrame) { 279 if (s.width == 0 || s.height == 0) return; 280 281 boolean needUpdate = false; 282 if (cFrame != null && !mConstrainedFrame.equals(cFrame)) { 283 mConstrainedFrame.set(cFrame); 284 mPlatform.updateDefaultXY(); 285 needUpdate = true; 286 } 287 needUpdate |= setBoxSize(index, s.width, s.height, false); 288 289 if (!needUpdate) return; 290 updateScaleAndGapLimit(); 291 snapAndRedraw(); 292 } 293 294 // Returns false if the box size doesn't change. setBoxSize(int i, int width, int height, boolean isViewSize)295 private boolean setBoxSize(int i, int width, int height, boolean isViewSize) { 296 Box b = mBoxes.get(i); 297 boolean wasViewSize = b.mUseViewSize; 298 299 // If we already have an image size, we don't want to use the view size. 300 if (!wasViewSize && isViewSize) return false; 301 302 b.mUseViewSize = isViewSize; 303 304 if (width == b.mImageW && height == b.mImageH) { 305 return false; 306 } 307 308 // The ratio of the old size and the new size. 309 // 310 // If the aspect ratio changes, we don't know if it is because one side 311 // grows or the other side shrinks. Currently we just assume the view 312 // angle of the longer side doesn't change (so the aspect ratio change 313 // is because the view angle of the shorter side changes). This matches 314 // what camera preview does. 315 float ratio = (width > height) 316 ? (float) b.mImageW / width 317 : (float) b.mImageH / height; 318 319 b.mImageW = width; 320 b.mImageH = height; 321 322 // If this is the first time we receive an image size, we change the 323 // scale directly. Otherwise adjust the scales by a ratio, and snapback 324 // will animate the scale into the min/max bounds if necessary. 325 if (wasViewSize && !isViewSize) { 326 b.mCurrentScale = getMinimalScale(b); 327 b.mAnimationStartTime = NO_ANIMATION; 328 } else { 329 b.mCurrentScale *= ratio; 330 b.mFromScale *= ratio; 331 b.mToScale *= ratio; 332 } 333 334 if (i == 0) { 335 mFocusX /= ratio; 336 mFocusY /= ratio; 337 } 338 339 return true; 340 } 341 startOpeningAnimationIfNeeded()342 private boolean startOpeningAnimationIfNeeded() { 343 if (mOpenAnimationRect == null) return false; 344 Box b = mBoxes.get(0); 345 if (b.mUseViewSize) return false; 346 347 // Start animation from the saved rectangle if we have one. 348 Rect r = mOpenAnimationRect; 349 mOpenAnimationRect = null; 350 351 mPlatform.mCurrentX = r.centerX() - mViewW / 2; 352 b.mCurrentY = r.centerY() - mViewH / 2; 353 b.mCurrentScale = Math.max(r.width() / (float) b.mImageW, 354 r.height() / (float) b.mImageH); 355 startAnimation(mPlatform.mDefaultX, 0, b.mScaleMin, 356 ANIM_KIND_OPENING); 357 358 // Animate from large gaps for neighbor boxes to avoid them 359 // shown on the screen during opening animation. 360 for (int i = -1; i < 1; i++) { 361 Gap g = mGaps.get(i); 362 g.mCurrentGap = mViewW; 363 g.doAnimation(g.mDefaultSize, ANIM_KIND_OPENING); 364 } 365 366 return true; 367 } 368 setFilmMode(boolean enabled)369 public void setFilmMode(boolean enabled) { 370 if (enabled == mFilmMode) return; 371 mFilmMode = enabled; 372 373 mPlatform.updateDefaultXY(); 374 updateScaleAndGapLimit(); 375 stopAnimation(); 376 snapAndRedraw(); 377 } 378 setExtraScalingRange(boolean enabled)379 public void setExtraScalingRange(boolean enabled) { 380 if (mExtraScalingRange == enabled) return; 381 mExtraScalingRange = enabled; 382 if (!enabled) { 383 snapAndRedraw(); 384 } 385 } 386 387 // This should be called whenever the scale range of boxes or the default 388 // gap size may change. Currently this can happen due to change of view 389 // size, image size, mFilmMode, mConstrained, and mConstrainedFrame. updateScaleAndGapLimit()390 private void updateScaleAndGapLimit() { 391 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 392 Box b = mBoxes.get(i); 393 b.mScaleMin = getMinimalScale(b); 394 b.mScaleMax = getMaximalScale(b); 395 } 396 397 for (int i = -BOX_MAX; i < BOX_MAX; i++) { 398 Gap g = mGaps.get(i); 399 g.mDefaultSize = getDefaultGapSize(i); 400 } 401 } 402 403 // Returns the default gap size according the the size of the boxes around 404 // the gap and the current mode. getDefaultGapSize(int i)405 private int getDefaultGapSize(int i) { 406 if (mFilmMode) return IMAGE_GAP; 407 Box a = mBoxes.get(i); 408 Box b = mBoxes.get(i + 1); 409 return IMAGE_GAP + Math.max(gapToSide(a), gapToSide(b)); 410 } 411 412 // Here is how we layout the boxes in the page mode. 413 // 414 // previous current next 415 // ___________ ________________ __________ 416 // | _______ | | __________ | | ______ | 417 // | | | | | | right->| | | | | | 418 // | | |<-------->|<--left | | | | | | 419 // | |_______| | | | |__________| | | |______| | 420 // |___________| | |________________| |__________| 421 // | <--> gapToSide() 422 // | 423 // IMAGE_GAP + MAX(gapToSide(previous), gapToSide(current)) gapToSide(Box b)424 private int gapToSide(Box b) { 425 return (int) ((mViewW - getMinimalScale(b) * b.mImageW) / 2 + 0.5f); 426 } 427 428 // Stop all animations at where they are now. stopAnimation()429 public void stopAnimation() { 430 mPlatform.mAnimationStartTime = NO_ANIMATION; 431 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 432 mBoxes.get(i).mAnimationStartTime = NO_ANIMATION; 433 } 434 for (int i = -BOX_MAX; i < BOX_MAX; i++) { 435 mGaps.get(i).mAnimationStartTime = NO_ANIMATION; 436 } 437 } 438 skipAnimation()439 public void skipAnimation() { 440 if (mPlatform.mAnimationStartTime != NO_ANIMATION) { 441 mPlatform.mCurrentX = mPlatform.mToX; 442 mPlatform.mCurrentY = mPlatform.mToY; 443 mPlatform.mAnimationStartTime = NO_ANIMATION; 444 } 445 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 446 Box b = mBoxes.get(i); 447 if (b.mAnimationStartTime == NO_ANIMATION) continue; 448 b.mCurrentY = b.mToY; 449 b.mCurrentScale = b.mToScale; 450 b.mAnimationStartTime = NO_ANIMATION; 451 } 452 for (int i = -BOX_MAX; i < BOX_MAX; i++) { 453 Gap g = mGaps.get(i); 454 if (g.mAnimationStartTime == NO_ANIMATION) continue; 455 g.mCurrentGap = g.mToGap; 456 g.mAnimationStartTime = NO_ANIMATION; 457 } 458 redraw(); 459 } 460 snapback()461 public void snapback() { 462 snapAndRedraw(); 463 } 464 skipToFinalPosition()465 public void skipToFinalPosition() { 466 stopAnimation(); 467 snapAndRedraw(); 468 skipAnimation(); 469 } 470 471 //////////////////////////////////////////////////////////////////////////// 472 // Start an animations for the focused box 473 //////////////////////////////////////////////////////////////////////////// 474 zoomIn(float tapX, float tapY, float targetScale)475 public void zoomIn(float tapX, float tapY, float targetScale) { 476 tapX -= mViewW / 2; 477 tapY -= mViewH / 2; 478 Box b = mBoxes.get(0); 479 480 // Convert the tap position to distance to center in bitmap coordinates 481 float tempX = (tapX - mPlatform.mCurrentX) / b.mCurrentScale; 482 float tempY = (tapY - b.mCurrentY) / b.mCurrentScale; 483 484 int x = (int) (-tempX * targetScale + 0.5f); 485 int y = (int) (-tempY * targetScale + 0.5f); 486 487 calculateStableBound(targetScale); 488 int targetX = Utils.clamp(x, mBoundLeft, mBoundRight); 489 int targetY = Utils.clamp(y, mBoundTop, mBoundBottom); 490 targetScale = Utils.clamp(targetScale, b.mScaleMin, b.mScaleMax); 491 492 startAnimation(targetX, targetY, targetScale, ANIM_KIND_ZOOM); 493 } 494 resetToFullView()495 public void resetToFullView() { 496 Box b = mBoxes.get(0); 497 startAnimation(mPlatform.mDefaultX, 0, b.mScaleMin, ANIM_KIND_ZOOM); 498 } 499 beginScale(float focusX, float focusY)500 public void beginScale(float focusX, float focusY) { 501 focusX -= mViewW / 2; 502 focusY -= mViewH / 2; 503 Box b = mBoxes.get(0); 504 Platform p = mPlatform; 505 mInScale = true; 506 mFocusX = (int) ((focusX - p.mCurrentX) / b.mCurrentScale + 0.5f); 507 mFocusY = (int) ((focusY - b.mCurrentY) / b.mCurrentScale + 0.5f); 508 } 509 510 // Scales the image by the given factor. 511 // Returns an out-of-range indicator: 512 // 1 if the intended scale is too large for the stable range. 513 // 0 if the intended scale is in the stable range. 514 // -1 if the intended scale is too small for the stable range. scaleBy(float s, float focusX, float focusY)515 public int scaleBy(float s, float focusX, float focusY) { 516 focusX -= mViewW / 2; 517 focusY -= mViewH / 2; 518 Box b = mBoxes.get(0); 519 Platform p = mPlatform; 520 521 // We want to keep the focus point (on the bitmap) the same as when we 522 // begin the scale guesture, that is, 523 // 524 // (focusX' - currentX') / scale' = (focusX - currentX) / scale 525 // 526 s = b.clampScale(s * getTargetScale(b)); 527 int x = mFilmMode ? p.mCurrentX : (int) (focusX - s * mFocusX + 0.5f); 528 int y = mFilmMode ? b.mCurrentY : (int) (focusY - s * mFocusY + 0.5f); 529 startAnimation(x, y, s, ANIM_KIND_SCALE); 530 if (s < b.mScaleMin) return -1; 531 if (s > b.mScaleMax) return 1; 532 return 0; 533 } 534 endScale()535 public void endScale() { 536 mInScale = false; 537 snapAndRedraw(); 538 } 539 540 // Slide the focused box to the center of the view. startHorizontalSlide()541 public void startHorizontalSlide() { 542 Box b = mBoxes.get(0); 543 startAnimation(mPlatform.mDefaultX, 0, b.mScaleMin, ANIM_KIND_SLIDE); 544 } 545 546 // Slide the focused box to the center of the view with the capture 547 // animation. In addition to the sliding, the animation will also scale the 548 // the focused box, the specified neighbor box, and the gap between the 549 // two. The specified offset should be 1 or -1. startCaptureAnimationSlide(int offset)550 public void startCaptureAnimationSlide(int offset) { 551 Box b = mBoxes.get(0); 552 Box n = mBoxes.get(offset); // the neighbor box 553 Gap g = mGaps.get(offset); // the gap between the two boxes 554 555 mPlatform.doAnimation(mPlatform.mDefaultX, mPlatform.mDefaultY, 556 ANIM_KIND_CAPTURE); 557 b.doAnimation(0, b.mScaleMin, ANIM_KIND_CAPTURE); 558 n.doAnimation(0, n.mScaleMin, ANIM_KIND_CAPTURE); 559 g.doAnimation(g.mDefaultSize, ANIM_KIND_CAPTURE); 560 redraw(); 561 } 562 563 // Only allow scrolling when we are not currently in an animation or we 564 // are in some animation with can be interrupted. canScroll()565 private boolean canScroll() { 566 Box b = mBoxes.get(0); 567 if (b.mAnimationStartTime == NO_ANIMATION) return true; 568 switch (b.mAnimationKind) { 569 case ANIM_KIND_SCROLL: 570 case ANIM_KIND_FLING: 571 case ANIM_KIND_FLING_X: 572 return true; 573 } 574 return false; 575 } 576 scrollPage(int dx, int dy)577 public void scrollPage(int dx, int dy) { 578 if (!canScroll()) return; 579 580 Box b = mBoxes.get(0); 581 Platform p = mPlatform; 582 583 calculateStableBound(b.mCurrentScale); 584 585 int x = p.mCurrentX + dx; 586 int y = b.mCurrentY + dy; 587 588 // Vertical direction: If we have space to move in the vertical 589 // direction, we show the edge effect when scrolling reaches the edge. 590 if (mBoundTop != mBoundBottom) { 591 if (y < mBoundTop) { 592 mListener.onPull(mBoundTop - y, EdgeView.BOTTOM); 593 } else if (y > mBoundBottom) { 594 mListener.onPull(y - mBoundBottom, EdgeView.TOP); 595 } 596 } 597 598 y = Utils.clamp(y, mBoundTop, mBoundBottom); 599 600 // Horizontal direction: we show the edge effect when the scrolling 601 // tries to go left of the first image or go right of the last image. 602 if (!mHasPrev && x > mBoundRight) { 603 int pixels = x - mBoundRight; 604 mListener.onPull(pixels, EdgeView.LEFT); 605 x = mBoundRight; 606 } else if (!mHasNext && x < mBoundLeft) { 607 int pixels = mBoundLeft - x; 608 mListener.onPull(pixels, EdgeView.RIGHT); 609 x = mBoundLeft; 610 } 611 612 startAnimation(x, y, b.mCurrentScale, ANIM_KIND_SCROLL); 613 } 614 scrollFilmX(int dx)615 public void scrollFilmX(int dx) { 616 if (!canScroll()) return; 617 618 Box b = mBoxes.get(0); 619 Platform p = mPlatform; 620 621 // Only allow scrolling when we are not currently in an animation or we 622 // are in some animation with can be interrupted. 623 if (b.mAnimationStartTime != NO_ANIMATION) { 624 switch (b.mAnimationKind) { 625 case ANIM_KIND_SCROLL: 626 case ANIM_KIND_FLING: 627 case ANIM_KIND_FLING_X: 628 break; 629 default: 630 return; 631 } 632 } 633 634 int x = p.mCurrentX + dx; 635 636 // Horizontal direction: we show the edge effect when the scrolling 637 // tries to go left of the first image or go right of the last image. 638 x -= mPlatform.mDefaultX; 639 if (!mHasPrev && x > 0) { 640 mListener.onPull(x, EdgeView.LEFT); 641 x = 0; 642 } else if (!mHasNext && x < 0) { 643 mListener.onPull(-x, EdgeView.RIGHT); 644 x = 0; 645 } 646 x += mPlatform.mDefaultX; 647 startAnimation(x, b.mCurrentY, b.mCurrentScale, ANIM_KIND_SCROLL); 648 } 649 scrollFilmY(int boxIndex, int dy)650 public void scrollFilmY(int boxIndex, int dy) { 651 if (!canScroll()) return; 652 653 Box b = mBoxes.get(boxIndex); 654 int y = b.mCurrentY + dy; 655 b.doAnimation(y, b.mCurrentScale, ANIM_KIND_SCROLL); 656 redraw(); 657 } 658 flingPage(int velocityX, int velocityY)659 public boolean flingPage(int velocityX, int velocityY) { 660 Box b = mBoxes.get(0); 661 Platform p = mPlatform; 662 663 // We only want to do fling when the picture is zoomed-in. 664 if (viewWiderThanScaledImage(b.mCurrentScale) && 665 viewTallerThanScaledImage(b.mCurrentScale)) { 666 return false; 667 } 668 669 // We only allow flinging in the directions where it won't go over the 670 // picture. 671 int edges = getImageAtEdges(); 672 if ((velocityX > 0 && (edges & IMAGE_AT_LEFT_EDGE) != 0) || 673 (velocityX < 0 && (edges & IMAGE_AT_RIGHT_EDGE) != 0)) { 674 velocityX = 0; 675 } 676 if ((velocityY > 0 && (edges & IMAGE_AT_TOP_EDGE) != 0) || 677 (velocityY < 0 && (edges & IMAGE_AT_BOTTOM_EDGE) != 0)) { 678 velocityY = 0; 679 } 680 681 if (velocityX == 0 && velocityY == 0) return false; 682 683 mPageScroller.fling(p.mCurrentX, b.mCurrentY, velocityX, velocityY, 684 mBoundLeft, mBoundRight, mBoundTop, mBoundBottom); 685 int targetX = mPageScroller.getFinalX(); 686 int targetY = mPageScroller.getFinalY(); 687 ANIM_TIME[ANIM_KIND_FLING] = mPageScroller.getDuration(); 688 return startAnimation(targetX, targetY, b.mCurrentScale, ANIM_KIND_FLING); 689 } 690 flingFilmX(int velocityX)691 public boolean flingFilmX(int velocityX) { 692 if (velocityX == 0) return false; 693 694 Box b = mBoxes.get(0); 695 Platform p = mPlatform; 696 697 // If we are already at the edge, don't start the fling. 698 int defaultX = p.mDefaultX; 699 if ((!mHasPrev && p.mCurrentX >= defaultX) 700 || (!mHasNext && p.mCurrentX <= defaultX)) { 701 return false; 702 } 703 704 mFilmScroller.fling(p.mCurrentX, 0, velocityX, 0, 705 Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0); 706 int targetX = mFilmScroller.getFinalX(); 707 return startAnimation( 708 targetX, b.mCurrentY, b.mCurrentScale, ANIM_KIND_FLING_X); 709 } 710 711 // Moves the specified box out of screen. If velocityY is 0, a default 712 // velocity is used. Returns the time for the duration, or -1 if we cannot 713 // not do the animation. flingFilmY(int boxIndex, int velocityY)714 public int flingFilmY(int boxIndex, int velocityY) { 715 Box b = mBoxes.get(boxIndex); 716 717 // Calculate targetY 718 int h = heightOf(b); 719 int targetY; 720 int FUZZY = 3; // TODO: figure out why this is needed. 721 if (velocityY < 0 || (velocityY == 0 && b.mCurrentY <= 0)) { 722 targetY = -mViewH / 2 - (h + 1) / 2 - FUZZY; 723 } else { 724 targetY = (mViewH + 1) / 2 + h / 2 + FUZZY; 725 } 726 727 // Calculate duration 728 int duration; 729 if (velocityY != 0) { 730 duration = (int) (Math.abs(targetY - b.mCurrentY) * 1000f 731 / Math.abs(velocityY)); 732 duration = Math.min(MAX_DELETE_ANIMATION_DURATION, duration); 733 } else { 734 duration = DEFAULT_DELETE_ANIMATION_DURATION; 735 } 736 737 // Start animation 738 ANIM_TIME[ANIM_KIND_DELETE] = duration; 739 if (b.doAnimation(targetY, b.mCurrentScale, ANIM_KIND_DELETE)) { 740 redraw(); 741 return duration; 742 } 743 return -1; 744 } 745 746 // Returns the index of the box which contains the given point (x, y) 747 // Returns Integer.MAX_VALUE if there is no hit. There may be more than 748 // one box contains the given point, and we want to give priority to the 749 // one closer to the focused index (0). hitTest(int x, int y)750 public int hitTest(int x, int y) { 751 for (int i = 0; i < 2 * BOX_MAX + 1; i++) { 752 int j = CENTER_OUT_INDEX[i]; 753 Rect r = mRects.get(j); 754 if (r.contains(x, y)) { 755 return j; 756 } 757 } 758 759 return Integer.MAX_VALUE; 760 } 761 762 //////////////////////////////////////////////////////////////////////////// 763 // Redraw 764 // 765 // If a method changes box positions directly, redraw() 766 // should be called. 767 // 768 // If a method may also cause a snapback to happen, snapAndRedraw() should 769 // be called. 770 // 771 // If a method starts an animation to change the position of focused box, 772 // startAnimation() should be called. 773 // 774 // If time advances to change the box position, advanceAnimation() should 775 // be called. 776 //////////////////////////////////////////////////////////////////////////// redraw()777 private void redraw() { 778 layoutAndSetPosition(); 779 mListener.invalidate(); 780 } 781 snapAndRedraw()782 private void snapAndRedraw() { 783 mPlatform.startSnapback(); 784 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 785 mBoxes.get(i).startSnapback(); 786 } 787 for (int i = -BOX_MAX; i < BOX_MAX; i++) { 788 mGaps.get(i).startSnapback(); 789 } 790 mFilmRatio.startSnapback(); 791 redraw(); 792 } 793 startAnimation(int targetX, int targetY, float targetScale, int kind)794 private boolean startAnimation(int targetX, int targetY, float targetScale, 795 int kind) { 796 boolean changed = false; 797 changed |= mPlatform.doAnimation(targetX, mPlatform.mDefaultY, kind); 798 changed |= mBoxes.get(0).doAnimation(targetY, targetScale, kind); 799 if (changed) redraw(); 800 return changed; 801 } 802 advanceAnimation()803 public void advanceAnimation() { 804 boolean changed = false; 805 changed |= mPlatform.advanceAnimation(); 806 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 807 changed |= mBoxes.get(i).advanceAnimation(); 808 } 809 for (int i = -BOX_MAX; i < BOX_MAX; i++) { 810 changed |= mGaps.get(i).advanceAnimation(); 811 } 812 changed |= mFilmRatio.advanceAnimation(); 813 if (changed) redraw(); 814 } 815 inOpeningAnimation()816 public boolean inOpeningAnimation() { 817 return (mPlatform.mAnimationKind == ANIM_KIND_OPENING && 818 mPlatform.mAnimationStartTime != NO_ANIMATION) || 819 (mBoxes.get(0).mAnimationKind == ANIM_KIND_OPENING && 820 mBoxes.get(0).mAnimationStartTime != NO_ANIMATION); 821 } 822 823 //////////////////////////////////////////////////////////////////////////// 824 // Layout 825 //////////////////////////////////////////////////////////////////////////// 826 827 // Returns the display width of this box. widthOf(Box b)828 private int widthOf(Box b) { 829 return (int) (b.mImageW * b.mCurrentScale + 0.5f); 830 } 831 832 // Returns the display height of this box. heightOf(Box b)833 private int heightOf(Box b) { 834 return (int) (b.mImageH * b.mCurrentScale + 0.5f); 835 } 836 837 // Returns the display width of this box, using the given scale. widthOf(Box b, float scale)838 private int widthOf(Box b, float scale) { 839 return (int) (b.mImageW * scale + 0.5f); 840 } 841 842 // Returns the display height of this box, using the given scale. heightOf(Box b, float scale)843 private int heightOf(Box b, float scale) { 844 return (int) (b.mImageH * scale + 0.5f); 845 } 846 847 // Convert the information in mPlatform and mBoxes to mRects, so the user 848 // can get the position of each box by getPosition(). 849 // 850 // Note we go from center-out because each box's X coordinate 851 // is relative to its anchor box (except the focused box). layoutAndSetPosition()852 private void layoutAndSetPosition() { 853 for (int i = 0; i < 2 * BOX_MAX + 1; i++) { 854 convertBoxToRect(CENTER_OUT_INDEX[i]); 855 } 856 //dumpState(); 857 } 858 dumpState()859 private void dumpState() { 860 for (int i = -BOX_MAX; i < BOX_MAX; i++) { 861 Log.d(TAG, "Gap " + i + ": " + mGaps.get(i).mCurrentGap); 862 } 863 864 for (int i = 0; i < 2 * BOX_MAX + 1; i++) { 865 dumpRect(CENTER_OUT_INDEX[i]); 866 } 867 868 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 869 for (int j = i + 1; j <= BOX_MAX; j++) { 870 if (Rect.intersects(mRects.get(i), mRects.get(j))) { 871 Log.d(TAG, "rect " + i + " and rect " + j + "intersects!"); 872 } 873 } 874 } 875 } 876 dumpRect(int i)877 private void dumpRect(int i) { 878 StringBuilder sb = new StringBuilder(); 879 Rect r = mRects.get(i); 880 sb.append("Rect " + i + ":"); 881 sb.append("("); 882 sb.append(r.centerX()); 883 sb.append(","); 884 sb.append(r.centerY()); 885 sb.append(") ["); 886 sb.append(r.width()); 887 sb.append("x"); 888 sb.append(r.height()); 889 sb.append("]"); 890 Log.d(TAG, sb.toString()); 891 } 892 convertBoxToRect(int i)893 private void convertBoxToRect(int i) { 894 Box b = mBoxes.get(i); 895 Rect r = mRects.get(i); 896 int y = b.mCurrentY + mPlatform.mCurrentY + mViewH / 2; 897 int w = widthOf(b); 898 int h = heightOf(b); 899 if (i == 0) { 900 int x = mPlatform.mCurrentX + mViewW / 2; 901 r.left = x - w / 2; 902 r.right = r.left + w; 903 } else if (i > 0) { 904 Rect a = mRects.get(i - 1); 905 Gap g = mGaps.get(i - 1); 906 r.left = a.right + g.mCurrentGap; 907 r.right = r.left + w; 908 } else { // i < 0 909 Rect a = mRects.get(i + 1); 910 Gap g = mGaps.get(i); 911 r.right = a.left - g.mCurrentGap; 912 r.left = r.right - w; 913 } 914 r.top = y - h / 2; 915 r.bottom = r.top + h; 916 } 917 918 // Returns the position of a box. getPosition(int index)919 public Rect getPosition(int index) { 920 return mRects.get(index); 921 } 922 923 //////////////////////////////////////////////////////////////////////////// 924 // Box management 925 //////////////////////////////////////////////////////////////////////////// 926 927 // Initialize the platform to be at the view center. initPlatform()928 private void initPlatform() { 929 mPlatform.updateDefaultXY(); 930 mPlatform.mCurrentX = mPlatform.mDefaultX; 931 mPlatform.mCurrentY = mPlatform.mDefaultY; 932 mPlatform.mAnimationStartTime = NO_ANIMATION; 933 } 934 935 // Initialize a box to have the size of the view. initBox(int index)936 private void initBox(int index) { 937 Box b = mBoxes.get(index); 938 b.mImageW = mViewW; 939 b.mImageH = mViewH; 940 b.mUseViewSize = true; 941 b.mScaleMin = getMinimalScale(b); 942 b.mScaleMax = getMaximalScale(b); 943 b.mCurrentY = 0; 944 b.mCurrentScale = b.mScaleMin; 945 b.mAnimationStartTime = NO_ANIMATION; 946 b.mAnimationKind = ANIM_KIND_NONE; 947 } 948 949 // Initialize a box to a given size. initBox(int index, Size size)950 private void initBox(int index, Size size) { 951 if (size.width == 0 || size.height == 0) { 952 initBox(index); 953 return; 954 } 955 Box b = mBoxes.get(index); 956 b.mImageW = size.width; 957 b.mImageH = size.height; 958 b.mUseViewSize = false; 959 b.mScaleMin = getMinimalScale(b); 960 b.mScaleMax = getMaximalScale(b); 961 b.mCurrentY = 0; 962 b.mCurrentScale = b.mScaleMin; 963 b.mAnimationStartTime = NO_ANIMATION; 964 b.mAnimationKind = ANIM_KIND_NONE; 965 } 966 967 // Initialize a gap. This can only be called after the boxes around the gap 968 // has been initialized. initGap(int index)969 private void initGap(int index) { 970 Gap g = mGaps.get(index); 971 g.mDefaultSize = getDefaultGapSize(index); 972 g.mCurrentGap = g.mDefaultSize; 973 g.mAnimationStartTime = NO_ANIMATION; 974 } 975 initGap(int index, int size)976 private void initGap(int index, int size) { 977 Gap g = mGaps.get(index); 978 g.mDefaultSize = getDefaultGapSize(index); 979 g.mCurrentGap = size; 980 g.mAnimationStartTime = NO_ANIMATION; 981 } 982 debugMoveBox(int fromIndex[])983 private void debugMoveBox(int fromIndex[]) { 984 StringBuilder s = new StringBuilder("moveBox:"); 985 for (int i = 0; i < fromIndex.length; i++) { 986 int j = fromIndex[i]; 987 if (j == Integer.MAX_VALUE) { 988 s.append(" N"); 989 } else { 990 s.append(" "); 991 s.append(fromIndex[i]); 992 } 993 } 994 Log.d(TAG, s.toString()); 995 } 996 997 // Move the boxes: it may indicate focus change, box deleted, box appearing, 998 // box reordered, etc. 999 // 1000 // Each element in the fromIndex array indicates where each box was in the 1001 // old array. If the value is Integer.MAX_VALUE (pictured as N below), it 1002 // means the box is new. 1003 // 1004 // For example: 1005 // N N N N N N N -- all new boxes 1006 // -3 -2 -1 0 1 2 3 -- nothing changed 1007 // -2 -1 0 1 2 3 N -- focus goes to the next box 1008 // N -3 -2 -1 0 1 2 -- focuse goes to the previous box 1009 // -3 -2 -1 1 2 3 N -- the focused box was deleted. 1010 // 1011 // hasPrev/hasNext indicates if there are previous/next boxes for the 1012 // focused box. constrained indicates whether the focused box should be put 1013 // into the constrained frame. moveBox(int fromIndex[], boolean hasPrev, boolean hasNext, boolean constrained, Size[] sizes)1014 public void moveBox(int fromIndex[], boolean hasPrev, boolean hasNext, 1015 boolean constrained, Size[] sizes) { 1016 //debugMoveBox(fromIndex); 1017 mHasPrev = hasPrev; 1018 mHasNext = hasNext; 1019 1020 RangeIntArray from = new RangeIntArray(fromIndex, -BOX_MAX, BOX_MAX); 1021 1022 // 1. Get the absolute X coordiates for the boxes. 1023 layoutAndSetPosition(); 1024 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 1025 Box b = mBoxes.get(i); 1026 Rect r = mRects.get(i); 1027 b.mAbsoluteX = r.centerX() - mViewW / 2; 1028 } 1029 1030 // 2. copy boxes and gaps to temporary storage. 1031 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 1032 mTempBoxes.put(i, mBoxes.get(i)); 1033 mBoxes.put(i, null); 1034 } 1035 for (int i = -BOX_MAX; i < BOX_MAX; i++) { 1036 mTempGaps.put(i, mGaps.get(i)); 1037 mGaps.put(i, null); 1038 } 1039 1040 // 3. move back boxes that are used in the new array. 1041 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 1042 int j = from.get(i); 1043 if (j == Integer.MAX_VALUE) continue; 1044 mBoxes.put(i, mTempBoxes.get(j)); 1045 mTempBoxes.put(j, null); 1046 } 1047 1048 // 4. move back gaps if both boxes around it are kept together. 1049 for (int i = -BOX_MAX; i < BOX_MAX; i++) { 1050 int j = from.get(i); 1051 if (j == Integer.MAX_VALUE) continue; 1052 int k = from.get(i + 1); 1053 if (k == Integer.MAX_VALUE) continue; 1054 if (j + 1 == k) { 1055 mGaps.put(i, mTempGaps.get(j)); 1056 mTempGaps.put(j, null); 1057 } 1058 } 1059 1060 // 5. recycle the boxes that are not used in the new array. 1061 int k = -BOX_MAX; 1062 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 1063 if (mBoxes.get(i) != null) continue; 1064 while (mTempBoxes.get(k) == null) { 1065 k++; 1066 } 1067 mBoxes.put(i, mTempBoxes.get(k++)); 1068 initBox(i, sizes[i + BOX_MAX]); 1069 } 1070 1071 // 6. Now give the recycled box a reasonable absolute X position. 1072 // 1073 // First try to find the first and the last box which the absolute X 1074 // position is known. 1075 int first, last; 1076 for (first = -BOX_MAX; first <= BOX_MAX; first++) { 1077 if (from.get(first) != Integer.MAX_VALUE) break; 1078 } 1079 for (last = BOX_MAX; last >= -BOX_MAX; last--) { 1080 if (from.get(last) != Integer.MAX_VALUE) break; 1081 } 1082 // If there is no box has known X position at all, make the focused one 1083 // as known. 1084 if (first > BOX_MAX) { 1085 mBoxes.get(0).mAbsoluteX = mPlatform.mCurrentX; 1086 first = last = 0; 1087 } 1088 // Now for those boxes between first and last, assign their position to 1089 // align to the previous box or the next box with known position. For 1090 // the boxes before first or after last, we will use a new default gap 1091 // size below. 1092 1093 // Align to the previous box 1094 for (int i = Math.max(0, first + 1); i < last; i++) { 1095 if (from.get(i) != Integer.MAX_VALUE) continue; 1096 Box a = mBoxes.get(i - 1); 1097 Box b = mBoxes.get(i); 1098 int wa = widthOf(a); 1099 int wb = widthOf(b); 1100 b.mAbsoluteX = a.mAbsoluteX + (wa - wa / 2) + wb / 2 1101 + getDefaultGapSize(i); 1102 if (mPopFromTop) { 1103 b.mCurrentY = -(mViewH / 2 + heightOf(b) / 2); 1104 } else { 1105 b.mCurrentY = (mViewH / 2 + heightOf(b) / 2); 1106 } 1107 } 1108 1109 // Align to the next box 1110 for (int i = Math.min(-1, last - 1); i > first; i--) { 1111 if (from.get(i) != Integer.MAX_VALUE) continue; 1112 Box a = mBoxes.get(i + 1); 1113 Box b = mBoxes.get(i); 1114 int wa = widthOf(a); 1115 int wb = widthOf(b); 1116 b.mAbsoluteX = a.mAbsoluteX - wa / 2 - (wb - wb / 2) 1117 - getDefaultGapSize(i); 1118 if (mPopFromTop) { 1119 b.mCurrentY = -(mViewH / 2 + heightOf(b) / 2); 1120 } else { 1121 b.mCurrentY = (mViewH / 2 + heightOf(b) / 2); 1122 } 1123 } 1124 1125 // 7. recycle the gaps that are not used in the new array. 1126 k = -BOX_MAX; 1127 for (int i = -BOX_MAX; i < BOX_MAX; i++) { 1128 if (mGaps.get(i) != null) continue; 1129 while (mTempGaps.get(k) == null) { 1130 k++; 1131 } 1132 mGaps.put(i, mTempGaps.get(k++)); 1133 Box a = mBoxes.get(i); 1134 Box b = mBoxes.get(i + 1); 1135 int wa = widthOf(a); 1136 int wb = widthOf(b); 1137 if (i >= first && i < last) { 1138 int g = b.mAbsoluteX - a.mAbsoluteX - wb / 2 - (wa - wa / 2); 1139 initGap(i, g); 1140 } else { 1141 initGap(i); 1142 } 1143 } 1144 1145 // 8. calculate the new absolute X coordinates for those box before 1146 // first or after last. 1147 for (int i = first - 1; i >= -BOX_MAX; i--) { 1148 Box a = mBoxes.get(i + 1); 1149 Box b = mBoxes.get(i); 1150 int wa = widthOf(a); 1151 int wb = widthOf(b); 1152 Gap g = mGaps.get(i); 1153 b.mAbsoluteX = a.mAbsoluteX - wa / 2 - (wb - wb / 2) - g.mCurrentGap; 1154 } 1155 1156 for (int i = last + 1; i <= BOX_MAX; i++) { 1157 Box a = mBoxes.get(i - 1); 1158 Box b = mBoxes.get(i); 1159 int wa = widthOf(a); 1160 int wb = widthOf(b); 1161 Gap g = mGaps.get(i - 1); 1162 b.mAbsoluteX = a.mAbsoluteX + (wa - wa / 2) + wb / 2 + g.mCurrentGap; 1163 } 1164 1165 // 9. offset the Platform position 1166 int dx = mBoxes.get(0).mAbsoluteX - mPlatform.mCurrentX; 1167 mPlatform.mCurrentX += dx; 1168 mPlatform.mFromX += dx; 1169 mPlatform.mToX += dx; 1170 mPlatform.mFlingOffset += dx; 1171 1172 if (mConstrained != constrained) { 1173 mConstrained = constrained; 1174 mPlatform.updateDefaultXY(); 1175 updateScaleAndGapLimit(); 1176 } 1177 1178 snapAndRedraw(); 1179 } 1180 1181 //////////////////////////////////////////////////////////////////////////// 1182 // Public utilities 1183 //////////////////////////////////////////////////////////////////////////// 1184 isAtMinimalScale()1185 public boolean isAtMinimalScale() { 1186 Box b = mBoxes.get(0); 1187 return isAlmostEqual(b.mCurrentScale, b.mScaleMin); 1188 } 1189 isCenter()1190 public boolean isCenter() { 1191 Box b = mBoxes.get(0); 1192 return mPlatform.mCurrentX == mPlatform.mDefaultX 1193 && b.mCurrentY == 0; 1194 } 1195 getImageWidth()1196 public int getImageWidth() { 1197 Box b = mBoxes.get(0); 1198 return b.mImageW; 1199 } 1200 getImageHeight()1201 public int getImageHeight() { 1202 Box b = mBoxes.get(0); 1203 return b.mImageH; 1204 } 1205 getImageScale()1206 public float getImageScale() { 1207 Box b = mBoxes.get(0); 1208 return b.mCurrentScale; 1209 } 1210 getImageAtEdges()1211 public int getImageAtEdges() { 1212 Box b = mBoxes.get(0); 1213 Platform p = mPlatform; 1214 calculateStableBound(b.mCurrentScale); 1215 int edges = 0; 1216 if (p.mCurrentX <= mBoundLeft) { 1217 edges |= IMAGE_AT_RIGHT_EDGE; 1218 } 1219 if (p.mCurrentX >= mBoundRight) { 1220 edges |= IMAGE_AT_LEFT_EDGE; 1221 } 1222 if (b.mCurrentY <= mBoundTop) { 1223 edges |= IMAGE_AT_BOTTOM_EDGE; 1224 } 1225 if (b.mCurrentY >= mBoundBottom) { 1226 edges |= IMAGE_AT_TOP_EDGE; 1227 } 1228 return edges; 1229 } 1230 isScrolling()1231 public boolean isScrolling() { 1232 return mPlatform.mAnimationStartTime != NO_ANIMATION 1233 && mPlatform.mCurrentX != mPlatform.mToX; 1234 } 1235 stopScrolling()1236 public void stopScrolling() { 1237 if (mPlatform.mAnimationStartTime == NO_ANIMATION) return; 1238 if (mFilmMode) mFilmScroller.forceFinished(true); 1239 mPlatform.mFromX = mPlatform.mToX = mPlatform.mCurrentX; 1240 } 1241 getFilmRatio()1242 public float getFilmRatio() { 1243 return mFilmRatio.mCurrentRatio; 1244 } 1245 setPopFromTop(boolean top)1246 public void setPopFromTop(boolean top) { 1247 mPopFromTop = top; 1248 } 1249 hasDeletingBox()1250 public boolean hasDeletingBox() { 1251 for(int i = -BOX_MAX; i <= BOX_MAX; i++) { 1252 if (mBoxes.get(i).mAnimationKind == ANIM_KIND_DELETE) { 1253 return true; 1254 } 1255 } 1256 return false; 1257 } 1258 1259 //////////////////////////////////////////////////////////////////////////// 1260 // Private utilities 1261 //////////////////////////////////////////////////////////////////////////// 1262 getMinimalScale(Box b)1263 private float getMinimalScale(Box b) { 1264 float wFactor = 1.0f; 1265 float hFactor = 1.0f; 1266 int viewW, viewH; 1267 1268 if (!mFilmMode && mConstrained && !mConstrainedFrame.isEmpty() 1269 && b == mBoxes.get(0)) { 1270 viewW = mConstrainedFrame.width(); 1271 viewH = mConstrainedFrame.height(); 1272 } else { 1273 viewW = mViewW; 1274 viewH = mViewH; 1275 } 1276 1277 if (mFilmMode) { 1278 if (mViewH > mViewW) { // portrait 1279 wFactor = FILM_MODE_PORTRAIT_WIDTH; 1280 hFactor = FILM_MODE_PORTRAIT_HEIGHT; 1281 } else { // landscape 1282 wFactor = FILM_MODE_LANDSCAPE_WIDTH; 1283 hFactor = FILM_MODE_LANDSCAPE_HEIGHT; 1284 } 1285 } 1286 1287 float s = Math.min(wFactor * viewW / b.mImageW, 1288 hFactor * viewH / b.mImageH); 1289 return Math.min(SCALE_LIMIT, s); 1290 } 1291 getMaximalScale(Box b)1292 private float getMaximalScale(Box b) { 1293 if (mFilmMode) return getMinimalScale(b); 1294 if (mConstrained && !mConstrainedFrame.isEmpty()) return getMinimalScale(b); 1295 return SCALE_LIMIT; 1296 } 1297 isAlmostEqual(float a, float b)1298 private static boolean isAlmostEqual(float a, float b) { 1299 float diff = a - b; 1300 return (diff < 0 ? -diff : diff) < 0.02f; 1301 } 1302 1303 // Calculates the stable region of mPlatform.mCurrentX and 1304 // mBoxes.get(0).mCurrentY, where "stable" means 1305 // 1306 // (1) If the dimension of scaled image >= view dimension, we will not 1307 // see black region outside the image (at that dimension). 1308 // (2) If the dimension of scaled image < view dimension, we will center 1309 // the scaled image. 1310 // 1311 // We might temporarily go out of this stable during user interaction, 1312 // but will "snap back" after user stops interaction. 1313 // 1314 // The results are stored in mBound{Left/Right/Top/Bottom}. 1315 // 1316 // An extra parameter "horizontalSlack" (which has the value of 0 usually) 1317 // is used to extend the stable region by some pixels on each side 1318 // horizontally. calculateStableBound(float scale, int horizontalSlack)1319 private void calculateStableBound(float scale, int horizontalSlack) { 1320 Box b = mBoxes.get(0); 1321 1322 // The width and height of the box in number of view pixels 1323 int w = widthOf(b, scale); 1324 int h = heightOf(b, scale); 1325 1326 // When the edge of the view is aligned with the edge of the box 1327 mBoundLeft = (mViewW + 1) / 2 - (w + 1) / 2 - horizontalSlack; 1328 mBoundRight = w / 2 - mViewW / 2 + horizontalSlack; 1329 mBoundTop = (mViewH + 1) / 2 - (h + 1) / 2; 1330 mBoundBottom = h / 2 - mViewH / 2; 1331 1332 // If the scaled height is smaller than the view height, 1333 // force it to be in the center. 1334 if (viewTallerThanScaledImage(scale)) { 1335 mBoundTop = mBoundBottom = 0; 1336 } 1337 1338 // Same for width 1339 if (viewWiderThanScaledImage(scale)) { 1340 mBoundLeft = mBoundRight = mPlatform.mDefaultX; 1341 } 1342 } 1343 calculateStableBound(float scale)1344 private void calculateStableBound(float scale) { 1345 calculateStableBound(scale, 0); 1346 } 1347 viewTallerThanScaledImage(float scale)1348 private boolean viewTallerThanScaledImage(float scale) { 1349 return mViewH >= heightOf(mBoxes.get(0), scale); 1350 } 1351 viewWiderThanScaledImage(float scale)1352 private boolean viewWiderThanScaledImage(float scale) { 1353 return mViewW >= widthOf(mBoxes.get(0), scale); 1354 } 1355 getTargetScale(Box b)1356 private float getTargetScale(Box b) { 1357 return b.mAnimationStartTime == NO_ANIMATION 1358 ? b.mCurrentScale : b.mToScale; 1359 } 1360 1361 //////////////////////////////////////////////////////////////////////////// 1362 // Animatable: an thing which can do animation. 1363 //////////////////////////////////////////////////////////////////////////// 1364 private abstract static class Animatable { 1365 public long mAnimationStartTime; 1366 public int mAnimationKind; 1367 public int mAnimationDuration; 1368 1369 // This should be overidden in subclass to change the animation values 1370 // give the progress value in [0, 1]. interpolate(float progress)1371 protected abstract boolean interpolate(float progress); startSnapback()1372 public abstract boolean startSnapback(); 1373 1374 // Returns true if the animation values changes, so things need to be 1375 // redrawn. advanceAnimation()1376 public boolean advanceAnimation() { 1377 if (mAnimationStartTime == NO_ANIMATION) { 1378 return false; 1379 } 1380 if (mAnimationStartTime == LAST_ANIMATION) { 1381 mAnimationStartTime = NO_ANIMATION; 1382 return startSnapback(); 1383 } 1384 1385 float progress; 1386 if (mAnimationDuration == 0) { 1387 progress = 1; 1388 } else { 1389 long now = AnimationTime.get(); 1390 progress = 1391 (float) (now - mAnimationStartTime) / mAnimationDuration; 1392 } 1393 1394 if (progress >= 1) { 1395 progress = 1; 1396 } else { 1397 progress = applyInterpolationCurve(mAnimationKind, progress); 1398 } 1399 1400 boolean done = interpolate(progress); 1401 1402 if (done) { 1403 mAnimationStartTime = LAST_ANIMATION; 1404 } 1405 1406 return true; 1407 } 1408 applyInterpolationCurve(int kind, float progress)1409 private static float applyInterpolationCurve(int kind, float progress) { 1410 float f = 1 - progress; 1411 switch (kind) { 1412 case ANIM_KIND_SCROLL: 1413 case ANIM_KIND_FLING: 1414 case ANIM_KIND_FLING_X: 1415 case ANIM_KIND_DELETE: 1416 case ANIM_KIND_CAPTURE: 1417 progress = 1 - f; // linear 1418 break; 1419 case ANIM_KIND_SCALE: 1420 progress = 1 - f * f; // quadratic 1421 break; 1422 case ANIM_KIND_OPENING: 1423 progress = 1 - f * f * f; // x^3 1424 break; 1425 case ANIM_KIND_SNAPBACK: 1426 case ANIM_KIND_ZOOM: 1427 case ANIM_KIND_SLIDE: 1428 progress = 1 - f * f * f * f * f; // x^5 1429 break; 1430 } 1431 return progress; 1432 } 1433 } 1434 1435 //////////////////////////////////////////////////////////////////////////// 1436 // Platform: captures the global X/Y movement. 1437 //////////////////////////////////////////////////////////////////////////// 1438 private class Platform extends Animatable { 1439 public int mCurrentX, mFromX, mToX, mDefaultX; 1440 public int mCurrentY, mFromY, mToY, mDefaultY; 1441 public int mFlingOffset; 1442 1443 @Override startSnapback()1444 public boolean startSnapback() { 1445 if (mAnimationStartTime != NO_ANIMATION) return false; 1446 if (mAnimationKind == ANIM_KIND_SCROLL 1447 && mListener.isHoldingDown()) return false; 1448 if (mInScale) return false; 1449 1450 Box b = mBoxes.get(0); 1451 float scaleMin = mExtraScalingRange ? 1452 b.mScaleMin * SCALE_MIN_EXTRA : b.mScaleMin; 1453 float scaleMax = mExtraScalingRange ? 1454 b.mScaleMax * SCALE_MAX_EXTRA : b.mScaleMax; 1455 float scale = Utils.clamp(b.mCurrentScale, scaleMin, scaleMax); 1456 int x = mCurrentX; 1457 int y = mDefaultY; 1458 if (mFilmMode) { 1459 int defaultX = mDefaultX; 1460 if (!mHasNext) x = Math.max(x, defaultX); 1461 if (!mHasPrev) x = Math.min(x, defaultX); 1462 } else { 1463 calculateStableBound(scale, HORIZONTAL_SLACK); 1464 // If the picture is zoomed-in, we want to keep the focus point 1465 // stay in the same position on screen, so we need to adjust 1466 // target mCurrentX (which is the center of the focused 1467 // box). The position of the focus point on screen (relative the 1468 // the center of the view) is: 1469 // 1470 // mCurrentX + scale * mFocusX = mCurrentX' + scale' * mFocusX 1471 // => mCurrentX' = mCurrentX + (scale - scale') * mFocusX 1472 // 1473 if (!viewWiderThanScaledImage(scale)) { 1474 float scaleDiff = b.mCurrentScale - scale; 1475 x += (int) (mFocusX * scaleDiff + 0.5f); 1476 } 1477 x = Utils.clamp(x, mBoundLeft, mBoundRight); 1478 } 1479 if (mCurrentX != x || mCurrentY != y) { 1480 return doAnimation(x, y, ANIM_KIND_SNAPBACK); 1481 } 1482 return false; 1483 } 1484 1485 // The updateDefaultXY() should be called whenever these variables 1486 // changes: (1) mConstrained (2) mConstrainedFrame (3) mViewW/H (4) 1487 // mFilmMode updateDefaultXY()1488 public void updateDefaultXY() { 1489 // We don't check mFilmMode and return 0 for mDefaultX. Because 1490 // otherwise if we decide to leave film mode because we are 1491 // centered, we will immediately back into film mode because we find 1492 // we are not centered. 1493 if (mConstrained && !mConstrainedFrame.isEmpty()) { 1494 mDefaultX = mConstrainedFrame.centerX() - mViewW / 2; 1495 mDefaultY = mFilmMode ? 0 : 1496 mConstrainedFrame.centerY() - mViewH / 2; 1497 } else { 1498 mDefaultX = 0; 1499 mDefaultY = 0; 1500 } 1501 } 1502 1503 // Starts an animation for the platform. doAnimation(int targetX, int targetY, int kind)1504 private boolean doAnimation(int targetX, int targetY, int kind) { 1505 if (mCurrentX == targetX && mCurrentY == targetY) return false; 1506 mAnimationKind = kind; 1507 mFromX = mCurrentX; 1508 mFromY = mCurrentY; 1509 mToX = targetX; 1510 mToY = targetY; 1511 mAnimationStartTime = AnimationTime.startTime(); 1512 mAnimationDuration = ANIM_TIME[kind]; 1513 mFlingOffset = 0; 1514 advanceAnimation(); 1515 return true; 1516 } 1517 1518 @Override interpolate(float progress)1519 protected boolean interpolate(float progress) { 1520 if (mAnimationKind == ANIM_KIND_FLING) { 1521 return interpolateFlingPage(progress); 1522 } else if (mAnimationKind == ANIM_KIND_FLING_X) { 1523 return interpolateFlingFilm(progress); 1524 } else { 1525 return interpolateLinear(progress); 1526 } 1527 } 1528 interpolateFlingFilm(float progress)1529 private boolean interpolateFlingFilm(float progress) { 1530 mFilmScroller.computeScrollOffset(); 1531 mCurrentX = mFilmScroller.getCurrX() + mFlingOffset; 1532 1533 int dir = EdgeView.INVALID_DIRECTION; 1534 if (mCurrentX < mDefaultX) { 1535 if (!mHasNext) { 1536 dir = EdgeView.RIGHT; 1537 } 1538 } else if (mCurrentX > mDefaultX) { 1539 if (!mHasPrev) { 1540 dir = EdgeView.LEFT; 1541 } 1542 } 1543 if (dir != EdgeView.INVALID_DIRECTION) { 1544 int v = (int) (mFilmScroller.getCurrVelocity() + 0.5f); 1545 mListener.onAbsorb(v, dir); 1546 mFilmScroller.forceFinished(true); 1547 mCurrentX = mDefaultX; 1548 } 1549 return mFilmScroller.isFinished(); 1550 } 1551 interpolateFlingPage(float progress)1552 private boolean interpolateFlingPage(float progress) { 1553 mPageScroller.computeScrollOffset(progress); 1554 Box b = mBoxes.get(0); 1555 calculateStableBound(b.mCurrentScale); 1556 1557 int oldX = mCurrentX; 1558 mCurrentX = mPageScroller.getCurrX(); 1559 1560 // Check if we hit the edges; show edge effects if we do. 1561 if (oldX > mBoundLeft && mCurrentX == mBoundLeft) { 1562 int v = (int) (-mPageScroller.getCurrVelocityX() + 0.5f); 1563 mListener.onAbsorb(v, EdgeView.RIGHT); 1564 } else if (oldX < mBoundRight && mCurrentX == mBoundRight) { 1565 int v = (int) (mPageScroller.getCurrVelocityX() + 0.5f); 1566 mListener.onAbsorb(v, EdgeView.LEFT); 1567 } 1568 1569 return progress >= 1; 1570 } 1571 interpolateLinear(float progress)1572 private boolean interpolateLinear(float progress) { 1573 // Other animations 1574 if (progress >= 1) { 1575 mCurrentX = mToX; 1576 mCurrentY = mToY; 1577 return true; 1578 } else { 1579 if (mAnimationKind == ANIM_KIND_CAPTURE) { 1580 progress = CaptureAnimation.calculateSlide(progress); 1581 } 1582 mCurrentX = (int) (mFromX + progress * (mToX - mFromX)); 1583 mCurrentY = (int) (mFromY + progress * (mToY - mFromY)); 1584 if (mAnimationKind == ANIM_KIND_CAPTURE) { 1585 return false; 1586 } else { 1587 return (mCurrentX == mToX && mCurrentY == mToY); 1588 } 1589 } 1590 } 1591 } 1592 1593 //////////////////////////////////////////////////////////////////////////// 1594 // Box: represents a rectangular area which shows a picture. 1595 //////////////////////////////////////////////////////////////////////////// 1596 private class Box extends Animatable { 1597 // Size of the bitmap 1598 public int mImageW, mImageH; 1599 1600 // This is true if we assume the image size is the same as view size 1601 // until we know the actual size of image. This is also used to 1602 // determine if there is an image ready to show. 1603 public boolean mUseViewSize; 1604 1605 // The minimum and maximum scale we allow for this box. 1606 public float mScaleMin, mScaleMax; 1607 1608 // The X/Y value indicates where the center of the box is on the view 1609 // coordinate. We always keep the mCurrent{X,Y,Scale} sync with the 1610 // actual values used currently. Note that the X values are implicitly 1611 // defined by Platform and Gaps. 1612 public int mCurrentY, mFromY, mToY; 1613 public float mCurrentScale, mFromScale, mToScale; 1614 1615 // The absolute X coordinate of the center of the box. This is only used 1616 // during moveBox(). 1617 public int mAbsoluteX; 1618 1619 @Override startSnapback()1620 public boolean startSnapback() { 1621 if (mAnimationStartTime != NO_ANIMATION) return false; 1622 if (mAnimationKind == ANIM_KIND_SCROLL 1623 && mListener.isHoldingDown()) return false; 1624 if (mAnimationKind == ANIM_KIND_DELETE 1625 && mListener.isHoldingDelete()) return false; 1626 if (mInScale && this == mBoxes.get(0)) return false; 1627 1628 int y = mCurrentY; 1629 float scale; 1630 1631 if (this == mBoxes.get(0)) { 1632 float scaleMin = mExtraScalingRange ? 1633 mScaleMin * SCALE_MIN_EXTRA : mScaleMin; 1634 float scaleMax = mExtraScalingRange ? 1635 mScaleMax * SCALE_MAX_EXTRA : mScaleMax; 1636 scale = Utils.clamp(mCurrentScale, scaleMin, scaleMax); 1637 if (mFilmMode) { 1638 y = 0; 1639 } else { 1640 calculateStableBound(scale, HORIZONTAL_SLACK); 1641 // If the picture is zoomed-in, we want to keep the focus 1642 // point stay in the same position on screen. See the 1643 // comment in Platform.startSnapback for details. 1644 if (!viewTallerThanScaledImage(scale)) { 1645 float scaleDiff = mCurrentScale - scale; 1646 y += (int) (mFocusY * scaleDiff + 0.5f); 1647 } 1648 y = Utils.clamp(y, mBoundTop, mBoundBottom); 1649 } 1650 } else { 1651 y = 0; 1652 scale = mScaleMin; 1653 } 1654 1655 if (mCurrentY != y || mCurrentScale != scale) { 1656 return doAnimation(y, scale, ANIM_KIND_SNAPBACK); 1657 } 1658 return false; 1659 } 1660 doAnimation(int targetY, float targetScale, int kind)1661 private boolean doAnimation(int targetY, float targetScale, int kind) { 1662 targetScale = clampScale(targetScale); 1663 1664 if (mCurrentY == targetY && mCurrentScale == targetScale 1665 && kind != ANIM_KIND_CAPTURE) { 1666 return false; 1667 } 1668 1669 // Now starts an animation for the box. 1670 mAnimationKind = kind; 1671 mFromY = mCurrentY; 1672 mFromScale = mCurrentScale; 1673 mToY = targetY; 1674 mToScale = targetScale; 1675 mAnimationStartTime = AnimationTime.startTime(); 1676 mAnimationDuration = ANIM_TIME[kind]; 1677 advanceAnimation(); 1678 return true; 1679 } 1680 1681 // Clamps the input scale to the range that doAnimation() can reach. clampScale(float s)1682 public float clampScale(float s) { 1683 return Utils.clamp(s, 1684 SCALE_MIN_EXTRA * mScaleMin, 1685 SCALE_MAX_EXTRA * mScaleMax); 1686 } 1687 1688 @Override interpolate(float progress)1689 protected boolean interpolate(float progress) { 1690 if (mAnimationKind == ANIM_KIND_FLING) { 1691 return interpolateFlingPage(progress); 1692 } else { 1693 return interpolateLinear(progress); 1694 } 1695 } 1696 interpolateFlingPage(float progress)1697 private boolean interpolateFlingPage(float progress) { 1698 mPageScroller.computeScrollOffset(progress); 1699 calculateStableBound(mCurrentScale); 1700 1701 int oldY = mCurrentY; 1702 mCurrentY = mPageScroller.getCurrY(); 1703 1704 // Check if we hit the edges; show edge effects if we do. 1705 if (oldY > mBoundTop && mCurrentY == mBoundTop) { 1706 int v = (int) (-mPageScroller.getCurrVelocityY() + 0.5f); 1707 mListener.onAbsorb(v, EdgeView.BOTTOM); 1708 } else if (oldY < mBoundBottom && mCurrentY == mBoundBottom) { 1709 int v = (int) (mPageScroller.getCurrVelocityY() + 0.5f); 1710 mListener.onAbsorb(v, EdgeView.TOP); 1711 } 1712 1713 return progress >= 1; 1714 } 1715 interpolateLinear(float progress)1716 private boolean interpolateLinear(float progress) { 1717 if (progress >= 1) { 1718 mCurrentY = mToY; 1719 mCurrentScale = mToScale; 1720 return true; 1721 } else { 1722 mCurrentY = (int) (mFromY + progress * (mToY - mFromY)); 1723 mCurrentScale = mFromScale + progress * (mToScale - mFromScale); 1724 if (mAnimationKind == ANIM_KIND_CAPTURE) { 1725 float f = CaptureAnimation.calculateScale(progress); 1726 mCurrentScale *= f; 1727 return false; 1728 } else { 1729 return (mCurrentY == mToY && mCurrentScale == mToScale); 1730 } 1731 } 1732 } 1733 } 1734 1735 //////////////////////////////////////////////////////////////////////////// 1736 // Gap: represents a rectangular area which is between two boxes. 1737 //////////////////////////////////////////////////////////////////////////// 1738 private class Gap extends Animatable { 1739 // The default gap size between two boxes. The value may vary for 1740 // different image size of the boxes and for different modes (page or 1741 // film). 1742 public int mDefaultSize; 1743 1744 // The gap size between the two boxes. 1745 public int mCurrentGap, mFromGap, mToGap; 1746 1747 @Override startSnapback()1748 public boolean startSnapback() { 1749 if (mAnimationStartTime != NO_ANIMATION) return false; 1750 return doAnimation(mDefaultSize, ANIM_KIND_SNAPBACK); 1751 } 1752 1753 // Starts an animation for a gap. doAnimation(int targetSize, int kind)1754 public boolean doAnimation(int targetSize, int kind) { 1755 if (mCurrentGap == targetSize && kind != ANIM_KIND_CAPTURE) { 1756 return false; 1757 } 1758 mAnimationKind = kind; 1759 mFromGap = mCurrentGap; 1760 mToGap = targetSize; 1761 mAnimationStartTime = AnimationTime.startTime(); 1762 mAnimationDuration = ANIM_TIME[mAnimationKind]; 1763 advanceAnimation(); 1764 return true; 1765 } 1766 1767 @Override interpolate(float progress)1768 protected boolean interpolate(float progress) { 1769 if (progress >= 1) { 1770 mCurrentGap = mToGap; 1771 return true; 1772 } else { 1773 mCurrentGap = (int) (mFromGap + progress * (mToGap - mFromGap)); 1774 if (mAnimationKind == ANIM_KIND_CAPTURE) { 1775 float f = CaptureAnimation.calculateScale(progress); 1776 mCurrentGap = (int) (mCurrentGap * f); 1777 return false; 1778 } else { 1779 return (mCurrentGap == mToGap); 1780 } 1781 } 1782 } 1783 } 1784 1785 //////////////////////////////////////////////////////////////////////////// 1786 // FilmRatio: represents the progress of film mode change. 1787 //////////////////////////////////////////////////////////////////////////// 1788 private class FilmRatio extends Animatable { 1789 // The film ratio: 1 means switching to film mode is complete, 0 means 1790 // switching to page mode is complete. 1791 public float mCurrentRatio, mFromRatio, mToRatio; 1792 1793 @Override startSnapback()1794 public boolean startSnapback() { 1795 float target = mFilmMode ? 1f : 0f; 1796 if (target == mToRatio) return false; 1797 return doAnimation(target, ANIM_KIND_SNAPBACK); 1798 } 1799 1800 // Starts an animation for the film ratio. doAnimation(float targetRatio, int kind)1801 private boolean doAnimation(float targetRatio, int kind) { 1802 mAnimationKind = kind; 1803 mFromRatio = mCurrentRatio; 1804 mToRatio = targetRatio; 1805 mAnimationStartTime = AnimationTime.startTime(); 1806 mAnimationDuration = ANIM_TIME[mAnimationKind]; 1807 advanceAnimation(); 1808 return true; 1809 } 1810 1811 @Override interpolate(float progress)1812 protected boolean interpolate(float progress) { 1813 if (progress >= 1) { 1814 mCurrentRatio = mToRatio; 1815 return true; 1816 } else { 1817 mCurrentRatio = mFromRatio + progress * (mToRatio - mFromRatio); 1818 return (mCurrentRatio == mToRatio); 1819 } 1820 } 1821 } 1822 } 1823