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