• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
L(String s, Object ... objects)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;
Params(Resources res)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 
MLand(Context context)183     public MLand(Context context) {
184         this(context, null);
185     }
186 
MLand(Context context, AttributeSet attrs)187     public MLand(Context context, AttributeSet attrs) {
188         this(context, attrs, 0);
189     }
190 
MLand(Context context, AttributeSet attrs, int defStyle)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
onAttachedToWindow()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
willNotDraw()230     public boolean willNotDraw() {
231         return !DEBUG;
232     }
233 
getGameWidth()234     public int getGameWidth() { return mWidth; }
getGameHeight()235     public int getGameHeight() { return mHeight; }
getGameTime()236     public float getGameTime() { return t; }
getLastTimeStep()237     public float getLastTimeStep() { return dt; }
238 
setScoreFieldHolder(ViewGroup vg)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 
setSplash(View v)254     public void setSplash(View v) {
255         mSplash = v;
256     }
257 
isGamePad(InputDevice dev)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 
getGameControllers()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 
getControllerPlayer(int id)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
onSizeChanged(int w, int h, int oldw, int oldh)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 
luma(int bgcolor)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 
getPlayer(int i)306     public Player getPlayer(int i) {
307         return i < mPlayers.size() ? mPlayers.get(i) : null;
308     }
309 
addPlayerInternal(Player p)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 
removePlayerInternal(Player p)325     private void removePlayerInternal(Player p) {
326         if (mPlayers.remove(p)) {
327             removeView(p);
328             mScoreFields.removeView(p.mScoreField);
329             realignPlayers();
330         }
331     }
332 
realignPlayers()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 
clearPlayers()343     private void clearPlayers() {
344         while (mPlayers.size() > 0) {
345             removePlayerInternal(mPlayers.get(0));
346         }
347     }
348 
setupPlayers(int num)349     public void setupPlayers(int num) {
350         clearPlayers();
351         for (int i=0; i<num; i++) {
352             addPlayerInternal(Player.create(this));
353         }
354     }
355 
addPlayer()356     public void addPlayer() {
357         if (getNumPlayers() == MAX_PLAYERS) return;
358         addPlayerInternal(Player.create(this));
359     }
360 
getNumPlayers()361     public int getNumPlayers() {
362         return mPlayers.size();
363     }
364 
removePlayer()365     public void removePlayer() {
366         if (getNumPlayers() == MIN_PLAYERS) return;
367         removePlayerInternal(mPlayers.get(mPlayers.size() - 1));
368     }
369 
thump(int playerIndex, long ms)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 
reset()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