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