Modified Snake Sample app to run on non-touch devices like Google Tv and also devices with no dpad support

2D canvas DrawVertex to create a vector graphic background
touch & keypad playable
landscape & portrait for all known device sizes.  All measurements in DP and nicely scaling.
using xml drawables to rotate "dpad" arrow

Change-Id: I995acaaf0935c13c03fcbcf974ce5af06f46780e
This commit is contained in:
Kartic Grover
2012-05-01 11:02:58 -07:00
parent 424c95da4b
commit a1843415aa
21 changed files with 585 additions and 274 deletions

View File

@@ -14,22 +14,31 @@
limitations under the License. limitations under the License.
--> -->
<!-- Declare the contents of this Android application. The namespace <!-- Declare the contents of this Android application. The namespace attribute
attribute brings in the Android platform namespace, and the package brings in the Android platform namespace, and the package supplies a unique
supplies a unique name for the application. When writing your name for the application. When writing your own application, the package
own application, the package name must be changed from "com.example.*" name must be changed from "com.example.*" to come from a domain that you
to come from a domain that you own or have control over. --> own or have control over. -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.snake"> package="com.example.android.snake">
<application android:label="Snake on a Phone">
<activity android:name="Snake" <uses-sdk android:minSdkVersion="3" android:targetSdkVersion="15" />
android:theme="@android:style/Theme.NoTitleBar" <!-- Declares that the app supports devices w/o touch, such as a Google TV device -->
android:screenOrientation="portrait" <uses-feature android:name="android.hardware.touchscreen"
android:configChanges="keyboardHidden|orientation"> android:required="false" />
<supports-screens android:largeScreens="true" />
<application android:icon="@drawable/ic_launcher" android:label="@string/app_name">
<activity android:name="Snake" android:configChanges="keyboardHidden|orientation"
android:screenOrientation="nosensor" android:theme="@android:style/Theme.NoTitleBar">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
</application> </application>
</manifest> </manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2012 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.
-->
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:fromDegrees="180"
android:toDegrees="180"
android:pivotX="50%"
android:pivotY="50%"
android:drawable="@drawable/dpad_up"/>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2012 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.
-->
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:fromDegrees="-90"
android:toDegrees="-90"
android:pivotX="50%"
android:pivotY="50%"
android:drawable="@drawable/dpad_up"/>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2012 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.
-->
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:fromDegrees="90"
android:toDegrees="90"
android:pivotX="50%"
android:pivotY="50%"
android:drawable="@drawable/dpad_up"/>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

70
samples/Snake/res/layout/snake_layout.xml Normal file → Executable file
View File

@@ -14,30 +14,70 @@
limitations under the License. limitations under the License.
--> -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res/com.example.android.snake">
<com.example.android.snake.BackgroundView
android:id="@+id/background"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
app:colorSegmentOne="@color/muted_red"
app:colorSegmentTwo="@color/muted_yellow"
app:colorSegmentThree="@color/muted_blue"
app:colorSegmentFour="@color/muted_green"
/>
<com.example.android.snake.SnakeView <com.example.android.snake.SnakeView
android:id="@+id/snake" android:id="@+id/snake"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tileSize="24" app:tileSize="24dp" />
/>
<RelativeLayout <TextView android:id="@+id/text"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" > android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:textColor="@color/text_violet"
android:textSize="24sp"
android:visibility="visible" />
<TextView <RelativeLayout android:id="@+id/arrowContainer"
android:id="@+id/text" android:layout_width="match_parent"
android:text="@string/snake_layout_text_text" android:layout_height="match_parent"
android:visibility="visible" android:visibility="gone">
<ImageView android:id="@+id/imageUp"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true" android:layout_alignParentTop="true"
android:gravity="center_horizontal" android:layout_centerHorizontal="true"
android:textColor="#ff8888ff" android:padding="20dp"
android:textSize="24sp"/> android:src="@drawable/dpad_up" />
<ImageView android:id="@+id/imageLeft"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:padding="20dp"
android:src="@drawable/dpad_left" />
<ImageView android:id="@+id/imageRight"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:padding="20dp"
android:src="@drawable/dpad_right" />
<ImageView android:id="@+id/imageDown"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:padding="20dp"
android:src="@drawable/dpad_down" />
</RelativeLayout> </RelativeLayout>
</FrameLayout>
</merge>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2012 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.
-->
<resources>
<string name="app_name">Snake</string>
<string name="mode_ready">Snake\nTouch Screen To Play</string>
<string name="mode_pause">Paused\nTouch Screen To Resume</string>
<string name="mode_lose">Game Over\nScore: %1$d \nTouch Screen To Play</string>
</resources>

16
samples/Snake/res/values/attrs.xml Normal file → Executable file
View File

@@ -15,8 +15,16 @@
--> -->
<resources> <resources>
<declare-styleable name="TileView">
<attr name="tileSize" format="integer" />
</declare-styleable>
</resources>
<declare-styleable name="TileView">
<attr name="tileSize" format="dimension" />
</declare-styleable>
<declare-styleable name="BackgroundView">
<!-- Defining properties to use four different colors in 4 segments of BackgroundView -->
<attr name="colorSegmentOne" format="color" />
<attr name="colorSegmentTwo" format="color" />
<attr name="colorSegmentThree" format="color" />
<attr name="colorSegmentFour" format="color" />
</declare-styleable>
</resources>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2012 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.
-->
<resources>
<color name="muted_red">#ff400010</color>
<color name="muted_yellow">#ff404000</color>
<color name="muted_blue">#ff100050</color>
<color name="muted_green">#ff004000</color>
<color name="text_violet">#ff8888ff</color>
</resources>

6
samples/Snake/res/values/strings.xml Normal file → Executable file
View File

@@ -15,10 +15,8 @@
--> -->
<resources> <resources>
<string name="app_name">Snake</string>
<string name="mode_ready">Snake\nPress Up To Play</string> <string name="mode_ready">Snake\nPress Up To Play</string>
<string name="mode_pause">Paused\nPress Up To Resume</string> <string name="mode_pause">Paused\nPress Up To Resume</string>
<string name="mode_lose_prefix">Game Over\nScore: </string> <string name="mode_lose">Game Over\nScore: %1$d \nPress Up To Play</string>
<string name="mode_lose_suffix">\nPress Up To Play</string>
<string name="snake_layout_text_text"></string>
</resources> </resources>

View File

@@ -0,0 +1,90 @@
/*
* Copyright (C) 2012 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.snake;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
import java.util.Arrays;
/**
* Background View: Draw 4 full-screen RGBY triangles
*/
public class BackgroundView extends View {
private int[] mColors = new int[4];
private final short[] mIndices =
{ 0, 1, 2, 0, 3, 4, 0, 1, 4 // Corner points for triangles (with offset = 2)
};
private float[] mVertexPoints = null;
public BackgroundView(Context context, AttributeSet attrs) {
super(context, attrs);
setFocusable(true);
// retrieve colors for 4 segments from styleable properties
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.BackgroundView);
mColors[0] = a.getColor(R.styleable.BackgroundView_colorSegmentOne, Color.RED);
mColors[1] = a.getColor(R.styleable.BackgroundView_colorSegmentTwo, Color.YELLOW);
mColors[2] = a.getColor(R.styleable.BackgroundView_colorSegmentThree, Color.BLUE);
mColors[3] = a.getColor(R.styleable.BackgroundView_colorSegmentFour, Color.GREEN);
a.recycle();
}
@Override
protected void onDraw(Canvas canvas) {
assert(mVertexPoints != null);
// Colors for each vertex
int[] mFillColors = new int[mVertexPoints.length];
for (int triangle = 0; triangle < mColors.length; triangle++) {
// Set color for all vertex points to current triangle color
Arrays.fill(mFillColors, mColors[triangle]);
// Draw one triangle
canvas.drawVertices(Canvas.VertexMode.TRIANGLES, mVertexPoints.length, mVertexPoints,
0, null, 0, // No Textures
mFillColors, 0, mIndices,
triangle * 2, 3, // Use 3 vertices via Index Array with offset 2
new Paint());
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// Construct our center and four corners
mVertexPoints = new float[] {
w / 2, h / 2,
0, 0,
w, 0,
w, h,
0, h
};
}
}

83
samples/Snake/src/com/example/android/snake/Snake.java Normal file → Executable file
View File

@@ -18,27 +18,38 @@ package com.example.android.snake;
import android.app.Activity; import android.app.Activity;
import android.os.Bundle; import android.os.Bundle;
import android.view.Window; import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.widget.TextView; import android.widget.TextView;
/** /**
* Snake: a simple game that everyone can enjoy. * Snake: a simple game that everyone can enjoy.
* *
* This is an implementation of the classic Game "Snake", in which you control a * This is an implementation of the classic Game "Snake", in which you control a serpent roaming
* serpent roaming around the garden looking for apples. Be careful, though, * around the garden looking for apples. Be careful, though, because when you catch one, not only
* because when you catch one, not only will you become longer, but you'll move * will you become longer, but you'll move faster. Running into yourself or the walls will end the
* faster. Running into yourself or the walls will end the game. * game.
* *
*/ */
public class Snake extends Activity { public class Snake extends Activity {
private SnakeView mSnakeView; /**
* Constants for desired direction of moving the snake
*/
public static int MOVE_LEFT = 0;
public static int MOVE_UP = 1;
public static int MOVE_DOWN = 2;
public static int MOVE_RIGHT = 3;
private static String ICICLE_KEY = "snake-view"; private static String ICICLE_KEY = "snake-view";
private SnakeView mSnakeView;
/** /**
* Called when Activity is first created. Turns off the title bar, sets up * Called when Activity is first created. Turns off the title bar, sets up the content views,
* the content views, and fires up the SnakeView. * and fires up the SnakeView.
* *
*/ */
@Override @Override
@@ -48,7 +59,8 @@ public class Snake extends Activity {
setContentView(R.layout.snake_layout); setContentView(R.layout.snake_layout);
mSnakeView = (SnakeView) findViewById(R.id.snake); mSnakeView = (SnakeView) findViewById(R.id.snake);
mSnakeView.setTextView((TextView) findViewById(R.id.text)); mSnakeView.setDependentViews((TextView) findViewById(R.id.text),
findViewById(R.id.arrowContainer), findViewById(R.id.background));
if (savedInstanceState == null) { if (savedInstanceState == null) {
// We were just launched -- set up a new game // We were just launched -- set up a new game
@@ -62,6 +74,31 @@ public class Snake extends Activity {
mSnakeView.setMode(SnakeView.PAUSE); mSnakeView.setMode(SnakeView.PAUSE);
} }
} }
mSnakeView.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (mSnakeView.getGameState() == SnakeView.RUNNING) {
// Normalize x,y between 0 and 1
float x = event.getX() / v.getWidth();
float y = event.getY() / v.getHeight();
// Direction will be [0,1,2,3] depending on quadrant
int direction = 0;
direction = (x > y) ? 1 : 0;
direction |= (x > 1 - y) ? 2 : 0;
// Direction is same as the quadrant which was clicked
mSnakeView.moveSnake(direction);
} else {
// If the game is not running then on touching any part of the screen
// we start the game by sending MOVE_UP signal to SnakeView
mSnakeView.moveSnake(MOVE_UP);
}
return false;
}
});
} }
@Override @Override
@@ -73,8 +110,34 @@ public class Snake extends Activity {
@Override @Override
public void onSaveInstanceState(Bundle outState) { public void onSaveInstanceState(Bundle outState) {
//Store the game state // Store the game state
outState.putBundle(ICICLE_KEY, mSnakeView.saveState()); outState.putBundle(ICICLE_KEY, mSnakeView.saveState());
} }
/**
* Handles key events in the game. Update the direction our snake is traveling based on the
* DPAD.
*
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent msg) {
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_UP:
mSnakeView.moveSnake(MOVE_UP);
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
mSnakeView.moveSnake(MOVE_RIGHT);
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
mSnakeView.moveSnake(MOVE_DOWN);
break;
case KeyEvent.KEYCODE_DPAD_LEFT:
mSnakeView.moveSnake(MOVE_LEFT);
break;
}
return super.onKeyDown(keyCode, msg);
}
} }

View File

@@ -16,33 +16,29 @@
package com.example.android.snake; package com.example.android.snake;
import java.util.ArrayList;
import java.util.Random;
import android.content.Context; import android.content.Context;
import android.content.res.Resources; import android.content.res.Resources;
import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.Message; import android.os.Message;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.os.Bundle;
import android.util.Log; import android.util.Log;
import android.view.KeyEvent;
import android.view.View; import android.view.View;
import android.widget.TextView; import android.widget.TextView;
import java.util.ArrayList;
import java.util.Random;
/** /**
* SnakeView: implementation of a simple game of Snake * SnakeView: implementation of a simple game of Snake
*
*
*/ */
public class SnakeView extends TileView { public class SnakeView extends TileView {
private static final String TAG = "SnakeView"; private static final String TAG = "SnakeView";
/** /**
* Current mode of application: READY to run, RUNNING, or you have already * Current mode of application: READY to run, RUNNING, or you have already lost. static final
* lost. static final ints are used instead of an enum for performance * ints are used instead of an enum for performance reasons.
* reasons.
*/ */
private int mMode = READY; private int mMode = READY;
public static final int PAUSE = 0; public static final int PAUSE = 0;
@@ -68,26 +64,36 @@ public class SnakeView extends TileView {
private static final int GREEN_STAR = 3; private static final int GREEN_STAR = 3;
/** /**
* mScore: used to track the number of apples captured mMoveDelay: number of * mScore: Used to track the number of apples captured mMoveDelay: number of milliseconds
* milliseconds between snake movements. This will decrease as apples are * between snake movements. This will decrease as apples are captured.
* captured.
*/ */
private long mScore = 0; private long mScore = 0;
private long mMoveDelay = 600; private long mMoveDelay = 600;
/** /**
* mLastMove: tracks the absolute time when the snake last moved, and is used * mLastMove: Tracks the absolute time when the snake last moved, and is used to determine if a
* to determine if a move should be made based on mMoveDelay. * move should be made based on mMoveDelay.
*/ */
private long mLastMove; private long mLastMove;
/** /**
* mStatusText: text shows to the user in some run states * mStatusText: Text shows to the user in some run states
*/ */
private TextView mStatusText; private TextView mStatusText;
/** /**
* mSnakeTrail: a list of Coordinates that make up the snake's body * mArrowsView: View which shows 4 arrows to signify 4 directions in which the snake can move
* mAppleList: the secret location of the juicy apples the snake craves. */
private View mArrowsView;
/**
* mBackgroundView: Background View which shows 4 different colored triangles pressing which
* moves the snake
*/
private View mBackgroundView;
/**
* mSnakeTrail: A list of Coordinates that make up the snake's body mAppleList: The secret
* location of the juicy apples the snake craves.
*/ */
private ArrayList<Coordinate> mSnakeTrail = new ArrayList<Coordinate>(); private ArrayList<Coordinate> mSnakeTrail = new ArrayList<Coordinate>();
private ArrayList<Coordinate> mAppleList = new ArrayList<Coordinate>(); private ArrayList<Coordinate> mAppleList = new ArrayList<Coordinate>();
@@ -98,10 +104,11 @@ public class SnakeView extends TileView {
private static final Random RNG = new Random(); private static final Random RNG = new Random();
/** /**
* Create a simple handler that we can use to cause animation to happen. We * Create a simple handler that we can use to cause animation to happen. We set ourselves as a
* set ourselves as a target and we can use the sleep() * target and we can use the sleep() function to cause an update/invalidate to occur at a later
* function to cause an update/invalidate to occur at a later date. * date.
*/ */
private RefreshHandler mRedrawHandler = new RefreshHandler(); private RefreshHandler mRedrawHandler = new RefreshHandler();
class RefreshHandler extends Handler { class RefreshHandler extends Handler {
@@ -118,7 +125,6 @@ public class SnakeView extends TileView {
} }
}; };
/** /**
* Constructs a SnakeView based on inflation from XML * Constructs a SnakeView based on inflation from XML
* *
@@ -127,15 +133,16 @@ public class SnakeView extends TileView {
*/ */
public SnakeView(Context context, AttributeSet attrs) { public SnakeView(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
initSnakeView(); initSnakeView(context);
} }
public SnakeView(Context context, AttributeSet attrs, int defStyle) { public SnakeView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle); super(context, attrs, defStyle);
initSnakeView(); initSnakeView(context);
} }
private void initSnakeView() { private void initSnakeView(Context context) {
setFocusable(true); setFocusable(true);
Resources r = this.getContext().getResources(); Resources r = this.getContext().getResources();
@@ -147,7 +154,6 @@ public class SnakeView extends TileView {
} }
private void initNewGame() { private void initNewGame() {
mSnakeTrail.clear(); mSnakeTrail.clear();
mAppleList.clear(); mAppleList.clear();
@@ -155,7 +161,6 @@ public class SnakeView extends TileView {
// For now we're just going to load up a short default eastbound snake // For now we're just going to load up a short default eastbound snake
// that's just turned north // that's just turned north
mSnakeTrail.add(new Coordinate(7, 7)); mSnakeTrail.add(new Coordinate(7, 7));
mSnakeTrail.add(new Coordinate(6, 7)); mSnakeTrail.add(new Coordinate(6, 7));
mSnakeTrail.add(new Coordinate(5, 7)); mSnakeTrail.add(new Coordinate(5, 7));
@@ -172,30 +177,29 @@ public class SnakeView extends TileView {
mScore = 0; mScore = 0;
} }
/** /**
* Given a ArrayList of coordinates, we need to flatten them into an array of * Given a ArrayList of coordinates, we need to flatten them into an array of ints before we can
* ints before we can stuff them into a map for flattening and storage. * stuff them into a map for flattening and storage.
* *
* @param cvec : a ArrayList of Coordinate objects * @param cvec : a ArrayList of Coordinate objects
* @return : a simple array containing the x/y values of the coordinates * @return : a simple array containing the x/y values of the coordinates as
* as [x1,y1,x2,y2,x3,y3...] * [x1,y1,x2,y2,x3,y3...]
*/ */
private int[] coordArrayListToArray(ArrayList<Coordinate> cvec) { private int[] coordArrayListToArray(ArrayList<Coordinate> cvec) {
int count = cvec.size(); int[] rawArray = new int[cvec.size() * 2];
int[] rawArray = new int[count * 2];
for (int index = 0; index < count; index++) { int i = 0;
Coordinate c = cvec.get(index); for (Coordinate c : cvec) {
rawArray[2 * index] = c.x; rawArray[i++] = c.x;
rawArray[2 * index + 1] = c.y; rawArray[i++] = c.y;
} }
return rawArray; return rawArray;
} }
/** /**
* Save game state so that the user does not lose anything * Save game state so that the user does not lose anything if the game process is killed while
* if the game process is killed while we are in the * we are in the background.
* background.
* *
* @return a Bundle with this view's state * @return a Bundle with this view's state
*/ */
@@ -213,8 +217,8 @@ public class SnakeView extends TileView {
} }
/** /**
* Given a flattened array of ordinate pairs, we reconstitute them into a * Given a flattened array of ordinate pairs, we reconstitute them into a ArrayList of
* ArrayList of Coordinate objects * Coordinate objects
* *
* @param rawArray : [x1,y1,x2,y2,...] * @param rawArray : [x1,y1,x2,y2,...]
* @return a ArrayList of Coordinates * @return a ArrayList of Coordinates
@@ -246,83 +250,79 @@ public class SnakeView extends TileView {
mSnakeTrail = coordArrayToArrayList(icicle.getIntArray("mSnakeTrail")); mSnakeTrail = coordArrayToArrayList(icicle.getIntArray("mSnakeTrail"));
} }
/* /**
* handles key events in the game. Update the direction our snake is traveling * Handles snake movement triggers from Snake Activity and moves the snake accordingly. Ignore
* based on the DPAD. Ignore events that would cause the snake to immediately * events that would cause the snake to immediately turn back on itself.
* turn back on itself.
* *
* (non-Javadoc) * @param direction The desired direction of movement
*
* @see android.view.View#onKeyDown(int, android.os.KeyEvent)
*/ */
@Override public void moveSnake(int direction) {
public boolean onKeyDown(int keyCode, KeyEvent msg) {
if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { if (direction == Snake.MOVE_UP) {
if (mMode == READY | mMode == LOSE) { if (mMode == READY | mMode == LOSE) {
/* /*
* At the beginning of the game, or the end of a previous one, * At the beginning of the game, or the end of a previous one,
* we should start a new game. * we should start a new game if UP key is clicked.
*/ */
initNewGame(); initNewGame();
setMode(RUNNING); setMode(RUNNING);
update(); update();
return (true); return;
} }
if (mMode == PAUSE) { if (mMode == PAUSE) {
/* /*
* If the game is merely paused, we should just continue where * If the game is merely paused, we should just continue where we left off.
* we left off.
*/ */
setMode(RUNNING); setMode(RUNNING);
update(); update();
return (true); return;
} }
if (mDirection != SOUTH) { if (mDirection != SOUTH) {
mNextDirection = NORTH; mNextDirection = NORTH;
} }
return (true); return;
} }
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { if (direction == Snake.MOVE_DOWN) {
if (mDirection != NORTH) { if (mDirection != NORTH) {
mNextDirection = SOUTH; mNextDirection = SOUTH;
} }
return (true); return;
} }
if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { if (direction == Snake.MOVE_LEFT) {
if (mDirection != EAST) { if (mDirection != EAST) {
mNextDirection = WEST; mNextDirection = WEST;
} }
return (true); return;
} }
if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { if (direction == Snake.MOVE_RIGHT) {
if (mDirection != WEST) { if (mDirection != WEST) {
mNextDirection = EAST; mNextDirection = EAST;
} }
return (true); return;
} }
return super.onKeyDown(keyCode, msg);
} }
/** /**
* Sets the TextView that will be used to give information (such as "Game * Sets the Dependent views that will be used to give information (such as "Game Over" to the
* Over" to the user. * user and also to handle touch events for making movements
* *
* @param newView * @param newView
*/ */
public void setTextView(TextView newView) { public void setDependentViews(TextView msgView, View arrowView, View backgroundView) {
mStatusText = newView; mStatusText = msgView;
mArrowsView = arrowView;
mBackgroundView = backgroundView;
} }
/** /**
* Updates the current mode of the application (RUNNING or PAUSED or the like) * Updates the current mode of the application (RUNNING or PAUSED or the like) as well as sets
* as well as sets the visibility of textview for notification * the visibility of textview for notification
* *
* @param newMode * @param newMode
*/ */
@@ -330,23 +330,33 @@ public class SnakeView extends TileView {
int oldMode = mMode; int oldMode = mMode;
mMode = newMode; mMode = newMode;
if (newMode == RUNNING & oldMode != RUNNING) { if (newMode == RUNNING && oldMode != RUNNING) {
// hide the game instructions
mStatusText.setVisibility(View.INVISIBLE); mStatusText.setVisibility(View.INVISIBLE);
update(); update();
// make the background and arrows visible as soon the snake starts moving
mArrowsView.setVisibility(View.VISIBLE);
mBackgroundView.setVisibility(View.VISIBLE);
return; return;
} }
Resources res = getContext().getResources(); Resources res = getContext().getResources();
CharSequence str = ""; CharSequence str = "";
if (newMode == PAUSE) { if (newMode == PAUSE) {
mArrowsView.setVisibility(View.GONE);
mBackgroundView.setVisibility(View.GONE);
str = res.getText(R.string.mode_pause); str = res.getText(R.string.mode_pause);
} }
if (newMode == READY) { if (newMode == READY) {
mArrowsView.setVisibility(View.GONE);
mBackgroundView.setVisibility(View.GONE);
str = res.getText(R.string.mode_ready); str = res.getText(R.string.mode_ready);
} }
if (newMode == LOSE) { if (newMode == LOSE) {
str = res.getString(R.string.mode_lose_prefix) + mScore mArrowsView.setVisibility(View.GONE);
+ res.getString(R.string.mode_lose_suffix); mBackgroundView.setVisibility(View.GONE);
str = res.getString(R.string.mode_lose, mScore);
} }
mStatusText.setText(str); mStatusText.setText(str);
@@ -354,11 +364,16 @@ public class SnakeView extends TileView {
} }
/** /**
* Selects a random location within the garden that is not currently covered * @return the Game state as Running, Ready, Paused, Lose
* by the snake. Currently _could_ go into an infinite loop if the snake */
* currently fills the garden, but we'll leave discovery of this prize to a public int getGameState() {
* truly excellent snake-player. return mMode;
* }
/**
* Selects a random location within the garden that is not currently covered by the snake.
* Currently _could_ go into an infinite loop if the snake currently fills the garden, but we'll
* leave discovery of this prize to a truly excellent snake-player.
*/ */
private void addRandomApple() { private void addRandomApple() {
Coordinate newCoord = null; Coordinate newCoord = null;
@@ -388,10 +403,9 @@ public class SnakeView extends TileView {
mAppleList.add(newCoord); mAppleList.add(newCoord);
} }
/** /**
* Handles the basic update loop, checking to see if we are in the running * Handles the basic update loop, checking to see if we are in the running state, determining if
* state, determining if a move should be made, updating the snake's location. * a move should be made, updating the snake's location.
*/ */
public void update() { public void update() {
if (mMode == RUNNING) { if (mMode == RUNNING) {
@@ -411,7 +425,6 @@ public class SnakeView extends TileView {
/** /**
* Draws some walls. * Draws some walls.
*
*/ */
private void updateWalls() { private void updateWalls() {
for (int x = 0; x < mXTileCount; x++) { for (int x = 0; x < mXTileCount; x++) {
@@ -426,7 +439,6 @@ public class SnakeView extends TileView {
/** /**
* Draws some apples. * Draws some apples.
*
*/ */
private void updateApples() { private void updateApples() {
for (Coordinate c : mAppleList) { for (Coordinate c : mAppleList) {
@@ -435,16 +447,14 @@ public class SnakeView extends TileView {
} }
/** /**
* Figure out which way the snake is going, see if he's run into anything (the * Figure out which way the snake is going, see if he's run into anything (the walls, himself,
* walls, himself, or an apple). If he's not going to die, we then add to the * or an apple). If he's not going to die, we then add to the front and subtract from the rear
* front and subtract from the rear in order to simulate motion. If we want to * in order to simulate motion. If we want to grow him, we don't subtract from the rear.
* grow him, we don't subtract from the rear.
*
*/ */
private void updateSnake() { private void updateSnake() {
boolean growSnake = false; boolean growSnake = false;
// grab the snake by the head // Grab the snake by the head
Coordinate head = mSnakeTrail.get(0); Coordinate head = mSnakeTrail.get(0);
Coordinate newHead = new Coordinate(1, 1); Coordinate newHead = new Coordinate(1, 1);
@@ -523,10 +533,8 @@ public class SnakeView extends TileView {
} }
/** /**
* Simple class containing two integer values and a comparison function. * Simple class containing two integer values and a comparison function. There's probably
* There's probably something I should use instead, but this was quick and * something I should use instead, but this was quick and easy to build.
* easy to build.
*
*/ */
private class Coordinate { private class Coordinate {
public int x; public int x;

142
samples/Snake/src/com/example/android/snake/TileView.java Normal file → Executable file
View File

@@ -25,18 +25,16 @@ import android.graphics.drawable.Drawable;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.View; import android.view.View;
/** /**
* TileView: a View-variant designed for handling arrays of "icons" or other * TileView: a View-variant designed for handling arrays of "icons" or other drawables.
* drawables.
* *
*/ */
public class TileView extends View { public class TileView extends View {
/** /**
* Parameters controlling the size of the tiles and their range within view. * Parameters controlling the size of the tiles and their range within view. Width/Height are in
* Width/Height are in pixels, and Drawables will be scaled to fit to these * pixels, and Drawables will be scaled to fit to these dimensions. X/Y Tile Counts are the
* dimensions. X/Y Tile Counts are the number of tiles that will be drawn. * number of tiles that will be drawn.
*/ */
protected static int mTileSize; protected static int mTileSize;
@@ -47,81 +45,37 @@ public class TileView extends View {
private static int mXOffset; private static int mXOffset;
private static int mYOffset; private static int mYOffset;
private final Paint mPaint = new Paint();
/** /**
* A hash that maps integer handles specified by the subclasser to the * A hash that maps integer handles specified by the subclasser to the drawable that will be
* drawable that will be used for that reference * used for that reference
*/ */
private Bitmap[] mTileArray; private Bitmap[] mTileArray;
/** /**
* A two-dimensional array of integers in which the number represents the * A two-dimensional array of integers in which the number represents the index of the tile that
* index of the tile that should be drawn at that locations * should be drawn at that locations
*/ */
private int[][] mTileGrid; private int[][] mTileGrid;
private final Paint mPaint = new Paint();
public TileView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TileView);
mTileSize = a.getInt(R.styleable.TileView_tileSize, 12);
a.recycle();
}
public TileView(Context context, AttributeSet attrs) { public TileView(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TileView); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TileView);
mTileSize = a.getDimensionPixelSize(R.styleable.TileView_tileSize, 12);
mTileSize = a.getInt(R.styleable.TileView_tileSize, 12);
a.recycle(); a.recycle();
} }
public TileView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TileView);
mTileSize = a.getDimensionPixelSize(R.styleable.TileView_tileSize, 12);
/** a.recycle();
* Rests the internal array of Bitmaps used for drawing tiles, and
* sets the maximum index of tiles to be inserted
*
* @param tilecount
*/
public void resetTiles(int tilecount) {
mTileArray = new Bitmap[tilecount];
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
mXTileCount = (int) Math.floor(w / mTileSize);
mYTileCount = (int) Math.floor(h / mTileSize);
mXOffset = ((w - (mTileSize * mXTileCount)) / 2);
mYOffset = ((h - (mTileSize * mYTileCount)) / 2);
mTileGrid = new int[mXTileCount][mYTileCount];
clearTiles();
}
/**
* Function to set the specified Drawable as the tile for a particular
* integer key.
*
* @param key
* @param tile
*/
public void loadTile(int key, Drawable tile) {
Bitmap bitmap = Bitmap.createBitmap(mTileSize, mTileSize, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
tile.setBounds(0, 0, mTileSize, mTileSize);
tile.draw(canvas);
mTileArray[key] = bitmap;
} }
/** /**
@@ -137,9 +91,48 @@ public class TileView extends View {
} }
/** /**
* Used to indicate that a particular tile (set with loadTile and referenced * Function to set the specified Drawable as the tile for a particular integer key.
* by an integer) should be drawn at the given x/y coordinates during the *
* next invalidate/draw cycle. * @param key
* @param tile
*/
public void loadTile(int key, Drawable tile) {
Bitmap bitmap = Bitmap.createBitmap(mTileSize, mTileSize, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
tile.setBounds(0, 0, mTileSize, mTileSize);
tile.draw(canvas);
mTileArray[key] = bitmap;
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int x = 0; x < mXTileCount; x += 1) {
for (int y = 0; y < mYTileCount; y += 1) {
if (mTileGrid[x][y] > 0) {
canvas.drawBitmap(mTileArray[mTileGrid[x][y]], mXOffset + x * mTileSize,
mYOffset + y * mTileSize, mPaint);
}
}
}
}
/**
* Rests the internal array of Bitmaps used for drawing tiles, and sets the maximum index of
* tiles to be inserted
*
* @param tilecount
*/
public void resetTiles(int tilecount) {
mTileArray = new Bitmap[tilecount];
}
/**
* Used to indicate that a particular tile (set with loadTile and referenced by an integer)
* should be drawn at the given x/y coordinates during the next invalidate/draw cycle.
* *
* @param tileindex * @param tileindex
* @param x * @param x
@@ -149,21 +142,16 @@ public class TileView extends View {
mTileGrid[x][y] = tileindex; mTileGrid[x][y] = tileindex;
} }
@Override @Override
public void onDraw(Canvas canvas) { protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onDraw(canvas); mXTileCount = (int) Math.floor(w / mTileSize);
for (int x = 0; x < mXTileCount; x += 1) { mYTileCount = (int) Math.floor(h / mTileSize);
for (int y = 0; y < mYTileCount; y += 1) {
if (mTileGrid[x][y] > 0) {
canvas.drawBitmap(mTileArray[mTileGrid[x][y]],
mXOffset + x * mTileSize,
mYOffset + y * mTileSize,
mPaint);
}
}
}
mXOffset = ((w - (mTileSize * mXTileCount)) / 2);
mYOffset = ((h - (mTileSize * mYTileCount)) / 2);
mTileGrid = new int[mXTileCount][mYTileCount];
clearTiles();
} }
} }