Hey all, this is quite an elaborate problem for me that i've been trying to figure out for a while now. Even explaining it is a little difficult for me but i'll give it a try.
I am using a gutted version of the Snake Android sample. Pretty much I am using the TileView class verbatim and am only trying to display a few tiles on the screen. Instead of the SnakeView class I am using a GameView class in which I only included the code I thought necessary to display a tile on the screen. Here is the class
public class GameView extends TileView {
/**
* Labels for the drawables that will be loaded into the TileView class
*/
private static final int RED_STAR = 1;
private static final int YELLOW_STAR = 2;
private static final int GREEN_STAR = 3;
/**
* mMoveDelay: number of milliseconds between animations.
*/
private long mMoveDelay = 600;
/**
* mLastMove: tracks the absolute time when the last animation happened.
*/
private long mLastMove;
/**
* Create a simple handler that we can use to cause animation to happen. We
* set ourselves as a target and we can use the sleep()
* function to cause an update/invalidate to occur at a later date.
*/
private RefreshHandler mRedrawHandler = new RefreshHandler();
class RefreshHandler extends Handler {
@Override
public void handleMessage(Message msg) {
GameView.this.update();
GameView.this.invalidate();
}
public void sleep(long delayMillis) {
this.removeMessages(0);
sendMessageDelayed(obtainMessage(0), delayMillis);
}
};
/**
* Constructs a GameView based on inflation from XML
*
* @param context
* @param attrs
*/
public GameView(Context context, AttributeSet attrs) {
super(context, attrs);
initGameView();
}
public GameView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initGameView();
}
private void initGameView() {
setFocusable(true);
Resources r = this.getContext().getResources();
resetTiles(4);
loadTile(RED_STAR, r.getDrawable(R.drawable.redstar));
loadTile(YELLOW_STAR, r.getDrawable(R.drawable.yellowstar));
loadTile(GREEN_STAR, r.getDrawable(R.drawable.greenstar));
}
public void initNewGame() {
// set the move delay. This tells the update method how often it should
// refresh the screen.
mMoveDelay = 600;
update();
}
public void update() {
long now = System.currentTimeMillis();
if (now - mLastMove > mMoveDelay) {
// clear any tiles on the screen
clearTiles();
updateWalls();
// draws the tiles storred in mCellularArray
//updateCellularArray();
mLastMove = now;
}
mRedrawHandler.sleep(mMoveDelay);
}
private void updateWalls() {
for (int x = 0; x < mXTileCount; x++) {
setTile(GREEN_STAR, x, 0);
setTile(GREEN_STAR, x, mYTileCount - 1);
}
for (int y = 1; y < mYTileCount - 1; y++) {
setTile(GREEN_STAR, 0, y);
setTile(GREEN_STAR, mXTileCount - 1, y);
}
}
private void updateCellularArray() {
setTile(YELLOW_STAR, 6, 7);
}
}
Now what is happening is that when the updateWalls() method is called, the tiles are placed within the View and comes up like this:
Now when I uncomment out the updateCellularArray()
method and comment out the updateWalls()
the program force closes and throws a NullPointerException. After some debugging I figured out that when the updateCellular array method is called, the mTileGrid array in the TileView class is not initialized, but it is when updateWalls()
is called. I am completely baffled as to why this is happening. It doesn't matter if i replace the for loop with a simple setTile(GREEN_STAR, 3, 3);
it still force closes.
Here is the TileView class I am using (again this is the same one in the Snake sample that comes with the Android SDK):
/**
* TileView: a View-variant designed for handling arrays of "icons" or other
* drawables.
*
*/
public class TileView extends View {
/**
* Parameters controlling the size of the tiles and their range within view.
* Width/Height are in pixels, and Drawables will be scaled to fit to these
* dimensions. X/Y Tile Counts are the number of tiles that will be drawn.
*/
protected static int mTileSize;
protected static int mXTileCount;
protected static int mYTileCount;
private static int mXOffset;
private static int mYOffset;
/**
* A hash that maps integer handles specified by the subclasser to the
* drawable that will be used for that reference
*/
private Bitmap[] mTileArray;
/**
* A two-dimensional array of integers in which the number represents the
* index of the tile that should be drawn at that locations
*/
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) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TileView);
mTileSize = a.getInt(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);
Log.d("Tomek", "TileGrid array dimensions are: " + mXTileCount + "," + mYTileCount);
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;
}
/**
* Resets all tiles to 0 (empty)
*
*/
public void clearTiles() {
for (int x = 0; x < mXTileCount; x++) {
for (int y = 0; y < mYTileCount; y++) {
setTile(0, x, y);
}
}
}
/**
* 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 x
* @param y
*/
public void setTile(int tileindex, int x, int y) {
//Log.d("Tomek", "Attempting to set tile: " + x + "," + y);
//Log.d("Tomek", "The current value at " + x + "," + y + " is " + mTileGrid[x][y]);
//Log.d("Tomek", "It will be changed to " + tileindex);
mTileGrid[x][y] = tileindex;
}
@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);
}
}
}
}
}
Sorry for posting all this code. Any help would be greatly appreciated.
Thanks much,
Tomek
EDIT: simplified GameView class
EDIT 2: Alright after modifying the updateCellularArray()
method to the following:
private void updateCellularArray() {
for (int x = 0; x < mXTileCount; x++) {
setTile(GREEN_STAR, 12, 12);
}
}
It finally placed the tile where I wanted it to...
I'm starting to wonder if it has something to do with the RefreshHandler. Possibly when the RedrawHandler's sleep method is called in the update method of the GameView class. I'm not quite sure how it works so I am going to try playing around with it and see what I can come up with.