blob: 81e047084290acc83350c7a757a7b1738384f815 [file] [log] [blame]
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.lunarlander;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.widget.TextView;
/**
* View that draws, takes keystrokes, etc. for a simple LunarLander game.
*
* Has a mode which RUNNING, PAUSED, etc. Has a x, y, dx, dy, ... capturing the
* current ship physics. All x/y etc. are measured with (0,0) at the lower left.
* updatePhysics() advances the physics based on realtime. draw() renders the
* ship, and does an invalidate() to prompt another draw() as soon as possible
* by the system.
*/
class LunarView extends SurfaceView implements SurfaceHolder.Callback {
class LunarThread extends Thread {
/*
* Difficulty setting constants
*/
public static final int DIFFICULTY_EASY = 0;
public static final int DIFFICULTY_HARD = 1;
public static final int DIFFICULTY_MEDIUM = 2;
/*
* Physics constants
*/
public static final int PHYS_DOWN_ACCEL_SEC = 35;
public static final int PHYS_FIRE_ACCEL_SEC = 80;
public static final int PHYS_FUEL_INIT = 60;
public static final int PHYS_FUEL_MAX = 100;
public static final int PHYS_FUEL_SEC = 10;
public static final int PHYS_SLEW_SEC = 120; // degrees/second rotate
public static final int PHYS_SPEED_HYPERSPACE = 180;
public static final int PHYS_SPEED_INIT = 30;
public static final int PHYS_SPEED_MAX = 120;
/*
* State-tracking constants
*/
public static final int STATE_LOSE = 1;
public static final int STATE_PAUSE = 2;
public static final int STATE_READY = 3;
public static final int STATE_RUNNING = 4;
public static final int STATE_WIN = 5;
/*
* Goal condition constants
*/
public static final int TARGET_ANGLE = 18; // > this angle means crash
public static final int TARGET_BOTTOM_PADDING = 17; // px below gear
public static final int TARGET_PAD_HEIGHT = 8; // how high above ground
public static final int TARGET_SPEED = 28; // > this speed means crash
public static final double TARGET_WIDTH = 1.6; // width of target
/*
* UI constants (i.e. the speed & fuel bars)
*/
public static final int UI_BAR = 100; // width of the bar(s)
public static final int UI_BAR_HEIGHT = 10; // height of the bar(s)
private static final String KEY_DIFFICULTY = "mDifficulty";
private static final String KEY_DX = "mDX";
private static final String KEY_DY = "mDY";
private static final String KEY_FUEL = "mFuel";
private static final String KEY_GOAL_ANGLE = "mGoalAngle";
private static final String KEY_GOAL_SPEED = "mGoalSpeed";
private static final String KEY_GOAL_WIDTH = "mGoalWidth";
private static final String KEY_GOAL_X = "mGoalX";
private static final String KEY_HEADING = "mHeading";
private static final String KEY_LANDER_HEIGHT = "mLanderHeight";
private static final String KEY_LANDER_WIDTH = "mLanderWidth";
private static final String KEY_WINS = "mWinsInARow";
private static final String KEY_X = "mX";
private static final String KEY_Y = "mY";
/*
* Member (state) fields
*/
/** The drawable to use as the background of the animation canvas */
private Bitmap mBackgroundImage;
/**
* Current height of the surface/canvas.
*
* @see #setSurfaceSize
*/
private int mCanvasHeight = 1;
/**
* Current width of the surface/canvas.
*
* @see #setSurfaceSize
*/
private int mCanvasWidth = 1;
/** What to draw for the Lander when it has crashed */
private Drawable mCrashedImage;
/**
* Current difficulty -- amount of fuel, allowed angle, etc. Default is
* MEDIUM.
*/
private int mDifficulty;
/** Velocity dx. */
private double mDX;
/** Velocity dy. */
private double mDY;
/** Is the engine burning? */
private boolean mEngineFiring;
/** What to draw for the Lander when the engine is firing */
private Drawable mFiringImage;
/** Fuel remaining */
private double mFuel;
/** Allowed angle. */
private int mGoalAngle;
/** Allowed speed. */
private int mGoalSpeed;
/** Width of the landing pad. */
private int mGoalWidth;
/** X of the landing pad. */
private int mGoalX;
/** Message handler used by thread to interact with TextView */
private Handler mHandler;
/**
* Lander heading in degrees, with 0 up, 90 right. Kept in the range
* 0..360.
*/
private double mHeading;
/** Pixel height of lander image. */
private int mLanderHeight;
/** What to draw for the Lander in its normal state */
private Drawable mLanderImage;
/** Pixel width of lander image. */
private int mLanderWidth;
/** Used to figure out elapsed time between frames */
private long mLastTime;
/** Paint to draw the lines on screen. */
private Paint mLinePaint;
/** "Bad" speed-too-high variant of the line color. */
private Paint mLinePaintBad;
/** The state of the game. One of READY, RUNNING, PAUSE, LOSE, or WIN */
private int mMode;
/** Currently rotating, -1 left, 0 none, 1 right. */
private int mRotating;
/** Indicate whether the surface has been created & is ready to draw */
private boolean mRun = false;
private final Object mRunLock = new Object();
/** Scratch rect object. */
private RectF mScratchRect;
/** Handle to the surface manager object we interact with */
private SurfaceHolder mSurfaceHolder;
/** Number of wins in a row. */
private int mWinsInARow;
/** X of lander center. */
private double mX;
/** Y of lander center. */
private double mY;
public LunarThread(SurfaceHolder surfaceHolder, Context context,
Handler handler) {
// get handles to some important objects
mSurfaceHolder = surfaceHolder;
mHandler = handler;
mContext = context;
Resources res = context.getResources();
// cache handles to our key sprites & other drawables
mLanderImage = context.getResources().getDrawable(
R.drawable.lander_plain);
mFiringImage = context.getResources().getDrawable(
R.drawable.lander_firing);
mCrashedImage = context.getResources().getDrawable(
R.drawable.lander_crashed);
// load background image as a Bitmap instead of a Drawable b/c
// we don't need to transform it and it's faster to draw this way
mBackgroundImage = BitmapFactory.decodeResource(res,
R.drawable.earthrise);
// Use the regular lander image as the model size for all sprites
mLanderWidth = mLanderImage.getIntrinsicWidth();
mLanderHeight = mLanderImage.getIntrinsicHeight();
// Initialize paints for speedometer
mLinePaint = new Paint();
mLinePaint.setAntiAlias(true);
mLinePaint.setARGB(255, 0, 255, 0);
mLinePaintBad = new Paint();
mLinePaintBad.setAntiAlias(true);
mLinePaintBad.setARGB(255, 120, 180, 0);
mScratchRect = new RectF(0, 0, 0, 0);
mWinsInARow = 0;
mDifficulty = DIFFICULTY_MEDIUM;
// initial show-up of lander (not yet playing)
mX = mLanderWidth;
mY = mLanderHeight * 2;
mFuel = PHYS_FUEL_INIT;
mDX = 0;
mDY = 0;
mHeading = 0;
mEngineFiring = true;
}
/**
* Starts the game, setting parameters for the current difficulty.
*/
public void doStart() {
synchronized (mSurfaceHolder) {
// First set the game for Medium difficulty
mFuel = PHYS_FUEL_INIT;
mEngineFiring = false;
mGoalWidth = (int) (mLanderWidth * TARGET_WIDTH);
mGoalSpeed = TARGET_SPEED;
mGoalAngle = TARGET_ANGLE;
int speedInit = PHYS_SPEED_INIT;
// Adjust difficulty params for EASY/HARD
if (mDifficulty == DIFFICULTY_EASY) {
mFuel = mFuel * 3 / 2;
mGoalWidth = mGoalWidth * 4 / 3;
mGoalSpeed = mGoalSpeed * 3 / 2;
mGoalAngle = mGoalAngle * 4 / 3;
speedInit = speedInit * 3 / 4;
} else if (mDifficulty == DIFFICULTY_HARD) {
mFuel = mFuel * 7 / 8;
mGoalWidth = mGoalWidth * 3 / 4;
mGoalSpeed = mGoalSpeed * 7 / 8;
speedInit = speedInit * 4 / 3;
}
// pick a convenient initial location for the lander sprite
mX = mCanvasWidth / 2;
mY = mCanvasHeight - mLanderHeight / 2;
// start with a little random motion
mDY = Math.random() * -speedInit;
mDX = Math.random() * 2 * speedInit - speedInit;
mHeading = 0;
// Figure initial spot for landing, not too near center
while (true) {
mGoalX = (int) (Math.random() * (mCanvasWidth - mGoalWidth));
if (Math.abs(mGoalX - (mX - mLanderWidth / 2)) > mCanvasHeight / 6)
break;
}
mLastTime = System.currentTimeMillis() + 100;
setState(STATE_RUNNING);
}
}
/**
* Pauses the physics update & animation.
*/
public void pause() {
synchronized (mSurfaceHolder) {
if (mMode == STATE_RUNNING) setState(STATE_PAUSE);
}
}
/**
* Restores game state from the indicated Bundle. Typically called when
* the Activity is being restored after having been previously
* destroyed.
*
* @param savedState Bundle containing the game state
*/
public synchronized void restoreState(Bundle savedState) {
synchronized (mSurfaceHolder) {
setState(STATE_PAUSE);
mRotating = 0;
mEngineFiring = false;
mDifficulty = savedState.getInt(KEY_DIFFICULTY);
mX = savedState.getDouble(KEY_X);
mY = savedState.getDouble(KEY_Y);
mDX = savedState.getDouble(KEY_DX);
mDY = savedState.getDouble(KEY_DY);
mHeading = savedState.getDouble(KEY_HEADING);
mLanderWidth = savedState.getInt(KEY_LANDER_WIDTH);
mLanderHeight = savedState.getInt(KEY_LANDER_HEIGHT);
mGoalX = savedState.getInt(KEY_GOAL_X);
mGoalSpeed = savedState.getInt(KEY_GOAL_SPEED);
mGoalAngle = savedState.getInt(KEY_GOAL_ANGLE);
mGoalWidth = savedState.getInt(KEY_GOAL_WIDTH);
mWinsInARow = savedState.getInt(KEY_WINS);
mFuel = savedState.getDouble(KEY_FUEL);
}
}
@Override
public void run() {
while (mRun) {
Canvas c = null;
try {
c = mSurfaceHolder.lockCanvas(null);
synchronized (mSurfaceHolder) {
if (mMode == STATE_RUNNING) updatePhysics();
// Critical section. Do not allow mRun to be set false until
// we are sure all canvas draw operations are complete.
//
// If mRun has been toggled false, inhibit canvas operations.
synchronized (mRunLock) {
if (mRun) doDraw(c);
}
}
} finally {
// do this in a finally so that if an exception is thrown
// during the above, we don't leave the Surface in an
// inconsistent state
if (c != null) {
mSurfaceHolder.unlockCanvasAndPost(c);
}
}
}
}
/**
* Dump game state to the provided Bundle. Typically called when the
* Activity is being suspended.
*
* @return Bundle with this view's state
*/
public Bundle saveState(Bundle map) {
synchronized (mSurfaceHolder) {
if (map != null) {
map.putInt(KEY_DIFFICULTY, Integer.valueOf(mDifficulty));
map.putDouble(KEY_X, Double.valueOf(mX));
map.putDouble(KEY_Y, Double.valueOf(mY));
map.putDouble(KEY_DX, Double.valueOf(mDX));
map.putDouble(KEY_DY, Double.valueOf(mDY));
map.putDouble(KEY_HEADING, Double.valueOf(mHeading));
map.putInt(KEY_LANDER_WIDTH, Integer.valueOf(mLanderWidth));
map.putInt(KEY_LANDER_HEIGHT, Integer
.valueOf(mLanderHeight));
map.putInt(KEY_GOAL_X, Integer.valueOf(mGoalX));
map.putInt(KEY_GOAL_SPEED, Integer.valueOf(mGoalSpeed));
map.putInt(KEY_GOAL_ANGLE, Integer.valueOf(mGoalAngle));
map.putInt(KEY_GOAL_WIDTH, Integer.valueOf(mGoalWidth));
map.putInt(KEY_WINS, Integer.valueOf(mWinsInARow));
map.putDouble(KEY_FUEL, Double.valueOf(mFuel));
}
}
return map;
}
/**
* Sets the current difficulty.
*
* @param difficulty
*/
public void setDifficulty(int difficulty) {
synchronized (mSurfaceHolder) {
mDifficulty = difficulty;
}
}
/**
* Sets if the engine is currently firing.
*/
public void setFiring(boolean firing) {
synchronized (mSurfaceHolder) {
mEngineFiring = firing;
}
}
/**
* Used to signal the thread whether it should be running or not.
* Passing true allows the thread to run; passing false will shut it
* down if it's already running. Calling start() after this was most
* recently called with false will result in an immediate shutdown.
*
* @param b true to run, false to shut down
*/
public void setRunning(boolean b) {
// Do not allow mRun to be modified while any canvas operations
// are potentially in-flight. See doDraw().
synchronized (mRunLock) {
mRun = b;
}
}
/**
* Sets the game mode. That is, whether we are running, paused, in the
* failure state, in the victory state, etc.
*
* @see #setState(int, CharSequence)
* @param mode one of the STATE_* constants
*/
public void setState(int mode) {
synchronized (mSurfaceHolder) {
setState(mode, null);
}
}
/**
* Sets the game mode. That is, whether we are running, paused, in the
* failure state, in the victory state, etc.
*
* @param mode one of the STATE_* constants
* @param message string to add to screen or null
*/
public void setState(int mode, CharSequence message) {
/*
* This method optionally can cause a text message to be displayed
* to the user when the mode changes. Since the View that actually
* renders that text is part of the main View hierarchy and not
* owned by this thread, we can't touch the state of that View.
* Instead we use a Message + Handler to relay commands to the main
* thread, which updates the user-text View.
*/
synchronized (mSurfaceHolder) {
mMode = mode;
if (mMode == STATE_RUNNING) {
Message msg = mHandler.obtainMessage();
Bundle b = new Bundle();
b.putString("text", "");
b.putInt("viz", View.INVISIBLE);
msg.setData(b);
mHandler.sendMessage(msg);
} else {
mRotating = 0;
mEngineFiring = false;
Resources res = mContext.getResources();
CharSequence str = "";
if (mMode == STATE_READY)
str = res.getText(R.string.mode_ready);
else if (mMode == STATE_PAUSE)
str = res.getText(R.string.mode_pause);
else if (mMode == STATE_LOSE)
str = res.getText(R.string.mode_lose);
else if (mMode == STATE_WIN)
str = res.getString(R.string.mode_win_prefix)
+ mWinsInARow + " "
+ res.getString(R.string.mode_win_suffix);
if (message != null) {
str = message + "\n" + str;
}
if (mMode == STATE_LOSE) mWinsInARow = 0;
Message msg = mHandler.obtainMessage();
Bundle b = new Bundle();
b.putString("text", str.toString());
b.putInt("viz", View.VISIBLE);
msg.setData(b);
mHandler.sendMessage(msg);
}
}
}
/* Callback invoked when the surface dimensions change. */
public void setSurfaceSize(int width, int height) {
// synchronized to make sure these all change atomically
synchronized (mSurfaceHolder) {
mCanvasWidth = width;
mCanvasHeight = height;
// don't forget to resize the background image
mBackgroundImage = Bitmap.createScaledBitmap(
mBackgroundImage, width, height, true);
}
}
/**
* Resumes from a pause.
*/
public void unpause() {
// Move the real time clock up to now
synchronized (mSurfaceHolder) {
mLastTime = System.currentTimeMillis() + 100;
}
setState(STATE_RUNNING);
}
/**
* Handles a key-down event.
*
* @param keyCode the key that was pressed
* @param msg the original event object
* @return true
*/
boolean doKeyDown(int keyCode, KeyEvent msg) {
synchronized (mSurfaceHolder) {
boolean okStart = false;
if (keyCode == KeyEvent.KEYCODE_DPAD_UP) okStart = true;
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) okStart = true;
if (keyCode == KeyEvent.KEYCODE_S) okStart = true;
if (okStart
&& (mMode == STATE_READY || mMode == STATE_LOSE || mMode == STATE_WIN)) {
// ready-to-start -> start
doStart();
return true;
} else if (mMode == STATE_PAUSE && okStart) {
// paused -> running
unpause();
return true;
} else if (mMode == STATE_RUNNING) {
// center/space -> fire
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER
|| keyCode == KeyEvent.KEYCODE_SPACE) {
setFiring(true);
return true;
// left/q -> left
} else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT
|| keyCode == KeyEvent.KEYCODE_Q) {
mRotating = -1;
return true;
// right/w -> right
} else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
|| keyCode == KeyEvent.KEYCODE_W) {
mRotating = 1;
return true;
// up -> pause
} else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
pause();
return true;
}
}
return false;
}
}
/**
* Handles a key-up event.
*
* @param keyCode the key that was pressed
* @param msg the original event object
* @return true if the key was handled and consumed, or else false
*/
boolean doKeyUp(int keyCode, KeyEvent msg) {
boolean handled = false;
synchronized (mSurfaceHolder) {
if (mMode == STATE_RUNNING) {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER
|| keyCode == KeyEvent.KEYCODE_SPACE) {
setFiring(false);
handled = true;
} else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT
|| keyCode == KeyEvent.KEYCODE_Q
|| keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
|| keyCode == KeyEvent.KEYCODE_W) {
mRotating = 0;
handled = true;
}
}
}
return handled;
}
/**
* Draws the ship, fuel/speed bars, and background to the provided
* Canvas.
*/
private void doDraw(Canvas canvas) {
// Draw the background image. Operations on the Canvas accumulate
// so this is like clearing the screen.
canvas.drawBitmap(mBackgroundImage, 0, 0, null);
int yTop = mCanvasHeight - ((int) mY + mLanderHeight / 2);
int xLeft = (int) mX - mLanderWidth / 2;
// Draw the fuel gauge
int fuelWidth = (int) (UI_BAR * mFuel / PHYS_FUEL_MAX);
mScratchRect.set(4, 4, 4 + fuelWidth, 4 + UI_BAR_HEIGHT);
canvas.drawRect(mScratchRect, mLinePaint);
// Draw the speed gauge, with a two-tone effect
double speed = Math.sqrt(mDX * mDX + mDY * mDY);
int speedWidth = (int) (UI_BAR * speed / PHYS_SPEED_MAX);
if (speed <= mGoalSpeed) {
mScratchRect.set(4 + UI_BAR + 4, 4,
4 + UI_BAR + 4 + speedWidth, 4 + UI_BAR_HEIGHT);
canvas.drawRect(mScratchRect, mLinePaint);
} else {
// Draw the bad color in back, with the good color in front of
// it
mScratchRect.set(4 + UI_BAR + 4, 4,
4 + UI_BAR + 4 + speedWidth, 4 + UI_BAR_HEIGHT);
canvas.drawRect(mScratchRect, mLinePaintBad);
int goalWidth = (UI_BAR * mGoalSpeed / PHYS_SPEED_MAX);
mScratchRect.set(4 + UI_BAR + 4, 4, 4 + UI_BAR + 4 + goalWidth,
4 + UI_BAR_HEIGHT);
canvas.drawRect(mScratchRect, mLinePaint);
}
// Draw the landing pad
canvas.drawLine(mGoalX, 1 + mCanvasHeight - TARGET_PAD_HEIGHT,
mGoalX + mGoalWidth, 1 + mCanvasHeight - TARGET_PAD_HEIGHT,
mLinePaint);
// Draw the ship with its current rotation
canvas.save();
canvas.rotate((float) mHeading, (float) mX, mCanvasHeight
- (float) mY);
if (mMode == STATE_LOSE) {
mCrashedImage.setBounds(xLeft, yTop, xLeft + mLanderWidth, yTop
+ mLanderHeight);
mCrashedImage.draw(canvas);
} else if (mEngineFiring) {
mFiringImage.setBounds(xLeft, yTop, xLeft + mLanderWidth, yTop
+ mLanderHeight);
mFiringImage.draw(canvas);
} else {
mLanderImage.setBounds(xLeft, yTop, xLeft + mLanderWidth, yTop
+ mLanderHeight);
mLanderImage.draw(canvas);
}
canvas.restore();
}
/**
* Figures the lander state (x, y, fuel, ...) based on the passage of
* realtime. Does not invalidate(). Called at the start of draw().
* Detects the end-of-game and sets the UI to the next state.
*/
private void updatePhysics() {
long now = System.currentTimeMillis();
// Do nothing if mLastTime is in the future.
// This allows the game-start to delay the start of the physics
// by 100ms or whatever.
if (mLastTime > now) return;
double elapsed = (now - mLastTime) / 1000.0;
// mRotating -- update heading
if (mRotating != 0) {
mHeading += mRotating * (PHYS_SLEW_SEC * elapsed);
// Bring things back into the range 0..360
if (mHeading < 0)
mHeading += 360;
else if (mHeading >= 360) mHeading -= 360;
}
// Base accelerations -- 0 for x, gravity for y
double ddx = 0.0;
double ddy = -PHYS_DOWN_ACCEL_SEC * elapsed;
if (mEngineFiring) {
// taking 0 as up, 90 as to the right
// cos(deg) is ddy component, sin(deg) is ddx component
double elapsedFiring = elapsed;
double fuelUsed = elapsedFiring * PHYS_FUEL_SEC;
// tricky case where we run out of fuel partway through the
// elapsed
if (fuelUsed > mFuel) {
elapsedFiring = mFuel / fuelUsed * elapsed;
fuelUsed = mFuel;
// Oddball case where we adjust the "control" from here
mEngineFiring = false;
}
mFuel -= fuelUsed;
// have this much acceleration from the engine
double accel = PHYS_FIRE_ACCEL_SEC * elapsedFiring;
double radians = 2 * Math.PI * mHeading / 360;
ddx = Math.sin(radians) * accel;
ddy += Math.cos(radians) * accel;
}
double dxOld = mDX;
double dyOld = mDY;
// figure speeds for the end of the period
mDX += ddx;
mDY += ddy;
// figure position based on average speed during the period
mX += elapsed * (mDX + dxOld) / 2;
mY += elapsed * (mDY + dyOld) / 2;
mLastTime = now;
// Evaluate if we have landed ... stop the game
double yLowerBound = TARGET_PAD_HEIGHT + mLanderHeight / 2
- TARGET_BOTTOM_PADDING;
if (mY <= yLowerBound) {
mY = yLowerBound;
int result = STATE_LOSE;
CharSequence message = "";
Resources res = mContext.getResources();
double speed = Math.sqrt(mDX * mDX + mDY * mDY);
boolean onGoal = (mGoalX <= mX - mLanderWidth / 2 && mX
+ mLanderWidth / 2 <= mGoalX + mGoalWidth);
// "Hyperspace" win -- upside down, going fast,
// puts you back at the top.
if (onGoal && Math.abs(mHeading - 180) < mGoalAngle
&& speed > PHYS_SPEED_HYPERSPACE) {
result = STATE_WIN;
mWinsInARow++;
doStart();
return;
// Oddball case: this case does a return, all other cases
// fall through to setMode() below.
} else if (!onGoal) {
message = res.getText(R.string.message_off_pad);
} else if (!(mHeading <= mGoalAngle || mHeading >= 360 - mGoalAngle)) {
message = res.getText(R.string.message_bad_angle);
} else if (speed > mGoalSpeed) {
message = res.getText(R.string.message_too_fast);
} else {
result = STATE_WIN;
mWinsInARow++;
}
setState(result, message);
}
}
}
/** Handle to the application context, used to e.g. fetch Drawables. */
private Context mContext;
/** Pointer to the text view to display "Paused.." etc. */
private TextView mStatusText;
/** The thread that actually draws the animation */
private LunarThread thread;
public LunarView(Context context, AttributeSet attrs) {
super(context, attrs);
// register our interest in hearing about changes to our surface
SurfaceHolder holder = getHolder();
holder.addCallback(this);
// create thread only; it's started in surfaceCreated()
thread = new LunarThread(holder, context, new Handler() {
@Override
public void handleMessage(Message m) {
mStatusText.setVisibility(m.getData().getInt("viz"));
mStatusText.setText(m.getData().getString("text"));
}
});
setFocusable(true); // make sure we get key events
}
/**
* Fetches the animation thread corresponding to this LunarView.
*
* @return the animation thread
*/
public LunarThread getThread() {
return thread;
}
/**
* Standard override to get key-press events.
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent msg) {
return thread.doKeyDown(keyCode, msg);
}
/**
* Standard override for key-up. We actually care about these, so we can
* turn off the engine or stop rotating.
*/
@Override
public boolean onKeyUp(int keyCode, KeyEvent msg) {
return thread.doKeyUp(keyCode, msg);
}
/**
* Standard window-focus override. Notice focus lost so we can pause on
* focus lost. e.g. user switches to take a call.
*/
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
if (!hasWindowFocus) thread.pause();
}
/**
* Installs a pointer to the text view used for messages.
*/
public void setTextView(TextView textView) {
mStatusText = textView;
}
/* Callback invoked when the surface dimensions change. */
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
thread.setSurfaceSize(width, height);
}
/*
* Callback invoked when the Surface has been created and is ready to be
* used.
*/
public void surfaceCreated(SurfaceHolder holder) {
// start the thread here so that we don't busy-wait in run()
// waiting for the surface to be created
thread.setRunning(true);
thread.start();
}
/*
* Callback invoked when the Surface has been destroyed and must no longer
* be touched. WARNING: after this method returns, the Surface/Canvas must
* never be touched again!
*/
public void surfaceDestroyed(SurfaceHolder holder) {
// we have to tell thread to shut down & wait for it to finish, or else
// it might touch the Surface after we return and explode
boolean retry = true;
thread.setRunning(false);
while (retry) {
try {
thread.join();
retry = false;
} catch (InterruptedException e) {
}
}
}
}