• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
run()60         public void run() {
61             PhotoTable.this.scheduleNext(mDropPeriod);
62             PhotoTable.this.launch();
63         }
64     }
65 
66     class FocusReaper implements Runnable {
67         @Override
run()68         public void run() {
69             PhotoTable.this.clearFocus();
70         }
71     }
72 
73     class SelectionReaper implements Runnable {
74         @Override
run()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 
PhotoTable(Context context, AttributeSet as)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
onFinishInflate()170     public void onFinishInflate() {
171         mBackground = (ViewGroup) findViewById(R.id.background);
172         mStageLeft = (ViewGroup) findViewById(R.id.stageleft);
173     }
174 
setDream(DreamService dream)175     public void setDream(DreamService dream) {
176         mDream = dream;
177     }
178 
hasSelection()179     public boolean hasSelection() {
180         return mSelection != null;
181     }
182 
getSelection()183     public View getSelection() {
184         return mSelection;
185     }
186 
clearSelection()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 
setSelection(View selected)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 
selectNext()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 
selectPrevious()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 
promoteSelection()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 
hasFocus()270     public boolean hasFocus() {
271         return mFocus != null;
272     }
273 
getFocus()274     public View getFocus() {
275         return mFocus;
276     }
277 
clearFocus()278     public void clearFocus() {
279         if (hasFocus()) {
280             setHighlight(getFocus(), false);
281         }
282         mFocus = null;
283     }
284 
setDefaultFocus()285     public void setDefaultFocus() {
286         setFocus(mOnTable.getLast());
287     }
288 
setFocus(View focus)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 
lerp(float a, float b, float f)298     static float lerp(float a, float b, float f) {
299         return (b-a)*f + a;
300     }
301 
randfrange(float a, float b)302     static float randfrange(float a, float b) {
303         return lerp(a, b, sRNG.nextFloat());
304     }
305 
randFromCurve(float t, PointF[] v)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 
randMultiDrop(int n, float i, float j, int width, int height)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 
cross(double[] a, double[] b)334     private double cross(double[] a, double[] b) {
335         return a[0] * b[1] - a[1] * b[0];
336     }
337 
norm(double[] a)338     private double norm(double[] a) {
339         return Math.hypot(a[0], a[1]);
340     }
341 
getCenter(View photo)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 
moveFocus(View focus, float direction)350     public View moveFocus(View focus, float direction) {
351         return moveFocus(focus, direction, 90f);
352     }
353 
moveFocus(View focus, float direction, float angle)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
onKeyDown(int keyCode, KeyEvent event)393     public boolean onKeyDown(int keyCode, KeyEvent event) {
394         return mKeyboardInterpreter.onKeyDown(keyCode, event);
395     }
396 
397     @Override
onGenericMotionEvent(MotionEvent event)398     public boolean onGenericMotionEvent(MotionEvent event) {
399         return mEdgeSwipeDetector.onTouchEvent(event) || mDragGestureDetector.onTouchEvent(event);
400     }
401 
402     @Override
onTouchEvent(MotionEvent event)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
onLayout(boolean changed, int left, int top, int right, int bottom)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
isOpaque()449     public boolean isOpaque() {
450         return true;
451     }
452 
453     /** Put a nice border on the bitmap. */
applyFrame(final PhotoTable table, final BitmapFactory.Options options, Bitmap decodedPhoto)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 
LoadNaturalSiblingTask(int slot)488         public LoadNaturalSiblingTask (int slot) {
489             mOptions = new BitmapFactory.Options();
490             mOptions.inTempStorage = new byte[32768];
491             mSlot = slot;
492         }
493 
494         @Override
doInBackground(View... views)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
onPostExecute(View photo)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 
PhotoLaunchTask()543         public PhotoLaunchTask () {
544             mOptions = new BitmapFactory.Options();
545             mOptions.inTempStorage = new byte[32768];
546         }
547 
548         @Override
doInBackground(Void... unused)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
onPostExecute(View photo)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. */
launch()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. */
fadeOutBackground(final View photo)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. */
fadeInBackground(final View photo)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. */
fadeAway(final View photo, final boolean replace)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. */
moveToTopOfPile(View photo)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. */
placeOnDeck(final View photo, final int slot )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. */
move(final View photo, float x, float y, float a)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. */
throwOnTable(final View photo)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 
move(final View photo, float dx, float dy, boolean drop)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. */
fling(final View photo)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. */
fling(final View photo, float dx, float dy, int duration, boolean spin)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     }
photoOffTable(final View photo)783     public boolean photoOffTable(final View photo) {
784         return photoOffTable(photo, 0, 0);
785     }
786 
photoOffTable(final View photo, final int dx, final int dy)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). */
dropOnTable(final View photo)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). */
dropOnTable(final View photo, final Interpolator interpolator)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 
moveToBackground(View photo)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 
exitStageLeft(View photo)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 
moveToForeground(View photo)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 
isInBackground(View photo)874     private boolean isInBackground(View photo) {
875         return mBackground.indexOfChild(photo) != -1;
876     }
877 
878     /** wrap all orientations to the interval [-180, 180). */
wrapAngle(float angle)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. */
pickUp(final View photo)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 
getBitmap(View photo)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 
recycle(View photo)938     private void recycle(View photo) {
939         if (photo != null) {
940             removeView(photo);
941             mPhotoSource.recycle(getBitmap(photo));
942         }
943     }
944 
setHighlight(View photo, boolean highlighted)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. */
start()956     public void start() {
957         if (!mStarted) {
958             log("kick it");
959             mStarted = true;
960             scheduleNext(0);
961         }
962     }
963 
refreshSelection()964     public void refreshSelection() {
965         scheduleSelectionReaper(mMaxFocusTime);
966     }
967 
scheduleSelectionReaper(int delay)968     public void scheduleSelectionReaper(int delay) {
969         removeCallbacks(mSelectionReaper);
970         postDelayed(mSelectionReaper, delay);
971     }
972 
refreshFocus()973     public void refreshFocus() {
974         scheduleFocusReaper(mMaxFocusTime);
975     }
976 
scheduleFocusReaper(int delay)977     public void scheduleFocusReaper(int delay) {
978         removeCallbacks(mFocusReaper);
979         postDelayed(mFocusReaper, delay);
980     }
981 
scheduleNext(int delay)982     public void scheduleNext(int delay) {
983         removeCallbacks(mLauncher);
984         postDelayed(mLauncher, delay);
985     }
986 
log(String message, Object... args)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