1/*
2 * Copyright (C) 2013 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
17package com.android.systemui;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.AnimatorSet;
22import android.animation.ObjectAnimator;
23import android.content.Context;
24import android.content.res.Resources;
25import android.graphics.Bitmap;
26import android.graphics.BitmapFactory;
27import android.graphics.Canvas;
28import android.graphics.Color;
29import android.graphics.ColorMatrixColorFilter;
30import android.graphics.Paint;
31import android.graphics.Point;
32import android.graphics.Rect;
33import android.graphics.drawable.BitmapDrawable;
34import android.graphics.drawable.Drawable;
35import android.os.Handler;
36import android.util.AttributeSet;
37import android.util.Log;
38import android.util.SparseArray;
39import android.view.View;
40import android.view.animation.AccelerateInterpolator;
41import android.view.animation.AnticipateOvershootInterpolator;
42import android.view.animation.DecelerateInterpolator;
43import android.widget.FrameLayout;
44import android.widget.ImageView;
45
46import java.util.HashSet;
47import java.util.Set;
48
49public class DessertCaseView extends FrameLayout {
50    private static final String TAG = DessertCaseView.class.getSimpleName();
51
52    private static final boolean DEBUG = false;
53
54    static final int START_DELAY = 5000;
55    static final int DELAY = 2000;
56    static final int DURATION = 500;
57
58    private static final int TAG_POS = 0x2000001;
59    private static final int TAG_SPAN = 0x2000002;
60
61    private static final int[] PASTRIES = {
62            R.drawable.dessert_kitkat,      // used with permission
63            R.drawable.dessert_android,     // thx irina
64    };
65
66    private static final int[] RARE_PASTRIES = {
67            R.drawable.dessert_cupcake,     // 2009
68            R.drawable.dessert_donut,       // 2009
69            R.drawable.dessert_eclair,      // 2009
70            R.drawable.dessert_froyo,       // 2010
71            R.drawable.dessert_gingerbread, // 2010
72            R.drawable.dessert_honeycomb,   // 2011
73            R.drawable.dessert_ics,         // 2011
74            R.drawable.dessert_jellybean,   // 2012
75    };
76
77    private static final int[] XRARE_PASTRIES = {
78            R.drawable.dessert_petitfour,   // the original and still delicious
79
80            R.drawable.dessert_donutburger, // remember kids, this was long before cronuts
81
82            R.drawable.dessert_flan,        //     sholes final approach
83                                            //     landing gear punted to flan
84                                            //     runway foam glistens
85                                            //         -- mcleron
86
87            R.drawable.dessert_keylimepie,  // from an alternative timeline
88    };
89    private static final int[] XXRARE_PASTRIES = {
90            R.drawable.dessert_zombiegingerbread, // thx hackbod
91            R.drawable.dessert_dandroid,    // thx morrildl
92            R.drawable.dessert_jandycane,   // thx nes
93    };
94
95    private static final int NUM_PASTRIES = PASTRIES.length + RARE_PASTRIES.length
96            + XRARE_PASTRIES.length + XXRARE_PASTRIES.length;
97
98    private SparseArray<Drawable> mDrawables = new SparseArray<Drawable>(NUM_PASTRIES);
99
100    private static final float[] MASK = {
101            0f,  0f,  0f,  0f, 255f,
102            0f,  0f,  0f,  0f, 255f,
103            0f,  0f,  0f,  0f, 255f,
104            1f,  0f,  0f,  0f, 0f
105    };
106
107    private static final float[] ALPHA_MASK = {
108            0f,  0f,  0f,  0f, 255f,
109            0f,  0f,  0f,  0f, 255f,
110            0f,  0f,  0f,  0f, 255f,
111            0f,  0f,  0f,  1f, 0f
112    };
113
114    private static final float[] WHITE_MASK = {
115            0f,  0f,  0f,  0f, 255f,
116            0f,  0f,  0f,  0f, 255f,
117            0f,  0f,  0f,  0f, 255f,
118            -1f,  0f,  0f,  0f, 255f
119    };
120
121    public static final float SCALE = 0.25f; // natural display size will be SCALE*mCellSize
122
123    private static final float PROB_2X = 0.33f;
124    private static final float PROB_3X = 0.1f;
125    private static final float PROB_4X = 0.01f;
126
127    private boolean mStarted;
128
129    private int mCellSize;
130    private int mWidth, mHeight;
131    private int mRows, mColumns;
132    private View[] mCells;
133
134    private final Set<Point> mFreeList = new HashSet<Point>();
135
136    private final Handler mHandler = new Handler();
137
138    private final Runnable mJuggle = new Runnable() {
139        @Override
140        public void run() {
141            final int N = getChildCount();
142
143            final int K = 1; //irand(1,3);
144            for (int i=0; i<K; i++) {
145                final View child = getChildAt((int) (Math.random() * N));
146                place(child, true);
147            }
148
149            fillFreeList();
150
151            if (mStarted) {
152                mHandler.postDelayed(mJuggle, DELAY);
153            }
154        }
155    };
156
157    public DessertCaseView(Context context) {
158        this(context, null);
159    }
160
161    public DessertCaseView(Context context, AttributeSet attrs) {
162        this(context, attrs, 0);
163    }
164
165    public DessertCaseView(Context context, AttributeSet attrs, int defStyle) {
166        super(context, attrs, defStyle);
167
168        final Resources res = getResources();
169
170        mStarted = false;
171
172        mCellSize = res.getDimensionPixelSize(R.dimen.dessert_case_cell_size);
173        final BitmapFactory.Options opts = new BitmapFactory.Options();
174        if (mCellSize < 512) { // assuming 512x512 images
175            opts.inSampleSize = 2;
176        }
177        opts.inMutable = true;
178        Bitmap loaded = null;
179        for (int[] list : new int[][] { PASTRIES, RARE_PASTRIES, XRARE_PASTRIES, XXRARE_PASTRIES }) {
180            for (int resid : list) {
181                opts.inBitmap = loaded;
182                loaded = BitmapFactory.decodeResource(res, resid, opts);
183                final BitmapDrawable d = new BitmapDrawable(res, convertToAlphaMask(loaded));
184                d.setColorFilter(new ColorMatrixColorFilter(ALPHA_MASK));
185                d.setBounds(0, 0, mCellSize, mCellSize);
186                mDrawables.append(resid, d);
187            }
188        }
189        loaded = null;
190        if (DEBUG) setWillNotDraw(false);
191    }
192
193    private static Bitmap convertToAlphaMask(Bitmap b) {
194        Bitmap a = Bitmap.createBitmap(b.getWidth(), b.getHeight(), Bitmap.Config.ALPHA_8);
195        Canvas c = new Canvas(a);
196        Paint pt = new Paint();
197        pt.setColorFilter(new ColorMatrixColorFilter(MASK));
198        c.drawBitmap(b, 0.0f, 0.0f, pt);
199        return a;
200    }
201
202    public void start() {
203        if (!mStarted) {
204            mStarted = true;
205            fillFreeList(DURATION * 4);
206        }
207        mHandler.postDelayed(mJuggle, START_DELAY);
208    }
209
210    public void stop() {
211        mStarted = false;
212        mHandler.removeCallbacks(mJuggle);
213    }
214
215    int pick(int[] a) {
216        return a[(int)(Math.random()*a.length)];
217    }
218
219    <T> T pick(T[] a) {
220        return a[(int)(Math.random()*a.length)];
221    }
222
223    <T> T pick(SparseArray<T> sa) {
224        return sa.valueAt((int)(Math.random()*sa.size()));
225    }
226
227    float[] hsv = new float[] { 0, 1f, .85f };
228    int random_color() {
229//        return 0xFF000000 | (int) (Math.random() * (float) 0xFFFFFF); // totally random
230        final int COLORS = 12;
231        hsv[0] = irand(0,COLORS) * (360f/COLORS);
232        return Color.HSVToColor(hsv);
233    }
234
235    @Override
236    protected synchronized void onSizeChanged (int w, int h, int oldw, int oldh) {
237        super.onSizeChanged(w, h, oldw, oldh);
238        if (mWidth == w && mHeight == h) return;
239
240        final boolean wasStarted = mStarted;
241        if (wasStarted) {
242            stop();
243        }
244
245        mWidth = w;
246        mHeight = h;
247
248        mCells = null;
249        removeAllViewsInLayout();
250        mFreeList.clear();
251
252        mRows = mHeight / mCellSize;
253        mColumns = mWidth / mCellSize;
254
255        mCells = new View[mRows * mColumns];
256
257        if (DEBUG) Log.v(TAG, String.format("New dimensions: %dx%d", mColumns, mRows));
258
259        setScaleX(SCALE);
260        setScaleY(SCALE);
261        setTranslationX(0.5f * (mWidth - mCellSize * mColumns) * SCALE);
262        setTranslationY(0.5f * (mHeight - mCellSize * mRows) * SCALE);
263
264        for (int j=0; j<mRows; j++) {
265            for (int i=0; i<mColumns; i++) {
266                mFreeList.add(new Point(i,j));
267            }
268        }
269
270        if (wasStarted) {
271            start();
272        }
273    }
274
275    public void fillFreeList() {
276        fillFreeList(DURATION);
277    }
278
279    public synchronized void fillFreeList(int animationLen) {
280        final Context ctx = getContext();
281        final FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(mCellSize, mCellSize);
282
283        while (! mFreeList.isEmpty()) {
284            Point pt = mFreeList.iterator().next();
285            mFreeList.remove(pt);
286            final int i=pt.x;
287            final int j=pt.y;
288
289            if (mCells[j*mColumns+i] != null) continue;
290            final ImageView v = new ImageView(ctx);
291            v.setOnClickListener(new OnClickListener() {
292                @Override
293                public void onClick(View view) {
294                    place(v, true);
295                    postDelayed(new Runnable() { public void run() { fillFreeList(); } }, DURATION/2);
296                }
297            });
298
299            final int c = random_color();
300            v.setBackgroundColor(c);
301
302            final float which = frand();
303            final Drawable d;
304            if (which < 0.0005f) {
305                d = mDrawables.get(pick(XXRARE_PASTRIES));
306            } else if (which < 0.005f) {
307                d = mDrawables.get(pick(XRARE_PASTRIES));
308            } else if (which < 0.5f) {
309                d = mDrawables.get(pick(RARE_PASTRIES));
310            } else if (which < 0.7f) {
311                d = mDrawables.get(pick(PASTRIES));
312            } else {
313                d = null;
314            }
315            if (d != null) {
316                v.getOverlay().add(d);
317            }
318
319            lp.width = lp.height = mCellSize;
320            addView(v, lp);
321            place(v, pt, false);
322            if (animationLen > 0) {
323                final float s = (Integer) v.getTag(TAG_SPAN);
324                v.setScaleX(0.5f * s);
325                v.setScaleY(0.5f * s);
326                v.setAlpha(0f);
327                v.animate().withLayer().scaleX(s).scaleY(s).alpha(1f).setDuration(animationLen);
328            }
329        }
330    }
331
332    public void place(View v, boolean animate) {
333        place(v, new Point(irand(0, mColumns), irand(0, mRows)), animate);
334    }
335
336    // we don't have .withLayer() on general Animators
337    private final Animator.AnimatorListener makeHardwareLayerListener(final View v) {
338        return new AnimatorListenerAdapter() {
339            @Override
340            public void onAnimationStart(Animator animator) {
341                v.setLayerType(View.LAYER_TYPE_HARDWARE, null);
342                v.buildLayer();
343            }
344            @Override
345            public void onAnimationEnd(Animator animator) {
346                v.setLayerType(View.LAYER_TYPE_NONE, null);
347            }
348        };
349    }
350
351    private final HashSet<View> tmpSet = new HashSet<View>();
352    public synchronized void place(View v, Point pt, boolean animate) {
353        final int i = pt.x;
354        final int j = pt.y;
355        final float rnd = frand();
356        if (v.getTag(TAG_POS) != null) {
357            for (final Point oc : getOccupied(v)) {
358                mFreeList.add(oc);
359                mCells[oc.y*mColumns + oc.x] = null;
360            }
361        }
362        int scale = 1;
363        if (rnd < PROB_4X) {
364            if (!(i >= mColumns-3 || j >= mRows-3)) {
365                scale = 4;
366            }
367        } else if (rnd < PROB_3X) {
368            if (!(i >= mColumns-2 || j >= mRows-2)) {
369                scale = 3;
370            }
371        } else if (rnd < PROB_2X) {
372            if (!(i == mColumns-1 || j == mRows-1)) {
373                scale = 2;
374            }
375        }
376
377        v.setTag(TAG_POS, pt);
378        v.setTag(TAG_SPAN, scale);
379
380        tmpSet.clear();
381
382        final Point[] occupied = getOccupied(v);
383        for (final Point oc : occupied) {
384            final View squatter = mCells[oc.y*mColumns + oc.x];
385            if (squatter != null) {
386                tmpSet.add(squatter);
387            }
388        }
389
390        for (final View squatter : tmpSet) {
391            for (final Point sq : getOccupied(squatter)) {
392                mFreeList.add(sq);
393                mCells[sq.y*mColumns + sq.x] = null;
394            }
395            if (squatter != v) {
396                squatter.setTag(TAG_POS, null);
397                if (animate) {
398                    squatter.animate().withLayer()
399                            .scaleX(0.5f).scaleY(0.5f).alpha(0)
400                            .setDuration(DURATION)
401                            .setInterpolator(new AccelerateInterpolator())
402                            .setListener(new Animator.AnimatorListener() {
403                                public void onAnimationStart(Animator animator) { }
404                                public void onAnimationEnd(Animator animator) {
405                                    removeView(squatter);
406                                }
407                                public void onAnimationCancel(Animator animator) { }
408                                public void onAnimationRepeat(Animator animator) { }
409                            })
410                            .start();
411                } else {
412                    removeView(squatter);
413                }
414            }
415        }
416
417        for (final Point oc : occupied) {
418            mCells[oc.y*mColumns + oc.x] = v;
419            mFreeList.remove(oc);
420        }
421
422        final float rot = (float)irand(0, 4) * 90f;
423
424        if (animate) {
425            v.bringToFront();
426
427            AnimatorSet set1 = new AnimatorSet();
428            set1.playTogether(
429                    ObjectAnimator.ofFloat(v, View.SCALE_X, (float) scale),
430                    ObjectAnimator.ofFloat(v, View.SCALE_Y, (float) scale)
431            );
432            set1.setInterpolator(new AnticipateOvershootInterpolator());
433            set1.setDuration(DURATION);
434
435            AnimatorSet set2 = new AnimatorSet();
436            set2.playTogether(
437                    ObjectAnimator.ofFloat(v, View.ROTATION, rot),
438                    ObjectAnimator.ofFloat(v, View.X, i* mCellSize + (scale-1) * mCellSize /2),
439                    ObjectAnimator.ofFloat(v, View.Y, j* mCellSize + (scale-1) * mCellSize /2)
440            );
441            set2.setInterpolator(new DecelerateInterpolator());
442            set2.setDuration(DURATION);
443
444            set1.addListener(makeHardwareLayerListener(v));
445
446            set1.start();
447            set2.start();
448        } else {
449            v.setX(i * mCellSize + (scale-1) * mCellSize /2);
450            v.setY(j * mCellSize + (scale-1) * mCellSize /2);
451            v.setScaleX((float) scale);
452            v.setScaleY((float) scale);
453            v.setRotation(rot);
454        }
455    }
456
457    private Point[] getOccupied(View v) {
458        final int scale = (Integer) v.getTag(TAG_SPAN);
459        final Point pt = (Point)v.getTag(TAG_POS);
460        if (pt == null || scale == 0) return new Point[0];
461
462        final Point[] result = new Point[scale * scale];
463        int p=0;
464        for (int i=0; i<scale; i++) {
465            for (int j=0; j<scale; j++) {
466                result[p++] = new Point(pt.x + i, pt.y + j);
467            }
468        }
469        return result;
470    }
471
472    static float frand() {
473        return (float)(Math.random());
474    }
475
476    static float frand(float a, float b) {
477        return (frand() * (b-a) + a);
478    }
479
480    static int irand(int a, int b) {
481        return (int)(frand(a, b));
482    }
483
484    @Override
485    public void onDraw(Canvas c) {
486        super.onDraw(c);
487        if (!DEBUG) return;
488
489        Paint pt = new Paint();
490        pt.setStyle(Paint.Style.STROKE);
491        pt.setColor(0xFFCCCCCC);
492        pt.setStrokeWidth(2.0f);
493
494        final Rect check = new Rect();
495        final int N = getChildCount();
496        for (int i = 0; i < N; i++) {
497            View stone = getChildAt(i);
498
499            stone.getHitRect(check);
500
501            c.drawRect(check, pt);
502        }
503    }
504
505    public static class RescalingContainer extends FrameLayout {
506        private DessertCaseView mView;
507        private float mDarkness;
508
509        public RescalingContainer(Context context) {
510            super(context);
511
512            setSystemUiVisibility(0
513                    | View.SYSTEM_UI_FLAG_FULLSCREEN
514                    | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
515                    | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
516                    | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
517                    | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
518            );
519        }
520
521        public void setView(DessertCaseView v) {
522            addView(v);
523            mView = v;
524        }
525
526        @Override
527        protected void onLayout (boolean changed, int left, int top, int right, int bottom) {
528            final float w = right-left;
529            final float h = bottom-top;
530            final int w2 = (int) (w / mView.SCALE / 2);
531            final int h2 = (int) (h / mView.SCALE / 2);
532            final int cx = (int) (left + w * 0.5f);
533            final int cy = (int) (top + h * 0.5f);
534            mView.layout(cx - w2, cy - h2, cx + w2, cy + h2);
535        }
536
537        public void setDarkness(float p) {
538            mDarkness = p;
539            getDarkness();
540            final int x = (int) (p * 0xff);
541            setBackgroundColor(x << 24 & 0xFF000000);
542        }
543
544        public float getDarkness() {
545            return mDarkness;
546        }
547    }
548}
549