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