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