1 /* 2 * Copyright (C) 2010 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 17 package com.example.android.tictactoe.library; 18 19 import java.util.Random; 20 21 import android.content.Context; 22 import android.content.res.Resources; 23 import android.graphics.Bitmap; 24 import android.graphics.BitmapFactory; 25 import android.graphics.Canvas; 26 import android.graphics.Paint; 27 import android.graphics.Rect; 28 import android.graphics.Bitmap.Config; 29 import android.graphics.BitmapFactory.Options; 30 import android.graphics.Paint.Style; 31 import android.graphics.drawable.Drawable; 32 import android.os.Bundle; 33 import android.os.Handler; 34 import android.os.Message; 35 import android.os.Parcelable; 36 import android.os.Handler.Callback; 37 import android.util.AttributeSet; 38 import android.view.MotionEvent; 39 import android.view.View; 40 41 //----------------------------------------------- 42 43 public class GameView extends View { 44 45 public static final long FPS_MS = 1000/2; 46 47 public enum State { 48 UNKNOWN(-3), 49 WIN(-2), 50 EMPTY(0), 51 PLAYER1(1), 52 PLAYER2(2); 53 54 private int mValue; 55 56 private State(int value) { 57 mValue = value; 58 } 59 60 public int getValue() { 61 return mValue; 62 } 63 64 public static State fromInt(int i) { 65 for (State s : values()) { 66 if (s.getValue() == i) { 67 return s; 68 } 69 } 70 return EMPTY; 71 } 72 } 73 74 private static final int MARGIN = 4; 75 private static final int MSG_BLINK = 1; 76 77 private final Handler mHandler = new Handler(new MyHandler()); 78 79 private final Rect mSrcRect = new Rect(); 80 private final Rect mDstRect = new Rect(); 81 82 private int mSxy; 83 private int mOffetX; 84 private int mOffetY; 85 private Paint mWinPaint; 86 private Paint mLinePaint; 87 private Paint mBmpPaint; 88 private Bitmap mBmpPlayer1; 89 private Bitmap mBmpPlayer2; 90 private Drawable mDrawableBg; 91 92 private ICellListener mCellListener; 93 94 /** Contains one of {@link State#EMPTY}, {@link State#PLAYER1} or {@link State#PLAYER2}. */ 95 private final State[] mData = new State[9]; 96 97 private int mSelectedCell = -1; 98 private State mSelectedValue = State.EMPTY; 99 private State mCurrentPlayer = State.UNKNOWN; 100 private State mWinner = State.EMPTY; 101 102 private int mWinCol = -1; 103 private int mWinRow = -1; 104 private int mWinDiag = -1; 105 106 private boolean mBlinkDisplayOff; 107 private final Rect mBlinkRect = new Rect(); 108 109 110 111 public interface ICellListener { 112 abstract void onCellSelected(); 113 } 114 115 public GameView(Context context, AttributeSet attrs) { 116 super(context, attrs); 117 requestFocus(); 118 119 mDrawableBg = getResources().getDrawable(R.drawable.lib_bg); 120 setBackgroundDrawable(mDrawableBg); 121 122 mBmpPlayer1 = getResBitmap(R.drawable.lib_cross); 123 mBmpPlayer2 = getResBitmap(R.drawable.lib_circle); 124 125 if (mBmpPlayer1 != null) { 126 mSrcRect.set(0, 0, mBmpPlayer1.getWidth() -1, mBmpPlayer1.getHeight() - 1); 127 } 128 129 mBmpPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 130 131 mLinePaint = new Paint(); 132 mLinePaint.setColor(0xFFFFFFFF); 133 mLinePaint.setStrokeWidth(5); 134 mLinePaint.setStyle(Style.STROKE); 135 136 mWinPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 137 mWinPaint.setColor(0xFFFF0000); 138 mWinPaint.setStrokeWidth(10); 139 mWinPaint.setStyle(Style.STROKE); 140 141 for (int i = 0; i < mData.length; i++) { 142 mData[i] = State.EMPTY; 143 } 144 145 if (isInEditMode()) { 146 // In edit mode (e.g. in the Eclipse ADT graphical layout editor) 147 // we'll use some random data to display the state. 148 Random rnd = new Random(); 149 for (int i = 0; i < mData.length; i++) { 150 mData[i] = State.fromInt(rnd.nextInt(3)); 151 } 152 } 153 } 154 155 public State[] getData() { 156 return mData; 157 } 158 159 public void setCell(int cellIndex, State value) { 160 mData[cellIndex] = value; 161 invalidate(); 162 } 163 164 public void setCellListener(ICellListener cellListener) { 165 mCellListener = cellListener; 166 } 167 168 public int getSelection() { 169 if (mSelectedValue == mCurrentPlayer) { 170 return mSelectedCell; 171 } 172 173 return -1; 174 } 175 176 public State getCurrentPlayer() { 177 return mCurrentPlayer; 178 } 179 180 public void setCurrentPlayer(State player) { 181 mCurrentPlayer = player; 182 mSelectedCell = -1; 183 } 184 185 public State getWinner() { 186 return mWinner; 187 } 188 189 public void setWinner(State winner) { 190 mWinner = winner; 191 } 192 193 /** Sets winning mark on specified column or row (0..2) or diagonal (0..1). */ 194 public void setFinished(int col, int row, int diagonal) { 195 mWinCol = col; 196 mWinRow = row; 197 mWinDiag = diagonal; 198 } 199 200 //----------------------------------------- 201 202 203 @Override 204 protected void onDraw(Canvas canvas) { 205 super.onDraw(canvas); 206 207 int sxy = mSxy; 208 int s3 = sxy * 3; 209 int x7 = mOffetX; 210 int y7 = mOffetY; 211 212 for (int i = 0, k = sxy; i < 2; i++, k += sxy) { 213 canvas.drawLine(x7 , y7 + k, x7 + s3 - 1, y7 + k , mLinePaint); 214 canvas.drawLine(x7 + k, y7 , x7 + k , y7 + s3 - 1, mLinePaint); 215 } 216 217 for (int j = 0, k = 0, y = y7; j < 3; j++, y += sxy) { 218 for (int i = 0, x = x7; i < 3; i++, k++, x += sxy) { 219 mDstRect.offsetTo(MARGIN+x, MARGIN+y); 220 221 State v; 222 if (mSelectedCell == k) { 223 if (mBlinkDisplayOff) { 224 continue; 225 } 226 v = mSelectedValue; 227 } else { 228 v = mData[k]; 229 } 230 231 switch(v) { 232 case PLAYER1: 233 if (mBmpPlayer1 != null) { 234 canvas.drawBitmap(mBmpPlayer1, mSrcRect, mDstRect, mBmpPaint); 235 } 236 break; 237 case PLAYER2: 238 if (mBmpPlayer2 != null) { 239 canvas.drawBitmap(mBmpPlayer2, mSrcRect, mDstRect, mBmpPaint); 240 } 241 break; 242 } 243 } 244 } 245 246 if (mWinRow >= 0) { 247 int y = y7 + mWinRow * sxy + sxy / 2; 248 canvas.drawLine(x7 + MARGIN, y, x7 + s3 - 1 - MARGIN, y, mWinPaint); 249 250 } else if (mWinCol >= 0) { 251 int x = x7 + mWinCol * sxy + sxy / 2; 252 canvas.drawLine(x, y7 + MARGIN, x, y7 + s3 - 1 - MARGIN, mWinPaint); 253 254 } else if (mWinDiag == 0) { 255 // diagonal 0 is from (0,0) to (2,2) 256 257 canvas.drawLine(x7 + MARGIN, y7 + MARGIN, 258 x7 + s3 - 1 - MARGIN, y7 + s3 - 1 - MARGIN, mWinPaint); 259 260 } else if (mWinDiag == 1) { 261 // diagonal 1 is from (0,2) to (2,0) 262 263 canvas.drawLine(x7 + MARGIN, y7 + s3 - 1 - MARGIN, 264 x7 + s3 - 1 - MARGIN, y7 + MARGIN, mWinPaint); 265 } 266 } 267 268 @Override 269 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 270 // Keep the view squared 271 int w = MeasureSpec.getSize(widthMeasureSpec); 272 int h = MeasureSpec.getSize(heightMeasureSpec); 273 int d = w == 0 ? h : h == 0 ? w : w < h ? w : h; 274 setMeasuredDimension(d, d); 275 } 276 277 @Override 278 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 279 super.onSizeChanged(w, h, oldw, oldh); 280 281 int sx = (w - 2 * MARGIN) / 3; 282 int sy = (h - 2 * MARGIN) / 3; 283 284 int size = sx < sy ? sx : sy; 285 286 mSxy = size; 287 mOffetX = (w - 3 * size) / 2; 288 mOffetY = (h - 3 * size) / 2; 289 290 mDstRect.set(MARGIN, MARGIN, size - MARGIN, size - MARGIN); 291 } 292 293 @Override 294 public boolean onTouchEvent(MotionEvent event) { 295 int action = event.getAction(); 296 297 if (action == MotionEvent.ACTION_DOWN) { 298 return true; 299 300 } else if (action == MotionEvent.ACTION_UP) { 301 int x = (int) event.getX(); 302 int y = (int) event.getY(); 303 304 int sxy = mSxy; 305 x = (x - MARGIN) / sxy; 306 y = (y - MARGIN) / sxy; 307 308 if (isEnabled() && x >= 0 && x < 3 && y >= 0 & y < 3) { 309 int cell = x + 3 * y; 310 311 State state = cell == mSelectedCell ? mSelectedValue : mData[cell]; 312 state = state == State.EMPTY ? mCurrentPlayer : State.EMPTY; 313 314 stopBlink(); 315 316 mSelectedCell = cell; 317 mSelectedValue = state; 318 mBlinkDisplayOff = false; 319 mBlinkRect.set(MARGIN + x * sxy, MARGIN + y * sxy, 320 MARGIN + (x + 1) * sxy, MARGIN + (y + 1) * sxy); 321 322 if (state != State.EMPTY) { 323 // Start the blinker 324 mHandler.sendEmptyMessageDelayed(MSG_BLINK, FPS_MS); 325 } 326 327 if (mCellListener != null) { 328 mCellListener.onCellSelected(); 329 } 330 } 331 332 return true; 333 } 334 335 return false; 336 } 337 338 public void stopBlink() { 339 boolean hadSelection = mSelectedCell != -1 && mSelectedValue != State.EMPTY; 340 mSelectedCell = -1; 341 mSelectedValue = State.EMPTY; 342 if (!mBlinkRect.isEmpty()) { 343 invalidate(mBlinkRect); 344 } 345 mBlinkDisplayOff = false; 346 mBlinkRect.setEmpty(); 347 mHandler.removeMessages(MSG_BLINK); 348 if (hadSelection && mCellListener != null) { 349 mCellListener.onCellSelected(); 350 } 351 } 352 353 @Override 354 protected Parcelable onSaveInstanceState() { 355 Bundle b = new Bundle(); 356 357 Parcelable s = super.onSaveInstanceState(); 358 b.putParcelable("gv_super_state", s); 359 360 b.putBoolean("gv_en", isEnabled()); 361 362 int[] data = new int[mData.length]; 363 for (int i = 0; i < data.length; i++) { 364 data[i] = mData[i].getValue(); 365 } 366 b.putIntArray("gv_data", data); 367 368 b.putInt("gv_sel_cell", mSelectedCell); 369 b.putInt("gv_sel_val", mSelectedValue.getValue()); 370 b.putInt("gv_curr_play", mCurrentPlayer.getValue()); 371 b.putInt("gv_winner", mWinner.getValue()); 372 373 b.putInt("gv_win_col", mWinCol); 374 b.putInt("gv_win_row", mWinRow); 375 b.putInt("gv_win_diag", mWinDiag); 376 377 b.putBoolean("gv_blink_off", mBlinkDisplayOff); 378 b.putParcelable("gv_blink_rect", mBlinkRect); 379 380 return b; 381 } 382 383 @Override 384 protected void onRestoreInstanceState(Parcelable state) { 385 386 if (!(state instanceof Bundle)) { 387 // Not supposed to happen. 388 super.onRestoreInstanceState(state); 389 return; 390 } 391 392 Bundle b = (Bundle) state; 393 Parcelable superState = b.getParcelable("gv_super_state"); 394 395 setEnabled(b.getBoolean("gv_en", true)); 396 397 int[] data = b.getIntArray("gv_data"); 398 if (data != null && data.length == mData.length) { 399 for (int i = 0; i < data.length; i++) { 400 mData[i] = State.fromInt(data[i]); 401 } 402 } 403 404 mSelectedCell = b.getInt("gv_sel_cell", -1); 405 mSelectedValue = State.fromInt(b.getInt("gv_sel_val", State.EMPTY.getValue())); 406 mCurrentPlayer = State.fromInt(b.getInt("gv_curr_play", State.EMPTY.getValue())); 407 mWinner = State.fromInt(b.getInt("gv_winner", State.EMPTY.getValue())); 408 409 mWinCol = b.getInt("gv_win_col", -1); 410 mWinRow = b.getInt("gv_win_row", -1); 411 mWinDiag = b.getInt("gv_win_diag", -1); 412 413 mBlinkDisplayOff = b.getBoolean("gv_blink_off", false); 414 Rect r = b.getParcelable("gv_blink_rect"); 415 if (r != null) { 416 mBlinkRect.set(r); 417 } 418 419 // let the blink handler decide if it should blink or not 420 mHandler.sendEmptyMessage(MSG_BLINK); 421 422 super.onRestoreInstanceState(superState); 423 } 424 425 //----- 426 427 private class MyHandler implements Callback { 428 public boolean handleMessage(Message msg) { 429 if (msg.what == MSG_BLINK) { 430 if (mSelectedCell >= 0 && mSelectedValue != State.EMPTY && mBlinkRect.top != 0) { 431 mBlinkDisplayOff = !mBlinkDisplayOff; 432 invalidate(mBlinkRect); 433 434 if (!mHandler.hasMessages(MSG_BLINK)) { 435 mHandler.sendEmptyMessageDelayed(MSG_BLINK, FPS_MS); 436 } 437 } 438 return true; 439 } 440 return false; 441 } 442 } 443 444 private Bitmap getResBitmap(int bmpResId) { 445 Options opts = new Options(); 446 opts.inDither = false; 447 448 Resources res = getResources(); 449 Bitmap bmp = BitmapFactory.decodeResource(res, bmpResId, opts); 450 451 if (bmp == null && isInEditMode()) { 452 // BitmapFactory.decodeResource doesn't work from the rendering 453 // library in Eclipse's Graphical Layout Editor. Use this workaround instead. 454 455 Drawable d = res.getDrawable(bmpResId); 456 int w = d.getIntrinsicWidth(); 457 int h = d.getIntrinsicHeight(); 458 bmp = Bitmap.createBitmap(w, h, Config.ARGB_8888); 459 Canvas c = new Canvas(bmp); 460 d.setBounds(0, 0, w - 1, h - 1); 461 d.draw(c); 462 } 463 464 return bmp; 465 } 466 } 467 468 469