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