井字棋實現方式有許多,最簡單的方法是將9個imagView組成棋盤,然後通過一些邏輯設計進行遊戲。本文采用的是自定義View的方式進行遊戲設計,通過繼承View進行棋盤,選中狀態繪製,效果如下:
1.棋盤繪製
通過選取設置寬高中最小值作爲控件寬高,並平均分爲3段作爲每小格的長度,在onDraw()方法中繪製棋盤,代碼如下:
//重寫onMeasure()設置寬高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int size = Math.min(MeasureSpec.getSize(widthMeasureSpec),MeasureSpec.getSize(heightMeasureSpec));
setMeasuredDimension(size,size);
}
//棋盤畫筆
private Paint mPaint;
@Override
protected void onDraw(Canvas canvas) {
//每小格長度
length = getWidth()/3;
//棋盤繪製
for(int i = 0;i < 4;i++){
canvas.drawLine(length*i,0,length*i,3*length,mPaint);
canvas.drawLine(0,length*i,length*3,length*i,mPaint);
}
}
2.選中狀態繪製
爲了區分選中操作的玩家,新建枚舉類型:
//代表不同玩家(NONE用來表示平局情況獲勝玩家)
public enum Player{
USER_ONE,USER_TWO,NONE
}
當玩家點擊屏幕時,我們需要知道他所選的格子是哪一個,通過實現 View.OnTouchListener 接口進行監聽,代碼如下(其中的判斷是避免滑動情況):
// down 事件 座標
private float lastX;
private float lastY;
@Override
public boolean onTouch(View v, MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_UP:
// UP 事件座標 與 Down事件座標 距離不能太遠
if(Math.abs(lastX-x)<length/2 && Math.abs(lastY-y)<length/2){
//通過UP事件座標計算選中格子
calculateTouchItem(x,y);
}
break;
}
return true;
}
其中calculateTouchItem方法功能爲計算選中格子,代碼如下:
//被選格子(玩家選擇前先通過此數組判斷是否可選,選擇後將對應格子值設爲非0)
private int[] locations = new int[9];
//每個格子對應字母編號,將通過字母組合判斷是否獲勝
private String[] items = new String[9];
//玩家所選格子對應字母集合
private List<String> user1Selected = new ArrayList<>();
private List<String> user2Selected = new ArrayList<>();
//已選格子信息集合(格子所選玩家,格子中心點座標)
private List<ItemInformation> mList = new ArrayList<>();
//當前回合玩家
private Player currentPlayer;
private void calculateTouchItem(float x, float y) {
//判斷所在行列
int j = (int)x/length;
int i = (int)y/length;
//判斷是否在棋盤內
if(i < 3 && j < 3){
//判斷是否可選
if(locations[i*3+j] == 0){
//創建選中格子信息並添加到選中格子list中
ItemInformation itemInformation = new ItemInformation(items[i*3+j],currentPlayer,i,j);
mList.add(itemInformation);
//將格子設爲已選狀態
locations[i*3+j] = 1;
//將選中格子字母編號添加到當前玩家選中格子list中
if(currentPlayer == Player.USER_ONE){
user1Selected.add(items[i*3+j]);
}else {
user2Selected.add(items[i*3+j]);
}
//重繪
invalidate();
}
}
}
其中ItemInformation保存選中格子相關信息,具體如下:
//存儲選中格子信息
private class ItemInformation{
private int i;
private int j;
//選中格子字母編號
private String location;
private Player player;
public ItemInformation(String location, Player player,int i,int j) {
this.location = location;
this.player = player;
this.i = i;
this.j = j;
}
public String getLocation() {
return location;
}
public Player getPlayer() {
return player;
}
public int getI() {
return i;
}
public int getJ() {
return j;
}
}
九個格子字母編號和數字編號如下:
A(0) | B(1) | C(2) |
D(3) | E(4) | F(5) |
G(6) | H(7) | I(8) |
計算好選中格子後調用invalidate()方法進行重繪,修改onDraw()方法,代碼如下:
//是否第一次加載
private boolean isFirst = true;
//玩家圖案對應顏色
private int userColorOne;
private int userColorTwo;
@Override
protected void onDraw(Canvas canvas) {
length = Math.min(getWidth(),getHeight())/3;
//棋盤繪製
for(int i = 0;i < 4;i++){
canvas.drawLine(length*i,0,length*i,3*length,mPaint);
canvas.drawLine(0,length*i,length*3,length*i,mPaint);
}
//選中格子繪製
if(!isFirst){
for(int i = 0;i < mList.size();i++){
switch (mList.get(i).getPlayer()){
case USER_ONE:
userPaint.setColor(userColorOne);
drawUserSelected(canvas,mList.get(i));
break;
case USER_TWO:
userPaint.setColor(userColorTwo);
drawUserSelected(canvas,mList.get(i));
break;
}
}
//查看遊戲狀態是否結束
checkStatus();
}
if(isFirst){
isFirst = false;
}
}
遍歷所有選中格子,通過對應ItemInformation信息知道所選玩家,設置對應顏色,再調用drawUserSelected()方法繪製,具體代碼如下:
//玩家選擇圖案畫筆
private Paint userPaint;
//繪製選中格子
private void drawUserSelected(Canvas canvas, ItemInformation itemInformation) {
//計算格子中心座標
int centerX = itemInformation.getJ() * length + length/2;
int centerY = itemInformation.getI() * length + length/2;
userPaint.setStrokeWidth(5f);
//玩家一 繪製 × 圖案
if(itemInformation.getPlayer() == Player.USER_ONE){
float delta = (float) Math.sqrt(0.08*length*length);
canvas.drawLine(centerX-delta,centerY-delta,centerX+delta,centerY+delta,userPaint);
canvas.drawLine(centerX+delta,centerY-delta,centerX-delta,centerY+delta,userPaint);
}else {
//玩家二 繪製 ○ 圖案
float radius = 0.4f * length;
canvas.drawCircle(centerX,centerY,radius,userPaint);
}
}
繪製後就調用checkStatus()方法進行判斷,是否有玩家獲勝,是否還有未選格子,如遊戲還能繼續則切換玩家。具體代碼如下:
//查看遊戲狀態
private void checkStatus(){
// 避免非用戶點擊情況下 onDraw()方法調用 造成 當前玩家的切換
if(mList.size() == 0 || mList.size()==lastCount) return;
lastCount = mList.size();
//查看是否有人獲勝
boolean isSuccess = checkIsSuccessful();
if(isSuccess){
if(mListener != null){
mListener.onSuccess(currentPlayer);
}
setOnTouchListener(null);
}else {
//判斷是否平局
if(mList.size() == 9){
if(mListener != null){
mListener.onSuccess(Player.NONE);
}
return;
}
//切換當前用戶
switch (currentPlayer){
case USER_TWO:
currentPlayer = Player.USER_ONE;
break;
case USER_ONE:
currentPlayer = Player.USER_TWO;
break;
}
}
Log.e("Chess:",currentPlayer+"");
}
其中checkIsSuccessful()方法判斷有人獲勝,具體代碼如下:
private boolean checkIsSuccessful() {
boolean isSuccess = false;
String tmp = "";
if(currentPlayer == Player.USER_ONE){
if(user1Selected.size() >= 3){
//將玩家所選格子字母編號排序
Collections.sort(user1Selected);
//回溯法判斷是否獲勝
searchResult(user1Selected,tmp,0);
isSuccess = result.size() > 0;
}
}else {
if(user2Selected.size() >= 3){
Collections.sort(user2Selected);
searchResult(user2Selected,tmp,0);
isSuccess = result.size() > 0;
}
}
return isSuccess;
}
通過調用searchResult()方法進行結果查找,具體代碼如下:
//獲勝的所有格子字母組合(按字典序排序)
private List<String> successResult = new ArrayList<>();
//存儲玩家獲勝的字母組合
private List<String> result = new ArrayList<>();
//回溯法 將所有情況進行判斷
private void searchResult(List<String> userSelected,String tmp,int index) {
if(tmp.length() == 3){
System.out.println(tmp);
if(successResult.contains(tmp)){
result.add(tmp);
}
return;
}
for(int i = index;i < userSelected.size();i++){
tmp += userSelected.get(i);
searchResult(userSelected,tmp,i+1);
tmp = tmp.substring(0,tmp.length()-1);
}
}
其中successResult存儲了所有獲勝情況下的字母組合(按字典序),在checkIsSuccessful()方法中先調用Collections.sort()將玩家所選格子字母編號進行排序,在通過回溯法將所有3個字母組合與successResult結果進行比較,如果存在即獲勝,將結果加入result中,在checkIsSuccessful()方法中可通過result長度可知是否獲勝。如果有人獲勝則回調遊戲結束接口,接口如下:
//遊戲結束監聽器
private OnSuccessListener mListener;
//遊戲結束回調接口
public interface OnSuccessListener{
public void onSuccess(Player player);
}
//設置遊戲結束回調接口
public void setOnSuccessListener(OnSuccessListener listener){
mListener = listener;
}
回調代碼在checkStatus()方法,如下:
//查看遊戲狀態
private void checkStatus(){
// 避免非用戶點擊情況下 onDraw()方法調用 造成 當前玩家的切換
if(mList.size() == 0 || mList.size()==lastCount) return;
lastCount = mList.size();
//查看是否有人獲勝
boolean isSuccess = checkIsSuccessful();
if(isSuccess){
if(mListener != null){
mListener.onSuccess(currentPlayer);
}
setOnTouchListener(null);
}else {
//判斷是否平局
if(mList.size() == 9){
if(mListener != null){
mListener.onSuccess(Player.NONE);
}
return;
}
//切換當前用戶
switch (currentPlayer){
case USER_TWO:
currentPlayer = Player.USER_ONE;
break;
case USER_ONE:
currentPlayer = Player.USER_TWO;
break;
}
}
Log.e("Chess:",currentPlayer+"");
}
到此我們的自定義view也完成了,上面代碼中的變量在構造函數中,進行初始化,其中部分屬性可設爲自定義屬性供用戶設置,自定義屬性及變量初始化如下:
<declare-styleable name="ChessView">
<attr name="lineColor" format="color"/>
<attr name="user_color_one" format="color"/>
<attr name="user_color_two" format="color"/>
</declare-styleable>
public ChessView(Context context) {
this(context,null);
}
public ChessView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public ChessView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context,attrs);
}
private void init(Context context,AttributeSet attributeSet) {
initData();
TypedArray typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.ChessView);
lineColor = typedArray.getColor(R.styleable.ChessView_lineColor,Color.BLACK);
userColorOne = typedArray.getColor(R.styleable.ChessView_user_color_one,Color.BLACK);
userColorTwo = typedArray.getColor(R.styleable.ChessView_user_color_two,Color.RED);
typedArray.recycle();
userPaint = new Paint();
userPaint.setStyle(Paint.Style.STROKE);
userPaint.setAntiAlias(true);
mPaint = new Paint();
mPaint.setColor(lineColor);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setAntiAlias(true);
currentPlayer = Player.USER_ONE;
}
private void initData(){
items[0] = "A";
items[1] = "B";
items[2] = "C";
items[3] = "D";
items[4] = "E";
items[5] = "F";
items[6] = "G";
items[7] = "H";
items[8] = "I";
successResult.add("ABC");
successResult.add("DEF");
successResult.add("GHI");
successResult.add("ADG");
successResult.add("BEH");
successResult.add("CFI");
successResult.add("AEI");
successResult.add("CEG");
}
我們可在佈局中使用此自定義view並實現遊戲結束接口,具體就不介紹了,代碼都有註釋,佈局代碼和activity代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ChessActivity">
<com.example.chess.ChessView
android:background="#ffffff"
android:id="@+id/chess_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/chess_button"
android:textSize="22sp"
android:textColor="#000000"
android:background="#03A9F4"
android:layout_marginTop="20dp"
app:layout_constraintTop_toBottomOf="@id/chess_view"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_width="250dp"
android:layout_height="70dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
public class ChessActivity extends AppCompatActivity implements View.OnClickListener {
private ChessView chessView;
private Button chessButton;
//遊戲結束信息提醒
private AlertDialog.Builder alertDialog;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_chess);
initDialog();
//判斷是否爲第一次進入遊戲 是的話需點擊開始遊戲,否則直接進行遊戲
Intent intent = getIntent();
chessButton = findViewById(R.id.chess_button);
chessView = findViewById(R.id.chess_view);
if(intent.getIntExtra("re",0) == 1){
chessButton.setVisibility(View.INVISIBLE);
//使棋盤接收點擊事件
chessView.setOnTouchListener();
}else {
chessButton.setText("開始遊戲");
chessButton.setOnClickListener(this);
}
chessView.setOnSuccessListener(new ChessView.OnSuccessListener() {
@Override
public void onSuccess(ChessView.Player player) {
//根據回調結果顯示結束信息
switch (player){
case NONE:
alertDialog.setMessage("平局!"); //設置提示信息
break;
case USER_ONE:
alertDialog.setMessage("×方獲勝!"); //設置提示信息
break;
case USER_TWO:
alertDialog.setMessage("○方獲勝!"); //設置提示信息
break;
}
alertDialog.show(); //顯示
}
});
}
//初始化Dialog
private void initDialog(){
alertDialog = new AlertDialog.Builder(this);
alertDialog.setTitle("遊戲信息"); //設置標題
alertDialog.setCancelable(false); //是否點擊屏幕可取消
//確定按鈕點擊事件
alertDialog.setPositiveButton("再來一局", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent intent = new Intent(ChessActivity.this,ChessActivity.class);
intent.putExtra("re",1);
startActivity(intent);
finish();
}
});
//取消按鈕點擊事件
alertDialog.setNegativeButton("返回", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
chessButton.setVisibility(View.VISIBLE);
chessButton.setText("再來一局");
chessButton.setOnClickListener(ChessActivity.this);
}
});
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.chess_button) {
if (chessButton.getText().toString().equals("開始遊戲")) {
Log.e("ChessActivity:","開始遊戲");
chessView.setOnTouchListener();
} else {
Intent intent = new Intent(ChessActivity.this, ChessActivity.class);
intent.putExtra("re", 1);
startActivity(intent);
finish();
}
}
}
}
代碼已上傳GitHub,地址 : https://github.com/YangRT/Chess