PhotoView.java revision 921895ba0b3511aeba053bdc0c965f9d3f62eb51
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 void notifyOnNewImage() { 625 mPositionController.setImageSize(0, 0); 626 } 627 628 public void startSlideInAnimation(int direction) { 629 PositionController a = mPositionController; 630 a.stopAnimation(); 631 switch (direction) { 632 case TRANS_SLIDE_IN_LEFT: 633 case TRANS_SLIDE_IN_RIGHT: { 634 mTransitionMode = direction; 635 a.startSlideInAnimation(direction); 636 break; 637 } 638 default: throw new IllegalArgumentException(String.valueOf(direction)); 639 } 640 } 641 642 private void switchToNextImage() { 643 // We update the texture here directly to prevent texture uploading. 644 ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS]; 645 ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT]; 646 mTileView.invalidateTiles(); 647 prevNail.updateScreenNail(mTileView.releaseScreenNail()); 648 mTileView.updateScreenNail(nextNail.releaseScreenNail()); 649 mModel.next(); 650 } 651 652 private void switchToPreviousImage() { 653 // We update the texture here directly to prevent texture uploading. 654 ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS]; 655 ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT]; 656 mTileView.invalidateTiles(); 657 nextNail.updateScreenNail(mTileView.releaseScreenNail()); 658 mTileView.updateScreenNail(prevNail.releaseScreenNail()); 659 mModel.previous(); 660 } 661 662 public void notifyTransitionComplete() { 663 mHandler.sendEmptyMessage(MSG_TRANSITION_COMPLETE); 664 } 665 666 private void onTransitionComplete() { 667 int mode = mTransitionMode; 668 mTransitionMode = TRANS_NONE; 669 670 if (mModel == null) return; 671 if (mode == TRANS_SWITCH_NEXT) { 672 switchToNextImage(); 673 } else if (mode == TRANS_SWITCH_PREVIOUS) { 674 switchToPreviousImage(); 675 } 676 } 677 678 public boolean isDown() { 679 return mGestureRecognizer.isDown(); 680 } 681 682 public static interface Model extends TileImageView.Model { 683 public void next(); 684 public void previous(); 685 public int getImageRotation(); 686 687 // Return null if the specified image is unavailable. 688 public ScreenNail getNextScreenNail(); 689 public ScreenNail getPrevScreenNail(); 690 } 691 692 private static int getRotated(int degree, int original, int theother) { 693 return ((degree / 90) & 1) == 0 ? original : theother; 694 } 695 696 private class ScreenNailEntry { 697 private boolean mVisible; 698 private boolean mEnabled; 699 700 private int mDrawWidth; 701 private int mDrawHeight; 702 private int mOffsetX; 703 private int mRotation; 704 705 private ScreenNail mScreenNail; 706 707 public void updateScreenNail(ScreenNail screenNail) { 708 mEnabled = (screenNail != null); 709 if (mScreenNail == screenNail) return; 710 if (mScreenNail != null) mScreenNail.pauseDraw(); 711 mScreenNail = screenNail; 712 if (mScreenNail != null) { 713 mRotation = mScreenNail.getRotation(); 714 updateDrawingSize(); 715 } 716 } 717 718 // Release the ownership of the ScreenNail from this entry. 719 public ScreenNail releaseScreenNail() { 720 ScreenNail s = mScreenNail; 721 mScreenNail = null; 722 return s; 723 } 724 725 public void layoutRightEdgeAt(int x) { 726 mVisible = x > 0; 727 mOffsetX = x - getRotated( 728 mRotation, mDrawWidth, mDrawHeight) / 2; 729 } 730 731 public void layoutLeftEdgeAt(int x) { 732 mVisible = x < getWidth(); 733 mOffsetX = x + getRotated( 734 mRotation, mDrawWidth, mDrawHeight) / 2; 735 } 736 737 public int gapToSide() { 738 return ((mRotation / 90) & 1) != 0 739 ? PhotoView.gapToSide(mDrawHeight, getWidth()) 740 : PhotoView.gapToSide(mDrawWidth, getWidth()); 741 } 742 743 public void updateDrawingSize() { 744 if (mScreenNail == null) return; 745 746 int width = mScreenNail.getWidth(); 747 int height = mScreenNail.getHeight(); 748 749 // Calculate the initial scale that will used by PositionController 750 // (usually fit-to-screen) 751 float s = ((mRotation / 90) & 0x01) == 0 752 ? mPositionController.getMinimalScale(width, height) 753 : mPositionController.getMinimalScale(height, width); 754 755 mDrawWidth = Math.round(width * s); 756 mDrawHeight = Math.round(height * s); 757 } 758 759 public boolean isEnabled() { 760 return mEnabled; 761 } 762 763 public void draw(GLCanvas canvas, boolean applyFadingAnimation) { 764 if (mScreenNail == null) return; 765 if (!mVisible) { 766 mScreenNail.noDraw(); 767 return; 768 } 769 770 int w = getWidth(); 771 int x = applyFadingAnimation ? w / 2 : mOffsetX; 772 int y = getHeight() / 2; 773 int flags = GLCanvas.SAVE_FLAG_MATRIX; 774 775 if (applyFadingAnimation) flags |= GLCanvas.SAVE_FLAG_ALPHA; 776 canvas.save(flags); 777 canvas.translate(x, y); 778 if (applyFadingAnimation) { 779 float progress = (float) (x - mOffsetX) / w; 780 float alpha = getScrollAlpha(progress); 781 float scale = getScrollScale(progress); 782 canvas.multiplyAlpha(alpha); 783 canvas.scale(scale, scale, 1); 784 } 785 if (mRotation != 0) { 786 canvas.rotate(mRotation, 0, 0, 1); 787 } 788 canvas.translate(-x, -y); 789 mScreenNail.draw(canvas, x - mDrawWidth / 2, y - mDrawHeight / 2, 790 mDrawWidth, mDrawHeight); 791 canvas.restore(); 792 } 793 } 794 795 // Returns the scrolling progress value for an object moving out of a 796 // view. The progress value measures how much the object has moving out of 797 // the view. The object currently displays in [left, right), and the view is 798 // at [0, viewWidth]. 799 // 800 // The returned value is negative when the object is moving right, and 801 // positive when the object is moving left. The value goes to -1 or 1 when 802 // the object just moves out of the view completely. The value is 0 if the 803 // object currently fills the view. 804 private static float calculateMoveOutProgress(int left, int right, 805 int viewWidth) { 806 // w = object width 807 // viewWidth = view width 808 int w = right - left; 809 810 // If the object width is smaller than the view width, 811 // |....view....| 812 // |<-->| progress = -1 when left = viewWidth 813 // |<-->| progress = 1 when left = -w 814 // So progress = 1 - 2 * (left + w) / (viewWidth + w) 815 if (w < viewWidth) { 816 return 1f - 2f * (left + w) / (viewWidth + w); 817 } 818 819 // If the object width is larger than the view width, 820 // |..view..| 821 // |<--------->| progress = -1 when left = viewWidth 822 // |<--------->| progress = 0 between left = 0 823 // |<--------->| and right = viewWidth 824 // |<--------->| progress = 1 when right = 0 825 if (left > 0) { 826 return -left / (float) viewWidth; 827 } 828 829 if (right < viewWidth) { 830 return (viewWidth - right) / (float) viewWidth; 831 } 832 833 return 0; 834 } 835 836 // Maps a scrolling progress value to the alpha factor in the fading 837 // animation. 838 private float getScrollAlpha(float scrollProgress) { 839 return scrollProgress < 0 ? mAlphaInterpolator.getInterpolation( 840 1 - Math.abs(scrollProgress)) : 1.0f; 841 } 842 843 // Maps a scrolling progress value to the scaling factor in the fading 844 // animation. 845 private float getScrollScale(float scrollProgress) { 846 float interpolatedProgress = mScaleInterpolator.getInterpolation( 847 Math.abs(scrollProgress)); 848 float scale = (1 - interpolatedProgress) + 849 interpolatedProgress * TRANSITION_SCALE_FACTOR; 850 return scale; 851 } 852 853 854 // This interpolator emulates the rate at which the perceived scale of an 855 // object changes as its distance from a camera increases. When this 856 // interpolator is applied to a scale animation on a view, it evokes the 857 // sense that the object is shrinking due to moving away from the camera. 858 private static class ZInterpolator { 859 private float focalLength; 860 861 public ZInterpolator(float foc) { 862 focalLength = foc; 863 } 864 865 public float getInterpolation(float input) { 866 return (1.0f - focalLength / (focalLength + input)) / 867 (1.0f - focalLength / (focalLength + 1.0f)); 868 } 869 } 870 871 public void pause() { 872 mPositionController.skipAnimation(); 873 mTransitionMode = TRANS_NONE; 874 mTileView.freeTextures(); 875 for (ScreenNailEntry entry : mScreenNails) { 876 entry.updateScreenNail(null); 877 } 878 } 879 880 public void resume() { 881 mTileView.prepareTextures(); 882 } 883 884 public void setOpenAnimationRect(Rect rect) { 885 mOpenAnimationRect = rect; 886 } 887 888 public void showVideoPlayIcon(boolean show) { 889 mShowVideoPlayIcon = show; 890 } 891 892 // Returns the opening animation rectangle saved by the previous page. 893 public Rect retrieveOpenAnimationRect() { 894 Rect r = mOpenAnimationRect; 895 mOpenAnimationRect = null; 896 return r; 897 } 898 899 public void openAnimationStarted() { 900 mTransitionMode = TRANS_OPEN_ANIMATION; 901 } 902 903 public boolean isInTransition() { 904 return mTransitionMode != TRANS_NONE; 905 } 906} 907