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