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