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