Home | History | Annotate | Download | only in egg
      1 /*
      2  * Copyright (C) 2015 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.LayoutTransition;
     20 import android.animation.TimeAnimator;
     21 import android.content.Context;
     22 import android.content.res.Resources;
     23 import android.graphics.Canvas;
     24 import android.graphics.Color;
     25 import android.graphics.Matrix;
     26 import android.graphics.Outline;
     27 import android.graphics.Paint;
     28 import android.graphics.Path;
     29 import android.graphics.PorterDuff;
     30 import android.graphics.PorterDuffColorFilter;
     31 import android.graphics.Rect;
     32 import android.graphics.drawable.Drawable;
     33 import android.graphics.drawable.GradientDrawable;
     34 import android.media.AudioAttributes;
     35 import android.media.AudioManager;
     36 import android.os.Vibrator;
     37 import android.util.AttributeSet;
     38 import android.util.Log;
     39 import android.view.Gravity;
     40 import android.view.InputDevice;
     41 import android.view.KeyEvent;
     42 import android.view.LayoutInflater;
     43 import android.view.MotionEvent;
     44 import android.view.View;
     45 import android.view.ViewGroup;
     46 import android.view.ViewOutlineProvider;
     47 import android.widget.FrameLayout;
     48 import android.widget.ImageView;
     49 import android.widget.TextView;
     50 
     51 import java.util.ArrayList;
     52 
     53 import com.android.internal.logging.MetricsLogger;
     54 
     55 import com.android.systemui.R;
     56 
     57 // It's like LLand, but "M"ultiplayer.
     58 public class MLand extends FrameLayout {
     59     public static final String TAG = "MLand";
     60 
     61     public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
     62     public static final boolean DEBUG_DRAW = false; // DEBUG
     63 
     64     public static final boolean SHOW_TOUCHES = true;
     65 
     66     public static void L(String s, Object ... objects) {
     67         if (DEBUG) {
     68             Log.d(TAG, objects.length == 0 ? s : String.format(s, objects));
     69         }
     70     }
     71 
     72     public static final float PI_2 = (float) (Math.PI/2);
     73 
     74     public static final boolean AUTOSTART = true;
     75     public static final boolean HAVE_STARS = true;
     76 
     77     public static final float DEBUG_SPEED_MULTIPLIER = 0.5f; // only if DEBUG
     78     public static final boolean DEBUG_IDDQD = Log.isLoggable(TAG + ".iddqd", Log.DEBUG);
     79 
     80     public static final int DEFAULT_PLAYERS = 1;
     81     public static final int MIN_PLAYERS = 1;
     82     public static final int MAX_PLAYERS = 6;
     83 
     84     static final float CONTROLLER_VIBRATION_MULTIPLIER = 2f;
     85 
     86     private static class Params {
     87         public float TRANSLATION_PER_SEC;
     88         public int OBSTACLE_SPACING, OBSTACLE_PERIOD;
     89         public int BOOST_DV;
     90         public int PLAYER_HIT_SIZE;
     91         public int PLAYER_SIZE;
     92         public int OBSTACLE_WIDTH, OBSTACLE_STEM_WIDTH;
     93         public int OBSTACLE_GAP;
     94         public int OBSTACLE_MIN;
     95         public int BUILDING_WIDTH_MIN, BUILDING_WIDTH_MAX;
     96         public int BUILDING_HEIGHT_MIN;
     97         public int CLOUD_SIZE_MIN, CLOUD_SIZE_MAX;
     98         public int STAR_SIZE_MIN, STAR_SIZE_MAX;
     99         public int G;
    100         public int MAX_V;
    101             public float SCENERY_Z, OBSTACLE_Z, PLAYER_Z, PLAYER_Z_BOOST, HUD_Z;
    102         public Params(Resources res) {
    103             TRANSLATION_PER_SEC = res.getDimension(R.dimen.translation_per_sec);
    104             OBSTACLE_SPACING = res.getDimensionPixelSize(R.dimen.obstacle_spacing);
    105             OBSTACLE_PERIOD = (int) (OBSTACLE_SPACING / TRANSLATION_PER_SEC);
    106             BOOST_DV = res.getDimensionPixelSize(R.dimen.boost_dv);
    107             PLAYER_HIT_SIZE = res.getDimensionPixelSize(R.dimen.player_hit_size);
    108             PLAYER_SIZE = res.getDimensionPixelSize(R.dimen.player_size);
    109             OBSTACLE_WIDTH = res.getDimensionPixelSize(R.dimen.obstacle_width);
    110             OBSTACLE_STEM_WIDTH = res.getDimensionPixelSize(R.dimen.obstacle_stem_width);
    111             OBSTACLE_GAP = res.getDimensionPixelSize(R.dimen.obstacle_gap);
    112             OBSTACLE_MIN = res.getDimensionPixelSize(R.dimen.obstacle_height_min);
    113             BUILDING_HEIGHT_MIN = res.getDimensionPixelSize(R.dimen.building_height_min);
    114             BUILDING_WIDTH_MIN = res.getDimensionPixelSize(R.dimen.building_width_min);
    115             BUILDING_WIDTH_MAX = res.getDimensionPixelSize(R.dimen.building_width_max);
    116             CLOUD_SIZE_MIN = res.getDimensionPixelSize(R.dimen.cloud_size_min);
    117             CLOUD_SIZE_MAX = res.getDimensionPixelSize(R.dimen.cloud_size_max);
    118             STAR_SIZE_MIN = res.getDimensionPixelSize(R.dimen.star_size_min);
    119             STAR_SIZE_MAX = res.getDimensionPixelSize(R.dimen.star_size_max);
    120 
    121             G = res.getDimensionPixelSize(R.dimen.G);
    122             MAX_V = res.getDimensionPixelSize(R.dimen.max_v);
    123 
    124             SCENERY_Z = res.getDimensionPixelSize(R.dimen.scenery_z);
    125             OBSTACLE_Z = res.getDimensionPixelSize(R.dimen.obstacle_z);
    126             PLAYER_Z = res.getDimensionPixelSize(R.dimen.player_z);
    127             PLAYER_Z_BOOST = res.getDimensionPixelSize(R.dimen.player_z_boost);
    128             HUD_Z = res.getDimensionPixelSize(R.dimen.hud_z);
    129 
    130             // Sanity checking
    131             if (OBSTACLE_MIN <= OBSTACLE_WIDTH / 2) {
    132                 L("error: obstacles might be too short, adjusting");
    133                 OBSTACLE_MIN = OBSTACLE_WIDTH / 2 + 1;
    134             }
    135         }
    136     }
    137 
    138     private TimeAnimator mAnim;
    139     private Vibrator mVibrator;
    140     private AudioManager mAudioManager;
    141     private final AudioAttributes mAudioAttrs = new AudioAttributes.Builder()
    142             .setUsage(AudioAttributes.USAGE_GAME).build();
    143 
    144     private View mSplash;
    145     private ViewGroup mScoreFields;
    146 
    147     private ArrayList<Player> mPlayers = new ArrayList<Player>();
    148     private ArrayList<Obstacle> mObstaclesInPlay = new ArrayList<Obstacle>();
    149 
    150     private float t, dt;
    151 
    152     private float mLastPipeTime; // in sec
    153     private int mCurrentPipeId; // basically, equivalent to the current score
    154     private int mWidth, mHeight;
    155     private boolean mAnimating, mPlaying;
    156     private boolean mFrozen; // after death, a short backoff
    157     private int mCountdown = 0;
    158     private boolean mFlipped;
    159 
    160     private int mTaps;
    161 
    162     private int mTimeOfDay;
    163     private static final int DAY = 0, NIGHT = 1, TWILIGHT = 2, SUNSET = 3;
    164     private static final int[][] SKIES = {
    165             { 0xFFc0c0FF, 0xFFa0a0FF }, // DAY
    166             { 0xFF000010, 0xFF000000 }, // NIGHT
    167             { 0xFF000040, 0xFF000010 }, // TWILIGHT
    168             { 0xFFa08020, 0xFF204080 }, // SUNSET
    169     };
    170 
    171     private int mScene;
    172     private static final int SCENE_CITY = 0, SCENE_TX = 1, SCENE_ZRH = 2;
    173     private static final int SCENE_COUNT = 3;
    174 
    175     private static Params PARAMS;
    176 
    177     private static float dp = 1f;
    178 
    179     private Paint mTouchPaint, mPlayerTracePaint;
    180 
    181     private ArrayList<Integer> mGameControllers = new ArrayList<>();
    182 
    183     public MLand(Context context) {
    184         this(context, null);
    185     }
    186 
    187     public MLand(Context context, AttributeSet attrs) {
    188         this(context, attrs, 0);
    189     }
    190 
    191     public MLand(Context context, AttributeSet attrs, int defStyle) {
    192         super(context, attrs, defStyle);
    193 
    194         mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
    195         mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
    196         setFocusable(true);
    197         PARAMS = new Params(getResources());
    198         mTimeOfDay = irand(0, SKIES.length - 1);
    199         mScene = irand(0, SCENE_COUNT);
    200 
    201         mTouchPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    202         mTouchPaint.setColor(0x80FFFFFF);
    203         mTouchPaint.setStyle(Paint.Style.FILL);
    204 
    205         mPlayerTracePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    206         mPlayerTracePaint.setColor(0x80FFFFFF);
    207         mPlayerTracePaint.setStyle(Paint.Style.STROKE);
    208         mPlayerTracePaint.setStrokeWidth(2 * dp);
    209 
    210         // we assume everything will be laid out left|top
    211         setLayoutDirection(LAYOUT_DIRECTION_LTR);
    212 
    213         setupPlayers(DEFAULT_PLAYERS);
    214 
    215         MetricsLogger.count(getContext(), "egg_mland_create", 1);
    216     }
    217 
    218     @Override
    219     public void onAttachedToWindow() {
    220         super.onAttachedToWindow();
    221         dp = getResources().getDisplayMetrics().density;
    222 
    223         reset();
    224         if (AUTOSTART) {
    225             start(false);
    226         }
    227     }
    228 
    229     @Override
    230     public boolean willNotDraw() {
    231         return !DEBUG;
    232     }
    233 
    234     public int getGameWidth() { return mWidth; }
    235     public int getGameHeight() { return mHeight; }
    236     public float getGameTime() { return t; }
    237     public float getLastTimeStep() { return dt; }
    238 
    239     public void setScoreFieldHolder(ViewGroup vg) {
    240         mScoreFields = vg;
    241         if (vg != null) {
    242             final LayoutTransition lt = new LayoutTransition();
    243             lt.setDuration(250);
    244             mScoreFields.setLayoutTransition(lt);
    245         }
    246         for (Player p : mPlayers) {
    247             mScoreFields.addView(p.mScoreField,
    248                     new MarginLayoutParams(
    249                             MarginLayoutParams.WRAP_CONTENT,
    250                             MarginLayoutParams.MATCH_PARENT));
    251         }
    252     }
    253 
    254     public void setSplash(View v) {
    255         mSplash = v;
    256     }
    257 
    258     public static boolean isGamePad(InputDevice dev) {
    259         int sources = dev.getSources();
    260 
    261         // Verify that the device has gamepad buttons, control sticks, or both.
    262         return (((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD)
    263                 || ((sources & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK));
    264     }
    265 
    266     public ArrayList getGameControllers() {
    267         mGameControllers.clear();
    268         int[] deviceIds = InputDevice.getDeviceIds();
    269         for (int deviceId : deviceIds) {
    270             InputDevice dev = InputDevice.getDevice(deviceId);
    271             if (isGamePad(dev)) {
    272                 if (!mGameControllers.contains(deviceId)) {
    273                     mGameControllers.add(deviceId);
    274                 }
    275             }
    276         }
    277         return mGameControllers;
    278     }
    279 
    280     public int getControllerPlayer(int id) {
    281         final int player = mGameControllers.indexOf(id);
    282         if (player < 0 || player >= mPlayers.size()) return 0;
    283         return player;
    284     }
    285 
    286     @Override
    287     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    288         dp = getResources().getDisplayMetrics().density;
    289 
    290         stop();
    291 
    292         reset();
    293         if (AUTOSTART) {
    294             start(false);
    295         }
    296     }
    297 
    298     final static float hsv[] = {0, 0, 0};
    299 
    300     private static float luma(int bgcolor) {
    301         return    0.2126f * (float) (bgcolor & 0xFF0000) / 0xFF0000
    302                 + 0.7152f * (float) (bgcolor & 0xFF00) / 0xFF00
    303                 + 0.0722f * (float) (bgcolor & 0xFF) / 0xFF;
    304     }
    305 
    306     public Player getPlayer(int i) {
    307         return i < mPlayers.size() ? mPlayers.get(i) : null;
    308     }
    309 
    310     private int addPlayerInternal(Player p) {
    311         mPlayers.add(p);
    312         realignPlayers();
    313         TextView scoreField = (TextView)
    314             LayoutInflater.from(getContext()).inflate(R.layout.mland_scorefield, null);
    315         if (mScoreFields != null) {
    316             mScoreFields.addView(scoreField,
    317                 new MarginLayoutParams(
    318                         MarginLayoutParams.WRAP_CONTENT,
    319                         MarginLayoutParams.MATCH_PARENT));
    320         }
    321         p.setScoreField(scoreField);
    322         return mPlayers.size()-1;
    323     }
    324 
    325     private void removePlayerInternal(Player p) {
    326         if (mPlayers.remove(p)) {
    327             removeView(p);
    328             mScoreFields.removeView(p.mScoreField);
    329             realignPlayers();
    330         }
    331     }
    332 
    333     private void realignPlayers() {
    334         final int N = mPlayers.size();
    335         float x = (mWidth - (N-1) * PARAMS.PLAYER_SIZE) / 2;
    336         for (int i=0; i<N; i++) {
    337             final Player p = mPlayers.get(i);
    338             p.setX(x);
    339             x += PARAMS.PLAYER_SIZE;
    340         }
    341     }
    342 
    343     private void clearPlayers() {
    344         while (mPlayers.size() > 0) {
    345             removePlayerInternal(mPlayers.get(0));
    346         }
    347     }
    348 
    349     public void setupPlayers(int num) {
    350         clearPlayers();
    351         for (int i=0; i<num; i++) {
    352             addPlayerInternal(Player.create(this));
    353         }
    354     }
    355 
    356     public void addPlayer() {
    357         if (getNumPlayers() == MAX_PLAYERS) return;
    358         addPlayerInternal(Player.create(this));
    359     }
    360 
    361     public int getNumPlayers() {
    362         return mPlayers.size();
    363     }
    364 
    365     public void removePlayer() {
    366         if (getNumPlayers() == MIN_PLAYERS) return;
    367         removePlayerInternal(mPlayers.get(mPlayers.size() - 1));
    368     }
    369 
    370     private void thump(int playerIndex, long ms) {
    371         if (mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_SILENT) {
    372             // No interruptions. Not even game haptics.
    373             return;
    374         }
    375         if (playerIndex < mGameControllers.size()) {
    376             int controllerId = mGameControllers.get(playerIndex);
    377             InputDevice dev = InputDevice.getDevice(controllerId);
    378             if (dev != null && dev.getVibrator().hasVibrator()) {
    379                 dev.getVibrator().vibrate(
    380                         (long) (ms * CONTROLLER_VIBRATION_MULTIPLIER),
    381                         mAudioAttrs);
    382                 return;
    383             }
    384         }
    385         mVibrator.vibrate(ms, mAudioAttrs);
    386     }
    387 
    388     public void reset() {
    389         L("reset");
    390         final Drawable sky = new GradientDrawable(
    391                 GradientDrawable.Orientation.BOTTOM_TOP,
    392                 SKIES[mTimeOfDay]
    393         );
    394         sky.setDither(true);
    395         setBackground(sky);
    396 
    397         mFlipped = frand() > 0.5f;
    398         setScaleX(mFlipped ? -1 : 1);
    399 
    400         int i = getChildCount();
    401         while (i-->0) {
    402             final View v = getChildAt(i);
    403             if (v instanceof GameView) {
    404                 removeViewAt(i);
    405             }
    406         }
    407 
    408         mObstaclesInPlay.clear();
    409         mCurrentPipeId = 0;
    410 
    411         mWidth = getWidth();
    412         mHeight = getHeight();
    413 
    414         boolean showingSun = (mTimeOfDay == DAY || mTimeOfDay == SUNSET) && frand() > 0.25;
    415         if (showingSun) {
    416             final Star sun = new Star(getContext());
    417             sun.setBackgroundResource(R.drawable.sun);
    418             final int w = getResources().getDimensionPixelSize(R.dimen.sun_size);
    419             sun.setTranslationX(frand(w, mWidth-w));
    420             if (mTimeOfDay == DAY) {
    421                 sun.setTranslationY(frand(w, (mHeight * 0.66f)));
    422                 sun.getBackground().setTint(0);
    423             } else {
    424                 sun.setTranslationY(frand(mHeight * 0.66f, mHeight - w));
    425                 sun.getBackground().setTintMode(PorterDuff.Mode.SRC_ATOP);
    426                 sun.getBackground().setTint(0xC0FF8000);
    427 
    428             }
    429             addView(sun, new LayoutParams(w, w));
    430         }
    431         if (!showingSun) {
    432             final boolean dark = mTimeOfDay == NIGHT || mTimeOfDay == TWILIGHT;
    433             final float ff = frand();
    434             if ((dark && ff < 0.75f) || ff < 0.5f) {
    435                 final Star moon = new Star(getContext());
    436                 moon.setBackgroundResource(R.drawable.moon);
    437                 moon.getBackground().setAlpha(dark ? 255 : 128);
    438                 moon.setScaleX(frand() > 0.5 ? -1 : 1);
    439                 moon.setRotation(moon.getScaleX() * frand(5, 30));
    440                 final int w = getResources().getDimensionPixelSize(R.dimen.sun_size);
    441                 moon.setTranslationX(frand(w, mWidth - w));
    442                 moon.setTranslationY(frand(w, mHeight - w));
    443                 addView(moon, new LayoutParams(w, w));
    444             }
    445         }
    446 
    447         final int mh = mHeight / 6;
    448         final boolean cloudless = frand() < 0.25;
    449         final int N = 20;
    450         for (i=0; i<N; i++) {
    451             final float r1 = frand();
    452             final Scenery s;
    453             if (HAVE_STARS && r1 < 0.3 && mTimeOfDay != DAY) {
    454                 s = new Star(getContext());
    455             } else if (r1 < 0.6 && !cloudless) {
    456                 s = new Cloud(getContext());
    457             } else {
    458                 switch (mScene) {
    459                     case SCENE_ZRH:
    460                         s = new Mountain(getContext());
    461                         break;
    462                     case SCENE_TX:
    463                         s = new Cactus(getContext());
    464                         break;
    465                     case SCENE_CITY:
    466                     default:
    467                         s = new Building(getContext());
    468                         break;
    469                 }
    470                 s.z = (float) i / N;
    471                 // no more shadows for these things
    472                 //s.setTranslationZ(PARAMS.SCENERY_Z * (1+s.z));
    473                 s.v = 0.85f * s.z; // buildings move proportional to their distance
    474                 if (mScene == SCENE_CITY) {
    475                     s.setBackgroundColor(Color.GRAY);
    476                     s.h = irand(PARAMS.BUILDING_HEIGHT_MIN, mh);
    477                 }
    478                 final int c = (int)(255f*s.z);
    479                 final Drawable bg = s.getBackground();
    480                 if (bg != null) bg.setColorFilter(Color.rgb(c,c,c), PorterDuff.Mode.MULTIPLY);
    481             }
    482             final LayoutParams lp = new LayoutParams(s.w, s.h);
    483             if (s instanceof Building) {
    484                 lp.gravity = Gravity.BOTTOM;
    485             } else {
    486                 lp.gravity = Gravity.TOP;
    487                 final float r = frand();
    488                 if (s instanceof Star) {
    489                     lp.topMargin = (int) (r * r * mHeight);
    490                 } else {
    491                     lp.topMargin = (int) (1 - r*r * mHeight/2) + mHeight/2;
    492                 }
    493             }
    494 
    495 
    496             addView(s, lp);
    497             s.setTranslationX(frand(-lp.width, mWidth + lp.width));
    498         }
    499 
    500         for (Player p : mPlayers) {
    501             addView(p); // put it back!
    502             p.reset();
    503         }
    504 
    505         realignPlayers();
    506 
    507         if (mAnim != null) {
    508             mAnim.cancel();
    509         }
    510         mAnim = new TimeAnimator();
    511         mAnim.setTimeListener(new TimeAnimator.TimeListener() {
    512             @Override
    513             public void onTimeUpdate(TimeAnimator timeAnimator, long t, long dt) {
    514                 step(t, dt);
    515             }
    516         });
    517     }
    518 
    519     public void start(boolean startPlaying) {
    520         L("start(startPlaying=%s)", startPlaying ? "true" : "false");
    521         if (startPlaying && mCountdown <= 0) {
    522             showSplash();
    523 
    524             mSplash.findViewById(R.id.play_button).setEnabled(false);
    525 
    526             final View playImage = mSplash.findViewById(R.id.play_button_image);
    527             final TextView playText = (TextView) mSplash.findViewById(R.id.play_button_text);
    528 
    529             playImage.animate().alpha(0f);
    530             playText.animate().alpha(1f);
    531 
    532             mCountdown = 3;
    533             post(new Runnable() {
    534                 @Override
    535                 public void run() {
    536                     if (mCountdown == 0) {
    537                         startPlaying();
    538                     } else {
    539                         postDelayed(this, 500);
    540                     }
    541                     playText.setText(String.valueOf(mCountdown));
    542                     mCountdown--;
    543                 }
    544             });
    545         }
    546 
    547         for (Player p : mPlayers) {
    548             p.setVisibility(View.INVISIBLE);
    549         }
    550 
    551         if (!mAnimating) {
    552             mAnim.start();
    553             mAnimating = true;
    554         }
    555     }
    556 
    557     public void hideSplash() {
    558         if (mSplash != null && mSplash.getVisibility() == View.VISIBLE) {
    559             mSplash.setClickable(false);
    560             mSplash.animate().alpha(0).translationZ(0).setDuration(300).withEndAction(
    561                     new Runnable() {
    562                         @Override
    563                         public void run() {
    564                             mSplash.setVisibility(View.GONE);
    565                         }
    566                     }
    567             );
    568         }
    569     }
    570 
    571     public void showSplash() {
    572         if (mSplash != null && mSplash.getVisibility() != View.VISIBLE) {
    573             mSplash.setClickable(true);
    574             mSplash.setAlpha(0f);
    575             mSplash.setVisibility(View.VISIBLE);
    576             mSplash.animate().alpha(1f).setDuration(1000);
    577             mSplash.findViewById(R.id.play_button_image).setAlpha(1f);
    578             mSplash.findViewById(R.id.play_button_text).setAlpha(0f);
    579             mSplash.findViewById(R.id.play_button).setEnabled(true);
    580             mSplash.findViewById(R.id.play_button).requestFocus();
    581         }
    582     }
    583 
    584     public void startPlaying() {
    585         mPlaying = true;
    586 
    587         t = 0;
    588         // there's a sucker born every OBSTACLE_PERIOD
    589         mLastPipeTime = getGameTime() - PARAMS.OBSTACLE_PERIOD;
    590 
    591         hideSplash();
    592 
    593         realignPlayers();
    594         mTaps = 0;
    595 
    596         final int N = mPlayers.size();
    597         MetricsLogger.histogram(getContext(), "egg_mland_players", N);
    598         for (int i=0; i<N; i++) {
    599             final Player p = mPlayers.get(i);
    600             p.setVisibility(View.VISIBLE);
    601             p.reset();
    602             p.start();
    603             p.boost(-1, -1); // start you off flying!
    604             p.unboost(); // not forever, though
    605         }
    606     }
    607 
    608     public void stop() {
    609         if (mAnimating) {
    610             mAnim.cancel();
    611             mAnim = null;
    612             mAnimating = false;
    613             mPlaying = false;
    614             mTimeOfDay = irand(0, SKIES.length - 1); // for next reset
    615             mScene = irand(0, SCENE_COUNT);
    616             mFrozen = true;
    617             for (Player p : mPlayers) {
    618                 p.die();
    619             }
    620             postDelayed(new Runnable() {
    621                     @Override
    622                     public void run() {
    623                         mFrozen = false;
    624                     }
    625                 }, 250);
    626         }
    627     }
    628 
    629     public static final float lerp(float x, float a, float b) {
    630         return (b - a) * x + a;
    631     }
    632 
    633     public static final float rlerp(float v, float a, float b) {
    634         return (v - a) / (b - a);
    635     }
    636 
    637     public static final float clamp(float f) {
    638         return f < 0f ? 0f : f > 1f ? 1f : f;
    639     }
    640 
    641     public static final float frand() {
    642         return (float) Math.random();
    643     }
    644 
    645     public static final float frand(float a, float b) {
    646         return lerp(frand(), a, b);
    647     }
    648 
    649     public static final int irand(int a, int b) {
    650         return Math.round(frand((float) a, (float) b));
    651     }
    652 
    653     public static int pick(int[] l) {
    654         return l[irand(0, l.length-1)];
    655     }
    656 
    657     private void step(long t_ms, long dt_ms) {
    658         t = t_ms / 1000f; // seconds
    659         dt = dt_ms / 1000f;
    660 
    661         if (DEBUG) {
    662             t *= DEBUG_SPEED_MULTIPLIER;
    663             dt *= DEBUG_SPEED_MULTIPLIER;
    664         }
    665 
    666         // 1. Move all objects and update bounds
    667         final int N = getChildCount();
    668         int i = 0;
    669         for (; i<N; i++) {
    670             final View v = getChildAt(i);
    671             if (v instanceof GameView) {
    672                 ((GameView) v).step(t_ms, dt_ms, t, dt);
    673             }
    674         }
    675 
    676         if (mPlaying) {
    677             int livingPlayers = 0;
    678             for (i = 0; i < mPlayers.size(); i++) {
    679                 final Player p = getPlayer(i);
    680 
    681                 if (p.mAlive) {
    682                     // 2. Check for altitude
    683                     if (p.below(mHeight)) {
    684                         if (DEBUG_IDDQD) {
    685                             poke(i);
    686                             unpoke(i);
    687                         } else {
    688                             L("player %d hit the floor", i);
    689                             thump(i, 80);
    690                             p.die();
    691                         }
    692                     }
    693 
    694                     // 3. Check for obstacles
    695                     int maxPassedStem = 0;
    696                     for (int j = mObstaclesInPlay.size(); j-- > 0; ) {
    697                         final Obstacle ob = mObstaclesInPlay.get(j);
    698                         if (ob.intersects(p) && !DEBUG_IDDQD) {
    699                             L("player hit an obstacle");
    700                             thump(i, 80);
    701                             p.die();
    702                         } else if (ob.cleared(p)) {
    703                             if (ob instanceof Stem) {
    704                                 maxPassedStem = Math.max(maxPassedStem, ((Stem)ob).id);
    705                             }
    706                         }
    707                     }
    708 
    709                     if (maxPassedStem > p.mScore) {
    710                         p.addScore(1);
    711                     }
    712                 }
    713 
    714                 if (p.mAlive) livingPlayers++;
    715             }
    716 
    717             if (livingPlayers == 0) {
    718                 stop();
    719 
    720                 MetricsLogger.count(getContext(), "egg_mland_taps", mTaps);
    721                 mTaps = 0;
    722                 final int playerCount = mPlayers.size();
    723                 for (int pi=0; pi<playerCount; pi++) {
    724                     final Player p = mPlayers.get(pi);
    725                     MetricsLogger.histogram(getContext(), "egg_mland_score", p.getScore());
    726                 }
    727             }
    728         }
    729 
    730         // 4. Handle edge of screen
    731         // Walk backwards to make sure removal is safe
    732         while (i-->0) {
    733             final View v = getChildAt(i);
    734             if (v instanceof Obstacle) {
    735                 if (v.getTranslationX() + v.getWidth() < 0) {
    736                     removeViewAt(i);
    737                     mObstaclesInPlay.remove(v);
    738                 }
    739             } else if (v instanceof Scenery) {
    740                 final Scenery s = (Scenery) v;
    741                 if (v.getTranslationX() + s.w < 0) {
    742                     v.setTranslationX(getWidth());
    743                 }
    744             }
    745         }
    746 
    747         // 3. Time for more obstacles!
    748         if (mPlaying && (t - mLastPipeTime) > PARAMS.OBSTACLE_PERIOD) {
    749             mLastPipeTime = t;
    750             mCurrentPipeId ++;
    751             final int obstacley =
    752                     (int)(frand() * (mHeight - 2*PARAMS.OBSTACLE_MIN - PARAMS.OBSTACLE_GAP)) +
    753                     PARAMS.OBSTACLE_MIN;
    754 
    755             final int inset = (PARAMS.OBSTACLE_WIDTH - PARAMS.OBSTACLE_STEM_WIDTH) / 2;
    756             final int yinset = PARAMS.OBSTACLE_WIDTH/2;
    757 
    758             final int d1 = irand(0,250);
    759             final Obstacle s1 = new Stem(getContext(), obstacley - yinset, false);
    760             addView(s1, new LayoutParams(
    761                     PARAMS.OBSTACLE_STEM_WIDTH,
    762                     (int) s1.h,
    763                     Gravity.TOP|Gravity.LEFT));
    764             s1.setTranslationX(mWidth+inset);
    765             s1.setTranslationY(-s1.h-yinset);
    766             s1.setTranslationZ(PARAMS.OBSTACLE_Z*0.75f);
    767             s1.animate()
    768                     .translationY(0)
    769                     .setStartDelay(d1)
    770                     .setDuration(250);
    771             mObstaclesInPlay.add(s1);
    772 
    773             final Obstacle p1 = new Pop(getContext(), PARAMS.OBSTACLE_WIDTH);
    774             addView(p1, new LayoutParams(
    775                     PARAMS.OBSTACLE_WIDTH,
    776                     PARAMS.OBSTACLE_WIDTH,
    777                     Gravity.TOP|Gravity.LEFT));
    778             p1.setTranslationX(mWidth);
    779             p1.setTranslationY(-PARAMS.OBSTACLE_WIDTH);
    780             p1.setTranslationZ(PARAMS.OBSTACLE_Z);
    781             p1.setScaleX(0.25f);
    782             p1.setScaleY(-0.25f);
    783             p1.animate()
    784                     .translationY(s1.h-inset)
    785                     .scaleX(1f)
    786                     .scaleY(-1f)
    787                     .setStartDelay(d1)
    788                     .setDuration(250);
    789             mObstaclesInPlay.add(p1);
    790 
    791             final int d2 = irand(0,250);
    792             final Obstacle s2 = new Stem(getContext(),
    793                     mHeight - obstacley - PARAMS.OBSTACLE_GAP - yinset,
    794                     true);
    795             addView(s2, new LayoutParams(
    796                     PARAMS.OBSTACLE_STEM_WIDTH,
    797                     (int) s2.h,
    798                     Gravity.TOP|Gravity.LEFT));
    799             s2.setTranslationX(mWidth+inset);
    800             s2.setTranslationY(mHeight+yinset);
    801             s2.setTranslationZ(PARAMS.OBSTACLE_Z*0.75f);
    802             s2.animate()
    803                     .translationY(mHeight-s2.h)
    804                     .setStartDelay(d2)
    805                     .setDuration(400);
    806             mObstaclesInPlay.add(s2);
    807 
    808             final Obstacle p2 = new Pop(getContext(), PARAMS.OBSTACLE_WIDTH);
    809             addView(p2, new LayoutParams(
    810                     PARAMS.OBSTACLE_WIDTH,
    811                     PARAMS.OBSTACLE_WIDTH,
    812                     Gravity.TOP|Gravity.LEFT));
    813             p2.setTranslationX(mWidth);
    814             p2.setTranslationY(mHeight);
    815             p2.setTranslationZ(PARAMS.OBSTACLE_Z);
    816             p2.setScaleX(0.25f);
    817             p2.setScaleY(0.25f);
    818             p2.animate()
    819                     .translationY(mHeight-s2.h-yinset)
    820                     .scaleX(1f)
    821                     .scaleY(1f)
    822                     .setStartDelay(d2)
    823                     .setDuration(400);
    824             mObstaclesInPlay.add(p2);
    825         }
    826 
    827         if (SHOW_TOUCHES || DEBUG_DRAW) invalidate();
    828     }
    829 
    830     @Override
    831     public boolean onTouchEvent(MotionEvent ev) {
    832         L("touch: %s", ev);
    833         final int actionIndex = ev.getActionIndex();
    834         final float x = ev.getX(actionIndex);
    835         final float y = ev.getY(actionIndex);
    836         int playerIndex = (int) (getNumPlayers() * (x / getWidth()));
    837         if (mFlipped) playerIndex = getNumPlayers() - 1 - playerIndex;
    838         switch (ev.getActionMasked()) {
    839             case MotionEvent.ACTION_DOWN:
    840             case MotionEvent.ACTION_POINTER_DOWN:
    841                 poke(playerIndex, x, y);
    842                 return true;
    843             case MotionEvent.ACTION_UP:
    844             case MotionEvent.ACTION_POINTER_UP:
    845                 unpoke(playerIndex);
    846                 return true;
    847         }
    848         return false;
    849     }
    850 
    851     @Override
    852     public boolean onTrackballEvent(MotionEvent ev) {
    853         L("trackball: %s", ev);
    854         switch (ev.getAction()) {
    855             case MotionEvent.ACTION_DOWN:
    856                 poke(0);
    857                 return true;
    858             case MotionEvent.ACTION_UP:
    859                 unpoke(0);
    860                 return true;
    861         }
    862         return false;
    863     }
    864 
    865     @Override
    866     public boolean onKeyDown(int keyCode, KeyEvent ev) {
    867         L("keyDown: %d", keyCode);
    868         switch (keyCode) {
    869             case KeyEvent.KEYCODE_DPAD_CENTER:
    870             case KeyEvent.KEYCODE_DPAD_UP:
    871             case KeyEvent.KEYCODE_SPACE:
    872             case KeyEvent.KEYCODE_ENTER:
    873             case KeyEvent.KEYCODE_BUTTON_A:
    874                 int player = getControllerPlayer(ev.getDeviceId());
    875                 poke(player);
    876                 return true;
    877         }
    878         return false;
    879     }
    880 
    881     @Override
    882     public boolean onKeyUp(int keyCode, KeyEvent ev) {
    883         L("keyDown: %d", keyCode);
    884         switch (keyCode) {
    885             case KeyEvent.KEYCODE_DPAD_CENTER:
    886             case KeyEvent.KEYCODE_DPAD_UP:
    887             case KeyEvent.KEYCODE_SPACE:
    888             case KeyEvent.KEYCODE_ENTER:
    889             case KeyEvent.KEYCODE_BUTTON_A:
    890                 int player = getControllerPlayer(ev.getDeviceId());
    891                 unpoke(player);
    892                 return true;
    893         }
    894         return false;
    895     }
    896 
    897     @Override
    898     public boolean onGenericMotionEvent (MotionEvent ev) {
    899         L("generic: %s", ev);
    900         return false;
    901     }
    902 
    903     private void poke(int playerIndex) {
    904         poke(playerIndex, -1, -1);
    905     }
    906 
    907     private void poke(int playerIndex, float x, float y) {
    908         L("poke(%d)", playerIndex);
    909         if (mFrozen) return;
    910         if (!mAnimating) {
    911             reset();
    912         }
    913         if (!mPlaying) {
    914             start(true);
    915         } else {
    916             final Player p = getPlayer(playerIndex);
    917             if (p == null) return; // no player for this controller
    918             p.boost(x, y);
    919             mTaps++;
    920             if (DEBUG) {
    921                 p.dv *= DEBUG_SPEED_MULTIPLIER;
    922                 p.animate().setDuration((long) (200 / DEBUG_SPEED_MULTIPLIER));
    923             }
    924         }
    925     }
    926 
    927     private void unpoke(int playerIndex) {
    928         L("unboost(%d)", playerIndex);
    929         if (mFrozen || !mAnimating || !mPlaying) return;
    930         final Player p = getPlayer(playerIndex);
    931         if (p == null) return; // no player for this controller
    932         p.unboost();
    933     }
    934 
    935     @Override
    936     public void onDraw(Canvas c) {
    937         super.onDraw(c);
    938 
    939         if (SHOW_TOUCHES) {
    940             for (Player p : mPlayers) {
    941                 if (p.mTouchX > 0) {
    942                     mTouchPaint.setColor(0x80FFFFFF & p.color);
    943                     mPlayerTracePaint.setColor(0x80FFFFFF & p.color);
    944                     float x1 = p.mTouchX;
    945                     float y1 = p.mTouchY;
    946                     c.drawCircle(x1, y1, 100, mTouchPaint);
    947                     float x2 = p.getX() + p.getPivotX();
    948                     float y2 = p.getY() + p.getPivotY();
    949                     float angle = PI_2 - (float) Math.atan2(x2-x1, y2-y1);
    950                     x1 += 100*Math.cos(angle);
    951                     y1 += 100*Math.sin(angle);
    952                     c.drawLine(x1, y1, x2, y2, mPlayerTracePaint);
    953                 }
    954             }
    955         }
    956 
    957         if (!DEBUG_DRAW) return;
    958 
    959         final Paint pt = new Paint();
    960         pt.setColor(0xFFFFFFFF);
    961         for (Player p : mPlayers) {
    962             final int L = p.corners.length;
    963             final int N = L / 2;
    964             for (int i = 0; i < N; i++) {
    965                 final int x = (int) p.corners[i * 2];
    966                 final int y = (int) p.corners[i * 2 + 1];
    967                 c.drawCircle(x, y, 4, pt);
    968                 c.drawLine(x, y,
    969                         p.corners[(i * 2 + 2) % L],
    970                         p.corners[(i * 2 + 3) % L],
    971                         pt);
    972             }
    973         }
    974 
    975         pt.setStyle(Paint.Style.STROKE);
    976         pt.setStrokeWidth(getResources().getDisplayMetrics().density);
    977 
    978         final int M = getChildCount();
    979         pt.setColor(0x8000FF00);
    980         for (int i=0; i<M; i++) {
    981             final View v = getChildAt(i);
    982             if (v instanceof Player) continue;
    983             if (!(v instanceof GameView)) continue;
    984             if (v instanceof Pop) {
    985                 final Pop pop = (Pop) v;
    986                 c.drawCircle(pop.cx, pop.cy, pop.r, pt);
    987             } else {
    988                 final Rect r = new Rect();
    989                 v.getHitRect(r);
    990                 c.drawRect(r, pt);
    991             }
    992         }
    993 
    994         pt.setColor(Color.BLACK);
    995         final StringBuilder sb = new StringBuilder("obstacles: ");
    996         for (Obstacle ob : mObstaclesInPlay) {
    997             sb.append(ob.hitRect.toShortString());
    998             sb.append(" ");
    999         }
   1000         pt.setTextSize(20f);
   1001         c.drawText(sb.toString(), 20, 100, pt);
   1002     }
   1003 
   1004     static final Rect sTmpRect = new Rect();
   1005 
   1006     private interface GameView {
   1007         public void step(long t_ms, long dt_ms, float t, float dt);
   1008     }
   1009 
   1010     private static class Player extends ImageView implements GameView {
   1011         public float dv;
   1012         public int color;
   1013         private MLand mLand;
   1014         private boolean mBoosting;
   1015         private float mTouchX = -1, mTouchY = -1;
   1016         private boolean mAlive;
   1017         private int mScore;
   1018         private TextView mScoreField;
   1019 
   1020         private final int[] sColors = new int[] {
   1021                 //0xFF78C557,
   1022                 0xFFDB4437,
   1023                 0xFF3B78E7,
   1024                 0xFFF4B400,
   1025                 0xFF0F9D58,
   1026                 0xFF7B1880,
   1027                 0xFF9E9E9E,
   1028         };
   1029         static int sNextColor = 0;
   1030 
   1031         private final float[] sHull = new float[] {
   1032                 0.3f,  0f,    // left antenna
   1033                 0.7f,  0f,    // right antenna
   1034                 0.92f, 0.33f, // off the right shoulder of Orion
   1035                 0.92f, 0.75f, // right hand (our right, not his right)
   1036                 0.6f,  1f,    // right foot
   1037                 0.4f,  1f,    // left foot BLUE!
   1038                 0.08f, 0.75f, // sinistram
   1039                 0.08f, 0.33f, // cold shoulder
   1040         };
   1041         public final float[] corners = new float[sHull.length];
   1042 
   1043         public static Player create(MLand land) {
   1044             final Player p = new Player(land.getContext());
   1045             p.mLand = land;
   1046             p.reset();
   1047             p.setVisibility(View.INVISIBLE);
   1048             land.addView(p, new LayoutParams(PARAMS.PLAYER_SIZE, PARAMS.PLAYER_SIZE));
   1049             return p;
   1050         }
   1051 
   1052         private void setScore(int score) {
   1053             mScore = score;
   1054             if (mScoreField != null) {
   1055                 mScoreField.setText(DEBUG_IDDQD ? "??" : String.valueOf(score));
   1056             }
   1057         }
   1058 
   1059         public int getScore() {
   1060             return mScore;
   1061         }
   1062 
   1063         private void addScore(int incr) {
   1064             setScore(mScore + incr);
   1065         }
   1066 
   1067         public void setScoreField(TextView tv) {
   1068             mScoreField = tv;
   1069             if (tv != null) {
   1070                 setScore(mScore); // reapply
   1071                 //mScoreField.setBackgroundResource(R.drawable.scorecard);
   1072                 mScoreField.getBackground().setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
   1073                 mScoreField.setTextColor(luma(color) > 0.7f ? 0xFF000000 : 0xFFFFFFFF);
   1074             }
   1075         }
   1076 
   1077         public void reset() {
   1078             //setX(mLand.mWidth / 2);
   1079             setY(mLand.mHeight / 2
   1080                     + (int)(Math.random() * PARAMS.PLAYER_SIZE)
   1081                     - PARAMS.PLAYER_SIZE / 2);
   1082             setScore(0);
   1083             setScoreField(mScoreField); // refresh color
   1084             mBoosting = false;
   1085             dv = 0;
   1086         }
   1087 
   1088         public Player(Context context) {
   1089             super(context);
   1090 
   1091             setBackgroundResource(R.drawable.android);
   1092             getBackground().setTintMode(PorterDuff.Mode.SRC_ATOP);
   1093             color = sColors[(sNextColor++%sColors.length)];
   1094             getBackground().setTint(color);
   1095             setOutlineProvider(new ViewOutlineProvider() {
   1096                 @Override
   1097                 public void getOutline(View view, Outline outline) {
   1098                     final int w = view.getWidth();
   1099                     final int h = view.getHeight();
   1100                     final int ix = (int) (w * 0.3f);
   1101                     final int iy = (int) (h * 0.2f);
   1102                     outline.setRect(ix, iy, w - ix, h - iy);
   1103                 }
   1104             });
   1105         }
   1106 
   1107         public void prepareCheckIntersections() {
   1108             final int inset = (PARAMS.PLAYER_SIZE - PARAMS.PLAYER_HIT_SIZE)/2;
   1109             final int scale = PARAMS.PLAYER_HIT_SIZE;
   1110             final int N = sHull.length/2;
   1111             for (int i=0; i<N; i++) {
   1112                 corners[i*2]   = scale * sHull[i*2]   + inset;
   1113                 corners[i*2+1] = scale * sHull[i*2+1] + inset;
   1114             }
   1115             final Matrix m = getMatrix();
   1116             m.mapPoints(corners);
   1117         }
   1118 
   1119         public boolean below(int h) {
   1120             final int N = corners.length/2;
   1121             for (int i=0; i<N; i++) {
   1122                 final int y = (int) corners[i*2+1];
   1123                 if (y >= h) return true;
   1124             }
   1125             return false;
   1126         }
   1127 
   1128         public void step(long t_ms, long dt_ms, float t, float dt) {
   1129             if (!mAlive) {
   1130                 // float away with the garbage
   1131                 setTranslationX(getTranslationX()-PARAMS.TRANSLATION_PER_SEC*dt);
   1132                 return;
   1133             }
   1134 
   1135             if (mBoosting) {
   1136                 dv = -PARAMS.BOOST_DV;
   1137             } else {
   1138                 dv += PARAMS.G;
   1139             }
   1140             if (dv < -PARAMS.MAX_V) dv = -PARAMS.MAX_V;
   1141             else if (dv > PARAMS.MAX_V) dv = PARAMS.MAX_V;
   1142 
   1143             final float y = getTranslationY() + dv * dt;
   1144             setTranslationY(y < 0 ? 0 : y);
   1145             setRotation(
   1146                     90 + lerp(clamp(rlerp(dv, PARAMS.MAX_V, -1 * PARAMS.MAX_V)), 90, -90));
   1147 
   1148             prepareCheckIntersections();
   1149         }
   1150 
   1151         public void boost(float x, float y) {
   1152             mTouchX = x;
   1153             mTouchY = y;
   1154             boost();
   1155         }
   1156 
   1157         public void boost() {
   1158             mBoosting = true;
   1159             dv = -PARAMS.BOOST_DV;
   1160 
   1161             animate().cancel();
   1162             animate()
   1163                     .scaleX(1.25f)
   1164                     .scaleY(1.25f)
   1165                     .translationZ(PARAMS.PLAYER_Z_BOOST)
   1166                     .setDuration(100);
   1167             setScaleX(1.25f);
   1168             setScaleY(1.25f);
   1169         }
   1170 
   1171         public void unboost() {
   1172             mBoosting = false;
   1173             mTouchX = mTouchY = -1;
   1174 
   1175             animate().cancel();
   1176             animate()
   1177                     .scaleX(1f)
   1178                     .scaleY(1f)
   1179                     .translationZ(PARAMS.PLAYER_Z)
   1180                     .setDuration(200);
   1181         }
   1182 
   1183         public void die() {
   1184             mAlive = false;
   1185             if (mScoreField != null) {
   1186                 //mScoreField.setTextColor(0xFFFFFFFF);
   1187                 //mScoreField.getBackground().setColorFilter(0xFF666666, PorterDuff.Mode.SRC_ATOP);
   1188                 //mScoreField.setBackgroundResource(R.drawable.scorecard_gameover);
   1189             }
   1190         }
   1191 
   1192         public void start() {
   1193             mAlive = true;
   1194         }
   1195     }
   1196 
   1197     private class Obstacle extends View implements GameView {
   1198         public float h;
   1199 
   1200         public final Rect hitRect = new Rect();
   1201 
   1202         public Obstacle(Context context, float h) {
   1203             super(context);
   1204             setBackgroundColor(0xFFFF0000);
   1205             this.h = h;
   1206         }
   1207 
   1208         public boolean intersects(Player p) {
   1209             final int N = p.corners.length/2;
   1210             for (int i=0; i<N; i++) {
   1211                 final int x = (int) p.corners[i*2];
   1212                 final int y = (int) p.corners[i*2+1];
   1213                 if (hitRect.contains(x, y)) return true;
   1214             }
   1215             return false;
   1216         }
   1217 
   1218         public boolean cleared(Player p) {
   1219             final int N = p.corners.length/2;
   1220             for (int i=0; i<N; i++) {
   1221                 final int x = (int) p.corners[i*2];
   1222                 if (hitRect.right >= x) return false;
   1223             }
   1224             return true;
   1225         }
   1226 
   1227         @Override
   1228         public void step(long t_ms, long dt_ms, float t, float dt) {
   1229             setTranslationX(getTranslationX()-PARAMS.TRANSLATION_PER_SEC*dt);
   1230             getHitRect(hitRect);
   1231         }
   1232     }
   1233 
   1234     static final int[] ANTENNAE = new int[] {R.drawable.mm_antennae, R.drawable.mm_antennae2};
   1235     static final int[] EYES = new int[] {R.drawable.mm_eyes, R.drawable.mm_eyes2};
   1236     static final int[] MOUTHS = new int[] {R.drawable.mm_mouth1, R.drawable.mm_mouth2,
   1237             R.drawable.mm_mouth3, R.drawable.mm_mouth4};
   1238     private class Pop extends Obstacle {
   1239         int mRotate;
   1240         int cx, cy, r;
   1241         // The marshmallow illustration and hitbox is 2/3 the size of its container.
   1242         Drawable antenna, eyes, mouth;
   1243 
   1244 
   1245         public Pop(Context context, float h) {
   1246             super(context, h);
   1247             setBackgroundResource(R.drawable.mm_head);
   1248             antenna = context.getDrawable(pick(ANTENNAE));
   1249             if (frand() > 0.5f) {
   1250                 eyes = context.getDrawable(pick(EYES));
   1251                 if (frand() > 0.8f) {
   1252                     mouth = context.getDrawable(pick(MOUTHS));
   1253                 }
   1254             }
   1255             setOutlineProvider(new ViewOutlineProvider() {
   1256                 @Override
   1257                 public void getOutline(View view, Outline outline) {
   1258                     final int pad = (int) (getWidth() * 1f/6);
   1259                     outline.setOval(pad, pad, getWidth()-pad, getHeight()-pad);
   1260                 }
   1261             });
   1262         }
   1263 
   1264         public boolean intersects(Player p) {
   1265             final int N = p.corners.length/2;
   1266             for (int i=0; i<N; i++) {
   1267                 final int x = (int) p.corners[i*2];
   1268                 final int y = (int) p.corners[i*2+1];
   1269                 if (Math.hypot(x-cx, y-cy) <= r) return true;
   1270             }
   1271             return false;
   1272         }
   1273 
   1274         @Override
   1275         public void step(long t_ms, long dt_ms, float t, float dt) {
   1276             super.step(t_ms, dt_ms, t, dt);
   1277             if (mRotate != 0) {
   1278                 setRotation(getRotation() + dt * 45 * mRotate);
   1279             }
   1280 
   1281             cx = (hitRect.left + hitRect.right)/2;
   1282             cy = (hitRect.top + hitRect.bottom)/2;
   1283             r = getWidth() / 3; // see above re 2/3 container size
   1284         }
   1285 
   1286         @Override
   1287         public void onDraw(Canvas c) {
   1288             super.onDraw(c);
   1289             if (antenna != null) {
   1290                 antenna.setBounds(0, 0, c.getWidth(), c.getHeight());
   1291                 antenna.draw(c);
   1292             }
   1293             if (eyes != null) {
   1294                 eyes.setBounds(0, 0, c.getWidth(), c.getHeight());
   1295                 eyes.draw(c);
   1296             }
   1297             if (mouth != null) {
   1298                 mouth.setBounds(0, 0, c.getWidth(), c.getHeight());
   1299                 mouth.draw(c);
   1300             }
   1301         }
   1302     }
   1303 
   1304     private class Stem extends Obstacle {
   1305         Paint mPaint = new Paint();
   1306         Path mShadow = new Path();
   1307         GradientDrawable mGradient = new GradientDrawable();
   1308         boolean mDrawShadow;
   1309         Path mJandystripe;
   1310         Paint mPaint2;
   1311         int id; // use this to track which pipes have been cleared
   1312 
   1313         public Stem(Context context, float h, boolean drawShadow) {
   1314             super(context, h);
   1315             id = mCurrentPipeId;
   1316 
   1317             mDrawShadow = drawShadow;
   1318             setBackground(null);
   1319             mGradient.setOrientation(GradientDrawable.Orientation.LEFT_RIGHT);
   1320             mPaint.setColor(0xFF000000);
   1321             mPaint.setColorFilter(new PorterDuffColorFilter(0x22000000, PorterDuff.Mode.MULTIPLY));
   1322 
   1323             if (frand() < 0.01f) {
   1324                 mGradient.setColors(new int[]{0xFFFFFFFF, 0xFFDDDDDD});
   1325                 mJandystripe = new Path();
   1326                 mPaint2 = new Paint();
   1327                 mPaint2.setColor(0xFFFF0000);
   1328                 mPaint2.setColorFilter(new PorterDuffColorFilter(0xFFFF0000, PorterDuff.Mode.MULTIPLY));
   1329             } else {
   1330                 //mPaint.setColor(0xFFA1887F);
   1331                 mGradient.setColors(new int[]{0xFFBCAAA4, 0xFFA1887F});
   1332             }
   1333         }
   1334 
   1335         @Override
   1336         public void onAttachedToWindow() {
   1337             super.onAttachedToWindow();
   1338             setWillNotDraw(false);
   1339             setOutlineProvider(new ViewOutlineProvider() {
   1340                 @Override
   1341                 public void getOutline(View view, Outline outline) {
   1342                     outline.setRect(0, 0, getWidth(), getHeight());
   1343                 }
   1344             });
   1345         }
   1346         @Override
   1347         public void onDraw(Canvas c) {
   1348             final int w = c.getWidth();
   1349             final int h = c.getHeight();
   1350             mGradient.setGradientCenter(w * 0.75f, 0);
   1351             mGradient.setBounds(0, 0, w, h);
   1352             mGradient.draw(c);
   1353 
   1354             if (mJandystripe != null) {
   1355                 mJandystripe.reset();
   1356                 mJandystripe.moveTo(0, w);
   1357                 mJandystripe.lineTo(w, 0);
   1358                 mJandystripe.lineTo(w, 2 * w);
   1359                 mJandystripe.lineTo(0, 3 * w);
   1360                 mJandystripe.close();
   1361                 for (int y=0; y<h; y+=4*w) {
   1362                     c.drawPath(mJandystripe, mPaint2);
   1363                     mJandystripe.offset(0, 4 * w);
   1364                 }
   1365             }
   1366 
   1367             if (!mDrawShadow) return;
   1368             mShadow.reset();
   1369             mShadow.moveTo(0, 0);
   1370             mShadow.lineTo(w, 0);
   1371             mShadow.lineTo(w, PARAMS.OBSTACLE_WIDTH * 0.4f + w*1.5f);
   1372             mShadow.lineTo(0, PARAMS.OBSTACLE_WIDTH * 0.4f);
   1373             mShadow.close();
   1374             c.drawPath(mShadow, mPaint);
   1375         }
   1376     }
   1377 
   1378     private class Scenery extends FrameLayout implements GameView {
   1379         public float z;
   1380         public float v;
   1381         public int h, w;
   1382         public Scenery(Context context) {
   1383             super(context);
   1384         }
   1385 
   1386         @Override
   1387         public void step(long t_ms, long dt_ms, float t, float dt) {
   1388             setTranslationX(getTranslationX() - PARAMS.TRANSLATION_PER_SEC * dt * v);
   1389         }
   1390     }
   1391 
   1392     private class Building extends Scenery {
   1393         public Building(Context context) {
   1394             super(context);
   1395 
   1396             w = irand(PARAMS.BUILDING_WIDTH_MIN, PARAMS.BUILDING_WIDTH_MAX);
   1397             h = 0; // will be setup later, along with z
   1398         }
   1399     }
   1400 
   1401     static final int[] CACTI = { R.drawable.cactus1, R.drawable.cactus2, R.drawable.cactus3 };
   1402     private class Cactus extends Building {
   1403         public Cactus(Context context) {
   1404             super(context);
   1405 
   1406             setBackgroundResource(pick(CACTI));
   1407             w = h = irand(PARAMS.BUILDING_WIDTH_MAX / 4, PARAMS.BUILDING_WIDTH_MAX / 2);
   1408         }
   1409     }
   1410 
   1411     static final int[] MOUNTAINS = {
   1412             R.drawable.mountain1, R.drawable.mountain2, R.drawable.mountain3 };
   1413     private class Mountain extends Building {
   1414         public Mountain(Context context) {
   1415             super(context);
   1416 
   1417             setBackgroundResource(pick(MOUNTAINS));
   1418             w = h = irand(PARAMS.BUILDING_WIDTH_MAX / 2, PARAMS.BUILDING_WIDTH_MAX);
   1419             z = 0;
   1420         }
   1421     }
   1422     private class Cloud extends Scenery {
   1423         public Cloud(Context context) {
   1424             super(context);
   1425             setBackgroundResource(frand() < 0.01f ? R.drawable.cloud_off : R.drawable.cloud);
   1426             getBackground().setAlpha(0x40);
   1427             w = h = irand(PARAMS.CLOUD_SIZE_MIN, PARAMS.CLOUD_SIZE_MAX);
   1428             z = 0;
   1429             v = frand(0.15f,0.5f);
   1430         }
   1431     }
   1432 
   1433     private class Star extends Scenery {
   1434         public Star(Context context) {
   1435             super(context);
   1436             setBackgroundResource(R.drawable.star);
   1437             w = h = irand(PARAMS.STAR_SIZE_MIN, PARAMS.STAR_SIZE_MAX);
   1438             v = z = 0;
   1439         }
   1440     }
   1441 }
   1442