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