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