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