井字棋实现方式有许多,最简单的方法是将9个imagView组成棋盘,然后通过一些逻辑设计进行游戏。本文采用的是自定义View的方式进行游戏设计,通过继承View进行棋盘,选中状态绘制,效果如下:
通过选取设置宽高中最小值作为控件宽高,并平均分为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);
}
}
为了区分选中操作的玩家,新建枚举类型:
//代表不同玩家(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)
其中calculateTouchItem方法功能为计算选中格子,代码如下:
//被选格子(玩家选择前先通过此数组判断是否可选,选择后将对应格子值设为非0)
private int[] locations = new int[9];
//每个格子对应字母编号,将通过字母组合判断是否获胜
private String[] items = new String[9];
//玩家所选格子对应字母集合
private List user1Selected = new ArrayList<>();
private List user2Selected = new ArrayList<>();
//已选格子信息集合(格子所选玩家,格子中心点坐标)
private List 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 successResult = new ArrayList<>();
//存储玩家获胜的字母组合
private List result = new ArrayList<>();
//回溯法 将所有情况进行判断
private void searchResult(List 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也完成了,上面代码中的变量在构造函数中,进行初始化,其中部分属性可设为自定义属性供用户设置,自定义属性及变量初始化如下:
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代码如下:
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