PhotoView.java revision 616a70fdb4473d2fbd7b70772a3a82b908aeae1e
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.Color; 21import android.graphics.Matrix; 22import android.graphics.Point; 23import android.graphics.Rect; 24import android.os.Message; 25import android.view.MotionEvent; 26import android.view.animation.AccelerateInterpolator; 27 28import com.android.gallery3d.R; 29import com.android.gallery3d.app.GalleryActivity; 30import com.android.gallery3d.common.Utils; 31import com.android.gallery3d.data.MediaItem; 32import com.android.gallery3d.data.MediaObject; 33import com.android.gallery3d.util.RangeArray; 34 35public class PhotoView extends GLView { 36 @SuppressWarnings("unused") 37 private static final String TAG = "PhotoView"; 38 private static final int PLACEHOLDER_COLOR = 0xFF222222; 39 40 public static final int INVALID_SIZE = -1; 41 public static final long INVALID_DATA_VERSION = 42 MediaObject.INVALID_DATA_VERSION; 43 44 public static class Size { 45 public int width; 46 public int height; 47 } 48 49 public interface Model extends TileImageView.Model { 50 public int getCurrentIndex(); 51 public void moveTo(int index); 52 53 // Returns the size for the specified picture. If the size information is 54 // not avaiable, width = height = 0. 55 public void getImageSize(int offset, Size size); 56 57 // Returns the media item for the specified picture. 58 public MediaItem getMediaItem(int offset); 59 60 // Returns the rotation for the specified picture. 61 public int getImageRotation(int offset); 62 63 // This amends the getScreenNail() method of TileImageView.Model to get 64 // ScreenNail at previous (negative offset) or next (positive offset) 65 // positions. Returns null if the specified ScreenNail is unavailable. 66 public ScreenNail getScreenNail(int offset); 67 68 // Set this to true if we need the model to provide full images. 69 public void setNeedFullImage(boolean enabled); 70 71 // Returns true if the item is the Camera preview. 72 public boolean isCamera(int offset); 73 74 // Returns true if the item is a Video. 75 public boolean isVideo(int offset); 76 } 77 78 public interface Listener { 79 public void onSingleTapUp(int x, int y); 80 public void lockOrientation(); 81 public void unlockOrientation(); 82 public void onFullScreenChanged(boolean full); 83 public void onActionBarAllowed(boolean allowed); 84 public void onCurrentImageUpdated(); 85 } 86 87 // Here is a graph showing the places we need to lock/unlock device 88 // orientation: 89 // 90 // +------------+ A +------------+ 91 // Page mode | Camera |<---| Photo | 92 // | [locked] |--->| [unlocked] | 93 // +------------+ B +------------+ 94 // ^ ^ 95 // | C | D 96 // +------------+ +------------+ 97 // | Camera | | Photo | 98 // Film mode | [*] | | [*] | 99 // +------------+ +------------+ 100 // 101 // In Page mode, we want to lock in Camera because we don't want the system 102 // rotation animation. We also want to unlock in Photo because we want to 103 // show the system action bar in the right place. 104 // 105 // We don't show action bar in Film mode, so it's fine for it to be locked 106 // or unlocked in Film mode. 107 // 108 // There are four transitions we need to check if we need to 109 // lock/unlock. Marked as A to D above and in the code. 110 111 private static final int MSG_SHOW_LOADING = 1; 112 private static final int MSG_CANCEL_EXTRA_SCALING = 2; 113 private static final int MSG_SWITCH_FOCUS = 3; 114 private static final int MSG_CAPTURE_ANIMATION_DONE = 4; 115 116 private static final long DELAY_SHOW_LOADING = 250; // 250ms; 117 118 private static final int LOADING_INIT = 0; 119 private static final int LOADING_TIMEOUT = 1; 120 private static final int LOADING_COMPLETE = 2; 121 private static final int LOADING_FAIL = 3; 122 123 private static final int MOVE_THRESHOLD = 256; 124 private static final float SWIPE_THRESHOLD = 300f; 125 126 private static final float DEFAULT_TEXT_SIZE = 20; 127 private static float TRANSITION_SCALE_FACTOR = 0.74f; 128 private static final int ICON_RATIO = 6; 129 130 // whether we want to apply card deck effect in page mode. 131 private static final boolean CARD_EFFECT = true; 132 133 // Used to calculate the scaling factor for the fading animation. 134 private ZInterpolator mScaleInterpolator = new ZInterpolator(0.5f); 135 136 // Used to calculate the alpha factor for the fading animation. 137 private AccelerateInterpolator mAlphaInterpolator = 138 new AccelerateInterpolator(0.9f); 139 140 // We keep this many previous ScreenNails. (also this many next ScreenNails) 141 public static final int SCREEN_NAIL_MAX = 3; 142 143 // The picture entries, the valid index is from -SCREEN_NAIL_MAX to 144 // SCREEN_NAIL_MAX. 145 private final RangeArray<Picture> mPictures = 146 new RangeArray<Picture>(-SCREEN_NAIL_MAX, SCREEN_NAIL_MAX); 147 148 private final MyGestureListener mGestureListener; 149 private final GestureRecognizer mGestureRecognizer; 150 private final PositionController mPositionController; 151 152 private Listener mListener; 153 private Model mModel; 154 private StringTexture mLoadingText; 155 private StringTexture mNoThumbnailText; 156 private TileImageView mTileView; 157 private EdgeView mEdgeView; 158 private Texture mVideoPlayIcon; 159 160 private ProgressSpinner mLoadingSpinner; 161 162 private SynchronizedHandler mHandler; 163 164 private int mLoadingState = LOADING_COMPLETE; 165 166 private Point mImageCenter = new Point(); 167 private boolean mCancelExtraScalingPending; 168 private boolean mFilmMode = false; 169 private int mDisplayRotation = 0; 170 private int mCompensation = 0; 171 private boolean mFullScreen = true; 172 private Rect mCameraRelativeFrame = new Rect(); 173 private Rect mCameraRect = new Rect(); 174 175 // [mPrevBound, mNextBound] is the range of index for all pictures in the 176 // model, if we assume the index of current focused picture is 0. So if 177 // there are some previous pictures, mPrevBound < 0, and if there are some 178 // next pictures, mNextBound > 0. 179 private int mPrevBound; 180 private int mNextBound; 181 182 // This variable prevents us doing snapback until its values goes to 0. This 183 // happens if the user gesture is still in progress or we are in a capture 184 // animation. 185 private int mHolding; 186 private static final int HOLD_TOUCH_DOWN = 1; 187 private static final int HOLD_CAPTURE_ANIMATION = 2; 188 189 public PhotoView(GalleryActivity activity) { 190 mTileView = new TileImageView(activity); 191 addComponent(mTileView); 192 Context context = activity.getAndroidContext(); 193 mEdgeView = new EdgeView(context); 194 addComponent(mEdgeView); 195 mLoadingSpinner = new ProgressSpinner(context); 196 mLoadingText = StringTexture.newInstance( 197 context.getString(R.string.loading), 198 DEFAULT_TEXT_SIZE, Color.WHITE); 199 mNoThumbnailText = StringTexture.newInstance( 200 context.getString(R.string.no_thumbnail), 201 DEFAULT_TEXT_SIZE, Color.WHITE); 202 203 mHandler = new MyHandler(activity.getGLRoot()); 204 205 mGestureListener = new MyGestureListener(); 206 mGestureRecognizer = new GestureRecognizer(context, mGestureListener); 207 208 mPositionController = new PositionController(context, 209 new PositionController.Listener() { 210 public void invalidate() { 211 PhotoView.this.invalidate(); 212 } 213 public boolean isHolding() { 214 return mHolding != 0; 215 } 216 public void onPull(int offset, int direction) { 217 mEdgeView.onPull(offset, direction); 218 } 219 public void onRelease() { 220 mEdgeView.onRelease(); 221 } 222 public void onAbsorb(int velocity, int direction) { 223 mEdgeView.onAbsorb(velocity, direction); 224 } 225 }); 226 mVideoPlayIcon = new ResourceTexture(context, R.drawable.ic_control_play); 227 for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) { 228 if (i == 0) { 229 mPictures.put(i, new FullPicture()); 230 } else { 231 mPictures.put(i, new ScreenNailPicture(i)); 232 } 233 } 234 } 235 236 public void setModel(Model model) { 237 mModel = model; 238 mTileView.setModel(mModel); 239 } 240 241 class MyHandler extends SynchronizedHandler { 242 public MyHandler(GLRoot root) { 243 super(root); 244 } 245 246 @Override 247 public void handleMessage(Message message) { 248 switch (message.what) { 249 case MSG_SHOW_LOADING: { 250 if (mLoadingState == LOADING_INIT) { 251 // We don't need the opening animation 252 mPositionController.setOpenAnimationRect(null); 253 254 mLoadingSpinner.startAnimation(); 255 mLoadingState = LOADING_TIMEOUT; 256 invalidate(); 257 } 258 break; 259 } 260 case MSG_CANCEL_EXTRA_SCALING: { 261 mGestureRecognizer.cancelScale(); 262 mPositionController.setExtraScalingRange(false); 263 mCancelExtraScalingPending = false; 264 break; 265 } 266 case MSG_SWITCH_FOCUS: { 267 switchFocus(); 268 break; 269 } 270 case MSG_CAPTURE_ANIMATION_DONE: { 271 // message.arg1 is the offset parameter passed to 272 // switchWithCaptureAnimation(). 273 captureAnimationDone(message.arg1); 274 break; 275 } 276 default: throw new AssertionError(message.what); 277 } 278 } 279 }; 280 281 private void updateLoadingState() { 282 // Possible transitions of mLoadingState: 283 // INIT --> TIMEOUT, COMPLETE, FAIL 284 // TIMEOUT --> COMPLETE, FAIL, INIT 285 // COMPLETE --> INIT 286 // FAIL --> INIT 287 if (mModel.getLevelCount() != 0 || mModel.getScreenNail() != null) { 288 mHandler.removeMessages(MSG_SHOW_LOADING); 289 mLoadingState = LOADING_COMPLETE; 290 } else if (mModel.isFailedToLoad()) { 291 mHandler.removeMessages(MSG_SHOW_LOADING); 292 mLoadingState = LOADING_FAIL; 293 // We don't want the opening animation after loading failure 294 mPositionController.setOpenAnimationRect(null); 295 } else if (mLoadingState != LOADING_INIT) { 296 mLoadingState = LOADING_INIT; 297 mHandler.removeMessages(MSG_SHOW_LOADING); 298 mHandler.sendEmptyMessageDelayed( 299 MSG_SHOW_LOADING, DELAY_SHOW_LOADING); 300 } 301 } 302 303 //////////////////////////////////////////////////////////////////////////// 304 // Data/Image change notifications 305 //////////////////////////////////////////////////////////////////////////// 306 307 public void notifyDataChange(int[] fromIndex, int prevBound, int nextBound) { 308 mPrevBound = prevBound; 309 mNextBound = nextBound; 310 311 // Move the boxes 312 mPositionController.moveBox(fromIndex, mPrevBound < 0, mNextBound > 0, 313 mModel.isCamera(0)); 314 315 // Update the ScreenNails. 316 for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) { 317 mPictures.get(i).reload(); 318 } 319 320 invalidate(); 321 } 322 323 public void notifyImageChange(int index) { 324 if (index == 0) { 325 mListener.onCurrentImageUpdated(); 326 } 327 mPictures.get(index).reload(); 328 invalidate(); 329 } 330 331 @Override 332 protected void onLayout( 333 boolean changeSize, int left, int top, int right, int bottom) { 334 int w = right - left; 335 int h = bottom - top; 336 mTileView.layout(0, 0, w, h); 337 mEdgeView.layout(0, 0, w, h); 338 339 GLRoot root = getGLRoot(); 340 int displayRotation = root.getDisplayRotation(); 341 int compensation = root.getCompensation(); 342 if (mDisplayRotation != displayRotation 343 || mCompensation != compensation) { 344 mDisplayRotation = displayRotation; 345 mCompensation = compensation; 346 347 // We need to change the size and rotation of the Camera ScreenNail, 348 // but we don't want it to animate because the size doen't actually 349 // change in the eye of the user. 350 for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) { 351 Picture p = mPictures.get(i); 352 if (p.isCamera()) { 353 p.updateSize(true); 354 } 355 } 356 } 357 358 updateConstrainedFrame(); 359 if (changeSize) { 360 mPositionController.setViewSize(getWidth(), getHeight()); 361 } 362 } 363 364 // Update the constrained frame due to layout change. 365 private void updateConstrainedFrame() { 366 // Get the width and height in framework orientation because the given 367 // mCameraRelativeFrame is in that coordinates. 368 int w = getWidth(); 369 int h = getHeight(); 370 if (mCompensation % 180 != 0) { 371 int tmp = w; 372 w = h; 373 h = tmp; 374 } 375 int l = mCameraRelativeFrame.left; 376 int t = mCameraRelativeFrame.top; 377 int r = mCameraRelativeFrame.right; 378 int b = mCameraRelativeFrame.bottom; 379 380 // Now convert it to the coordinates we are using. 381 switch (mCompensation) { 382 case 0: mCameraRect.set(l, t, r, b); break; 383 case 90: mCameraRect.set(h - b, l, h - t, r); break; 384 case 180: mCameraRect.set(w - r, h - b, w - l, h - t); break; 385 case 270: mCameraRect.set(t, w - r, b, w - l); break; 386 } 387 388 Log.d(TAG, "compensation = " + mCompensation 389 + ", CameraRelativeFrame = " + mCameraRelativeFrame 390 + ", mCameraRect = " + mCameraRect); 391 mPositionController.setConstrainedFrame(mCameraRect); 392 } 393 394 public void setCameraRelativeFrame(Rect frame) { 395 mCameraRelativeFrame.set(frame); 396 updateConstrainedFrame(); 397 } 398 399 // Returns the rotation we need to do to the camera texture before drawing 400 // it to the canvas, assuming the camera texture is correct when the device 401 // is in its natural orientation. 402 private int getCameraRotation() { 403 return (mCompensation - mDisplayRotation + 360) % 360; 404 } 405 406 //////////////////////////////////////////////////////////////////////////// 407 // Pictures 408 //////////////////////////////////////////////////////////////////////////// 409 410 private interface Picture { 411 void reload(); 412 void draw(GLCanvas canvas, Rect r); 413 void setScreenNail(ScreenNail s); 414 boolean isCamera(); // whether the picture is a camera preview 415 void updateSize(boolean force); // called when mCompensation changes 416 }; 417 418 class FullPicture implements Picture { 419 private int mRotation; 420 private boolean mIsCamera; 421 private boolean mIsVideo; 422 private boolean mWasCameraCenter; 423 424 public void FullPicture(TileImageView tileView) { 425 mTileView = tileView; 426 } 427 428 @Override 429 public void reload() { 430 // mImageWidth and mImageHeight will get updated 431 mTileView.notifyModelInvalidated(); 432 433 mIsCamera = mModel.isCamera(0); 434 mIsVideo = mModel.isVideo(0); 435 setScreenNail(mModel.getScreenNail(0)); 436 updateSize(false); 437 updateLoadingState(); 438 } 439 440 @Override 441 public void updateSize(boolean force) { 442 if (mIsCamera) { 443 mRotation = getCameraRotation(); 444 } else { 445 mRotation = mModel.getImageRotation(0); 446 } 447 448 int w = mTileView.mImageWidth; 449 int h = mTileView.mImageHeight; 450 mPositionController.setImageSize(0, 451 getRotated(mRotation, w, h), 452 getRotated(mRotation, h, w), 453 force); 454 } 455 456 @Override 457 public void draw(GLCanvas canvas, Rect r) { 458 boolean isCenter = mPositionController.isCenter(); 459 460 if (mLoadingState == LOADING_COMPLETE) { 461 drawTileView(canvas, r); 462 } 463 renderMessage(canvas, r.centerX(), r.centerY()); 464 465 if (mIsCamera) { 466 boolean full = !mFilmMode && isCenter 467 && mPositionController.isAtMinimalScale(); 468 if (full != mFullScreen) { 469 mFullScreen = full; 470 mListener.onFullScreenChanged(full); 471 } 472 } 473 474 // We want to have the following transitions: 475 // (1) Move camera preview out of its place: switch to film mode 476 // (2) Move camera preview into its place: switch to page mode 477 // The extra mWasCenter check makes sure (1) does not apply if in 478 // page mode, we move _to_ the camera preview from another picture. 479 480 // Holdings except touch-down prevent the transitions. 481 if ((mHolding & ~HOLD_TOUCH_DOWN) != 0) return; 482 483 boolean isCameraCenter = mIsCamera && isCenter; 484 485 if (mWasCameraCenter && mIsCamera && !isCenter && !mFilmMode) { 486 // Temporary disabled to de-emphasize filmstrip. 487 // setFilmMode(true); 488 } else if (!mWasCameraCenter && isCameraCenter && mFilmMode) { 489 setFilmMode(false); 490 } 491 492 if (isCenter && !mFilmMode) { 493 if (mIsCamera) { 494 // move into camera, lock 495 mListener.lockOrientation(); // Transition A 496 } else { 497 // move out of camera, unlock 498 mListener.unlockOrientation(); // Transition B 499 } 500 } 501 502 mWasCameraCenter = isCameraCenter; 503 } 504 505 @Override 506 public void setScreenNail(ScreenNail s) { 507 mTileView.setScreenNail(s); 508 } 509 510 @Override 511 public boolean isCamera() { 512 return mIsCamera; 513 } 514 515 private void drawTileView(GLCanvas canvas, Rect r) { 516 float imageScale = mPositionController.getImageScale(); 517 int viewW = getWidth(); 518 int viewH = getHeight(); 519 float cx = r.exactCenterX(); 520 float cy = r.exactCenterY(); 521 float scale = 1f; // the scaling factor due to card effect 522 523 canvas.save(GLCanvas.SAVE_FLAG_MATRIX | GLCanvas.SAVE_FLAG_ALPHA); 524 float filmRatio = mPositionController.getFilmRatio(); 525 boolean wantsCardEffect = CARD_EFFECT && !mIsCamera 526 && filmRatio != 1f && !mPictures.get(-1).isCamera() 527 && !mPositionController.inOpeningAnimation(); 528 if (wantsCardEffect) { 529 // Calculate the move-out progress value. 530 int left = r.left; 531 int right = r.right; 532 float progress = calculateMoveOutProgress(left, right, viewW); 533 progress = Utils.clamp(progress, -1f, 1f); 534 535 // We only want to apply the fading animation if the scrolling 536 // movement is to the right. 537 if (progress < 0) { 538 scale = getScrollScale(progress); 539 float alpha = getScrollAlpha(progress); 540 scale = interpolate(filmRatio, scale, 1f); 541 alpha = interpolate(filmRatio, alpha, 1f); 542 543 imageScale *= scale; 544 canvas.multiplyAlpha(alpha); 545 546 float cxPage; // the cx value in page mode 547 if (right - left <= viewW) { 548 // If the picture is narrower than the view, keep it at 549 // the center of the view. 550 cxPage = viewW / 2f; 551 } else { 552 // If the picture is wider than the view (it's 553 // zoomed-in), keep the left edge of the object align 554 // the the left edge of the view. 555 cxPage = (right - left) * scale / 2f; 556 } 557 cx = interpolate(filmRatio, cxPage, cx); 558 } 559 } 560 561 // Draw the tile view. 562 setTileViewPosition(cx, cy, viewW, viewH, imageScale); 563 PhotoView.super.render(canvas); 564 565 // Draw the play video icon. 566 if (mIsVideo) { 567 canvas.translate((int) (cx + 0.5f), (int) (cy + 0.5f)); 568 int s = (int) (scale * Math.min(r.width(), r.height()) + 0.5f); 569 drawVideoPlayIcon(canvas, s); 570 } 571 572 canvas.restore(); 573 } 574 575 // Set the position of the tile view 576 private void setTileViewPosition(float cx, float cy, 577 int viewW, int viewH, float scale) { 578 // Find out the bitmap coordinates of the center of the view 579 int imageW = mPositionController.getImageWidth(); 580 int imageH = mPositionController.getImageHeight(); 581 int centerX = (int) (imageW / 2f + (viewW / 2f - cx) / scale + 0.5f); 582 int centerY = (int) (imageH / 2f + (viewH / 2f - cy) / scale + 0.5f); 583 584 int inverseX = imageW - centerX; 585 int inverseY = imageH - centerY; 586 int x, y; 587 switch (mRotation) { 588 case 0: x = centerX; y = centerY; break; 589 case 90: x = centerY; y = inverseX; break; 590 case 180: x = inverseX; y = inverseY; break; 591 case 270: x = inverseY; y = centerX; break; 592 default: 593 throw new RuntimeException(String.valueOf(mRotation)); 594 } 595 mTileView.setPosition(x, y, scale, mRotation); 596 } 597 598 private void renderMessage(GLCanvas canvas, int x, int y) { 599 // Draw the progress spinner and the text below it 600 // 601 // (x, y) is where we put the center of the spinner. 602 // s is the size of the video play icon, and we use s to layout text 603 // because we want to keep the text at the same place when the video 604 // play icon is shown instead of the spinner. 605 int w = getWidth(); 606 int h = getHeight(); 607 int s = Math.min(w, h) / ICON_RATIO; 608 609 if (mLoadingState == LOADING_TIMEOUT) { 610 StringTexture m = mLoadingText; 611 ProgressSpinner p = mLoadingSpinner; 612 p.draw(canvas, x - p.getWidth() / 2, y - p.getHeight() / 2); 613 m.draw(canvas, x - m.getWidth() / 2, y + s / 2 + 5); 614 invalidate(); // we need to keep the spinner rotating 615 } else if (mLoadingState == LOADING_FAIL) { 616 StringTexture m = mNoThumbnailText; 617 m.draw(canvas, x - m.getWidth() / 2, y + s / 2 + 5); 618 } 619 620 // Draw a debug indicator showing which picture has focus (index == 621 // 0). 622 // canvas.fillRect(x - 10, y - 10, 20, 20, 0x80FF00FF); 623 } 624 } 625 626 private class ScreenNailPicture implements Picture { 627 private int mIndex; 628 private int mRotation; 629 private ScreenNail mScreenNail; 630 private Size mSize = new Size(); 631 private boolean mIsCamera; 632 private boolean mIsVideo; 633 634 public ScreenNailPicture(int index) { 635 mIndex = index; 636 } 637 638 @Override 639 public void reload() { 640 mIsCamera = mModel.isCamera(mIndex); 641 mIsVideo = mModel.isVideo(mIndex); 642 setScreenNail(mModel.getScreenNail(mIndex)); 643 } 644 645 @Override 646 public void draw(GLCanvas canvas, Rect r) { 647 if (mScreenNail == null) { 648 // Draw a placeholder rectange if there will be a picture in 649 // this position. 650 if (mIndex >= mPrevBound && mIndex <= mNextBound) { 651 canvas.fillRect(r.left, r.top, r.width(), r.height(), 652 PLACEHOLDER_COLOR); 653 } 654 return; 655 } 656 if (r.left >= getWidth() || r.right <= 0 || 657 r.top >= getHeight() || r.bottom <= 0) { 658 mScreenNail.noDraw(); 659 return; 660 } 661 662 if (mIsCamera && mFullScreen != false) { 663 mFullScreen = false; 664 mListener.onFullScreenChanged(false); 665 } 666 667 float filmRatio = mPositionController.getFilmRatio(); 668 boolean wantsCardEffect = CARD_EFFECT && mIndex > 0 669 && filmRatio != 1f && !mPictures.get(0).isCamera(); 670 int w = getWidth(); 671 int cx = wantsCardEffect 672 ? (int) (interpolate(filmRatio, w / 2, r.centerX()) + 0.5f) 673 : r.centerX(); 674 int cy = r.centerY(); 675 canvas.save(GLCanvas.SAVE_FLAG_MATRIX | GLCanvas.SAVE_FLAG_ALPHA); 676 canvas.translate(cx, cy); 677 if (wantsCardEffect) { 678 float progress = (float) (w / 2 - r.centerX()) / w; 679 progress = Utils.clamp(progress, -1, 1); 680 float alpha = getScrollAlpha(progress); 681 float scale = getScrollScale(progress); 682 alpha = interpolate(filmRatio, alpha, 1f); 683 scale = interpolate(filmRatio, scale, 1f); 684 canvas.multiplyAlpha(alpha); 685 canvas.scale(scale, scale, 1); 686 } 687 if (mRotation != 0) { 688 canvas.rotate(mRotation, 0, 0, 1); 689 } 690 int drawW = getRotated(mRotation, r.width(), r.height()); 691 int drawH = getRotated(mRotation, r.height(), r.width()); 692 mScreenNail.draw(canvas, -drawW / 2, -drawH / 2, drawW, drawH); 693 if (mIsVideo) drawVideoPlayIcon(canvas, Math.min(drawW, drawH)); 694 canvas.restore(); 695 } 696 697 @Override 698 public void setScreenNail(ScreenNail s) { 699 if (mScreenNail == s) return; 700 mScreenNail = s; 701 updateSize(false); 702 } 703 704 @Override 705 public void updateSize(boolean force) { 706 if (mIsCamera) { 707 mRotation = getCameraRotation(); 708 } else { 709 mRotation = mModel.getImageRotation(mIndex); 710 } 711 712 int w = 0, h = 0; 713 if (mScreenNail != null) { 714 w = mScreenNail.getWidth(); 715 h = mScreenNail.getHeight(); 716 } else if (mModel != null) { 717 // If we don't have ScreenNail available, we can still try to 718 // get the size information of it. 719 mModel.getImageSize(mIndex, mSize); 720 w = mSize.width; 721 h = mSize.height; 722 } 723 724 if (w != 0 && h != 0) { 725 mPositionController.setImageSize(mIndex, 726 getRotated(mRotation, w, h), 727 getRotated(mRotation, h, w), 728 force); 729 } 730 } 731 732 @Override 733 public boolean isCamera() { 734 return mIsCamera; 735 } 736 } 737 738 // Draw the video play icon (in the place where the spinner was) 739 private void drawVideoPlayIcon(GLCanvas canvas, int side) { 740 int s = side / ICON_RATIO; 741 // Draw the video play icon at the center 742 mVideoPlayIcon.draw(canvas, -s / 2, -s / 2, s, s); 743 } 744 745 private static int getRotated(int degree, int original, int theother) { 746 return (degree % 180 == 0) ? original : theother; 747 } 748 749 //////////////////////////////////////////////////////////////////////////// 750 // Gestures Handling 751 //////////////////////////////////////////////////////////////////////////// 752 753 @Override 754 protected boolean onTouch(MotionEvent event) { 755 mGestureRecognizer.onTouchEvent(event); 756 return true; 757 } 758 759 private class MyGestureListener implements GestureRecognizer.Listener { 760 private boolean mIgnoreUpEvent = false; 761 // If we can change mode for this scale gesture. 762 private boolean mCanChangeMode; 763 // If we have changed the film mode in this scaling gesture. 764 private boolean mModeChanged; 765 // If this scaling gesture should be ignored. 766 private boolean mIgnoreScalingGesture; 767 // whether the down action happened while the view is scrolling. 768 private boolean mDownInScrolling; 769 // If we should ignore all gestures other than onSingleTapUp. 770 private boolean mIgnoreSwipingGesture; 771 772 @Override 773 public boolean onSingleTapUp(float x, float y) { 774 // We do this in addition to onUp() because we want the snapback of 775 // setFilmMode to happen. 776 mHolding &= ~HOLD_TOUCH_DOWN; 777 778 if (mFilmMode && !mDownInScrolling) { 779 switchToHitPicture((int) (x + 0.5f), (int) (y + 0.5f)); 780 setFilmMode(false); 781 mIgnoreUpEvent = true; 782 return true; 783 } 784 785 if (mListener != null) { 786 // Do the inverse transform of the touch coordinates. 787 Matrix m = getGLRoot().getCompensationMatrix(); 788 Matrix inv = new Matrix(); 789 m.invert(inv); 790 float[] pts = new float[] {x, y}; 791 inv.mapPoints(pts); 792 mListener.onSingleTapUp((int) (pts[0] + 0.5f), (int) (pts[1] + 0.5f)); 793 } 794 return true; 795 } 796 797 @Override 798 public boolean onDoubleTap(float x, float y) { 799 if (mIgnoreSwipingGesture) return true; 800 if (mPictures.get(0).isCamera()) return false; 801 PositionController controller = mPositionController; 802 float scale = controller.getImageScale(); 803 // onDoubleTap happened on the second ACTION_DOWN. 804 // We need to ignore the next UP event. 805 mIgnoreUpEvent = true; 806 if (scale <= 1.0f || controller.isAtMinimalScale()) { 807 controller.zoomIn(x, y, Math.max(1.5f, scale * 1.5f)); 808 } else { 809 controller.resetToFullView(); 810 } 811 return true; 812 } 813 814 @Override 815 public boolean onScroll(float dx, float dy) { 816 if (mIgnoreSwipingGesture) return true; 817 mPositionController.startScroll(-dx, -dy); 818 return true; 819 } 820 821 @Override 822 public boolean onFling(float velocityX, float velocityY) { 823 if (mIgnoreSwipingGesture) return true; 824 if (swipeImages(velocityX, velocityY)) { 825 mIgnoreUpEvent = true; 826 } else if (mPositionController.fling(velocityX, velocityY)) { 827 mIgnoreUpEvent = true; 828 } 829 return true; 830 } 831 832 @Override 833 public boolean onScaleBegin(float focusX, float focusY) { 834 if (mIgnoreSwipingGesture) return true; 835 // We ignore the scaling gesture if it is a camera preview. 836 mIgnoreScalingGesture = mPictures.get(0).isCamera(); 837 if (mIgnoreScalingGesture) { 838 return true; 839 } 840 mPositionController.beginScale(focusX, focusY); 841 // We can change mode if we are in film mode, or we are in page 842 // mode and at minimal scale. 843 mCanChangeMode = mFilmMode 844 || mPositionController.isAtMinimalScale(); 845 mModeChanged = false; 846 return true; 847 } 848 849 @Override 850 public boolean onScale(float focusX, float focusY, float scale) { 851 if (mIgnoreSwipingGesture) return true; 852 if (mIgnoreScalingGesture) return true; 853 if (mModeChanged) return true; 854 if (Float.isNaN(scale) || Float.isInfinite(scale)) return false; 855 856 // We wait for the scale change accumulated to a large enough change 857 // before reacting to it. Otherwise we may mistakenly treat a 858 // zoom-in gesture as zoom-out or vice versa. 859 if (scale > 0.99f && scale < 1.01f) return false; 860 861 int outOfRange = mPositionController.scaleBy(scale, focusX, focusY); 862 863 // If mode changes, we treat this scaling gesture has ended. 864 if (mCanChangeMode) { 865 if ((outOfRange < 0 && !mFilmMode) || 866 (outOfRange > 0 && mFilmMode)) { 867 stopExtraScalingIfNeeded(); 868 869 // Removing the touch down flag allows snapback to happen 870 // for film mode change. 871 mHolding &= ~HOLD_TOUCH_DOWN; 872 setFilmMode(!mFilmMode); 873 874 // We need to call onScaleEnd() before setting mModeChanged 875 // to true. 876 onScaleEnd(); 877 mModeChanged = true; 878 return true; 879 } 880 } 881 882 if (outOfRange != 0) { 883 startExtraScalingIfNeeded(); 884 } else { 885 stopExtraScalingIfNeeded(); 886 } 887 return true; 888 } 889 890 @Override 891 public void onScaleEnd() { 892 if (mIgnoreSwipingGesture) return; 893 if (mIgnoreScalingGesture) return; 894 if (mModeChanged) return; 895 mPositionController.endScale(); 896 } 897 898 private void startExtraScalingIfNeeded() { 899 if (!mCancelExtraScalingPending) { 900 mHandler.sendEmptyMessageDelayed( 901 MSG_CANCEL_EXTRA_SCALING, 700); 902 mPositionController.setExtraScalingRange(true); 903 mCancelExtraScalingPending = true; 904 } 905 } 906 907 private void stopExtraScalingIfNeeded() { 908 if (mCancelExtraScalingPending) { 909 mHandler.removeMessages(MSG_CANCEL_EXTRA_SCALING); 910 mPositionController.setExtraScalingRange(false); 911 mCancelExtraScalingPending = false; 912 } 913 } 914 915 @Override 916 public void onDown() { 917 if (mIgnoreSwipingGesture) return; 918 919 mHolding |= HOLD_TOUCH_DOWN; 920 921 if (mFilmMode && mPositionController.isScrolling()) { 922 mDownInScrolling = true; 923 mPositionController.stopScrolling(); 924 } else { 925 mDownInScrolling = false; 926 } 927 } 928 929 @Override 930 public void onUp() { 931 if (mIgnoreSwipingGesture) return; 932 933 mHolding &= ~HOLD_TOUCH_DOWN; 934 mEdgeView.onRelease(); 935 936 if (mIgnoreUpEvent) { 937 mIgnoreUpEvent = false; 938 return; 939 } 940 941 snapback(); 942 } 943 944 public void setSwipingEnabled(boolean enabled) { 945 mIgnoreSwipingGesture = !enabled; 946 } 947 } 948 949 public void setSwipingEnabled(boolean enabled) { 950 mGestureListener.setSwipingEnabled(enabled); 951 } 952 953 private void setFilmMode(boolean enabled) { 954 if (mFilmMode == enabled) return; 955 mFilmMode = enabled; 956 mPositionController.setFilmMode(mFilmMode); 957 mModel.setNeedFullImage(!enabled); 958 mListener.onActionBarAllowed(!enabled); 959 960 // If we leave filmstrip mode, we should lock/unlock 961 if (!enabled) { 962 if (mPictures.get(0).isCamera()) { 963 mListener.lockOrientation(); // Transition C 964 } else { 965 mListener.unlockOrientation(); // Transition D 966 } 967 } 968 } 969 970 public boolean getFilmMode() { 971 return mFilmMode; 972 } 973 974 //////////////////////////////////////////////////////////////////////////// 975 // Framework events 976 //////////////////////////////////////////////////////////////////////////// 977 978 public void pause() { 979 mPositionController.skipAnimation(); 980 mTileView.freeTextures(); 981 for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) { 982 mPictures.get(i).setScreenNail(null); 983 } 984 } 985 986 public void resume() { 987 mTileView.prepareTextures(); 988 } 989 990 // move to the camera preview and show controls after resume 991 public void resetToFirstPicture() { 992 mModel.moveTo(0); 993 setFilmMode(false); 994 } 995 996 //////////////////////////////////////////////////////////////////////////// 997 // Rendering 998 //////////////////////////////////////////////////////////////////////////// 999 1000 @Override 1001 protected void render(GLCanvas canvas) { 1002 float filmRatio = mPositionController.getFilmRatio(); 1003 1004 // Draw next photos. In page mode, we draw only one next photo. 1005 int lastPhoto = (filmRatio == 0f) ? 1 : SCREEN_NAIL_MAX; 1006 for (int i = lastPhoto; i > 0; i--) { 1007 Rect r = mPositionController.getPosition(i); 1008 mPictures.get(i).draw(canvas, r); 1009 } 1010 1011 // Draw current photo 1012 mPictures.get(0).draw(canvas, mPositionController.getPosition(0)); 1013 1014 // Draw previous photos. In page mode, we draw only one previous photo. 1015 lastPhoto = (filmRatio == 0f) ? -1: -SCREEN_NAIL_MAX; 1016 for (int i = -1; i >= lastPhoto; i--) { 1017 Rect r = mPositionController.getPosition(i); 1018 mPictures.get(i).draw(canvas, r); 1019 } 1020 1021 mPositionController.advanceAnimation(); 1022 checkFocusSwitching(); 1023 } 1024 1025 //////////////////////////////////////////////////////////////////////////// 1026 // Film mode focus switching 1027 //////////////////////////////////////////////////////////////////////////// 1028 1029 // Runs in GL thread. 1030 private void checkFocusSwitching() { 1031 if (!mFilmMode) return; 1032 if (mHandler.hasMessages(MSG_SWITCH_FOCUS)) return; 1033 if (switchPosition() != 0) { 1034 mHandler.sendEmptyMessage(MSG_SWITCH_FOCUS); 1035 } 1036 } 1037 1038 // Runs in main thread. 1039 private void switchFocus() { 1040 if (mHolding != 0) return; 1041 switch (switchPosition()) { 1042 case -1: 1043 switchToPrevImage(); 1044 break; 1045 case 1: 1046 switchToNextImage(); 1047 break; 1048 } 1049 } 1050 1051 // Returns -1 if we should switch focus to the previous picture, +1 if we 1052 // should switch to the next, 0 otherwise. 1053 private int switchPosition() { 1054 Rect curr = mPositionController.getPosition(0); 1055 int center = getWidth() / 2; 1056 1057 if (curr.left > center && mPrevBound < 0) { 1058 Rect prev = mPositionController.getPosition(-1); 1059 int currDist = curr.left - center; 1060 int prevDist = center - prev.right; 1061 if (prevDist < currDist) { 1062 return -1; 1063 } 1064 } else if (curr.right < center && mNextBound > 0) { 1065 Rect next = mPositionController.getPosition(1); 1066 int currDist = center - curr.right; 1067 int nextDist = next.left - center; 1068 if (nextDist < currDist) { 1069 return 1; 1070 } 1071 } 1072 1073 return 0; 1074 } 1075 1076 // Switch to the previous or next picture if the hit position is inside 1077 // one of their boxes. This runs in main thread. 1078 private void switchToHitPicture(int x, int y) { 1079 if (mPrevBound < 0) { 1080 Rect r = mPositionController.getPosition(-1); 1081 if (r.right >= x) { 1082 slideToPrevPicture(); 1083 return; 1084 } 1085 } 1086 1087 if (mNextBound > 0) { 1088 Rect r = mPositionController.getPosition(1); 1089 if (r.left <= x) { 1090 slideToNextPicture(); 1091 return; 1092 } 1093 } 1094 } 1095 1096 //////////////////////////////////////////////////////////////////////////// 1097 // Page mode focus switching 1098 // 1099 // We slide image to the next one or the previous one in two cases: 1: If 1100 // the user did a fling gesture with enough velocity. 2 If the user has 1101 // moved the picture a lot. 1102 //////////////////////////////////////////////////////////////////////////// 1103 1104 private boolean swipeImages(float velocityX, float velocityY) { 1105 if (mFilmMode) return false; 1106 1107 // Avoid swiping images if we're possibly flinging to view the 1108 // zoomed in picture vertically. 1109 PositionController controller = mPositionController; 1110 boolean isMinimal = controller.isAtMinimalScale(); 1111 int edges = controller.getImageAtEdges(); 1112 if (!isMinimal && Math.abs(velocityY) > Math.abs(velocityX)) 1113 if ((edges & PositionController.IMAGE_AT_TOP_EDGE) == 0 1114 || (edges & PositionController.IMAGE_AT_BOTTOM_EDGE) == 0) 1115 return false; 1116 1117 // If we are at the edge of the current photo and the sweeping velocity 1118 // exceeds the threshold, slide to the next / previous image. 1119 if (velocityX < -SWIPE_THRESHOLD && (isMinimal 1120 || (edges & PositionController.IMAGE_AT_RIGHT_EDGE) != 0)) { 1121 return slideToNextPicture(); 1122 } else if (velocityX > SWIPE_THRESHOLD && (isMinimal 1123 || (edges & PositionController.IMAGE_AT_LEFT_EDGE) != 0)) { 1124 return slideToPrevPicture(); 1125 } 1126 1127 return false; 1128 } 1129 1130 private void snapback() { 1131 if (mHolding != 0) return; 1132 if (!snapToNeighborImage()) { 1133 mPositionController.snapback(); 1134 } 1135 } 1136 1137 private boolean snapToNeighborImage() { 1138 if (mFilmMode) return false; 1139 1140 Rect r = mPositionController.getPosition(0); 1141 int viewW = getWidth(); 1142 int threshold = MOVE_THRESHOLD + gapToSide(r.width(), viewW); 1143 1144 // If we have moved the picture a lot, switching. 1145 if (viewW - r.right > threshold) { 1146 return slideToNextPicture(); 1147 } else if (r.left > threshold) { 1148 return slideToPrevPicture(); 1149 } 1150 1151 return false; 1152 } 1153 1154 private boolean slideToNextPicture() { 1155 if (mNextBound <= 0) return false; 1156 switchToNextImage(); 1157 mPositionController.startHorizontalSlide(); 1158 return true; 1159 } 1160 1161 private boolean slideToPrevPicture() { 1162 if (mPrevBound >= 0) return false; 1163 switchToPrevImage(); 1164 mPositionController.startHorizontalSlide(); 1165 return true; 1166 } 1167 1168 private static int gapToSide(int imageWidth, int viewWidth) { 1169 return Math.max(0, (viewWidth - imageWidth) / 2); 1170 } 1171 1172 //////////////////////////////////////////////////////////////////////////// 1173 // Focus switching 1174 //////////////////////////////////////////////////////////////////////////// 1175 1176 private void switchToNextImage() { 1177 mModel.moveTo(mModel.getCurrentIndex() + 1); 1178 } 1179 1180 private void switchToPrevImage() { 1181 mModel.moveTo(mModel.getCurrentIndex() - 1); 1182 } 1183 1184 private void switchToFirstImage() { 1185 mModel.moveTo(0); 1186 } 1187 1188 //////////////////////////////////////////////////////////////////////////// 1189 // Opening Animation 1190 //////////////////////////////////////////////////////////////////////////// 1191 1192 public void setOpenAnimationRect(Rect rect) { 1193 mPositionController.setOpenAnimationRect(rect); 1194 } 1195 1196 //////////////////////////////////////////////////////////////////////////// 1197 // Capture Animation 1198 //////////////////////////////////////////////////////////////////////////// 1199 1200 public boolean switchWithCaptureAnimation(int offset) { 1201 GLRoot root = getGLRoot(); 1202 root.lockRenderThread(); 1203 try { 1204 return switchWithCaptureAnimationLocked(offset); 1205 } finally { 1206 root.unlockRenderThread(); 1207 } 1208 } 1209 1210 private boolean switchWithCaptureAnimationLocked(int offset) { 1211 if (mHolding != 0) return true; 1212 if (offset == 1) { 1213 if (mNextBound <= 0) return false; 1214 // Temporary disable action bar until the capture animation is done. 1215 if (!mFilmMode) mListener.onActionBarAllowed(false); 1216 switchToNextImage(); 1217 mPositionController.startCaptureAnimationSlide(-1); 1218 } else if (offset == -1) { 1219 if (mPrevBound >= 0) return false; 1220 switchToFirstImage(); 1221 mPositionController.startCaptureAnimationSlide(1); 1222 } else { 1223 return false; 1224 } 1225 mHolding |= HOLD_CAPTURE_ANIMATION; 1226 Message m = mHandler.obtainMessage(MSG_CAPTURE_ANIMATION_DONE, offset, 0); 1227 mHandler.sendMessageDelayed(m, 800); 1228 return true; 1229 } 1230 1231 private void captureAnimationDone(int offset) { 1232 mHolding &= ~HOLD_CAPTURE_ANIMATION; 1233 if (offset == 1) { 1234 // move out of camera, unlock 1235 if (!mFilmMode) { 1236 // Now the capture animation is done, enable the action bar. 1237 mListener.onActionBarAllowed(true); 1238 } 1239 } 1240 snapback(); 1241 } 1242 1243 //////////////////////////////////////////////////////////////////////////// 1244 // Card deck effect calculation 1245 //////////////////////////////////////////////////////////////////////////// 1246 1247 // Returns the scrolling progress value for an object moving out of a 1248 // view. The progress value measures how much the object has moving out of 1249 // the view. The object currently displays in [left, right), and the view is 1250 // at [0, viewWidth]. 1251 // 1252 // The returned value is negative when the object is moving right, and 1253 // positive when the object is moving left. The value goes to -1 or 1 when 1254 // the object just moves out of the view completely. The value is 0 if the 1255 // object currently fills the view. 1256 private static float calculateMoveOutProgress(int left, int right, 1257 int viewWidth) { 1258 // w = object width 1259 // viewWidth = view width 1260 int w = right - left; 1261 1262 // If the object width is smaller than the view width, 1263 // |....view....| 1264 // |<-->| progress = -1 when left = viewWidth 1265 // |<-->| progress = 0 when left = viewWidth / 2 - w / 2 1266 // |<-->| progress = 1 when left = -w 1267 if (w < viewWidth) { 1268 int zx = viewWidth / 2 - w / 2; 1269 if (left > zx) { 1270 return -(left - zx) / (float) (viewWidth - zx); // progress = (0, -1] 1271 } else { 1272 return (left - zx) / (float) (-w - zx); // progress = [0, 1] 1273 } 1274 } 1275 1276 // If the object width is larger than the view width, 1277 // |..view..| 1278 // |<--------->| progress = -1 when left = viewWidth 1279 // |<--------->| progress = 0 between left = 0 1280 // |<--------->| and right = viewWidth 1281 // |<--------->| progress = 1 when right = 0 1282 if (left > 0) { 1283 return -left / (float) viewWidth; 1284 } 1285 1286 if (right < viewWidth) { 1287 return (viewWidth - right) / (float) viewWidth; 1288 } 1289 1290 return 0; 1291 } 1292 1293 // Maps a scrolling progress value to the alpha factor in the fading 1294 // animation. 1295 private float getScrollAlpha(float scrollProgress) { 1296 return scrollProgress < 0 ? mAlphaInterpolator.getInterpolation( 1297 1 - Math.abs(scrollProgress)) : 1.0f; 1298 } 1299 1300 // Maps a scrolling progress value to the scaling factor in the fading 1301 // animation. 1302 private float getScrollScale(float scrollProgress) { 1303 float interpolatedProgress = mScaleInterpolator.getInterpolation( 1304 Math.abs(scrollProgress)); 1305 float scale = (1 - interpolatedProgress) + 1306 interpolatedProgress * TRANSITION_SCALE_FACTOR; 1307 return scale; 1308 } 1309 1310 1311 // This interpolator emulates the rate at which the perceived scale of an 1312 // object changes as its distance from a camera increases. When this 1313 // interpolator is applied to a scale animation on a view, it evokes the 1314 // sense that the object is shrinking due to moving away from the camera. 1315 private static class ZInterpolator { 1316 private float focalLength; 1317 1318 public ZInterpolator(float foc) { 1319 focalLength = foc; 1320 } 1321 1322 public float getInterpolation(float input) { 1323 return (1.0f - focalLength / (focalLength + input)) / 1324 (1.0f - focalLength / (focalLength + 1.0f)); 1325 } 1326 } 1327 1328 // Returns an interpolated value for the page/film transition. 1329 // When ratio = 0, the result is from. 1330 // When ratio = 1, the result is to. 1331 private static float interpolate(float ratio, float from, float to) { 1332 return from + (to - from) * ratio * ratio; 1333 } 1334 1335 //////////////////////////////////////////////////////////////////////////// 1336 // Simple public utilities 1337 //////////////////////////////////////////////////////////////////////////// 1338 1339 public void setListener(Listener listener) { 1340 mListener = listener; 1341 } 1342 1343 public Rect getPhotoRect(int index) { 1344 return mPositionController.getPosition(index); 1345 } 1346 1347 1348 public PhotoFallbackEffect buildFallbackEffect(GLView root, GLCanvas canvas) { 1349 Rect location = new Rect(); 1350 Utils.assertTrue(root.getBoundsOf(this, location)); 1351 1352 Rect fullRect = bounds(); 1353 PhotoFallbackEffect effect = new PhotoFallbackEffect(); 1354 for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) { 1355 MediaItem item = mModel.getMediaItem(i); 1356 if (item == null) continue; 1357 ScreenNail sc = mModel.getScreenNail(i); 1358 if (sc == null) continue; 1359 Rect rect = new Rect(getPhotoRect(i)); 1360 if (!Rect.intersects(fullRect, rect)) continue; 1361 rect.offset(location.left, location.top); 1362 1363 RawTexture texture = new RawTexture(sc.getWidth(), sc.getHeight(), true); 1364 canvas.beginRenderTarget(texture); 1365 sc.draw(canvas, 0, 0, sc.getWidth(), sc.getHeight()); 1366 canvas.endRenderTarget(); 1367 effect.addEntry(item.getPath(), rect, texture); 1368 } 1369 return effect; 1370 } 1371} 1372