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