Home | History | Annotate | Download | only in egg
      1 /*
      2  * Copyright (C) 2014 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.egg;
     18 
     19 import android.animation.TimeAnimator;
     20 import android.content.Context;
     21 import android.content.res.Resources;
     22 import android.graphics.Canvas;
     23 import android.graphics.Color;
     24 import android.graphics.Matrix;
     25 import android.graphics.Outline;
     26 import android.graphics.Paint;
     27 import android.graphics.Path;
     28 import android.graphics.PorterDuff;
     29 import android.graphics.Rect;
     30 import android.graphics.drawable.Drawable;
     31 import android.graphics.drawable.GradientDrawable;
     32 import android.util.AttributeSet;
     33 import android.util.Log;
     34 import android.view.Gravity;
     35 import android.view.KeyEvent;
     36 import android.view.MotionEvent;
     37 import android.view.View;
     38 import android.view.ViewOutlineProvider;
     39 import android.view.animation.DecelerateInterpolator;
     40 import android.widget.FrameLayout;
     41 import android.widget.ImageView;
     42 import android.widget.TextView;
     43 
     44 import com.android.systemui.R;
     45 
     46 import java.util.ArrayList;
     47 
     48 public class LLand extends FrameLayout {
     49     public static final String TAG = "LLand";
     50 
     51     public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
     52     public static final boolean DEBUG_DRAW = false; // DEBUG
     53 
     54     public static final void L(String s, Object ... objects) {
     55         if (DEBUG) {
     56             Log.d(TAG, String.format(s, objects));
     57         }
     58     }
     59 
     60     public static final boolean AUTOSTART = true;
     61     public static final boolean HAVE_STARS = true;
     62 
     63     public static final float DEBUG_SPEED_MULTIPLIER = 1f; // 0.1f;
     64     public static final boolean DEBUG_IDDQD = false;
     65 
     66     final static int[] POPS = {
     67             // resid                // spinny!
     68             R.drawable.pop_belt,    0,
     69             R.drawable.pop_droid,   0,
     70             R.drawable.pop_pizza,   1,
     71             R.drawable.pop_stripes, 0,
     72             R.drawable.pop_swirl,   1,
     73             R.drawable.pop_vortex,  1,
     74             R.drawable.pop_vortex2, 1,
     75     };
     76 
     77     private static class Params {
     78         public float TRANSLATION_PER_SEC;
     79         public int OBSTACLE_SPACING, OBSTACLE_PERIOD;
     80         public int BOOST_DV;
     81         public int PLAYER_HIT_SIZE;
     82         public int PLAYER_SIZE;
     83         public int OBSTACLE_WIDTH, OBSTACLE_STEM_WIDTH;
     84         public int OBSTACLE_GAP;
     85         public int OBSTACLE_MIN;
     86         public int BUILDING_WIDTH_MIN, BUILDING_WIDTH_MAX;
     87         public int BUILDING_HEIGHT_MIN;
     88         public int CLOUD_SIZE_MIN, CLOUD_SIZE_MAX;
     89         public int STAR_SIZE_MIN, STAR_SIZE_MAX;
     90         public int G;
     91         public int MAX_V;
     92             public float SCENERY_Z, OBSTACLE_Z, PLAYER_Z, PLAYER_Z_BOOST, HUD_Z;
     93         public Params(Resources res) {
     94             TRANSLATION_PER_SEC = res.getDimension(R.dimen.translation_per_sec);
     95             OBSTACLE_SPACING = res.getDimensionPixelSize(R.dimen.obstacle_spacing);
     96             OBSTACLE_PERIOD = (int) (OBSTACLE_SPACING / TRANSLATION_PER_SEC);
     97             BOOST_DV = res.getDimensionPixelSize(R.dimen.boost_dv);
     98             PLAYER_HIT_SIZE = res.getDimensionPixelSize(R.dimen.player_hit_size);
     99             PLAYER_SIZE = res.getDimensionPixelSize(R.dimen.player_size);
    100             OBSTACLE_WIDTH = res.getDimensionPixelSize(R.dimen.obstacle_width);
    101             OBSTACLE_STEM_WIDTH = res.getDimensionPixelSize(R.dimen.obstacle_stem_width);
    102             OBSTACLE_GAP = res.getDimensionPixelSize(R.dimen.obstacle_gap);
    103             OBSTACLE_MIN = res.getDimensionPixelSize(R.dimen.obstacle_height_min);
    104             BUILDING_HEIGHT_MIN = res.getDimensionPixelSize(R.dimen.building_height_min);
    105             BUILDING_WIDTH_MIN = res.getDimensionPixelSize(R.dimen.building_width_min);
    106             BUILDING_WIDTH_MAX = res.getDimensionPixelSize(R.dimen.building_width_max);
    107             CLOUD_SIZE_MIN = res.getDimensionPixelSize(R.dimen.cloud_size_min);
    108             CLOUD_SIZE_MAX = res.getDimensionPixelSize(R.dimen.cloud_size_max);
    109             STAR_SIZE_MIN = res.getDimensionPixelSize(R.dimen.star_size_min);
    110             STAR_SIZE_MAX = res.getDimensionPixelSize(R.dimen.star_size_max);
    111 
    112             G = res.getDimensionPixelSize(R.dimen.G);
    113             MAX_V = res.getDimensionPixelSize(R.dimen.max_v);
    114 
    115             SCENERY_Z = res.getDimensionPixelSize(R.dimen.scenery_z);
    116             OBSTACLE_Z = res.getDimensionPixelSize(R.dimen.obstacle_z);
    117             PLAYER_Z = res.getDimensionPixelSize(R.dimen.player_z);
    118             PLAYER_Z_BOOST = res.getDimensionPixelSize(R.dimen.player_z_boost);
    119             HUD_Z = res.getDimensionPixelSize(R.dimen.hud_z);
    120         }
    121     }
    122 
    123     private TimeAnimator mAnim;
    124 
    125     private TextView mScoreField;
    126     private View mSplash;
    127 
    128     private Player mDroid;
    129     private ArrayList<Obstacle> mObstaclesInPlay = new ArrayList<Obstacle>();
    130 
    131     private float t, dt;
    132 
    133     private int mScore;
    134     private float mLastPipeTime; // in sec
    135     private int mWidth, mHeight;
    136     private boolean mAnimating, mPlaying;
    137     private boolean mFrozen; // after death, a short backoff
    138     private boolean mFlipped;
    139 
    140     private int mTimeOfDay;
    141     private static final int DAY = 0, NIGHT = 1, TWILIGHT = 2, SUNSET = 3;
    142     private static final int[][] SKIES = {
    143             { 0xFFc0c0FF, 0xFFa0a0FF }, // DAY
    144             { 0xFF000010, 0xFF000000 }, // NIGHT
    145             { 0xFF000040, 0xFF000010 }, // TWILIGHT
    146             { 0xFFa08020, 0xFF204080 }, // SUNSET
    147     };
    148 
    149     private static Params PARAMS;
    150 
    151     public LLand(Context context) {
    152         this(context, null);
    153     }
    154 
    155     public LLand(Context context, AttributeSet attrs) {
    156         this(context, attrs, 0);
    157     }
    158 
    159     public LLand(Context context, AttributeSet attrs, int defStyle) {
    160         super(context, attrs, defStyle);
    161         setFocusable(true);
    162         PARAMS = new Params(getResources());
    163         mTimeOfDay = irand(0, SKIES.length);
    164     }
    165 
    166     @Override
    167     public boolean willNotDraw() {
    168         return !DEBUG;
    169     }
    170 
    171     public int getGameWidth() { return mWidth; }
    172     public int getGameHeight() { return mHeight; }
    173     public float getGameTime() { return t; }
    174     public float getLastTimeStep() { return dt; }
    175 
    176     public void setScoreField(TextView tv) {
    177         mScoreField = tv;
    178         if (tv != null) {
    179             tv.setTranslationZ(PARAMS.HUD_Z);
    180             if (!(mAnimating && mPlaying)) {
    181                 tv.setTranslationY(-500);
    182             }
    183         }
    184     }
    185 
    186     public void setSplash(View v) {
    187         mSplash = v;
    188     }
    189 
    190     @Override
    191     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    192         stop();
    193         reset();
    194         if (AUTOSTART) {
    195             start(false);
    196         }
    197     }
    198 
    199     final float hsv[] = {0, 0, 0};
    200 
    201     private void reset() {
    202         L("reset");
    203         final Drawable sky = new GradientDrawable(
    204                 GradientDrawable.Orientation.BOTTOM_TOP,
    205                 SKIES[mTimeOfDay]
    206         );
    207         sky.setDither(true);
    208         setBackground(sky);
    209 
    210         mFlipped = frand() > 0.5f;
    211         setScaleX(mFlipped ? -1 : 1);
    212 
    213         setScore(0);
    214 
    215         int i = getChildCount();
    216         while (i-->0) {
    217             final View v = getChildAt(i);
    218             if (v instanceof GameView) {
    219                 removeViewAt(i);
    220             }
    221         }
    222 
    223         mObstaclesInPlay.clear();
    224 
    225         mWidth = getWidth();
    226         mHeight = getHeight();
    227 
    228         boolean showingSun = (mTimeOfDay == DAY || mTimeOfDay == SUNSET) && frand() > 0.25;
    229         if (showingSun) {
    230             final Star sun = new Star(getContext());
    231             sun.setBackgroundResource(R.drawable.sun);
    232             final int w = getResources().getDimensionPixelSize(R.dimen.sun_size);
    233             sun.setTranslationX(frand(w, mWidth-w));
    234             if (mTimeOfDay == DAY) {
    235                 sun.setTranslationY(frand(w, (mHeight * 0.66f)));
    236                 sun.getBackground().setTint(0);
    237             } else {
    238                 sun.setTranslationY(frand(mHeight * 0.66f, mHeight - w));
    239                 sun.getBackground().setTintMode(PorterDuff.Mode.SRC_ATOP);
    240                 sun.getBackground().setTint(0xC0FF8000);
    241 
    242             }
    243             addView(sun, new LayoutParams(w, w));
    244         }
    245         if (!showingSun) {
    246             final boolean dark = mTimeOfDay == NIGHT || mTimeOfDay == TWILIGHT;
    247             final float ff = frand();
    248             if ((dark && ff < 0.75f) || ff < 0.5f) {
    249                 final Star moon = new Star(getContext());
    250                 moon.setBackgroundResource(R.drawable.moon);
    251                 moon.getBackground().setAlpha(dark ? 255 : 128);
    252                 moon.setScaleX(frand() > 0.5 ? -1 : 1);
    253                 moon.setRotation(moon.getScaleX() * frand(5, 30));
    254                 final int w = getResources().getDimensionPixelSize(R.dimen.sun_size);
    255                 moon.setTranslationX(frand(w, mWidth - w));
    256                 moon.setTranslationY(frand(w, mHeight - w));
    257                 addView(moon, new LayoutParams(w, w));
    258             }
    259         }
    260 
    261         final int mh = mHeight / 6;
    262         final boolean cloudless = frand() < 0.25;
    263         final int N = 20;
    264         for (i=0; i<N; i++) {
    265             final float r1 = frand();
    266             final Scenery s;
    267             if (HAVE_STARS && r1 < 0.3 && mTimeOfDay != DAY) {
    268                 s = new Star(getContext());
    269             } else if (r1 < 0.6 && !cloudless) {
    270                 s = new Cloud(getContext());
    271             } else {
    272                 s = new Building(getContext());
    273 
    274                 s.z = (float)i/N;
    275                 s.setTranslationZ(PARAMS.SCENERY_Z * (1+s.z));
    276                 s.v = 0.85f * s.z; // buildings move proportional to their distance
    277                 hsv[0] = 175;
    278                 hsv[1] = 0.25f;
    279                 hsv[2] = 1 * s.z;
    280                 s.setBackgroundColor(Color.HSVToColor(hsv));
    281                 s.h = irand(PARAMS.BUILDING_HEIGHT_MIN, mh);
    282             }
    283             final LayoutParams lp = new LayoutParams(s.w, s.h);
    284             if (s instanceof Building) {
    285                 lp.gravity = Gravity.BOTTOM;
    286             } else {
    287                 lp.gravity = Gravity.TOP;
    288                 final float r = frand();
    289                 if (s instanceof Star) {
    290                     lp.topMargin = (int) (r * r * mHeight);
    291                 } else {
    292                     lp.topMargin = (int) (1 - r*r * mHeight/2) + mHeight/2;
    293                 }
    294             }
    295 
    296             addView(s, lp);
    297             s.setTranslationX(frand(-lp.width, mWidth + lp.width));
    298         }
    299 
    300         mDroid = new Player(getContext());
    301         mDroid.setX(mWidth / 2);
    302         mDroid.setY(mHeight / 2);
    303         addView(mDroid, new LayoutParams(PARAMS.PLAYER_SIZE, PARAMS.PLAYER_SIZE));
    304 
    305         mAnim = new TimeAnimator();
    306         mAnim.setTimeListener(new TimeAnimator.TimeListener() {
    307             @Override
    308             public void onTimeUpdate(TimeAnimator timeAnimator, long t, long dt) {
    309                 step(t, dt);
    310             }
    311         });
    312     }
    313 
    314     private void setScore(int score) {
    315         mScore = score;
    316         if (mScoreField != null) mScoreField.setText(String.valueOf(score));
    317     }
    318 
    319     private void addScore(int incr) {
    320         setScore(mScore + incr);
    321     }
    322 
    323     private void start(boolean startPlaying) {
    324         L("start(startPlaying=%s)", startPlaying?"true":"false");
    325         if (startPlaying) {
    326             mPlaying = true;
    327 
    328             t = 0;
    329             // there's a sucker born every OBSTACLE_PERIOD
    330             mLastPipeTime = getGameTime() - PARAMS.OBSTACLE_PERIOD;
    331 
    332             if (mSplash != null && mSplash.getAlpha() > 0f) {
    333                 mSplash.setTranslationZ(PARAMS.HUD_Z);
    334                 mSplash.animate().alpha(0).translationZ(0).setDuration(400);
    335 
    336                 mScoreField.animate().translationY(0)
    337                         .setInterpolator(new DecelerateInterpolator())
    338                         .setDuration(1500);
    339             }
    340 
    341             mScoreField.setTextColor(0xFFAAAAAA);
    342             mScoreField.setBackgroundResource(R.drawable.scorecard);
    343             mDroid.setVisibility(View.VISIBLE);
    344             mDroid.setX(mWidth / 2);
    345             mDroid.setY(mHeight / 2);
    346         } else {
    347             mDroid.setVisibility(View.GONE);
    348         }
    349         if (!mAnimating) {
    350             mAnim.start();
    351             mAnimating = true;
    352         }
    353     }
    354 
    355     private void stop() {
    356         if (mAnimating) {
    357             mAnim.cancel();
    358             mAnim = null;
    359             mAnimating = false;
    360             mScoreField.setTextColor(0xFFFFFFFF);
    361             mScoreField.setBackgroundResource(R.drawable.scorecard_gameover);
    362             mTimeOfDay = irand(0, SKIES.length); // for next reset
    363             mFrozen = true;
    364             postDelayed(new Runnable() {
    365                     @Override
    366                     public void run() {
    367                         mFrozen = false;
    368                     }
    369                 }, 250);
    370         }
    371     }
    372 
    373     public static final float lerp(float x, float a, float b) {
    374         return (b - a) * x + a;
    375     }
    376 
    377     public static final float rlerp(float v, float a, float b) {
    378         return (v - a) / (b - a);
    379     }
    380 
    381     public static final float clamp(float f) {
    382         return f < 0f ? 0f : f > 1f ? 1f : f;
    383     }
    384 
    385     public static final float frand() {
    386         return (float) Math.random();
    387     }
    388 
    389     public static final float frand(float a, float b) {
    390         return lerp(frand(), a, b);
    391     }
    392 
    393     public static final int irand(int a, int b) {
    394         return (int) lerp(frand(), (float) a, (float) b);
    395     }
    396 
    397     private void step(long t_ms, long dt_ms) {
    398         t = t_ms / 1000f; // seconds
    399         dt = dt_ms / 1000f;
    400 
    401         if (DEBUG) {
    402             t *= DEBUG_SPEED_MULTIPLIER;
    403             dt *= DEBUG_SPEED_MULTIPLIER;
    404         }
    405 
    406         // 1. Move all objects and update bounds
    407         final int N = getChildCount();
    408         int i = 0;
    409         for (; i<N; i++) {
    410             final View v = getChildAt(i);
    411             if (v instanceof GameView) {
    412                 ((GameView) v).step(t_ms, dt_ms, t, dt);
    413             }
    414         }
    415 
    416         // 2. Check for altitude
    417         if (mPlaying && mDroid.below(mHeight)) {
    418             if (DEBUG_IDDQD) {
    419                 poke();
    420             } else {
    421                 L("player hit the floor");
    422                 stop();
    423             }
    424         }
    425 
    426         // 3. Check for obstacles
    427         boolean passedBarrier = false;
    428         for (int j = mObstaclesInPlay.size(); j-->0;) {
    429             final Obstacle ob = mObstaclesInPlay.get(j);
    430             if (mPlaying && ob.intersects(mDroid) && !DEBUG_IDDQD) {
    431                 L("player hit an obstacle");
    432                 stop();
    433             } else if (ob.cleared(mDroid)) {
    434                 if (ob instanceof Stem) passedBarrier = true;
    435                 mObstaclesInPlay.remove(j);
    436             }
    437         }
    438 
    439         if (mPlaying && passedBarrier) {
    440             addScore(1);
    441         }
    442 
    443         // 4. Handle edge of screen
    444         // Walk backwards to make sure removal is safe
    445         while (i-->0) {
    446             final View v = getChildAt(i);
    447             if (v instanceof Obstacle) {
    448                 if (v.getTranslationX() + v.getWidth() < 0) {
    449                     removeViewAt(i);
    450                 }
    451             } else if (v instanceof Scenery) {
    452                 final Scenery s = (Scenery) v;
    453                 if (v.getTranslationX() + s.w < 0) {
    454                     v.setTranslationX(getWidth());
    455                 }
    456             }
    457         }
    458 
    459         // 3. Time for more obstacles!
    460         if (mPlaying && (t - mLastPipeTime) > PARAMS.OBSTACLE_PERIOD) {
    461             mLastPipeTime = t;
    462             final int obstacley = (int) (Math.random()
    463                     * (mHeight - 2*PARAMS.OBSTACLE_MIN - PARAMS.OBSTACLE_GAP)) + PARAMS.OBSTACLE_MIN;
    464 
    465             final int inset = (PARAMS.OBSTACLE_WIDTH - PARAMS.OBSTACLE_STEM_WIDTH) / 2;
    466             final int yinset = PARAMS.OBSTACLE_WIDTH/2;
    467 
    468             final int d1 = irand(0,250);
    469             final Obstacle s1 = new Stem(getContext(), obstacley - yinset, false);
    470             addView(s1, new LayoutParams(
    471                     PARAMS.OBSTACLE_STEM_WIDTH,
    472                     (int) s1.h,
    473                     Gravity.TOP|Gravity.LEFT));
    474             s1.setTranslationX(mWidth+inset);
    475             s1.setTranslationY(-s1.h-yinset);
    476             s1.setTranslationZ(PARAMS.OBSTACLE_Z*0.75f);
    477             s1.animate()
    478                     .translationY(0)
    479                     .setStartDelay(d1)
    480                     .setDuration(250);
    481             mObstaclesInPlay.add(s1);
    482 
    483             final Obstacle p1 = new Pop(getContext(), PARAMS.OBSTACLE_WIDTH);
    484             addView(p1, new LayoutParams(
    485                     PARAMS.OBSTACLE_WIDTH,
    486                     PARAMS.OBSTACLE_WIDTH,
    487                     Gravity.TOP|Gravity.LEFT));
    488             p1.setTranslationX(mWidth);
    489             p1.setTranslationY(-PARAMS.OBSTACLE_WIDTH);
    490             p1.setTranslationZ(PARAMS.OBSTACLE_Z);
    491             p1.setScaleX(0.25f);
    492             p1.setScaleY(0.25f);
    493             p1.animate()
    494                     .translationY(s1.h-inset)
    495                     .scaleX(1f)
    496                     .scaleY(1f)
    497                     .setStartDelay(d1)
    498                     .setDuration(250);
    499             mObstaclesInPlay.add(p1);
    500 
    501             final int d2 = irand(0,250);
    502             final Obstacle s2 = new Stem(getContext(),
    503                     mHeight - obstacley - PARAMS.OBSTACLE_GAP - yinset,
    504                     true);
    505             addView(s2, new LayoutParams(
    506                     PARAMS.OBSTACLE_STEM_WIDTH,
    507                     (int) s2.h,
    508                     Gravity.TOP|Gravity.LEFT));
    509             s2.setTranslationX(mWidth+inset);
    510             s2.setTranslationY(mHeight+yinset);
    511             s2.setTranslationZ(PARAMS.OBSTACLE_Z*0.75f);
    512             s2.animate()
    513                     .translationY(mHeight-s2.h)
    514                     .setStartDelay(d2)
    515                     .setDuration(400);
    516             mObstaclesInPlay.add(s2);
    517 
    518             final Obstacle p2 = new Pop(getContext(), PARAMS.OBSTACLE_WIDTH);
    519             addView(p2, new LayoutParams(
    520                     PARAMS.OBSTACLE_WIDTH,
    521                     PARAMS.OBSTACLE_WIDTH,
    522                     Gravity.TOP|Gravity.LEFT));
    523             p2.setTranslationX(mWidth);
    524             p2.setTranslationY(mHeight);
    525             p2.setTranslationZ(PARAMS.OBSTACLE_Z);
    526             p2.setScaleX(0.25f);
    527             p2.setScaleY(0.25f);
    528             p2.animate()
    529                     .translationY(mHeight-s2.h-yinset)
    530                     .scaleX(1f)
    531                     .scaleY(1f)
    532                     .setStartDelay(d2)
    533                     .setDuration(400);
    534             mObstaclesInPlay.add(p2);
    535         }
    536 
    537         if (DEBUG_DRAW) invalidate();
    538     }
    539 
    540     @Override
    541     public boolean onTouchEvent(MotionEvent ev) {
    542         if (DEBUG) L("touch: %s", ev);
    543         switch (ev.getAction()) {
    544             case MotionEvent.ACTION_DOWN:
    545                 poke();
    546                 return true;
    547             case MotionEvent.ACTION_UP:
    548                 unpoke();
    549                 return true;
    550         }
    551         return false;
    552     }
    553 
    554     @Override
    555     public boolean onTrackballEvent(MotionEvent ev) {
    556         if (DEBUG) L("trackball: %s", ev);
    557         switch (ev.getAction()) {
    558             case MotionEvent.ACTION_DOWN:
    559                 poke();
    560                 return true;
    561             case MotionEvent.ACTION_UP:
    562                 unpoke();
    563                 return true;
    564         }
    565         return false;
    566     }
    567 
    568     @Override
    569     public boolean onKeyDown(int keyCode, KeyEvent ev) {
    570         if (DEBUG) L("keyDown: %d", keyCode);
    571         switch (keyCode) {
    572             case KeyEvent.KEYCODE_DPAD_CENTER:
    573             case KeyEvent.KEYCODE_DPAD_UP:
    574             case KeyEvent.KEYCODE_SPACE:
    575             case KeyEvent.KEYCODE_ENTER:
    576             case KeyEvent.KEYCODE_BUTTON_A:
    577                 poke();
    578                 return true;
    579         }
    580         return false;
    581     }
    582 
    583     @Override
    584     public boolean onKeyUp(int keyCode, KeyEvent ev) {
    585         if (DEBUG) L("keyDown: %d", keyCode);
    586         switch (keyCode) {
    587             case KeyEvent.KEYCODE_DPAD_CENTER:
    588             case KeyEvent.KEYCODE_DPAD_UP:
    589             case KeyEvent.KEYCODE_SPACE:
    590             case KeyEvent.KEYCODE_ENTER:
    591             case KeyEvent.KEYCODE_BUTTON_A:
    592                 unpoke();
    593                 return true;
    594         }
    595         return false;
    596     }
    597 
    598     @Override
    599     public boolean onGenericMotionEvent (MotionEvent ev) {
    600         if (DEBUG) L("generic: %s", ev);
    601         return false;
    602     }
    603 
    604     private void poke() {
    605         L("poke");
    606         if (mFrozen) return;
    607         if (!mAnimating) {
    608             reset();
    609             start(true);
    610         } else if (!mPlaying) {
    611             start(true);
    612         }
    613         mDroid.boost();
    614         if (DEBUG) {
    615             mDroid.dv *= DEBUG_SPEED_MULTIPLIER;
    616             mDroid.animate().setDuration((long) (200/DEBUG_SPEED_MULTIPLIER));
    617         }
    618     }
    619 
    620     private void unpoke() {
    621         L("unboost");
    622         if (mFrozen) return;
    623         if (!mAnimating) return;
    624         mDroid.unboost();
    625     }
    626 
    627     @Override
    628     public void onDraw(Canvas c) {
    629         super.onDraw(c);
    630 
    631         if (!DEBUG_DRAW) return;
    632 
    633         final Paint pt = new Paint();
    634         pt.setColor(0xFFFFFFFF);
    635         final int L = mDroid.corners.length;
    636         final int N = L/2;
    637         for (int i=0; i<N; i++) {
    638             final int x = (int) mDroid.corners[i*2];
    639             final int y = (int) mDroid.corners[i*2+1];
    640             c.drawCircle(x, y, 4, pt);
    641             c.drawLine(x, y,
    642                     mDroid.corners[(i*2+2)%L],
    643                     mDroid.corners[(i*2+3)%L],
    644                     pt);
    645         }
    646 
    647         pt.setStyle(Paint.Style.STROKE);
    648         pt.setStrokeWidth(getResources().getDisplayMetrics().density);
    649 
    650         final int M = getChildCount();
    651         pt.setColor(0x8000FF00);
    652         for (int i=0; i<M; i++) {
    653             final View v = getChildAt(i);
    654             if (v == mDroid) continue;
    655             if (!(v instanceof GameView)) continue;
    656             if (v instanceof Pop) {
    657                 final Pop p = (Pop) v;
    658                 c.drawCircle(p.cx, p.cy, p.r, pt);
    659             } else {
    660                 final Rect r = new Rect();
    661                 v.getHitRect(r);
    662                 c.drawRect(r, pt);
    663             }
    664         }
    665 
    666         pt.setColor(Color.BLACK);
    667         final StringBuilder sb = new StringBuilder("obstacles: ");
    668         for (Obstacle ob : mObstaclesInPlay) {
    669             sb.append(ob.hitRect.toShortString());
    670             sb.append(" ");
    671         }
    672         pt.setTextSize(20f);
    673         c.drawText(sb.toString(), 20, 100, pt);
    674     }
    675 
    676     static final Rect sTmpRect = new Rect();
    677 
    678     private interface GameView {
    679         public void step(long t_ms, long dt_ms, float t, float dt);
    680     }
    681 
    682     private class Player extends ImageView implements GameView {
    683         public float dv;
    684 
    685         private boolean mBoosting;
    686 
    687         private final float[] sHull = new float[] {
    688                 0.3f,  0f,    // left antenna
    689                 0.7f,  0f,    // right antenna
    690                 0.92f, 0.33f, // off the right shoulder of Orion
    691                 0.92f, 0.75f, // right hand (our right, not his right)
    692                 0.6f,  1f,    // right foot
    693                 0.4f,  1f,    // left foot BLUE!
    694                 0.08f, 0.75f, // sinistram
    695                 0.08f, 0.33f,  // cold shoulder
    696         };
    697         public final float[] corners = new float[sHull.length];
    698 
    699         public Player(Context context) {
    700             super(context);
    701 
    702             setBackgroundResource(R.drawable.android);
    703             getBackground().setTintMode(PorterDuff.Mode.SRC_ATOP);
    704             getBackground().setTint(0xFF00FF00);
    705             setOutlineProvider(new ViewOutlineProvider() {
    706                 @Override
    707                 public void getOutline(View view, Outline outline) {
    708                     final int w = view.getWidth();
    709                     final int h = view.getHeight();
    710                     final int ix = (int) (w * 0.3f);
    711                     final int iy = (int) (h * 0.2f);
    712                     outline.setRect(ix, iy, w - ix, h - iy);
    713                 }
    714             });
    715         }
    716 
    717         public void prepareCheckIntersections() {
    718             final int inset = (PARAMS.PLAYER_SIZE - PARAMS.PLAYER_HIT_SIZE)/2;
    719             final int scale = PARAMS.PLAYER_HIT_SIZE;
    720             final int N = sHull.length/2;
    721             for (int i=0; i<N; i++) {
    722                 corners[i*2]   = scale * sHull[i*2]   + inset;
    723                 corners[i*2+1] = scale * sHull[i*2+1] + inset;
    724             }
    725             final Matrix m = getMatrix();
    726             m.mapPoints(corners);
    727         }
    728 
    729         public boolean below(int h) {
    730             final int N = corners.length/2;
    731             for (int i=0; i<N; i++) {
    732                 final int y = (int) corners[i*2+1];
    733                 if (y >= h) return true;
    734             }
    735             return false;
    736         }
    737 
    738         public void step(long t_ms, long dt_ms, float t, float dt) {
    739             if (getVisibility() != View.VISIBLE) return; // not playing yet
    740 
    741             if (mBoosting) {
    742                 dv = -PARAMS.BOOST_DV;
    743             } else {
    744                 dv += PARAMS.G;
    745             }
    746             if (dv < -PARAMS.MAX_V) dv = -PARAMS.MAX_V;
    747             else if (dv > PARAMS.MAX_V) dv = PARAMS.MAX_V;
    748 
    749             final float y = getTranslationY() + dv * dt;
    750             setTranslationY(y < 0 ? 0 : y);
    751             setRotation(
    752                     90 + lerp(clamp(rlerp(dv, PARAMS.MAX_V, -1 * PARAMS.MAX_V)), 90, -90));
    753 
    754             prepareCheckIntersections();
    755         }
    756 
    757         public void boost() {
    758             mBoosting = true;
    759             dv = -PARAMS.BOOST_DV;
    760 
    761             animate().cancel();
    762             animate()
    763                     .scaleX(1.25f)
    764                     .scaleY(1.25f)
    765                     .translationZ(PARAMS.PLAYER_Z_BOOST)
    766                     .setDuration(100);
    767             setScaleX(1.25f);
    768             setScaleY(1.25f);
    769         }
    770 
    771         public void unboost() {
    772             mBoosting = false;
    773 
    774             animate().cancel();
    775             animate()
    776                     .scaleX(1f)
    777                     .scaleY(1f)
    778                     .translationZ(PARAMS.PLAYER_Z)
    779                     .setDuration(200);
    780         }
    781     }
    782 
    783     private class Obstacle extends View implements GameView {
    784         public float h;
    785 
    786         public final Rect hitRect = new Rect();
    787 
    788         public Obstacle(Context context, float h) {
    789             super(context);
    790             setBackgroundColor(0xFFFF0000);
    791             this.h = h;
    792         }
    793 
    794         public boolean intersects(Player p) {
    795             final int N = p.corners.length/2;
    796             for (int i=0; i<N; i++) {
    797                 final int x = (int) p.corners[i*2];
    798                 final int y = (int) p.corners[i*2+1];
    799                 if (hitRect.contains(x, y)) return true;
    800             }
    801             return false;
    802         }
    803 
    804         public boolean cleared(Player p) {
    805             final int N = p.corners.length/2;
    806             for (int i=0; i<N; i++) {
    807                 final int x = (int) p.corners[i*2];
    808                 if (hitRect.right >= x) return false;
    809             }
    810             return true;
    811         }
    812 
    813         @Override
    814         public void step(long t_ms, long dt_ms, float t, float dt) {
    815             setTranslationX(getTranslationX()-PARAMS.TRANSLATION_PER_SEC*dt);
    816             getHitRect(hitRect);
    817         }
    818     }
    819 
    820     private class Pop extends Obstacle {
    821         int mRotate;
    822         int cx, cy, r;
    823         public Pop(Context context, float h) {
    824             super(context, h);
    825             int idx = 2*irand(0, POPS.length/2);
    826             setBackgroundResource(POPS[idx]);
    827             setScaleX(frand() < 0.5f ? -1 : 1);
    828             mRotate = POPS[idx+1] == 0 ? 0 : (frand() < 0.5f ? -1 : 1);
    829             setOutlineProvider(new ViewOutlineProvider() {
    830                 @Override
    831                 public void getOutline(View view, Outline outline) {
    832                     final int pad = (int) (getWidth() * 0.02f);
    833                     outline.setOval(pad, pad, getWidth()-pad, getHeight()-pad);
    834                 }
    835             });
    836         }
    837 
    838         public boolean intersects(Player p) {
    839             final int N = p.corners.length/2;
    840             for (int i=0; i<N; i++) {
    841                 final int x = (int) p.corners[i*2];
    842                 final int y = (int) p.corners[i*2+1];
    843                 if (Math.hypot(x-cx, y-cy) <= r) return true;
    844             }
    845             return false;
    846         }
    847 
    848         @Override
    849         public void step(long t_ms, long dt_ms, float t, float dt) {
    850             super.step(t_ms, dt_ms, t, dt);
    851             if (mRotate != 0) {
    852                 setRotation(getRotation() + dt * 45 * mRotate);
    853             }
    854 
    855             cx = (hitRect.left + hitRect.right)/2;
    856             cy = (hitRect.top + hitRect.bottom)/2;
    857             r = getWidth()/2;
    858         }
    859     }
    860 
    861     private class Stem extends Obstacle {
    862         Paint mPaint = new Paint();
    863         Path mShadow = new Path();
    864         boolean mDrawShadow;
    865 
    866         public Stem(Context context, float h, boolean drawShadow) {
    867             super(context, h);
    868             mDrawShadow = drawShadow;
    869             mPaint.setColor(0xFFAAAAAA);
    870             setBackground(null);
    871         }
    872 
    873         @Override
    874         public void onAttachedToWindow() {
    875             super.onAttachedToWindow();
    876             setWillNotDraw(false);
    877             setOutlineProvider(new ViewOutlineProvider() {
    878                 @Override
    879                 public void getOutline(View view, Outline outline) {
    880                     outline.setRect(0, 0, getWidth(), getHeight());
    881                 }
    882             });
    883         }
    884         @Override
    885         public void onDraw(Canvas c) {
    886             final int w = c.getWidth();
    887             final int h = c.getHeight();
    888             final GradientDrawable g = new GradientDrawable();
    889             g.setOrientation(GradientDrawable.Orientation.LEFT_RIGHT);
    890             g.setGradientCenter(w * 0.75f, 0);
    891             g.setColors(new int[] { 0xFFFFFFFF, 0xFFAAAAAA });
    892             g.setBounds(0, 0, w, h);
    893             g.draw(c);
    894             if (!mDrawShadow) return;
    895             mShadow.reset();
    896             mShadow.moveTo(0,0);
    897             mShadow.lineTo(w, 0);
    898             mShadow.lineTo(w, PARAMS.OBSTACLE_WIDTH/2+w*1.5f);
    899             mShadow.lineTo(0, PARAMS.OBSTACLE_WIDTH/2);
    900             mShadow.close();
    901             c.drawPath(mShadow, mPaint);
    902         }
    903     }
    904 
    905     private class Scenery extends FrameLayout implements GameView {
    906         public float z;
    907         public float v;
    908         public int h, w;
    909         public Scenery(Context context) {
    910             super(context);
    911         }
    912 
    913         @Override
    914         public void step(long t_ms, long dt_ms, float t, float dt) {
    915             setTranslationX(getTranslationX() - PARAMS.TRANSLATION_PER_SEC * dt * v);
    916         }
    917     }
    918 
    919     private class Building extends Scenery {
    920         public Building(Context context) {
    921             super(context);
    922 
    923             w = irand(PARAMS.BUILDING_WIDTH_MIN, PARAMS.BUILDING_WIDTH_MAX);
    924             h = 0; // will be setup later, along with z
    925 
    926             setTranslationZ(PARAMS.SCENERY_Z);
    927         }
    928     }
    929 
    930     private class Cloud extends Scenery {
    931         public Cloud(Context context) {
    932             super(context);
    933             setBackgroundResource(frand() < 0.01f ? R.drawable.cloud_off : R.drawable.cloud);
    934             getBackground().setAlpha(0x40);
    935             w = h = irand(PARAMS.CLOUD_SIZE_MIN, PARAMS.CLOUD_SIZE_MAX);
    936             z = 0;
    937             v = frand(0.15f,0.5f);
    938         }
    939     }
    940 
    941     private class Star extends Scenery {
    942         public Star(Context context) {
    943             super(context);
    944             setBackgroundResource(R.drawable.star);
    945             w = h = irand(PARAMS.STAR_SIZE_MIN, PARAMS.STAR_SIZE_MAX);
    946             v = z = 0;
    947         }
    948     }
    949 }
    950