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