PhotoView.java revision 517e1bd25305d4e82d101a8c06be0119dde2eab3
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.util.FloatMath; 26import android.view.MotionEvent; 27import android.view.View.MeasureSpec; 28import android.view.animation.AccelerateInterpolator; 29 30import com.android.gallery3d.R; 31import com.android.gallery3d.app.GalleryActivity; 32import com.android.gallery3d.common.Utils; 33import com.android.gallery3d.data.MediaItem; 34import com.android.gallery3d.data.MediaObject; 35import com.android.gallery3d.data.Path; 36import com.android.gallery3d.util.GalleryUtils; 37import com.android.gallery3d.util.RangeArray; 38 39public class PhotoView extends GLView { 40 @SuppressWarnings("unused") 41 private static final String TAG = "PhotoView"; 42 private static final int PLACEHOLDER_COLOR = 0xFF222222; 43 44 public static final int INVALID_SIZE = -1; 45 public static final long INVALID_DATA_VERSION = 46 MediaObject.INVALID_DATA_VERSION; 47 48 public static class Size { 49 public int width; 50 public int height; 51 } 52 53 public interface Model extends TileImageView.Model { 54 public int getCurrentIndex(); 55 public void moveTo(int index); 56 57 // Returns the size for the specified picture. If the size information is 58 // not avaiable, width = height = 0. 59 public void getImageSize(int offset, Size size); 60 61 // Returns the media item for the specified picture. 62 public MediaItem getMediaItem(int offset); 63 64 // Returns the rotation for the specified picture. 65 public int getImageRotation(int offset); 66 67 // This amends the getScreenNail() method of TileImageView.Model to get 68 // ScreenNail at previous (negative offset) or next (positive offset) 69 // positions. Returns null if the specified ScreenNail is unavailable. 70 public ScreenNail getScreenNail(int offset); 71 72 // Set this to true if we need the model to provide full images. 73 public void setNeedFullImage(boolean enabled); 74 75 // Returns true if the item is the Camera preview. 76 public boolean isCamera(int offset); 77 78 // Returns true if the item is the Panorama. 79 public boolean isPanorama(int offset); 80 81 // Returns true if the item is a Video. 82 public boolean isVideo(int offset); 83 84 // Returns true if the item can be deleted. 85 public boolean isDeletable(int offset); 86 87 public static final int LOADING_INIT = 0; 88 public static final int LOADING_COMPLETE = 1; 89 public static final int LOADING_FAIL = 2; 90 91 public int getLoadingState(int offset); 92 93 // When data change happens, we need to decide which MediaItem to focus 94 // on. 95 // 96 // 1. If focus hint path != null, we try to focus on it if we can find 97 // it. This is used for undo a deletion, so we can focus on the 98 // undeleted item. 99 // 100 // 2. Otherwise try to focus on the MediaItem that is currently focused, 101 // if we can find it. 102 // 103 // 3. Otherwise try to focus on the previous MediaItem or the next 104 // MediaItem, depending on the value of focus hint direction. 105 public static final int FOCUS_HINT_NEXT = 0; 106 public static final int FOCUS_HINT_PREVIOUS = 1; 107 public void setFocusHintDirection(int direction); 108 public void setFocusHintPath(Path path); 109 } 110 111 public interface Listener { 112 public void onSingleTapUp(int x, int y); 113 public void lockOrientation(); 114 public void unlockOrientation(); 115 public void onFullScreenChanged(boolean full); 116 public void onActionBarAllowed(boolean allowed); 117 public void onActionBarWanted(); 118 public void onCurrentImageUpdated(); 119 public void onDeleteImage(Path path, int offset); 120 public void onUndoDeleteImage(); 121 public void onCommitDeleteImage(); 122 } 123 124 // The rules about orientation locking: 125 // 126 // (1) We need to lock the orientation if we are in page mode camera 127 // preview, so there is no (unwanted) rotation animation when the user 128 // rotates the device. 129 // 130 // (2) We need to unlock the orientation if we want to show the action bar 131 // because the action bar follows the system orientation. 132 // 133 // The rules about action bar: 134 // 135 // (1) If we are in film mode, we don't show action bar. 136 // 137 // (2) If we go from camera to gallery with capture animation, we show 138 // action bar. 139 private static final int MSG_CANCEL_EXTRA_SCALING = 2; 140 private static final int MSG_SWITCH_FOCUS = 3; 141 private static final int MSG_CAPTURE_ANIMATION_DONE = 4; 142 private static final int MSG_DELETE_ANIMATION_DONE = 5; 143 private static final int MSG_DELETE_DONE = 6; 144 private static final int MSG_HIDE_UNDO_BAR = 7; 145 146 private static final int MOVE_THRESHOLD = 256; 147 private static final float SWIPE_THRESHOLD = 300f; 148 149 private static final float DEFAULT_TEXT_SIZE = 20; 150 private static float TRANSITION_SCALE_FACTOR = 0.74f; 151 private static final int ICON_RATIO = 6; 152 153 // whether we want to apply card deck effect in page mode. 154 private static final boolean CARD_EFFECT = true; 155 156 // whether we want to apply offset effect in film mode. 157 private static final boolean OFFSET_EFFECT = true; 158 159 // Used to calculate the scaling factor for the card deck effect. 160 private ZInterpolator mScaleInterpolator = new ZInterpolator(0.5f); 161 162 // Used to calculate the alpha factor for the fading animation. 163 private AccelerateInterpolator mAlphaInterpolator = 164 new AccelerateInterpolator(0.9f); 165 166 // We keep this many previous ScreenNails. (also this many next ScreenNails) 167 public static final int SCREEN_NAIL_MAX = 3; 168 169 // These are constants for the delete gesture. 170 private static final int SWIPE_ESCAPE_VELOCITY = 500; // dp/sec 171 private static final int MAX_DISMISS_VELOCITY = 2000; // dp/sec 172 173 // The picture entries, the valid index is from -SCREEN_NAIL_MAX to 174 // SCREEN_NAIL_MAX. 175 private final RangeArray<Picture> mPictures = 176 new RangeArray<Picture>(-SCREEN_NAIL_MAX, SCREEN_NAIL_MAX); 177 private Size[] mSizes = new Size[2 * SCREEN_NAIL_MAX + 1]; 178 179 private final MyGestureListener mGestureListener; 180 private final GestureRecognizer mGestureRecognizer; 181 private final PositionController mPositionController; 182 183 private Listener mListener; 184 private Model mModel; 185 private StringTexture mLoadingText; 186 private StringTexture mNoThumbnailText; 187 private TileImageView mTileView; 188 private EdgeView mEdgeView; 189 private UndoBarView mUndoBar; 190 private Texture mVideoPlayIcon; 191 192 private SynchronizedHandler mHandler; 193 194 private Point mImageCenter = new Point(); 195 private boolean mCancelExtraScalingPending; 196 private boolean mFilmMode = false; 197 private int mDisplayRotation = 0; 198 private int mCompensation = 0; 199 private boolean mFullScreenCamera; 200 private Rect mCameraRelativeFrame = new Rect(); 201 private Rect mCameraRect = new Rect(); 202 203 // [mPrevBound, mNextBound] is the range of index for all pictures in the 204 // model, if we assume the index of current focused picture is 0. So if 205 // there are some previous pictures, mPrevBound < 0, and if there are some 206 // next pictures, mNextBound > 0. 207 private int mPrevBound; 208 private int mNextBound; 209 210 // This variable prevents us doing snapback until its values goes to 0. This 211 // happens if the user gesture is still in progress or we are in a capture 212 // animation. 213 private int mHolding; 214 private static final int HOLD_TOUCH_DOWN = 1; 215 private static final int HOLD_CAPTURE_ANIMATION = 2; 216 private static final int HOLD_DELETE = 4; 217 218 // mTouchBoxIndex is the index of the box that is touched by the down 219 // gesture in film mode. The value Integer.MAX_VALUE means no box was 220 // touched. 221 private int mTouchBoxIndex = Integer.MAX_VALUE; 222 // Whether the box indicated by mTouchBoxIndex is deletable. Only meaningful 223 // if mTouchBoxIndex is not Integer.MAX_VALUE. 224 private boolean mTouchBoxDeletable; 225 // This is the index of the last deleted item. This is only used as a hint 226 // to hide the undo button when we are too far away from the deleted 227 // item. The value Integer.MAX_VALUE means there is no such hint. 228 private int mUndoIndexHint = Integer.MAX_VALUE; 229 230 public PhotoView(GalleryActivity activity) { 231 mTileView = new TileImageView(activity); 232 addComponent(mTileView); 233 Context context = activity.getAndroidContext(); 234 mEdgeView = new EdgeView(context); 235 addComponent(mEdgeView); 236 mUndoBar = new UndoBarView(context); 237 addComponent(mUndoBar); 238 mUndoBar.setVisibility(GLView.INVISIBLE); 239 mUndoBar.setOnClickListener(new OnClickListener() { 240 @Override 241 public void onClick(GLView v) { 242 mListener.onUndoDeleteImage(); 243 } 244 }); 245 mLoadingText = StringTexture.newInstance( 246 context.getString(R.string.loading), 247 DEFAULT_TEXT_SIZE, Color.WHITE); 248 mNoThumbnailText = StringTexture.newInstance( 249 context.getString(R.string.no_thumbnail), 250 DEFAULT_TEXT_SIZE, Color.WHITE); 251 252 mHandler = new MyHandler(activity.getGLRoot()); 253 254 mGestureListener = new MyGestureListener(); 255 mGestureRecognizer = new GestureRecognizer(context, mGestureListener); 256 257 mPositionController = new PositionController(context, 258 new PositionController.Listener() { 259 public void invalidate() { 260 PhotoView.this.invalidate(); 261 } 262 public boolean isHoldingDown() { 263 return (mHolding & HOLD_TOUCH_DOWN) != 0; 264 } 265 public boolean isHoldingDelete() { 266 return (mHolding & HOLD_DELETE) != 0; 267 } 268 public void onPull(int offset, int direction) { 269 mEdgeView.onPull(offset, direction); 270 } 271 public void onRelease() { 272 mEdgeView.onRelease(); 273 } 274 public void onAbsorb(int velocity, int direction) { 275 mEdgeView.onAbsorb(velocity, direction); 276 } 277 }); 278 mVideoPlayIcon = new ResourceTexture(context, R.drawable.ic_control_play); 279 for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) { 280 if (i == 0) { 281 mPictures.put(i, new FullPicture()); 282 } else { 283 mPictures.put(i, new ScreenNailPicture(i)); 284 } 285 } 286 } 287 288 public void setModel(Model model) { 289 mModel = model; 290 mTileView.setModel(mModel); 291 } 292 293 class MyHandler extends SynchronizedHandler { 294 public MyHandler(GLRoot root) { 295 super(root); 296 } 297 298 @Override 299 public void handleMessage(Message message) { 300 switch (message.what) { 301 case MSG_CANCEL_EXTRA_SCALING: { 302 mGestureRecognizer.cancelScale(); 303 mPositionController.setExtraScalingRange(false); 304 mCancelExtraScalingPending = false; 305 break; 306 } 307 case MSG_SWITCH_FOCUS: { 308 switchFocus(); 309 break; 310 } 311 case MSG_CAPTURE_ANIMATION_DONE: { 312 // message.arg1 is the offset parameter passed to 313 // switchWithCaptureAnimation(). 314 captureAnimationDone(message.arg1); 315 break; 316 } 317 case MSG_DELETE_ANIMATION_DONE: { 318 // message.obj is the Path of the MediaItem which should be 319 // deleted. message.arg1 is the offset of the image. 320 mListener.onDeleteImage((Path) message.obj, message.arg1); 321 // Normally a box which finishes delete animation will hold 322 // position until the underlying MediaItem is actually 323 // deleted, and HOLD_DELETE will be cancelled that time. In 324 // case the MediaItem didn't actually get deleted in 2 325 // seconds, we will cancel HOLD_DELETE and make it bounce 326 // back. 327 328 // We make sure there is at most one MSG_DELETE_DONE 329 // in the handler. 330 mHandler.removeMessages(MSG_DELETE_DONE); 331 Message m = mHandler.obtainMessage(MSG_DELETE_DONE); 332 mHandler.sendMessageDelayed(m, 2000); 333 break; 334 } 335 case MSG_DELETE_DONE: { 336 if (!mHandler.hasMessages(MSG_DELETE_ANIMATION_DONE)) { 337 mHolding &= ~HOLD_DELETE; 338 snapback(); 339 } 340 break; 341 } 342 case MSG_HIDE_UNDO_BAR: { 343 checkHideUndoBar(UNDO_BAR_TIMEOUT); 344 break; 345 } 346 default: throw new AssertionError(message.what); 347 } 348 } 349 }; 350 351 //////////////////////////////////////////////////////////////////////////// 352 // Data/Image change notifications 353 //////////////////////////////////////////////////////////////////////////// 354 355 public void notifyDataChange(int[] fromIndex, int prevBound, int nextBound) { 356 mPrevBound = prevBound; 357 mNextBound = nextBound; 358 359 // Update mTouchBoxIndex 360 if (mTouchBoxIndex != Integer.MAX_VALUE) { 361 int k = mTouchBoxIndex; 362 mTouchBoxIndex = Integer.MAX_VALUE; 363 for (int i = 0; i < 2 * SCREEN_NAIL_MAX + 1; i++) { 364 if (fromIndex[i] == k) { 365 mTouchBoxIndex = i - SCREEN_NAIL_MAX; 366 break; 367 } 368 } 369 } 370 371 // Hide undo button if we are too far away 372 if (mUndoIndexHint != Integer.MAX_VALUE) { 373 if (Math.abs(mUndoIndexHint - mModel.getCurrentIndex()) >= 3) { 374 hideUndoBar(); 375 } 376 } 377 378 // Update the ScreenNails. 379 for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) { 380 Picture p = mPictures.get(i); 381 p.reload(); 382 mSizes[i + SCREEN_NAIL_MAX] = p.getSize(); 383 } 384 385 boolean wasDeleting = mPositionController.hasDeletingBox(); 386 387 // Move the boxes 388 mPositionController.moveBox(fromIndex, mPrevBound < 0, mNextBound > 0, 389 mModel.isCamera(0), mSizes); 390 391 for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) { 392 setPictureSize(i); 393 } 394 395 boolean isDeleting = mPositionController.hasDeletingBox(); 396 397 // If the deletion is done, make HOLD_DELETE persist for only the time 398 // needed for a snapback animation. 399 if (wasDeleting && !isDeleting) { 400 mHandler.removeMessages(MSG_DELETE_DONE); 401 Message m = mHandler.obtainMessage(MSG_DELETE_DONE); 402 mHandler.sendMessageDelayed( 403 m, PositionController.SNAPBACK_ANIMATION_TIME); 404 } 405 406 invalidate(); 407 } 408 409 public boolean isDeleting() { 410 return (mHolding & HOLD_DELETE) != 0 411 && mPositionController.hasDeletingBox(); 412 } 413 414 public void notifyImageChange(int index) { 415 if (index == 0) { 416 mListener.onCurrentImageUpdated(); 417 } 418 mPictures.get(index).reload(); 419 setPictureSize(index); 420 invalidate(); 421 } 422 423 private void setPictureSize(int index) { 424 Picture p = mPictures.get(index); 425 mPositionController.setImageSize(index, p.getSize(), 426 index == 0 && p.isCamera() ? mCameraRect : null); 427 } 428 429 @Override 430 protected void onLayout( 431 boolean changeSize, int left, int top, int right, int bottom) { 432 int w = right - left; 433 int h = bottom - top; 434 mTileView.layout(0, 0, w, h); 435 mEdgeView.layout(0, 0, w, h); 436 mUndoBar.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); 437 mUndoBar.layout(0, h - mUndoBar.getMeasuredHeight(), w, h); 438 439 GLRoot root = getGLRoot(); 440 int displayRotation = root.getDisplayRotation(); 441 int compensation = root.getCompensation(); 442 if (mDisplayRotation != displayRotation 443 || mCompensation != compensation) { 444 mDisplayRotation = displayRotation; 445 mCompensation = compensation; 446 447 // We need to change the size and rotation of the Camera ScreenNail, 448 // but we don't want it to animate because the size doen't actually 449 // change in the eye of the user. 450 for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) { 451 Picture p = mPictures.get(i); 452 if (p.isCamera()) { 453 p.forceSize(); 454 } 455 } 456 } 457 458 updateCameraRect(); 459 mPositionController.setConstrainedFrame(mCameraRect); 460 if (changeSize) { 461 mPositionController.setViewSize(getWidth(), getHeight()); 462 } 463 } 464 465 // Update the camera rectangle due to layout change or camera relative frame 466 // change. 467 private void updateCameraRect() { 468 // Get the width and height in framework orientation because the given 469 // mCameraRelativeFrame is in that coordinates. 470 int w = getWidth(); 471 int h = getHeight(); 472 if (mCompensation % 180 != 0) { 473 int tmp = w; 474 w = h; 475 h = tmp; 476 } 477 int l = mCameraRelativeFrame.left; 478 int t = mCameraRelativeFrame.top; 479 int r = mCameraRelativeFrame.right; 480 int b = mCameraRelativeFrame.bottom; 481 482 // Now convert it to the coordinates we are using. 483 switch (mCompensation) { 484 case 0: mCameraRect.set(l, t, r, b); break; 485 case 90: mCameraRect.set(h - b, l, h - t, r); break; 486 case 180: mCameraRect.set(w - r, h - b, w - l, h - t); break; 487 case 270: mCameraRect.set(t, w - r, b, w - l); break; 488 } 489 490 Log.d(TAG, "compensation = " + mCompensation 491 + ", CameraRelativeFrame = " + mCameraRelativeFrame 492 + ", mCameraRect = " + mCameraRect); 493 } 494 495 public void setCameraRelativeFrame(Rect frame) { 496 mCameraRelativeFrame.set(frame); 497 updateCameraRect(); 498 // Originally we do 499 // mPositionController.setConstrainedFrame(mCameraRect); 500 // here, but it is moved to a parameter of the setImageSize() call, so 501 // it can be updated atomically with the CameraScreenNail's size change. 502 } 503 504 // Returns the rotation we need to do to the camera texture before drawing 505 // it to the canvas, assuming the camera texture is correct when the device 506 // is in its natural orientation. 507 private int getCameraRotation() { 508 return (mCompensation - mDisplayRotation + 360) % 360; 509 } 510 511 private int getPanoramaRotation() { 512 return mCompensation; 513 } 514 515 //////////////////////////////////////////////////////////////////////////// 516 // Pictures 517 //////////////////////////////////////////////////////////////////////////// 518 519 private interface Picture { 520 void reload(); 521 void draw(GLCanvas canvas, Rect r); 522 void setScreenNail(ScreenNail s); 523 boolean isCamera(); // whether the picture is a camera preview 524 boolean isDeletable(); // whether the picture can be deleted 525 void forceSize(); // called when mCompensation changes 526 Size getSize(); 527 }; 528 529 class FullPicture implements Picture { 530 private int mRotation; 531 private boolean mIsCamera; 532 private boolean mIsPanorama; 533 private boolean mIsVideo; 534 private boolean mIsDeletable; 535 private int mLoadingState = Model.LOADING_INIT; 536 private Size mSize = new Size(); 537 private boolean mWasCameraCenter; 538 public void FullPicture(TileImageView tileView) { 539 mTileView = tileView; 540 } 541 542 @Override 543 public void reload() { 544 // mImageWidth and mImageHeight will get updated 545 mTileView.notifyModelInvalidated(); 546 547 mIsCamera = mModel.isCamera(0); 548 mIsPanorama = mModel.isPanorama(0); 549 mIsVideo = mModel.isVideo(0); 550 mIsDeletable = mModel.isDeletable(0); 551 mLoadingState = mModel.getLoadingState(0); 552 setScreenNail(mModel.getScreenNail(0)); 553 updateSize(); 554 } 555 556 @Override 557 public Size getSize() { 558 return mSize; 559 } 560 561 @Override 562 public void forceSize() { 563 updateSize(); 564 mPositionController.forceImageSize(0, mSize); 565 } 566 567 private void updateSize() { 568 if (mIsPanorama) { 569 mRotation = getPanoramaRotation(); 570 } else if (mIsCamera) { 571 mRotation = getCameraRotation(); 572 } else { 573 mRotation = mModel.getImageRotation(0); 574 } 575 576 int w = mTileView.mImageWidth; 577 int h = mTileView.mImageHeight; 578 mSize.width = getRotated(mRotation, w, h); 579 mSize.height = getRotated(mRotation, h, w); 580 } 581 582 @Override 583 public void draw(GLCanvas canvas, Rect r) { 584 drawTileView(canvas, r); 585 586 // We want to have the following transitions: 587 // (1) Move camera preview out of its place: switch to film mode 588 // (2) Move camera preview into its place: switch to page mode 589 // The extra mWasCenter check makes sure (1) does not apply if in 590 // page mode, we move _to_ the camera preview from another picture. 591 592 // Holdings except touch-down prevent the transitions. 593 if ((mHolding & ~HOLD_TOUCH_DOWN) != 0) return; 594 595 boolean isCenter = mPositionController.isCenter(); 596 boolean isCameraCenter = mIsCamera && isCenter; 597 598 if (mWasCameraCenter && mIsCamera && !isCenter && !mFilmMode) { 599 // Temporary disabled to de-emphasize filmstrip. 600 // setFilmMode(true); 601 } else if (!mWasCameraCenter && isCameraCenter && mFilmMode) { 602 setFilmMode(false); 603 } 604 605 if (isCameraCenter && !mFilmMode) { 606 // Move into camera in page mode, lock 607 mListener.lockOrientation(); 608 } 609 610 mWasCameraCenter = isCameraCenter; 611 } 612 613 @Override 614 public void setScreenNail(ScreenNail s) { 615 mTileView.setScreenNail(s); 616 } 617 618 @Override 619 public boolean isCamera() { 620 return mIsCamera; 621 } 622 623 @Override 624 public boolean isDeletable() { 625 return mIsDeletable; 626 } 627 628 private void drawTileView(GLCanvas canvas, Rect r) { 629 float imageScale = mPositionController.getImageScale(); 630 int viewW = getWidth(); 631 int viewH = getHeight(); 632 float cx = r.exactCenterX(); 633 float cy = r.exactCenterY(); 634 float scale = 1f; // the scaling factor due to card effect 635 636 canvas.save(GLCanvas.SAVE_FLAG_MATRIX | GLCanvas.SAVE_FLAG_ALPHA); 637 float filmRatio = mPositionController.getFilmRatio(); 638 boolean wantsCardEffect = CARD_EFFECT && !mIsCamera 639 && filmRatio != 1f && !mPictures.get(-1).isCamera() 640 && !mPositionController.inOpeningAnimation(); 641 boolean wantsOffsetEffect = OFFSET_EFFECT && mIsDeletable 642 && filmRatio == 1f && r.centerY() != viewH / 2; 643 if (wantsCardEffect) { 644 // Calculate the move-out progress value. 645 int left = r.left; 646 int right = r.right; 647 float progress = calculateMoveOutProgress(left, right, viewW); 648 progress = Utils.clamp(progress, -1f, 1f); 649 650 // We only want to apply the fading animation if the scrolling 651 // movement is to the right. 652 if (progress < 0) { 653 scale = getScrollScale(progress); 654 float alpha = getScrollAlpha(progress); 655 scale = interpolate(filmRatio, scale, 1f); 656 alpha = interpolate(filmRatio, alpha, 1f); 657 658 imageScale *= scale; 659 canvas.multiplyAlpha(alpha); 660 661 float cxPage; // the cx value in page mode 662 if (right - left <= viewW) { 663 // If the picture is narrower than the view, keep it at 664 // the center of the view. 665 cxPage = viewW / 2f; 666 } else { 667 // If the picture is wider than the view (it's 668 // zoomed-in), keep the left edge of the object align 669 // the the left edge of the view. 670 cxPage = (right - left) * scale / 2f; 671 } 672 cx = interpolate(filmRatio, cxPage, cx); 673 } 674 } else if (wantsOffsetEffect) { 675 float offset = (float) (r.centerY() - viewH / 2) / viewH; 676 float alpha = getOffsetAlpha(offset); 677 canvas.multiplyAlpha(alpha); 678 } 679 680 // Draw the tile view. 681 setTileViewPosition(cx, cy, viewW, viewH, imageScale); 682 renderChild(canvas, mTileView); 683 684 // Draw the play video icon and the message. 685 canvas.translate((int) (cx + 0.5f), (int) (cy + 0.5f)); 686 int s = (int) (scale * Math.min(r.width(), r.height()) + 0.5f); 687 if (mIsVideo) drawVideoPlayIcon(canvas, s); 688 if (mLoadingState == Model.LOADING_FAIL) { 689 drawLoadingFailMessage(canvas); 690 } 691 692 // Draw a debug indicator showing which picture has focus (index == 693 // 0). 694 //canvas.fillRect(-10, -10, 20, 20, 0x80FF00FF); 695 696 canvas.restore(); 697 } 698 699 // Set the position of the tile view 700 private void setTileViewPosition(float cx, float cy, 701 int viewW, int viewH, float scale) { 702 // Find out the bitmap coordinates of the center of the view 703 int imageW = mPositionController.getImageWidth(); 704 int imageH = mPositionController.getImageHeight(); 705 int centerX = (int) (imageW / 2f + (viewW / 2f - cx) / scale + 0.5f); 706 int centerY = (int) (imageH / 2f + (viewH / 2f - cy) / scale + 0.5f); 707 708 int inverseX = imageW - centerX; 709 int inverseY = imageH - centerY; 710 int x, y; 711 switch (mRotation) { 712 case 0: x = centerX; y = centerY; break; 713 case 90: x = centerY; y = inverseX; break; 714 case 180: x = inverseX; y = inverseY; break; 715 case 270: x = inverseY; y = centerX; break; 716 default: 717 throw new RuntimeException(String.valueOf(mRotation)); 718 } 719 mTileView.setPosition(x, y, scale, mRotation); 720 } 721 } 722 723 private class ScreenNailPicture implements Picture { 724 private int mIndex; 725 private int mRotation; 726 private ScreenNail mScreenNail; 727 private boolean mIsCamera; 728 private boolean mIsPanorama; 729 private boolean mIsVideo; 730 private boolean mIsDeletable; 731 private int mLoadingState = Model.LOADING_INIT; 732 private Size mSize = new Size(); 733 734 public ScreenNailPicture(int index) { 735 mIndex = index; 736 } 737 738 @Override 739 public void reload() { 740 mIsCamera = mModel.isCamera(mIndex); 741 mIsPanorama = mModel.isPanorama(mIndex); 742 mIsVideo = mModel.isVideo(mIndex); 743 mIsDeletable = mModel.isDeletable(mIndex); 744 mLoadingState = mModel.getLoadingState(mIndex); 745 setScreenNail(mModel.getScreenNail(mIndex)); 746 updateSize(); 747 } 748 749 @Override 750 public Size getSize() { 751 return mSize; 752 } 753 754 @Override 755 public void draw(GLCanvas canvas, Rect r) { 756 if (mScreenNail == null) { 757 // Draw a placeholder rectange if there should be a picture in 758 // this position (but somehow there isn't). 759 if (mIndex >= mPrevBound && mIndex <= mNextBound) { 760 drawPlaceHolder(canvas, r); 761 } 762 return; 763 } 764 int w = getWidth(); 765 int h = getHeight(); 766 if (r.left >= w || r.right <= 0 || r.top >= h || r.bottom <= 0) { 767 mScreenNail.noDraw(); 768 return; 769 } 770 771 float filmRatio = mPositionController.getFilmRatio(); 772 boolean wantsCardEffect = CARD_EFFECT && mIndex > 0 773 && filmRatio != 1f && !mPictures.get(0).isCamera(); 774 boolean wantsOffsetEffect = OFFSET_EFFECT && mIsDeletable 775 && filmRatio == 1f && r.centerY() != h / 2; 776 int cx = wantsCardEffect 777 ? (int) (interpolate(filmRatio, w / 2, r.centerX()) + 0.5f) 778 : r.centerX(); 779 int cy = r.centerY(); 780 canvas.save(GLCanvas.SAVE_FLAG_MATRIX | GLCanvas.SAVE_FLAG_ALPHA); 781 canvas.translate(cx, cy); 782 if (wantsCardEffect) { 783 float progress = (float) (w / 2 - r.centerX()) / w; 784 progress = Utils.clamp(progress, -1, 1); 785 float alpha = getScrollAlpha(progress); 786 float scale = getScrollScale(progress); 787 alpha = interpolate(filmRatio, alpha, 1f); 788 scale = interpolate(filmRatio, scale, 1f); 789 canvas.multiplyAlpha(alpha); 790 canvas.scale(scale, scale, 1); 791 } else if (wantsOffsetEffect) { 792 float offset = (float) (r.centerY() - h / 2) / h; 793 float alpha = getOffsetAlpha(offset); 794 canvas.multiplyAlpha(alpha); 795 } 796 if (mRotation != 0) { 797 canvas.rotate(mRotation, 0, 0, 1); 798 } 799 int drawW = getRotated(mRotation, r.width(), r.height()); 800 int drawH = getRotated(mRotation, r.height(), r.width()); 801 mScreenNail.draw(canvas, -drawW / 2, -drawH / 2, drawW, drawH); 802 if (isScreenNailAnimating()) { 803 invalidate(); 804 } 805 int s = Math.min(drawW, drawH); 806 if (mIsVideo) drawVideoPlayIcon(canvas, s); 807 if (mLoadingState == Model.LOADING_FAIL) { 808 drawLoadingFailMessage(canvas); 809 } 810 canvas.restore(); 811 } 812 813 private boolean isScreenNailAnimating() { 814 return (mScreenNail instanceof BitmapScreenNail) 815 && ((BitmapScreenNail) mScreenNail).isAnimating(); 816 } 817 818 @Override 819 public void setScreenNail(ScreenNail s) { 820 mScreenNail = s; 821 } 822 823 @Override 824 public void forceSize() { 825 updateSize(); 826 mPositionController.forceImageSize(mIndex, mSize); 827 } 828 829 private void updateSize() { 830 if (mIsPanorama) { 831 mRotation = getPanoramaRotation(); 832 } else if (mIsCamera) { 833 mRotation = getCameraRotation(); 834 } else { 835 mRotation = mModel.getImageRotation(mIndex); 836 } 837 838 if (mScreenNail != null) { 839 mSize.width = mScreenNail.getWidth(); 840 mSize.height = mScreenNail.getHeight(); 841 } else { 842 // If we don't have ScreenNail available, we can still try to 843 // get the size information of it. 844 mModel.getImageSize(mIndex, mSize); 845 } 846 847 int w = mSize.width; 848 int h = mSize.height; 849 mSize.width = getRotated(mRotation, w, h); 850 mSize.height = getRotated(mRotation, h, w); 851 } 852 853 @Override 854 public boolean isCamera() { 855 return mIsCamera; 856 } 857 858 @Override 859 public boolean isDeletable() { 860 return mIsDeletable; 861 } 862 } 863 864 // Draw a gray placeholder in the specified rectangle. 865 private void drawPlaceHolder(GLCanvas canvas, Rect r) { 866 canvas.fillRect(r.left, r.top, r.width(), r.height(), PLACEHOLDER_COLOR); 867 } 868 869 // Draw the video play icon (in the place where the spinner was) 870 private void drawVideoPlayIcon(GLCanvas canvas, int side) { 871 int s = side / ICON_RATIO; 872 // Draw the video play icon at the center 873 mVideoPlayIcon.draw(canvas, -s / 2, -s / 2, s, s); 874 } 875 876 // Draw the "no thumbnail" message 877 private void drawLoadingFailMessage(GLCanvas canvas) { 878 StringTexture m = mNoThumbnailText; 879 m.draw(canvas, -m.getWidth() / 2, -m.getHeight() / 2); 880 } 881 882 private static int getRotated(int degree, int original, int theother) { 883 return (degree % 180 == 0) ? original : theother; 884 } 885 886 //////////////////////////////////////////////////////////////////////////// 887 // Gestures Handling 888 //////////////////////////////////////////////////////////////////////////// 889 890 @Override 891 protected boolean onTouch(MotionEvent event) { 892 mGestureRecognizer.onTouchEvent(event); 893 return true; 894 } 895 896 private class MyGestureListener implements GestureRecognizer.Listener { 897 private boolean mIgnoreUpEvent = false; 898 // If we can change mode for this scale gesture. 899 private boolean mCanChangeMode; 900 // If we have changed the film mode in this scaling gesture. 901 private boolean mModeChanged; 902 // If this scaling gesture should be ignored. 903 private boolean mIgnoreScalingGesture; 904 // If we have seen a scaling gesture. 905 private boolean mSeenScaling; 906 // whether the down action happened while the view is scrolling. 907 private boolean mDownInScrolling; 908 // If we should ignore all gestures other than onSingleTapUp. 909 private boolean mIgnoreSwipingGesture; 910 // If a scrolling has happened after a down gesture. 911 private boolean mScrolledAfterDown; 912 // If the first scrolling move is in X direction. In the film mode, X 913 // direction scrolling is normal scrolling. but Y direction scrolling is 914 // a delete gesture. 915 private boolean mFirstScrollX; 916 // The accumulated Y delta that has been sent to mPositionController. 917 private int mDeltaY; 918 // The accumulated scaling change from a scaling gesture. 919 private float mAccScale; 920 921 @Override 922 public boolean onSingleTapUp(float x, float y) { 923 // We do this in addition to onUp() because we want the snapback of 924 // setFilmMode to happen. 925 mHolding &= ~HOLD_TOUCH_DOWN; 926 927 if (mFilmMode && !mDownInScrolling) { 928 switchToHitPicture((int) (x + 0.5f), (int) (y + 0.5f)); 929 setFilmMode(false); 930 mIgnoreUpEvent = true; 931 return true; 932 } 933 934 if (mListener != null) { 935 // Do the inverse transform of the touch coordinates. 936 Matrix m = getGLRoot().getCompensationMatrix(); 937 Matrix inv = new Matrix(); 938 m.invert(inv); 939 float[] pts = new float[] {x, y}; 940 inv.mapPoints(pts); 941 mListener.onSingleTapUp((int) (pts[0] + 0.5f), (int) (pts[1] + 0.5f)); 942 } 943 return true; 944 } 945 946 @Override 947 public boolean onDoubleTap(float x, float y) { 948 if (mIgnoreSwipingGesture) return true; 949 if (mPictures.get(0).isCamera()) return false; 950 PositionController controller = mPositionController; 951 float scale = controller.getImageScale(); 952 // onDoubleTap happened on the second ACTION_DOWN. 953 // We need to ignore the next UP event. 954 mIgnoreUpEvent = true; 955 if (scale <= 1.0f || controller.isAtMinimalScale()) { 956 controller.zoomIn(x, y, Math.max(1.5f, scale * 1.5f)); 957 } else { 958 controller.resetToFullView(); 959 } 960 return true; 961 } 962 963 @Override 964 public boolean onScroll(float dx, float dy, float totalX, float totalY) { 965 if (mIgnoreSwipingGesture) return true; 966 if (!mScrolledAfterDown) { 967 mScrolledAfterDown = true; 968 mFirstScrollX = (Math.abs(dx) > Math.abs(dy)); 969 } 970 971 int dxi = (int) (-dx + 0.5f); 972 int dyi = (int) (-dy + 0.5f); 973 if (mFilmMode) { 974 if (mFirstScrollX) { 975 mPositionController.scrollFilmX(dxi); 976 } else { 977 if (mTouchBoxIndex == Integer.MAX_VALUE) return true; 978 int newDeltaY = calculateDeltaY(totalY); 979 int d = newDeltaY - mDeltaY; 980 if (d != 0) { 981 mPositionController.scrollFilmY(mTouchBoxIndex, d); 982 mDeltaY = newDeltaY; 983 } 984 } 985 } else { 986 mPositionController.scrollPage(dxi, dyi); 987 } 988 return true; 989 } 990 991 private int calculateDeltaY(float delta) { 992 if (mTouchBoxDeletable) return (int) (delta + 0.5f); 993 994 // don't let items that can't be deleted be dragged more than 995 // maxScrollDistance, and make it harder and harder to drag. 996 int size = getHeight(); 997 float maxScrollDistance = 0.15f * size; 998 if (Math.abs(delta) >= size) { 999 delta = delta > 0 ? maxScrollDistance : -maxScrollDistance; 1000 } else { 1001 delta = maxScrollDistance * 1002 FloatMath.sin((delta / size) * (float) (Math.PI / 2)); 1003 } 1004 return (int) (delta + 0.5f); 1005 } 1006 1007 @Override 1008 public boolean onFling(float velocityX, float velocityY) { 1009 if (mIgnoreSwipingGesture) return true; 1010 if (mSeenScaling) return true; 1011 if (swipeImages(velocityX, velocityY)) { 1012 mIgnoreUpEvent = true; 1013 } else { 1014 flingImages(velocityX, velocityY); 1015 } 1016 return true; 1017 } 1018 1019 private boolean flingImages(float velocityX, float velocityY) { 1020 int vx = (int) (velocityX + 0.5f); 1021 int vy = (int) (velocityY + 0.5f); 1022 if (!mFilmMode) { 1023 return mPositionController.flingPage(vx, vy); 1024 } 1025 if (Math.abs(velocityX) > Math.abs(velocityY)) { 1026 return mPositionController.flingFilmX(vx); 1027 } 1028 // If we scrolled in Y direction fast enough, treat it as a delete 1029 // gesture. 1030 if (!mFilmMode || mTouchBoxIndex == Integer.MAX_VALUE 1031 || !mTouchBoxDeletable) { 1032 return false; 1033 } 1034 int maxVelocity = (int) GalleryUtils.dpToPixel(MAX_DISMISS_VELOCITY); 1035 int escapeVelocity = 1036 (int) GalleryUtils.dpToPixel(SWIPE_ESCAPE_VELOCITY); 1037 int centerY = mPositionController.getPosition(mTouchBoxIndex) 1038 .centerY(); 1039 boolean fastEnough = (Math.abs(vy) > escapeVelocity) 1040 && (Math.abs(vy) > Math.abs(vx)) 1041 && ((vy > 0) == (centerY > getHeight() / 2)); 1042 if (fastEnough) { 1043 vy = Math.min(vy, maxVelocity); 1044 int duration = mPositionController.flingFilmY(mTouchBoxIndex, vy); 1045 if (duration >= 0) { 1046 mPositionController.setPopFromTop(vy < 0); 1047 deleteAfterAnimation(duration); 1048 // We reset mTouchBoxIndex, so up() won't check if Y 1049 // scrolled far enough to be a delete gesture. 1050 mTouchBoxIndex = Integer.MAX_VALUE; 1051 return true; 1052 } 1053 } 1054 return false; 1055 } 1056 1057 private void deleteAfterAnimation(int duration) { 1058 MediaItem item = mModel.getMediaItem(mTouchBoxIndex); 1059 if (item == null) return; 1060 mListener.onCommitDeleteImage(); 1061 mUndoIndexHint = mModel.getCurrentIndex() + mTouchBoxIndex; 1062 mHolding |= HOLD_DELETE; 1063 Message m = mHandler.obtainMessage(MSG_DELETE_ANIMATION_DONE); 1064 m.obj = item.getPath(); 1065 m.arg1 = mTouchBoxIndex; 1066 mHandler.sendMessageDelayed(m, duration); 1067 } 1068 1069 @Override 1070 public boolean onScaleBegin(float focusX, float focusY) { 1071 if (mIgnoreSwipingGesture) return true; 1072 // We ignore the scaling gesture if it is a camera preview. 1073 mIgnoreScalingGesture = mPictures.get(0).isCamera(); 1074 if (mIgnoreScalingGesture) { 1075 return true; 1076 } 1077 mPositionController.beginScale(focusX, focusY); 1078 // We can change mode if we are in film mode, or we are in page 1079 // mode and at minimal scale. 1080 mCanChangeMode = mFilmMode 1081 || mPositionController.isAtMinimalScale(); 1082 mModeChanged = false; 1083 mSeenScaling = true; 1084 mAccScale = 1f; 1085 return true; 1086 } 1087 1088 @Override 1089 public boolean onScale(float focusX, float focusY, float scale) { 1090 if (mIgnoreSwipingGesture) return true; 1091 if (mIgnoreScalingGesture) return true; 1092 if (mModeChanged) return true; 1093 if (Float.isNaN(scale) || Float.isInfinite(scale)) return false; 1094 1095 int outOfRange = mPositionController.scaleBy(scale, focusX, focusY); 1096 1097 // We wait for a large enough scale change before changing mode. 1098 // Otherwise we may mistakenly treat a zoom-in gesture as zoom-out 1099 // or vice versa. 1100 mAccScale *= scale; 1101 boolean largeEnough = (mAccScale < 0.97f || mAccScale > 1.03f); 1102 1103 // If mode changes, we treat this scaling gesture has ended. 1104 if (mCanChangeMode && largeEnough) { 1105 if ((outOfRange < 0 && !mFilmMode) || 1106 (outOfRange > 0 && mFilmMode)) { 1107 stopExtraScalingIfNeeded(); 1108 1109 // Removing the touch down flag allows snapback to happen 1110 // for film mode change. 1111 mHolding &= ~HOLD_TOUCH_DOWN; 1112 setFilmMode(!mFilmMode); 1113 1114 // We need to call onScaleEnd() before setting mModeChanged 1115 // to true. 1116 onScaleEnd(); 1117 mModeChanged = true; 1118 return true; 1119 } 1120 } 1121 1122 if (outOfRange != 0) { 1123 startExtraScalingIfNeeded(); 1124 } else { 1125 stopExtraScalingIfNeeded(); 1126 } 1127 return true; 1128 } 1129 1130 @Override 1131 public void onScaleEnd() { 1132 if (mIgnoreSwipingGesture) return; 1133 if (mIgnoreScalingGesture) return; 1134 if (mModeChanged) return; 1135 mPositionController.endScale(); 1136 } 1137 1138 private void startExtraScalingIfNeeded() { 1139 if (!mCancelExtraScalingPending) { 1140 mHandler.sendEmptyMessageDelayed( 1141 MSG_CANCEL_EXTRA_SCALING, 700); 1142 mPositionController.setExtraScalingRange(true); 1143 mCancelExtraScalingPending = true; 1144 } 1145 } 1146 1147 private void stopExtraScalingIfNeeded() { 1148 if (mCancelExtraScalingPending) { 1149 mHandler.removeMessages(MSG_CANCEL_EXTRA_SCALING); 1150 mPositionController.setExtraScalingRange(false); 1151 mCancelExtraScalingPending = false; 1152 } 1153 } 1154 1155 @Override 1156 public void onDown(float x, float y) { 1157 checkHideUndoBar(UNDO_BAR_TOUCHED); 1158 1159 mDeltaY = 0; 1160 mSeenScaling = false; 1161 1162 if (mIgnoreSwipingGesture) return; 1163 1164 mHolding |= HOLD_TOUCH_DOWN; 1165 1166 if (mFilmMode && mPositionController.isScrolling()) { 1167 mDownInScrolling = true; 1168 mPositionController.stopScrolling(); 1169 } else { 1170 mDownInScrolling = false; 1171 } 1172 1173 mScrolledAfterDown = false; 1174 if (mFilmMode) { 1175 int xi = (int) (x + 0.5f); 1176 int yi = (int) (y + 0.5f); 1177 mTouchBoxIndex = mPositionController.hitTest(xi, yi); 1178 if (mTouchBoxIndex < mPrevBound || mTouchBoxIndex > mNextBound) { 1179 mTouchBoxIndex = Integer.MAX_VALUE; 1180 } else { 1181 mTouchBoxDeletable = 1182 mPictures.get(mTouchBoxIndex).isDeletable(); 1183 } 1184 } else { 1185 mTouchBoxIndex = Integer.MAX_VALUE; 1186 } 1187 } 1188 1189 @Override 1190 public void onUp() { 1191 if (mIgnoreSwipingGesture) return; 1192 1193 mHolding &= ~HOLD_TOUCH_DOWN; 1194 mEdgeView.onRelease(); 1195 1196 // If we scrolled in Y direction far enough, treat it as a delete 1197 // gesture. 1198 if (mFilmMode && mScrolledAfterDown && !mFirstScrollX 1199 && mTouchBoxIndex != Integer.MAX_VALUE) { 1200 Rect r = mPositionController.getPosition(mTouchBoxIndex); 1201 int h = getHeight(); 1202 if (Math.abs(r.centerY() - h * 0.5f) > 0.4f * h) { 1203 int duration = mPositionController 1204 .flingFilmY(mTouchBoxIndex, 0); 1205 if (duration >= 0) { 1206 mPositionController.setPopFromTop(r.centerY() < h * 0.5f); 1207 deleteAfterAnimation(duration); 1208 } 1209 } 1210 } 1211 1212 if (mIgnoreUpEvent) { 1213 mIgnoreUpEvent = false; 1214 return; 1215 } 1216 1217 snapback(); 1218 } 1219 1220 public void setSwipingEnabled(boolean enabled) { 1221 mIgnoreSwipingGesture = !enabled; 1222 } 1223 } 1224 1225 public void setSwipingEnabled(boolean enabled) { 1226 mGestureListener.setSwipingEnabled(enabled); 1227 } 1228 1229 private void setFilmMode(boolean enabled) { 1230 if (mFilmMode == enabled) return; 1231 mFilmMode = enabled; 1232 mPositionController.setFilmMode(mFilmMode); 1233 mModel.setNeedFullImage(!enabled); 1234 mModel.setFocusHintDirection( 1235 mFilmMode ? Model.FOCUS_HINT_PREVIOUS : Model.FOCUS_HINT_NEXT); 1236 mListener.onActionBarAllowed(!enabled); 1237 1238 // Move into camera in page mode, lock 1239 if (!enabled && mPictures.get(0).isCamera()) { 1240 mListener.lockOrientation(); 1241 } 1242 } 1243 1244 public boolean getFilmMode() { 1245 return mFilmMode; 1246 } 1247 1248 //////////////////////////////////////////////////////////////////////////// 1249 // Framework events 1250 //////////////////////////////////////////////////////////////////////////// 1251 1252 public void pause() { 1253 mPositionController.skipAnimation(); 1254 mTileView.freeTextures(); 1255 for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) { 1256 mPictures.get(i).setScreenNail(null); 1257 } 1258 } 1259 1260 public void resume() { 1261 mTileView.prepareTextures(); 1262 } 1263 1264 // move to the camera preview and show controls after resume 1265 public void resetToFirstPicture() { 1266 mModel.moveTo(0); 1267 setFilmMode(false); 1268 } 1269 1270 //////////////////////////////////////////////////////////////////////////// 1271 // Undo Bar 1272 //////////////////////////////////////////////////////////////////////////// 1273 1274 private int mUndoBarState; 1275 private static final int UNDO_BAR_SHOW = 1; 1276 private static final int UNDO_BAR_TIMEOUT = 2; 1277 private static final int UNDO_BAR_TOUCHED = 4; 1278 1279 public void showUndoBar() { 1280 mHandler.removeMessages(MSG_HIDE_UNDO_BAR); 1281 mUndoBarState = UNDO_BAR_SHOW; 1282 mUndoBar.animateVisibility(GLView.VISIBLE); 1283 mHandler.sendEmptyMessageDelayed(MSG_HIDE_UNDO_BAR, 3000); 1284 } 1285 1286 public void hideUndoBar() { 1287 mHandler.removeMessages(MSG_HIDE_UNDO_BAR); 1288 mListener.onCommitDeleteImage(); 1289 mUndoBar.animateVisibility(GLView.INVISIBLE); 1290 mUndoBarState = 0; 1291 mUndoIndexHint = Integer.MAX_VALUE; 1292 } 1293 1294 // Check if the all conditions for hiding the undo bar have been met. The 1295 // conditions are: it has been three seconds since last showing, and the 1296 // user has touched. 1297 private void checkHideUndoBar(int addition) { 1298 mUndoBarState |= addition; 1299 if (mUndoBarState == 1300 (UNDO_BAR_SHOW | UNDO_BAR_TIMEOUT | UNDO_BAR_TOUCHED)) { 1301 hideUndoBar(); 1302 } 1303 } 1304 1305 //////////////////////////////////////////////////////////////////////////// 1306 // Rendering 1307 //////////////////////////////////////////////////////////////////////////// 1308 1309 @Override 1310 protected void render(GLCanvas canvas) { 1311 // Check if the camera preview occupies the full screen. 1312 boolean full = !mFilmMode && mPictures.get(0).isCamera() 1313 && mPositionController.isCenter() 1314 && mPositionController.isAtMinimalScale(); 1315 if (full != mFullScreenCamera) { 1316 mFullScreenCamera = full; 1317 mListener.onFullScreenChanged(full); 1318 } 1319 1320 // Determine how many photos we need to draw in addition to the center 1321 // one. 1322 int neighbors; 1323 if (mFullScreenCamera) { 1324 neighbors = 0; 1325 } else { 1326 // In page mode, we draw only one previous/next photo. But if we are 1327 // doing capture animation, we want to draw all photos. 1328 boolean inPageMode = (mPositionController.getFilmRatio() == 0f); 1329 boolean inCaptureAnimation = 1330 ((mHolding & HOLD_CAPTURE_ANIMATION) != 0); 1331 if (inPageMode && !inCaptureAnimation) { 1332 neighbors = 1; 1333 } else { 1334 neighbors = SCREEN_NAIL_MAX; 1335 } 1336 } 1337 1338 // Draw photos from back to front 1339 for (int i = neighbors; i >= -neighbors; i--) { 1340 Rect r = mPositionController.getPosition(i); 1341 mPictures.get(i).draw(canvas, r); 1342 } 1343 1344 renderChild(canvas, mEdgeView); 1345 renderChild(canvas, mUndoBar); 1346 1347 mPositionController.advanceAnimation(); 1348 checkFocusSwitching(); 1349 } 1350 1351 //////////////////////////////////////////////////////////////////////////// 1352 // Film mode focus switching 1353 //////////////////////////////////////////////////////////////////////////// 1354 1355 // Runs in GL thread. 1356 private void checkFocusSwitching() { 1357 if (!mFilmMode) return; 1358 if (mHandler.hasMessages(MSG_SWITCH_FOCUS)) return; 1359 if (switchPosition() != 0) { 1360 mHandler.sendEmptyMessage(MSG_SWITCH_FOCUS); 1361 } 1362 } 1363 1364 // Runs in main thread. 1365 private void switchFocus() { 1366 if (mHolding != 0) return; 1367 switch (switchPosition()) { 1368 case -1: 1369 switchToPrevImage(); 1370 break; 1371 case 1: 1372 switchToNextImage(); 1373 break; 1374 } 1375 } 1376 1377 // Returns -1 if we should switch focus to the previous picture, +1 if we 1378 // should switch to the next, 0 otherwise. 1379 private int switchPosition() { 1380 Rect curr = mPositionController.getPosition(0); 1381 int center = getWidth() / 2; 1382 1383 if (curr.left > center && mPrevBound < 0) { 1384 Rect prev = mPositionController.getPosition(-1); 1385 int currDist = curr.left - center; 1386 int prevDist = center - prev.right; 1387 if (prevDist < currDist) { 1388 return -1; 1389 } 1390 } else if (curr.right < center && mNextBound > 0) { 1391 Rect next = mPositionController.getPosition(1); 1392 int currDist = center - curr.right; 1393 int nextDist = next.left - center; 1394 if (nextDist < currDist) { 1395 return 1; 1396 } 1397 } 1398 1399 return 0; 1400 } 1401 1402 // Switch to the previous or next picture if the hit position is inside 1403 // one of their boxes. This runs in main thread. 1404 private void switchToHitPicture(int x, int y) { 1405 if (mPrevBound < 0) { 1406 Rect r = mPositionController.getPosition(-1); 1407 if (r.right >= x) { 1408 slideToPrevPicture(); 1409 return; 1410 } 1411 } 1412 1413 if (mNextBound > 0) { 1414 Rect r = mPositionController.getPosition(1); 1415 if (r.left <= x) { 1416 slideToNextPicture(); 1417 return; 1418 } 1419 } 1420 } 1421 1422 //////////////////////////////////////////////////////////////////////////// 1423 // Page mode focus switching 1424 // 1425 // We slide image to the next one or the previous one in two cases: 1: If 1426 // the user did a fling gesture with enough velocity. 2 If the user has 1427 // moved the picture a lot. 1428 //////////////////////////////////////////////////////////////////////////// 1429 1430 private boolean swipeImages(float velocityX, float velocityY) { 1431 if (mFilmMode) return false; 1432 1433 // Avoid swiping images if we're possibly flinging to view the 1434 // zoomed in picture vertically. 1435 PositionController controller = mPositionController; 1436 boolean isMinimal = controller.isAtMinimalScale(); 1437 int edges = controller.getImageAtEdges(); 1438 if (!isMinimal && Math.abs(velocityY) > Math.abs(velocityX)) 1439 if ((edges & PositionController.IMAGE_AT_TOP_EDGE) == 0 1440 || (edges & PositionController.IMAGE_AT_BOTTOM_EDGE) == 0) 1441 return false; 1442 1443 // If we are at the edge of the current photo and the sweeping velocity 1444 // exceeds the threshold, slide to the next / previous image. 1445 if (velocityX < -SWIPE_THRESHOLD && (isMinimal 1446 || (edges & PositionController.IMAGE_AT_RIGHT_EDGE) != 0)) { 1447 return slideToNextPicture(); 1448 } else if (velocityX > SWIPE_THRESHOLD && (isMinimal 1449 || (edges & PositionController.IMAGE_AT_LEFT_EDGE) != 0)) { 1450 return slideToPrevPicture(); 1451 } 1452 1453 return false; 1454 } 1455 1456 private void snapback() { 1457 if ((mHolding & ~HOLD_DELETE) != 0) return; 1458 if (!snapToNeighborImage()) { 1459 mPositionController.snapback(); 1460 } 1461 } 1462 1463 private boolean snapToNeighborImage() { 1464 if (mFilmMode) return false; 1465 1466 Rect r = mPositionController.getPosition(0); 1467 int viewW = getWidth(); 1468 int threshold = MOVE_THRESHOLD + gapToSide(r.width(), viewW); 1469 1470 // If we have moved the picture a lot, switching. 1471 if (viewW - r.right > threshold) { 1472 return slideToNextPicture(); 1473 } else if (r.left > threshold) { 1474 return slideToPrevPicture(); 1475 } 1476 1477 return false; 1478 } 1479 1480 private boolean slideToNextPicture() { 1481 if (mNextBound <= 0) return false; 1482 switchToNextImage(); 1483 mPositionController.startHorizontalSlide(); 1484 return true; 1485 } 1486 1487 private boolean slideToPrevPicture() { 1488 if (mPrevBound >= 0) return false; 1489 switchToPrevImage(); 1490 mPositionController.startHorizontalSlide(); 1491 return true; 1492 } 1493 1494 private static int gapToSide(int imageWidth, int viewWidth) { 1495 return Math.max(0, (viewWidth - imageWidth) / 2); 1496 } 1497 1498 //////////////////////////////////////////////////////////////////////////// 1499 // Focus switching 1500 //////////////////////////////////////////////////////////////////////////// 1501 1502 private void switchToNextImage() { 1503 mModel.moveTo(mModel.getCurrentIndex() + 1); 1504 } 1505 1506 private void switchToPrevImage() { 1507 mModel.moveTo(mModel.getCurrentIndex() - 1); 1508 } 1509 1510 private void switchToFirstImage() { 1511 mModel.moveTo(0); 1512 } 1513 1514 //////////////////////////////////////////////////////////////////////////// 1515 // Opening Animation 1516 //////////////////////////////////////////////////////////////////////////// 1517 1518 public void setOpenAnimationRect(Rect rect) { 1519 mPositionController.setOpenAnimationRect(rect); 1520 } 1521 1522 //////////////////////////////////////////////////////////////////////////// 1523 // Capture Animation 1524 //////////////////////////////////////////////////////////////////////////// 1525 1526 public boolean switchWithCaptureAnimation(int offset) { 1527 GLRoot root = getGLRoot(); 1528 root.lockRenderThread(); 1529 try { 1530 return switchWithCaptureAnimationLocked(offset); 1531 } finally { 1532 root.unlockRenderThread(); 1533 } 1534 } 1535 1536 private boolean switchWithCaptureAnimationLocked(int offset) { 1537 if (mHolding != 0) return true; 1538 if (offset == 1) { 1539 if (mNextBound <= 0) return false; 1540 // Temporary disable action bar until the capture animation is done. 1541 if (!mFilmMode) mListener.onActionBarAllowed(false); 1542 switchToNextImage(); 1543 mPositionController.startCaptureAnimationSlide(-1); 1544 } else if (offset == -1) { 1545 if (mPrevBound >= 0) return false; 1546 if (mFilmMode) setFilmMode(false); 1547 1548 // If we are too far away from the first image (so that we don't 1549 // have all the ScreenNails in-between), we go directly without 1550 // animation. 1551 if (mModel.getCurrentIndex() > SCREEN_NAIL_MAX) { 1552 switchToFirstImage(); 1553 mPositionController.skipToFinalPosition(); 1554 return true; 1555 } 1556 1557 switchToFirstImage(); 1558 mPositionController.startCaptureAnimationSlide(1); 1559 } else { 1560 return false; 1561 } 1562 mHolding |= HOLD_CAPTURE_ANIMATION; 1563 Message m = mHandler.obtainMessage(MSG_CAPTURE_ANIMATION_DONE, offset, 0); 1564 mHandler.sendMessageDelayed(m, PositionController.CAPTURE_ANIMATION_TIME); 1565 return true; 1566 } 1567 1568 private void captureAnimationDone(int offset) { 1569 mHolding &= ~HOLD_CAPTURE_ANIMATION; 1570 if (offset == 1 && !mFilmMode) { 1571 // Now the capture animation is done, enable the action bar. 1572 mListener.onActionBarAllowed(true); 1573 mListener.onActionBarWanted(); 1574 } 1575 snapback(); 1576 } 1577 1578 //////////////////////////////////////////////////////////////////////////// 1579 // Card deck effect calculation 1580 //////////////////////////////////////////////////////////////////////////// 1581 1582 // Returns the scrolling progress value for an object moving out of a 1583 // view. The progress value measures how much the object has moving out of 1584 // the view. The object currently displays in [left, right), and the view is 1585 // at [0, viewWidth]. 1586 // 1587 // The returned value is negative when the object is moving right, and 1588 // positive when the object is moving left. The value goes to -1 or 1 when 1589 // the object just moves out of the view completely. The value is 0 if the 1590 // object currently fills the view. 1591 private static float calculateMoveOutProgress(int left, int right, 1592 int viewWidth) { 1593 // w = object width 1594 // viewWidth = view width 1595 int w = right - left; 1596 1597 // If the object width is smaller than the view width, 1598 // |....view....| 1599 // |<-->| progress = -1 when left = viewWidth 1600 // |<-->| progress = 0 when left = viewWidth / 2 - w / 2 1601 // |<-->| progress = 1 when left = -w 1602 if (w < viewWidth) { 1603 int zx = viewWidth / 2 - w / 2; 1604 if (left > zx) { 1605 return -(left - zx) / (float) (viewWidth - zx); // progress = (0, -1] 1606 } else { 1607 return (left - zx) / (float) (-w - zx); // progress = [0, 1] 1608 } 1609 } 1610 1611 // If the object width is larger than the view width, 1612 // |..view..| 1613 // |<--------->| progress = -1 when left = viewWidth 1614 // |<--------->| progress = 0 between left = 0 1615 // |<--------->| and right = viewWidth 1616 // |<--------->| progress = 1 when right = 0 1617 if (left > 0) { 1618 return -left / (float) viewWidth; 1619 } 1620 1621 if (right < viewWidth) { 1622 return (viewWidth - right) / (float) viewWidth; 1623 } 1624 1625 return 0; 1626 } 1627 1628 // Maps a scrolling progress value to the alpha factor in the fading 1629 // animation. 1630 private float getScrollAlpha(float scrollProgress) { 1631 return scrollProgress < 0 ? mAlphaInterpolator.getInterpolation( 1632 1 - Math.abs(scrollProgress)) : 1.0f; 1633 } 1634 1635 // Maps a scrolling progress value to the scaling factor in the fading 1636 // animation. 1637 private float getScrollScale(float scrollProgress) { 1638 float interpolatedProgress = mScaleInterpolator.getInterpolation( 1639 Math.abs(scrollProgress)); 1640 float scale = (1 - interpolatedProgress) + 1641 interpolatedProgress * TRANSITION_SCALE_FACTOR; 1642 return scale; 1643 } 1644 1645 1646 // This interpolator emulates the rate at which the perceived scale of an 1647 // object changes as its distance from a camera increases. When this 1648 // interpolator is applied to a scale animation on a view, it evokes the 1649 // sense that the object is shrinking due to moving away from the camera. 1650 private static class ZInterpolator { 1651 private float focalLength; 1652 1653 public ZInterpolator(float foc) { 1654 focalLength = foc; 1655 } 1656 1657 public float getInterpolation(float input) { 1658 return (1.0f - focalLength / (focalLength + input)) / 1659 (1.0f - focalLength / (focalLength + 1.0f)); 1660 } 1661 } 1662 1663 // Returns an interpolated value for the page/film transition. 1664 // When ratio = 0, the result is from. 1665 // When ratio = 1, the result is to. 1666 private static float interpolate(float ratio, float from, float to) { 1667 return from + (to - from) * ratio * ratio; 1668 } 1669 1670 // Returns the alpha factor in film mode if a picture is not in the center. 1671 // The 0.03 lower bound is to make the item always visible a bit. 1672 private float getOffsetAlpha(float offset) { 1673 offset /= 0.5f; 1674 float alpha = (offset > 0) ? (1 - offset) : (1 + offset); 1675 return Utils.clamp(alpha, 0.03f, 1f); 1676 } 1677 1678 //////////////////////////////////////////////////////////////////////////// 1679 // Simple public utilities 1680 //////////////////////////////////////////////////////////////////////////// 1681 1682 public void setListener(Listener listener) { 1683 mListener = listener; 1684 } 1685 1686 public Rect getPhotoRect(int index) { 1687 return mPositionController.getPosition(index); 1688 } 1689 1690 public PhotoFallbackEffect buildFallbackEffect(GLView root, GLCanvas canvas) { 1691 Rect location = new Rect(); 1692 Utils.assertTrue(root.getBoundsOf(this, location)); 1693 1694 Rect fullRect = bounds(); 1695 PhotoFallbackEffect effect = new PhotoFallbackEffect(); 1696 for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) { 1697 MediaItem item = mModel.getMediaItem(i); 1698 if (item == null) continue; 1699 ScreenNail sc = mModel.getScreenNail(i); 1700 if (!(sc instanceof BitmapScreenNail) 1701 || ((BitmapScreenNail) sc).isShowingPlaceholder()) continue; 1702 1703 // Now, sc is BitmapScreenNail and is not showing placeholder 1704 Rect rect = new Rect(getPhotoRect(i)); 1705 if (!Rect.intersects(fullRect, rect)) continue; 1706 rect.offset(location.left, location.top); 1707 1708 RawTexture texture = new RawTexture(sc.getWidth(), sc.getHeight(), true); 1709 canvas.beginRenderTarget(texture); 1710 sc.draw(canvas, 0, 0, sc.getWidth(), sc.getHeight()); 1711 canvas.endRenderTarget(); 1712 effect.addEntry(item.getPath(), rect, texture); 1713 } 1714 return effect; 1715 } 1716} 1717