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
17package com.android.systemui.egg;
18
19import android.animation.LayoutTransition;
20import android.animation.TimeAnimator;
21import android.content.Context;
22import android.content.res.Resources;
23import android.graphics.Canvas;
24import android.graphics.Color;
25import android.graphics.Matrix;
26import android.graphics.Outline;
27import android.graphics.Paint;
28import android.graphics.Path;
29import android.graphics.PorterDuff;
30import android.graphics.PorterDuffColorFilter;
31import android.graphics.Rect;
32import android.graphics.drawable.Drawable;
33import android.graphics.drawable.GradientDrawable;
34import android.media.AudioAttributes;
35import android.media.AudioManager;
36import android.os.Vibrator;
37import android.util.AttributeSet;
38import android.util.Log;
39import android.view.Gravity;
40import android.view.InputDevice;
41import android.view.KeyEvent;
42import android.view.LayoutInflater;
43import android.view.MotionEvent;
44import android.view.View;
45import android.view.ViewGroup;
46import android.view.ViewOutlineProvider;
47import android.widget.FrameLayout;
48import android.widget.ImageView;
49import android.widget.TextView;
50
51import com.android.internal.logging.MetricsLogger;
52import com.android.systemui.R;
53
54import java.util.ArrayList;
55
56// It's like LLand, but "M"ultiplayer.
57public 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
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;
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
182    public MLand(Context context) {
183        this(context, null);
184    }
185
186    public MLand(Context context, AttributeSet attrs) {
187        this(context, attrs, 0);
188    }
189
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
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
229    public boolean willNotDraw() {
230        return !DEBUG;
231    }
232
233    public int getGameWidth() { return mWidth; }
234    public int getGameHeight() { return mHeight; }
235    public float getGameTime() { return t; }
236    public float getLastTimeStep() { return dt; }
237
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
253    public void setSplash(View v) {
254        mSplash = v;
255    }
256
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
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
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
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
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
305    public Player getPlayer(int i) {
306        return i < mPlayers.size() ? mPlayers.get(i) : null;
307    }
308
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
324    private void removePlayerInternal(Player p) {
325        if (mPlayers.remove(p)) {
326            removeView(p);
327            mScoreFields.removeView(p.mScoreField);
328            realignPlayers();
329        }
330    }
331
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
342    private void clearPlayers() {
343        while (mPlayers.size() > 0) {
344            removePlayerInternal(mPlayers.get(0));
345        }
346    }
347
348    public void setupPlayers(int num) {
349        clearPlayers();
350        for (int i=0; i<num; i++) {
351            addPlayerInternal(Player.create(this));
352        }
353    }
354
355    public void addPlayer() {
356        if (getNumPlayers() == MAX_PLAYERS) return;
357        addPlayerInternal(Player.create(this));
358    }
359
360    public int getNumPlayers() {
361        return mPlayers.size();
362    }
363
364    public void removePlayer() {
365        if (getNumPlayers() == MIN_PLAYERS) return;
366        removePlayerInternal(mPlayers.get(mPlayers.size() - 1));
367    }
368
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
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