Home | History | Annotate | Download | only in phototable
      1 /*
      2  * Copyright (C) 2012 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 package com.android.dreams.phototable;
     17 
     18 import android.content.Context;
     19 import android.content.res.Resources;
     20 import android.graphics.Bitmap;
     21 import android.graphics.BitmapFactory;
     22 import android.graphics.PointF;
     23 import android.graphics.PorterDuff;
     24 import android.graphics.Rect;
     25 import android.graphics.drawable.BitmapDrawable;
     26 import android.graphics.drawable.Drawable;
     27 import android.graphics.drawable.LayerDrawable;
     28 import android.os.AsyncTask;
     29 import android.service.dreams.DreamService;
     30 import android.util.AttributeSet;
     31 import android.util.Log;
     32 import android.view.KeyEvent;
     33 import android.view.LayoutInflater;
     34 import android.view.MotionEvent;
     35 import android.view.View;
     36 import android.view.ViewGroup;
     37 import android.view.ViewPropertyAnimator;
     38 import android.view.animation.DecelerateInterpolator;
     39 import android.view.animation.Interpolator;
     40 import android.widget.FrameLayout;
     41 import android.widget.ImageView;
     42 
     43 import java.util.ArrayList;
     44 import java.util.Formatter;
     45 import java.util.HashSet;
     46 import java.util.LinkedList;
     47 import java.util.List;
     48 import java.util.Random;
     49 import java.util.Set;
     50 
     51 /**
     52  * A surface where photos sit.
     53  */
     54 public class PhotoTable extends FrameLayout {
     55     private static final String TAG = "PhotoTable";
     56     private static final boolean DEBUG = false;
     57 
     58     class Launcher implements Runnable {
     59         @Override
     60         public void run() {
     61             PhotoTable.this.scheduleNext(mDropPeriod);
     62             PhotoTable.this.launch();
     63         }
     64     }
     65 
     66     class FocusReaper implements Runnable {
     67         @Override
     68         public void run() {
     69             PhotoTable.this.clearFocus();
     70         }
     71     }
     72 
     73     class SelectionReaper implements Runnable {
     74         @Override
     75         public void run() {
     76             PhotoTable.this.clearSelection();
     77         }
     78     }
     79 
     80     private static final int NEXT = 1;
     81     private static final int PREV = 0;
     82     private static Random sRNG = new Random();
     83 
     84     private final Launcher mLauncher;
     85     private final FocusReaper mFocusReaper;
     86     private final SelectionReaper mSelectionReaper;
     87     private final LinkedList<View> mOnTable;
     88     private final int mDropPeriod;
     89     private final int mFastDropPeriod;
     90     private final int mNowDropDelay;
     91     private final float mImageRatio;
     92     private final float mTableRatio;
     93     private final float mImageRotationLimit;
     94     private final float mThrowRotation;
     95     private final float mThrowSpeed;
     96     private final boolean mTapToExit;
     97     private final int mTableCapacity;
     98     private final int mRedealCount;
     99     private final int mInset;
    100     private final PhotoSource mPhotoSource;
    101     private final Resources mResources;
    102     private final Interpolator mThrowInterpolator;
    103     private final Interpolator mDropInterpolator;
    104     private final DragGestureDetector mDragGestureDetector;
    105     private final EdgeSwipeDetector mEdgeSwipeDetector;
    106     private final KeyboardInterpreter mKeyboardInterpreter;
    107     private final boolean mStoryModeEnabled;
    108     private final long mPickUpDuration;
    109     private final int mMaxSelectionTime;
    110     private final int mMaxFocusTime;
    111     private final List<View> mAnimating;
    112 
    113     private DreamService mDream;
    114     private PhotoLaunchTask mPhotoLaunchTask;
    115     private LoadNaturalSiblingTask mLoadOnDeckTasks[];
    116     private boolean mStarted;
    117     private boolean mIsLandscape;
    118     private int mLongSide;
    119     private int mShortSide;
    120     private int mWidth;
    121     private int mHeight;
    122     private View mSelection;
    123     private View mOnDeck[];
    124     private View mFocus;
    125     private int mHighlightColor;
    126     private ViewGroup mBackground;
    127     private ViewGroup mStageLeft;
    128 
    129     public PhotoTable(Context context, AttributeSet as) {
    130         super(context, as);
    131         mResources = getResources();
    132         mInset = mResources.getDimensionPixelSize(R.dimen.photo_inset);
    133         mDropPeriod = mResources.getInteger(R.integer.table_drop_period);
    134         mFastDropPeriod = mResources.getInteger(R.integer.fast_drop);
    135         mNowDropDelay = mResources.getInteger(R.integer.now_drop);
    136         mImageRatio = mResources.getInteger(R.integer.image_ratio) / 1000000f;
    137         mTableRatio = mResources.getInteger(R.integer.table_ratio) / 1000000f;
    138         mImageRotationLimit = (float) mResources.getInteger(R.integer.max_image_rotation);
    139         mThrowSpeed = mResources.getDimension(R.dimen.image_throw_speed);
    140         mPickUpDuration = mResources.getInteger(R.integer.photo_pickup_duration);
    141         mThrowRotation = (float) mResources.getInteger(R.integer.image_throw_rotatioan);
    142         mTableCapacity = mResources.getInteger(R.integer.table_capacity);
    143         mRedealCount = mResources.getInteger(R.integer.redeal_count);
    144         mTapToExit = mResources.getBoolean(R.bool.enable_tap_to_exit);
    145         mStoryModeEnabled = mResources.getBoolean(R.bool.enable_story_mode);
    146         mHighlightColor = mResources.getColor(R.color.highlight_color);
    147         mMaxSelectionTime = mResources.getInteger(R.integer.max_selection_time);
    148         mMaxFocusTime = mResources.getInteger(R.integer.max_focus_time);
    149         mThrowInterpolator = new SoftLandingInterpolator(
    150                 mResources.getInteger(R.integer.soft_landing_time) / 1000000f,
    151                 mResources.getInteger(R.integer.soft_landing_distance) / 1000000f);
    152         mDropInterpolator = new DecelerateInterpolator(
    153                 (float) mResources.getInteger(R.integer.drop_deceleration_exponent));
    154         mOnTable = new LinkedList<View>();
    155         mPhotoSource = new PhotoSourcePlexor(getContext(),
    156                 getContext().getSharedPreferences(PhotoTableDreamSettings.PREFS_NAME, 0));
    157         mAnimating = new ArrayList<View>();
    158         mLauncher = new Launcher();
    159         mFocusReaper = new FocusReaper();
    160         mSelectionReaper = new SelectionReaper();
    161         mDragGestureDetector = new DragGestureDetector(context, this);
    162         mEdgeSwipeDetector = new EdgeSwipeDetector(context, this);
    163         mKeyboardInterpreter = new KeyboardInterpreter(this);
    164         mLoadOnDeckTasks = new LoadNaturalSiblingTask[2];
    165         mOnDeck = new View[2];
    166         mStarted = false;
    167     }
    168 
    169     @Override
    170     public void onFinishInflate() {
    171         mBackground = (ViewGroup) findViewById(R.id.background);
    172         mStageLeft = (ViewGroup) findViewById(R.id.stageleft);
    173     }
    174 
    175     public void setDream(DreamService dream) {
    176         mDream = dream;
    177     }
    178 
    179     public boolean hasSelection() {
    180         return mSelection != null;
    181     }
    182 
    183     public View getSelection() {
    184         return mSelection;
    185     }
    186 
    187     public void clearSelection() {
    188         if (hasSelection()) {
    189             dropOnTable(mSelection);
    190             mPhotoSource.donePaging(getBitmap(mSelection));
    191             if (mStoryModeEnabled) {
    192                 fadeInBackground(mSelection);
    193             }
    194             mSelection = null;
    195         }
    196         for (int slot = 0; slot < mOnDeck.length; slot++) {
    197             if (mOnDeck[slot] != null) {
    198                 fadeAway(mOnDeck[slot], false);
    199                 mOnDeck[slot] = null;
    200             }
    201             if (mLoadOnDeckTasks[slot] != null &&
    202                     mLoadOnDeckTasks[slot].getStatus() != AsyncTask.Status.FINISHED) {
    203                 mLoadOnDeckTasks[slot].cancel(true);
    204                 mLoadOnDeckTasks[slot] = null;
    205             }
    206         }
    207     }
    208 
    209     public void setSelection(View selected) {
    210         if (selected != null) {
    211             clearSelection();
    212             mSelection = selected;
    213             promoteSelection();
    214             if (mStoryModeEnabled) {
    215                 fadeOutBackground(mSelection);
    216             }
    217         }
    218     }
    219 
    220     public void selectNext() {
    221         if (mStoryModeEnabled) {
    222             log("selectNext");
    223             if (hasSelection() && mOnDeck[NEXT] != null) {
    224                 placeOnDeck(mSelection, PREV);
    225                 mSelection = mOnDeck[NEXT];
    226                 mOnDeck[NEXT] = null;
    227                 promoteSelection();
    228             }
    229         } else {
    230             clearSelection();
    231         }
    232     }
    233 
    234     public void selectPrevious() {
    235         if (mStoryModeEnabled) {
    236             log("selectPrevious");
    237             if (hasSelection() && mOnDeck[PREV] != null) {
    238                 placeOnDeck(mSelection, NEXT);
    239                 mSelection = mOnDeck[PREV];
    240                 mOnDeck[PREV] = null;
    241                 promoteSelection();
    242             }
    243         } else {
    244             clearSelection();
    245         }
    246     }
    247 
    248     private void promoteSelection() {
    249         if (hasSelection()) {
    250             scheduleSelectionReaper(mMaxSelectionTime);
    251             mSelection.animate().cancel();
    252             mSelection.setAlpha(1f);
    253             moveToTopOfPile(mSelection);
    254             pickUp(mSelection);
    255             if (mStoryModeEnabled) {
    256                 for (int slot = 0; slot < mOnDeck.length; slot++) {
    257                     if (mLoadOnDeckTasks[slot] != null &&
    258                             mLoadOnDeckTasks[slot].getStatus() != AsyncTask.Status.FINISHED) {
    259                         mLoadOnDeckTasks[slot].cancel(true);
    260                     }
    261                     if (mOnDeck[slot] == null) {
    262                         mLoadOnDeckTasks[slot] = new LoadNaturalSiblingTask(slot);
    263                         mLoadOnDeckTasks[slot].execute(mSelection);
    264                     }
    265                 }
    266             }
    267         }
    268     }
    269 
    270     public boolean hasFocus() {
    271         return mFocus != null;
    272     }
    273 
    274     public View getFocus() {
    275         return mFocus;
    276     }
    277 
    278     public void clearFocus() {
    279         if (hasFocus()) {
    280             setHighlight(getFocus(), false);
    281         }
    282         mFocus = null;
    283     }
    284 
    285     public void setDefaultFocus() {
    286         setFocus(mOnTable.getLast());
    287     }
    288 
    289     public void setFocus(View focus) {
    290         assert(focus != null);
    291         clearFocus();
    292         mFocus = focus;
    293         moveToTopOfPile(focus);
    294         setHighlight(focus, true);
    295         scheduleFocusReaper(mMaxFocusTime);
    296     }
    297 
    298     static float lerp(float a, float b, float f) {
    299         return (b-a)*f + a;
    300     }
    301 
    302     static float randfrange(float a, float b) {
    303         return lerp(a, b, sRNG.nextFloat());
    304     }
    305 
    306     static PointF randFromCurve(float t, PointF[] v) {
    307         PointF p = new PointF();
    308         if (v.length == 4 && t >= 0f && t <= 1f) {
    309             float a = (float) Math.pow(1f-t, 3f);
    310             float b = (float) Math.pow(1f-t, 2f) * t;
    311             float c = (1f-t) * (float) Math.pow(t, 2f);
    312             float d = (float) Math.pow(t, 3f);
    313 
    314             p.x = a * v[0].x + 3 * b * v[1].x + 3 * c * v[2].x + d * v[3].x;
    315             p.y = a * v[0].y + 3 * b * v[1].y + 3 * c * v[2].y + d * v[3].y;
    316         }
    317         return p;
    318     }
    319 
    320     private static PointF randMultiDrop(int n, float i, float j, int width, int height) {
    321         log("randMultiDrop (%d, %f, %f, %d, %d)", n, i, j, width, height);
    322         final float[] cx = {0.3f, 0.3f, 0.5f, 0.7f, 0.7f};
    323         final float[] cy = {0.3f, 0.7f, 0.5f, 0.3f, 0.7f};
    324         n = Math.abs(n);
    325         float x = cx[n % cx.length];
    326         float y = cy[n % cx.length];
    327         PointF p = new PointF();
    328         p.x = x * width + 0.05f * width * i;
    329         p.y = y * height + 0.05f * height * j;
    330         log("randInCenter returning %f, %f", p.x, p.y);
    331         return p;
    332     }
    333 
    334     private double cross(double[] a, double[] b) {
    335         return a[0] * b[1] - a[1] * b[0];
    336     }
    337 
    338     private double norm(double[] a) {
    339         return Math.hypot(a[0], a[1]);
    340     }
    341 
    342     private double[] getCenter(View photo) {
    343         float width = (float) ((Integer) photo.getTag(R.id.photo_width)).intValue();
    344         float height = (float) ((Integer) photo.getTag(R.id.photo_height)).intValue();
    345         double[] center = { photo.getX() + width / 2f,
    346                             - (photo.getY() + height / 2f) };
    347         return center;
    348     }
    349 
    350     public View moveFocus(View focus, float direction) {
    351         return moveFocus(focus, direction, 90f);
    352     }
    353 
    354     public View moveFocus(View focus, float direction, float angle) {
    355         if (focus == null) {
    356             setFocus(mOnTable.getLast());
    357         } else {
    358             final double alpha = Math.toRadians(direction);
    359             final double beta = Math.toRadians(Math.min(angle, 180f) / 2f);
    360             final double[] left = { Math.sin(alpha - beta),
    361                                     Math.cos(alpha - beta) };
    362             final double[] right = { Math.sin(alpha + beta),
    363                                      Math.cos(alpha + beta) };
    364             final double[] a = getCenter(focus);
    365             View bestFocus = null;
    366             double bestDistance = Double.MAX_VALUE;
    367             for (View candidate: mOnTable) {
    368                 if (candidate != focus) {
    369                     final double[] b = getCenter(candidate);
    370                     final double[] delta = { b[0] - a[0],
    371                                              b[1] - a[1] };
    372                     if (cross(delta, left) > 0.0 && cross(delta, right) < 0.0) {
    373                         final double distance = norm(delta);
    374                         if (bestDistance > distance) {
    375                             bestDistance = distance;
    376                             bestFocus = candidate;
    377                         }
    378                     }
    379                 }
    380             }
    381             if (bestFocus == null) {
    382                 if (angle < 180f) {
    383                     return moveFocus(focus, direction, 180f);
    384                 }
    385             } else {
    386                 setFocus(bestFocus);
    387             }
    388         }
    389         return getFocus();
    390     }
    391 
    392     @Override
    393     public boolean onKeyDown(int keyCode, KeyEvent event) {
    394         return mKeyboardInterpreter.onKeyDown(keyCode, event);
    395     }
    396 
    397     @Override
    398     public boolean onGenericMotionEvent(MotionEvent event) {
    399         return mEdgeSwipeDetector.onTouchEvent(event) || mDragGestureDetector.onTouchEvent(event);
    400     }
    401 
    402     @Override
    403     public boolean onTouchEvent(MotionEvent event) {
    404         if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
    405             if (hasSelection()) {
    406                 clearSelection();
    407             } else  {
    408                 if (mTapToExit && mDream != null) {
    409                     mDream.finish();
    410                 }
    411             }
    412             return true;
    413         }
    414         return false;
    415     }
    416 
    417     @Override
    418     public void onLayout(boolean changed, int left, int top, int right, int bottom) {
    419         super.onLayout(changed, left, top, right, bottom);
    420         log("onLayout (%d, %d, %d, %d)", left, top, right, bottom);
    421 
    422         mHeight = bottom - top;
    423         mWidth = right - left;
    424 
    425         mLongSide = (int) (mImageRatio * Math.max(mWidth, mHeight));
    426         mShortSide = (int) (mImageRatio * Math.min(mWidth, mHeight));
    427 
    428         boolean isLandscape = mWidth > mHeight;
    429         if (mIsLandscape != isLandscape) {
    430             for (View photo: mOnTable) {
    431                 if (photo != getSelection()) {
    432                     dropOnTable(photo);
    433                 }
    434             }
    435             if (hasSelection()) {
    436                 pickUp(getSelection());
    437                 for (int slot = 0; slot < mOnDeck.length; slot++) {
    438                     if (mOnDeck[slot] != null) {
    439                         placeOnDeck(mOnDeck[slot], slot);
    440                     }
    441                 }
    442             }
    443             mIsLandscape = isLandscape;
    444         }
    445         start();
    446     }
    447 
    448     @Override
    449     public boolean isOpaque() {
    450         return true;
    451     }
    452 
    453     /** Put a nice border on the bitmap. */
    454     private static View applyFrame(final PhotoTable table, final BitmapFactory.Options options,
    455             Bitmap decodedPhoto) {
    456         LayoutInflater inflater = (LayoutInflater) table.getContext()
    457             .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    458         View photo = inflater.inflate(R.layout.photo, null);
    459         ImageView image = (ImageView) photo;
    460         Drawable[] layers = new Drawable[2];
    461         int photoWidth = options.outWidth;
    462         int photoHeight = options.outHeight;
    463         if (decodedPhoto == null || options.outWidth <= 0 || options.outHeight <= 0) {
    464             photo = null;
    465         } else {
    466             decodedPhoto.setHasMipMap(true);
    467             layers[0] = new BitmapDrawable(table.mResources, decodedPhoto);
    468             layers[1] = table.mResources.getDrawable(R.drawable.frame);
    469             LayerDrawable layerList = new LayerDrawable(layers);
    470             layerList.setLayerInset(0, table.mInset, table.mInset,
    471                                     table.mInset, table.mInset);
    472             image.setImageDrawable(layerList);
    473 
    474             photo.setTag(R.id.photo_width, Integer.valueOf(photoWidth));
    475             photo.setTag(R.id.photo_height, Integer.valueOf(photoHeight));
    476 
    477             photo.setOnTouchListener(new PhotoTouchListener(table.getContext(),
    478                                                             table));
    479         }
    480         return photo;
    481     }
    482 
    483     private class LoadNaturalSiblingTask extends AsyncTask<View, Void, View> {
    484         private final BitmapFactory.Options mOptions;
    485         private final int mSlot;
    486         private View mParent;
    487 
    488         public LoadNaturalSiblingTask (int slot) {
    489             mOptions = new BitmapFactory.Options();
    490             mOptions.inTempStorage = new byte[32768];
    491             mSlot = slot;
    492         }
    493 
    494         @Override
    495         public View doInBackground(View... views) {
    496             log("load natural %s", (mSlot == NEXT ? "next" : "previous"));
    497             final PhotoTable table = PhotoTable.this;
    498             mParent = views[0];
    499             final Bitmap current = getBitmap(mParent);
    500             Bitmap decodedPhoto;
    501             if (mSlot == NEXT) {
    502                 decodedPhoto = table.mPhotoSource.naturalNext(current,
    503                     mOptions, table.mLongSide, table.mShortSide);
    504             } else {
    505                 decodedPhoto = table.mPhotoSource.naturalPrevious(current,
    506                     mOptions, table.mLongSide, table.mShortSide);
    507             }
    508             return applyFrame(PhotoTable.this, mOptions, decodedPhoto);
    509         }
    510 
    511         @Override
    512         public void onPostExecute(View photo) {
    513             if (photo != null) {
    514                 if (hasSelection() && getSelection() == mParent) {
    515                     log("natural %s being rendered", (mSlot == NEXT ? "next" : "previous"));
    516                     PhotoTable.this.addView(photo, new LayoutParams(LayoutParams.WRAP_CONTENT,
    517                             LayoutParams.WRAP_CONTENT));
    518                     PhotoTable.this.mOnDeck[mSlot] = photo;
    519                     float width = (float) ((Integer) photo.getTag(R.id.photo_width)).intValue();
    520                     float height = (float) ((Integer) photo.getTag(R.id.photo_height)).intValue();
    521                     photo.setX(mSlot == PREV ? -2 * width : mWidth + 2 * width);
    522                     photo.setY((mHeight - height) / 2);
    523                     photo.addOnLayoutChangeListener(new OnLayoutChangeListener() {
    524                         @Override
    525                         public void onLayoutChange(View v, int left, int top, int right, int bottom,
    526                                 int oldLeft, int oldTop, int oldRight, int oldBottom) {
    527                             PhotoTable.this.placeOnDeck(v, mSlot);
    528                             v.removeOnLayoutChangeListener(this);
    529                         }
    530                     });
    531                 } else {
    532                    recycle(photo);
    533                 }
    534             } else {
    535                 log("natural, %s was null!", (mSlot == NEXT ? "next" : "previous"));
    536             }
    537         }
    538     };
    539 
    540     private class PhotoLaunchTask extends AsyncTask<Void, Void, View> {
    541         private final BitmapFactory.Options mOptions;
    542 
    543         public PhotoLaunchTask () {
    544             mOptions = new BitmapFactory.Options();
    545             mOptions.inTempStorage = new byte[32768];
    546         }
    547 
    548         @Override
    549         public View doInBackground(Void... unused) {
    550             log("load a new photo");
    551             final PhotoTable table = PhotoTable.this;
    552             return applyFrame(PhotoTable.this, mOptions,
    553                  table.mPhotoSource.next(mOptions,
    554                       table.mLongSide, table.mShortSide));
    555         }
    556 
    557         @Override
    558         public void onPostExecute(View photo) {
    559             if (photo != null) {
    560                 final PhotoTable table = PhotoTable.this;
    561 
    562                 table.addView(photo, new LayoutParams(LayoutParams.WRAP_CONTENT,
    563                     LayoutParams.WRAP_CONTENT));
    564                 if (table.hasSelection()) {
    565                     for (int slot = 0; slot < mOnDeck.length; slot++) {
    566                         if (mOnDeck[slot] != null) {
    567                             table.moveToTopOfPile(mOnDeck[slot]);
    568                         }
    569                     }
    570                     table.moveToTopOfPile(table.getSelection());
    571                 }
    572 
    573                 log("drop it");
    574                 table.throwOnTable(photo);
    575 
    576                 if (mOnTable.size() > mTableCapacity) {
    577                     int targetSize = Math.max(0, mOnTable.size() - mRedealCount);
    578                     while (mOnTable.size() > targetSize) {
    579                         fadeAway(mOnTable.poll(), false);
    580                     }
    581                 }
    582 
    583                 if(table.mOnTable.size() < table.mTableCapacity) {
    584                     table.scheduleNext(table.mFastDropPeriod);
    585                 }
    586             }
    587         }
    588     };
    589 
    590     /** Bring a new photo onto the table. */
    591     public void launch() {
    592         log("launching");
    593         setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
    594         if (!hasSelection()) {
    595             log("inflate it");
    596             if (mPhotoLaunchTask == null ||
    597                 mPhotoLaunchTask.getStatus() == AsyncTask.Status.FINISHED) {
    598                 mPhotoLaunchTask = new PhotoLaunchTask();
    599                 mPhotoLaunchTask.execute();
    600             }
    601         }
    602     }
    603 
    604     /** De-emphasize the other photos on the table. */
    605     public void fadeOutBackground(final View photo) {
    606         mBackground.animate()
    607         .withLayer()
    608         .setDuration(mPickUpDuration)
    609         .alpha(0f);
    610     }
    611 
    612 
    613     /** Return the other photos to foreground status. */
    614     public void fadeInBackground(final View photo) {
    615         mAnimating.add(photo);
    616         mBackground.animate()
    617         .withLayer()
    618         .setDuration(mPickUpDuration)
    619         .alpha(1f)
    620         .withEndAction(new Runnable() {
    621             @Override
    622             public void run() {
    623                 mAnimating.remove(photo);
    624                 if (!mAnimating.contains(photo)) {
    625                     moveToBackground(photo);
    626                 }
    627             }
    628         });
    629     }
    630 
    631     /** Dispose of the photo gracefully, in case we can see some of it. */
    632     public void fadeAway(final View photo, final boolean replace) {
    633         // fade out of view
    634         mOnTable.remove(photo);
    635         exitStageLeft(photo);
    636         photo.setOnTouchListener(null);
    637         photo.animate().cancel();
    638         photo.animate()
    639                 .withLayer()
    640                 .alpha(0f)
    641                 .setDuration(mPickUpDuration)
    642                 .withEndAction(new Runnable() {
    643                         @Override
    644                         public void run() {
    645                             if (photo == getFocus()) {
    646                                 clearFocus();
    647                             }
    648                             mStageLeft.removeView(photo);
    649                             recycle(photo);
    650                             if (replace) {
    651                                 scheduleNext(mNowDropDelay);
    652                             }
    653                         }
    654                     });
    655     }
    656 
    657     /** Visually on top, and also freshest, for the purposes of timeouts. */
    658     public void moveToTopOfPile(View photo) {
    659         // make this photo the last to be removed.
    660         if (isInBackground(photo)) {
    661            mBackground.bringChildToFront(photo);
    662         } else {
    663             bringChildToFront(photo);
    664         }
    665         invalidate();
    666         mOnTable.remove(photo);
    667         mOnTable.offer(photo);
    668     }
    669 
    670     /** On deck is to the left or right of the selected photo. */
    671     private void placeOnDeck(final View photo, final int slot ) {
    672         if (slot < mOnDeck.length) {
    673             if (mOnDeck[slot] != null && mOnDeck[slot] != photo) {
    674                 fadeAway(mOnDeck[slot], false);
    675             }
    676             mOnDeck[slot] = photo;
    677             float photoWidth = photo.getWidth();
    678             float photoHeight = photo.getHeight();
    679             float scale = Math.min(getHeight() / photoHeight, getWidth() / photoWidth);
    680 
    681             float x = (getWidth() - photoWidth) / 2f;
    682             float y = (getHeight() - photoHeight) / 2f;
    683 
    684             float offset = (((float) mWidth + scale * (photoWidth - 2f * mInset)) / 2f);
    685             x += (slot == NEXT? 1f : -1f) * offset;
    686 
    687             photo.animate()
    688                 .withLayer()
    689                 .rotation(0f)
    690                 .rotationY(0f)
    691                 .scaleX(scale)
    692                 .scaleY(scale)
    693                 .x(x)
    694                 .y(y)
    695                 .setDuration(mPickUpDuration)
    696                 .setInterpolator(new DecelerateInterpolator(2f));
    697         }
    698     }
    699 
    700     /** Move in response to touch. */
    701     public void move(final View photo, float x, float y, float a) {
    702         photo.animate().cancel();
    703         photo.setAlpha(1f);
    704         photo.setX((int) x);
    705         photo.setY((int) y);
    706         photo.setRotation((int) a);
    707     }
    708 
    709     /** Wind up off screen, so we can animate in. */
    710     private void throwOnTable(final View photo) {
    711         mOnTable.offer(photo);
    712         log("start offscreen");
    713         photo.setRotation(mThrowRotation);
    714         photo.setX(-mLongSide);
    715         photo.setY(-mLongSide);
    716 
    717         dropOnTable(photo, mThrowInterpolator);
    718     }
    719 
    720     public void move(final View photo, float dx, float dy, boolean drop) {
    721         if (photo != null) {
    722             final float x = photo.getX() + dx;
    723             final float y = photo.getY() + dy;
    724             photo.setX(x);
    725             photo.setY(y);
    726             Log.d(TAG, "[" + photo.getX() + ", " + photo.getY() + "] + (" + dx + "," + dy + ")");
    727             if (drop && photoOffTable(photo)) {
    728                 fadeAway(photo, true);
    729             }
    730         }
    731     }
    732 
    733     /** Fling with no touch hints, then land off screen. */
    734     public void fling(final View photo) {
    735         final float[] o = { mWidth + mLongSide / 2f,
    736                             mHeight + mLongSide / 2f };
    737         final float[] a = { photo.getX(), photo.getY() };
    738         final float[] b = { o[0], a[1] + o[0] - a[0] };
    739         final float[] c = { a[0] + o[1] - a[1], o[1] };
    740         float[] delta = { 0f, 0f };
    741         if (Math.hypot(b[0] - a[0], b[1] - a[1]) < Math.hypot(c[0] - a[0], c[1] - a[1])) {
    742             delta[0] = b[0] - a[0];
    743             delta[1] = b[1] - a[1];
    744         } else {
    745             delta[0] = c[0] - a[0];
    746             delta[1] = c[1] - a[1];
    747         }
    748 
    749         final float dist = (float) Math.hypot(delta[0], delta[1]);
    750         final int duration = (int) (1000f * dist / mThrowSpeed);
    751         fling(photo, delta[0], delta[1], duration, true);
    752     }
    753 
    754     /** Continue dynamically after a fling gesture, possibly off the screen. */
    755     public void fling(final View photo, float dx, float dy, int duration, boolean spin) {
    756         if (photo == getFocus()) {
    757             if (moveFocus(photo, 0f) == null) {
    758                 moveFocus(photo, 180f);
    759             }
    760         }
    761         moveToForeground(photo);
    762         ViewPropertyAnimator animator = photo.animate()
    763                 .withLayer()
    764                 .xBy(dx)
    765                 .yBy(dy)
    766                 .setDuration(duration)
    767                 .setInterpolator(new DecelerateInterpolator(2f));
    768 
    769         if (spin) {
    770             animator.rotation(mThrowRotation);
    771         }
    772 
    773         if (photoOffTable(photo, (int) dx, (int) dy)) {
    774             log("fling away");
    775             animator.withEndAction(new Runnable() {
    776                     @Override
    777                     public void run() {
    778                         fadeAway(photo, true);
    779                     }
    780                 });
    781         }
    782     }
    783     public boolean photoOffTable(final View photo) {
    784         return photoOffTable(photo, 0, 0);
    785     }
    786 
    787     public boolean photoOffTable(final View photo, final int dx, final int dy) {
    788         Rect hit = new Rect();
    789         photo.getHitRect(hit);
    790         hit.offset(dx, dy);
    791         return (hit.bottom < 0f || hit.top > getHeight() ||
    792                 hit.right < 0f || hit.left > getWidth());
    793     }
    794 
    795     /** Animate to a random place and orientation, down on the table (visually small). */
    796     public void dropOnTable(final View photo) {
    797         dropOnTable(photo, mDropInterpolator);
    798     }
    799 
    800     /** Animate to a random place and orientation, down on the table (visually small). */
    801     public void dropOnTable(final View photo, final Interpolator interpolator) {
    802         float angle = randfrange(-mImageRotationLimit, mImageRotationLimit);
    803         PointF p = randMultiDrop(sRNG.nextInt(),
    804                                  (float) sRNG.nextGaussian(), (float) sRNG.nextGaussian(),
    805                                  mWidth, mHeight);
    806         float x = p.x;
    807         float y = p.y;
    808 
    809         log("drop it at %f, %f", x, y);
    810 
    811         float x0 = photo.getX();
    812         float y0 = photo.getY();
    813 
    814         x -= mLongSide / 2f;
    815         y -= mShortSide / 2f;
    816         log("fixed offset is %f, %f ", x, y);
    817 
    818         float dx = x - x0;
    819         float dy = y - y0;
    820 
    821         float dist = (float) Math.hypot(dx, dy);
    822         int duration = (int) (1000f * dist / mThrowSpeed);
    823         duration = Math.max(duration, 1000);
    824 
    825         log("animate it");
    826         // toss onto table
    827         mAnimating.add(photo);
    828         photo.animate()
    829             .withLayer()
    830             .scaleX(mTableRatio / mImageRatio)
    831             .scaleY(mTableRatio / mImageRatio)
    832             .rotation(angle)
    833             .x(x)
    834             .y(y)
    835             .setDuration(duration)
    836             .setInterpolator(interpolator)
    837             .withEndAction(new Runnable() {
    838                 @Override
    839                 public void run() {
    840                     mAnimating.remove(photo);
    841                     if (!mAnimating.contains(photo)) {
    842                         moveToBackground(photo);
    843                     }
    844                 }
    845             });
    846     }
    847 
    848     private void moveToBackground(View photo) {
    849         if (!isInBackground(photo)) {
    850             removeView(photo);
    851             mBackground.addView(photo, new LayoutParams(LayoutParams.WRAP_CONTENT,
    852                     LayoutParams.WRAP_CONTENT));
    853         }
    854     }
    855 
    856     private void exitStageLeft(View photo) {
    857         if (isInBackground(photo)) {
    858             mBackground.removeView(photo);
    859         } else {
    860             removeView(photo);
    861         }
    862         mStageLeft.addView(photo, new LayoutParams(LayoutParams.WRAP_CONTENT,
    863                 LayoutParams.WRAP_CONTENT));
    864     }
    865 
    866     private void moveToForeground(View photo) {
    867         if (isInBackground(photo)) {
    868             mBackground.removeView(photo);
    869             addView(photo, new LayoutParams(LayoutParams.WRAP_CONTENT,
    870                     LayoutParams.WRAP_CONTENT));
    871         }
    872     }
    873 
    874     private boolean isInBackground(View photo) {
    875         return mBackground.indexOfChild(photo) != -1;
    876     }
    877 
    878     /** wrap all orientations to the interval [-180, 180). */
    879     private float wrapAngle(float angle) {
    880         float result = angle + 180;
    881         result = ((result % 360) + 360) % 360; // catch negative numbers
    882         result -= 180;
    883         return result;
    884     }
    885 
    886     /** Animate the selected photo to the foregound: zooming in to bring it foreward. */
    887     private void pickUp(final View photo) {
    888         float photoWidth = photo.getWidth();
    889         float photoHeight = photo.getHeight();
    890 
    891         float scale = Math.min(getHeight() / photoHeight, getWidth() / photoWidth);
    892 
    893         log("scale is %f", scale);
    894         log("target it");
    895         float x = (getWidth() - photoWidth) / 2f;
    896         float y = (getHeight() - photoHeight) / 2f;
    897 
    898         photo.setRotation(wrapAngle(photo.getRotation()));
    899 
    900         log("animate it");
    901         // lift up to the glass for a good look
    902         moveToForeground(photo);
    903         photo.animate()
    904             .withLayer()
    905             .rotation(0f)
    906             .rotationY(0f)
    907             .alpha(1f)
    908             .scaleX(scale)
    909             .scaleY(scale)
    910             .x(x)
    911             .y(y)
    912             .setDuration(mPickUpDuration)
    913             .setInterpolator(new DecelerateInterpolator(2f))
    914             .withEndAction(new Runnable() {
    915                 @Override
    916                 public void run() {
    917                     log("endtimes: %f", photo.getX());
    918                 }
    919             });
    920     }
    921 
    922     private Bitmap getBitmap(View photo) {
    923         if (photo == null) {
    924             return null;
    925         }
    926         ImageView image = (ImageView) photo;
    927         LayerDrawable layers = (LayerDrawable) image.getDrawable();
    928         if (layers == null) {
    929             return null;
    930         }
    931         BitmapDrawable bitmap = (BitmapDrawable) layers.getDrawable(0);
    932         if (bitmap == null) {
    933             return null;
    934         }
    935         return bitmap.getBitmap();
    936     }
    937 
    938     private void recycle(View photo) {
    939         if (photo != null) {
    940             removeView(photo);
    941             mPhotoSource.recycle(getBitmap(photo));
    942         }
    943     }
    944 
    945     public void setHighlight(View photo, boolean highlighted) {
    946         ImageView image = (ImageView) photo;
    947         LayerDrawable layers = (LayerDrawable) image.getDrawable();
    948         if (highlighted) {
    949             layers.getDrawable(1).setColorFilter(mHighlightColor, PorterDuff.Mode.SRC_IN);
    950         } else {
    951             layers.getDrawable(1).clearColorFilter();
    952         }
    953     }
    954 
    955     /** Schedule the first launch.  Idempotent. */
    956     public void start() {
    957         if (!mStarted) {
    958             log("kick it");
    959             mStarted = true;
    960             scheduleNext(0);
    961         }
    962     }
    963 
    964     public void refreshSelection() {
    965         scheduleSelectionReaper(mMaxFocusTime);
    966     }
    967 
    968     public void scheduleSelectionReaper(int delay) {
    969         removeCallbacks(mSelectionReaper);
    970         postDelayed(mSelectionReaper, delay);
    971     }
    972 
    973     public void refreshFocus() {
    974         scheduleFocusReaper(mMaxFocusTime);
    975     }
    976 
    977     public void scheduleFocusReaper(int delay) {
    978         removeCallbacks(mFocusReaper);
    979         postDelayed(mFocusReaper, delay);
    980     }
    981 
    982     public void scheduleNext(int delay) {
    983         removeCallbacks(mLauncher);
    984         postDelayed(mLauncher, delay);
    985     }
    986 
    987     private static void log(String message, Object... args) {
    988         if (DEBUG) {
    989             Formatter formatter = new Formatter();
    990             formatter.format(message, args);
    991             Log.i(TAG, formatter.toString());
    992         }
    993     }
    994 }
    995