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