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