PositionController.java revision d848862c1dd0689df8df5d684476cfd3897557d6
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 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 OverScroller 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 OverScroller(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 boolean wasViewSize = b.mUseViewSize; 198 199 // If we already have an image size, we don't want to use the view size. 200 if (!wasViewSize && isViewSize) return; 201 202 b.mUseViewSize = isViewSize; 203 204 if (width == b.mImageW && height == b.mImageH) { 205 return; 206 } 207 208 // The ratio of the old size and the new size. 209 float ratio = Math.min( 210 (float) b.mImageW / width, (float) b.mImageH / height); 211 212 // If this is the first time we receive an image size, we change the 213 // scale directly. Otherwise adjust the scales by a ratio, and snapback 214 // will animate the scale into the min/max bounds if necessary. 215 if (wasViewSize && !isViewSize) { 216 b.mCurrentScale = getMinimalScale(width, height); 217 b.mAnimationStartTime = NO_ANIMATION; 218 } else { 219 b.mCurrentScale *= ratio; 220 b.mFromScale *= ratio; 221 b.mToScale *= ratio; 222 } 223 224 b.mImageW = width; 225 b.mImageH = height; 226 227 if (i == 0) { 228 mFocusX /= ratio; 229 mFocusY /= ratio; 230 } 231 } 232 233 private void startOpeningAnimationIfNeeded() { 234 if (mOpenAnimationRect == null) return; 235 Box b = mBoxes.get(0); 236 if (b.mUseViewSize) return; 237 238 // Start animation from the saved rectangle if we have one. 239 Rect r = mOpenAnimationRect; 240 mOpenAnimationRect = null; 241 mPlatform.mCurrentX = r.centerX(); 242 b.mCurrentY = r.centerY(); 243 b.mCurrentScale = Math.max(r.width() / (float) b.mImageW, 244 r.height() / (float) b.mImageH); 245 startAnimation(mViewW / 2, mViewH / 2, b.mScaleMin, ANIM_KIND_OPENING); 246 } 247 248 public void setFilmMode(boolean enabled) { 249 if (enabled == mFilmMode) return; 250 mFilmMode = enabled; 251 252 updateScaleAndGapLimit(); 253 stopAnimation(); 254 snapAndRedraw(); 255 } 256 257 public void setExtraScalingRange(boolean enabled) { 258 if (mExtraScalingRange == enabled) return; 259 mExtraScalingRange = enabled; 260 if (!enabled) { 261 snapAndRedraw(); 262 } 263 } 264 265 // This should be called whenever the scale range of boxes or the default 266 // gap size may change. Currently this can happen due to change of view 267 // size, image size, and mode. 268 private void updateScaleAndGapLimit() { 269 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 270 Box b = mBoxes.get(i); 271 b.mScaleMin = getMinimalScale(b.mImageW, b.mImageH); 272 b.mScaleMax = getMaximalScale(b.mImageW, b.mImageH); 273 } 274 275 for (int i = -BOX_MAX; i < BOX_MAX; i++) { 276 Gap g = mGaps.get(i); 277 g.mDefaultSize = getDefaultGapSize(i); 278 } 279 } 280 281 // Returns the default gap size according the the size of the boxes around 282 // the gap and the current mode. 283 private int getDefaultGapSize(int i) { 284 if (mFilmMode) return IMAGE_GAP; 285 Box a = mBoxes.get(i); 286 Box b = mBoxes.get(i + 1); 287 return IMAGE_GAP + Math.max(gapToSide(a), gapToSide(b)); 288 } 289 290 // Here is how we layout the boxes in the page mode. 291 // 292 // previous current next 293 // ___________ ________________ __________ 294 // | _______ | | __________ | | ______ | 295 // | | | | | | right->| | | | | | 296 // | | |<-------->|<--left | | | | | | 297 // | |_______| | | | |__________| | | |______| | 298 // |___________| | |________________| |__________| 299 // | <--> gapToSide() 300 // | 301 // IMAGE_GAP + MAX(gapToSide(previous), gapToSide(current)) 302 private int gapToSide(Box b) { 303 return (int) ((mViewW - getMinimalScale(b) * b.mImageW) / 2 + 0.5f); 304 } 305 306 // Stop all animations at where they are now. 307 public void stopAnimation() { 308 mPlatform.mAnimationStartTime = NO_ANIMATION; 309 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 310 mBoxes.get(i).mAnimationStartTime = NO_ANIMATION; 311 } 312 for (int i = -BOX_MAX; i < BOX_MAX; i++) { 313 mGaps.get(i).mAnimationStartTime = NO_ANIMATION; 314 } 315 } 316 317 public void skipAnimation() { 318 if (mPlatform.mAnimationStartTime != NO_ANIMATION) { 319 mPlatform.mCurrentX = mPlatform.mToX; 320 mPlatform.mAnimationStartTime = NO_ANIMATION; 321 } 322 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 323 Box b = mBoxes.get(i); 324 if (b.mAnimationStartTime == NO_ANIMATION) continue; 325 b.mCurrentY = b.mToY; 326 b.mCurrentScale = b.mToScale; 327 b.mAnimationStartTime = NO_ANIMATION; 328 } 329 for (int i = -BOX_MAX; i < BOX_MAX; i++) { 330 Gap g = mGaps.get(i); 331 if (g.mAnimationStartTime == NO_ANIMATION) continue; 332 g.mCurrentGap = g.mToGap; 333 g.mAnimationStartTime = NO_ANIMATION; 334 } 335 redraw(); 336 } 337 338 public void up() { 339 snapAndRedraw(); 340 } 341 342 //////////////////////////////////////////////////////////////////////////// 343 // Start an animations for the focused box 344 //////////////////////////////////////////////////////////////////////////// 345 346 public void zoomIn(float tapX, float tapY, float targetScale) { 347 Box b = mBoxes.get(0); 348 349 // Convert the tap position to distance to center in bitmap coordinates 350 float tempX = (tapX - mPlatform.mCurrentX) / b.mCurrentScale; 351 float tempY = (tapY - b.mCurrentY) / b.mCurrentScale; 352 353 int x = (int) (mViewW / 2 - tempX * targetScale + 0.5f); 354 int y = (int) (mViewH / 2 - tempY * targetScale + 0.5f); 355 356 calculateStableBound(targetScale); 357 int targetX = Utils.clamp(x, mBoundLeft, mBoundRight); 358 int targetY = Utils.clamp(y, mBoundTop, mBoundBottom); 359 targetScale = Utils.clamp(targetScale, b.mScaleMin, b.mScaleMax); 360 361 startAnimation(targetX, targetY, targetScale, ANIM_KIND_ZOOM); 362 } 363 364 public void resetToFullView() { 365 Box b = mBoxes.get(0); 366 startAnimation(mViewW / 2, mViewH / 2, b.mScaleMin, ANIM_KIND_ZOOM); 367 } 368 369 public void beginScale(float focusX, float focusY) { 370 Box b = mBoxes.get(0); 371 Platform p = mPlatform; 372 mInScale = true; 373 mFocusX = (int) ((focusX - p.mCurrentX) / b.mCurrentScale + 0.5f); 374 mFocusY = (int) ((focusY - b.mCurrentY) / b.mCurrentScale + 0.5f); 375 } 376 377 // Scales the image by the given factor. 378 // Returns an out-of-range indicator: 379 // 1 if the intended scale is too large for the stable range. 380 // 0 if the intended scale is in the stable range. 381 // -1 if the intended scale is too small for the stable range. 382 public int scaleBy(float s, float focusX, float focusY) { 383 Box b = mBoxes.get(0); 384 Platform p = mPlatform; 385 386 // We want to keep the focus point (on the bitmap) the same as when we 387 // begin the scale guesture, that is, 388 // 389 // (focusX' - currentX') / scale' = (focusX - currentX) / scale 390 // 391 s *= getTargetScale(b); 392 int x = mFilmMode ? p.mCurrentX : (int) (focusX - s * mFocusX + 0.5f); 393 int y = mFilmMode ? b.mCurrentY : (int) (focusY - s * mFocusY + 0.5f); 394 startAnimation(x, y, s, ANIM_KIND_SCALE); 395 if (s < b.mScaleMin) return -1; 396 if (s > b.mScaleMax) return 1; 397 return 0; 398 } 399 400 public void endScale() { 401 mInScale = false; 402 snapAndRedraw(); 403 } 404 405 public void startHorizontalSlide(int distance) { 406 Box b = mBoxes.get(0); 407 Platform p = mPlatform; 408 startAnimation(getTargetX(p) + distance, getTargetY(b), 409 b.mCurrentScale, ANIM_KIND_SLIDE); 410 } 411 412 public void startScroll(float dx, float dy) { 413 boolean hasPrev = hasPrevImages(); 414 boolean hasNext = hasNextImages(); 415 416 Box b = mBoxes.get(0); 417 Platform p = mPlatform; 418 419 int x = getTargetX(p) + (int) (dx + 0.5f); 420 int y = getTargetY(b) + (int) (dy + 0.5f); 421 422 if (mFilmMode) { 423 scrollToFilm(x, y, hasPrev, hasNext); 424 } else { 425 scrollToPage(x, y, hasPrev, hasNext); 426 } 427 } 428 429 private void scrollToPage(int x, int y, boolean hasPrev, boolean hasNext) { 430 Box b = mBoxes.get(0); 431 432 calculateStableBound(b.mCurrentScale); 433 434 // Vertical direction: If we have space to move in the vertical 435 // direction, we show the edge effect when scrolling reaches the edge. 436 if (mBoundTop != mBoundBottom) { 437 if (y < mBoundTop) { 438 mListener.onPull(mBoundTop - y, EdgeView.BOTTOM); 439 } else if (y > mBoundBottom) { 440 mListener.onPull(y - mBoundBottom, EdgeView.TOP); 441 } 442 } 443 444 y = Utils.clamp(y, mBoundTop, mBoundBottom); 445 446 // Horizontal direction: we show the edge effect when the scrolling 447 // tries to go left of the first image or go right of the last image. 448 if (!hasPrev && x > mBoundRight) { 449 int pixels = x - mBoundRight; 450 mListener.onPull(pixels, EdgeView.LEFT); 451 x = mBoundRight; 452 } else if (!hasNext && x < mBoundLeft) { 453 int pixels = mBoundLeft - x; 454 mListener.onPull(pixels, EdgeView.RIGHT); 455 x = mBoundLeft; 456 } 457 458 startAnimation(x, y, b.mCurrentScale, ANIM_KIND_SCROLL); 459 } 460 461 private void scrollToFilm(int x, int y, boolean hasPrev, boolean hasNext) { 462 Box b = mBoxes.get(0); 463 464 // Horizontal direction: we show the edge effect when the scrolling 465 // tries to go left of the first image or go right of the last image. 466 int cx = mViewW / 2; 467 if (!hasPrev && x > cx) { 468 int pixels = x - cx; 469 mListener.onPull(pixels, EdgeView.LEFT); 470 x = cx; 471 } else if (!hasNext && x < cx) { 472 int pixels = cx - x; 473 mListener.onPull(pixels, EdgeView.RIGHT); 474 x = cx; 475 } 476 477 startAnimation(x, y, b.mCurrentScale, ANIM_KIND_SCROLL); 478 } 479 480 public boolean fling(float velocityX, float velocityY) { 481 int vx = (int) (velocityX + 0.5f); 482 int vy = (int) (velocityY + 0.5f); 483 return mFilmMode ? flingFilm(vx, vy) : flingPage(vx, vy); 484 } 485 486 private boolean flingPage(int velocityX, int velocityY) { 487 Box b = mBoxes.get(0); 488 Platform p = mPlatform; 489 490 // We only want to do fling when the picture is zoomed-in. 491 if (viewWiderThanScaledImage(b.mCurrentScale) && 492 viewTallerThanScaledImage(b.mCurrentScale)) { 493 return false; 494 } 495 496 // We only allow flinging in the directions where it won't go over the 497 // picture. 498 int edges = getImageAtEdges(); 499 if ((velocityX > 0 && (edges & IMAGE_AT_LEFT_EDGE) != 0) || 500 (velocityX < 0 && (edges & IMAGE_AT_RIGHT_EDGE) != 0)) { 501 velocityX = 0; 502 } 503 if ((velocityY > 0 && (edges & IMAGE_AT_TOP_EDGE) != 0) || 504 (velocityY < 0 && (edges & IMAGE_AT_BOTTOM_EDGE) != 0)) { 505 velocityY = 0; 506 } 507 508 if (velocityX == 0 && velocityY == 0) return false; 509 510 mPageScroller.fling(p.mCurrentX, b.mCurrentY, velocityX, velocityY, 511 mBoundLeft, mBoundRight, mBoundTop, mBoundBottom); 512 int targetX = mPageScroller.getFinalX(); 513 int targetY = mPageScroller.getFinalY(); 514 ANIM_TIME[ANIM_KIND_FLING] = mPageScroller.getDuration(); 515 startAnimation(targetX, targetY, b.mCurrentScale, ANIM_KIND_FLING); 516 return true; 517 } 518 519 private boolean flingFilm(int velocityX, int velocityY) { 520 boolean hasPrev = hasPrevImages(); 521 boolean hasNext = hasNextImages(); 522 523 Box b = mBoxes.get(0); 524 Platform p = mPlatform; 525 526 // If we are already at the edge, don't start the fling. 527 int cx = mViewW / 2; 528 if ((!hasPrev && p.mCurrentX >= cx) || (!hasNext && p.mCurrentX <= cx)) { 529 return false; 530 } 531 532 if (velocityX == 0) return false; 533 534 mFilmScroller.fling(p.mCurrentX, 0, velocityX, 0, 535 Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0); 536 int targetX = mFilmScroller.getFinalX(); 537 ANIM_TIME[ANIM_KIND_FLING] = mFilmScroller.getDuration(); 538 startAnimation(targetX, b.mCurrentY, b.mCurrentScale, ANIM_KIND_FLING); 539 return true; 540 } 541 542 //////////////////////////////////////////////////////////////////////////// 543 // Redraw 544 // 545 // If a method changes box positions directly, redraw() 546 // should be called. 547 // 548 // If a method may also cause a snapback to happen, snapAndRedraw() should 549 // be called. 550 // 551 // If a method starts an animation to change the position of focused box, 552 // startAnimation() should be called. 553 // 554 // If time advances to change the box position, advanceAnimation() should 555 // be called. 556 //////////////////////////////////////////////////////////////////////////// 557 private void redraw() { 558 layoutAndSetPosition(); 559 mListener.invalidate(); 560 } 561 562 private void snapAndRedraw() { 563 mPlatform.startSnapback(); 564 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 565 mBoxes.get(i).startSnapback(); 566 } 567 for (int i = -BOX_MAX; i < BOX_MAX; i++) { 568 mGaps.get(i).startSnapback(); 569 } 570 redraw(); 571 } 572 573 private void startAnimation(int targetX, int targetY, float targetScale, 574 int kind) { 575 boolean changed = false; 576 changed |= mPlatform.doAnimation(targetX, kind); 577 changed |= mBoxes.get(0).doAnimation(targetY, targetScale, kind); 578 if (changed) redraw(); 579 } 580 581 public boolean advanceAnimation() { 582 boolean changed = false; 583 changed |= mPlatform.advanceAnimation(); 584 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 585 changed |= mBoxes.get(i).advanceAnimation(); 586 } 587 for (int i = -BOX_MAX; i < BOX_MAX; i++) { 588 changed |= mGaps.get(i).advanceAnimation(); 589 } 590 if (changed) redraw(); 591 return changed; 592 } 593 594 //////////////////////////////////////////////////////////////////////////// 595 // Layout 596 //////////////////////////////////////////////////////////////////////////// 597 598 // Returns the display width of this box. 599 private int widthOf(Box b) { 600 return (int) (b.mImageW * b.mCurrentScale + 0.5f); 601 } 602 603 // Returns the display height of this box. 604 private int heightOf(Box b) { 605 return (int) (b.mImageH * b.mCurrentScale + 0.5f); 606 } 607 608 // Returns the display width of this box, using the given scale. 609 private int widthOf(Box b, float scale) { 610 return (int) (b.mImageW * scale + 0.5f); 611 } 612 613 // Returns the display height of this box, using the given scale. 614 private int heightOf(Box b, float scale) { 615 return (int) (b.mImageH * scale + 0.5f); 616 } 617 618 // Convert the information in mPlatform and mBoxes to mRects, so the user 619 // can get the position of each box by getPosition(). 620 // 621 // Note the loop index goes from inside-out because each box's X coordinate 622 // is relative to its anchor box (except the focused box). 623 private void layoutAndSetPosition() { 624 // layout box 0 (focused box) 625 convertBoxToRect(0); 626 for (int i = 1; i <= BOX_MAX; i++) { 627 // layout box i and -i 628 convertBoxToRect(i); 629 convertBoxToRect(-i); 630 } 631 //dumpState(); 632 } 633 634 private void dumpState() { 635 for (int i = -BOX_MAX; i < BOX_MAX; i++) { 636 Log.d(TAG, "Gap " + i + ": " + mGaps.get(i).mCurrentGap); 637 } 638 639 dumpRect(0); 640 for (int i = 1; i <= BOX_MAX; i++) { 641 dumpRect(i); 642 dumpRect(-i); 643 } 644 645 for (int i = -BOX_MAX; i <= BOX_MAX; i++) { 646 for (int j = i + 1; j <= BOX_MAX; j++) { 647 if (Rect.intersects(mRects.get(i), mRects.get(j))) { 648 Log.d(TAG, "rect " + i + " and rect " + j + "intersects!"); 649 } 650 } 651 } 652 } 653 654 private void dumpRect(int i) { 655 StringBuilder sb = new StringBuilder(); 656 Rect r = mRects.get(i); 657 sb.append("Rect " + i + ":"); 658 sb.append("("); 659 sb.append(r.centerX()); 660 sb.append(","); 661 sb.append(r.centerY()); 662 sb.append(") ["); 663 sb.append(r.width()); 664 sb.append("x"); 665 sb.append(r.height()); 666 sb.append("]"); 667 Log.d(TAG, sb.toString()); 668 } 669 670 private void convertBoxToRect(int i) { 671 Box b = mBoxes.get(i); 672 Rect r = mRects.get(i); 673 int y = b.mCurrentY; 674 int w = widthOf(b); 675 int h = heightOf(b); 676 if (i == 0) { 677 int x = mPlatform.mCurrentX; 678 r.left = x - w / 2; 679 r.right = r.left + w; 680 } else if (i > 0) { 681 Rect a = mRects.get(i - 1); 682 Gap g = mGaps.get(i - 1); 683 r.left = a.right + g.mCurrentGap; 684 r.right = r.left + w; 685 } else { // i < 0 686 Rect a = mRects.get(i + 1); 687 Gap g = mGaps.get(i); 688 r.right = a.left - g.mCurrentGap; 689 r.left = r.right - w; 690 } 691 r.top = y - h / 2; 692 r.bottom = r.top + h; 693 } 694 695 // Returns the position of a box. 696 public Rect getPosition(int index) { 697 return mRects.get(index); 698 } 699 700 //////////////////////////////////////////////////////////////////////////// 701 // Box management 702 //////////////////////////////////////////////////////////////////////////// 703 704 // Initialize the platform to be at the view center. 705 private void initPlatform() { 706 mPlatform.mCurrentX = mViewW / 2; 707 mPlatform.mAnimationStartTime = NO_ANIMATION; 708 } 709 710 // Initialize a box to have the size of the view. 711 private void initBox(int index) { 712 Box b = mBoxes.get(index); 713 b.mImageW = mViewW; 714 b.mImageH = mViewH; 715 b.mUseViewSize = true; 716 b.mScaleMin = getMinimalScale(b.mImageW, b.mImageH); 717 b.mScaleMax = getMaximalScale(b.mImageW, b.mImageH); 718 b.mCurrentY = mViewH / 2; 719 b.mCurrentScale = b.mScaleMin; 720 b.mAnimationStartTime = NO_ANIMATION; 721 } 722 723 // Initialize a gap. This can only be called after the boxes around the gap 724 // has been initialized. 725 private void initGap(int index) { 726 Gap g = mGaps.get(index); 727 g.mDefaultSize = getDefaultGapSize(index); 728 g.mCurrentGap = g.mDefaultSize; 729 g.mAnimationStartTime = NO_ANIMATION; 730 } 731 732 private void initGap(int index, int size) { 733 Gap g = mGaps.get(index); 734 g.mDefaultSize = getDefaultGapSize(index); 735 g.mCurrentGap = size; 736 g.mAnimationStartTime = NO_ANIMATION; 737 } 738 739 private void debugMoveBox(int fromIndex[]) { 740 StringBuilder s = new StringBuilder("moveBox:"); 741 for (int i = 0; i < fromIndex.length; i++) { 742 int j = fromIndex[i]; 743 if (j == Integer.MAX_VALUE) { 744 s.append(" N"); 745 } else { 746 s.append(" "); 747 s.append(fromIndex[i]); 748 } 749 } 750 Log.d(TAG, s.toString()); 751 } 752 753 // Move the boxes: it may indicate focus change, box deleted, box appearing, 754 // box reordered, etc. 755 // 756 // Each element in the fromIndex array indicates where each box was in the 757 // old array. If the value is Integer.MAX_VALUE (pictured as N below), it 758 // means the box is new. 759 // 760 // For example: 761 // N N N N N N N -- all new boxes 762 // -3 -2 -1 0 1 2 3 -- nothing changed 763 // -2 -1 0 1 2 3 N -- focus goes to the next box 764 // N-3 -2 -1 0 1 2 -- focuse goes to the previous box 765 // -3 -2 -1 1 2 3 N -- the focused box was deleted. 766 public void moveBox(int fromIndex[]) { 767 //debugMoveBox(fromIndex); 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 hasNextImages() { 1004 for (int i = 1; i <= BOX_MAX; i++) { 1005 if (!mBoxes.get(i).mUseViewSize) return true; 1006 } 1007 return false; 1008 } 1009 1010 private boolean hasPrevImages() { 1011 for (int i = -1; i >= -BOX_MAX; i--) { 1012 if (!mBoxes.get(i).mUseViewSize) return true; 1013 } 1014 return false; 1015 } 1016 1017 private boolean viewTallerThanScaledImage(float scale) { 1018 return mViewH >= heightOf(mBoxes.get(0), scale); 1019 } 1020 1021 private boolean viewWiderThanScaledImage(float scale) { 1022 return mViewW >= widthOf(mBoxes.get(0), scale); 1023 } 1024 1025 private float getTargetScale(Box b) { 1026 return useCurrentValueAsTarget(b) ? b.mCurrentScale : b.mToScale; 1027 } 1028 1029 private int getTargetX(Platform p) { 1030 return useCurrentValueAsTarget(p) ? p.mCurrentX : p.mToX; 1031 } 1032 1033 private int getTargetY(Box b) { 1034 return useCurrentValueAsTarget(b) ? b.mCurrentY : b.mToY; 1035 } 1036 1037 private boolean useCurrentValueAsTarget(Animatable a) { 1038 return a.mAnimationStartTime == NO_ANIMATION || 1039 a.mAnimationKind == ANIM_KIND_SNAPBACK || 1040 a.mAnimationKind == ANIM_KIND_FLING; 1041 } 1042 1043 // Returns the index of the anchor box. 1044 private int anchorIndex(int i) { 1045 if (i > 0) return i - 1; 1046 if (i < 0) return i + 1; 1047 throw new IllegalArgumentException(); 1048 } 1049 1050 //////////////////////////////////////////////////////////////////////////// 1051 // Animatable: an thing which can do animation. 1052 //////////////////////////////////////////////////////////////////////////// 1053 private abstract static class Animatable { 1054 public long mAnimationStartTime; 1055 public int mAnimationKind; 1056 public int mAnimationDuration; 1057 1058 // This should be overidden in subclass to change the animation values 1059 // give the progress value in [0, 1]. 1060 protected abstract boolean interpolate(float progress); 1061 public abstract boolean startSnapback(); 1062 1063 // Returns true if the animation values changes, so things need to be 1064 // redrawn. 1065 public boolean advanceAnimation() { 1066 if (mAnimationStartTime == NO_ANIMATION) { 1067 return false; 1068 } 1069 if (mAnimationStartTime == LAST_ANIMATION) { 1070 mAnimationStartTime = NO_ANIMATION; 1071 return startSnapback(); 1072 } 1073 1074 float progress; 1075 if (mAnimationDuration == 0) { 1076 progress = 1; 1077 } else { 1078 long now = AnimationTime.get(); 1079 progress = 1080 (float) (now - mAnimationStartTime) / mAnimationDuration; 1081 } 1082 1083 if (progress >= 1) { 1084 progress = 1; 1085 } else { 1086 progress = applyInterpolationCurve(mAnimationKind, progress); 1087 } 1088 1089 boolean done = interpolate(progress); 1090 1091 if (done) { 1092 mAnimationStartTime = LAST_ANIMATION; 1093 } 1094 1095 return true; 1096 } 1097 1098 private static float applyInterpolationCurve(int kind, float progress) { 1099 float f = 1 - progress; 1100 switch (kind) { 1101 case ANIM_KIND_SCROLL: 1102 case ANIM_KIND_FLING: 1103 progress = 1 - f; // linear 1104 break; 1105 case ANIM_KIND_SCALE: 1106 progress = 1 - f * f; // quadratic 1107 break; 1108 case ANIM_KIND_SNAPBACK: 1109 case ANIM_KIND_ZOOM: 1110 case ANIM_KIND_SLIDE: 1111 case ANIM_KIND_OPENING: 1112 progress = 1 - f * f * f * f * f; // x^5 1113 break; 1114 } 1115 return progress; 1116 } 1117 } 1118 1119 //////////////////////////////////////////////////////////////////////////// 1120 // Platform: captures the global X movement. 1121 //////////////////////////////////////////////////////////////////////////// 1122 private class Platform extends Animatable { 1123 public int mCurrentX, mFromX, mToX; 1124 public int mFlingOffset; 1125 1126 @Override 1127 public boolean startSnapback() { 1128 if (mAnimationStartTime != NO_ANIMATION) return false; 1129 if (mAnimationKind == ANIM_KIND_SCROLL 1130 && mListener.isDown()) return false; 1131 1132 Box b = mBoxes.get(0); 1133 float scaleMin = mExtraScalingRange ? 1134 b.mScaleMin * SCALE_MIN_EXTRA : b.mScaleMin; 1135 float scaleMax = mExtraScalingRange ? 1136 b.mScaleMax * SCALE_MAX_EXTRA : b.mScaleMax; 1137 float scale = Utils.clamp(b.mCurrentScale, scaleMin, scaleMax); 1138 int x = mCurrentX; 1139 if (mFilmMode) { 1140 if (!hasNextImages()) x = Math.max(x, mViewW / 2); 1141 if (!hasPrevImages()) x = Math.min(x, mViewW / 2); 1142 } else { 1143 calculateStableBound(scale, HORIZONTAL_SLACK); 1144 x = Utils.clamp(x, mBoundLeft, mBoundRight); 1145 } 1146 if (mCurrentX != x) { 1147 return doAnimation(x, ANIM_KIND_SNAPBACK); 1148 } 1149 return false; 1150 } 1151 1152 // Starts an animation for the platform. 1153 public boolean doAnimation(int targetX, int kind) { 1154 if (mCurrentX == targetX) return false; 1155 mAnimationKind = kind; 1156 mFromX = mCurrentX; 1157 mToX = targetX; 1158 mAnimationStartTime = AnimationTime.startTime(); 1159 mAnimationDuration = ANIM_TIME[kind]; 1160 mFlingOffset = 0; 1161 advanceAnimation(); 1162 return true; 1163 } 1164 1165 @Override 1166 protected boolean interpolate(float progress) { 1167 if (mAnimationKind == ANIM_KIND_FLING) { 1168 return mFilmMode 1169 ? interpolateFlingFilm(progress) 1170 : interpolateFlingPage(progress); 1171 } else { 1172 return interpolateLinear(progress); 1173 } 1174 } 1175 1176 private boolean interpolateFlingFilm(float progress) { 1177 mFilmScroller.computeScrollOffset(); 1178 mCurrentX = mFilmScroller.getCurrX() + mFlingOffset; 1179 1180 int dir = EdgeView.INVALID_DIRECTION; 1181 if (mCurrentX < mViewW / 2) { 1182 if (!hasNextImages()) { 1183 dir = EdgeView.RIGHT; 1184 } 1185 } else if (mCurrentX > mViewW / 2) { 1186 if (!hasPrevImages()) { 1187 dir = EdgeView.LEFT; 1188 } 1189 } 1190 if (dir != EdgeView.INVALID_DIRECTION) { 1191 int v = (int) (mFilmScroller.getCurrVelocity() + 0.5f); 1192 mListener.onAbsorb(v, dir); 1193 mFilmScroller.forceFinished(true); 1194 mCurrentX = mViewW / 2; 1195 } 1196 return mFilmScroller.isFinished(); 1197 } 1198 1199 private boolean interpolateFlingPage(float progress) { 1200 mPageScroller.computeScrollOffset(progress); 1201 Box b = mBoxes.get(0); 1202 calculateStableBound(b.mCurrentScale); 1203 1204 int oldX = mCurrentX; 1205 mCurrentX = mPageScroller.getCurrX(); 1206 1207 // Check if we hit the edges; show edge effects if we do. 1208 if (oldX > mBoundLeft && mCurrentX == mBoundLeft) { 1209 int v = (int) (-mPageScroller.getCurrVelocityX() + 0.5f); 1210 mListener.onAbsorb(v, EdgeView.RIGHT); 1211 } else if (oldX < mBoundRight && mCurrentX == mBoundRight) { 1212 int v = (int) (mPageScroller.getCurrVelocityX() + 0.5f); 1213 mListener.onAbsorb(v, EdgeView.LEFT); 1214 } 1215 1216 return progress >= 1; 1217 } 1218 1219 private boolean interpolateLinear(float progress) { 1220 // Other animations 1221 if (progress >= 1) { 1222 mCurrentX = mToX; 1223 return true; 1224 } else { 1225 mCurrentX = (int) (mFromX + progress * (mToX - mFromX)); 1226 return (mCurrentX == mToX); 1227 } 1228 } 1229 } 1230 1231 //////////////////////////////////////////////////////////////////////////// 1232 // Box: represents a rectangular area which shows a picture. 1233 //////////////////////////////////////////////////////////////////////////// 1234 private class Box extends Animatable { 1235 // Size of the bitmap 1236 public int mImageW, mImageH; 1237 1238 // This is true if we assume the image size is the same as view size 1239 // until we know the actual size of image. This is also used to 1240 // determine if there is an image ready to show. 1241 public boolean mUseViewSize; 1242 1243 // The minimum and maximum scale we allow for this box. 1244 public float mScaleMin, mScaleMax; 1245 1246 // The X/Y value indicates where the center of the box is on the view 1247 // coordinate. We always keep the mCurrent{X,Y,Scale} sync with the 1248 // actual values used currently. Note that the X values are implicitly 1249 // defined by Platform and Gaps. 1250 public int mCurrentY, mFromY, mToY; 1251 public float mCurrentScale, mFromScale, mToScale; 1252 1253 // The absolute X coordinate of the center of the box. This is only used 1254 // during moveBox(). 1255 public int mAbsoluteX; 1256 1257 @Override 1258 public boolean startSnapback() { 1259 if (mAnimationStartTime != NO_ANIMATION) return false; 1260 if (mAnimationKind == ANIM_KIND_SCROLL 1261 && mListener.isDown()) return false; 1262 if (mInScale && this == mBoxes.get(0)) return false; 1263 1264 int y; 1265 float scale; 1266 1267 if (this == mBoxes.get(0)) { 1268 float scaleMin = mExtraScalingRange ? 1269 mScaleMin * SCALE_MIN_EXTRA : mScaleMin; 1270 float scaleMax = mExtraScalingRange ? 1271 mScaleMax * SCALE_MAX_EXTRA : mScaleMax; 1272 scale = Utils.clamp(mCurrentScale, scaleMin, scaleMax); 1273 if (mFilmMode) { 1274 y = mViewH / 2; 1275 } else { 1276 calculateStableBound(scale, HORIZONTAL_SLACK); 1277 y = Utils.clamp(mCurrentY, mBoundTop, mBoundBottom); 1278 } 1279 } else { 1280 y = mViewH / 2; 1281 scale = mScaleMin; 1282 } 1283 1284 if (mCurrentY != y || mCurrentScale != scale) { 1285 return doAnimation(y, scale, ANIM_KIND_SNAPBACK); 1286 } 1287 return false; 1288 } 1289 1290 private boolean doAnimation(int targetY, float targetScale, int kind) { 1291 targetScale = Utils.clamp(targetScale, 1292 SCALE_MIN_EXTRA * mScaleMin, 1293 SCALE_MAX_EXTRA * mScaleMax); 1294 1295 // If the scaled height is smaller than the view height, force it to be 1296 // in the center. (We do this for height only, not width, because the 1297 // user may want to scroll to the previous/next image.) 1298 if (!mInScale && viewTallerThanScaledImage(targetScale)) { 1299 targetY = mViewH / 2; 1300 } 1301 1302 if (mCurrentY == targetY && mCurrentScale == targetScale) { 1303 return false; 1304 } 1305 1306 // Now starts an animation for the box. 1307 mAnimationKind = kind; 1308 mFromY = mCurrentY; 1309 mFromScale = mCurrentScale; 1310 mToY = targetY; 1311 mToScale = targetScale; 1312 mAnimationStartTime = AnimationTime.startTime(); 1313 mAnimationDuration = ANIM_TIME[kind]; 1314 advanceAnimation(); 1315 return true; 1316 } 1317 1318 @Override 1319 protected boolean interpolate(float progress) { 1320 if (mAnimationKind == ANIM_KIND_FLING) { 1321 // Currently a Box can only be flung in page mode. 1322 return interpolateFlingPage(progress); 1323 } else { 1324 return interpolateLinear(progress); 1325 } 1326 } 1327 1328 private boolean interpolateFlingPage(float progress) { 1329 mPageScroller.computeScrollOffset(progress); 1330 calculateStableBound(mCurrentScale); 1331 1332 int oldY = mCurrentY; 1333 mCurrentY = mPageScroller.getCurrY(); 1334 1335 // Check if we hit the edges; show edge effects if we do. 1336 if (oldY > mBoundTop && mCurrentY == mBoundTop) { 1337 int v = (int) (-mPageScroller.getCurrVelocityY() + 0.5f); 1338 mListener.onAbsorb(v, EdgeView.BOTTOM); 1339 } else if (oldY < mBoundBottom && mCurrentY == mBoundBottom) { 1340 int v = (int) (mPageScroller.getCurrVelocityY() + 0.5f); 1341 mListener.onAbsorb(v, EdgeView.TOP); 1342 } 1343 1344 return progress >= 1; 1345 } 1346 1347 private boolean interpolateLinear(float progress) { 1348 if (progress >= 1) { 1349 mCurrentY = mToY; 1350 mCurrentScale = mToScale; 1351 return true; 1352 } else { 1353 mCurrentY = (int) (mFromY + progress * (mToY - mFromY)); 1354 mCurrentScale = mFromScale + progress * (mToScale - mFromScale); 1355 return (mCurrentY == mToY && mCurrentScale == mToScale); 1356 } 1357 } 1358 } 1359 1360 //////////////////////////////////////////////////////////////////////////// 1361 // Gap: represents a rectangular area which is between two boxes. 1362 //////////////////////////////////////////////////////////////////////////// 1363 private class Gap extends Animatable { 1364 // The default gap size between two boxes. The value may vary for 1365 // different image size of the boxes and for different modes (page or 1366 // film). 1367 public int mDefaultSize; 1368 1369 // The gap size between the two boxes. 1370 public int mCurrentGap, mFromGap, mToGap; 1371 1372 @Override 1373 public boolean startSnapback() { 1374 if (mAnimationStartTime != NO_ANIMATION) return false; 1375 return doAnimation(mDefaultSize); 1376 } 1377 1378 // Starts an animation for a gap. 1379 public boolean doAnimation(int targetSize) { 1380 if (mCurrentGap == targetSize) return false; 1381 mAnimationKind = ANIM_KIND_SNAPBACK; 1382 mFromGap = mCurrentGap; 1383 mToGap = targetSize; 1384 mAnimationStartTime = AnimationTime.startTime(); 1385 mAnimationDuration = ANIM_TIME[mAnimationKind]; 1386 advanceAnimation(); 1387 return true; 1388 } 1389 1390 @Override 1391 protected boolean interpolate(float progress) { 1392 if (progress >= 1) { 1393 mCurrentGap = mToGap; 1394 return true; 1395 } else { 1396 mCurrentGap = (int) (mFromGap + progress * (mToGap - mFromGap)); 1397 return (mCurrentGap == mToGap); 1398 } 1399 } 1400 } 1401} 1402