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.Scroller; 23 24 import com.android.gallery3d.app.PhotoPage; 25 import com.android.gallery3d.common.Utils; 26 import com.android.gallery3d.ui.PhotoView.Size; 27 import com.android.gallery3d.util.GalleryUtils; 28 import com.android.gallery3d.util.RangeArray; 29 import com.android.gallery3d.util.RangeIntArray; 30 31 class PositionController { 32 private static final String TAG = "PositionController"; 33 34 public static final int IMAGE_AT_LEFT_EDGE = 1; 35 public static final int IMAGE_AT_RIGHT_EDGE = 2; 36 public static final int IMAGE_AT_TOP_EDGE = 4; 37 public static final int IMAGE_AT_BOTTOM_EDGE = 8; 38 39 public static final int CAPTURE_ANIMATION_TIME = 700; 40 public static final int SNAPBACK_ANIMATION_TIME = 600; 41 42 // Special values for animation time. 43 private static final long NO_ANIMATION = -1; 44 private static final long LAST_ANIMATION = -2; 45 46 private static final int ANIM_KIND_NONE = -1; 47 private static final int ANIM_KIND_SCROLL = 0; 48 private static final int ANIM_KIND_SCALE = 1; 49 private static final int ANIM_KIND_SNAPBACK = 2; 50 private static final int ANIM_KIND_SLIDE = 3; 51 private static final int ANIM_KIND_ZOOM = 4; 52 private static final int ANIM_KIND_OPENING = 5; 53 private static final int ANIM_KIND_FLING = 6; 54 private static final int ANIM_KIND_FLING_X = 7; 55 private static final int ANIM_KIND_DELETE = 8; 56 private static final int ANIM_KIND_CAPTURE = 9; 57 58 // Animation time in milliseconds. The order must match ANIM_KIND_* above. 59 // 60 // The values for ANIM_KIND_FLING_X does't matter because we use 61 // mFilmScroller.isFinished() to decide when to stop. We set it to 0 so it's 62 // faster for Animatable.advanceAnimation() to calculate the progress 63 // (always 1). 64 private static final int ANIM_TIME[] = { 65 0, // ANIM_KIND_SCROLL 66 0, // ANIM_KIND_SCALE 67 SNAPBACK_ANIMATION_TIME, // ANIM_KIND_SNAPBACK 68 400, // ANIM_KIND_SLIDE 69 300, // ANIM_KIND_ZOOM 70 300, // ANIM_KIND_OPENING 71 0, // ANIM_KIND_FLING (the duration is calculated dynamically) 72 0, // ANIM_KIND_FLING_X (see the comment above) 73 0, // ANIM_KIND_DELETE (the duration is calculated dynamically) 74 CAPTURE_ANIMATION_TIME, // ANIM_KIND_CAPTURE 75 }; 76 77 // We try to scale up the image to fill the screen. But in order not to 78 // scale too much for small icons, we limit the max up-scaling factor here. 79 private static final float SCALE_LIMIT = 4; 80 81 // For user's gestures, we give a temporary extra scaling range which goes 82 // above or below the usual scaling limits. 83 private static final float SCALE_MIN_EXTRA = 0.7f; 84 private static final float SCALE_MAX_EXTRA = 1.4f; 85 86 // Setting this true makes the extra scaling range permanent (until this is 87 // set to false again). 88 private boolean mExtraScalingRange = false; 89 90 // Film Mode v.s. Page Mode: in film mode we show smaller pictures. 91 private boolean mFilmMode = false; 92 93 // These are the limits for width / height of the picture in film mode. 94 private static final float FILM_MODE_PORTRAIT_HEIGHT = 0.48f; 95 private static final float FILM_MODE_PORTRAIT_WIDTH = 0.7f; 96 private static final float FILM_MODE_LANDSCAPE_HEIGHT = 0.7f; 97 private static final float FILM_MODE_LANDSCAPE_WIDTH = 0.7f; 98 99 // In addition to the focused box (index == 0). We also keep information 100 // about this many boxes on each side. 101 private static final int BOX_MAX = PhotoView.SCREEN_NAIL_MAX; 102 private static final int[] CENTER_OUT_INDEX = new int[2 * BOX_MAX + 1]; 103 104 private static final int IMAGE_GAP = GalleryUtils.dpToPixel(16); 105 private static final int HORIZONTAL_SLACK = GalleryUtils.dpToPixel(12); 106 107 // These are constants for the delete gesture. 108 private static final int DEFAULT_DELETE_ANIMATION_DURATION = 200; // ms 109 private static final int MAX_DELETE_ANIMATION_DURATION = 400; // ms 110 111 private Listener mListener; 112 private volatile Rect mOpenAnimationRect; 113 114 // Use a large enough value, so we won't see the gray shadow in the beginning. 115 private int mViewW = 1200; 116 private int mViewH = 1200; 117 118 // A scaling gesture is in progress. 119 private boolean mInScale; 120 // The focus point of the scaling gesture, relative to the center of the 121 // picture in bitmap pixels. 122 private float mFocusX, mFocusY; 123 124 // whether there is a previous/next picture. 125 private boolean mHasPrev, mHasNext; 126 127 // This is used by the fling animation (page mode). 128 private FlingScroller mPageScroller; 129 130 // This is used by the fling animation (film mode). 131 private Scroller mFilmScroller; 132 133 // The bound of the stable region that the focused box can stay, see the 134 // comments above calculateStableBound() for details. 135 private int mBoundLeft, mBoundRight, mBoundTop, mBoundBottom; 136 137 // Constrained frame is a rectangle that the focused box should fit into if 138 // it is constrained. It has two effects: 139 // 140 // (1) In page mode, if the focused box is constrained, scaling for the 141 // focused box is adjusted to fit into the constrained frame, instead of the 142 // whole view. 143 // 144 // (2) In page mode, if the focused box is constrained, the mPlatform's 145 // default center (mDefaultX/Y) is moved to the center of the constrained 146 // frame, instead of the view center. 147 // 148 private Rect mConstrainedFrame = new Rect(); 149 150 // Whether the focused box is constrained. 151 // 152 // Our current program's first call to moveBox() sets constrained = true, so 153 // we set the initial value of this variable to true, and we will not see 154 // see unwanted transition animation. 155 private boolean mConstrained = true; 156 157 // 158 // ___________________________________________________________ 159 // | _____ _____ _____ _____ _____ | 160 // | | | | | | | | | | | | 161 // | | Box | | Box | | Box*| | Box | | Box | | 162 // | |_____|.....|_____|.....|_____|.....|_____|.....|_____| | 163 // | Gap Gap Gap Gap | 164 // |___________________________________________________________| 165 // 166 // <-- Platform --> 167 // 168 // The focused box (Box*) centers at mPlatform's (mCurrentX, mCurrentY) 169 170 private Platform mPlatform = new Platform(); 171 private RangeArray<Box> mBoxes = new RangeArray<Box>(-BOX_MAX, BOX_MAX); 172 // The gap at the right of a Box i is at index i. The gap at the left of a 173 // Box i is at index i - 1. 174 private RangeArray<Gap> mGaps = new RangeArray<Gap>(-BOX_MAX, BOX_MAX - 1); 175 private FilmRatio mFilmRatio = new FilmRatio(); 176 177 // These are only used during moveBox(). 178 private RangeArray<Box> mTempBoxes = new RangeArray<Box>(-BOX_MAX, BOX_MAX); 179 private RangeArray<Gap> mTempGaps = 180 new RangeArray<Gap>(-BOX_MAX, BOX_MAX - 1); 181 182 // The output of the PositionController. Available through getPosition(). 183 private RangeArray<Rect> mRects = new RangeArray<Rect>(-BOX_MAX, BOX_MAX); 184 185 // The direction of a new picture should appear. New pictures pop from top 186 // if this value is true, or from bottom if this value is false. 187 boolean mPopFromTop; 188 189 public interface Listener { invalidate()190 void invalidate(); isHoldingDown()191 boolean isHoldingDown(); isHoldingDelete()192 boolean isHoldingDelete(); 193 194 // EdgeView onPull(int offset, int direction)195 void onPull(int offset, int direction); onRelease()196 void onRelease(); onAbsorb(int velocity, int direction)197 void onAbsorb(int velocity, int direction); 198 } 199 200 static { 201 // Initialize the CENTER_OUT_INDEX array. 202 // The array maps 0, 1, 2, 3, 4, ..., 2 * BOX_MAX 203 // to 0, 1, -1, 2, -2, ..., BOX_MAX, -BOX_MAX 204 for (int i = 0; i < CENTER_OUT_INDEX.length; i++) { 205 int j = (i + 1) / 2; 206 if ((i & 1) == 0) j = -j; 207 CENTER_OUT_INDEX[i] = j; 208 } 209 } 210 PositionController(Context context, Listener listener)211 public PositionController(Context context, Listener listener) { 212 mListener = listener; 213 mPageScroller = new FlingScroller(); 214 mFilmScroller = new Scroller(context, null, false); 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 or we are in fullscreen, 323 // we change the scale directly. Otherwise adjust the scales by a ratio, 324 // and snapback will animate the scale into the min/max bounds if necessary. 325 if ((wasViewSize && !isViewSize) || !mFilmMode) { 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 gesture, 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 859 @SuppressWarnings("unused") dumpState()860 private void dumpState() { 861 for (int i = -BOX_MAX; i < BOX_MAX; i++) { 862 Log.d(TAG, "Gap " + i + ": " + mGaps.get(i).mCurrentGap); 863 } 864 865 for (int i = 0; i < 2 * BOX_MAX + 1; i++) { 866 dumpRect(CENTER_OUT_INDEX[i]); 867 } 868 869 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 870 for (int j = i + 1; j <= BOX_MAX; j++) { 871 if (Rect.intersects(mRects.get(i), mRects.get(j))) { 872 Log.d(TAG, "rect " + i + " and rect " + j + "intersects!"); 873 } 874 } 875 } 876 } 877 dumpRect(int i)878 private void dumpRect(int i) { 879 StringBuilder sb = new StringBuilder(); 880 Rect r = mRects.get(i); 881 sb.append("Rect " + i + ":"); 882 sb.append("("); 883 sb.append(r.centerX()); 884 sb.append(","); 885 sb.append(r.centerY()); 886 sb.append(") ["); 887 sb.append(r.width()); 888 sb.append("x"); 889 sb.append(r.height()); 890 sb.append("]"); 891 Log.d(TAG, sb.toString()); 892 } 893 convertBoxToRect(int i)894 private void convertBoxToRect(int i) { 895 Box b = mBoxes.get(i); 896 Rect r = mRects.get(i); 897 int y = b.mCurrentY + mPlatform.mCurrentY + mViewH / 2; 898 int w = widthOf(b); 899 int h = heightOf(b); 900 if (i == 0) { 901 int x = mPlatform.mCurrentX + mViewW / 2; 902 r.left = x - w / 2; 903 r.right = r.left + w; 904 } else if (i > 0) { 905 Rect a = mRects.get(i - 1); 906 Gap g = mGaps.get(i - 1); 907 r.left = a.right + g.mCurrentGap; 908 r.right = r.left + w; 909 } else { // i < 0 910 Rect a = mRects.get(i + 1); 911 Gap g = mGaps.get(i); 912 r.right = a.left - g.mCurrentGap; 913 r.left = r.right - w; 914 } 915 r.top = y - h / 2; 916 r.bottom = r.top + h; 917 } 918 919 // Returns the position of a box. getPosition(int index)920 public Rect getPosition(int index) { 921 return mRects.get(index); 922 } 923 924 //////////////////////////////////////////////////////////////////////////// 925 // Box management 926 //////////////////////////////////////////////////////////////////////////// 927 928 // Initialize the platform to be at the view center. initPlatform()929 private void initPlatform() { 930 mPlatform.updateDefaultXY(); 931 mPlatform.mCurrentX = mPlatform.mDefaultX; 932 mPlatform.mCurrentY = mPlatform.mDefaultY; 933 mPlatform.mAnimationStartTime = NO_ANIMATION; 934 } 935 936 // Initialize a box to have the size of the view. initBox(int index)937 private void initBox(int index) { 938 Box b = mBoxes.get(index); 939 b.mImageW = mViewW; 940 b.mImageH = mViewH; 941 b.mUseViewSize = true; 942 b.mScaleMin = getMinimalScale(b); 943 b.mScaleMax = getMaximalScale(b); 944 b.mCurrentY = 0; 945 b.mCurrentScale = b.mScaleMin; 946 b.mAnimationStartTime = NO_ANIMATION; 947 b.mAnimationKind = ANIM_KIND_NONE; 948 } 949 950 // Initialize a box to a given size. initBox(int index, Size size)951 private void initBox(int index, Size size) { 952 if (size.width == 0 || size.height == 0) { 953 initBox(index); 954 return; 955 } 956 Box b = mBoxes.get(index); 957 b.mImageW = size.width; 958 b.mImageH = size.height; 959 b.mUseViewSize = false; 960 b.mScaleMin = getMinimalScale(b); 961 b.mScaleMax = getMaximalScale(b); 962 b.mCurrentY = 0; 963 b.mCurrentScale = b.mScaleMin; 964 b.mAnimationStartTime = NO_ANIMATION; 965 b.mAnimationKind = ANIM_KIND_NONE; 966 } 967 968 // Initialize a gap. This can only be called after the boxes around the gap 969 // has been initialized. initGap(int index)970 private void initGap(int index) { 971 Gap g = mGaps.get(index); 972 g.mDefaultSize = getDefaultGapSize(index); 973 g.mCurrentGap = g.mDefaultSize; 974 g.mAnimationStartTime = NO_ANIMATION; 975 } 976 initGap(int index, int size)977 private void initGap(int index, int size) { 978 Gap g = mGaps.get(index); 979 g.mDefaultSize = getDefaultGapSize(index); 980 g.mCurrentGap = size; 981 g.mAnimationStartTime = NO_ANIMATION; 982 } 983 984 @SuppressWarnings("unused") debugMoveBox(int fromIndex[])985 private void debugMoveBox(int fromIndex[]) { 986 StringBuilder s = new StringBuilder("moveBox:"); 987 for (int i = 0; i < fromIndex.length; i++) { 988 int j = fromIndex[i]; 989 if (j == Integer.MAX_VALUE) { 990 s.append(" N"); 991 } else { 992 s.append(" "); 993 s.append(fromIndex[i]); 994 } 995 } 996 Log.d(TAG, s.toString()); 997 } 998 999 // Move the boxes: it may indicate focus change, box deleted, box appearing, 1000 // box reordered, etc. 1001 // 1002 // Each element in the fromIndex array indicates where each box was in the 1003 // old array. If the value is Integer.MAX_VALUE (pictured as N below), it 1004 // means the box is new. 1005 // 1006 // For example: 1007 // N N N N N N N -- all new boxes 1008 // -3 -2 -1 0 1 2 3 -- nothing changed 1009 // -2 -1 0 1 2 3 N -- focus goes to the next box 1010 // N -3 -2 -1 0 1 2 -- focus goes to the previous box 1011 // -3 -2 -1 1 2 3 N -- the focused box was deleted. 1012 // 1013 // hasPrev/hasNext indicates if there are previous/next boxes for the 1014 // focused box. constrained indicates whether the focused box should be put 1015 // into the constrained frame. moveBox(int fromIndex[], boolean hasPrev, boolean hasNext, boolean constrained, Size[] sizes)1016 public void moveBox(int fromIndex[], boolean hasPrev, boolean hasNext, 1017 boolean constrained, Size[] sizes) { 1018 //debugMoveBox(fromIndex); 1019 mHasPrev = hasPrev; 1020 mHasNext = hasNext; 1021 1022 RangeIntArray from = new RangeIntArray(fromIndex, -BOX_MAX, BOX_MAX); 1023 1024 // 1. Get the absolute X coordinates for the boxes. 1025 layoutAndSetPosition(); 1026 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 1027 Box b = mBoxes.get(i); 1028 Rect r = mRects.get(i); 1029 b.mAbsoluteX = r.centerX() - mViewW / 2; 1030 } 1031 1032 // 2. copy boxes and gaps to temporary storage. 1033 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 1034 mTempBoxes.put(i, mBoxes.get(i)); 1035 mBoxes.put(i, null); 1036 } 1037 for (int i = -BOX_MAX; i < BOX_MAX; i++) { 1038 mTempGaps.put(i, mGaps.get(i)); 1039 mGaps.put(i, null); 1040 } 1041 1042 // 3. move back boxes that are used in the new array. 1043 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 1044 int j = from.get(i); 1045 if (j == Integer.MAX_VALUE) continue; 1046 mBoxes.put(i, mTempBoxes.get(j)); 1047 mTempBoxes.put(j, null); 1048 } 1049 1050 // 4. move back gaps if both boxes around it are kept together. 1051 for (int i = -BOX_MAX; i < BOX_MAX; i++) { 1052 int j = from.get(i); 1053 if (j == Integer.MAX_VALUE) continue; 1054 int k = from.get(i + 1); 1055 if (k == Integer.MAX_VALUE) continue; 1056 if (j + 1 == k) { 1057 mGaps.put(i, mTempGaps.get(j)); 1058 mTempGaps.put(j, null); 1059 } 1060 } 1061 1062 // 5. recycle the boxes that are not used in the new array. 1063 int k = -BOX_MAX; 1064 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 1065 if (mBoxes.get(i) != null) continue; 1066 while (mTempBoxes.get(k) == null) { 1067 k++; 1068 } 1069 mBoxes.put(i, mTempBoxes.get(k++)); 1070 initBox(i, sizes[i + BOX_MAX]); 1071 } 1072 1073 // 6. Now give the recycled box a reasonable absolute X position. 1074 // 1075 // First try to find the first and the last box which the absolute X 1076 // position is known. 1077 int first, last; 1078 for (first = -BOX_MAX; first <= BOX_MAX; first++) { 1079 if (from.get(first) != Integer.MAX_VALUE) break; 1080 } 1081 for (last = BOX_MAX; last >= -BOX_MAX; last--) { 1082 if (from.get(last) != Integer.MAX_VALUE) break; 1083 } 1084 // If there is no box has known X position at all, make the focused one 1085 // as known. 1086 if (first > BOX_MAX) { 1087 mBoxes.get(0).mAbsoluteX = mPlatform.mCurrentX; 1088 first = last = 0; 1089 } 1090 // Now for those boxes between first and last, assign their position to 1091 // align to the previous box or the next box with known position. For 1092 // the boxes before first or after last, we will use a new default gap 1093 // size below. 1094 1095 // Align to the previous box 1096 for (int i = Math.max(0, first + 1); i < last; i++) { 1097 if (from.get(i) != Integer.MAX_VALUE) continue; 1098 Box a = mBoxes.get(i - 1); 1099 Box b = mBoxes.get(i); 1100 int wa = widthOf(a); 1101 int wb = widthOf(b); 1102 b.mAbsoluteX = a.mAbsoluteX + (wa - wa / 2) + wb / 2 1103 + getDefaultGapSize(i); 1104 if (mPopFromTop) { 1105 b.mCurrentY = -(mViewH / 2 + heightOf(b) / 2); 1106 } else { 1107 b.mCurrentY = (mViewH / 2 + heightOf(b) / 2); 1108 } 1109 } 1110 1111 // Align to the next box 1112 for (int i = Math.min(-1, last - 1); i > first; i--) { 1113 if (from.get(i) != Integer.MAX_VALUE) continue; 1114 Box a = mBoxes.get(i + 1); 1115 Box b = mBoxes.get(i); 1116 int wa = widthOf(a); 1117 int wb = widthOf(b); 1118 b.mAbsoluteX = a.mAbsoluteX - wa / 2 - (wb - wb / 2) 1119 - getDefaultGapSize(i); 1120 if (mPopFromTop) { 1121 b.mCurrentY = -(mViewH / 2 + heightOf(b) / 2); 1122 } else { 1123 b.mCurrentY = (mViewH / 2 + heightOf(b) / 2); 1124 } 1125 } 1126 1127 // 7. recycle the gaps that are not used in the new array. 1128 k = -BOX_MAX; 1129 for (int i = -BOX_MAX; i < BOX_MAX; i++) { 1130 if (mGaps.get(i) != null) continue; 1131 while (mTempGaps.get(k) == null) { 1132 k++; 1133 } 1134 mGaps.put(i, mTempGaps.get(k++)); 1135 Box a = mBoxes.get(i); 1136 Box b = mBoxes.get(i + 1); 1137 int wa = widthOf(a); 1138 int wb = widthOf(b); 1139 if (i >= first && i < last) { 1140 int g = b.mAbsoluteX - a.mAbsoluteX - wb / 2 - (wa - wa / 2); 1141 initGap(i, g); 1142 } else { 1143 initGap(i); 1144 } 1145 } 1146 1147 // 8. calculate the new absolute X coordinates for those box before 1148 // first or after last. 1149 for (int i = first - 1; i >= -BOX_MAX; i--) { 1150 Box a = mBoxes.get(i + 1); 1151 Box b = mBoxes.get(i); 1152 int wa = widthOf(a); 1153 int wb = widthOf(b); 1154 Gap g = mGaps.get(i); 1155 b.mAbsoluteX = a.mAbsoluteX - wa / 2 - (wb - wb / 2) - g.mCurrentGap; 1156 } 1157 1158 for (int i = last + 1; i <= BOX_MAX; i++) { 1159 Box a = mBoxes.get(i - 1); 1160 Box b = mBoxes.get(i); 1161 int wa = widthOf(a); 1162 int wb = widthOf(b); 1163 Gap g = mGaps.get(i - 1); 1164 b.mAbsoluteX = a.mAbsoluteX + (wa - wa / 2) + wb / 2 + g.mCurrentGap; 1165 } 1166 1167 // 9. offset the Platform position 1168 int dx = mBoxes.get(0).mAbsoluteX - mPlatform.mCurrentX; 1169 mPlatform.mCurrentX += dx; 1170 mPlatform.mFromX += dx; 1171 mPlatform.mToX += dx; 1172 mPlatform.mFlingOffset += dx; 1173 1174 if (mConstrained != constrained) { 1175 mConstrained = constrained; 1176 mPlatform.updateDefaultXY(); 1177 updateScaleAndGapLimit(); 1178 } 1179 1180 snapAndRedraw(); 1181 } 1182 1183 //////////////////////////////////////////////////////////////////////////// 1184 // Public utilities 1185 //////////////////////////////////////////////////////////////////////////// 1186 isAtMinimalScale()1187 public boolean isAtMinimalScale() { 1188 Box b = mBoxes.get(0); 1189 return isAlmostEqual(b.mCurrentScale, b.mScaleMin); 1190 } 1191 isCenter()1192 public boolean isCenter() { 1193 Box b = mBoxes.get(0); 1194 return mPlatform.mCurrentX == mPlatform.mDefaultX 1195 && b.mCurrentY == 0; 1196 } 1197 getImageWidth()1198 public int getImageWidth() { 1199 Box b = mBoxes.get(0); 1200 return b.mImageW; 1201 } 1202 getImageHeight()1203 public int getImageHeight() { 1204 Box b = mBoxes.get(0); 1205 return b.mImageH; 1206 } 1207 getImageScale()1208 public float getImageScale() { 1209 Box b = mBoxes.get(0); 1210 return b.mCurrentScale; 1211 } 1212 getImageAtEdges()1213 public int getImageAtEdges() { 1214 Box b = mBoxes.get(0); 1215 Platform p = mPlatform; 1216 calculateStableBound(b.mCurrentScale); 1217 int edges = 0; 1218 if (p.mCurrentX <= mBoundLeft) { 1219 edges |= IMAGE_AT_RIGHT_EDGE; 1220 } 1221 if (p.mCurrentX >= mBoundRight) { 1222 edges |= IMAGE_AT_LEFT_EDGE; 1223 } 1224 if (b.mCurrentY <= mBoundTop) { 1225 edges |= IMAGE_AT_BOTTOM_EDGE; 1226 } 1227 if (b.mCurrentY >= mBoundBottom) { 1228 edges |= IMAGE_AT_TOP_EDGE; 1229 } 1230 return edges; 1231 } 1232 isScrolling()1233 public boolean isScrolling() { 1234 return mPlatform.mAnimationStartTime != NO_ANIMATION 1235 && mPlatform.mCurrentX != mPlatform.mToX; 1236 } 1237 stopScrolling()1238 public void stopScrolling() { 1239 if (mPlatform.mAnimationStartTime == NO_ANIMATION) return; 1240 if (mFilmMode) mFilmScroller.forceFinished(true); 1241 mPlatform.mFromX = mPlatform.mToX = mPlatform.mCurrentX; 1242 } 1243 getFilmRatio()1244 public float getFilmRatio() { 1245 return mFilmRatio.mCurrentRatio; 1246 } 1247 setPopFromTop(boolean top)1248 public void setPopFromTop(boolean top) { 1249 mPopFromTop = top; 1250 } 1251 hasDeletingBox()1252 public boolean hasDeletingBox() { 1253 for(int i = -BOX_MAX; i <= BOX_MAX; i++) { 1254 if (mBoxes.get(i).mAnimationKind == ANIM_KIND_DELETE) { 1255 return true; 1256 } 1257 } 1258 return false; 1259 } 1260 1261 //////////////////////////////////////////////////////////////////////////// 1262 // Private utilities 1263 //////////////////////////////////////////////////////////////////////////// 1264 getMinimalScale(Box b)1265 private float getMinimalScale(Box b) { 1266 float wFactor = 1.0f; 1267 float hFactor = 1.0f; 1268 int viewW, viewH; 1269 1270 if (!mFilmMode && mConstrained && !mConstrainedFrame.isEmpty() 1271 && b == mBoxes.get(0)) { 1272 viewW = mConstrainedFrame.width(); 1273 viewH = mConstrainedFrame.height(); 1274 } else { 1275 viewW = mViewW; 1276 viewH = mViewH; 1277 } 1278 1279 if (mFilmMode) { 1280 if (mViewH > mViewW) { // portrait 1281 wFactor = FILM_MODE_PORTRAIT_WIDTH; 1282 hFactor = FILM_MODE_PORTRAIT_HEIGHT; 1283 } else { // landscape 1284 wFactor = FILM_MODE_LANDSCAPE_WIDTH; 1285 hFactor = FILM_MODE_LANDSCAPE_HEIGHT; 1286 } 1287 } 1288 1289 float s = Math.min(wFactor * viewW / b.mImageW, 1290 hFactor * viewH / b.mImageH); 1291 return Math.min(SCALE_LIMIT, s); 1292 } 1293 getMaximalScale(Box b)1294 private float getMaximalScale(Box b) { 1295 if (mFilmMode) return getMinimalScale(b); 1296 if (mConstrained && !mConstrainedFrame.isEmpty()) return getMinimalScale(b); 1297 return SCALE_LIMIT; 1298 } 1299 isAlmostEqual(float a, float b)1300 private static boolean isAlmostEqual(float a, float b) { 1301 float diff = a - b; 1302 return (diff < 0 ? -diff : diff) < 0.02f; 1303 } 1304 1305 // Calculates the stable region of mPlatform.mCurrentX and 1306 // mBoxes.get(0).mCurrentY, where "stable" means 1307 // 1308 // (1) If the dimension of scaled image >= view dimension, we will not 1309 // see black region outside the image (at that dimension). 1310 // (2) If the dimension of scaled image < view dimension, we will center 1311 // the scaled image. 1312 // 1313 // We might temporarily go out of this stable during user interaction, 1314 // but will "snap back" after user stops interaction. 1315 // 1316 // The results are stored in mBound{Left/Right/Top/Bottom}. 1317 // 1318 // An extra parameter "horizontalSlack" (which has the value of 0 usually) 1319 // is used to extend the stable region by some pixels on each side 1320 // horizontally. calculateStableBound(float scale, int horizontalSlack)1321 private void calculateStableBound(float scale, int horizontalSlack) { 1322 Box b = mBoxes.get(0); 1323 1324 // The width and height of the box in number of view pixels 1325 int w = widthOf(b, scale); 1326 int h = heightOf(b, scale); 1327 1328 // When the edge of the view is aligned with the edge of the box 1329 mBoundLeft = (mViewW + 1) / 2 - (w + 1) / 2 - horizontalSlack; 1330 mBoundRight = w / 2 - mViewW / 2 + horizontalSlack; 1331 mBoundTop = (mViewH + 1) / 2 - (h + 1) / 2; 1332 mBoundBottom = h / 2 - mViewH / 2; 1333 1334 // If the scaled height is smaller than the view height, 1335 // force it to be in the center. 1336 if (viewTallerThanScaledImage(scale)) { 1337 mBoundTop = mBoundBottom = 0; 1338 } 1339 1340 // Same for width 1341 if (viewWiderThanScaledImage(scale)) { 1342 mBoundLeft = mBoundRight = mPlatform.mDefaultX; 1343 } 1344 } 1345 calculateStableBound(float scale)1346 private void calculateStableBound(float scale) { 1347 calculateStableBound(scale, 0); 1348 } 1349 viewTallerThanScaledImage(float scale)1350 private boolean viewTallerThanScaledImage(float scale) { 1351 return mViewH >= heightOf(mBoxes.get(0), scale); 1352 } 1353 viewWiderThanScaledImage(float scale)1354 private boolean viewWiderThanScaledImage(float scale) { 1355 return mViewW >= widthOf(mBoxes.get(0), scale); 1356 } 1357 getTargetScale(Box b)1358 private float getTargetScale(Box b) { 1359 return b.mAnimationStartTime == NO_ANIMATION 1360 ? b.mCurrentScale : b.mToScale; 1361 } 1362 1363 //////////////////////////////////////////////////////////////////////////// 1364 // Animatable: an thing which can do animation. 1365 //////////////////////////////////////////////////////////////////////////// 1366 private abstract static class Animatable { 1367 public long mAnimationStartTime; 1368 public int mAnimationKind; 1369 public int mAnimationDuration; 1370 1371 // This should be overridden in subclass to change the animation values 1372 // give the progress value in [0, 1]. interpolate(float progress)1373 protected abstract boolean interpolate(float progress); startSnapback()1374 public abstract boolean startSnapback(); 1375 1376 // Returns true if the animation values changes, so things need to be 1377 // redrawn. advanceAnimation()1378 public boolean advanceAnimation() { 1379 if (mAnimationStartTime == NO_ANIMATION) { 1380 return false; 1381 } 1382 if (mAnimationStartTime == LAST_ANIMATION) { 1383 mAnimationStartTime = NO_ANIMATION; 1384 return startSnapback(); 1385 } 1386 1387 float progress; 1388 if (mAnimationDuration == 0) { 1389 progress = 1; 1390 } else { 1391 long now = AnimationTime.get(); 1392 progress = 1393 (float) (now - mAnimationStartTime) / mAnimationDuration; 1394 } 1395 1396 if (progress >= 1) { 1397 progress = 1; 1398 } else { 1399 progress = applyInterpolationCurve(mAnimationKind, progress); 1400 } 1401 1402 boolean done = interpolate(progress); 1403 1404 if (done) { 1405 mAnimationStartTime = LAST_ANIMATION; 1406 } 1407 1408 return true; 1409 } 1410 applyInterpolationCurve(int kind, float progress)1411 private static float applyInterpolationCurve(int kind, float progress) { 1412 float f = 1 - progress; 1413 switch (kind) { 1414 case ANIM_KIND_SCROLL: 1415 case ANIM_KIND_FLING: 1416 case ANIM_KIND_FLING_X: 1417 case ANIM_KIND_DELETE: 1418 case ANIM_KIND_CAPTURE: 1419 progress = 1 - f; // linear 1420 break; 1421 case ANIM_KIND_OPENING: 1422 case ANIM_KIND_SCALE: 1423 progress = 1 - f * f; // quadratic 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 x = mDefaultX; 1460 } else { 1461 calculateStableBound(scale, HORIZONTAL_SLACK); 1462 // If the picture is zoomed-in, we want to keep the focus point 1463 // stay in the same position on screen, so we need to adjust 1464 // target mCurrentX (which is the center of the focused 1465 // box). The position of the focus point on screen (relative the 1466 // the center of the view) is: 1467 // 1468 // mCurrentX + scale * mFocusX = mCurrentX' + scale' * mFocusX 1469 // => mCurrentX' = mCurrentX + (scale - scale') * mFocusX 1470 // 1471 if (!viewWiderThanScaledImage(scale)) { 1472 float scaleDiff = b.mCurrentScale - scale; 1473 x += (int) (mFocusX * scaleDiff + 0.5f); 1474 } 1475 x = Utils.clamp(x, mBoundLeft, mBoundRight); 1476 } 1477 if (mCurrentX != x || mCurrentY != y) { 1478 return doAnimation(x, y, ANIM_KIND_SNAPBACK); 1479 } 1480 return false; 1481 } 1482 1483 // The updateDefaultXY() should be called whenever these variables 1484 // changes: (1) mConstrained (2) mConstrainedFrame (3) mViewW/H (4) 1485 // mFilmMode updateDefaultXY()1486 public void updateDefaultXY() { 1487 // We don't check mFilmMode and return 0 for mDefaultX. Because 1488 // otherwise if we decide to leave film mode because we are 1489 // centered, we will immediately back into film mode because we find 1490 // we are not centered. 1491 if (mConstrained && !mConstrainedFrame.isEmpty()) { 1492 mDefaultX = mConstrainedFrame.centerX() - mViewW / 2; 1493 mDefaultY = mFilmMode ? 0 : 1494 mConstrainedFrame.centerY() - mViewH / 2; 1495 } else { 1496 mDefaultX = 0; 1497 mDefaultY = 0; 1498 } 1499 } 1500 1501 // Starts an animation for the platform. doAnimation(int targetX, int targetY, int kind)1502 private boolean doAnimation(int targetX, int targetY, int kind) { 1503 if (mCurrentX == targetX && mCurrentY == targetY) return false; 1504 mAnimationKind = kind; 1505 mFromX = mCurrentX; 1506 mFromY = mCurrentY; 1507 mToX = targetX; 1508 mToY = targetY; 1509 mAnimationStartTime = AnimationTime.startTime(); 1510 mAnimationDuration = ANIM_TIME[kind]; 1511 mFlingOffset = 0; 1512 advanceAnimation(); 1513 return true; 1514 } 1515 1516 @Override interpolate(float progress)1517 protected boolean interpolate(float progress) { 1518 if (mAnimationKind == ANIM_KIND_FLING) { 1519 return interpolateFlingPage(progress); 1520 } else if (mAnimationKind == ANIM_KIND_FLING_X) { 1521 return interpolateFlingFilm(progress); 1522 } else { 1523 return interpolateLinear(progress); 1524 } 1525 } 1526 interpolateFlingFilm(float progress)1527 private boolean interpolateFlingFilm(float progress) { 1528 mFilmScroller.computeScrollOffset(); 1529 mCurrentX = mFilmScroller.getCurrX() + mFlingOffset; 1530 1531 int dir = EdgeView.INVALID_DIRECTION; 1532 if (mCurrentX < mDefaultX) { 1533 if (!mHasNext) { 1534 dir = EdgeView.RIGHT; 1535 } 1536 } else if (mCurrentX > mDefaultX) { 1537 if (!mHasPrev) { 1538 dir = EdgeView.LEFT; 1539 } 1540 } 1541 if (dir != EdgeView.INVALID_DIRECTION) { 1542 // TODO: restore this onAbsorb call 1543 //int v = (int) (mFilmScroller.getCurrVelocity() + 0.5f); 1544 //mListener.onAbsorb(v, dir); 1545 mFilmScroller.forceFinished(true); 1546 mCurrentX = mDefaultX; 1547 } 1548 return mFilmScroller.isFinished(); 1549 } 1550 interpolateFlingPage(float progress)1551 private boolean interpolateFlingPage(float progress) { 1552 mPageScroller.computeScrollOffset(progress); 1553 Box b = mBoxes.get(0); 1554 calculateStableBound(b.mCurrentScale); 1555 1556 int oldX = mCurrentX; 1557 mCurrentX = mPageScroller.getCurrX(); 1558 1559 // Check if we hit the edges; show edge effects if we do. 1560 if (oldX > mBoundLeft && mCurrentX == mBoundLeft) { 1561 int v = (int) (-mPageScroller.getCurrVelocityX() + 0.5f); 1562 mListener.onAbsorb(v, EdgeView.RIGHT); 1563 } else if (oldX < mBoundRight && mCurrentX == mBoundRight) { 1564 int v = (int) (mPageScroller.getCurrVelocityX() + 0.5f); 1565 mListener.onAbsorb(v, EdgeView.LEFT); 1566 } 1567 1568 return progress >= 1; 1569 } 1570 interpolateLinear(float progress)1571 private boolean interpolateLinear(float progress) { 1572 // Other animations 1573 if (progress >= 1) { 1574 mCurrentX = mToX; 1575 mCurrentY = mToY; 1576 return true; 1577 } else { 1578 if (mAnimationKind == ANIM_KIND_CAPTURE) { 1579 progress = CaptureAnimation.calculateSlide(progress); 1580 } 1581 mCurrentX = (int) (mFromX + progress * (mToX - mFromX)); 1582 mCurrentY = (int) (mFromY + progress * (mToY - mFromY)); 1583 if (mAnimationKind == ANIM_KIND_CAPTURE) { 1584 return false; 1585 } else { 1586 return (mCurrentX == mToX && mCurrentY == mToY); 1587 } 1588 } 1589 } 1590 } 1591 1592 //////////////////////////////////////////////////////////////////////////// 1593 // Box: represents a rectangular area which shows a picture. 1594 //////////////////////////////////////////////////////////////////////////// 1595 private class Box extends Animatable { 1596 // Size of the bitmap 1597 public int mImageW, mImageH; 1598 1599 // This is true if we assume the image size is the same as view size 1600 // until we know the actual size of image. This is also used to 1601 // determine if there is an image ready to show. 1602 public boolean mUseViewSize; 1603 1604 // The minimum and maximum scale we allow for this box. 1605 public float mScaleMin, mScaleMax; 1606 1607 // The X/Y value indicates where the center of the box is on the view 1608 // coordinate. We always keep the mCurrent{X,Y,Scale} sync with the 1609 // actual values used currently. Note that the X values are implicitly 1610 // defined by Platform and Gaps. 1611 public int mCurrentY, mFromY, mToY; 1612 public float mCurrentScale, mFromScale, mToScale; 1613 1614 // The absolute X coordinate of the center of the box. This is only used 1615 // during moveBox(). 1616 public int mAbsoluteX; 1617 1618 @Override startSnapback()1619 public boolean startSnapback() { 1620 if (mAnimationStartTime != NO_ANIMATION) return false; 1621 if (mAnimationKind == ANIM_KIND_SCROLL 1622 && mListener.isHoldingDown()) return false; 1623 if (mAnimationKind == ANIM_KIND_DELETE 1624 && mListener.isHoldingDelete()) return false; 1625 if (mInScale && this == mBoxes.get(0)) return false; 1626 1627 int y = mCurrentY; 1628 float scale; 1629 1630 if (this == mBoxes.get(0)) { 1631 float scaleMin = mExtraScalingRange ? 1632 mScaleMin * SCALE_MIN_EXTRA : mScaleMin; 1633 float scaleMax = mExtraScalingRange ? 1634 mScaleMax * SCALE_MAX_EXTRA : mScaleMax; 1635 scale = Utils.clamp(mCurrentScale, scaleMin, scaleMax); 1636 if (mFilmMode) { 1637 y = 0; 1638 } else { 1639 calculateStableBound(scale, HORIZONTAL_SLACK); 1640 // If the picture is zoomed-in, we want to keep the focus 1641 // point stay in the same position on screen. See the 1642 // comment in Platform.startSnapback for details. 1643 if (!viewTallerThanScaledImage(scale)) { 1644 float scaleDiff = mCurrentScale - scale; 1645 y += (int) (mFocusY * scaleDiff + 0.5f); 1646 } 1647 y = Utils.clamp(y, mBoundTop, mBoundBottom); 1648 } 1649 } else { 1650 y = 0; 1651 scale = mScaleMin; 1652 } 1653 1654 if (mCurrentY != y || mCurrentScale != scale) { 1655 return doAnimation(y, scale, ANIM_KIND_SNAPBACK); 1656 } 1657 return false; 1658 } 1659 doAnimation(int targetY, float targetScale, int kind)1660 private boolean doAnimation(int targetY, float targetScale, int kind) { 1661 targetScale = clampScale(targetScale); 1662 1663 if (mCurrentY == targetY && mCurrentScale == targetScale 1664 && kind != ANIM_KIND_CAPTURE) { 1665 return false; 1666 } 1667 1668 // Now starts an animation for the box. 1669 mAnimationKind = kind; 1670 mFromY = mCurrentY; 1671 mFromScale = mCurrentScale; 1672 mToY = targetY; 1673 mToScale = targetScale; 1674 mAnimationStartTime = AnimationTime.startTime(); 1675 mAnimationDuration = ANIM_TIME[kind]; 1676 advanceAnimation(); 1677 return true; 1678 } 1679 1680 // Clamps the input scale to the range that doAnimation() can reach. clampScale(float s)1681 public float clampScale(float s) { 1682 return Utils.clamp(s, 1683 SCALE_MIN_EXTRA * mScaleMin, 1684 SCALE_MAX_EXTRA * mScaleMax); 1685 } 1686 1687 @Override interpolate(float progress)1688 protected boolean interpolate(float progress) { 1689 if (mAnimationKind == ANIM_KIND_FLING) { 1690 return interpolateFlingPage(progress); 1691 } else { 1692 return interpolateLinear(progress); 1693 } 1694 } 1695 interpolateFlingPage(float progress)1696 private boolean interpolateFlingPage(float progress) { 1697 mPageScroller.computeScrollOffset(progress); 1698 calculateStableBound(mCurrentScale); 1699 1700 int oldY = mCurrentY; 1701 mCurrentY = mPageScroller.getCurrY(); 1702 1703 // Check if we hit the edges; show edge effects if we do. 1704 if (oldY > mBoundTop && mCurrentY == mBoundTop) { 1705 int v = (int) (-mPageScroller.getCurrVelocityY() + 0.5f); 1706 mListener.onAbsorb(v, EdgeView.BOTTOM); 1707 } else if (oldY < mBoundBottom && mCurrentY == mBoundBottom) { 1708 int v = (int) (mPageScroller.getCurrVelocityY() + 0.5f); 1709 mListener.onAbsorb(v, EdgeView.TOP); 1710 } 1711 1712 return progress >= 1; 1713 } 1714 interpolateLinear(float progress)1715 private boolean interpolateLinear(float progress) { 1716 if (progress >= 1) { 1717 mCurrentY = mToY; 1718 mCurrentScale = mToScale; 1719 return true; 1720 } else { 1721 mCurrentY = (int) (mFromY + progress * (mToY - mFromY)); 1722 mCurrentScale = mFromScale + progress * (mToScale - mFromScale); 1723 if (mAnimationKind == ANIM_KIND_CAPTURE) { 1724 float f = CaptureAnimation.calculateScale(progress); 1725 mCurrentScale *= f; 1726 return false; 1727 } else { 1728 return (mCurrentY == mToY && mCurrentScale == mToScale); 1729 } 1730 } 1731 } 1732 } 1733 1734 //////////////////////////////////////////////////////////////////////////// 1735 // Gap: represents a rectangular area which is between two boxes. 1736 //////////////////////////////////////////////////////////////////////////// 1737 private class Gap extends Animatable { 1738 // The default gap size between two boxes. The value may vary for 1739 // different image size of the boxes and for different modes (page or 1740 // film). 1741 public int mDefaultSize; 1742 1743 // The gap size between the two boxes. 1744 public int mCurrentGap, mFromGap, mToGap; 1745 1746 @Override startSnapback()1747 public boolean startSnapback() { 1748 if (mAnimationStartTime != NO_ANIMATION) return false; 1749 return doAnimation(mDefaultSize, ANIM_KIND_SNAPBACK); 1750 } 1751 1752 // Starts an animation for a gap. doAnimation(int targetSize, int kind)1753 public boolean doAnimation(int targetSize, int kind) { 1754 if (mCurrentGap == targetSize && kind != ANIM_KIND_CAPTURE) { 1755 return false; 1756 } 1757 mAnimationKind = kind; 1758 mFromGap = mCurrentGap; 1759 mToGap = targetSize; 1760 mAnimationStartTime = AnimationTime.startTime(); 1761 mAnimationDuration = ANIM_TIME[mAnimationKind]; 1762 advanceAnimation(); 1763 return true; 1764 } 1765 1766 @Override interpolate(float progress)1767 protected boolean interpolate(float progress) { 1768 if (progress >= 1) { 1769 mCurrentGap = mToGap; 1770 return true; 1771 } else { 1772 mCurrentGap = (int) (mFromGap + progress * (mToGap - mFromGap)); 1773 if (mAnimationKind == ANIM_KIND_CAPTURE) { 1774 float f = CaptureAnimation.calculateScale(progress); 1775 mCurrentGap = (int) (mCurrentGap * f); 1776 return false; 1777 } else { 1778 return (mCurrentGap == mToGap); 1779 } 1780 } 1781 } 1782 } 1783 1784 //////////////////////////////////////////////////////////////////////////// 1785 // FilmRatio: represents the progress of film mode change. 1786 //////////////////////////////////////////////////////////////////////////// 1787 private class FilmRatio extends Animatable { 1788 // The film ratio: 1 means switching to film mode is complete, 0 means 1789 // switching to page mode is complete. 1790 public float mCurrentRatio, mFromRatio, mToRatio; 1791 1792 @Override startSnapback()1793 public boolean startSnapback() { 1794 float target = mFilmMode ? 1f : 0f; 1795 if (target == mToRatio) return false; 1796 return doAnimation(target, ANIM_KIND_SNAPBACK); 1797 } 1798 1799 // Starts an animation for the film ratio. doAnimation(float targetRatio, int kind)1800 private boolean doAnimation(float targetRatio, int kind) { 1801 mAnimationKind = kind; 1802 mFromRatio = mCurrentRatio; 1803 mToRatio = targetRatio; 1804 mAnimationStartTime = AnimationTime.startTime(); 1805 mAnimationDuration = ANIM_TIME[mAnimationKind]; 1806 advanceAnimation(); 1807 return true; 1808 } 1809 1810 @Override interpolate(float progress)1811 protected boolean interpolate(float progress) { 1812 if (progress >= 1) { 1813 mCurrentRatio = mToRatio; 1814 return true; 1815 } else { 1816 mCurrentRatio = mFromRatio + progress * (mToRatio - mFromRatio); 1817 return (mCurrentRatio == mToRatio); 1818 } 1819 } 1820 } 1821 } 1822