PhotoView.java revision ba12eae90b5b1a80ee002aa0df8c5c5189c4faa3
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.graphics.Color; 21import android.graphics.Point; 22import android.graphics.Rect; 23import android.os.Message; 24import android.view.MotionEvent; 25import android.view.animation.AccelerateInterpolator; 26 27import com.android.gallery3d.R; 28import com.android.gallery3d.app.GalleryActivity; 29import com.android.gallery3d.common.Utils; 30import com.android.gallery3d.data.MediaObject; 31import com.android.gallery3d.util.RangeArray; 32 33import java.util.Arrays; 34 35public class PhotoView extends GLView { 36 @SuppressWarnings("unused") 37 private static final String TAG = "PhotoView"; 38 private static final int PLACEHOLDER_COLOR = 0xFF222222; 39 40 public static final int INVALID_SIZE = -1; 41 public static final long INVALID_DATA_VERSION = 42 MediaObject.INVALID_DATA_VERSION; 43 44 public static class Size { 45 public int width; 46 public int height; 47 } 48 49 public interface Model extends TileImageView.Model { 50 public int getCurrentIndex(); 51 public void moveTo(int index); 52 53 // Returns the size for the specified picture. If the size information is 54 // not avaiable, width = height = 0. 55 public void getImageSize(int offset, Size size); 56 57 // Returns the rotation for the specified picture. 58 public int getImageRotation(int offset); 59 60 // This amends the getScreenNail() method of TileImageView.Model to get 61 // ScreenNail at previous (negative offset) or next (positive offset) 62 // positions. Returns null if the specified ScreenNail is unavailable. 63 public ScreenNail getScreenNail(int offset); 64 65 // Set this to true if we need the model to provide full images. 66 public void setNeedFullImage(boolean enabled); 67 68 // Returns true if the item is the Camera preview. 69 public boolean isCamera(int offset); 70 71 // Returns true if the item is a Video. 72 public boolean isVideo(int offset); 73 } 74 75 public interface Listener { 76 public void onSingleTapUp(int x, int y); 77 public void lockOrientation(); 78 public void unlockOrientation(); 79 public void onFullScreenChanged(boolean full); 80 public void onActionBarAllowed(boolean allowed); 81 } 82 83 // Here is a graph showing the places we need to lock/unlock device 84 // orientation: 85 // 86 // +------------+ A +------------+ 87 // Page mode | Camera |<---| Photo | 88 // | [locked] |--->| [unlocked] | 89 // +------------+ B +------------+ 90 // ^ ^ 91 // | C | D 92 // +------------+ +------------+ 93 // | Camera | | Photo | 94 // Film mode | [*] | | [*] | 95 // +------------+ +------------+ 96 // 97 // In Page mode, we want to lock in Camera because we don't want the system 98 // rotation animation. We also want to unlock in Photo because we want to 99 // show the system action bar in the right place. 100 // 101 // We don't show action bar in Film mode, so it's fine for it to be locked 102 // or unlocked in Film mode. 103 // 104 // There are four transitions we need to check if we need to 105 // lock/unlock. Marked as A to D above and in the code. 106 107 private static final int MSG_SHOW_LOADING = 1; 108 private static final int MSG_CANCEL_EXTRA_SCALING = 2; 109 private static final int MSG_SWITCH_FOCUS = 3; 110 private static final int MSG_CAPTURE_ANIMATION_DONE = 4; 111 112 private static final long DELAY_SHOW_LOADING = 250; // 250ms; 113 114 private static final int LOADING_INIT = 0; 115 private static final int LOADING_TIMEOUT = 1; 116 private static final int LOADING_COMPLETE = 2; 117 private static final int LOADING_FAIL = 3; 118 119 private static final int MOVE_THRESHOLD = 256; 120 private static final float SWIPE_THRESHOLD = 300f; 121 122 private static final float DEFAULT_TEXT_SIZE = 20; 123 private static float TRANSITION_SCALE_FACTOR = 0.74f; 124 private static final int ICON_RATIO = 6; 125 126 // whether we want to apply card deck effect in page mode. 127 private static final boolean CARD_EFFECT = true; 128 129 // Used to calculate the scaling factor for the fading animation. 130 private ZInterpolator mScaleInterpolator = new ZInterpolator(0.5f); 131 132 // Used to calculate the alpha factor for the fading animation. 133 private AccelerateInterpolator mAlphaInterpolator = 134 new AccelerateInterpolator(0.9f); 135 136 // We keep this many previous ScreenNails. (also this many next ScreenNails) 137 public static final int SCREEN_NAIL_MAX = 3; 138 139 // The picture entries, the valid index is from -SCREEN_NAIL_MAX to 140 // SCREEN_NAIL_MAX. 141 private final RangeArray<Picture> mPictures = 142 new RangeArray<Picture>(-SCREEN_NAIL_MAX, SCREEN_NAIL_MAX); 143 144 private final long mDataVersion[] = new long[2 * SCREEN_NAIL_MAX + 1]; 145 private final int mFromIndex[] = new int[2 * SCREEN_NAIL_MAX + 1]; 146 147 private final MyGestureListener mGestureListener; 148 private final GestureRecognizer mGestureRecognizer; 149 private final PositionController mPositionController; 150 151 private Listener mListener; 152 private Model mModel; 153 private StringTexture mLoadingText; 154 private StringTexture mNoThumbnailText; 155 private TileImageView mTileView; 156 private EdgeView mEdgeView; 157 private Texture mVideoPlayIcon; 158 159 private ProgressSpinner mLoadingSpinner; 160 161 private SynchronizedHandler mHandler; 162 163 private int mLoadingState = LOADING_COMPLETE; 164 165 private Point mImageCenter = new Point(); 166 private boolean mCancelExtraScalingPending; 167 private boolean mFilmMode = false; 168 private int mDisplayRotation = 0; 169 private int mCompensation = 0; 170 private boolean mFullScreen = true; 171 private Rect mCameraNaturalFrame = new Rect(); 172 private Rect mCameraRect = new Rect(); 173 174 // [mPrevBound, mNextBound] is the range of index for all pictures in the 175 // model, if we assume the index of current focused picture is 0. So if 176 // there are some previous pictures, mPrevBound < 0, and if there are some 177 // next pictures, mNextBound > 0. 178 private int mPrevBound; 179 private int mNextBound; 180 181 // This variable prevents us doing snapback until its values goes to 0. This 182 // happens if the user gesture is still in progress or we are in a capture 183 // animation. 184 private int mHolding; 185 private static final int HOLD_TOUCH_DOWN = 1; 186 private static final int HOLD_CAPTURE_ANIMATION = 2; 187 188 public PhotoView(GalleryActivity activity) { 189 mTileView = new TileImageView(activity); 190 addComponent(mTileView); 191 Context context = activity.getAndroidContext(); 192 mEdgeView = new EdgeView(context); 193 addComponent(mEdgeView); 194 mLoadingSpinner = new ProgressSpinner(context); 195 mLoadingText = StringTexture.newInstance( 196 context.getString(R.string.loading), 197 DEFAULT_TEXT_SIZE, Color.WHITE); 198 mNoThumbnailText = StringTexture.newInstance( 199 context.getString(R.string.no_thumbnail), 200 DEFAULT_TEXT_SIZE, Color.WHITE); 201 202 mHandler = new MyHandler(activity.getGLRoot()); 203 204 mGestureListener = new MyGestureListener(); 205 mGestureRecognizer = new GestureRecognizer(context, mGestureListener); 206 207 mPositionController = new PositionController(context, 208 new PositionController.Listener() { 209 public void invalidate() { 210 PhotoView.this.invalidate(); 211 } 212 public boolean isHolding() { 213 return mHolding != 0; 214 } 215 public void onPull(int offset, int direction) { 216 mEdgeView.onPull(offset, direction); 217 } 218 public void onRelease() { 219 mEdgeView.onRelease(); 220 } 221 public void onAbsorb(int velocity, int direction) { 222 mEdgeView.onAbsorb(velocity, direction); 223 } 224 }); 225 mVideoPlayIcon = new ResourceTexture(context, R.drawable.ic_control_play); 226 Arrays.fill(mDataVersion, INVALID_DATA_VERSION); 227 for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) { 228 if (i == 0) { 229 mPictures.put(i, new FullPicture()); 230 } else { 231 mPictures.put(i, new ScreenNailPicture(i)); 232 } 233 } 234 } 235 236 public void setModel(Model model) { 237 mModel = model; 238 mTileView.setModel(mModel); 239 } 240 241 class MyHandler extends SynchronizedHandler { 242 public MyHandler(GLRoot root) { 243 super(root); 244 } 245 246 @Override 247 public void handleMessage(Message message) { 248 switch (message.what) { 249 case MSG_SHOW_LOADING: { 250 if (mLoadingState == LOADING_INIT) { 251 // We don't need the opening animation 252 mPositionController.setOpenAnimationRect(null); 253 254 mLoadingSpinner.startAnimation(); 255 mLoadingState = LOADING_TIMEOUT; 256 invalidate(); 257 } 258 break; 259 } 260 case MSG_CANCEL_EXTRA_SCALING: { 261 mGestureRecognizer.cancelScale(); 262 mPositionController.setExtraScalingRange(false); 263 mCancelExtraScalingPending = false; 264 break; 265 } 266 case MSG_SWITCH_FOCUS: { 267 switchFocus(); 268 break; 269 } 270 case MSG_CAPTURE_ANIMATION_DONE: { 271 // message.arg1 is the offset parameter passed to 272 // switchWithCaptureAnimation(). 273 captureAnimationDone(message.arg1); 274 break; 275 } 276 default: throw new AssertionError(message.what); 277 } 278 } 279 }; 280 281 private void updateLoadingState() { 282 // Possible transitions of mLoadingState: 283 // INIT --> TIMEOUT, COMPLETE, FAIL 284 // TIMEOUT --> COMPLETE, FAIL, INIT 285 // COMPLETE --> INIT 286 // FAIL --> INIT 287 if (mModel.getLevelCount() != 0 || mModel.getScreenNail() != null) { 288 mHandler.removeMessages(MSG_SHOW_LOADING); 289 mLoadingState = LOADING_COMPLETE; 290 } else if (mModel.isFailedToLoad()) { 291 mHandler.removeMessages(MSG_SHOW_LOADING); 292 mLoadingState = LOADING_FAIL; 293 // We don't want the opening animation after loading failure 294 mPositionController.setOpenAnimationRect(null); 295 } else if (mLoadingState != LOADING_INIT) { 296 mLoadingState = LOADING_INIT; 297 mHandler.removeMessages(MSG_SHOW_LOADING); 298 mHandler.sendEmptyMessageDelayed( 299 MSG_SHOW_LOADING, DELAY_SHOW_LOADING); 300 } 301 } 302 303 //////////////////////////////////////////////////////////////////////////// 304 // Data/Image change notifications 305 //////////////////////////////////////////////////////////////////////////// 306 307 public void notifyDataChange(long[] versions, int prevBound, int nextBound) { 308 mPrevBound = prevBound; 309 mNextBound = nextBound; 310 311 // Check if the data version actually changed. 312 boolean changed = false; 313 int N = 2 * SCREEN_NAIL_MAX + 1; 314 for (int i = 0; i < N; i++) { 315 if (versions[i] != mDataVersion[i]) { 316 changed = true; 317 break; 318 } 319 } 320 if (!changed) return; 321 322 // Create the mFromIndex array, which records the index where the picture 323 // come from. The value Integer.MAX_VALUE means it's a new picture. 324 for (int i = 0; i < N; i++) { 325 long v = versions[i]; 326 if (v == INVALID_DATA_VERSION) { 327 mFromIndex[i] = Integer.MAX_VALUE; 328 continue; 329 } 330 331 // Try to find the same version number in the old array 332 int j; 333 for (j = 0; j < N; j++) { 334 if (mDataVersion[j] == v) { 335 break; 336 } 337 } 338 mFromIndex[i] = (j < N) ? j - SCREEN_NAIL_MAX : Integer.MAX_VALUE; 339 } 340 341 // Copy the new data version 342 for (int i = 0; i < N; i++) { 343 mDataVersion[i] = versions[i]; 344 } 345 346 // Move the boxes 347 mPositionController.moveBox(mFromIndex, mPrevBound < 0, mNextBound > 0, 348 mModel.isCamera(0)); 349 350 // Update the ScreenNails. 351 for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) { 352 mPictures.get(i).reload(); 353 } 354 355 invalidate(); 356 } 357 358 public void notifyImageChange(int index) { 359 mPictures.get(index).reload(); 360 invalidate(); 361 } 362 363 @Override 364 protected void onOrient(int displayRotation, int compensation) { 365 // onLayout will be called soon. We need to change the size and rotation 366 // of the Camera ScreenNail, but we don't want it start moving because 367 // the view size will be changed soon. 368 mDisplayRotation = displayRotation; 369 mCompensation = compensation; 370 for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) { 371 Picture p = mPictures.get(i); 372 if (p.isCamera()) { 373 p.updateSize(true); 374 } 375 } 376 } 377 378 @Override 379 protected void onLayout( 380 boolean changeSize, int left, int top, int right, int bottom) { 381 mTileView.layout(left, top, right, bottom); 382 mEdgeView.layout(left, top, right, bottom); 383 updateConstrainedFrame(); 384 if (changeSize) { 385 mPositionController.setViewSize(getWidth(), getHeight()); 386 } 387 } 388 389 // Update the constrained frame due to layout change. 390 private void updateConstrainedFrame() { 391 int w = getWidth(); 392 int h = getHeight(); 393 int rotation = getCameraRotation(); 394 if (rotation % 180 != 0) { 395 int tmp = w; 396 w = h; 397 h = tmp; 398 } 399 400 int l = mCameraNaturalFrame.left; 401 int t = mCameraNaturalFrame.top; 402 int r = mCameraNaturalFrame.right; 403 int b = mCameraNaturalFrame.bottom; 404 405 switch (rotation) { 406 case 0: mCameraRect.set(l, t, r, b); break; 407 case 90: mCameraRect.set(h - b, l, h - t, r); break; 408 case 180: mCameraRect.set(w - r, h - b, w - l, h - t); break; 409 case 270: mCameraRect.set(t, w - r, b, w - l); break; 410 } 411 412 mPositionController.setConstrainedFrame(mCameraRect); 413 } 414 415 public void setCameraNaturalFrame(Rect frame) { 416 mCameraNaturalFrame.set(frame); 417 } 418 419 // Returns the rotation we need to do to the camera texture before drawing 420 // it to the canvas, assuming the camera texture is correct when the device 421 // is in its natural orientation. 422 private int getCameraRotation() { 423 return (mCompensation - mDisplayRotation + 360) % 360; 424 } 425 426 //////////////////////////////////////////////////////////////////////////// 427 // Pictures 428 //////////////////////////////////////////////////////////////////////////// 429 430 private interface Picture { 431 void reload(); 432 void draw(GLCanvas canvas, Rect r); 433 void setScreenNail(ScreenNail s); 434 boolean isCamera(); // whether the picture is a camera preview 435 void updateSize(boolean force); // called when mCompensation changes 436 }; 437 438 class FullPicture implements Picture { 439 private int mRotation; 440 private boolean mIsCamera; 441 private boolean mIsVideo; 442 private boolean mWasCameraCenter; 443 444 public void FullPicture(TileImageView tileView) { 445 mTileView = tileView; 446 } 447 448 @Override 449 public void reload() { 450 // mImageWidth and mImageHeight will get updated 451 mTileView.notifyModelInvalidated(); 452 453 mIsCamera = mModel.isCamera(0); 454 mIsVideo = mModel.isVideo(0); 455 setScreenNail(mModel.getScreenNail(0)); 456 updateSize(false); 457 updateLoadingState(); 458 } 459 460 @Override 461 public void updateSize(boolean force) { 462 if (mIsCamera) { 463 mRotation = getCameraRotation(); 464 } else { 465 mRotation = mModel.getImageRotation(0); 466 } 467 468 int w = mTileView.mImageWidth; 469 int h = mTileView.mImageHeight; 470 mPositionController.setImageSize(0, 471 getRotated(mRotation, w, h), 472 getRotated(mRotation, h, w), 473 force); 474 } 475 476 @Override 477 public void draw(GLCanvas canvas, Rect r) { 478 boolean isCenter = mPositionController.isCenter(); 479 480 if (mLoadingState == LOADING_COMPLETE) { 481 drawTileView(canvas, r); 482 } 483 renderMessage(canvas, r.centerX(), r.centerY()); 484 485 if (mIsCamera) { 486 boolean full = !mFilmMode && isCenter 487 && mPositionController.isAtMinimalScale(); 488 if (full != mFullScreen) { 489 mFullScreen = full; 490 mListener.onFullScreenChanged(full); 491 } 492 } 493 494 // We want to have the following transitions: 495 // (1) Move camera preview out of its place: switch to film mode 496 // (2) Move camera preview into its place: switch to page mode 497 // The extra mWasCenter check makes sure (1) does not apply if in 498 // page mode, we move _to_ the camera preview from another picture. 499 500 // Holdings except touch-down prevent the transitions. 501 if ((mHolding & ~HOLD_TOUCH_DOWN) != 0) return; 502 503 boolean isCameraCenter = mIsCamera && isCenter; 504 505 if (mWasCameraCenter && mIsCamera && !isCenter && !mFilmMode) { 506 // Temporary disabled to de-emphasize filmstrip. 507 // setFilmMode(true); 508 } else if (!mWasCameraCenter && isCameraCenter && mFilmMode) { 509 setFilmMode(false); 510 } 511 512 if (isCenter && !mFilmMode) { 513 if (mIsCamera) { 514 // move into camera, lock 515 mListener.lockOrientation(); // Transition A 516 } else { 517 // move out of camera, unlock 518 mListener.unlockOrientation(); // Transition B 519 } 520 } 521 522 mWasCameraCenter = isCameraCenter; 523 } 524 525 @Override 526 public void setScreenNail(ScreenNail s) { 527 mTileView.setScreenNail(s); 528 } 529 530 @Override 531 public boolean isCamera() { 532 return mIsCamera; 533 } 534 535 private void drawTileView(GLCanvas canvas, Rect r) { 536 float imageScale = mPositionController.getImageScale(); 537 int viewW = getWidth(); 538 int viewH = getHeight(); 539 float cx = r.exactCenterX(); 540 float cy = r.exactCenterY(); 541 float scale = 1f; // the scaling factor due to card effect 542 543 canvas.save(GLCanvas.SAVE_FLAG_MATRIX | GLCanvas.SAVE_FLAG_ALPHA); 544 float filmRatio = mPositionController.getFilmRatio(); 545 boolean wantsCardEffect = CARD_EFFECT && !mIsCamera 546 && filmRatio != 1f && !mPictures.get(-1).isCamera(); 547 if (wantsCardEffect) { 548 // Calculate the move-out progress value. 549 int left = r.left; 550 int right = r.right; 551 float progress = calculateMoveOutProgress(left, right, viewW); 552 progress = Utils.clamp(progress, -1f, 1f); 553 554 // We only want to apply the fading animation if the scrolling 555 // movement is to the right. 556 if (progress < 0) { 557 scale = getScrollScale(progress); 558 float alpha = getScrollAlpha(progress); 559 scale = interpolate(filmRatio, scale, 1f); 560 alpha = interpolate(filmRatio, alpha, 1f); 561 562 imageScale *= scale; 563 canvas.multiplyAlpha(alpha); 564 565 float cxPage; // the cx value in page mode 566 if (right - left <= viewW) { 567 // If the picture is narrower than the view, keep it at 568 // the center of the view. 569 cxPage = viewW / 2f; 570 } else { 571 // If the picture is wider than the view (it's 572 // zoomed-in), keep the left edge of the object align 573 // the the left edge of the view. 574 cxPage = (right - left) * scale / 2f; 575 } 576 cx = interpolate(filmRatio, cxPage, cx); 577 } 578 } 579 580 // Draw the tile view. 581 setTileViewPosition(cx, cy, viewW, viewH, imageScale); 582 PhotoView.super.render(canvas); 583 584 // Draw the play video icon. 585 if (mIsVideo) { 586 canvas.translate((int) (cx + 0.5f), (int) (cy + 0.5f)); 587 int s = (int) (scale * Math.min(r.width(), r.height()) + 0.5f); 588 drawVideoPlayIcon(canvas, s); 589 } 590 591 canvas.restore(); 592 } 593 594 // Set the position of the tile view 595 private void setTileViewPosition(float cx, float cy, 596 int viewW, int viewH, float scale) { 597 // Find out the bitmap coordinates of the center of the view 598 int imageW = mPositionController.getImageWidth(); 599 int imageH = mPositionController.getImageHeight(); 600 int centerX = (int) (imageW / 2f + (viewW / 2f - cx) / scale + 0.5f); 601 int centerY = (int) (imageH / 2f + (viewH / 2f - cy) / scale + 0.5f); 602 603 int inverseX = imageW - centerX; 604 int inverseY = imageH - centerY; 605 int x, y; 606 switch (mRotation) { 607 case 0: x = centerX; y = centerY; break; 608 case 90: x = centerY; y = inverseX; break; 609 case 180: x = inverseX; y = inverseY; break; 610 case 270: x = inverseY; y = centerX; break; 611 default: 612 throw new RuntimeException(String.valueOf(mRotation)); 613 } 614 mTileView.setPosition(x, y, scale, mRotation); 615 } 616 617 private void renderMessage(GLCanvas canvas, int x, int y) { 618 // Draw the progress spinner and the text below it 619 // 620 // (x, y) is where we put the center of the spinner. 621 // s is the size of the video play icon, and we use s to layout text 622 // because we want to keep the text at the same place when the video 623 // play icon is shown instead of the spinner. 624 int w = getWidth(); 625 int h = getHeight(); 626 int s = Math.min(w, h) / ICON_RATIO; 627 628 if (mLoadingState == LOADING_TIMEOUT) { 629 StringTexture m = mLoadingText; 630 ProgressSpinner p = mLoadingSpinner; 631 p.draw(canvas, x - p.getWidth() / 2, y - p.getHeight() / 2); 632 m.draw(canvas, x - m.getWidth() / 2, y + s / 2 + 5); 633 invalidate(); // we need to keep the spinner rotating 634 } else if (mLoadingState == LOADING_FAIL) { 635 StringTexture m = mNoThumbnailText; 636 m.draw(canvas, x - m.getWidth() / 2, y + s / 2 + 5); 637 } 638 639 // Draw a debug indicator showing which picture has focus (index == 640 // 0). 641 // canvas.fillRect(x - 10, y - 10, 20, 20, 0x80FF00FF); 642 } 643 } 644 645 private class ScreenNailPicture implements Picture { 646 private int mIndex; 647 private int mRotation; 648 private ScreenNail mScreenNail; 649 private Size mSize = new Size(); 650 private boolean mIsCamera; 651 private boolean mIsVideo; 652 653 public ScreenNailPicture(int index) { 654 mIndex = index; 655 } 656 657 @Override 658 public void reload() { 659 mIsCamera = mModel.isCamera(mIndex); 660 mIsVideo = mModel.isVideo(mIndex); 661 setScreenNail(mModel.getScreenNail(mIndex)); 662 } 663 664 @Override 665 public void draw(GLCanvas canvas, Rect r) { 666 if (mScreenNail == null) { 667 // Draw a placeholder rectange if there will be a picture in 668 // this position. 669 if (mIndex >= mPrevBound && mIndex <= mNextBound) { 670 canvas.fillRect(r.left, r.top, r.width(), r.height(), 671 PLACEHOLDER_COLOR); 672 } 673 return; 674 } 675 if (r.left >= getWidth() || r.right <= 0 || 676 r.top >= getHeight() || r.bottom <= 0) { 677 mScreenNail.noDraw(); 678 return; 679 } 680 681 if (mIsCamera && mFullScreen != false) { 682 mFullScreen = false; 683 mListener.onFullScreenChanged(false); 684 } 685 686 float filmRatio = mPositionController.getFilmRatio(); 687 boolean wantsCardEffect = CARD_EFFECT && mIndex > 0 688 && filmRatio != 1f && !mPictures.get(0).isCamera(); 689 int w = getWidth(); 690 int cx = wantsCardEffect 691 ? (int) (interpolate(filmRatio, w / 2, r.centerX()) + 0.5f) 692 : r.centerX(); 693 int cy = r.centerY(); 694 canvas.save(GLCanvas.SAVE_FLAG_MATRIX | GLCanvas.SAVE_FLAG_ALPHA); 695 canvas.translate(cx, cy); 696 if (wantsCardEffect) { 697 float progress = (float) (w / 2 - r.centerX()) / w; 698 progress = Utils.clamp(progress, -1, 1); 699 float alpha = getScrollAlpha(progress); 700 float scale = getScrollScale(progress); 701 alpha = interpolate(filmRatio, alpha, 1f); 702 scale = interpolate(filmRatio, scale, 1f); 703 canvas.multiplyAlpha(alpha); 704 canvas.scale(scale, scale, 1); 705 } 706 if (mRotation != 0) { 707 canvas.rotate(mRotation, 0, 0, 1); 708 } 709 int drawW = getRotated(mRotation, r.width(), r.height()); 710 int drawH = getRotated(mRotation, r.height(), r.width()); 711 mScreenNail.draw(canvas, -drawW / 2, -drawH / 2, drawW, drawH); 712 if (mIsVideo) drawVideoPlayIcon(canvas, Math.min(drawW, drawH)); 713 canvas.restore(); 714 } 715 716 @Override 717 public void setScreenNail(ScreenNail s) { 718 if (mScreenNail == s) return; 719 mScreenNail = s; 720 updateSize(false); 721 } 722 723 @Override 724 public void updateSize(boolean force) { 725 if (mIsCamera) { 726 mRotation = getCameraRotation(); 727 } else { 728 mRotation = mModel.getImageRotation(mIndex); 729 } 730 731 int w = 0, h = 0; 732 if (mScreenNail != null) { 733 w = mScreenNail.getWidth(); 734 h = mScreenNail.getHeight(); 735 } else if (mModel != null) { 736 // If we don't have ScreenNail available, we can still try to 737 // get the size information of it. 738 mModel.getImageSize(mIndex, mSize); 739 w = mSize.width; 740 h = mSize.height; 741 } 742 743 if (w != 0 && h != 0) { 744 mPositionController.setImageSize(mIndex, 745 getRotated(mRotation, w, h), 746 getRotated(mRotation, h, w), 747 force); 748 } 749 } 750 751 @Override 752 public boolean isCamera() { 753 return mIsCamera; 754 } 755 } 756 757 // Draw the video play icon (in the place where the spinner was) 758 private void drawVideoPlayIcon(GLCanvas canvas, int side) { 759 int s = side / ICON_RATIO; 760 // Draw the video play icon at the center 761 mVideoPlayIcon.draw(canvas, -s / 2, -s / 2, s, s); 762 } 763 764 private static int getRotated(int degree, int original, int theother) { 765 return (degree % 180 == 0) ? original : theother; 766 } 767 768 //////////////////////////////////////////////////////////////////////////// 769 // Gestures Handling 770 //////////////////////////////////////////////////////////////////////////// 771 772 @Override 773 protected boolean onTouch(MotionEvent event) { 774 mGestureRecognizer.onTouchEvent(event); 775 return true; 776 } 777 778 private class MyGestureListener implements GestureRecognizer.Listener { 779 private boolean mIgnoreUpEvent = false; 780 // If we can change mode for this scale gesture. 781 private boolean mCanChangeMode; 782 // If we have changed the film mode in this scaling gesture. 783 private boolean mModeChanged; 784 // If this scaling gesture should be ignored. 785 private boolean mIgnoreScalingGesture; 786 // whether the down action happened while the view is scrolling. 787 private boolean mDownInScrolling; 788 // If we should ignore all gestures other than onSingleTapUp. 789 private boolean mIgnoreSwipingGesture; 790 791 @Override 792 public boolean onSingleTapUp(float x, float y) { 793 // We do this in addition to onUp() because we want the snapback of 794 // setFilmMode to happen. 795 mHolding &= ~HOLD_TOUCH_DOWN; 796 797 if (mFilmMode && !mDownInScrolling) { 798 switchToHitPicture((int) (x + 0.5f), (int) (y + 0.5f)); 799 setFilmMode(false); 800 mIgnoreUpEvent = true; 801 return true; 802 } 803 804 if (mListener != null) { 805 mListener.onSingleTapUp((int) x, (int) y); 806 } 807 return true; 808 } 809 810 @Override 811 public boolean onDoubleTap(float x, float y) { 812 if (mIgnoreSwipingGesture) return true; 813 if (mPictures.get(0).isCamera()) return false; 814 PositionController controller = mPositionController; 815 float scale = controller.getImageScale(); 816 // onDoubleTap happened on the second ACTION_DOWN. 817 // We need to ignore the next UP event. 818 mIgnoreUpEvent = true; 819 if (scale <= 1.0f || controller.isAtMinimalScale()) { 820 controller.zoomIn(x, y, Math.max(1.5f, scale * 1.5f)); 821 } else { 822 controller.resetToFullView(); 823 } 824 return true; 825 } 826 827 @Override 828 public boolean onScroll(float dx, float dy) { 829 if (mIgnoreSwipingGesture) return true; 830 mPositionController.startScroll(-dx, -dy); 831 return true; 832 } 833 834 @Override 835 public boolean onFling(float velocityX, float velocityY) { 836 if (mIgnoreSwipingGesture) return true; 837 if (swipeImages(velocityX, velocityY)) { 838 mIgnoreUpEvent = true; 839 } else if (mPositionController.fling(velocityX, velocityY)) { 840 mIgnoreUpEvent = true; 841 } 842 return true; 843 } 844 845 @Override 846 public boolean onScaleBegin(float focusX, float focusY) { 847 if (mIgnoreSwipingGesture) return true; 848 // We ignore the scaling gesture if it is a camera preview. 849 mIgnoreScalingGesture = mPictures.get(0).isCamera(); 850 if (mIgnoreScalingGesture) { 851 return true; 852 } 853 mPositionController.beginScale(focusX, focusY); 854 // We can change mode if we are in film mode, or we are in page 855 // mode and at minimal scale. 856 mCanChangeMode = mFilmMode 857 || mPositionController.isAtMinimalScale(); 858 mModeChanged = false; 859 return true; 860 } 861 862 @Override 863 public boolean onScale(float focusX, float focusY, float scale) { 864 if (mIgnoreSwipingGesture) return true; 865 if (mIgnoreScalingGesture) return true; 866 if (mModeChanged) return true; 867 if (Float.isNaN(scale) || Float.isInfinite(scale)) return false; 868 869 // We wait for the scale change accumulated to a large enough change 870 // before reacting to it. Otherwise we may mistakenly treat a 871 // zoom-in gesture as zoom-out or vice versa. 872 if (scale > 0.99f && scale < 1.01f) return false; 873 874 int outOfRange = mPositionController.scaleBy(scale, focusX, focusY); 875 876 // If mode changes, we treat this scaling gesture has ended. 877 if (mCanChangeMode) { 878 if ((outOfRange < 0 && !mFilmMode) || 879 (outOfRange > 0 && mFilmMode)) { 880 stopExtraScalingIfNeeded(); 881 882 // Removing the touch down flag allows snapback to happen 883 // for film mode change. 884 mHolding &= ~HOLD_TOUCH_DOWN; 885 setFilmMode(!mFilmMode); 886 887 // We need to call onScaleEnd() before setting mModeChanged 888 // to true. 889 onScaleEnd(); 890 mModeChanged = true; 891 return true; 892 } 893 } 894 895 if (outOfRange != 0) { 896 startExtraScalingIfNeeded(); 897 } else { 898 stopExtraScalingIfNeeded(); 899 } 900 return true; 901 } 902 903 @Override 904 public void onScaleEnd() { 905 if (mIgnoreSwipingGesture) return; 906 if (mIgnoreScalingGesture) return; 907 if (mModeChanged) return; 908 mPositionController.endScale(); 909 } 910 911 private void startExtraScalingIfNeeded() { 912 if (!mCancelExtraScalingPending) { 913 mHandler.sendEmptyMessageDelayed( 914 MSG_CANCEL_EXTRA_SCALING, 700); 915 mPositionController.setExtraScalingRange(true); 916 mCancelExtraScalingPending = true; 917 } 918 } 919 920 private void stopExtraScalingIfNeeded() { 921 if (mCancelExtraScalingPending) { 922 mHandler.removeMessages(MSG_CANCEL_EXTRA_SCALING); 923 mPositionController.setExtraScalingRange(false); 924 mCancelExtraScalingPending = false; 925 } 926 } 927 928 @Override 929 public void onDown() { 930 if (mIgnoreSwipingGesture) return; 931 932 mHolding |= HOLD_TOUCH_DOWN; 933 934 if (mFilmMode && mPositionController.isScrolling()) { 935 mDownInScrolling = true; 936 mPositionController.stopScrolling(); 937 } else { 938 mDownInScrolling = false; 939 } 940 } 941 942 @Override 943 public void onUp() { 944 if (mIgnoreSwipingGesture) return; 945 946 mHolding &= ~HOLD_TOUCH_DOWN; 947 mEdgeView.onRelease(); 948 949 if (mIgnoreUpEvent) { 950 mIgnoreUpEvent = false; 951 return; 952 } 953 954 snapback(); 955 } 956 957 public void setSwipingEnabled(boolean enabled) { 958 mIgnoreSwipingGesture = !enabled; 959 } 960 } 961 962 public void setSwipingEnabled(boolean enabled) { 963 mGestureListener.setSwipingEnabled(enabled); 964 } 965 966 private void setFilmMode(boolean enabled) { 967 if (mFilmMode == enabled) return; 968 mFilmMode = enabled; 969 mPositionController.setFilmMode(mFilmMode); 970 mModel.setNeedFullImage(!enabled); 971 mListener.onActionBarAllowed(!enabled); 972 973 // If we leave filmstrip mode, we should lock/unlock 974 if (!enabled) { 975 if (mPictures.get(0).isCamera()) { 976 mListener.lockOrientation(); // Transition C 977 } else { 978 mListener.unlockOrientation(); // Transition D 979 } 980 } 981 } 982 983 public boolean getFilmMode() { 984 return mFilmMode; 985 } 986 987 //////////////////////////////////////////////////////////////////////////// 988 // Framework events 989 //////////////////////////////////////////////////////////////////////////// 990 991 public void pause() { 992 mPositionController.skipAnimation(); 993 mTileView.freeTextures(); 994 for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) { 995 mPictures.get(i).setScreenNail(null); 996 } 997 } 998 999 public void resume() { 1000 mTileView.prepareTextures(); 1001 } 1002 1003 // move to the camera preview and show controls after resume 1004 public void resetToFirstPicture() { 1005 mModel.moveTo(0); 1006 setFilmMode(false); 1007 } 1008 1009 //////////////////////////////////////////////////////////////////////////// 1010 // Rendering 1011 //////////////////////////////////////////////////////////////////////////// 1012 1013 @Override 1014 protected void render(GLCanvas canvas) { 1015 float filmRatio = mPositionController.getFilmRatio(); 1016 1017 // Draw next photos. In page mode, we draw only one next photo. 1018 int lastPhoto = (filmRatio == 0f) ? 1 : SCREEN_NAIL_MAX; 1019 for (int i = lastPhoto; i > 0; i--) { 1020 Rect r = mPositionController.getPosition(i); 1021 mPictures.get(i).draw(canvas, r); 1022 } 1023 1024 // Draw current photo 1025 mPictures.get(0).draw(canvas, mPositionController.getPosition(0)); 1026 1027 // Draw previous photos. In page mode, we draw only one previous photo. 1028 lastPhoto = (filmRatio == 0f) ? -1: -SCREEN_NAIL_MAX; 1029 for (int i = -1; i >= lastPhoto; i--) { 1030 Rect r = mPositionController.getPosition(i); 1031 mPictures.get(i).draw(canvas, r); 1032 } 1033 1034 mPositionController.advanceAnimation(); 1035 checkFocusSwitching(); 1036 } 1037 1038 //////////////////////////////////////////////////////////////////////////// 1039 // Film mode focus switching 1040 //////////////////////////////////////////////////////////////////////////// 1041 1042 // Runs in GL thread. 1043 private void checkFocusSwitching() { 1044 if (!mFilmMode) return; 1045 if (mHandler.hasMessages(MSG_SWITCH_FOCUS)) return; 1046 if (switchPosition() != 0) { 1047 mHandler.sendEmptyMessage(MSG_SWITCH_FOCUS); 1048 } 1049 } 1050 1051 // Runs in main thread. 1052 private void switchFocus() { 1053 if (mHolding != 0) return; 1054 switch (switchPosition()) { 1055 case -1: 1056 switchToPrevImage(); 1057 break; 1058 case 1: 1059 switchToNextImage(); 1060 break; 1061 } 1062 } 1063 1064 // Returns -1 if we should switch focus to the previous picture, +1 if we 1065 // should switch to the next, 0 otherwise. 1066 private int switchPosition() { 1067 Rect curr = mPositionController.getPosition(0); 1068 int center = getWidth() / 2; 1069 1070 if (curr.left > center && mPrevBound < 0) { 1071 Rect prev = mPositionController.getPosition(-1); 1072 int currDist = curr.left - center; 1073 int prevDist = center - prev.right; 1074 if (prevDist < currDist) { 1075 return -1; 1076 } 1077 } else if (curr.right < center && mNextBound > 0) { 1078 Rect next = mPositionController.getPosition(1); 1079 int currDist = center - curr.right; 1080 int nextDist = next.left - center; 1081 if (nextDist < currDist) { 1082 return 1; 1083 } 1084 } 1085 1086 return 0; 1087 } 1088 1089 // Switch to the previous or next picture if the hit position is inside 1090 // one of their boxes. This runs in main thread. 1091 private void switchToHitPicture(int x, int y) { 1092 if (mPrevBound < 0) { 1093 Rect r = mPositionController.getPosition(-1); 1094 if (r.right >= x) { 1095 slideToPrevPicture(); 1096 return; 1097 } 1098 } 1099 1100 if (mNextBound > 0) { 1101 Rect r = mPositionController.getPosition(1); 1102 if (r.left <= x) { 1103 slideToNextPicture(); 1104 return; 1105 } 1106 } 1107 } 1108 1109 //////////////////////////////////////////////////////////////////////////// 1110 // Page mode focus switching 1111 // 1112 // We slide image to the next one or the previous one in two cases: 1: If 1113 // the user did a fling gesture with enough velocity. 2 If the user has 1114 // moved the picture a lot. 1115 //////////////////////////////////////////////////////////////////////////// 1116 1117 private boolean swipeImages(float velocityX, float velocityY) { 1118 if (mFilmMode) return false; 1119 1120 // Avoid swiping images if we're possibly flinging to view the 1121 // zoomed in picture vertically. 1122 PositionController controller = mPositionController; 1123 boolean isMinimal = controller.isAtMinimalScale(); 1124 int edges = controller.getImageAtEdges(); 1125 if (!isMinimal && Math.abs(velocityY) > Math.abs(velocityX)) 1126 if ((edges & PositionController.IMAGE_AT_TOP_EDGE) == 0 1127 || (edges & PositionController.IMAGE_AT_BOTTOM_EDGE) == 0) 1128 return false; 1129 1130 // If we are at the edge of the current photo and the sweeping velocity 1131 // exceeds the threshold, slide to the next / previous image. 1132 if (velocityX < -SWIPE_THRESHOLD && (isMinimal 1133 || (edges & PositionController.IMAGE_AT_RIGHT_EDGE) != 0)) { 1134 return slideToNextPicture(); 1135 } else if (velocityX > SWIPE_THRESHOLD && (isMinimal 1136 || (edges & PositionController.IMAGE_AT_LEFT_EDGE) != 0)) { 1137 return slideToPrevPicture(); 1138 } 1139 1140 return false; 1141 } 1142 1143 private void snapback() { 1144 if (mHolding != 0) return; 1145 if (!snapToNeighborImage()) { 1146 mPositionController.snapback(); 1147 } 1148 } 1149 1150 private boolean snapToNeighborImage() { 1151 if (mFilmMode) return false; 1152 1153 Rect r = mPositionController.getPosition(0); 1154 int viewW = getWidth(); 1155 int threshold = MOVE_THRESHOLD + gapToSide(r.width(), viewW); 1156 1157 // If we have moved the picture a lot, switching. 1158 if (viewW - r.right > threshold) { 1159 return slideToNextPicture(); 1160 } else if (r.left > threshold) { 1161 return slideToPrevPicture(); 1162 } 1163 1164 return false; 1165 } 1166 1167 private boolean slideToNextPicture() { 1168 if (mNextBound <= 0) return false; 1169 switchToNextImage(); 1170 mPositionController.startHorizontalSlide(); 1171 return true; 1172 } 1173 1174 private boolean slideToPrevPicture() { 1175 if (mPrevBound >= 0) return false; 1176 switchToPrevImage(); 1177 mPositionController.startHorizontalSlide(); 1178 return true; 1179 } 1180 1181 private static int gapToSide(int imageWidth, int viewWidth) { 1182 return Math.max(0, (viewWidth - imageWidth) / 2); 1183 } 1184 1185 //////////////////////////////////////////////////////////////////////////// 1186 // Focus switching 1187 //////////////////////////////////////////////////////////////////////////// 1188 1189 private void switchToNextImage() { 1190 mModel.moveTo(mModel.getCurrentIndex() + 1); 1191 } 1192 1193 private void switchToPrevImage() { 1194 mModel.moveTo(mModel.getCurrentIndex() - 1); 1195 } 1196 1197 private void switchToFirstImage() { 1198 mModel.moveTo(0); 1199 } 1200 1201 //////////////////////////////////////////////////////////////////////////// 1202 // Opening Animation 1203 //////////////////////////////////////////////////////////////////////////// 1204 1205 public void setOpenAnimationRect(Rect rect) { 1206 mPositionController.setOpenAnimationRect(rect); 1207 } 1208 1209 //////////////////////////////////////////////////////////////////////////// 1210 // Capture Animation 1211 //////////////////////////////////////////////////////////////////////////// 1212 1213 public boolean switchWithCaptureAnimation(int offset) { 1214 GLRoot root = getGLRoot(); 1215 root.lockRenderThread(); 1216 try { 1217 return switchWithCaptureAnimationLocked(offset); 1218 } finally { 1219 root.unlockRenderThread(); 1220 } 1221 } 1222 1223 private boolean switchWithCaptureAnimationLocked(int offset) { 1224 if (mHolding != 0) return true; 1225 if (offset == 1) { 1226 if (mNextBound <= 0) return false; 1227 // Temporary disable action bar until the capture animation is done. 1228 if (!mFilmMode) mListener.onActionBarAllowed(false); 1229 switchToNextImage(); 1230 mPositionController.startCaptureAnimationSlide(-1); 1231 } else if (offset == -1) { 1232 if (mPrevBound >= 0) return false; 1233 switchToFirstImage(); 1234 mPositionController.startCaptureAnimationSlide(1); 1235 } else { 1236 return false; 1237 } 1238 mHolding |= HOLD_CAPTURE_ANIMATION; 1239 Message m = mHandler.obtainMessage(MSG_CAPTURE_ANIMATION_DONE, offset, 0); 1240 mHandler.sendMessageDelayed(m, 800); 1241 return true; 1242 } 1243 1244 private void captureAnimationDone(int offset) { 1245 mHolding &= ~HOLD_CAPTURE_ANIMATION; 1246 if (offset == 1) { 1247 // move out of camera, unlock 1248 if (!mFilmMode) { 1249 // Now the capture animation is done, enable the action bar. 1250 mListener.onActionBarAllowed(true); 1251 } 1252 } 1253 snapback(); 1254 } 1255 1256 //////////////////////////////////////////////////////////////////////////// 1257 // Card deck effect calculation 1258 //////////////////////////////////////////////////////////////////////////// 1259 1260 // Returns the scrolling progress value for an object moving out of a 1261 // view. The progress value measures how much the object has moving out of 1262 // the view. The object currently displays in [left, right), and the view is 1263 // at [0, viewWidth]. 1264 // 1265 // The returned value is negative when the object is moving right, and 1266 // positive when the object is moving left. The value goes to -1 or 1 when 1267 // the object just moves out of the view completely. The value is 0 if the 1268 // object currently fills the view. 1269 private static float calculateMoveOutProgress(int left, int right, 1270 int viewWidth) { 1271 // w = object width 1272 // viewWidth = view width 1273 int w = right - left; 1274 1275 // If the object width is smaller than the view width, 1276 // |....view....| 1277 // |<-->| progress = -1 when left = viewWidth 1278 // |<-->| progress = 0 when left = viewWidth / 2 - w / 2 1279 // |<-->| progress = 1 when left = -w 1280 if (w < viewWidth) { 1281 int zx = viewWidth / 2 - w / 2; 1282 if (left > zx) { 1283 return -(left - zx) / (float) (viewWidth - zx); // progress = (0, -1] 1284 } else { 1285 return (left - zx) / (float) (-w - zx); // progress = [0, 1] 1286 } 1287 } 1288 1289 // If the object width is larger than the view width, 1290 // |..view..| 1291 // |<--------->| progress = -1 when left = viewWidth 1292 // |<--------->| progress = 0 between left = 0 1293 // |<--------->| and right = viewWidth 1294 // |<--------->| progress = 1 when right = 0 1295 if (left > 0) { 1296 return -left / (float) viewWidth; 1297 } 1298 1299 if (right < viewWidth) { 1300 return (viewWidth - right) / (float) viewWidth; 1301 } 1302 1303 return 0; 1304 } 1305 1306 // Maps a scrolling progress value to the alpha factor in the fading 1307 // animation. 1308 private float getScrollAlpha(float scrollProgress) { 1309 return scrollProgress < 0 ? mAlphaInterpolator.getInterpolation( 1310 1 - Math.abs(scrollProgress)) : 1.0f; 1311 } 1312 1313 // Maps a scrolling progress value to the scaling factor in the fading 1314 // animation. 1315 private float getScrollScale(float scrollProgress) { 1316 float interpolatedProgress = mScaleInterpolator.getInterpolation( 1317 Math.abs(scrollProgress)); 1318 float scale = (1 - interpolatedProgress) + 1319 interpolatedProgress * TRANSITION_SCALE_FACTOR; 1320 return scale; 1321 } 1322 1323 1324 // This interpolator emulates the rate at which the perceived scale of an 1325 // object changes as its distance from a camera increases. When this 1326 // interpolator is applied to a scale animation on a view, it evokes the 1327 // sense that the object is shrinking due to moving away from the camera. 1328 private static class ZInterpolator { 1329 private float focalLength; 1330 1331 public ZInterpolator(float foc) { 1332 focalLength = foc; 1333 } 1334 1335 public float getInterpolation(float input) { 1336 return (1.0f - focalLength / (focalLength + input)) / 1337 (1.0f - focalLength / (focalLength + 1.0f)); 1338 } 1339 } 1340 1341 // Returns an interpolated value for the page/film transition. 1342 // When ratio = 0, the result is from. 1343 // When ratio = 1, the result is to. 1344 private static float interpolate(float ratio, float from, float to) { 1345 return from + (to - from) * ratio * ratio; 1346 } 1347 1348 //////////////////////////////////////////////////////////////////////////// 1349 // Simple public utilities 1350 //////////////////////////////////////////////////////////////////////////// 1351 1352 public void setListener(Listener listener) { 1353 mListener = listener; 1354 } 1355} 1356