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