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