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 
17 package com.android.systemui;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.AnimatorSet;
22 import android.animation.ObjectAnimator;
23 import android.content.Context;
24 import android.content.res.Resources;
25 import android.graphics.*;
26 import android.graphics.drawable.BitmapDrawable;
27 import android.graphics.drawable.Drawable;
28 import android.os.Handler;
29 import android.util.AttributeSet;
30 import android.util.Log;
31 import android.util.SparseArray;
32 import android.view.View;
33 import android.view.animation.AccelerateInterpolator;
34 import android.view.animation.AnticipateOvershootInterpolator;
35 import android.view.animation.DecelerateInterpolator;
36 import android.widget.FrameLayout;
37 import android.widget.ImageView;
38 
39 import java.util.HashSet;
40 import java.util.Set;
41 
42 public 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 
DessertCaseView(Context context)150     public DessertCaseView(Context context) {
151         this(context, null);
152     }
153 
DessertCaseView(Context context, AttributeSet attrs)154     public DessertCaseView(Context context, AttributeSet attrs) {
155         this(context, attrs, 0);
156     }
157 
DessertCaseView(Context context, AttributeSet attrs, int defStyle)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 
convertToAlphaMask(Bitmap b)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 
start()195     public void start() {
196         if (!mStarted) {
197             mStarted = true;
198             fillFreeList(DURATION * 4);
199         }
200         mHandler.postDelayed(mJuggle, START_DELAY);
201     }
202 
stop()203     public void stop() {
204         mStarted = false;
205         mHandler.removeCallbacks(mJuggle);
206     }
207 
pick(int[] a)208     int pick(int[] a) {
209         return a[(int)(Math.random()*a.length)];
210     }
211 
pick(T[] a)212     <T> T pick(T[] a) {
213         return a[(int)(Math.random()*a.length)];
214     }
215 
pick(SparseArray<T> sa)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 };
random_color()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
onSizeChanged(int w, int h, int oldw, int oldh)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 
fillFreeList()268     public void fillFreeList() {
269         fillFreeList(DURATION);
270     }
271 
fillFreeList(int animationLen)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 
place(View v, boolean animate)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
makeHardwareLayerListener(final View v)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