To become familiar with mobile programming plattforms I've implemented a little game (a minesweeper clone) in J2ME. After having finished this I ported this game to Android.
While porting to Android I explored the following features:
- Rendering images via View.onDraw()
- Working with multiple Activities (Main Activity, Options Activity)
- Passing data from one Activity to another
- Handling Activity states (freeze, restore)
- Handling custom menues (onCreateOptionsMenu)
- etc.
Disclaimer: Please note, that my intention was to explore some features of the plattforms. There is still room for improvement as I've not implemented a timer, a highscore table etc..
Attached you can find the Eclipse (3.4) projects of both implementations: The following sources are extracts from the Android implementation:
TrapMain.java
package at.lacherstorfer.trap.android; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; public class TrapMain extends Activity { private TrapView trapView; private static int EDIT_OPTIONS = 1; /** Called when the activity is first created. */ @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.trap_main_layout); trapView = (TrapView) findViewById(R.id.trap); if (icicle != null) { // We are being restored Bundle map = icicle.getBundle("trapView"); if (map != null) { trapView.restoreState(map); } } trapView.doStart(); } @Override protected void onSaveInstanceState(Bundle outState) { // store game state outState.putBundle("trapView", trapView.saveState()); } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); MenuItem menuItem = menu.add(0, 0, 0, "Start"); menuItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { trapView.doStart(); return true; } }); menuItem = menu.add(0, 0, 0, "Options"); menuItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { Intent optionsIntent = new Intent(TrapMain.this, TrapOptions.class); Bundle extras = new Bundle(); extras.putInt("numberRows", trapView.getNumberRows()); extras.putInt("numberCols", trapView.getNumberCols()); extras.putInt("numberTraps", trapView.getNumberTraps()); optionsIntent.putExtras(extras); startActivityForResult(optionsIntent, EDIT_OPTIONS); return true; } }); return true; } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == RESULT_OK) { trapView.doStart(data.getIntExtra("numberRows", 8), data.getIntExtra("numberCols", 8), data.getIntExtra("numberTraps", 8)); } } }
TrapView.java
package at.lacherstorfer.trap.android; import android.app.AlertDialog; import android.app.AlertDialog.Builder; import android.content.Context; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.util.AttributeSet; import android.util.Log; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import at.lacherstorfer.trap.shared.Cell; import at.lacherstorfer.trap.shared.Field; public class TrapView extends View { private Field field; private int numberRows = 8; private int numberCols = 8; private int numberTraps = 8; private int cursorX; private int cursorY; private Drawable cellClosedImage; private Drawable cell0Image; private Drawable cell1Image; private Drawable cell2Image; private Drawable cell3Image; private Drawable cell4Image; private Drawable cellBombImage; private Drawable cellExplodedImage; private Drawable cellFlaggedImage; private Drawable cursorImage; public TrapView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); loadImages(context); } public TrapView(Context context, AttributeSet attrs) { super(context, attrs); loadImages(context); } public TrapView(Context context) { super(context); loadImages(context); } public void loadImages(Context context) { cellClosedImage = context.getResources().getDrawable( R.drawable.cellclosed); cell0Image = context.getResources().getDrawable(R.drawable.cell0); cell1Image = context.getResources().getDrawable(R.drawable.cell1); cell2Image = context.getResources().getDrawable(R.drawable.cell2); cell3Image = context.getResources().getDrawable(R.drawable.cell3); cell4Image = context.getResources().getDrawable(R.drawable.cell4); cellBombImage = context.getResources().getDrawable(R.drawable.cellbomb); cellExplodedImage = context.getResources().getDrawable( R.drawable.cellexploded); cellFlaggedImage = context.getResources().getDrawable( R.drawable.cellflagged); cursorImage = context.getResources().getDrawable(R.drawable.cursor); setFocusable(true); } public Bundle saveState() { Bundle map = new Bundle(); map.putInt("numberRows", Integer.valueOf(numberRows)); map.putInt("numberCols", Integer.valueOf(numberCols)); map.putInt("numberTraps", Integer.valueOf(numberTraps)); map.putInt("cursorX", Integer.valueOf(cursorX)); map.putInt("cursorY", Integer.valueOf(cursorY)); int[] fieldState = field.getState(); for (int i = 0; i < fieldState.length; i++) { map.putInt("field-" + i, fieldState[i]); } return map; } public void restoreState(Bundle icicle) { numberRows = icicle.getInt("numberRows"); numberCols = icicle.getInt("numberCols"); numberTraps = icicle.getInt("numberTraps"); cursorX = icicle.getInt("cursorX"); cursorY = icicle.getInt("cursorY"); int[] fieldState = new int[icicle.size() - 5]; for (int i = 0; i < fieldState.length; i++) { fieldState[i] = icicle.getInt("field-" + i); } field = new Field(fieldState); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { Log.i("TrapView", "pressed key=" + keyCode); boolean handled = false; if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { cursorX -= cursorX > 0 ? 1 : 0; handled = true; } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { cursorX += cursorX < numberCols - 1 ? 1 : 0; handled = true; } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { cursorY -= cursorY > 0 ? 1 : 0; handled = true; } else if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { cursorY += cursorY < numberRows - 1 ? 1 : 0; handled = true; } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) { fire(); handled = true; } else if (keyCode == KeyEvent.KEYCODE_SPACE) { flag(); handled = true; } if (handled) { postInvalidate(); } return handled; } @Override public boolean onTouchEvent(MotionEvent event) { boolean handled = false; if (event.getAction() == MotionEvent.ACTION_DOWN) { cursorX = (int) (event.getX() / 15); cursorX = cursorX > numberCols ? numberCols - 1 : cursorX; cursorY = (int) (event.getY() / 15); cursorY = cursorY > numberRows ? numberRows - 1 : cursorY; handled = true; } postInvalidate(); return handled; } private void fire() { if (field == null) return; // game not started yet int cellValue = field.fireAt(cursorY, cursorX); if (cellValue == Cell.TRAP) { field.reveal(); Builder b = new AlertDialog.Builder(this.getContext()); b.setTitle("Trap"); b.setIcon(0); b.setMessage("Game Over"); b.show(); } else if (field.isSolved()) { Builder b = new AlertDialog.Builder(this.getContext()); b.setTitle("Trap"); b.setIcon(0); b.setMessage("Congratulation, You Win!"); b.show(); } } private void flag() { if (field == null) return; // game not started yet field.flagAt(cursorY, cursorX); if (field.isSolved()) { Builder b = new AlertDialog.Builder(this.getContext()); b.setTitle("Trap"); b.setIcon(0); b.setMessage("Congratulation, You Win!"); b.show(); } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (field == null) return; // game not started yet // draw the field for (int row = 0; row < numberRows; row++) { for (int col = 0; col < numberCols; col++) { Cell cell = field.getCellAt(row, col); Drawable cellImage = getDrawable(cell); cellImage.setBounds(/* left */col * 15, /* top */row * 15, /* right */ col * 15 + 15, /* bottom */row * 15 + 15); cellImage.draw(canvas); } } // draw the cursor cursorImage.setBounds(cursorX * 15, cursorY * 15, cursorX * 15 + 15, cursorY * 15 + 15); cursorImage.draw(canvas); } public void doStart(int numberRows, int numberCols, int numberTraps) { this.numberRows = numberRows; this.numberCols = numberCols; this.numberTraps = numberTraps; field = new Field(numberRows, numberCols, numberTraps); postInvalidate(); } public void doStart() { if (field == null) { field = new Field(numberRows, numberCols, numberTraps); } else { field.restart(); } postInvalidate(); } private Drawable getDrawable(Cell cell) { if (cell.isOpen()) { switch (cell.getValue()) { case Cell.TRAP: return cellBombImage; case 0: return cell0Image; case 1: return cell1Image; case 2: return cell2Image; case 3: return cell3Image; case 4: return cell4Image; } } else if (cell.isClosed()) { return cellClosedImage; } else if (cell.isFlagged()) { return cellFlaggedImage; } else if (cell.isExploded()) { return cellExplodedImage; } else { throw new RuntimeException("Internal Error"); } return null; } public int getNumberRows() { return numberRows; } public int getNumberCols() { return numberCols; } public int getNumberTraps() { return numberTraps; } }
TrapOptions.java
package at.lacherstorfer.trap.android; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.widget.EditText; public class TrapOptions extends Activity { private EditText etNumberCols; private EditText etNumberRows; private EditText etNumberTraps; /** Called when the activity is first created. */ @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.trap_options_layout); etNumberRows = (EditText) findViewById(R.id.number_rows); etNumberCols = (EditText) findViewById(R.id.number_cols); etNumberTraps = (EditText) findViewById(R.id.number_traps); Bundle extras = getIntent().getExtras(); etNumberRows.setText(""+extras.getInt("numberRows")); etNumberCols.setText(""+extras.getInt("numberCols")); etNumberTraps.setText(""+extras.getInt("numberTraps")); } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); MenuItem menuItem = menu.add(0, 0, 0, "Ok"); menuItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { int numberRows = Integer.parseInt(etNumberRows.getText().toString()); int numberCols = Integer.parseInt(etNumberCols.getText().toString()); int numberTraps = Integer.parseInt(etNumberTraps.getText().toString()); Bundle b = new Bundle(); b.putInt("numberRows", numberRows); b.putInt("numberCols", numberCols); b.putInt("numberTraps", numberTraps); Intent resultIntent = new Intent(); resultIntent.putExtras(b); setResult(RESULT_OK, resultIntent); finish(); return true; } }); menuItem = menu.add(0, 0, 0, "Cancel"); menuItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { setResult(RESULT_CANCELED); finish(); return true; } }); return true; } }
trap_main_layout.xml
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas./apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent"> <at.lacherstorfer.trap.android.TrapView android:id="@+id/trap" android:layout_width="fill_parent" android:layout_height="fill_parent" /> </FrameLayout>
trap_options_layout.xml
<?xml version="1.0" encoding="utf-8"?> <TableLayout xmlns:android="http://schemas./apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TableRow> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Number Columns"/> <EditText android:id="@+id/number_cols" android:layout_width="fill_parent" android:layout_height="wrap_content" android:maxLength="2" /> </TableRow> <TableRow> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Number Rows"/> <EditText android:id="@+id/number_rows" android:layout_width="wrap_content" android:layout_height="wrap_content" android:maxLength="2" /> </TableRow> <TableRow> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Number Traps"/> <EditText android:id="@+id/number_traps" android:layout_width="fill_parent" android:layout_height="wrap_content" android:maxLength="2"/> </TableRow> </TableLayout>
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas./apk/res/android" package="at.lacherstorfer.trap.android"> <application android:icon="@drawable/icon" android:label="@string/app_name"> <activity android:name="TrapMain" android:label="@string/app_name"> <intent-filter> <action android:value="android.intent.action.MAIN" android:name="android.intent.action.MAIN"/> <category android:value="android.intent.category.LAUNCHER" android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <activity android:name="TrapOptions" android:label="@string/app_name"/> </application> </manifest>