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