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 */
16package com.android.dreams.phototable;
17
18import android.service.dreams.DreamService;
19import android.content.Context;
20import android.content.SharedPreferences;
21import android.content.res.Resources;
22import android.graphics.Bitmap;
23import android.graphics.BitmapFactory;
24import android.graphics.PointF;
25import android.graphics.drawable.BitmapDrawable;
26import android.graphics.drawable.Drawable;
27import android.graphics.drawable.LayerDrawable;
28import android.os.AsyncTask;
29import android.util.AttributeSet;
30import android.util.Log;
31import android.view.LayoutInflater;
32import android.view.MotionEvent;
33import android.view.View;
34import android.view.animation.DecelerateInterpolator;
35import android.view.animation.Interpolator;
36import android.widget.FrameLayout;
37import android.widget.FrameLayout.LayoutParams;
38import android.widget.ImageView;
39
40import java.util.LinkedList;
41import java.util.Random;
42
43/**
44 * A surface where photos sit.
45 */
46public 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;
52        public Launcher(PhotoTable table) {
53            mTable = table;
54        }
55
56        @Override
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
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
123    public void setDream(DreamService dream) {
124        mDream = dream;
125    }
126
127    public boolean hasSelection() {
128        return mSelected != null;
129    }
130
131    public View getSelected() {
132        return mSelected;
133    }
134
135    public void clearSelection() {
136        mSelected = null;
137    }
138
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
150    static float lerp(float a, float b, float f) {
151        return (b-a)*f + a;
152    }
153
154    static float randfrange(float a, float b) {
155        return lerp(a, b, sRNG.nextFloat());
156    }
157
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
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
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
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
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
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
244        public PhotoLaunchTask () {
245            mOptions = new BitmapFactory.Options();
246            mOptions.inTempStorage = new byte[32768];
247        }
248
249        @Override
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
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
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    }
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
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
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
363    public void dropOnTable(final View photo) {
364        dropOnTable(photo, mDropInterpolator);
365    }
366
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). */
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
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
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
471    public void start() {
472        if (!mStarted) {
473            log("kick it");
474            mStarted = true;
475            scheduleNext(mDropPeriod);
476            launch();
477        }
478    }
479
480    public void scheduleNext(int delay) {
481        removeCallbacks(mLauncher);
482        postDelayed(mLauncher, delay);
483    }
484
485    private static void log(String message) {
486        if (DEBUG) {
487            Log.i(TAG, message);
488        }
489    }
490}
491