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
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 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 
PhotoTable(Context context, AttributeSet as)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
onFinishInflate()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 
setDream(DreamService dream)178     public void setDream(DreamService dream) {
179         mDream = dream;
180     }
181 
hasSelection()182     public boolean hasSelection() {
183         return mSelection != null;
184     }
185 
getSelection()186     public View getSelection() {
187         return mSelection;
188     }
189 
clearSelection()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 
setSelection(View selected)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 
selectNext()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 
selectPrevious()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 
promoteSelection()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 
hasFocus()273     public boolean hasFocus() {
274         return mFocus != null;
275     }
276 
getFocus()277     public View getFocus() {
278         return mFocus;
279     }
280 
clearFocus()281     public void clearFocus() {
282         if (hasFocus()) {
283             setHighlight(getFocus(), false);
284         }
285         mFocus = null;
286     }
287 
setDefaultFocus()288     public void setDefaultFocus() {
289         if (mOnTable.size() > 0) {
290             setFocus(mOnTable.getLast());
291         }
292     }
293 
setFocus(View focus)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 
lerp(float a, float b, float f)303     static float lerp(float a, float b, float f) {
304         return (b-a)*f + a;
305     }
306 
randfrange(float a, float b)307     static float randfrange(float a, float b) {
308         return lerp(a, b, sRNG.nextFloat());
309     }
310 
randFromCurve(float t, PointF[] v)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 
randMultiDrop(int n, float i, float j, int width, int height)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 
cross(double[] a, double[] b)339     private double cross(double[] a, double[] b) {
340         return a[0] * b[1] - a[1] * b[0];
341     }
342 
norm(double[] a)343     private double norm(double[] a) {
344         return Math.hypot(a[0], a[1]);
345     }
346 
getCenter(View photo)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 
moveFocus(View focus, float direction)355     public View moveFocus(View focus, float direction) {
356         return moveFocus(focus, direction, 90f);
357     }
358 
moveFocus(View focus, float direction, float angle)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
onKeyDown(int keyCode, KeyEvent event)400     public boolean onKeyDown(int keyCode, KeyEvent event) {
401         return mKeyboardInterpreter.onKeyDown(keyCode, event);
402     }
403 
404     @Override
onGenericMotionEvent(MotionEvent event)405     public boolean onGenericMotionEvent(MotionEvent event) {
406         return mEdgeSwipeDetector.onTouchEvent(event) || mDragGestureDetector.onTouchEvent(event);
407     }
408 
409     @Override
onTouchEvent(MotionEvent event)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
onLayout(boolean changed, int left, int top, int right, int bottom)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
isOpaque()456     public boolean isOpaque() {
457         return true;
458     }
459 
460     /** Put a nice border on the bitmap. */
applyFrame(final PhotoTable table, final BitmapFactory.Options options, Bitmap decodedPhoto)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 
LoadNaturalSiblingTask(int slot)495         public LoadNaturalSiblingTask (int slot) {
496             mOptions = new BitmapFactory.Options();
497             mOptions.inTempStorage = new byte[32768];
498             mSlot = slot;
499         }
500 
501         @Override
doInBackground(View... views)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
onPostExecute(View photo)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 
PhotoLaunchTask()550         public PhotoLaunchTask () {
551             mOptions = new BitmapFactory.Options();
552             mOptions.inTempStorage = new byte[32768];
553         }
554 
555         @Override
doInBackground(Void... unused)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
onPostExecute(View photo)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. */
launch()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. */
fadeOutBackground(final View photo)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. */
fadeInBackground(final View photo)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 
resolveBackgroundQueue()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. */
fadeAway(final View photo, final boolean replace)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. */
moveToTopOfPile(View photo)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. */
placeOnDeck(final View photo, final int slot )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. */
move(final View photo, float x, float y, float a)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. */
throwOnTable(final View photo)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 
move(final View photo, float dx, float dy, boolean drop)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. */
fling(final View photo)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. */
fling(final View photo, float dx, float dy, int duration, boolean spin)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     }
photoOffTable(final View photo)821     public boolean photoOffTable(final View photo) {
822         return photoOffTable(photo, 0, 0);
823     }
824 
photoOffTable(final View photo, final int dx, final int dy)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). */
dropOnTable(final View photo)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). */
dropOnTable(final View photo, final Interpolator interpolator)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 
moveToBackground(View photo)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 
exitStageLeft(View photo)891     private void exitStageLeft(View photo) {
892         removeViewFromParent(photo);
893         mStageLeft.addView(photo, new LayoutParams(LayoutParams.WRAP_CONTENT,
894                 LayoutParams.WRAP_CONTENT));
895     }
896 
removeViewFromParent(View photo)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 
moveToForeground(View photo)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 
isInBackground(View photo)912     private boolean isInBackground(View photo) {
913         return mBackgroudOptimization && mBackground.indexOfChild(photo) != -1;
914     }
915 
916     /** wrap all orientations to the interval [-180, 180). */
wrapAngle(float angle)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. */
pickUp(final View photo)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 
getBitmap(View photo)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 
recycle(View photo)977     private void recycle(View photo) {
978         if (photo != null) {
979             removeViewFromParent(photo);
980             mPhotoSource.recycle(getBitmap(photo));
981         }
982     }
983 
setHighlight(View photo, boolean highlighted)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. */
start()995     public void start() {
996         if (!mStarted) {
997             log("kick it");
998             mStarted = true;
999             scheduleNext(0);
1000         }
1001     }
1002 
refreshSelection()1003     public void refreshSelection() {
1004         scheduleSelectionReaper(mMaxFocusTime);
1005     }
1006 
scheduleSelectionReaper(int delay)1007     public void scheduleSelectionReaper(int delay) {
1008         removeCallbacks(mSelectionReaper);
1009         postDelayed(mSelectionReaper, delay);
1010     }
1011 
refreshFocus()1012     public void refreshFocus() {
1013         scheduleFocusReaper(mMaxFocusTime);
1014     }
1015 
scheduleFocusReaper(int delay)1016     public void scheduleFocusReaper(int delay) {
1017         removeCallbacks(mFocusReaper);
1018         postDelayed(mFocusReaper, delay);
1019     }
1020 
scheduleNext(int delay)1021     public void scheduleNext(int delay) {
1022         removeCallbacks(mLauncher);
1023         postDelayed(mLauncher, delay);
1024     }
1025 
log(String message, Object... args)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