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