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