• 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.service.dreams.DreamService;
19 import android.content.Context;
20 import android.content.SharedPreferences;
21 import android.content.res.Resources;
22 import android.graphics.Bitmap;
23 import android.graphics.BitmapFactory;
24 import android.graphics.PointF;
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.util.AttributeSet;
30 import android.util.Log;
31 import android.view.LayoutInflater;
32 import android.view.MotionEvent;
33 import android.view.View;
34 import android.view.animation.DecelerateInterpolator;
35 import android.view.animation.Interpolator;
36 import android.widget.FrameLayout;
37 import android.widget.FrameLayout.LayoutParams;
38 import android.widget.ImageView;
39 
40 import java.util.LinkedList;
41 import java.util.Random;
42 
43 /**
44  * A surface where photos sit.
45  */
46 public class PhotoTable extends FrameLayout {
47     private static final String TAG = "PhotoTable";
48     private static final boolean DEBUG = false;
49 
50     class Launcher implements Runnable {
51         private final PhotoTable mTable;
Launcher(PhotoTable table)52         public Launcher(PhotoTable table) {
53             mTable = table;
54         }
55 
56         @Override
run()57         public void run() {
58             mTable.scheduleNext(mDropPeriod);
59             mTable.launch();
60         }
61     }
62 
63     private static final long MAX_SELECTION_TIME = 10000L;
64     private static Random sRNG = new Random();
65 
66     private final Launcher mLauncher;
67     private final LinkedList<View> mOnTable;
68     private final int mDropPeriod;
69     private final int mFastDropPeriod;
70     private final int mNowDropDelay;
71     private final float mImageRatio;
72     private final float mTableRatio;
73     private final float mImageRotationLimit;
74     private final float mThrowRotation;
75     private final float mThrowSpeed;
76     private final boolean mTapToExit;
77     private final int mTableCapacity;
78     private final int mRedealCount;
79     private final int mInset;
80     private final PhotoSourcePlexor mPhotoSource;
81     private final Resources mResources;
82     private final Interpolator mThrowInterpolator;
83     private final Interpolator mDropInterpolator;
84     private DreamService mDream;
85     private PhotoLaunchTask mPhotoLaunchTask;
86     private boolean mStarted;
87     private boolean mIsLandscape;
88     private int mLongSide;
89     private int mShortSide;
90     private int mWidth;
91     private int mHeight;
92     private View mSelected;
93     private long mSelectedTime;
94 
PhotoTable(Context context, AttributeSet as)95     public PhotoTable(Context context, AttributeSet as) {
96         super(context, as);
97         mResources = getResources();
98         mInset = mResources.getDimensionPixelSize(R.dimen.photo_inset);
99         mDropPeriod = mResources.getInteger(R.integer.table_drop_period);
100         mFastDropPeriod = mResources.getInteger(R.integer.fast_drop);
101         mNowDropDelay = mResources.getInteger(R.integer.now_drop);
102         mImageRatio = mResources.getInteger(R.integer.image_ratio) / 1000000f;
103         mTableRatio = mResources.getInteger(R.integer.table_ratio) / 1000000f;
104         mImageRotationLimit = (float) mResources.getInteger(R.integer.max_image_rotation);
105         mThrowSpeed = mResources.getDimension(R.dimen.image_throw_speed);
106         mThrowRotation = (float) mResources.getInteger(R.integer.image_throw_rotatioan);
107         mTableCapacity = mResources.getInteger(R.integer.table_capacity);
108         mRedealCount = mResources.getInteger(R.integer.redeal_count);
109         mTapToExit = mResources.getBoolean(R.bool.enable_tap_to_exit);
110         mThrowInterpolator = new SoftLandingInterpolator(
111                 mResources.getInteger(R.integer.soft_landing_time) / 1000000f,
112                 mResources.getInteger(R.integer.soft_landing_distance) / 1000000f);
113         mDropInterpolator = new DecelerateInterpolator(
114                 (float) mResources.getInteger(R.integer.drop_deceleration_exponent));
115         mOnTable = new LinkedList<View>();
116         mPhotoSource = new PhotoSourcePlexor(getContext(),
117                 getContext().getSharedPreferences(PhotoTableDreamSettings.PREFS_NAME, 0));
118         mLauncher = new Launcher(this);
119         mStarted = false;
120     }
121 
122 
setDream(DreamService dream)123     public void setDream(DreamService dream) {
124         mDream = dream;
125     }
126 
hasSelection()127     public boolean hasSelection() {
128         return mSelected != null;
129     }
130 
getSelected()131     public View getSelected() {
132         return mSelected;
133     }
134 
clearSelection()135     public void clearSelection() {
136         mSelected = null;
137     }
138 
setSelection(View selected)139     public void setSelection(View selected) {
140         assert(selected != null);
141         if (mSelected != null) {
142             dropOnTable(mSelected);
143         }
144         mSelected = selected;
145         mSelectedTime = System.currentTimeMillis();
146         bringChildToFront(selected);
147         pickUp(selected);
148     }
149 
lerp(float a, float b, float f)150     static float lerp(float a, float b, float f) {
151         return (b-a)*f + a;
152     }
153 
randfrange(float a, float b)154     static float randfrange(float a, float b) {
155         return lerp(a, b, sRNG.nextFloat());
156     }
157 
randFromCurve(float t, PointF[] v)158     static PointF randFromCurve(float t, PointF[] v) {
159         PointF p = new PointF();
160         if (v.length == 4 && t >= 0f && t <= 1f) {
161             float a = (float) Math.pow(1f-t, 3f);
162             float b = (float) Math.pow(1f-t, 2f) * t;
163             float c = (1f-t) * (float) Math.pow(t, 2f);
164             float d = (float) Math.pow(t, 3f);
165 
166             p.x = a * v[0].x + 3 * b * v[1].x + 3 * c * v[2].x + d * v[3].x;
167             p.y = a * v[0].y + 3 * b * v[1].y + 3 * c * v[2].y + d * v[3].y;
168         }
169         return p;
170     }
171 
randInCenter(float i, float j, int width, int height)172     private static PointF randInCenter(float i, float j, int width, int height) {
173         log("randInCenter (" + i + ", " + j + ", " + width + ", " + height + ")");
174         PointF p = new PointF();
175         p.x = 0.5f * width + 0.15f * width * i;
176         p.y = 0.5f * height + 0.15f * height * j;
177         log("randInCenter returning " + p.x + "," + p.y);
178         return p;
179     }
180 
randMultiDrop(int n, float i, float j, int width, int height)181     private static PointF randMultiDrop(int n, float i, float j, int width, int height) {
182         log("randMultiDrop (" + n + "," + i + ", " + j + ", " + width + ", " + height + ")");
183         final float[] cx = {0.3f, 0.3f, 0.5f, 0.7f, 0.7f};
184         final float[] cy = {0.3f, 0.7f, 0.5f, 0.3f, 0.7f};
185         n = Math.abs(n);
186         float x = cx[n % cx.length];
187         float y = cy[n % cx.length];
188         PointF p = new PointF();
189         p.x = x * width + 0.05f * width * i;
190         p.y = y * height + 0.05f * height * j;
191         log("randInCenter returning " + p.x + "," + p.y);
192         return p;
193     }
194 
195     @Override
onTouchEvent(MotionEvent event)196     public boolean onTouchEvent(MotionEvent event) {
197         if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
198             if (hasSelection()) {
199                 dropOnTable(getSelected());
200                 clearSelection();
201             } else  {
202                 if (mTapToExit && mDream != null) {
203                     mDream.finish();
204                 }
205             }
206             return true;
207         }
208         return false;
209     }
210 
211     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)212     public void onLayout(boolean changed, int left, int top, int right, int bottom) {
213         super.onLayout(changed, left, top, right, bottom);
214         log("onLayout (" + left + ", " + top + ", " + right + ", " + bottom + ")");
215 
216         mHeight = bottom - top;
217         mWidth = right - left;
218 
219         mLongSide = (int) (mImageRatio * Math.max(mWidth, mHeight));
220         mShortSide = (int) (mImageRatio * Math.min(mWidth, mHeight));
221 
222         boolean isLandscape = mWidth > mHeight;
223         if (mIsLandscape != isLandscape) {
224             for (View photo: mOnTable) {
225                 if (photo == getSelected()) {
226                     pickUp(photo);
227                 } else {
228                     dropOnTable(photo);
229                 }
230             }
231             mIsLandscape = isLandscape;
232         }
233         start();
234     }
235 
236     @Override
isOpaque()237     public boolean isOpaque() {
238         return true;
239     }
240 
241     private class PhotoLaunchTask extends AsyncTask<Void, Void, View> {
242         private final BitmapFactory.Options mOptions;
243 
PhotoLaunchTask()244         public PhotoLaunchTask () {
245             mOptions = new BitmapFactory.Options();
246             mOptions.inTempStorage = new byte[32768];
247         }
248 
249         @Override
doInBackground(Void... unused)250         public View doInBackground(Void... unused) {
251             log("load a new photo");
252             final PhotoTable table = PhotoTable.this;
253 
254             LayoutInflater inflater = (LayoutInflater) table.getContext()
255                    .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
256             View photo = inflater.inflate(R.layout.photo, null);
257             ImageView image = (ImageView) photo;
258             Drawable[] layers = new Drawable[2];
259             Bitmap decodedPhoto = table.mPhotoSource.next(mOptions,
260                     table.mLongSide, table.mShortSide);
261             int photoWidth = mOptions.outWidth;
262             int photoHeight = mOptions.outHeight;
263             if (mOptions.outWidth <= 0 || mOptions.outHeight <= 0) {
264                 photo = null;
265             } else {
266                 decodedPhoto.setHasMipMap(true);
267                 layers[0] = new BitmapDrawable(table.mResources, decodedPhoto);
268                 layers[1] = table.mResources.getDrawable(R.drawable.frame);
269                 LayerDrawable layerList = new LayerDrawable(layers);
270                 layerList.setLayerInset(0, table.mInset, table.mInset,
271                                         table.mInset, table.mInset);
272                 image.setImageDrawable(layerList);
273 
274                 photo.setTag(R.id.photo_width, new Integer(photoWidth));
275                 photo.setTag(R.id.photo_height, new Integer(photoHeight));
276 
277                 photo.setOnTouchListener(new PhotoTouchListener(table.getContext(),
278                                                                 table));
279             }
280 
281             return photo;
282         }
283 
284         @Override
onPostExecute(View photo)285         public void onPostExecute(View photo) {
286             if (photo != null) {
287                 final PhotoTable table = PhotoTable.this;
288 
289                 table.addView(photo, new LayoutParams(LayoutParams.WRAP_CONTENT,
290                                                        LayoutParams.WRAP_CONTENT));
291                 if (table.hasSelection()) {
292                     table.bringChildToFront(table.getSelected());
293                 }
294                 int width = ((Integer) photo.getTag(R.id.photo_width)).intValue();
295                 int height = ((Integer) photo.getTag(R.id.photo_height)).intValue();
296 
297                 log("drop it");
298                 table.throwOnTable(photo);
299 
300                 if(table.mOnTable.size() < table.mTableCapacity) {
301                     table.scheduleNext(table.mFastDropPeriod);
302                 }
303             }
304         }
305     };
306 
launch()307     public void launch() {
308         log("launching");
309         setSystemUiVisibility(View.STATUS_BAR_HIDDEN);
310         if (hasSelection() &&
311                 (System.currentTimeMillis() - mSelectedTime) > MAX_SELECTION_TIME) {
312             dropOnTable(getSelected());
313             clearSelection();
314         } else {
315             log("inflate it");
316             if (mPhotoLaunchTask == null ||
317                 mPhotoLaunchTask.getStatus() == AsyncTask.Status.FINISHED) {
318                 mPhotoLaunchTask = new PhotoLaunchTask();
319                 mPhotoLaunchTask.execute();
320             }
321         }
322     }
fadeAway(final View photo, final boolean replace)323     public void fadeAway(final View photo, final boolean replace) {
324         // fade out of view
325         mOnTable.remove(photo);
326         photo.animate().cancel();
327         photo.animate()
328                 .withLayer()
329                 .alpha(0f)
330                 .setDuration(1000)
331                 .withEndAction(new Runnable() {
332                         @Override
333                         public void run() {
334                             removeView(photo);
335                             recycle(photo);
336                             if (replace) {
337                                 scheduleNext(mNowDropDelay);
338                             }
339                         }
340                     });
341     }
342 
moveToBackOfQueue(View photo)343     public void moveToBackOfQueue(View photo) {
344         // make this photo the last to be removed.
345         bringChildToFront(photo);
346         invalidate();
347         mOnTable.remove(photo);
348         mOnTable.offer(photo);
349     }
350 
throwOnTable(final View photo)351     private void throwOnTable(final View photo) {
352         mOnTable.offer(photo);
353         log("start offscreen");
354         int width = ((Integer) photo.getTag(R.id.photo_width));
355         int height = ((Integer) photo.getTag(R.id.photo_height));
356         photo.setRotation(mThrowRotation);
357         photo.setX(-mLongSide);
358         photo.setY(-mLongSide);
359 
360         dropOnTable(photo, mThrowInterpolator);
361     }
362 
dropOnTable(final View photo)363     public void dropOnTable(final View photo) {
364         dropOnTable(photo, mDropInterpolator);
365     }
366 
dropOnTable(final View photo, final Interpolator interpolator)367     public void dropOnTable(final View photo, final Interpolator interpolator) {
368         float angle = randfrange(-mImageRotationLimit, mImageRotationLimit);
369         PointF p = randMultiDrop(sRNG.nextInt(),
370                                  (float) sRNG.nextGaussian(), (float) sRNG.nextGaussian(),
371                                  mWidth, mHeight);
372         float x = p.x;
373         float y = p.y;
374 
375         log("drop it at " + x + ", " + y);
376 
377         float x0 = photo.getX();
378         float y0 = photo.getY();
379         float width = (float) ((Integer) photo.getTag(R.id.photo_width)).intValue();
380         float height = (float) ((Integer) photo.getTag(R.id.photo_height)).intValue();
381 
382         x -= mLongSide / 2f;
383         y -= mShortSide / 2f;
384         log("fixed offset is " + x + ", " + y);
385 
386         float dx = x - x0;
387         float dy = y - y0;
388 
389         float dist = (float) (Math.sqrt(dx * dx + dy * dy));
390         int duration = (int) (1000f * dist / mThrowSpeed);
391         duration = Math.max(duration, 1000);
392 
393         log("animate it");
394         // toss onto table
395         photo.animate()
396                 .scaleX(mTableRatio / mImageRatio)
397                 .scaleY(mTableRatio / mImageRatio)
398                 .rotation(angle)
399                 .x(x)
400                 .y(y)
401                 .setDuration(duration)
402                 .setInterpolator(interpolator)
403                 .withEndAction(new Runnable() {
404                         @Override
405                             public void run() {
406                             if (mOnTable.size() > mTableCapacity) {
407                                 while (mOnTable.size() > (mTableCapacity - mRedealCount)) {
408                                     fadeAway(mOnTable.poll(), false);
409                                 }
410                                 // zero delay because we already waited duration ms
411                                 scheduleNext(0);
412                             }
413                         }
414                     });
415     }
416 
417     /** wrap all orientations to the interval [-180, 180). */
wrapAngle(float angle)418     private float wrapAngle(float angle) {
419         float result = angle + 180;
420         result = ((result % 360) + 360) % 360; // catch negative numbers
421         result -= 180;
422         return result;
423     }
424 
pickUp(final View photo)425     private void pickUp(final View photo) {
426         float photoWidth = photo.getWidth();
427         float photoHeight = photo.getHeight();
428 
429         float scale = Math.min(getHeight() / photoHeight, getWidth() / photoWidth);
430 
431         log("target it");
432         float x = (getWidth() - photoWidth) / 2f;
433         float y = (getHeight() - photoHeight) / 2f;
434 
435         float x0 = photo.getX();
436         float y0 = photo.getY();
437         float dx = x - x0;
438         float dy = y - y0;
439 
440         float dist = (float) (Math.sqrt(dx * dx + dy * dy));
441         int duration = (int) (1000f * dist / 600f);
442         duration = Math.max(duration, 500);
443 
444         photo.setRotation(wrapAngle(photo.getRotation()));
445 
446         log("animate it");
447         // toss onto table
448         photo.animate()
449                 .rotation(0f)
450                 .scaleX(scale)
451                 .scaleY(scale)
452                 .x(x)
453                 .y(y)
454                 .setDuration(duration)
455                 .setInterpolator(new DecelerateInterpolator(2f))
456                 .withEndAction(new Runnable() {
457                         @Override
458                             public void run() {
459                             log("endtimes: " + photo.getX());
460                         }
461                     });
462     }
463 
recycle(View photo)464     private void recycle(View photo) {
465         ImageView image = (ImageView) photo;
466         LayerDrawable layers = (LayerDrawable) image.getDrawable();
467         BitmapDrawable bitmap = (BitmapDrawable) layers.getDrawable(0);
468         bitmap.getBitmap().recycle();
469     }
470 
start()471     public void start() {
472         if (!mStarted) {
473             log("kick it");
474             mStarted = true;
475             scheduleNext(mDropPeriod);
476             launch();
477         }
478     }
479 
scheduleNext(int delay)480     public void scheduleNext(int delay) {
481         removeCallbacks(mLauncher);
482         postDelayed(mLauncher, delay);
483     }
484 
log(String message)485     private static void log(String message) {
486         if (DEBUG) {
487             Log.i(TAG, message);
488         }
489     }
490 }
491