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.Bitmap;
26 import android.graphics.BitmapFactory;
27 import android.graphics.Canvas;
28 import android.graphics.Color;
29 import android.graphics.ColorMatrixColorFilter;
30 import android.graphics.Paint;
31 import android.graphics.Point;
32 import android.graphics.Rect;
33 import android.graphics.drawable.BitmapDrawable;
34 import android.graphics.drawable.Drawable;
35 import android.os.Handler;
36 import android.util.AttributeSet;
37 import android.util.Log;
38 import android.util.SparseArray;
39 import android.view.View;
40 import android.view.animation.AccelerateInterpolator;
41 import android.view.animation.AnticipateOvershootInterpolator;
42 import android.view.animation.DecelerateInterpolator;
43 import android.widget.FrameLayout;
44 import android.widget.ImageView;
45 
46 import java.util.HashSet;
47 import java.util.Set;
48 
49 public 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 
DessertCaseView(Context context)157     public DessertCaseView(Context context) {
158         this(context, null);
159     }
160 
DessertCaseView(Context context, AttributeSet attrs)161     public DessertCaseView(Context context, AttributeSet attrs) {
162         this(context, attrs, 0);
163     }
164 
DessertCaseView(Context context, AttributeSet attrs, int defStyle)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 
convertToAlphaMask(Bitmap b)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 
start()202     public void start() {
203         if (!mStarted) {
204             mStarted = true;
205             fillFreeList(DURATION * 4);
206         }
207         mHandler.postDelayed(mJuggle, START_DELAY);
208     }
209 
stop()210     public void stop() {
211         mStarted = false;
212         mHandler.removeCallbacks(mJuggle);
213     }
214 
pick(int[] a)215     int pick(int[] a) {
216         return a[(int)(Math.random()*a.length)];
217     }
218 
pick(T[] a)219     <T> T pick(T[] a) {
220         return a[(int)(Math.random()*a.length)];
221     }
222 
pick(SparseArray<T> sa)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 };
random_color()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
onSizeChanged(int w, int h, int oldw, int oldh)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 
fillFreeList()275     public void fillFreeList() {
276         fillFreeList(DURATION);
277     }
278 
fillFreeList(int animationLen)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 
place(View v, boolean animate)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
makeHardwareLayerListener(final View v)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