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