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