用Android Studio做一个超好玩的拼图游戏,附送超详细注释的源码

用Android Studio做一个超好玩的拼图游戏,附送超详细注释的源码在这篇博客里面 我们就来开发一款简单的拼图游戏 这款拼图游戏就和我们小时候玩的游戏是一样的 这里面的涉及到的算法不多 可以很容易学会 这次的拼图游戏项目是一个非常好的 Android 实现案例 涉及到很多常用的控件和知识点 希望大家拿到源码后 能对照着教程和注释好好学习掌握

一、项目概述

之前有不少粉丝私信我说,能不能用Android原生的语言开发一款在手机上运行的游戏呢?

说实话,使用java语言直接开发游戏这个需求有点难,因为一些比较复杂的游戏都是通过cocos2D或者Unity3D等游戏引擎开发出来的,然后再移植到Android手机当中,使用完整的游戏引擎开发的过程比较简单,而且界面比较流畅,观感和体验度都很好。

所以直接使用java开发的游戏并不多。当然,虽说不多但也有。简单些的比如:2048、拼图游戏、贪吃蛇、推箱子等,复杂点的比如:斗地主,这些都可以用java语言开发。因为这些游戏刷新界面次数比较少,是可以用java开发出来的。

所以在这篇博客里面,我们就来开发一款简单的拼图游戏,这款拼图游戏就和我们小时候玩的游戏是一样的,这里面的涉及到的算法不多,可以很容易学会,是作为入门Android的一个非常好的实例。

二、开发环境

在这里插入图片描述

三、需求分析

我们先来看下最终要实现的效果

  1. 拼图游戏布局绘制
  2. 拼图游戏时间计时
  3. 拼图游戏打乱显示
  4. 拼图游戏碎片位置切换
  5. 拼图游戏成功的条件
  6. 拼图游戏重新开始

我们来看下需要准备的图片素材

四、实现过程

1、拼图游戏布局绘制

我们首先来分析下游戏的layout布局

再来看下最终实现的效果图,先分析一下怎么绘制布局,实现一个项目的第一步是将布局按照自己期望的样子完成。

我们来绘制游戏的layout布局
从上至下的第一个布局是显示时间的TextView,我们将它的id设置为pt_tv_time,layout_width和layout_height都设置为wrap_content,就是适应内容大小,然后text文本内容设为“时间:0”,这个是方便测试写上文本的,因为边写代码可以边看旁边的效果变化。

然后layout_gravity设置为”center”,就是设置自己在父容器(顶层的LinearLayout)中居中,这里补充下知识点:

  • gravity是设置自身内部元素的对齐方式。比如一个TextView,则是设置内部文字的对齐方式。如果是ViewGroup组件如LinearLayout的话,则为设置它内部view组件的对齐方式。
  • layout_gravity是设置自身相当于父容器的对齐方式。比如,一个TextView设置layout_gravity属性,则表示这TextView相对于父容器的对齐方式。

再来改变下字体大小,设置textSize为20sp,sp是像素,补充下单位的知识点:

  • dp: device independent pixels(设备独立像素),不同设备有不同的显示效果,和设备硬件有关。
  • px: pixels(像素).,不同设备显示效果相同,这个用的比较多。
  • pt: point,是一个标准的长度单位,1pt=1/72英寸,用于印刷业,非常简单易用。
  • sp: scaled pixels(放大像素),主要用于字体显示best for textsize。

最后设置字体颜色为#FF0000,即红色。一般是通过colors.xml资源来引用,这里因为红色比较好表示就直接设置了。

TextView代码如下:

<TextView android:id="@+id/pt_tv_time" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="时间 : 0" android:layout_gravity="center" android:textSize="20sp" android:textColor="#FF0000"/> 

orientation选择的是水平方向,因为每一行是水平放置的,layout_gravity设置为”center”,表示居中,代码如下。

<LinearLayout android:id="@+id/pt_line1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" android:layout_gravity="center">  
     LinearLayout> 

设置第一张图片,选择的控件是ImageButton,顾名思义:图片按钮,正常按钮就规规矩矩的,而图片按钮就很好看,一张图片也可以进行点击,这里设置它的id=“@+id/pt_ib_00x00”,方便在MainActivity里面调用。

00×00不用我多说了吧,上面解释过了,将九宫格看成3X3的二维数组,那么行列下标就是0行0列,这里每行数和列数都用2位数字表示而已。

再设置个onClick方法,方法名为”onClick”,我们后面会在MainActivity里面进行编写点击事件。第一张图片的代码如下:

<ImageButton android:id="@+id/pt_ib_00x00" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/img_xiaoxiong_00x00" android:padding="0dp" android:onClick="onClick"/> 

依次类推,第二张和第三张图片,我只要改下id和src就可以了,所以直接放上第一个小LinearLayout的代码:

<LinearLayout android:id="@+id/pt_line1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" android:layout_gravity="center"> <ImageButton android:id="@+id/pt_ib_00x00" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/img_xiaoxiong_00x00" android:padding="0dp" android:onClick="onClick"/> <ImageButton android:id="@+id/pt_ib_00x01" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/img_xiaoxiong_00x01" android:padding="0dp" android:onClick="onClick"/> <ImageButton android:id="@+id/pt_ib_00x02" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/img_xiaoxiong_00x02" android:padding="0dp" android:onClick="onClick"/>  
     LinearLayout> 
<LinearLayout android:id="@+id/pt_line1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" android:layout_gravity="center"> <ImageButton android:id="@+id/pt_ib_00x00" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/img_xiaoxiong_00x00" android:padding="0dp" android:onClick="onClick"/> <ImageButton android:id="@+id/pt_ib_00x01" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/img_xiaoxiong_00x01" android:padding="0dp" android:onClick="onClick"/> <ImageButton android:id="@+id/pt_ib_00x02" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/img_xiaoxiong_00x02" android:padding="0dp" android:onClick="onClick"/>  
     LinearLayout> <LinearLayout android:id="@+id/pt_line2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" android:layout_gravity="center"> <ImageButton android:id="@+id/pt_ib_01x00" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/img_xiaoxiong_01x00" android:padding="0dp" android:onClick="onClick"/> <ImageButton android:id="@+id/pt_ib_01x01" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/img_xiaoxiong_01x01" android:padding="0dp" android:onClick="onClick"/> <ImageButton android:id="@+id/pt_ib_01x02" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/img_xiaoxiong_01x02" android:padding="0dp" android:onClick="onClick"/>  
      LinearLayout> <LinearLayout android:id="@+id/pt_line3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" android:layout_gravity="center"> <ImageButton android:id="@+id/pt_ib_02x00" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/img_xiaoxiong_02x00" android:padding="0dp" android:onClick="onClick"/> <ImageButton android:id="@+id/pt_ib_02x01" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/img_xiaoxiong_02x01" android:padding="0dp" android:onClick="onClick"/> <ImageButton android:id="@+id/pt_ib_02x02" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/img_xiaoxiong_02x02" android:padding="0dp" android:onClick="onClick" android:visibility="invisible"/>  
       LinearLayout> 

有一点需要注意的,不知道有没有同学发现——第三行的第三张图片,也就是右下角的那张图片,它有个属性,其他的图片都没有:visibility=“invisible”,这是干什么的呢?

这个比较简单了,主要设置了onClick=“restart”,这个后面会在MainActivity里面编写重新开始游戏的逻辑,还设置了android:layout_marginTop=“20dp”,这是设置此控件与上面控件边距相隔20dp,为了和九宫格保持一定间距,代码如下:

<Button android:id="@+id/pt_btn_restart" android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="restart" android:layout_gravity="center" android:text="重新开始" android:layout_marginTop="20dp"/> 
 <ImageView android:id="@+id/pt_iv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:src="@mipmap/yangtu" android:layout_marginTop="20dp"/> 
我们来编写下MainActivity的基本框架
可以先来看下什么都没有的MainActivity。里面只有onClick()和restart()两个新的方法,这是在上面布局中设置的方法,onClick是图片按钮的点击事件,restart是重新开始按钮的点击事件,这两个方法的具体实现逻辑会在下面讲到。
public class MainActivity extends AppCompatActivity { 
    @Override protected void onCreate(Bundle savedInstanceState) { 
    super.onCreate(savedInstanceState); // 设置要显示的视图 setContentView(R.layout.activity_main); } // 图片按钮的点击事件  public void onClick(View view) { 
    } /* 重新开始按钮的点击事件*/ public void restart(View view) { 
    } } 

这里我们要做的是把所有在布局中用到的控件定义好,然后初始化这些控件

先来定义九个图片按钮,命名方法也是00,01这样的横纵坐标,一个重启按钮和一个显示时间的文本框

// 定义九个图片按钮,命名方法也是00,01这样的横纵坐标 ImageButton ib00,ib01,ib02,ib10,ib11,ib12,ib20,ib21,ib22; // 一个重启按钮 Button restartBtn; // 一个显示时间的文本框 TextView timeTv; 

然后我们在onCreate中定义一个initView()方法,这个方法是用来初始化控件的

// 初始化layout控件的方法 initView(); 

然后创建该方法,在该方法里面初始化定义的控件,通过findViewById()进行绑定控件,将声明的变量和layout中对应的控件进行绑定,实现引用的效果,代码如下:

/* 初始化控件:绑定9个图片按钮,1个显示时间的文本框,1个重启按钮*/ private void initView() { 
    ib00 = findViewById(R.id.pt_ib_00x00); ib01 = findViewById(R.id.pt_ib_00x01); ib02 = findViewById(R.id.pt_ib_00x02); ib10 = findViewById(R.id.pt_ib_01x00); ib11 = findViewById(R.id.pt_ib_01x01); ib12 = findViewById(R.id.pt_ib_01x02); ib20 = findViewById(R.id.pt_ib_02x00); ib21 = findViewById(R.id.pt_ib_02x01); ib22 = findViewById(R.id.pt_ib_02x02); timeTv = findViewById(R.id.pt_tv_time); restartBtn = findViewById(R.id.pt_btn_restart); } 

初始化的完整代码,可以作为模板:

public class MainActivity extends AppCompatActivity { 
    // 定义九个图片按钮,命名方法也是00,01这样的横纵坐标 ImageButton ib00,ib01,ib02,ib10,ib11,ib12,ib20,ib21,ib22; // 一个重启按钮 Button restartBtn; // 一个显示时间的文本框 TextView timeTv; @Override protected void onCreate(Bundle savedInstanceState) { 
    super.onCreate(savedInstanceState); // 设置要显示的视图 setContentView(R.layout.activity_main); initView(); } private void initView() { 
    ib00 = findViewById(R.id.pt_ib_00x00); ib01 = findViewById(R.id.pt_ib_00x01); ib02 = findViewById(R.id.pt_ib_00x02); ib10 = findViewById(R.id.pt_ib_01x00); ib11 = findViewById(R.id.pt_ib_01x01); ib12 = findViewById(R.id.pt_ib_01x02); ib20 = findViewById(R.id.pt_ib_02x00); ib21 = findViewById(R.id.pt_ib_02x01); ib22 = findViewById(R.id.pt_ib_02x02); timeTv = findViewById(R.id.pt_tv_time); restartBtn = findViewById(R.id.pt_btn_restart); } // 图片按钮的点击事件  public void onClick(View view) { 
    } /* 重新开始按钮的点击事件*/ public void restart(View view) { 
    } } 

2、拼图游戏时间计时

完成基本工作后,我们思考下——如何实现时间的计时操作,这就相当于计时器的功能。这里我们可以用Handler消息机制来实现,补充下知识点:

  • Handler:作用就是发送与处理信息
  • Message:Handler接收与处理的消息对象

当我们的子线程想修改Activity中的UI组件时,我们可以新建一个Handler对象,通过这个对象向主线程发送信息;而我们发送的信息会先到主线程的MessageQueue进行等待,由Looper按先入先出顺序取出,再根据message对象的what属性分发给对应的Handler进行处理!

简单来说:Handler就是用来发送消息和处理消息的一种机制,上面这段话可能听起来有些懵,不过没关系,其实没有这么深奥,下面会让大家明白怎么使用它来实现计时的。

先定义个时间变量,初值为0,因为从0开始计时

// 定义计数时间的变量 int time = 0; 

然后定义发送和处理消息的对象handler,我们来重写handleMessage方法,在方法里面我们进行了if判断,如果这条消息的what值为1,那么时间time就+1,然后timeTv显示时间为time秒,然后继续向自己发送消息。

handler.sendEmptyMessageDelayed(1,1000)这句话的意思就是:延时1000毫秒后发送参数what为1的空信息,这样它自己就能循环接收自己发的消息,实现计时的功能了,就这么简单。

这是定义的handler的代码:

// 定义发送和处理消息的对象handler Handler handler = new Handler(){ 
    @Override // 重写handleMessage方法,根据msg中what的值判断是否执行后续操作 public void handleMessage(Message msg) { 
    if (msg.what==1) { 
    time++; timeTv.setText("时间 : "+time+" 秒"); // 指定延时1000毫秒后发送参数what为1的空信息 handler.sendEmptyMessageDelayed(1,1000); } } }; 

这是在onCreate方法里面定义的一条消息

handler.sendEmptyMessageDelayed(1,1000); 

这里我们只需要在restart方法里面先停止handler的消息发送,保证时间不会再继续+1了,然后将时间重新归0,显示当前时间,最后每隔1s发送参数what为1的消息msg,这样就实现了重新开始计时,代码如下:

/* 重新开始按钮的点击事件*/ public void restart(View view) { 
    // 停止handler的消息发送 handler.removeMessages(1); // 将时间重新归0,并且重新开始计时 time = 0; timeTv.setText("时间 : "+time+" 秒"); // 每隔1s发送参数what为1的消息msg handler.sendEmptyMessageDelayed(1,1000); } 

3、拼图游戏打乱显示

首先定义一个image数组,里面存放每张碎片(九宫格图片)的id,int型数组是可以存放图片的id的,但是不能存放图片,注意这个区别。

// 将每张碎片的id存放到数组中,便于进行统一的管理,int型数组存放的肯定是int型变量 private int[]image = { 
   R.mipmap.img_xiaoxiong_00x00,R.mipmap.img_xiaoxiong_00x01,R.mipmap.img_xiaoxiong_00x02, R.mipmap.img_xiaoxiong_01x00,R.mipmap.img_xiaoxiong_01x01,R.mipmap.img_xiaoxiong_01x02, R.mipmap.img_xiaoxiong_02x00,R.mipmap.img_xiaoxiong_02x01,R.mipmap.img_xiaoxiong_02x02}; 

再声明一个imageIndex数组,它来存放上面图片数组的下标,一共九张图片,所以下标为0-8,它存储的也就是0-8。我们为了让上面九张图片被打乱,所以,这里的下标等下会被打乱。

// 声明上面图片数组下标的数组,随机排列这个数组,九张图片,下标为0-8 private int[]imageIndex = new int[image.length]; 

先给下标数组每个元素赋值,下标是i,值就为i,就是imageIndex[i] = i。

// 给下标数组每个元素赋值,下标是i,值就为i for (int i = 0; i < imageIndex.length; i++) { 
    imageIndex[i] = i; } 

ps:我在这里卡了2h至少,因为这个小细节点没注意到,所以一定不能想当然,要查资料以求准确。

Math.random()的值域为[0,1),然后imageIndex.length-1就是8其实,*8那就是[0,8),再int取整最终值域为{0,1,2,3,4,5,6,7},因为int取整只会取整数位,不会四舍五入!

再用do-while循环实现了rand2的生成,之所以在do-while里面生成rand2,是为了判断二次生成的角标和第一次是否相同,不同则break立刻跳出循环,执行swap交换;若第二次生成的与第一次相同,则重新进入do-while循环生成rand2,这部分代码如下:

// 规定20次,随机选择两个角标对应的值进行交换 int rand1,rand2; for (int j = 0; j < 20; j++) { 
    // 随机生成第一个角标 // Math.random()产生的随机数为0~1之间的小数 此处说的0~1是包含左不包含右,即包含0不包含1 // Math.random()的值域为[0,1),然后*8就是[0,8),再int取整最终值域为{0,1,2,3,4,5,,6,7} rand1 = (int)(Math.random()*(imageIndex.length-1)); // 第二次随机生成的角标,不能和第一次随机生成的角标相同,如果相同,就不方便交换了 do { 
    rand2 = (int)(Math.random()*(imageIndex.length-1)); // 判断第一次和第二次生成的角标是否相同,不同则break立刻跳出循环,执行swap交换 if (rand1!=rand2) { 
    break; } // 若第二次生成的与第一次相同,则重新进入do-while循环生成rand2 }while (true); swap(rand1,rand2); } 

这里的swap方法很简单,就是交换两个数的值,只不过这里参数是数组的下标:

// 交换数组指定角标(0-7这八个自然数)上的数据 private void swap(int rand1, int rand2) { 
    int temp = imageIndex[rand1]; imageIndex[rand1] = imageIndex[rand2]; imageIndex[rand2] = temp; } 

这里有个整个游戏的一个核心点:我们打乱的拼图下标是{0,1,2,3,4,5,6,7}这八个,第九张拼图的下标是不参与打乱的,有同学问为什么?是因为第九张图片是不显示出来的,而且不会参与到拼图中,所以我们是将第九个图片按钮就设置成第九张图片,然后invisible。

最后我们将每个图片按钮设置图片,这时候 imageIndex[i]就是被打乱的下标,有可能是这样的顺序:{2,6,5,4,1,7,0,3,8},也有可能是这样的顺序{1,3,0,5,2,7,4,6,8}等等,不管怎么样, imageIndex[8]一直是8,上面解释过。代码如下:

// ib00是绑定的第一块图片按钮,设置图片资源, // imageIndex[i]就是被打乱的下标,然后image[x]就表示对应下标为x的图片的id ib00.setImageResource(image[imageIndex[0]]); ib01.setImageResource(image[imageIndex[1]]); ib02.setImageResource(image[imageIndex[2]]); ib10.setImageResource(image[imageIndex[3]]); ib11.setImageResource(image[imageIndex[4]]); ib12.setImageResource(image[imageIndex[5]]); ib20.setImageResource(image[imageIndex[6]]); ib21.setImageResource(image[imageIndex[7]]); ib22.setImageResource(image[imageIndex[8]]); 

综上,disruptRandom()的整体逻辑代码如下:

// 随机打乱数组当中元素,以不规则的形式进行图片显示 private void disruptRandom() { 
    // 给下标数组每个元素赋值,下标是i,值就为i for (int i = 0; i < imageIndex.length; i++) { 
    imageIndex[i] = i; } // 规定20次,随机选择两个角标对应的值进行交换 int rand1,rand2; for (int j = 0; j < 20; j++) { 
    // 随机生成第一个角标 // Math.random()产生的随机数为0~1之间的小数 此处说的0~1是包含左不包含右,即包含0不包含1 // Math.random()的值域为[0,1),然后*8就是[0,8),再int取整最终值域为{0,1,2,3,4,5,,6,7} rand1 = (int)(Math.random()*(imageIndex.length-1)); // 第二次随机生成的角标,不能和第一次随机生成的角标相同,如果相同,就不方便交换了 do { 
    rand2 = (int)(Math.random()*(imageIndex.length-1)); // 判断第一次和第二次生成的角标是否相同,不同则break立刻跳出循环,执行swap交换 if (rand1!=rand2) { 
    break; } // 若第二次生成的与第一次相同,则重新进入do-while循环生成rand2 }while (true); // 交换两个角标上对应的值 swap(rand1,rand2); } // 随机排列到指定的控件上 // ib00是绑定的第一块图片按钮,设置图片资源,imageIndex[i]就是被打乱的图片数组下标,然后image[x]就表示对应下标为x的图片的id ib00.setImageResource(image[imageIndex[0]]); ib01.setImageResource(image[imageIndex[1]]); ib02.setImageResource(image[imageIndex[2]]); ib10.setImageResource(image[imageIndex[3]]); ib11.setImageResource(image[imageIndex[4]]); ib12.setImageResource(image[imageIndex[5]]); ib20.setImageResource(image[imageIndex[6]]); ib21.setImageResource(image[imageIndex[7]]); ib22.setImageResource(image[imageIndex[8]]); } 

4、拼图游戏碎片位置切换

我们完成乱序后,这时候拼图碎片还不能移动,所以我们要设置点击事件,来移动拼图。

拼图移动的规则也要注意一下:只有和空白区域在同一行或者同一列相邻的拼图才能移动,只要知道了这个逻辑,实现起来就不难了。
|我们来编写九个图片按钮的onClick()方法 |
|–|–|
这里因为九个id不同的imagebutton点击事件的逻辑相同,所以我们使用switch 语句来编写,根据它们的id来执行移动,按照从左到右、从上到下的顺序进行了case设置。移动我们定义了move()函数,将它单独封装成了一个方法,下面就会讲到。点击事件的代码如下:






 public void onClick(View view) { 
    int id = view.getId(); // 九个按钮执行的点击事件的逻辑应该是相同的,如果有空格在周围,可以改变图片显示的位置,否则点击事件不响应 switch (id) { 
    case R.id.pt_ib_00x00: move(R.id.pt_ib_00x00,0); break; case R.id.pt_ib_00x01: move(R.id.pt_ib_00x01,1); break; case R.id.pt_ib_00x02: move(R.id.pt_ib_00x02,2); break; case R.id.pt_ib_01x00: move(R.id.pt_ib_01x00,3); break; case R.id.pt_ib_01x01: move(R.id.pt_ib_01x01,4); break; case R.id.pt_ib_01x02: move(R.id.pt_ib_01x02,5); break; case R.id.pt_ib_02x00: move(R.id.pt_ib_02x00,6); break; case R.id.pt_ib_02x01: move(R.id.pt_ib_02x01,7); break; case R.id.pt_ib_02x02: move(R.id.pt_ib_02x02,8); break; } } 

blankImgid就是空白区域的按钮id,我们这里直接固定了R.id.pt_ib_02x02,就是第九个图片按钮,它一直是空白区域!

// 每行的图片个数 private int imageX = 3; // 每列的图片个数 private int imageY = 3; // 图片的总数目 private int imgCount = imageX*imageY; // 空白区域的位置 private int blankSwap = imgCount-1; // 初始化空白区域的按钮id private int blankImgid = R.id.pt_ib_02x02; 
  1. 可以移动的条件有两个:
    1.在同一行,列数相减,绝对值为1,可移动
    2.在同一列,行数相减,绝对值为1,可以移动




  2. 两个参数: imagebuttonId是被选中的图片的id,site是该图片在9宫格的位置(0-8)
  3. 将移动后的图片按钮设为不可见的,即显示为空白区域
  4. 移动之前是不可见的,移动之后将图标按钮设置为可见
  5. 进行移动后将改变角标的过程记录到存储图片位置的数组当中
 /*表示移动指定位置的按钮的函数,将图片和空白区域进行交换*/ //imagebuttonId是被选中的图片的id,site是该图片在9宫格的位置(0-8) private void move(int imagebuttonId, int site) {     // 判断选中的图片在第几行,imageX为3,所以进行取整运算 int sitex = site / imageX; // 判断选中的图片在第几列,imageY为3,所以进行取模运算 int sitey = site % imageY; // 获取空白区域的坐标,blankx为行坐标,blanky为列坐标 int blankx = blankSwap / imageX; int blanky = blankSwap % imageY; // 可以移动的条件有两个 // 1.在同一行,列数相减,绝对值为1,可移动 2.在同一列,行数相减,绝对值为1,可以移动 int x = Math.abs(sitex-blankx); int y = Math.abs(sitey-blanky); if ((x==0&&y==1)||(y==0&&x==1)){     // 通过id,查找到这个可以移动的按钮 ImageButton clickButton = findViewById(imagebuttonId); // 将这个选中的图片设为不可见的,即显示为空白区域 clickButton.setVisibility(View.INVISIBLE); // 查找到空白区域的按钮 ImageButton blankButton = findViewById(blankImgid); // 将空白区域的按钮设置为图片,image[imageIndex[site]就是刚刚选中的图片,因为这在上面disruptRandom()设置过 blankButton.setImageResource(image[imageIndex[site]]); // 移动之前是不可见的,移动之后将控件设置为可见 blankButton.setVisibility(View.VISIBLE); // 将改变角标的过程记录到存储图片位置的数组当中 swap(site,blankSwap); // 新的空白区域位置更新等于传入的点击按钮的位置 blankSwap = site; // 新的空白图片id更新等于传入的点击按钮的id blankImgid = imagebuttonId; } } 

运行效果:

在这里插入图片描述
在这里插入图片描述

5、拼图游戏成功的条件

 boolean loop = true; //定义标志位loop for (int i = 0; i < imageIndex.length; i++) { 
    if (imageIndex[i]!=i) { 
    loop = false; break; } } 
 if (loop) { 
    // 拼图成功了 // 停止计时 handler.removeMessages(1); // 拼图成功后,禁止玩家继续移动按钮 ib00.setClickable(false); ib01.setClickable(false); ib02.setClickable(false); ib10.setClickable(false); ib11.setClickable(false); ib12.setClickable(false); ib20.setClickable(false); ib21.setClickable(false); ib22.setClickable(false); // 拼图成功后,第九块空白显示出图片,即下标为8的第九张图片 ib22.setImageResource(image[8]); ib22.setVisibility(View.VISIBLE); 
  1. 第一步:创建AlertDialog.Builder对象
  2. 第二步:设置对话框的内容:setMessage()方法来指定显示的内容
  3. 第三步:调用setPositive/Negative/NeutralButton()设置:确定,取消,中立按钮
  4. 第四歩:调用create()方法创建这个对象
  5. 第五歩:调用show()方法来显示我们的AlertDialog对话框

非常简单,按照上面的流程,我们来设置下对话框:

// 弹出提示用户成功的对话框,并且设置确实的按钮 // 第一步:创建AlertDialog.Builder对象 AlertDialog.Builder builder = new AlertDialog.Builder(this); // 调用setIcon()设置图标,setTitle()或setCustomTitle()设置标题 // 第二步:设置对话框的内容:setMessage()方法来指定显示的内容 builder.setMessage("恭喜,拼图成功!您用的时间为"+time+"秒") // 第三步:调用setPositive/Negative/NeutralButton()设置:确定,取消,中立按钮 .setPositiveButton("确认",null); // 第四歩:调用create()方法创建这个对象 AlertDialog dialog = builder.create(); // 第五歩:调用show()方法来显示我们的AlertDialog对话框 dialog.show(); 

6、拼图游戏重新开始

我们在上面实现了拼图游戏成功的条件和提示了,现在到了最后一步——如何让游戏重新开始?

另外,还要还原被点击的图片按钮变成初始化的模样, ImageButton clickBtn = findViewById(blankImgid)其实就是绑定最后一次被隐藏的那块拼图,然后clickBtn.setVisibility(View.VISIBLE)将它显示出来。ImageButton blankBtn = findViewById(R.id.pt_ib_02x02)就是绑定的第九块拼图,blankBtn.setVisibility(View.INVISIBLE)设置为不可见。最后blankImgid = R.id.pt_ib_02x02来初始化空白区域的按钮id。

restore()的代码如下:

// 状态还原函数,我们把它封装起来 private void restore() { 
    // 拼图游戏重新开始,允许移动碎片按钮 ib00.setClickable(true); ib01.setClickable(true); ib02.setClickable(true); ib10.setClickable(true); ib11.setClickable(true); ib12.setClickable(true); ib20.setClickable(true); ib21.setClickable(true); ib22.setClickable(true); // 还原被点击的图片按钮变成初始化的模样 ImageButton clickBtn = findViewById(blankImgid); clickBtn.setVisibility(View.VISIBLE); // 默认隐藏第九张图片 ImageButton blankBtn = findViewById(R.id.pt_ib_02x02); blankBtn.setVisibility(View.INVISIBLE); // 初始化空白区域的按钮id blankImgid = R.id.pt_ib_02x02; blankSwap = imgCount - 1; } 
  1. 将状态还原 将拼图重新打乱
  2. 停止handler的消息发送
  3. 将时间重新归0,并且重新开始计时
  4. 每隔1s发送参数what为1的消息msg
 /* 重新开始按钮的点击事件*/ public void restart(View view) { 
    // 将状态还原 restore(); // 将拼图重新打乱 disruptRandom(); // 停止handler的消息发送 handler.removeMessages(1); // 将时间重新归0,并且重新开始计时 time = 0; timeTv.setText("时间 : "+time+" 秒"); // 每隔1s发送参数what为1的消息msg handler.sendEmptyMessageDelayed(1,1000); } 

五、运行效果

Android Studio实现拼图游戏

六、项目总结

这次实现的拼图游戏,说它简单,其实它实现起来也并不是那么简单,还是会有很多比较难的逻辑点,需要思考才能写出来;说它难,其实也不算难,比起来我前面发的那些项目【天气预报】、【饮食搭配】来说逻辑实现还是比较简单的,毕竟它只有一个MainActivity和一个layout。所以,说一个项目的难易得看你选的参照物了。

这篇文章一共25000多个字,820行,我写这篇文章,不连上写代码时间,前后一共11个小时,前面构思和注释了4个小时,然后具体写了7个小时,中间只有喝水and上厕所。可以说我完全是按照开发这款拼图游戏的逻辑顺序来写下这篇教程。就是我们平时怎么开发Android项目,这篇博客就是怎么写的。

我之所以写的这么详细,也是因为现在网上缺少一个从头到尾讲实现过程的Android项目的教程,因为这实在太花时间了,我深有体会,极少有人一步一步地去把实现过程写出来,但是我还是决定写下这篇教程,为了让更多的人喜欢上Android,让更多的人对Android不再陌生,让小白们不再望而却步,让小白们有个很好的实现案例,这是我的想法。

当然,我也是正在学习Android的选手之一,才疏学浅,知识浅薄,文章中难免会有纰漏和错误,还希望大佬们批评指正。

七、项目源码

这次的拼图游戏项目是一个非常好的Android实现案例,涉及到很多常用的控件和知识点,希望大家拿到源码后,能对照着教程和注释好好学习掌握。

源码几乎每条语句我都加上了注释,这么良心的博主,点个三连支持下吧,源码就送你啦,祝大家身体健康,学习愉快~

关注我的公众号《萌新加油站》,后台回复:拼图游戏,即可获取源码。

面向阳光时,阴影在你背后。背向阳光时,阴影在你眼前。世界从未改变,改变的只是我们面对世界的方向!加油!你值得更好!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/226381.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月16日 下午11:02
下一篇 2026年3月16日 下午11:02


相关推荐

  • javascript几种写空格符的方法

    javascript几种写空格符的方法javascript 几种写空格符的方法

    2026年3月20日
    2
  • PyCharm使用教程(mac版教程)

    PyCharm使用教程(mac版教程)1 修改 PyCharm 的主题 字体等 2 添加插件 点击 Plusins 然后在搜索框中可以搜索需要的插件 然后点击 install 安装 3 开始创建项目设置路径 点击 create 创建 如下目录是我们刚才创建后 自动生成的目录 创建 Python 文件输入文件名称 如何允许当前项目 打开控制台 然后点击绿色的运行按钮

    2026年3月27日
    2
  • OpenClaw Windows 一键包:免环境配置极速版安装教程

    OpenClaw Windows 一键包:免环境配置极速版安装教程

    2026年3月12日
    3
  • datagrip 2021.10.1 激活码_在线激活[通俗易懂]

    (datagrip 2021.10.1 激活码)最近有小伙伴私信我,问我这边有没有免费的intellijIdea的激活码,然后我将全栈君台教程分享给他了。激活成功之后他一直表示感谢,哈哈~https://javaforall.net/100143.htmlIntelliJ2021最新激活注册码,破解教程可免费永久激活,亲测有效,上面是详细链接哦~1STL…

    2022年3月27日
    47
  • NAL详解

    NAL详解SODB 数据比特串 最原始的编码数据 RBSP 原始字节序列载荷 在 SODB 的后面填加了结尾比特 RBSPtrailing 一个 bit 1 若干比特 0 以便字节对齐 EBSP 扩展字节序列载荷在 RBSP 基础上填加了仿校验字节 0X03 它的原因是 在 NALU 加到 Annexb 上时 需要填加每组 NALU 之前的开始码 StartCodePre 如果该 NAL

    2026年3月18日
    1
  • JAVA设计模式初探之装饰者模式

    这个模式花费了挺长时间,开始有点难理解,其实就是定义:动态给一个对象添加一些额外的职责,就象在墙上刷油漆.使用Decorator模式相比用生成子类方式达到功能的扩充显得更为灵活。设计初衷:通常可以使用继承来实现功能的拓展,如果这些需要拓展的功能的种类很繁多,那么势必生成很多子类,增加系统的复杂性,同时,使用继承实现功能拓展,我们必须可预见这些拓展功能,这些功能是编译时就确定了,是静态的。…

    2022年3月11日
    38

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号