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