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 
State(int value)56         private State(int value) {
57             mValue = value;
58         }
59 
getValue()60         public int getValue() {
61             return mValue;
62         }
63 
fromInt(int i)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 {
onCellSelected()112         abstract void onCellSelected();
113     }
114 
GameView(Context context, AttributeSet attrs)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 
getData()155     public State[] getData() {
156         return mData;
157     }
158 
setCell(int cellIndex, State value)159     public void setCell(int cellIndex, State value) {
160         mData[cellIndex] = value;
161         invalidate();
162     }
163 
setCellListener(ICellListener cellListener)164     public void setCellListener(ICellListener cellListener) {
165         mCellListener = cellListener;
166     }
167 
getSelection()168     public int getSelection() {
169         if (mSelectedValue == mCurrentPlayer) {
170             return mSelectedCell;
171         }
172 
173         return -1;
174     }
175 
getCurrentPlayer()176     public State getCurrentPlayer() {
177         return mCurrentPlayer;
178     }
179 
setCurrentPlayer(State player)180     public void setCurrentPlayer(State player) {
181         mCurrentPlayer = player;
182         mSelectedCell = -1;
183     }
184 
getWinner()185     public State getWinner() {
186         return mWinner;
187     }
188 
setWinner(State winner)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). */
setFinished(int col, int row, int diagonal)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
onDraw(Canvas canvas)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
onMeasure(int widthMeasureSpec, int heightMeasureSpec)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