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