PositionController.java revision 642561dd3177f9511c8bbeb90839066ecc451054
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 17package com.android.gallery3d.ui; 18 19import android.content.Context; 20import android.graphics.Rect; 21import android.util.Log; 22import android.widget.Scroller; 23 24import com.android.gallery3d.common.Utils; 25import com.android.gallery3d.util.GalleryUtils; 26import com.android.gallery3d.util.RangeArray; 27import com.android.gallery3d.util.RangeIntArray; 28 29class PositionController { 30 private static final String TAG = "PositionController"; 31 32 public static final int IMAGE_AT_LEFT_EDGE = 1; 33 public static final int IMAGE_AT_RIGHT_EDGE = 2; 34 public static final int IMAGE_AT_TOP_EDGE = 4; 35 public static final int IMAGE_AT_BOTTOM_EDGE = 8; 36 37 // Special values for animation time. 38 private static final long NO_ANIMATION = -1; 39 private static final long LAST_ANIMATION = -2; 40 41 private static final int ANIM_KIND_SCROLL = 0; 42 private static final int ANIM_KIND_SCALE = 1; 43 private static final int ANIM_KIND_SNAPBACK = 2; 44 private static final int ANIM_KIND_SLIDE = 3; 45 private static final int ANIM_KIND_ZOOM = 4; 46 private static final int ANIM_KIND_OPENING = 5; 47 private static final int ANIM_KIND_FLING = 6; 48 49 // Animation time in milliseconds. The order must match ANIM_KIND_* above. 50 private static final int ANIM_TIME[] = { 51 0, // ANIM_KIND_SCROLL 52 50, // ANIM_KIND_SCALE 53 600, // ANIM_KIND_SNAPBACK 54 400, // ANIM_KIND_SLIDE 55 300, // ANIM_KIND_ZOOM 56 600, // ANIM_KIND_OPENING 57 0, // ANIM_KIND_FLING (the duration is calculated dynamically) 58 }; 59 60 // We try to scale up the image to fill the screen. But in order not to 61 // scale too much for small icons, we limit the max up-scaling factor here. 62 private static final float SCALE_LIMIT = 4; 63 64 // For user's gestures, we give a temporary extra scaling range which goes 65 // above or below the usual scaling limits. 66 private static final float SCALE_MIN_EXTRA = 0.7f; 67 private static final float SCALE_MAX_EXTRA = 1.4f; 68 69 // Setting this true makes the extra scaling range permanent (until this is 70 // set to false again). 71 private boolean mExtraScalingRange = false; 72 73 // Film Mode v.s. Page Mode: in film mode we show smaller pictures. 74 private boolean mFilmMode = false; 75 76 // These are the limits for width / height of the picture in film mode. 77 private static final float FILM_MODE_PORTRAIT_HEIGHT = 0.48f; 78 private static final float FILM_MODE_PORTRAIT_WIDTH = 0.7f; 79 private static final float FILM_MODE_LANDSCAPE_HEIGHT = 0.7f; 80 private static final float FILM_MODE_LANDSCAPE_WIDTH = 0.7f; 81 82 // In addition to the focused box (index == 0). We also keep information 83 // about this many boxes on each side. 84 private static final int BOX_MAX = PhotoView.SCREEN_NAIL_MAX; 85 86 public static final int IMAGE_GAP = GalleryUtils.dpToPixel(16); 87 private static final int HORIZONTAL_SLACK = GalleryUtils.dpToPixel(12); 88 89 private Listener mListener; 90 private volatile Rect mOpenAnimationRect; 91 private int mViewW = 640; 92 private int mViewH = 480;; 93 94 // A scaling guesture is in progress. 95 private boolean mInScale; 96 // The focus point of the scaling gesture, relative to the center of the 97 // picture in bitmap pixels. 98 private float mFocusX, mFocusY; 99 100 // This is used by the fling animation (page mode). 101 private FlingScroller mPageScroller; 102 103 // This is used by the fling animation (film mode). 104 private Scroller mFilmScroller; 105 106 // The bound of the stable region that the focused box can stay, see the 107 // comments above calculateStableBound() for details. 108 private int mBoundLeft, mBoundRight, mBoundTop, mBoundBottom; 109 110 // 111 // ___________________________________________________________ 112 // | _____ _____ _____ _____ _____ | 113 // | | | | | | | | | | | | 114 // | | Box | | Box | | Box*| | Box | | Box | | 115 // | |_____|.....|_____|.....|_____|.....|_____|.....|_____| | 116 // | Gap Gap Gap Gap | 117 // |___________________________________________________________| 118 // 119 // <-- Platform --> 120 // 121 // The focused box (Box*) centers at mPlatform.mCurrentX 122 123 private Platform mPlatform = new Platform(); 124 private RangeArray<Box> mBoxes = new RangeArray<Box>(-BOX_MAX, BOX_MAX); 125 // The gap at the right of a Box i is at index i. The gap at the left of a 126 // Box i is at index i - 1. 127 private RangeArray<Gap> mGaps = new RangeArray<Gap>(-BOX_MAX, BOX_MAX - 1); 128 129 // These are only used during moveBox(). 130 private RangeArray<Box> mTempBoxes = new RangeArray<Box>(-BOX_MAX, BOX_MAX); 131 private RangeArray<Gap> mTempGaps = new RangeArray<Gap>(-BOX_MAX, BOX_MAX - 1); 132 133 // The output of the PositionController. Available throught getPosition(). 134 private RangeArray<Rect> mRects = new RangeArray<Rect>(-BOX_MAX, BOX_MAX); 135 136 public interface Listener { 137 void invalidate(); 138 boolean isDown(); 139 140 // EdgeView 141 void onPull(int offset, int direction); 142 void onRelease(); 143 void onAbsorb(int velocity, int direction); 144 } 145 146 public PositionController(Context context, Listener listener) { 147 mListener = listener; 148 mPageScroller = new FlingScroller(); 149 mFilmScroller = new Scroller(context); 150 151 // Initialize the areas. 152 initPlatform(); 153 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 154 mBoxes.put(i, new Box()); 155 initBox(i); 156 mRects.put(i, new Rect()); 157 } 158 for (int i = -BOX_MAX; i < BOX_MAX; i++) { 159 mGaps.put(i, new Gap()); 160 initGap(i); 161 } 162 } 163 164 public void setOpenAnimationRect(Rect r) { 165 mOpenAnimationRect = r; 166 } 167 168 public void setViewSize(int viewW, int viewH) { 169 if (viewW == mViewW && viewH == mViewH) return; 170 171 mViewW = viewW; 172 mViewH = viewH; 173 initPlatform(); 174 175 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 176 setBoxSize(i, viewW, viewH, true); 177 } 178 179 updateScaleAndGapLimit(); 180 snapAndRedraw(); 181 } 182 183 public void setImageSize(int index, int width, int height) { 184 if (width == 0 || height == 0) { 185 initBox(index); 186 } else { 187 setBoxSize(index, width, height, false); 188 } 189 190 updateScaleAndGapLimit(); 191 startOpeningAnimationIfNeeded(); 192 snapAndRedraw(); 193 } 194 195 private void setBoxSize(int i, int width, int height, boolean isViewSize) { 196 Box b = mBoxes.get(i); 197 198 // If we already have image size, we don't want to use the view size. 199 if (isViewSize && !b.mUseViewSize) return; 200 b.mUseViewSize = isViewSize; 201 202 if (width == b.mImageW && height == b.mImageH) { 203 return; 204 } 205 206 // The ratio of the old size and the new size. 207 float ratio = Math.min( 208 (float) b.mImageW / width, (float) b.mImageH / height); 209 210 b.mCurrentScale *= ratio; 211 b.mFromScale *= ratio; 212 b.mToScale *= ratio; 213 214 b.mImageW = width; 215 b.mImageH = height; 216 217 if (i == 0) { 218 mFocusX /= ratio; 219 mFocusY /= ratio; 220 } 221 } 222 223 private void startOpeningAnimationIfNeeded() { 224 if (mOpenAnimationRect == null) return; 225 Box b = mBoxes.get(0); 226 if (b.mUseViewSize) return; 227 228 // Start animation from the saved rectangle if we have one. 229 Rect r = mOpenAnimationRect; 230 mOpenAnimationRect = null; 231 mPlatform.mCurrentX = r.centerX(); 232 b.mCurrentY = r.centerY(); 233 b.mCurrentScale = Math.max(r.width() / (float) b.mImageW, 234 r.height() / (float) b.mImageH); 235 startAnimation(mViewW / 2, mViewH / 2, b.mScaleMin, ANIM_KIND_OPENING); 236 } 237 238 public void setFilmMode(boolean enabled) { 239 if (enabled == mFilmMode) return; 240 mFilmMode = enabled; 241 242 updateScaleAndGapLimit(); 243 stopAnimation(); 244 snapAndRedraw(); 245 } 246 247 public void setExtraScalingRange(boolean enabled) { 248 if (mExtraScalingRange == enabled) return; 249 mExtraScalingRange = enabled; 250 if (!enabled) { 251 snapAndRedraw(); 252 } 253 } 254 255 // This should be called whenever the scale range of boxes or the default 256 // gap size may change. Currently this can happen due to change of view 257 // size, image size, and mode. 258 private void updateScaleAndGapLimit() { 259 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 260 Box b = mBoxes.get(i); 261 b.mScaleMin = getMinimalScale(b.mImageW, b.mImageH); 262 b.mScaleMax = getMaximalScale(b.mImageW, b.mImageH); 263 } 264 265 for (int i = -BOX_MAX; i < BOX_MAX; i++) { 266 Gap g = mGaps.get(i); 267 g.mDefaultSize = getDefaultGapSize(i); 268 } 269 } 270 271 // Returns the default gap size according the the size of the boxes around 272 // the gap and the current mode. 273 private int getDefaultGapSize(int i) { 274 if (mFilmMode) return IMAGE_GAP; 275 Box a = mBoxes.get(i); 276 Box b = mBoxes.get(i + 1); 277 return IMAGE_GAP + Math.max(gapToSide(a), gapToSide(b)); 278 } 279 280 // Here is how we layout the boxes in the page mode. 281 // 282 // previous current next 283 // ___________ ________________ __________ 284 // | _______ | | __________ | | ______ | 285 // | | | | | | right->| | | | | | 286 // | | |<-------->|<--left | | | | | | 287 // | |_______| | | | |__________| | | |______| | 288 // |___________| | |________________| |__________| 289 // | <--> gapToSide() 290 // | 291 // IMAGE_GAP + MAX(gapToSide(previous), gapToSide(current)) 292 private int gapToSide(Box b) { 293 return (int) ((mViewW - getMinimalScale(b) * b.mImageW) / 2 + 0.5f); 294 } 295 296 // Stop all animations at where they are now. 297 public void stopAnimation() { 298 mPlatform.mAnimationStartTime = NO_ANIMATION; 299 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 300 mBoxes.get(i).mAnimationStartTime = NO_ANIMATION; 301 } 302 for (int i = -BOX_MAX; i < BOX_MAX; i++) { 303 mGaps.get(i).mAnimationStartTime = NO_ANIMATION; 304 } 305 } 306 307 public void skipAnimation() { 308 if (mPlatform.mAnimationStartTime != NO_ANIMATION) { 309 mPlatform.mCurrentX = mPlatform.mToX; 310 mPlatform.mAnimationStartTime = NO_ANIMATION; 311 } 312 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 313 Box b = mBoxes.get(i); 314 if (b.mAnimationStartTime == NO_ANIMATION) continue; 315 b.mCurrentY = b.mToY; 316 b.mCurrentScale = b.mToScale; 317 b.mAnimationStartTime = NO_ANIMATION; 318 } 319 for (int i = -BOX_MAX; i < BOX_MAX; i++) { 320 Gap g = mGaps.get(i); 321 if (g.mAnimationStartTime == NO_ANIMATION) continue; 322 g.mCurrentGap = g.mToGap; 323 g.mAnimationStartTime = NO_ANIMATION; 324 } 325 redraw(); 326 } 327 328 public void up() { 329 snapAndRedraw(); 330 } 331 332 //////////////////////////////////////////////////////////////////////////// 333 // Start an animations for the focused box 334 //////////////////////////////////////////////////////////////////////////// 335 336 public void zoomIn(float tapX, float tapY, float targetScale) { 337 Box b = mBoxes.get(0); 338 339 // Convert the tap position to distance to center in bitmap coordinates 340 float tempX = (tapX - mPlatform.mCurrentX) / b.mCurrentScale; 341 float tempY = (tapY - b.mCurrentY) / b.mCurrentScale; 342 343 int x = (int) (mViewW / 2 - tempX * targetScale + 0.5f); 344 int y = (int) (mViewH / 2 - tempY * targetScale + 0.5f); 345 346 calculateStableBound(targetScale); 347 int targetX = Utils.clamp(x, mBoundLeft, mBoundRight); 348 int targetY = Utils.clamp(y, mBoundTop, mBoundBottom); 349 targetScale = Utils.clamp(targetScale, b.mScaleMin, b.mScaleMax); 350 351 startAnimation(targetX, targetY, targetScale, ANIM_KIND_ZOOM); 352 } 353 354 public void resetToFullView() { 355 Box b = mBoxes.get(0); 356 startAnimation(mViewW / 2, mViewH / 2, b.mScaleMin, ANIM_KIND_ZOOM); 357 } 358 359 public void beginScale(float focusX, float focusY) { 360 Box b = mBoxes.get(0); 361 Platform p = mPlatform; 362 mInScale = true; 363 mFocusX = (int) ((focusX - p.mCurrentX) / b.mCurrentScale + 0.5f); 364 mFocusY = (int) ((focusY - b.mCurrentY) / b.mCurrentScale + 0.5f); 365 } 366 367 // Scales the image by the given factor. 368 // Returns an out-of-range indicator: 369 // 1 if the intended scale is too large for the stable range. 370 // 0 if the intended scale is in the stable range. 371 // -1 if the intended scale is too small for the stable range. 372 public int scaleBy(float s, float focusX, float focusY) { 373 Box b = mBoxes.get(0); 374 Platform p = mPlatform; 375 376 // We want to keep the focus point (on the bitmap) the same as when we 377 // begin the scale guesture, that is, 378 // 379 // (focusX' - currentX') / scale' = (focusX - currentX) / scale 380 // 381 s *= getTargetScale(b); 382 int x = mFilmMode ? p.mCurrentX : (int) (focusX - s * mFocusX + 0.5f); 383 int y = mFilmMode ? b.mCurrentY : (int) (focusY - s * mFocusY + 0.5f); 384 startAnimation(x, y, s, ANIM_KIND_SCALE); 385 if (s < b.mScaleMin) return -1; 386 if (s > b.mScaleMax) return 1; 387 return 0; 388 } 389 390 public void endScale() { 391 mInScale = false; 392 snapAndRedraw(); 393 } 394 395 public void startHorizontalSlide(int distance) { 396 Box b = mBoxes.get(0); 397 Platform p = mPlatform; 398 startAnimation(getTargetX(p) + distance, getTargetY(b), 399 b.mCurrentScale, ANIM_KIND_SLIDE); 400 } 401 402 public void startScroll(float dx, float dy) { 403 boolean hasPrev = hasPrevImages(); 404 boolean hasNext = hasNextImages(); 405 406 Box b = mBoxes.get(0); 407 Platform p = mPlatform; 408 409 int x = getTargetX(p) + (int) (dx + 0.5f); 410 int y = getTargetY(b) + (int) (dy + 0.5f); 411 412 if (mFilmMode) { 413 scrollToFilm(x, y, hasPrev, hasNext); 414 } else { 415 scrollToPage(x, y, hasPrev, hasNext); 416 } 417 } 418 419 private void scrollToPage(int x, int y, boolean hasPrev, boolean hasNext) { 420 Box b = mBoxes.get(0); 421 422 calculateStableBound(b.mCurrentScale); 423 424 // Vertical direction: If we have space to move in the vertical 425 // direction, we show the edge effect when scrolling reaches the edge. 426 if (mBoundTop != mBoundBottom) { 427 if (y < mBoundTop) { 428 mListener.onPull(mBoundTop - y, EdgeView.BOTTOM); 429 } else if (y > mBoundBottom) { 430 mListener.onPull(y - mBoundBottom, EdgeView.TOP); 431 } 432 } 433 434 y = Utils.clamp(y, mBoundTop, mBoundBottom); 435 436 // Horizontal direction: we show the edge effect when the scrolling 437 // tries to go left of the first image or go right of the last image. 438 if (!hasPrev && x > mBoundRight) { 439 int pixels = x - mBoundRight; 440 mListener.onPull(pixels, EdgeView.LEFT); 441 x = mBoundRight; 442 } else if (!hasNext && x < mBoundLeft) { 443 int pixels = mBoundLeft - x; 444 mListener.onPull(pixels, EdgeView.RIGHT); 445 x = mBoundLeft; 446 } 447 448 startAnimation(x, y, b.mCurrentScale, ANIM_KIND_SCROLL); 449 } 450 451 private void scrollToFilm(int x, int y, boolean hasPrev, boolean hasNext) { 452 Box b = mBoxes.get(0); 453 454 // Horizontal direction: we show the edge effect when the scrolling 455 // tries to go left of the first image or go right of the last image. 456 int cx = mViewW / 2; 457 if (!hasPrev && x > cx) { 458 int pixels = x - cx; 459 mListener.onPull(pixels, EdgeView.LEFT); 460 x = cx; 461 } else if (!hasNext && x < cx) { 462 int pixels = cx - x; 463 mListener.onPull(pixels, EdgeView.RIGHT); 464 x = cx; 465 } 466 467 startAnimation(x, y, b.mCurrentScale, ANIM_KIND_SCROLL); 468 } 469 470 public boolean fling(float velocityX, float velocityY) { 471 int vx = (int) (velocityX + 0.5f); 472 int vy = (int) (velocityY + 0.5f); 473 return mFilmMode ? flingFilm(vx, vy) : flingPage(vx, vy); 474 } 475 476 private boolean flingPage(int velocityX, int velocityY) { 477 Box b = mBoxes.get(0); 478 Platform p = mPlatform; 479 480 // We only want to do fling when the picture is zoomed-in. 481 if (viewWiderThanScaledImage(b.mCurrentScale) && 482 viewTallerThanScaledImage(b.mCurrentScale)) { 483 return false; 484 } 485 486 // We only allow flinging in the directions where it won't go over the 487 // picture. 488 int edges = getImageAtEdges(); 489 if ((velocityX > 0 && (edges & IMAGE_AT_LEFT_EDGE) != 0) || 490 (velocityX < 0 && (edges & IMAGE_AT_RIGHT_EDGE) != 0)) { 491 velocityX = 0; 492 } 493 if ((velocityY > 0 && (edges & IMAGE_AT_TOP_EDGE) != 0) || 494 (velocityY < 0 && (edges & IMAGE_AT_BOTTOM_EDGE) != 0)) { 495 velocityY = 0; 496 } 497 498 if (velocityX == 0 && velocityY == 0) return false; 499 500 mPageScroller.fling(p.mCurrentX, b.mCurrentY, velocityX, velocityY, 501 mBoundLeft, mBoundRight, mBoundTop, mBoundBottom); 502 int targetX = mPageScroller.getFinalX(); 503 int targetY = mPageScroller.getFinalY(); 504 ANIM_TIME[ANIM_KIND_FLING] = mPageScroller.getDuration(); 505 startAnimation(targetX, targetY, b.mCurrentScale, ANIM_KIND_FLING); 506 return true; 507 } 508 509 private boolean flingFilm(int velocityX, int velocityY) { 510 boolean hasPrev = hasPrevImages(); 511 boolean hasNext = hasNextImages(); 512 513 Box b = mBoxes.get(0); 514 Platform p = mPlatform; 515 516 // If we are already at the edge, don't start the fling. 517 int cx = mViewW / 2; 518 if ((!hasPrev && p.mCurrentX >= cx) || (!hasNext && p.mCurrentX <= cx)) { 519 return false; 520 } 521 522 if (velocityX == 0) return false; 523 524 mFilmScroller.fling(p.mCurrentX, 0, velocityX, 0, 525 Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0); 526 int targetX = mFilmScroller.getFinalX(); 527 ANIM_TIME[ANIM_KIND_FLING] = mFilmScroller.getDuration(); 528 startAnimation(targetX, b.mCurrentY, b.mCurrentScale, ANIM_KIND_FLING); 529 return true; 530 } 531 532 //////////////////////////////////////////////////////////////////////////// 533 // Redraw 534 // 535 // If a method changes box positions directly, redraw() 536 // should be called. 537 // 538 // If a method may also cause a snapback to happen, snapAndRedraw() should 539 // be called. 540 // 541 // If a method starts an animation to change the position of focused box, 542 // startAnimation() should be called. 543 // 544 // If time advances to change the box position, advanceAnimation() should 545 // be called. 546 //////////////////////////////////////////////////////////////////////////// 547 private void redraw() { 548 layoutAndSetPosition(); 549 mListener.invalidate(); 550 } 551 552 private void snapAndRedraw() { 553 mPlatform.startSnapback(); 554 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 555 mBoxes.get(i).startSnapback(); 556 } 557 for (int i = -BOX_MAX; i < BOX_MAX; i++) { 558 mGaps.get(i).startSnapback(); 559 } 560 redraw(); 561 } 562 563 private void startAnimation(int targetX, int targetY, float targetScale, 564 int kind) { 565 boolean changed = false; 566 changed |= mPlatform.doAnimation(targetX, kind); 567 changed |= mBoxes.get(0).doAnimation(targetY, targetScale, kind); 568 if (changed) redraw(); 569 } 570 571 public boolean advanceAnimation() { 572 boolean changed = false; 573 changed |= mPlatform.advanceAnimation(); 574 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 575 changed |= mBoxes.get(i).advanceAnimation(); 576 } 577 for (int i = -BOX_MAX; i < BOX_MAX; i++) { 578 changed |= mGaps.get(i).advanceAnimation(); 579 } 580 if (changed) redraw(); 581 return changed; 582 } 583 584 //////////////////////////////////////////////////////////////////////////// 585 // Layout 586 //////////////////////////////////////////////////////////////////////////// 587 588 // Returns the display width of this box. 589 private int widthOf(Box b) { 590 return (int) (b.mImageW * b.mCurrentScale + 0.5f); 591 } 592 593 // Returns the display height of this box. 594 private int heightOf(Box b) { 595 return (int) (b.mImageH * b.mCurrentScale + 0.5f); 596 } 597 598 // Returns the display width of this box, using the given scale. 599 private int widthOf(Box b, float scale) { 600 return (int) (b.mImageW * scale + 0.5f); 601 } 602 603 // Returns the display height of this box, using the given scale. 604 private int heightOf(Box b, float scale) { 605 return (int) (b.mImageH * scale + 0.5f); 606 } 607 608 // Convert the information in mPlatform and mBoxes to mRects, so the user 609 // can get the position of each box by getPosition(). 610 // 611 // Note the loop index goes from inside-out because each box's X coordinate 612 // is relative to its anchor box (except the focused box). 613 private void layoutAndSetPosition() { 614 // layout box 0 (focused box) 615 convertBoxToRect(0); 616 for (int i = 1; i <= BOX_MAX; i++) { 617 // layout box i and -i 618 convertBoxToRect(i); 619 convertBoxToRect(-i); 620 } 621 //dumpState(); 622 } 623 624 private void dumpState() { 625 for (int i = -BOX_MAX; i < BOX_MAX; i++) { 626 Log.d(TAG, "Gap " + i + ": " + mGaps.get(i).mCurrentGap); 627 } 628 629 dumpRect(0); 630 for (int i = 1; i <= BOX_MAX; i++) { 631 dumpRect(i); 632 dumpRect(-i); 633 } 634 635 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 636 for (int j = i + 1; j <= BOX_MAX; j++) { 637 if (Rect.intersects(mRects.get(i), mRects.get(j))) { 638 Log.d(TAG, "rect " + i + " and rect " + j + "intersects!"); 639 } 640 } 641 } 642 } 643 644 private void dumpRect(int i) { 645 StringBuilder sb = new StringBuilder(); 646 Rect r = mRects.get(i); 647 sb.append("Rect " + i + ":"); 648 sb.append("("); 649 sb.append(r.centerX()); 650 sb.append(","); 651 sb.append(r.centerY()); 652 sb.append(") ["); 653 sb.append(r.width()); 654 sb.append("x"); 655 sb.append(r.height()); 656 sb.append("]"); 657 Log.d(TAG, sb.toString()); 658 } 659 660 private void convertBoxToRect(int i) { 661 Box b = mBoxes.get(i); 662 Rect r = mRects.get(i); 663 int y = b.mCurrentY; 664 int w = widthOf(b); 665 int h = heightOf(b); 666 if (i == 0) { 667 int x = mPlatform.mCurrentX; 668 r.left = x - w / 2; 669 r.right = r.left + w; 670 } else if (i > 0) { 671 Rect a = mRects.get(i - 1); 672 Gap g = mGaps.get(i - 1); 673 r.left = a.right + g.mCurrentGap; 674 r.right = r.left + w; 675 } else { // i < 0 676 Rect a = mRects.get(i + 1); 677 Gap g = mGaps.get(i); 678 r.right = a.left - g.mCurrentGap; 679 r.left = r.right - w; 680 } 681 r.top = y - h / 2; 682 r.bottom = r.top + h; 683 } 684 685 // Returns the position of a box. 686 public Rect getPosition(int index) { 687 return mRects.get(index); 688 } 689 690 //////////////////////////////////////////////////////////////////////////// 691 // Box management 692 //////////////////////////////////////////////////////////////////////////// 693 694 // Initialize the platform to be at the view center. 695 private void initPlatform() { 696 mPlatform.mCurrentX = mViewW / 2; 697 mPlatform.mAnimationStartTime = NO_ANIMATION; 698 } 699 700 // Initialize a box to have the size of the view. 701 private void initBox(int index) { 702 Box b = mBoxes.get(index); 703 b.mImageW = mViewW; 704 b.mImageH = mViewH; 705 b.mUseViewSize = true; 706 b.mScaleMin = getMinimalScale(b.mImageW, b.mImageH); 707 b.mScaleMax = getMaximalScale(b.mImageW, b.mImageH); 708 b.mCurrentY = mViewH / 2; 709 b.mCurrentScale = b.mScaleMin; 710 b.mAnimationStartTime = NO_ANIMATION; 711 } 712 713 // Initialize a gap. This can only be called after the boxes around the gap 714 // has been initialized. 715 private void initGap(int index) { 716 Gap g = mGaps.get(index); 717 g.mDefaultSize = getDefaultGapSize(index); 718 g.mCurrentGap = g.mDefaultSize; 719 g.mAnimationStartTime = NO_ANIMATION; 720 } 721 722 private void initGap(int index, int size) { 723 Gap g = mGaps.get(index); 724 g.mDefaultSize = getDefaultGapSize(index); 725 g.mCurrentGap = size; 726 g.mAnimationStartTime = NO_ANIMATION; 727 } 728 729 private void debugMoveBox(int fromIndex[]) { 730 StringBuilder s = new StringBuilder("moveBox:"); 731 for (int i = 0; i < fromIndex.length; i++) { 732 int j = fromIndex[i]; 733 if (j == Integer.MAX_VALUE) { 734 s.append(" N"); 735 } else { 736 s.append(" "); 737 s.append(fromIndex[i]); 738 } 739 } 740 Log.d(TAG, s.toString()); 741 } 742 743 // Move the boxes: it may indicate focus change, box deleted, box appearing, 744 // box reordered, etc. 745 // 746 // Each element in the fromIndex array indicates where each box was in the 747 // old array. If the value is Integer.MAX_VALUE (pictured as N below), it 748 // means the box is new. 749 // 750 // For example: 751 // N N N N N N N -- all new boxes 752 // -3 -2 -1 0 1 2 3 -- nothing changed 753 // -2 -1 0 1 2 3 N -- focus goes to the next box 754 // N-3 -2 -1 0 1 2 -- focuse goes to the previous box 755 // -3 -2 -1 1 2 3 N -- the focused box was deleted. 756 public void moveBox(int fromIndex[]) { 757 //debugMoveBox(fromIndex); 758 RangeIntArray from = new RangeIntArray(fromIndex, -BOX_MAX, BOX_MAX); 759 760 // 1. Get the absolute X coordiates for the boxes. 761 layoutAndSetPosition(); 762 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 763 Box b = mBoxes.get(i); 764 Rect r = mRects.get(i); 765 b.mAbsoluteX = r.centerX(); 766 } 767 768 // 2. copy boxes and gaps to temporary storage. 769 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 770 mTempBoxes.put(i, mBoxes.get(i)); 771 mBoxes.put(i, null); 772 } 773 for (int i = -BOX_MAX; i < BOX_MAX; i++) { 774 mTempGaps.put(i, mGaps.get(i)); 775 mGaps.put(i, null); 776 } 777 778 // 3. move back boxes that are used in the new array. 779 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 780 int j = from.get(i); 781 if (j == Integer.MAX_VALUE) continue; 782 mBoxes.put(i, mTempBoxes.get(j)); 783 mTempBoxes.put(j, null); 784 } 785 786 // 4. move back gaps if both boxes around it are kept together. 787 for (int i = -BOX_MAX; i < BOX_MAX; i++) { 788 int j = from.get(i); 789 if (j == Integer.MAX_VALUE) continue; 790 int k = from.get(i + 1); 791 if (k == Integer.MAX_VALUE) continue; 792 if (j + 1 == k) { 793 mGaps.put(i, mTempGaps.get(j)); 794 mTempGaps.put(j, null); 795 } 796 } 797 798 // 5. recycle the boxes that are not used in the new array. 799 int k = -BOX_MAX; 800 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 801 if (mBoxes.get(i) != null) continue; 802 while (mTempBoxes.get(k) == null) { 803 k++; 804 } 805 mBoxes.put(i, mTempBoxes.get(k++)); 806 initBox(i); 807 } 808 809 // 6. Now give the recycled box a reasonable absolute X position. 810 // 811 // First try to find the first and the last box which the absolute X 812 // position is known. 813 int first, last; 814 for (first = -BOX_MAX; first <= BOX_MAX; first++) { 815 if (from.get(first) != Integer.MAX_VALUE) break; 816 } 817 for (last = BOX_MAX; last >= -BOX_MAX; last--) { 818 if (from.get(last) != Integer.MAX_VALUE) break; 819 } 820 // If there is no box has known X position at all, make the focused one 821 // as known. 822 if (first > BOX_MAX) { 823 mBoxes.get(0).mAbsoluteX = mPlatform.mCurrentX; 824 first = last = 0; 825 } 826 // Now for those boxes between first and last, just assign the same 827 // position as the previous box. (We can do better, but this should be 828 // rare). For the boxes before first or after last, we will use a new 829 // default gap size below. 830 for (int i = first + 1; i < last; i++) { 831 if (from.get(i) != Integer.MAX_VALUE) continue; 832 mBoxes.get(i).mAbsoluteX = mBoxes.get(i - 1).mAbsoluteX; 833 } 834 835 // 7. recycle the gaps that are not used in the new array. 836 k = -BOX_MAX; 837 for (int i = -BOX_MAX; i < BOX_MAX; i++) { 838 if (mGaps.get(i) != null) continue; 839 while (mTempGaps.get(k) == null) { 840 k++; 841 } 842 mGaps.put(i, mTempGaps.get(k++)); 843 Box a = mBoxes.get(i); 844 Box b = mBoxes.get(i + 1); 845 int wa = widthOf(a); 846 int wb = widthOf(b); 847 if (i >= first && i < last) { 848 int g = b.mAbsoluteX - a.mAbsoluteX - wb / 2 - (wa - wa / 2); 849 initGap(i, g); 850 } else { 851 initGap(i); 852 } 853 } 854 855 // 8. offset the Platform position 856 int dx = mBoxes.get(0).mAbsoluteX - mPlatform.mCurrentX; 857 mPlatform.mCurrentX += dx; 858 mPlatform.mFromX += dx; 859 mPlatform.mToX += dx; 860 mPlatform.mFlingOffset += dx; 861 862 snapAndRedraw(); 863 } 864 865 //////////////////////////////////////////////////////////////////////////// 866 // Public utilities 867 //////////////////////////////////////////////////////////////////////////// 868 869 public float getMinimalScale(int imageW, int imageH) { 870 float wFactor = 1.0f; 871 float hFactor = 1.0f; 872 873 if (mFilmMode) { 874 if (mViewH > mViewW) { // portrait 875 wFactor = FILM_MODE_PORTRAIT_WIDTH; 876 hFactor = FILM_MODE_PORTRAIT_HEIGHT; 877 } else { // landscape 878 wFactor = FILM_MODE_LANDSCAPE_WIDTH; 879 hFactor = FILM_MODE_LANDSCAPE_HEIGHT; 880 } 881 } 882 883 float s = Math.min(wFactor * mViewW / imageW, 884 hFactor * mViewH / imageH); 885 return Math.min(SCALE_LIMIT, s); 886 } 887 888 public float getMaximalScale(int imageW, int imageH) { 889 return mFilmMode ? getMinimalScale(imageW, imageH) : SCALE_LIMIT; 890 } 891 892 public boolean isAtMinimalScale() { 893 Box b = mBoxes.get(0); 894 return isAlmostEqual(b.mCurrentScale, b.mScaleMin); 895 } 896 897 public int getImageWidth() { 898 Box b = mBoxes.get(0); 899 return b.mImageW; 900 } 901 902 public int getImageHeight() { 903 Box b = mBoxes.get(0); 904 return b.mImageH; 905 } 906 907 public float getImageScale() { 908 Box b = mBoxes.get(0); 909 return b.mCurrentScale; 910 } 911 912 public int getImageAtEdges() { 913 Box b = mBoxes.get(0); 914 Platform p = mPlatform; 915 calculateStableBound(b.mCurrentScale); 916 int edges = 0; 917 if (p.mCurrentX <= mBoundLeft) { 918 edges |= IMAGE_AT_RIGHT_EDGE; 919 } 920 if (p.mCurrentX >= mBoundRight) { 921 edges |= IMAGE_AT_LEFT_EDGE; 922 } 923 if (b.mCurrentY <= mBoundTop) { 924 edges |= IMAGE_AT_BOTTOM_EDGE; 925 } 926 if (b.mCurrentY >= mBoundBottom) { 927 edges |= IMAGE_AT_TOP_EDGE; 928 } 929 return edges; 930 } 931 932 //////////////////////////////////////////////////////////////////////////// 933 // Private utilities 934 //////////////////////////////////////////////////////////////////////////// 935 936 private float getMinimalScale(Box b) { 937 return getMinimalScale(b.mImageW, b.mImageH); 938 } 939 940 private float getMaxmimalScale(Box b) { 941 return getMaximalScale(b.mImageW, b.mImageH); 942 } 943 944 private static boolean isAlmostEqual(float a, float b) { 945 float diff = a - b; 946 return (diff < 0 ? -diff : diff) < 0.02f; 947 } 948 949 // Calculates the stable region of mCurrent{X/Y}, where "stable" means 950 // 951 // (1) If the dimension of scaled image >= view dimension, we will not 952 // see black region outside the image (at that dimension). 953 // (2) If the dimension of scaled image < view dimension, we will center 954 // the scaled image. 955 // 956 // We might temporarily go out of this stable during user interaction, 957 // but will "snap back" after user stops interaction. 958 // 959 // The results are stored in mBound{Left/Right/Top/Bottom}. 960 // 961 // An extra parameter "horizontalSlack" (which has the value of 0 usually) 962 // is used to extend the stable region by some pixels on each side 963 // horizontally. 964 private void calculateStableBound(float scale, int horizontalSlack) { 965 Box b = mBoxes.get(0); 966 967 // The width and height of the box in number of view pixels 968 int w = widthOf(b, scale); 969 int h = heightOf(b, scale); 970 971 // When the edge of the view is aligned with the edge of the box 972 mBoundLeft = (mViewW - horizontalSlack) - w / 2; 973 mBoundRight = mViewW - mBoundLeft; 974 mBoundTop = mViewH - h / 2; 975 mBoundBottom = mViewH - mBoundTop; 976 977 // If the scaled height is smaller than the view height, 978 // force it to be in the center. 979 if (viewTallerThanScaledImage(scale)) { 980 mBoundTop = mBoundBottom = mViewH / 2; 981 } 982 983 // Same for width 984 if (viewWiderThanScaledImage(scale)) { 985 mBoundLeft = mBoundRight = mViewW / 2; 986 } 987 } 988 989 private void calculateStableBound(float scale) { 990 calculateStableBound(scale, 0); 991 } 992 993 private boolean hasNextImages() { 994 for (int i = 1; i <= BOX_MAX; i++) { 995 if (!mBoxes.get(i).mUseViewSize) return true; 996 } 997 return false; 998 } 999 1000 private boolean hasPrevImages() { 1001 for (int i = -1; i >= -BOX_MAX; i--) { 1002 if (!mBoxes.get(i).mUseViewSize) return true; 1003 } 1004 return false; 1005 } 1006 1007 private boolean viewTallerThanScaledImage(float scale) { 1008 return mViewH >= heightOf(mBoxes.get(0), scale); 1009 } 1010 1011 private boolean viewWiderThanScaledImage(float scale) { 1012 return mViewW >= widthOf(mBoxes.get(0), scale); 1013 } 1014 1015 private float getTargetScale(Box b) { 1016 return useCurrentValueAsTarget(b) ? b.mCurrentScale : b.mToScale; 1017 } 1018 1019 private int getTargetX(Platform p) { 1020 return useCurrentValueAsTarget(p) ? p.mCurrentX : p.mToX; 1021 } 1022 1023 private int getTargetY(Box b) { 1024 return useCurrentValueAsTarget(b) ? b.mCurrentY : b.mToY; 1025 } 1026 1027 private boolean useCurrentValueAsTarget(Animatable a) { 1028 return a.mAnimationStartTime == NO_ANIMATION || 1029 a.mAnimationKind == ANIM_KIND_SNAPBACK || 1030 a.mAnimationKind == ANIM_KIND_FLING; 1031 } 1032 1033 // Returns the index of the anchor box. 1034 private int anchorIndex(int i) { 1035 if (i > 0) return i - 1; 1036 if (i < 0) return i + 1; 1037 throw new IllegalArgumentException(); 1038 } 1039 1040 //////////////////////////////////////////////////////////////////////////// 1041 // Animatable: an thing which can do animation. 1042 //////////////////////////////////////////////////////////////////////////// 1043 private abstract static class Animatable { 1044 public long mAnimationStartTime; 1045 public int mAnimationKind; 1046 public int mAnimationDuration; 1047 1048 // This should be overidden in subclass to change the animation values 1049 // give the progress value in [0, 1]. 1050 protected abstract boolean interpolate(float progress); 1051 public abstract boolean startSnapback(); 1052 1053 // Returns true if the animation values changes, so things need to be 1054 // redrawn. 1055 public boolean advanceAnimation() { 1056 if (mAnimationStartTime == NO_ANIMATION) { 1057 return false; 1058 } 1059 if (mAnimationStartTime == LAST_ANIMATION) { 1060 mAnimationStartTime = NO_ANIMATION; 1061 return startSnapback(); 1062 } 1063 1064 float progress; 1065 if (mAnimationDuration == 0) { 1066 progress = 1; 1067 } else { 1068 long now = AnimationTime.get(); 1069 progress = 1070 (float) (now - mAnimationStartTime) / mAnimationDuration; 1071 } 1072 1073 if (progress >= 1) { 1074 progress = 1; 1075 } else { 1076 progress = applyInterpolationCurve(mAnimationKind, progress); 1077 } 1078 1079 boolean done = interpolate(progress); 1080 1081 if (done) { 1082 mAnimationStartTime = LAST_ANIMATION; 1083 } 1084 1085 return true; 1086 } 1087 1088 private static float applyInterpolationCurve(int kind, float progress) { 1089 float f = 1 - progress; 1090 switch (kind) { 1091 case ANIM_KIND_SCROLL: 1092 case ANIM_KIND_FLING: 1093 progress = 1 - f; // linear 1094 break; 1095 case ANIM_KIND_SCALE: 1096 progress = 1 - f * f; // quadratic 1097 break; 1098 case ANIM_KIND_SNAPBACK: 1099 case ANIM_KIND_ZOOM: 1100 case ANIM_KIND_SLIDE: 1101 case ANIM_KIND_OPENING: 1102 progress = 1 - f * f * f * f * f; // x^5 1103 break; 1104 } 1105 return progress; 1106 } 1107 } 1108 1109 //////////////////////////////////////////////////////////////////////////// 1110 // Platform: captures the global X movement. 1111 //////////////////////////////////////////////////////////////////////////// 1112 private class Platform extends Animatable { 1113 public int mCurrentX, mFromX, mToX; 1114 public int mFlingOffset; 1115 1116 @Override 1117 public boolean startSnapback() { 1118 if (mAnimationStartTime != NO_ANIMATION) return false; 1119 if (mAnimationKind == ANIM_KIND_SCROLL 1120 && mListener.isDown()) return false; 1121 1122 Box b = mBoxes.get(0); 1123 float scaleMin = mExtraScalingRange ? 1124 b.mScaleMin * SCALE_MIN_EXTRA : b.mScaleMin; 1125 float scaleMax = mExtraScalingRange ? 1126 b.mScaleMax * SCALE_MAX_EXTRA : b.mScaleMax; 1127 float scale = Utils.clamp(b.mCurrentScale, scaleMin, scaleMax); 1128 int x = mCurrentX; 1129 if (mFilmMode) { 1130 if (!hasNextImages()) x = Math.max(x, mViewW / 2); 1131 if (!hasPrevImages()) x = Math.min(x, mViewW / 2); 1132 } else { 1133 calculateStableBound(scale, HORIZONTAL_SLACK); 1134 x = Utils.clamp(x, mBoundLeft, mBoundRight); 1135 } 1136 if (mCurrentX != x) { 1137 return doAnimation(x, ANIM_KIND_SNAPBACK); 1138 } 1139 return false; 1140 } 1141 1142 // Starts an animation for the platform. 1143 public boolean doAnimation(int targetX, int kind) { 1144 if (mCurrentX == targetX) return false; 1145 mAnimationKind = kind; 1146 mFromX = mCurrentX; 1147 mToX = targetX; 1148 mAnimationStartTime = AnimationTime.startTime(); 1149 mAnimationDuration = ANIM_TIME[kind]; 1150 mFlingOffset = 0; 1151 advanceAnimation(); 1152 return true; 1153 } 1154 1155 @Override 1156 protected boolean interpolate(float progress) { 1157 if (mAnimationKind == ANIM_KIND_FLING) { 1158 return mFilmMode 1159 ? interpolateFlingFilm(progress) 1160 : interpolateFlingPage(progress); 1161 } else { 1162 return interpolateLinear(progress); 1163 } 1164 } 1165 1166 private boolean interpolateFlingFilm(float progress) { 1167 mFilmScroller.computeScrollOffset(); 1168 mCurrentX = mFilmScroller.getCurrX() + mFlingOffset; 1169 1170 int dir = EdgeView.INVALID_DIRECTION; 1171 if (mCurrentX < mViewW / 2) { 1172 if (!hasNextImages()) { 1173 dir = EdgeView.RIGHT; 1174 } 1175 } else if (mCurrentX > mViewW / 2) { 1176 if (!hasPrevImages()) { 1177 dir = EdgeView.LEFT; 1178 } 1179 } 1180 if (dir != EdgeView.INVALID_DIRECTION) { 1181 int v = (int) (mFilmScroller.getCurrVelocity() + 0.5f); 1182 mListener.onAbsorb(v, dir); 1183 mFilmScroller.forceFinished(true); 1184 mCurrentX = mViewW / 2; 1185 } 1186 return mFilmScroller.isFinished(); 1187 } 1188 1189 private boolean interpolateFlingPage(float progress) { 1190 mPageScroller.computeScrollOffset(progress); 1191 Box b = mBoxes.get(0); 1192 calculateStableBound(b.mCurrentScale); 1193 1194 int oldX = mCurrentX; 1195 mCurrentX = mPageScroller.getCurrX(); 1196 1197 // Check if we hit the edges; show edge effects if we do. 1198 if (oldX > mBoundLeft && mCurrentX == mBoundLeft) { 1199 int v = (int) (-mPageScroller.getCurrVelocityX() + 0.5f); 1200 mListener.onAbsorb(v, EdgeView.RIGHT); 1201 } else if (oldX < mBoundRight && mCurrentX == mBoundRight) { 1202 int v = (int) (mPageScroller.getCurrVelocityX() + 0.5f); 1203 mListener.onAbsorb(v, EdgeView.LEFT); 1204 } 1205 1206 return progress >= 1; 1207 } 1208 1209 private boolean interpolateLinear(float progress) { 1210 // Other animations 1211 if (progress >= 1) { 1212 mCurrentX = mToX; 1213 return true; 1214 } else { 1215 mCurrentX = (int) (mFromX + progress * (mToX - mFromX)); 1216 return (mCurrentX == mToX); 1217 } 1218 } 1219 } 1220 1221 //////////////////////////////////////////////////////////////////////////// 1222 // Box: represents a rectangular area which shows a picture. 1223 //////////////////////////////////////////////////////////////////////////// 1224 private class Box extends Animatable { 1225 // Size of the bitmap 1226 public int mImageW, mImageH; 1227 1228 // This is true if we assume the image size is the same as view size 1229 // until we know the actual size of image. This is also used to 1230 // determine if there is an image ready to show. 1231 public boolean mUseViewSize; 1232 1233 // The minimum and maximum scale we allow for this box. 1234 public float mScaleMin, mScaleMax; 1235 1236 // The X/Y value indicates where the center of the box is on the view 1237 // coordinate. We always keep the mCurrent{X,Y,Scale} sync with the 1238 // actual values used currently. Note that the X values are implicitly 1239 // defined by Platform and Gaps. 1240 public int mCurrentY, mFromY, mToY; 1241 public float mCurrentScale, mFromScale, mToScale; 1242 1243 // The absolute X coordinate of the center of the box. This is only used 1244 // during moveBox(). 1245 public int mAbsoluteX; 1246 1247 @Override 1248 public boolean startSnapback() { 1249 if (mAnimationStartTime != NO_ANIMATION) return false; 1250 if (mAnimationKind == ANIM_KIND_SCROLL 1251 && mListener.isDown()) return false; 1252 if (mInScale && this == mBoxes.get(0)) return false; 1253 1254 int y; 1255 float scale; 1256 1257 if (this == mBoxes.get(0)) { 1258 float scaleMin = mExtraScalingRange ? 1259 mScaleMin * SCALE_MIN_EXTRA : mScaleMin; 1260 float scaleMax = mExtraScalingRange ? 1261 mScaleMax * SCALE_MAX_EXTRA : mScaleMax; 1262 scale = Utils.clamp(mCurrentScale, scaleMin, scaleMax); 1263 if (mFilmMode) { 1264 y = mViewH / 2; 1265 } else { 1266 calculateStableBound(scale, HORIZONTAL_SLACK); 1267 y = Utils.clamp(mCurrentY, mBoundTop, mBoundBottom); 1268 } 1269 } else { 1270 y = mViewH / 2; 1271 scale = mScaleMin; 1272 } 1273 1274 if (mCurrentY != y || mCurrentScale != scale) { 1275 return doAnimation(y, scale, ANIM_KIND_SNAPBACK); 1276 } 1277 return false; 1278 } 1279 1280 private boolean doAnimation(int targetY, float targetScale, int kind) { 1281 targetScale = Utils.clamp(targetScale, 1282 SCALE_MIN_EXTRA * mScaleMin, 1283 SCALE_MAX_EXTRA * mScaleMax); 1284 1285 // If the scaled height is smaller than the view height, force it to be 1286 // in the center. (We do this for height only, not width, because the 1287 // user may want to scroll to the previous/next image.) 1288 if (!mInScale && viewTallerThanScaledImage(targetScale)) { 1289 targetY = mViewH / 2; 1290 } 1291 1292 if (mCurrentY == targetY && mCurrentScale == targetScale) { 1293 return false; 1294 } 1295 1296 // Now starts an animation for the box. 1297 mAnimationKind = kind; 1298 mFromY = mCurrentY; 1299 mFromScale = mCurrentScale; 1300 mToY = targetY; 1301 mToScale = targetScale; 1302 mAnimationStartTime = AnimationTime.startTime(); 1303 mAnimationDuration = ANIM_TIME[kind]; 1304 advanceAnimation(); 1305 return true; 1306 } 1307 1308 @Override 1309 protected boolean interpolate(float progress) { 1310 if (mAnimationKind == ANIM_KIND_FLING) { 1311 // Currently a Box can only be flung in page mode. 1312 return interpolateFlingPage(progress); 1313 } else { 1314 return interpolateLinear(progress); 1315 } 1316 } 1317 1318 private boolean interpolateFlingPage(float progress) { 1319 mPageScroller.computeScrollOffset(progress); 1320 calculateStableBound(mCurrentScale); 1321 1322 int oldY = mCurrentY; 1323 mCurrentY = mPageScroller.getCurrY(); 1324 1325 // Check if we hit the edges; show edge effects if we do. 1326 if (oldY > mBoundTop && mCurrentY == mBoundTop) { 1327 int v = (int) (-mPageScroller.getCurrVelocityY() + 0.5f); 1328 mListener.onAbsorb(v, EdgeView.BOTTOM); 1329 } else if (oldY < mBoundBottom && mCurrentY == mBoundBottom) { 1330 int v = (int) (mPageScroller.getCurrVelocityY() + 0.5f); 1331 mListener.onAbsorb(v, EdgeView.TOP); 1332 } 1333 1334 return progress >= 1; 1335 } 1336 1337 private boolean interpolateLinear(float progress) { 1338 if (progress >= 1) { 1339 mCurrentY = mToY; 1340 mCurrentScale = mToScale; 1341 return true; 1342 } else { 1343 mCurrentY = (int) (mFromY + progress * (mToY - mFromY)); 1344 mCurrentScale = mFromScale + progress * (mToScale - mFromScale); 1345 return (mCurrentY == mToY && mCurrentScale == mToScale); 1346 } 1347 } 1348 } 1349 1350 //////////////////////////////////////////////////////////////////////////// 1351 // Gap: represents a rectangular area which is between two boxes. 1352 //////////////////////////////////////////////////////////////////////////// 1353 private class Gap extends Animatable { 1354 // The default gap size between two boxes. The value may vary for 1355 // different image size of the boxes and for different modes (page or 1356 // film). 1357 public int mDefaultSize; 1358 1359 // The gap size between the two boxes. 1360 public int mCurrentGap, mFromGap, mToGap; 1361 1362 @Override 1363 public boolean startSnapback() { 1364 if (mAnimationStartTime != NO_ANIMATION) return false; 1365 return doAnimation(mDefaultSize); 1366 } 1367 1368 // Starts an animation for a gap. 1369 public boolean doAnimation(int targetSize) { 1370 if (mCurrentGap == targetSize) return false; 1371 mAnimationKind = ANIM_KIND_SNAPBACK; 1372 mFromGap = mCurrentGap; 1373 mToGap = targetSize; 1374 mAnimationStartTime = AnimationTime.startTime(); 1375 mAnimationDuration = ANIM_TIME[mAnimationKind]; 1376 advanceAnimation(); 1377 return true; 1378 } 1379 1380 @Override 1381 protected boolean interpolate(float progress) { 1382 if (progress >= 1) { 1383 mCurrentGap = mToGap; 1384 return true; 1385 } else { 1386 mCurrentGap = (int) (mFromGap + progress * (mToGap - mFromGap)); 1387 return (mCurrentGap == mToGap); 1388 } 1389 } 1390 } 1391} 1392