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