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