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