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