PhotoView.java revision b29a27f475a2c449abdda8d4e03d30914feed8c6
1/* 2 * Copyright (C) 2010 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.Bitmap; 21import android.graphics.Color; 22import android.graphics.Point; 23import android.graphics.Rect; 24import android.graphics.RectF; 25import android.os.Message; 26import android.view.MotionEvent; 27import android.view.animation.AccelerateInterpolator; 28 29import com.android.gallery3d.R; 30import com.android.gallery3d.app.GalleryActivity; 31import com.android.gallery3d.common.Utils; 32 33public class PhotoView extends GLView { 34 @SuppressWarnings("unused") 35 private static final String TAG = "PhotoView"; 36 37 public static final int INVALID_SIZE = -1; 38 39 private static final int MSG_TRANSITION_COMPLETE = 1; 40 private static final int MSG_SHOW_LOADING = 2; 41 private static final int MSG_CANCEL_EXTRA_SCALING = 3; 42 43 private static final long DELAY_SHOW_LOADING = 250; // 250ms; 44 45 private static final int TRANS_NONE = 0; 46 private static final int TRANS_SWITCH_NEXT = 3; 47 private static final int TRANS_SWITCH_PREVIOUS = 4; 48 49 public static final int TRANS_SLIDE_IN_RIGHT = 1; 50 public static final int TRANS_SLIDE_IN_LEFT = 2; 51 public static final int TRANS_OPEN_ANIMATION = 5; 52 53 private static final int LOADING_INIT = 0; 54 private static final int LOADING_TIMEOUT = 1; 55 private static final int LOADING_COMPLETE = 2; 56 private static final int LOADING_FAIL = 3; 57 58 private static final int ENTRY_PREVIOUS = 0; 59 private static final int ENTRY_NEXT = 1; 60 61 private static final int IMAGE_GAP = 96; 62 private static final int SWITCH_THRESHOLD = 256; 63 private static final float SWIPE_THRESHOLD = 300f; 64 65 private static final float DEFAULT_TEXT_SIZE = 20; 66 private static float TRANSITION_SCALE_FACTOR = 0.74f; 67 68 // Used to calculate the scaling factor for the fading animation. 69 private ZInterpolator mScaleInterpolator = new ZInterpolator(0.5f); 70 71 // Used to calculate the alpha factor for the fading animation. 72 private AccelerateInterpolator mAlphaInterpolator = 73 new AccelerateInterpolator(0.9f); 74 75 public interface PhotoTapListener { 76 public void onSingleTapUp(int x, int y); 77 } 78 79 // the previous/next image entries 80 private final ScreenNailEntry mScreenNails[] = new ScreenNailEntry[2]; 81 82 private final GestureRecognizer mGestureRecognizer; 83 84 private PhotoTapListener mPhotoTapListener; 85 86 private final PositionController mPositionController; 87 88 private Model mModel; 89 private StringTexture mLoadingText; 90 private StringTexture mNoThumbnailText; 91 private int mTransitionMode = TRANS_NONE; 92 private final TileImageView mTileView; 93 private EdgeView mEdgeView; 94 private Texture mVideoPlayIcon; 95 96 private boolean mShowVideoPlayIcon; 97 private ProgressSpinner mLoadingSpinner; 98 99 private SynchronizedHandler mHandler; 100 101 private int mLoadingState = LOADING_COMPLETE; 102 103 private int mImageRotation; 104 105 private Rect mOpenAnimationRect; 106 private Point mImageCenter = new Point(); 107 private boolean mCancelExtraScalingPending; 108 109 public PhotoView(GalleryActivity activity) { 110 mTileView = new TileImageView(activity); 111 addComponent(mTileView); 112 Context context = activity.getAndroidContext(); 113 mEdgeView = new EdgeView(context); 114 addComponent(mEdgeView); 115 mLoadingSpinner = new ProgressSpinner(context); 116 mLoadingText = StringTexture.newInstance( 117 context.getString(R.string.loading), 118 DEFAULT_TEXT_SIZE, Color.WHITE); 119 mNoThumbnailText = StringTexture.newInstance( 120 context.getString(R.string.no_thumbnail), 121 DEFAULT_TEXT_SIZE, Color.WHITE); 122 123 mHandler = new SynchronizedHandler(activity.getGLRoot()) { 124 @Override 125 public void handleMessage(Message message) { 126 switch (message.what) { 127 case MSG_TRANSITION_COMPLETE: { 128 onTransitionComplete(); 129 break; 130 } 131 case MSG_SHOW_LOADING: { 132 if (mLoadingState == LOADING_INIT) { 133 // We don't need the opening animation 134 mOpenAnimationRect = null; 135 136 mLoadingSpinner.startAnimation(); 137 mLoadingState = LOADING_TIMEOUT; 138 invalidate(); 139 } 140 break; 141 } 142 case MSG_CANCEL_EXTRA_SCALING: { 143 mGestureRecognizer.cancelScale(); 144 mPositionController.setExtraScalingRange(false); 145 mCancelExtraScalingPending = false; 146 break; 147 } 148 default: throw new AssertionError(message.what); 149 } 150 } 151 }; 152 153 mGestureRecognizer = new GestureRecognizer( 154 context, new MyGestureListener()); 155 156 for (int i = 0, n = mScreenNails.length; i < n; ++i) { 157 mScreenNails[i] = new ScreenNailEntry(); 158 } 159 160 mPositionController = new PositionController(this, context, mEdgeView); 161 mVideoPlayIcon = new ResourceTexture(context, R.drawable.ic_control_play); 162 } 163 164 165 public void setModel(Model model) { 166 if (mModel == model) return; 167 mModel = model; 168 mTileView.setModel(model); 169 if (model != null) notifyOnNewImage(); 170 } 171 172 public void setPhotoTapListener(PhotoTapListener listener) { 173 mPhotoTapListener = listener; 174 } 175 176 private void setTileViewPosition(int centerX, int centerY, float scale) { 177 TileImageView t = mTileView; 178 179 // Calculate the move-out progress value. 180 RectF bounds = mPositionController.getImageBounds(); 181 int left = Math.round(bounds.left); 182 int right = Math.round(bounds.right); 183 int width = getWidth(); 184 float progress = calculateMoveOutProgress(left, right, width); 185 progress = Utils.clamp(progress, -1f, 1f); 186 187 // We only want to apply the fading animation if the scrolling movement 188 // is to the right. 189 if (progress < 0) { 190 if (right - left < width) { 191 // If the picture is narrower than the view, keep it at the center 192 // of the view. 193 centerX = mPositionController.getImageWidth() / 2; 194 } else { 195 // If the picture is wider than the view (it's zoomed-in), keep 196 // the left edge of the object align the the left edge of the view. 197 centerX = Math.round(width / 2f / scale); 198 } 199 scale *= getScrollScale(progress); 200 t.setAlpha(getScrollAlpha(progress)); 201 } 202 203 // set the position of the tile view 204 int inverseX = mPositionController.getImageWidth() - centerX; 205 int inverseY = mPositionController.getImageHeight() - centerY; 206 int rotation = mImageRotation; 207 switch (rotation) { 208 case 0: t.setPosition(centerX, centerY, scale, 0); break; 209 case 90: t.setPosition(centerY, inverseX, scale, 90); break; 210 case 180: t.setPosition(inverseX, inverseY, scale, 180); break; 211 case 270: t.setPosition(inverseY, centerX, scale, 270); break; 212 default: throw new IllegalArgumentException(String.valueOf(rotation)); 213 } 214 } 215 216 public void setPosition(int centerX, int centerY, float scale) { 217 setTileViewPosition(centerX, centerY, scale); 218 layoutScreenNails(); 219 } 220 221 private void updateScreenNailEntry(int which, ScreenNail screenNail) { 222 if (mTransitionMode == TRANS_SWITCH_NEXT 223 || mTransitionMode == TRANS_SWITCH_PREVIOUS) { 224 // ignore screen nail updating during switching 225 return; 226 } 227 ScreenNailEntry entry = mScreenNails[which]; 228 entry.updateScreenNail(screenNail); 229 } 230 231 // -1 previous, 0 current, 1 next 232 public void notifyImageInvalidated(int which) { 233 switch (which) { 234 case -1: { 235 updateScreenNailEntry( 236 ENTRY_PREVIOUS, mModel.getPrevScreenNail()); 237 layoutScreenNails(); 238 invalidate(); 239 break; 240 } 241 case 1: { 242 updateScreenNailEntry(ENTRY_NEXT, mModel.getNextScreenNail()); 243 layoutScreenNails(); 244 invalidate(); 245 break; 246 } 247 case 0: { 248 // mImageWidth and mImageHeight will get updated 249 mTileView.notifyModelInvalidated(); 250 mTileView.setAlpha(1.0f); 251 252 mImageRotation = mModel.getImageRotation(); 253 if (((mImageRotation / 90) & 1) == 0) { 254 mPositionController.setImageSize( 255 mTileView.mImageWidth, mTileView.mImageHeight); 256 } else { 257 mPositionController.setImageSize( 258 mTileView.mImageHeight, mTileView.mImageWidth); 259 } 260 updateLoadingState(); 261 break; 262 } 263 } 264 } 265 266 private void updateLoadingState() { 267 // Possible transitions of mLoadingState: 268 // INIT --> TIMEOUT, COMPLETE, FAIL 269 // TIMEOUT --> COMPLETE, FAIL, INIT 270 // COMPLETE --> INIT 271 // FAIL --> INIT 272 if (mModel.getLevelCount() != 0 || mModel.getScreenNail() != null) { 273 mHandler.removeMessages(MSG_SHOW_LOADING); 274 mLoadingState = LOADING_COMPLETE; 275 } else if (mModel.isFailedToLoad()) { 276 mHandler.removeMessages(MSG_SHOW_LOADING); 277 mLoadingState = LOADING_FAIL; 278 // We don't want the opening animation after loading failure 279 mOpenAnimationRect = null; 280 } else if (mLoadingState != LOADING_INIT) { 281 mLoadingState = LOADING_INIT; 282 mHandler.removeMessages(MSG_SHOW_LOADING); 283 mHandler.sendEmptyMessageDelayed( 284 MSG_SHOW_LOADING, DELAY_SHOW_LOADING); 285 } 286 } 287 288 public void notifyModelInvalidated() { 289 if (mModel == null) { 290 updateScreenNailEntry(ENTRY_PREVIOUS, null); 291 updateScreenNailEntry(ENTRY_NEXT, null); 292 } else { 293 updateScreenNailEntry(ENTRY_PREVIOUS, mModel.getPrevScreenNail()); 294 updateScreenNailEntry(ENTRY_NEXT, mModel.getNextScreenNail()); 295 } 296 layoutScreenNails(); 297 298 if (mModel == null) { 299 mTileView.notifyModelInvalidated(); 300 mTileView.setAlpha(1.0f); 301 mImageRotation = 0; 302 mPositionController.setImageSize(0, 0); 303 updateLoadingState(); 304 } else { 305 notifyImageInvalidated(0); 306 } 307 } 308 309 @Override 310 protected boolean onTouch(MotionEvent event) { 311 mGestureRecognizer.onTouchEvent(event); 312 return true; 313 } 314 315 @Override 316 protected void onLayout( 317 boolean changeSize, int left, int top, int right, int bottom) { 318 mTileView.layout(left, top, right, bottom); 319 mEdgeView.layout(left, top, right, bottom); 320 if (changeSize) { 321 mPositionController.setViewSize(getWidth(), getHeight()); 322 for (ScreenNailEntry entry : mScreenNails) { 323 entry.updateDrawingSize(); 324 } 325 } 326 } 327 328 private static int gapToSide(int imageWidth, int viewWidth) { 329 return Math.max(0, (viewWidth - imageWidth) / 2); 330 } 331 332 /* 333 * Here is how we layout the screen nails 334 * 335 * previous current next 336 * ___________ ________________ __________ 337 * | _______ | | __________ | | ______ | 338 * | | | | | | right->| | | | | | 339 * | | |<-------->|<--left | | | | | | 340 * | |_______| | | | |__________| | | |______| | 341 * |___________| | |________________| |__________| 342 * | <--> gapToSide() 343 * | 344 * IMAGE_GAP + Max(previous.gapToSide(), current.gapToSide) 345 */ 346 private void layoutScreenNails() { 347 int width = getWidth(); 348 int height = getHeight(); 349 350 // Use the image width in AC, since we may fake the size if the 351 // image is unavailable 352 RectF bounds = mPositionController.getImageBounds(); 353 int left = Math.round(bounds.left); 354 int right = Math.round(bounds.right); 355 int gap = gapToSide(right - left, width); 356 357 // layout the previous image 358 ScreenNailEntry entry = mScreenNails[ENTRY_PREVIOUS]; 359 360 if (entry.isEnabled()) { 361 entry.layoutRightEdgeAt(left - ( 362 IMAGE_GAP + Math.max(gap, entry.gapToSide()))); 363 } 364 365 // layout the next image 366 entry = mScreenNails[ENTRY_NEXT]; 367 if (entry.isEnabled()) { 368 entry.layoutLeftEdgeAt(right + ( 369 IMAGE_GAP + Math.max(gap, entry.gapToSide()))); 370 } 371 } 372 373 @Override 374 protected void render(GLCanvas canvas) { 375 boolean drawScreenNail = (mTransitionMode != TRANS_SLIDE_IN_LEFT 376 && mTransitionMode != TRANS_SLIDE_IN_RIGHT 377 && mTransitionMode != TRANS_OPEN_ANIMATION); 378 379 // Draw the next photo 380 if (drawScreenNail) { 381 ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT]; 382 nextNail.draw(canvas, true); 383 } 384 385 // Draw the current photo 386 if (mLoadingState == LOADING_COMPLETE) { 387 super.render(canvas); 388 } 389 390 // If the photo is loaded, draw the message/icon at the center of it, 391 // otherwise draw the message/icon at the center of the view. 392 if (mLoadingState == LOADING_COMPLETE) { 393 mTileView.getImageCenter(mImageCenter); 394 renderMessage(canvas, mImageCenter.x, mImageCenter.y); 395 } else { 396 renderMessage(canvas, getWidth() / 2, getHeight() / 2); 397 } 398 399 // Draw the previous photo 400 if (drawScreenNail) { 401 ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS]; 402 prevNail.draw(canvas, false); 403 } 404 } 405 406 private void renderMessage(GLCanvas canvas, int x, int y) { 407 // Draw the progress spinner and the text below it 408 // 409 // (x, y) is where we put the center of the spinner. 410 // s is the size of the video play icon, and we use s to layout text 411 // because we want to keep the text at the same place when the video 412 // play icon is shown instead of the spinner. 413 int w = getWidth(); 414 int h = getHeight(); 415 int s = Math.min(getWidth(), getHeight()) / 6; 416 417 if (mLoadingState == LOADING_TIMEOUT) { 418 StringTexture m = mLoadingText; 419 ProgressSpinner r = mLoadingSpinner; 420 r.draw(canvas, x - r.getWidth() / 2, y - r.getHeight() / 2); 421 m.draw(canvas, x - m.getWidth() / 2, y + s / 2 + 5); 422 invalidate(); // we need to keep the spinner rotating 423 } else if (mLoadingState == LOADING_FAIL) { 424 StringTexture m = mNoThumbnailText; 425 m.draw(canvas, x - m.getWidth() / 2, y + s / 2 + 5); 426 } 427 428 // Draw the video play icon (in the place where the spinner was) 429 if (mShowVideoPlayIcon 430 && mLoadingState != LOADING_INIT 431 && mLoadingState != LOADING_TIMEOUT) { 432 mVideoPlayIcon.draw(canvas, x - s / 2, y - s / 2, s, s); 433 } 434 435 mPositionController.advanceAnimation(); 436 } 437 438 private void stopCurrentSwipingIfNeeded() { 439 // Enable fast swiping 440 if (mTransitionMode == TRANS_SWITCH_NEXT) { 441 mTransitionMode = TRANS_NONE; 442 mPositionController.stopAnimation(); 443 switchToNextImage(); 444 } else if (mTransitionMode == TRANS_SWITCH_PREVIOUS) { 445 mTransitionMode = TRANS_NONE; 446 mPositionController.stopAnimation(); 447 switchToPreviousImage(); 448 } 449 } 450 451 private boolean swipeImages(float velocityX, float velocityY) { 452 if (mTransitionMode != TRANS_NONE 453 && mTransitionMode != TRANS_SWITCH_NEXT 454 && mTransitionMode != TRANS_SWITCH_PREVIOUS) return false; 455 456 // Avoid swiping images if we're possibly flinging to view the 457 // zoomed in picture vertically. 458 PositionController controller = mPositionController; 459 boolean isMinimal = controller.isAtMinimalScale(); 460 int edges = controller.getImageAtEdges(); 461 if (!isMinimal && Math.abs(velocityY) > Math.abs(velocityX)) 462 if ((edges & PositionController.IMAGE_AT_TOP_EDGE) == 0 463 || (edges & PositionController.IMAGE_AT_BOTTOM_EDGE) == 0) 464 return false; 465 466 // If we are at the edge of the current photo and the sweeping velocity 467 // exceeds the threshold, switch to next / previous image. 468 int halfWidth = getWidth() / 2; 469 if (velocityX < -SWIPE_THRESHOLD && (isMinimal 470 || (edges & PositionController.IMAGE_AT_RIGHT_EDGE) != 0)) { 471 stopCurrentSwipingIfNeeded(); 472 ScreenNailEntry next = mScreenNails[ENTRY_NEXT]; 473 if (next.isEnabled()) { 474 mTransitionMode = TRANS_SWITCH_NEXT; 475 controller.startHorizontalSlide(next.mOffsetX - halfWidth); 476 return true; 477 } 478 } else if (velocityX > SWIPE_THRESHOLD && (isMinimal 479 || (edges & PositionController.IMAGE_AT_LEFT_EDGE) != 0)) { 480 stopCurrentSwipingIfNeeded(); 481 ScreenNailEntry prev = mScreenNails[ENTRY_PREVIOUS]; 482 if (prev.isEnabled()) { 483 mTransitionMode = TRANS_SWITCH_PREVIOUS; 484 controller.startHorizontalSlide(prev.mOffsetX - halfWidth); 485 return true; 486 } 487 } 488 489 return false; 490 } 491 492 private boolean snapToNeighborImage() { 493 if (mTransitionMode != TRANS_NONE) return false; 494 495 PositionController controller = mPositionController; 496 RectF bounds = controller.getImageBounds(); 497 int left = Math.round(bounds.left); 498 int right = Math.round(bounds.right); 499 int width = getWidth(); 500 int threshold = SWITCH_THRESHOLD + gapToSide(right - left, width); 501 502 // If we have moved the picture a lot, switching. 503 ScreenNailEntry next = mScreenNails[ENTRY_NEXT]; 504 if (next.isEnabled() && threshold < width - right) { 505 mTransitionMode = TRANS_SWITCH_NEXT; 506 controller.startHorizontalSlide(next.mOffsetX - width / 2); 507 return true; 508 } 509 ScreenNailEntry prev = mScreenNails[ENTRY_PREVIOUS]; 510 if (prev.isEnabled() && threshold < left) { 511 mTransitionMode = TRANS_SWITCH_PREVIOUS; 512 controller.startHorizontalSlide(prev.mOffsetX - width / 2); 513 return true; 514 } 515 516 return false; 517 } 518 519 private class MyGestureListener implements GestureRecognizer.Listener { 520 private boolean mIgnoreUpEvent = false; 521 522 @Override 523 public boolean onSingleTapUp(float x, float y) { 524 if (mPhotoTapListener != null) { 525 mPhotoTapListener.onSingleTapUp((int) x, (int) y); 526 } 527 return true; 528 } 529 530 @Override 531 public boolean onDoubleTap(float x, float y) { 532 if (mTransitionMode != TRANS_NONE) return true; 533 PositionController controller = mPositionController; 534 float scale = controller.getCurrentScale(); 535 // onDoubleTap happened on the second ACTION_DOWN. 536 // We need to ignore the next UP event. 537 mIgnoreUpEvent = true; 538 if (scale <= 1.0f || controller.isAtMinimalScale()) { 539 controller.zoomIn(x, y, Math.max(1.5f, scale * 1.5f)); 540 } else { 541 controller.resetToFullView(); 542 } 543 return true; 544 } 545 546 @Override 547 public boolean onScroll(float dx, float dy) { 548 if (mTransitionMode != TRANS_NONE) return true; 549 550 ScreenNailEntry next = mScreenNails[ENTRY_NEXT]; 551 ScreenNailEntry prev = mScreenNails[ENTRY_PREVIOUS]; 552 553 mPositionController.startScroll(dx, dy, next.isEnabled(), 554 prev.isEnabled()); 555 return true; 556 } 557 558 @Override 559 public boolean onFling(float velocityX, float velocityY) { 560 if (swipeImages(velocityX, velocityY)) { 561 mIgnoreUpEvent = true; 562 } else if (mTransitionMode != TRANS_NONE) { 563 // do nothing 564 } else if (mPositionController.fling(velocityX, velocityY)) { 565 mIgnoreUpEvent = true; 566 } 567 return true; 568 } 569 570 @Override 571 public boolean onScaleBegin(float focusX, float focusY) { 572 if (mTransitionMode != TRANS_NONE) return false; 573 mPositionController.beginScale(focusX, focusY); 574 return true; 575 } 576 577 @Override 578 public boolean onScale(float focusX, float focusY, float scale) { 579 if (Float.isNaN(scale) || Float.isInfinite(scale) 580 || mTransitionMode != TRANS_NONE) return true; 581 boolean outOfRange = mPositionController.scaleBy( 582 scale, focusX, focusY); 583 if (outOfRange) { 584 if (!mCancelExtraScalingPending) { 585 mHandler.sendEmptyMessageDelayed( 586 MSG_CANCEL_EXTRA_SCALING, 700); 587 mPositionController.setExtraScalingRange(true); 588 mCancelExtraScalingPending = true; 589 } 590 } else { 591 if (mCancelExtraScalingPending) { 592 mHandler.removeMessages(MSG_CANCEL_EXTRA_SCALING); 593 mPositionController.setExtraScalingRange(false); 594 mCancelExtraScalingPending = false; 595 } 596 } 597 return true; 598 } 599 600 @Override 601 public void onScaleEnd() { 602 mPositionController.endScale(); 603 snapToNeighborImage(); 604 } 605 606 @Override 607 public void onDown() { 608 } 609 610 @Override 611 public void onUp() { 612 mEdgeView.onRelease(); 613 614 if (mIgnoreUpEvent) { 615 mIgnoreUpEvent = false; 616 return; 617 } 618 if (!snapToNeighborImage() && mTransitionMode == TRANS_NONE) { 619 mPositionController.up(); 620 } 621 } 622 } 623 624 public boolean jumpTo(int index) { 625 if (mTransitionMode != TRANS_NONE) return false; 626 mModel.jumpTo(index); 627 return true; 628 } 629 630 public void notifyOnNewImage() { 631 mPositionController.setImageSize(0, 0); 632 } 633 634 public void startSlideInAnimation(int direction) { 635 PositionController a = mPositionController; 636 a.stopAnimation(); 637 switch (direction) { 638 case TRANS_SLIDE_IN_LEFT: 639 case TRANS_SLIDE_IN_RIGHT: { 640 mTransitionMode = direction; 641 a.startSlideInAnimation(direction); 642 break; 643 } 644 default: throw new IllegalArgumentException(String.valueOf(direction)); 645 } 646 } 647 648 private void switchToNextImage() { 649 // We update the texture here directly to prevent texture uploading. 650 ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS]; 651 ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT]; 652 mTileView.invalidateTiles(); 653 prevNail.updateScreenNail(mTileView.releaseScreenNail()); 654 mTileView.updateScreenNail(nextNail.releaseScreenNail()); 655 mModel.next(); 656 } 657 658 private void switchToPreviousImage() { 659 // We update the texture here directly to prevent texture uploading. 660 ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS]; 661 ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT]; 662 mTileView.invalidateTiles(); 663 nextNail.updateScreenNail(mTileView.releaseScreenNail()); 664 mTileView.updateScreenNail(prevNail.releaseScreenNail()); 665 mModel.previous(); 666 } 667 668 public void notifyTransitionComplete() { 669 mHandler.sendEmptyMessage(MSG_TRANSITION_COMPLETE); 670 } 671 672 private void onTransitionComplete() { 673 int mode = mTransitionMode; 674 mTransitionMode = TRANS_NONE; 675 676 if (mModel == null) return; 677 if (mode == TRANS_SWITCH_NEXT) { 678 switchToNextImage(); 679 } else if (mode == TRANS_SWITCH_PREVIOUS) { 680 switchToPreviousImage(); 681 } 682 } 683 684 public boolean isDown() { 685 return mGestureRecognizer.isDown(); 686 } 687 688 public static interface Model extends TileImageView.Model { 689 public void next(); 690 public void previous(); 691 public void jumpTo(int index); 692 public int getImageRotation(); 693 694 // Return null if the specified image is unavailable. 695 public ScreenNail getNextScreenNail(); 696 public ScreenNail getPrevScreenNail(); 697 } 698 699 private static int getRotated(int degree, int original, int theother) { 700 return ((degree / 90) & 1) == 0 ? original : theother; 701 } 702 703 private class ScreenNailEntry { 704 private boolean mVisible; 705 private boolean mEnabled; 706 707 private int mDrawWidth; 708 private int mDrawHeight; 709 private int mOffsetX; 710 private int mRotation; 711 712 private ScreenNail mScreenNail; 713 714 public void updateScreenNail(ScreenNail screenNail) { 715 mEnabled = (screenNail != null); 716 if (mScreenNail == screenNail) return; 717 if (mScreenNail != null) mScreenNail.pauseDraw(); 718 mScreenNail = screenNail; 719 if (mScreenNail != null) { 720 mRotation = mScreenNail.getRotation(); 721 updateDrawingSize(); 722 } 723 } 724 725 // Release the ownership of the ScreenNail from this entry. 726 public ScreenNail releaseScreenNail() { 727 ScreenNail s = mScreenNail; 728 mScreenNail = null; 729 return s; 730 } 731 732 public void layoutRightEdgeAt(int x) { 733 mVisible = x > 0; 734 mOffsetX = x - getRotated( 735 mRotation, mDrawWidth, mDrawHeight) / 2; 736 } 737 738 public void layoutLeftEdgeAt(int x) { 739 mVisible = x < getWidth(); 740 mOffsetX = x + getRotated( 741 mRotation, mDrawWidth, mDrawHeight) / 2; 742 } 743 744 public int gapToSide() { 745 return ((mRotation / 90) & 1) != 0 746 ? PhotoView.gapToSide(mDrawHeight, getWidth()) 747 : PhotoView.gapToSide(mDrawWidth, getWidth()); 748 } 749 750 public void updateDrawingSize() { 751 if (mScreenNail == null) return; 752 753 int width = mScreenNail.getWidth(); 754 int height = mScreenNail.getHeight(); 755 756 // Calculate the initial scale that will used by PositionController 757 // (usually fit-to-screen) 758 float s = ((mRotation / 90) & 0x01) == 0 759 ? mPositionController.getMinimalScale(width, height) 760 : mPositionController.getMinimalScale(height, width); 761 762 mDrawWidth = Math.round(width * s); 763 mDrawHeight = Math.round(height * s); 764 } 765 766 public boolean isEnabled() { 767 return mEnabled; 768 } 769 770 public void draw(GLCanvas canvas, boolean applyFadingAnimation) { 771 if (mScreenNail == null) return; 772 if (!mVisible) { 773 mScreenNail.noDraw(); 774 return; 775 } 776 777 int w = getWidth(); 778 int x = applyFadingAnimation ? w / 2 : mOffsetX; 779 int y = getHeight() / 2; 780 int flags = GLCanvas.SAVE_FLAG_MATRIX; 781 782 if (applyFadingAnimation) flags |= GLCanvas.SAVE_FLAG_ALPHA; 783 canvas.save(flags); 784 canvas.translate(x, y); 785 if (applyFadingAnimation) { 786 float progress = (float) (x - mOffsetX) / w; 787 float alpha = getScrollAlpha(progress); 788 float scale = getScrollScale(progress); 789 canvas.multiplyAlpha(alpha); 790 canvas.scale(scale, scale, 1); 791 } 792 if (mRotation != 0) { 793 canvas.rotate(mRotation, 0, 0, 1); 794 } 795 canvas.translate(-x, -y); 796 mScreenNail.draw(canvas, x - mDrawWidth / 2, y - mDrawHeight / 2, 797 mDrawWidth, mDrawHeight); 798 canvas.restore(); 799 } 800 } 801 802 // Returns the scrolling progress value for an object moving out of a 803 // view. The progress value measures how much the object has moving out of 804 // the view. The object currently displays in [left, right), and the view is 805 // at [0, viewWidth]. 806 // 807 // The returned value is negative when the object is moving right, and 808 // positive when the object is moving left. The value goes to -1 or 1 when 809 // the object just moves out of the view completely. The value is 0 if the 810 // object currently fills the view. 811 private static float calculateMoveOutProgress(int left, int right, 812 int viewWidth) { 813 // w = object width 814 // viewWidth = view width 815 int w = right - left; 816 817 // If the object width is smaller than the view width, 818 // |....view....| 819 // |<-->| progress = -1 when left = viewWidth 820 // |<-->| progress = 1 when left = -w 821 // So progress = 1 - 2 * (left + w) / (viewWidth + w) 822 if (w < viewWidth) { 823 return 1f - 2f * (left + w) / (viewWidth + w); 824 } 825 826 // If the object width is larger than the view width, 827 // |..view..| 828 // |<--------->| progress = -1 when left = viewWidth 829 // |<--------->| progress = 0 between left = 0 830 // |<--------->| and right = viewWidth 831 // |<--------->| progress = 1 when right = 0 832 if (left > 0) { 833 return -left / (float) viewWidth; 834 } 835 836 if (right < viewWidth) { 837 return (viewWidth - right) / (float) viewWidth; 838 } 839 840 return 0; 841 } 842 843 // Maps a scrolling progress value to the alpha factor in the fading 844 // animation. 845 private float getScrollAlpha(float scrollProgress) { 846 return scrollProgress < 0 ? mAlphaInterpolator.getInterpolation( 847 1 - Math.abs(scrollProgress)) : 1.0f; 848 } 849 850 // Maps a scrolling progress value to the scaling factor in the fading 851 // animation. 852 private float getScrollScale(float scrollProgress) { 853 float interpolatedProgress = mScaleInterpolator.getInterpolation( 854 Math.abs(scrollProgress)); 855 float scale = (1 - interpolatedProgress) + 856 interpolatedProgress * TRANSITION_SCALE_FACTOR; 857 return scale; 858 } 859 860 861 // This interpolator emulates the rate at which the perceived scale of an 862 // object changes as its distance from a camera increases. When this 863 // interpolator is applied to a scale animation on a view, it evokes the 864 // sense that the object is shrinking due to moving away from the camera. 865 private static class ZInterpolator { 866 private float focalLength; 867 868 public ZInterpolator(float foc) { 869 focalLength = foc; 870 } 871 872 public float getInterpolation(float input) { 873 return (1.0f - focalLength / (focalLength + input)) / 874 (1.0f - focalLength / (focalLength + 1.0f)); 875 } 876 } 877 878 public void pause() { 879 mPositionController.skipAnimation(); 880 mTransitionMode = TRANS_NONE; 881 mTileView.freeTextures(); 882 for (ScreenNailEntry entry : mScreenNails) { 883 entry.updateScreenNail(null); 884 } 885 } 886 887 public void resume() { 888 mTileView.prepareTextures(); 889 } 890 891 public void setOpenAnimationRect(Rect rect) { 892 mOpenAnimationRect = rect; 893 } 894 895 public void showVideoPlayIcon(boolean show) { 896 mShowVideoPlayIcon = show; 897 } 898 899 // Returns the opening animation rectangle saved by the previous page. 900 public Rect retrieveOpenAnimationRect() { 901 Rect r = mOpenAnimationRect; 902 mOpenAnimationRect = null; 903 return r; 904 } 905 906 public void openAnimationStarted() { 907 mTransitionMode = TRANS_OPEN_ANIMATION; 908 } 909 910 public boolean isInTransition() { 911 return mTransitionMode != TRANS_NONE; 912 } 913} 914