102
社区成员
发帖
与我相关
我的任务
分享参考相关教程,自己动手实践使用Android Studio开发一款滑动拼图游戏。
滑动拼图是一款策略游戏类游戏,把一幅图案分为相等的若干正方型小方块,取走其中一块或多制造出一块空位,利用多余的空格滑动其他小方块,打乱图案形成拼图。玩的方法是打乱和复位时都不能取下任何小方块,利用缺少的空位滑动图案中的小方块使其复原。

支持不同难度级别的选择,如3x3、4x4、5x5拼图模式,适应不同玩家的技能水平。
允许玩家选择图片,并将其分割成多个拼图块。
不仅支持内置系统图片,还可以处理用户上传的图片并将其转换为拼图。
通过滑动手势来移动拼图块,使游戏操作更加直观和流畅。
包含计时器和步数计数功能,帮助玩家了解自己解决拼图的效率。
提供背景音乐及操作音效,增强游戏的沉浸感。玩家可以通过界面上的按钮控制音效和音乐的开关。
玩家可以通过长按特定按钮来预览原图,帮助解决更复杂的拼图。
提供游戏操作指南和帮助信息,帮助新玩家快速上手。
玩家可以随时重启游戏。
采用RelativeLayout布局方式,允许子视图彼此之间以及相对于父容器进行布局,可以更精确地控制视图的位置和大小。
<?xml version="1.0" encoding="utf-8"?>
<!--the main layout-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background"
android:fitsSystemWindows="true">
<!--layout_centerHorizontal="true 将控件置于水平方向的中心位置 -->
<!--layout_below="@+id/splash_title"位于此id所属控件之下 -->
<TextView
android:id="@+id/splash_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/main_title"
android:textSize="51dp"
android:textColor="@color/darkorange"
android:layout_marginTop="100dp"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"/>
<TextView
android:id="@+id/t2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/splash_title"
android:layout_centerHorizontal="true"
android:text="@string/t2name"
android:textColor="@color/colorAccent"
android:textSize="23dp"
/>
<!-- layout_marginBottom 下偏移的值(距下方控件多少)-->
<LinearLayout
android:id="@+id/button_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_alignParentBottom="true"
android:layout_marginStart="40dp"
android:layout_marginEnd="40dp"
android:layout_marginBottom="80dp"
>
<Button
android:id="@+id/button1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/btn_judge"
android:onClick="onClick"
android:text="开始游戏"
android:textColor="@color/white"
android:textSize="20sp" />
<Button
android:id="@+id/button4"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="休闲模式"
android:layout_marginTop="20dp"
android:textColor="@color/white"
android:textSize="20sp"
android:background="@drawable/btn_judge"
android:onClick="onClick"
/>
<Button
android:id="@+id/button2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="帮助"
android:layout_marginTop="20dp"
android:textColor="@color/white"
android:textSize="20sp"
android:background="@drawable/btn_judge"
android:onClick="onClick"
/>
<Button
android:id="@+id/button3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="退出"
android:layout_marginTop="20dp"
android:textColor="@color/white"
android:textSize="20sp"
android:background="@drawable/btn_judge"
android:onClick="onClick"
/>
</LinearLayout>
<TextView
android:id="@+id/version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/version"
android:textSize="16dp"
android:textColor="@color/mediumturquoise"
android:layout_centerHorizontal="true"
android:layout_alignParentBottom="true"
android:layout_marginBottom="16dp"
/>
</RelativeLayout>
效果如下:

onCreate通过setContentView加载布局资源,初始化界面元素和设置字体样式。此外,启动了一个HandlerThread和一个匿名线程,用于处理耗时操作并通过消息机制更新UI,以避免阻塞主线程。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_01);// 加载xml布局资源
initView();// 初始化按钮和布局
setTextTypeface();// initialize the text display(字型)
HandlerThread handlerThread = new HandlerThread("HandlerThread");
handlerThread.start();// 开启子线程
new Thread() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
} finally {
Message message = new Message();
message.what = 1;
mHandler.sendMessage(message);
}
// 通过sendMessage()发送
}
}.start();
}
定义一个Handler实例mHandler,用于处理来自不同线程的消息,并在Android的主线程(UI线程)上执行UI操作。Handler的handleMessage方法根据消息的类型(通过msg.what标识)执行不同的操作:
消息类型为1:执行渐变动画,使布局组件(group)的透明度从0变到1。通过使用AlphaAnimation类实现,动画持续时间设置为1秒。动画监听器AnimationListener被设置来监听动画的开始、结束和重复事件。在动画结束时,调用setButtonEnabled(true)方法,这通常用于启用一些初始时被禁用的按钮,确保用户在动画完成后才能与这些按钮互动。
消息类型为2:跳转到TakePicturesPage页面进行图片选择。
这样使用Handler可以在不同的线程中安排一些任务或命令,然后由主线程来安全地更新UI。
@SuppressLint("HandlerLeak")
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 1:
AlphaAnimation animation = new AlphaAnimation(0, 1);// 布局持续动画的渐变效果,开始透明度为0,最终透明度为1
animation.setAnimationListener(new Animation.AnimationListener() {// 监听动画运行过程
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
setButtonEnabled(true);
}
@Override
public void onAnimationRepeat(Animation animation) {
}
// 这三个方法分别是Animation开始的时候调用,完成的时候调用,重复的时候调用
});
animation.setDuration(1000);// 动画持续效果为1s
group.startAnimation(animation);// 启动动画
group.setAlpha(1);
break;
case 2:
Intent intent2 = new Intent(WelcomePage.this, TakePicturesPage.class);
// intent2.putExtra("modelFlag", modelFlag);
startActivity(intent2);
break;
}
}
};
定义一个onClick方法,处理用户点击事件。该方法根据被点击的视图(通过View v的getId()方法识别)执行不同的操作:
点击button1:弹出一个对话框,让用户选择游戏难度(初级、中级、高级)。用户的选择会影响PuzzlePage.GAME_TYPE的值,这个值决定了拼图游戏的难度级别。选择难度后,再弹出一个对话框让用户选择是从图库中选择图片还是使用自带图片。根据用户的选择,要么调用selectPic()方法跳转到系统相册选择图片,要么通过Handler发送消息处理自带图片的逻辑。
点击button2:弹出一个帮助对话框,解释游戏的基本操作和规则,如如何开始游戏、选择图片、移动图片等。
点击button3:弹出一个确认对话框询问用户是否确定要退出游戏。如果用户确认,应用将模拟按下Home键,返回手机的主屏幕,并结束当前活动。
点击button4:设置游戏为休闲模式,使用较简单的图片和较低的难度(3x3)。
// 按钮触发函数
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public void onClick(View v) {
if (v.getId() == R.id.button1) {
// 弹出对话框,需要用Builder方法创建
final AlertDialog.Builder builder = new AlertDialog.Builder(WelcomePage.this);
builder.setTitle("难度选择:");
builder.setSingleChoiceItems(new String[] { "初级", "中级", "高级" }, 0,
(dialog, which) -> {
switch (which) {
case 0:
PuzzlePage.GAME_TYPE = 3;
break;
case 1:
PuzzlePage.GAME_TYPE = 4;
break;
case 2:
PuzzlePage.GAME_TYPE = 5;
break;
}
});
builder.setPositiveButton("确定",
(dialog, which) -> new AlertDialog.Builder(WelcomePage.this).setTitle("选择图片").setItems(
new String[] { "从图库中选择", "自带图片" }, (dialog1, which1) -> {
switch (which1) {
/**
* 从图库获取图片,通配符*遍历图片
*/
case 0:
selectPic();// 用于跳转系统相册的函数
break;
/**
* 自带图片,handler机制
*/
case 1:
mHandler.sendEmptyMessage(2);
break;
}
}).show());
builder.setNegativeButton("取消", (dialog, which) -> {
});
builder.show();
} else if (v.getId() == R.id.button2) {
AlertDialog.Builder builder1 = new AlertDialog.Builder(WelcomePage.this);
builder1.setMessage("点击开始游戏可以选择难度,可以选择本地图片或者游戏自带图片,进入已打乱" + "\n" +
"的游戏页面,选择一片要移动的图片,即可再拖向相邻的空白区域最下方有静音" + "\n" +
"按钮,长按叹号即可显示完整图片,也可以按第三个按钮重新选择难度,点击" + "\n" +
"第四个按钮可以重置初始位置");
builder1.setTitle("游戏帮助");
builder1.create().show();
} else if (v.getId() == R.id.button3) {
new AlertDialog.Builder(this).setMessage("确定要退出游戏吗?")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
Intent MyIntent = new Intent(Intent.ACTION_MAIN);
MyIntent.addCategory(Intent.CATEGORY_HOME);
startActivity(MyIntent);
finish();
}
}).show();
}
// 休闲模式采用自带简单图片,3x3切割
else if (v.getId() == R.id.button4) {
PuzzlePage.GAME_TYPE = 3;
PuzzlePage.flag = false;
// modelFlag = true;
mHandler.sendEmptyMessage(2);
}
}
游戏里可以打开本地相册选择图片来自定义生成拼图。所以这里添加了选择图片的逻辑。
selectPic() 函数负责启动一个活动以供用户从设备的相册中选择图片。根据Android版本的不同,该函数使用不同的Intent动作:对于Android KitKat (API 19) 以下版本,使用 Intent.ACTION_GET_CONTENT 来让用户选择图片;对于Android KitKat及以上版本,则使用 Intent.ACTION_PICK 并指定外部存储的媒体内容URI。
askPermissions() 函数用于请求运行时权限,这在Android 6.0 (API 23) 及以上版本中尤为重要,因为用户可以在应用运行时授予或拒绝权限。该函数会检查并请求读写联系人和外部存储的权限。如果应用没有这些权限,它会通过 requestPermissions() 方法向用户请求必要的权限。
/**
* 打开本地相册选择图片
*/
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
@SuppressLint("IntentReset")
private void selectPic() {
Intent intent;
if (Build.VERSION.SDK_INT < 19) {
intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("image/*");
} else {
intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
}
startActivityForResult(intent, 0);
}
// 因为读取相册权限缘故添加的函数
private void askPermissions() {// 动态申请权限!
if (Build.VERSION.SDK_INT >= 23) {
int REQUEST_CODE_CONTACT = 101;
String[] permissions = { Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS, // 联系人的权限
Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE };// 读写SD卡权限
// 验证是否许可权限
for (String str : permissions) {
if (this.checkSelfPermission(str) != PackageManager.PERMISSION_GRANTED) {
// 申请权限
this.requestPermissions(permissions, REQUEST_CODE_CONTACT);
}
}
}
}
拿到图片后,将图片传给游戏场景。
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK) {
if (requestCode == 0 && data != null) {
Uri selectedImageUri = data.getData();
if (selectedImageUri != null) {
Log.d("data", "URI: " + selectedImageUri.toString());
Intent intent = new Intent(WelcomePage.this, PuzzlePage.class);
intent.putExtra("imageUri", selectedImageUri.toString()); // 传递URI的字符串表示
startActivity(intent);
}
}
}
}
当用户按下返回键(KEYCODE_BACK)时,会弹出一个确认对话框询问用户是否确定要退出游戏。如果用户点击对话框中的"确定"按钮,应用将模拟返回主屏幕的操作,通过发送一个指向主屏幕的Intent (Intent.ACTION_MAIN 并设置类别为 CATEGORY_HOME),并调用finish()方法来结束当前活动。
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
// TODO Auto-generated method stub
// 弹出确定退出对话框
if (keyCode == KeyEvent.KEYCODE_BACK) {
// setPositiveButton表示设置弹框后的确定按钮
new AlertDialog.Builder(this).setMessage("确定要退出游戏吗?")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
Intent MyIntent = new Intent(Intent.ACTION_MAIN);
MyIntent.addCategory(Intent.CATEGORY_HOME);// 使Intent指向Home界面
startActivity(MyIntent);
finish();
}
}).show();
}
return true;
}
专门写了一个工具类SizeHelper用于处理与设备屏幕尺寸和密度相关的转换问题,以确保应用在不同设备上的用户界面元素能够保持视觉一致性。
prepare(Context c)初始化上下文,确保上下文只被设置一次,除非传入新的上下文。
fromPx(int px)和fromPxWidth(int px)分别根据设备的密度和屏幕宽度将设计时使用的像素值转换为当前设备上的像素值。
fromDp(int dp)将设计时使用的dp(密度无关像素)转换为当前设备的像素值,这个过程包括将dp转换为px,然后再进行密度转换。
这些方法可以在不同分辨率和屏幕尺寸的设备上进行像素转换,从而使得应用的用户界面能够在各种设备上展示出预期的布局和尺寸。
package com.example.puzzlegame.Utils;
import android.content.Context;
public class SizeHelper {
private static float designedDensity = 1.5f;
private static int designedScreenWidth = 540;
private static Context context = null;
protected static SizeHelper helper;
private SizeHelper() {
}
public static void prepare(Context c) {
if(context == null || context != c.getApplicationContext()) {
context = c;
}
}
/**
* 根据density转换设计的px到目标机器,返回px大小
* @return 像素大小
*/
public static int fromPx(int px) {
return Util.designToDevice(context, designedDensity, px);
}
/**
* 根据屏幕宽度转换设计的px到目标机器,返回px大小
* @return 像素大小
*/
public static int fromPxWidth(int px) {
return Util.designToDevice(context, designedScreenWidth, px);
}
/**
* 根据density转换设计的dp到目标机器,返回px大小
* @return 像素大小
*/
public static int fromDp(int dp) {
int px = Util.dipToPx(context, dp);
return Util.designToDevice(context, designedDensity, px);
}
}
因为TakePicturePage是列出一系列图片供用户选择,所以该页面采用了LinearLayout布局,可以比较方便地以垂直或水平的顺序排列子视图。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:background="@drawable/bg2"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/listview"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--尽可能填充布局单元内空间,等同于match_parent -->
</ListView>
</LinearLayout>
效果如下:

这个页面是用户不选择自定义图片,而选择获取app自带图片时进入的页面。
把预存在res/drawable目录下的图片拿出来。
listview = (ListView) findViewById(R.id.listview);// 后面listview是布局里面控件ListView的id
final String pictures[] = new String[] { "图片1", "图片2", "图片3", "图片4",
"图片5", "图片6", "图片7", "图片8",
"图片9", "图片10", "图片11", "图片12" };
final int images[] = new int[] {
R.drawable.image1, R.drawable.image2, R.drawable.image3, R.drawable.image4,
R.drawable.image5, R.drawable.image6, R.drawable.image7, R.drawable.image8,
R.drawable.image9, R.drawable.image10, R.drawable.image11, R.drawable.image12
}
// 创建list集合,存储图片
List<Map<String, Object>> data = new ArrayList<Map<String, Object>>();
for (int i = 0; i < pictures.length; i++) {
Map<String, Object> map = new HashMap<String, Object>();// 每次循环的时候都实例化一个新的map对象,这样list在执行add方法的时候,每次都是存的不一样的map对象。
// 需要创建不同的map对象的时候,需要在循环里面进行map的创建,在外面创建map的时候会造成多个对象都相同
// put方法存储键值对 <String类型,任意类型 >
map.put("picture", pictures[i]);// 存图片对应序号
map.put("icon", images[i]);// 存入image1这些图片
data.add(map);// 再把map对象都添加到List集合中
}
ListView所展示数据的格式则是有一定的要求的。所以要建立一个数据适配器建立了数据源与ListView之间的适配关系,将数据源转换为ListView能够显示的数据格式,从而将数据的来源与数据的显示进行解耦,降低程序的耦合性。
adapter1 = new SimpleAdapter(TakePicturesPage.this, data, R.layout.listxml, new String[] { "icon", "picture" },
new int[] { R.id.image1, R.id.textview1 });
listview.setAdapter(adapter1); // setAdapter() 设置数据适配器
当用户点击列表中的某一项时,首先通过点击的视图获取到对应的ImageView控件,并从中提取出图片的Bitmap。接着,这个Bitmap被转换成一个Uri,用于在应用的下一个页面(PuzzlePage)中显示。如果转换成功,应用将创建一个Intent,将图片的Uri作为字符串传递给PuzzlePage,并启动这个活动。如果转换失败,则显示一个提示错误的Toast。
listview.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
ImageView image = (ImageView) view.findViewById(R.id.image1); // 绑定对应的ImageView控件
// 获取ImageView中的Bitmap
Bitmap bitmap = ((BitmapDrawable) image.getDrawable()).getBitmap();
// 将Bitmap转换为Uri
Uri imageUri = bitmap2uri(TakePicturesPage.this, bitmap);
if (imageUri != null) {
// 创建Intent并将Uri的字符串形式放入,然后启动PuzzlePage
Intent intent = new Intent(TakePicturesPage.this, PuzzlePage.class);
intent.putExtra("imageUri", imageUri.toString()); // 使用Uri的字符串形式
startActivity(intent);
finish(); // 结束当前Activity
} else {
// 如果Uri转换失败,显示错误信息
Toast.makeText(TakePicturesPage.this, "无法加载图片", Toast.LENGTH_SHORT).show();
}
}
});
在puzzlepage的接受图片格式设置为uri会比较方便操作,所以需要在这里提取图片Bitmap后,将其转为uri。
首先使用当前时间戳创建一个唯一的文件名,防止覆盖已存在的图片文件,并将其保存在应用的缓存目录中。这个目录通过调用Context的getCacheDir()方法获取,确保了文件存储在一个安全的、应用专有的空间内。接着,使用FileOutputStream将Bitmap以JPEG格式压缩并写入到前面创建的文件中,压缩率设置为80以平衡图片质量和文件大小。如果文件成功写入,方法将返回一个指向该文件的Uri。
public static Uri bitmap2uri(Context c, Bitmap b) {
// 用时间戳作为路径可以防止覆盖旧图
File path = new File(c.getCacheDir() + File.separator + System.currentTimeMillis() + ".jpg");
try {
OutputStream os = new FileOutputStream(path);// 输出流,向path处文件写入数据
// b.compress()方法用于压缩图片大小,包含参数(图片的压缩格式,图像压缩率,写入压缩数据的输出流)
b.compress(Bitmap.CompressFormat.JPEG, 80, os);// quality表示压缩率
os.close();// 关闭资源
return Uri.fromFile(path);// Creates a Uri from a file
} catch (Exception ignored) {
}
return null;
}
PuzzlePage是游玩的核心界面,交互逻辑很多,之前尝试过全写在一个界面里,太乱了,而且要考虑的东西很多。所以多定义了几个类。
这是一个拼图工具类,主要实现拼图的交换与生成算法。首先需要介绍一下一个重要的概念:逆序数。
在滑动拼图游戏中,逆序数可以用于判断一个给定的拼图布局是否可以通过一系列的滑动操作来解决。这里的“解决”指的是将拼图块重新排列成原始的、正确的顺序。
什么时逆序数?逆序数是指在一个数字序列中,一个较大数字出现在一个较小数字前面的情况的总数。例如,在序列 [2, 3, 1] 中,数字 2 和 1 形成一个逆序对,数字 3 和 1 也形成一个逆序对,因此这个序列的逆序数是 2。
在滑动拼图中,每个拼图块可以用一个数字来标识,通常按照从左到右、从上到下的顺序排列。当你打乱这些拼图块时,就形成了一个新的序列。根据这个序列的逆序数,可以判断这个拼图是否可以被还原:
奇数和偶数行的影响:拼图游戏的可解性不仅取决于逆序数,还受到空白块(玩家用来移动拼图块的空格)所在行的影响。具体规则如下:
如果拼图的行数是奇数(例如,3x3拼图),那么逆序数也必须是偶数,这样的拼图才有解。
如果拼图的行数是偶数(例如,4x4拼图),空白块所在的行(从底部开始计数)的奇偶性将影响判断:
如果空白块在奇数行,逆序数必须是偶数。
如果空白块在偶数行,逆序数必须是奇数。
遍历当前的拼图块序列,对于序列中的每个拼图块,计算在它后面且数字小于它的拼图块的数量,所有这些数量加起来就是整个序列的逆序数。可以使用逆序数来检查当前的拼图布局是否有解,这样做可以避免陷入无解的情况,从而提高游戏的可玩性。
通过随机交换拼图块来生成一个打乱的拼图布局。首先随机选择一个拼图块与空白块进行多次交换,以此来打乱初始的拼图顺序。然后,检查当前的拼图布局是否有解,如果没有解,则递归调用自身直到生成一个有解的布局。
//生成随机的item(打乱初始状态)
public static void getPuzzleGenertor(){
int index = 0;
for(int i=0; i<mImgBeans.size() * 2; i++){ //有1/2概率可能生成无解的序列
index = (int)(Math.random() * PuzzlePage.GAME_TYPE * PuzzlePage.GAME_TYPE);//生成一定范围内的随机数作为form的标识符[0,9)
swapItems(mImgBeans.get(index), GameUtil.mBlankImgBean);
}
blankPosition = index ;//记录空白块位置,便于计算序列是否有解
List<Integer> list = new ArrayList<>();
for (int i=0; i<mImgBeans.size(); i++){
list.add(mImgBeans.get(i).getmBitmapId());//存储当前id序列
}
// 判断生成是否有解
if (canSolve(list)){
return;
} else {
getPuzzleGenertor();
}
}
当玩家选择一个拼图块时,如果该拼图块与空白块相邻,这个方法会被调用来交换这两个拼图块的位置。交换两个ImgBean对象的图像和ID,更新游戏界面上的显示,并将原来的空白块的位置更新为新的空白块的位置。
//交换空格与碎片的位置
public static void swapItems(ImgBean form, ImgBean blank){//form为交换格,blank为空白格
ImgBean tempImgBean = new ImgBean();
// 交换BitmapId
tempImgBean.setmBitmapId(form.getmBitmapId());//temp
form.setmBitmapId(blank.getmBitmapId());
blank.setmBitmapId(tempImgBean.getmBitmapId());
// 交换Bitmap
tempImgBean.setmBitmap(form.getmBitmap()); //temp
form.setmBitmap(blank.getmBitmap());
blank.setmBitmap(tempImgBean.getmBitmap());
// 设置新的Blank
GameUtil.mBlankImgBean = form;
}
判断当前的拼图布局是否可以通过滑动拼图块来解决。首先计算拼图块序列的逆序数,然后根据逆序数的奇偶性和空白块的行位置来判断拼图是否有解。这是确保玩家不会遇到无解的拼图布局,从而可以通过逻辑和技巧完成游戏。
//是否可以完成拼图,拼图算法的判断
public static boolean canSolve(List<Integer> list){
//获取空格ID
int blankId = mBlankImgBean.getmItemId();
//可行性判断
if(list.size() % 2 ==1) {//序列长度为奇数
return getInversion(list) % 2 == 0;
} else {//序列长度为偶数
if(((blankId -1)/ PuzzlePage.GAME_TYPE)%2 == 1){//从底往上数,空格位于奇数行
return getInversion(list)%2 == 0;
}else{//从底往上数,空格位于偶数行
return getInversion(list)%2 == 1;
}
}
}
逆序数的计算用于判断拼图是否有解。getInversion()方法通过双层循环遍历拼图块列表,比较每对拼图块的ID,计算出逆序对的总数。
//计算逆序算法
private static int getInversion(List<Integer> list){
int inversion = 0;
int inversionCount = 0;
for (int i=0; i<list.size(); i++){
for(int j=i+1; j<list.size(); j++){
int index = list.get(i);
if(list.get(j) != 0 && list.get(j) < index) {
inversionCount++; //逆序对个数+1
}
}
inversion += inversionCount; //逆序对总数
inversionCount = 0;
}
return inversion;
}
这个工具类的作用是对图片进行处理和切割。
//图片处理,切割类
public class ImagesUtil {
public static ImgBean imgBean;
public static int itemWidth = 0;
public static int itemHeight = 0;
private static Paint mPaint;
public static float scale ;
////对传入图片进行处理,分割
public static void createInitBitmaps(Context context, int type, Bitmap picSelected){
GameUtil.mImgBeans.clear();
Bitmap bitmap = null;
//获取所选图片的长宽
int width = picSelected.getWidth();
int height = picSelected.getHeight();
scale = (float)width/height; //计算(压缩)比例
int screenWidth = ScreenUtil.getScreenWidth(context);
picSelected = resizeBitmap(screenWidth, screenWidth/scale, picSelected);//根据手机屏幕宽度来对图片进行处理
List<Bitmap> bitmapItems = new ArrayList<>();
itemWidth = picSelected.getWidth()/type; //type范围:3,4,5
itemHeight = picSelected.getHeight()/type;
for (int i=1; i<=type; i++){
for (int j=1; j<=type; j++){
bitmap = Bitmap.createBitmap(
picSelected,
(j-1)*itemWidth, //裁剪x方向的起始位置
(i-1)*itemHeight,
itemWidth, //裁剪宽度
itemHeight //裁剪高度
);
bitmap = getRoundedCornerBitmap(bitmap);//获得圆角图片
bitmapItems.add(bitmap);//加入集合中
imgBean = new ImgBean(
(i-1)*type+j,//1,2,3,4....9记录id
(i-1)*type+j,
false,
bitmap
);
GameUtil.mImgBeans.add(imgBean);//封装成单元格
}
}
// 设置最后一个为空Item
GameUtil.mLastBitmap = bitmapItems.get(type*type-1);//最后一块图
bitmapItems.remove(type*type-1);//移除最后一块图
GameUtil.mImgBeans.remove(type*type-1);
Bitmap blankBitmap = BitmapFactory.decodeResource(
context.getResources(), R.drawable.blank
);
blankBitmap = Bitmap.createBitmap(
blankBitmap, 0, 0, itemWidth, itemHeight); //生成空白块
bitmapItems.add(blankBitmap);//把空白块加入List
GameUtil.mImgBeans.add(new ImgBean(type*type, 0, false, blankBitmap));//空白图片添加(代号为0)
GameUtil.mBlankImgBean = GameUtil.mImgBeans.get(type*type-1);
}
public static Bitmap resizeBitmap(float newWidth, float newHeight, Bitmap bitmap){
Matrix matrix = new Matrix();
matrix.postScale( //获取想要缩放的matrix
newWidth/bitmap.getWidth(),
newHeight/bitmap.getHeight()
);
Bitmap newBitmap = Bitmap.createBitmap( //获取新的bitmap
bitmap, 0, 0,
bitmap.getWidth(),
bitmap.getHeight(),
matrix, true);
return newBitmap;
}
//获得圆角图片的方法
public static Bitmap getRoundedCornerBitmap(Bitmap bitmap) {
Bitmap mBitmap = Bitmap.createBitmap(bitmap.getWidth(),
bitmap.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(mBitmap);
mPaint = new Paint();
mPaint.setAntiAlias(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
canvas.drawRoundRect(0, 0, bitmap.getWidth(), bitmap.getHeight(),
20, 20, mPaint);
}
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); //取两层绘制交集。显示上层
canvas.drawBitmap(bitmap, 0, 0, mPaint);
return mBitmap;
}
}
这是一个拼图Item逻辑实体类。
//拼图Item逻辑实体类:封装逻辑相关属性
public class ImgBean {
private int mItemId;
private int mBitmapId;
private Bitmap mBitmap;
public boolean isSelected() {
return isSelected;
}
public void setSelected(boolean selected) {
isSelected = selected;
}
private boolean isSelected;
public int getmItemId() {
return mItemId;
}
public void setmItemId(int mItemId) {
this.mItemId = mItemId;
}
public int getmBitmapId() {
return mBitmapId;
}
public void setmBitmapId(int mBitmapId) {
this.mBitmapId = mBitmapId;
}
public Bitmap getmBitmap() {
return mBitmap;
}
public void setmBitmap(Bitmap mBitmap) {
this.mBitmap = mBitmap;
}
public ImgBean(){}
public ImgBean(int mItemId, int nmBitmapId, boolean isSelected, Bitmap mBitmap){
this.mItemId = mItemId;
this.mBitmapId = nmBitmapId;
this.mBitmap = mBitmap;
}
@Override
public String toString() {
return mBitmapId+"";
}
}
单元格移动工具类,用来处理单元格移动动画。
public class ScrollGridView extends GridView { //单元格移动
public static int Gesture_Top = 1;
public static int Gesture_RIGHT = 2;
public static int Gesture_DOWN = 3;
public static int Gesture_LEFT = 4;
private boolean isReady = true ;
private GameInterface mInterface;
int blankPosition = -1;
public ScrollGridView(Context context) {
this(context, null);
}
public ScrollGridView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ScrollGridView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setPosition(int position){
this.blankPosition = position ;
}
public void setInterface(GameInterface anInterface) {
mInterface = anInterface;
}
public void setGesture(int gesture){ //手势
int type = PuzzlePage.GAME_TYPE;
int i = blankPosition / type ;
int j = (blankPosition+1) % type ;
if (gesture == Gesture_Top){
if (i != (type-1)){//判断此碎片是否能被移出,以3x3为例,空格除去7,8,9这三个位置外均能 执行上滑操作
startAnimator(blankPosition+type, "translationY");//translationY,相对于最初位置的y方向的偏移值
}
}
if (gesture == Gesture_DOWN){
if (i != 0){ //以3x3为例,空格除去1,2,3这三个位置外均能 执行下滑操作
startAnimator(blankPosition-type, "translationY");
}
}
if (gesture == Gesture_LEFT){
if (j != 0){
startAnimator(blankPosition+1, "translationX");//translationX,相对于最初位置的x方向的偏移值
}
}
if (gesture == Gesture_RIGHT){
if (j != 1){
startAnimator(blankPosition-1, "translationX");
}
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
return false ;
}
//触摸事件与动画
private void startAnimator(final int childId, String type) {
if (!isReady){
return;
}
getChildAt(blankPosition).setVisibility(INVISIBLE);//getChildAt根据参数返回对应view内容,并且设置为不可见
float distance = 0f;
if (type == "translationX"){
distance = getChildAt(blankPosition).getX() - getChildAt(childId).getX();
}
if (type == "translationY"){
distance = getChildAt(blankPosition).getY() - getChildAt(childId).getY();
}
final int x = (int) getChildAt(childId).getTranslationX();
final int y = (int) getChildAt(childId).getTranslationY();
//ObjectAnimator.ofFloat动画
ObjectAnimator animator = ObjectAnimator.ofFloat(getChildAt(childId), type,
0, distance); //distance为移动像素
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
isReady = false;
mInterface.addStep();
}
@Override
public void onAnimationEnd(Animator animation) { //演示动画结束后要进行数据更新
isReady = true;
GameUtil.swapItems(GameUtil.mImgBeans.get(childId),
GameUtil.mBlankImgBean); //滑动操作结束后交换空格和item格子
PuzzlePage.adapter.notifyDataSetChanged();//更新UI
getChildAt(childId).setTranslationX(x);
getChildAt(childId).setTranslationY(y);
getChildAt(blankPosition).setVisibility(VISIBLE);//空白视图 设置为可见
blankPosition = childId ;//更新空格id
if (isSuccess()){
mInterface.isSuccessful();
}
}
});
animator.setDuration(300);
animator.start();
}
//是否拼图成功,即当前图片Item的ID与初始状态下图片的ID是否相同。
private boolean isSuccess(){
for (ImgBean tempBean : GameUtil.mImgBeans){
Log.d("TAG","PuzzlePage.GAME_TYPE"+PuzzlePage.GAME_TYPE);
if(tempBean.getmBitmapId() != 0 && tempBean.getmItemId() == tempBean.getmBitmapId()){
continue; //空格此处无须比较
} else if (tempBean.getmBitmapId() == 0 && (tempBean.getmItemId() == (PuzzlePage.GAME_TYPE * PuzzlePage.GAME_TYPE))){
continue; //空白格与原图第九片重合
} else {
return false;
}
}
return true;
}
}
<?xml version="1.0" encoding="utf-8"?>
<!--the puzzle layout-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:orientation="vertical"
android:id="@+id/content"
>
<RelativeLayout
android:id="@+id/titlebar"
android:layout_width="match_parent"
android:layout_height="56dp"
android:orientation="horizontal"
android:background="#ff303537"
>
<ImageView
android:id="@+id/left_btn"
android:layout_width="25dp"
android:layout_height="25dp"
android:background="@drawable/btn_press"
android:layout_marginStart="17dp"
android:layout_centerVertical="true"
android:layout_alignParentStart="true"
android:clickable="true"
android:onClick="onClick"
android:layout_marginLeft="17dp"
android:layout_alignParentLeft="true" />
<TextView
android:id="@+id/help"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="帮助"
android:textSize="20dp"
android:gravity="center_horizontal"
android:clickable="true"
android:onClick="onClick"
android:layout_marginEnd="17dp"
android:layout_alignBottom="@+id/left_btn"
android:layout_alignParentEnd="true"
android:layout_marginRight="17dp"
android:layout_alignParentRight="true" />
</RelativeLayout>
<LinearLayout
android:id="@+id/score_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="0"
android:orientation="horizontal"
android:layout_margin="10dp"
>
<TextView
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:text="用时:"
android:gravity="right"
android:textColor="@color/colorAccent"
android:textSize="16sp"
/>
<TextView
android:id="@+id/timer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="0s"
android:textColor="@color/colorAccent"
/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="步数:"
android:gravity="right"
android:textColor="@color/colorAccent"
android:textSize="16sp"
/>
<!-- 权重android:layout_weight="1" 都是1那就等分-->
<TextView
android:id="@+id/step"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="0"
android:textColor="@color/colorAccent"
/>
</LinearLayout>
<com.example.puzzlegame.view.ScrollGridView
android:id="@+id/grid_list"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@color/white"
android:horizontalSpacing="5px"
android:numColumns="3"
android:scrollbars="none"
android:verticalSpacing="5px" />
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="70dp"
android:layout_marginEnd="70dp"
android:layout_marginBottom="20dp"
android:layout_weight="0"
android:background="@color/white"
>
<ImageButton android:background="@drawable/nosilence"
android:id="@+id/nosilence"
android:layout_width="100dp"
android:layout_height="50dp"
android:layout_weight="1"
android:layout_marginEnd="20dp"
android:onClick="onClick"
android:layout_marginRight="20dp" />
<ImageButton android:background="@drawable/help"
android:id="@+id/help1"
android:layout_width="100dp"
android:layout_height="50dp"
android:layout_weight="1"
android:layout_marginEnd="20dp"
android:onClick="onClick"
android:layout_marginRight="20dp" />
<ImageButton android:background="@drawable/difficulty"
android:id="@+id/difficulty"
android:layout_width="100dp"
android:layout_height="50dp"
android:layout_weight="1"
android:layout_marginEnd="20dp"
android:onClick="onClick"
android:layout_marginRight="20dp" />
<ImageButton
android:id="@+id/fanhui"
android:layout_width="100dp"
android:layout_height="50dp"
android:background="@drawable/restart"
android:layout_weight="1"
android:onClick="onClick"/>
</LinearLayout>
</LinearLayout>
效果如下:

定义一个Handler,处理不同类型的消息,并根据消息的类型执行相应的操作。
case 1:表示拼图成功。播放成功音效,显示一个对话框,告知用户拼图成功,并显示用时和步数。对话框中有一个按钮,点击后会跳转回欢迎页面。
case 2:用于更新计时器显示。将计时器TextView更新为当前时间,单位是秒。
case 3:用于更新步数显示。将步数TextView更新为当前步数。
case 4:重置游戏。重置时间和步数,取消当前的计时任务并重新开始计时,使用保存的Uri重新初始化拼图网格视图。
case 5:表示拼图步数过多,游戏失败。显示一个对话框,提示用户游戏失败,可以尝试切换难度,并提供一个按钮跳转回欢迎页面。
@SuppressLint("HandlerLeak")
public Handler mHandler = new Handler() {
private Uri imageUri;
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 1:
Log.d("successful", "true");
timer.setEnabled(false);
// 拼图成功音效
MediaPlayer mMediaPlayer;// MediaPlayer类是同步执行的只能一个文件一个文件的播放
mMediaPlayer = MediaPlayer.create(PuzzlePage.this, R.raw.cheer);
mMediaPlayer.start();
new AlertDialog.Builder(PuzzlePage.this).setMessage("恭喜您拼图成功,您用了" + time + "秒," + step + "步")
.setPositiveButton("确定", (dialog, id) -> {
Intent intent = new Intent(PuzzlePage.this, WelcomePage.class);// 跳回主页
startActivity(intent);
}).setCancelable(false).show();
break;
case 2:
timer.setText(time + "s");
break;
case 3:
stepCount.setText(step + "");
break;
case 4:
time = -1;
task.cancel();
startTimer();
step = 0;
stepCount.setText(step + "");
initGridView(this.imageUri); // 使用存储的Uri重新初始化视图
break;
case 5: // 拼图步数过多失败
Log.d("false", "true");
timer.setEnabled(false);
new AlertDialog.Builder(PuzzlePage.this).setMessage("哎呀,是不是太难了,您可以尝试切换难度~")
.setPositiveButton("确定", (dialog, id) -> {
Intent intent = new Intent(PuzzlePage.this, WelcomePage.class);// 跳回主页
startActivity(intent);
}).setCancelable(false).show();
break;
}
}
};
启动了一个计时器,初始化了界面,并设置了声音池。此外,从上一个界面获取一个图片的Uri字符串,并将其转换成Uri对象,存储为类的成员变量,然后使用这个Uri初始化了游戏的网格视图。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.moving_recording);
SizeHelper.prepare(this);
mWindowManager = (WindowManager) this.getSystemService(Context.WINDOW_SERVICE);// 获取窗口服务
audio = (AudioManager) getSystemService(Service.AUDIO_SERVICE); // 获取AudioManage引用
startTimer();
initView();
id = initSoundpool();
// 获取传递的Uri字符串
Intent intent = getIntent();
String imageUriString = intent.getStringExtra("imageUri");
Log.d("PuzzlePage", "Received image URI: " + imageUriString);
System.out.println(imageUriString);
Uri imageUri = Uri.parse(imageUriString); // 将字符串转换为Uri
this.imageUri = imageUri; // 存储为类成员变量
initGridView(this.imageUri); // 修改此处调用,传递Uri
playerbg = MediaPlayer.create(this, R.raw.bgm);
playerbg.setLooping(true);
playerbg.start();
playerbgstate = 1;
}
@SuppressLint("ClickableViewAccessibility")
private void initView() {
mGestureDetector = new GestureDetector(this, listener);// 手势监听检测
screenWidth = ScreenUtil.getScreenWidth(this);
look = (ImageButton) findViewById(R.id.help1); // 绑定“提示”按钮
look.setOnTouchListener((v, event) -> {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:// 检测按下
creatMirrorImage();
break;
case MotionEvent.ACTION_UP:// 检测抬起
deleteMirrorImage();
break;
}
return false;
});
timer = (TextView) findViewById(R.id.timer);
stepCount = (TextView) findViewById(R.id.step);
RelativeLayout layout = (RelativeLayout) findViewById(R.id.titlebar); // 顶端条
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
SizeHelper.fromPx(74));// 设置子视图(顶端条)的宽,高
layout.setLayoutParams(params);
}
private int initSoundpool() {
// Sdk版本>=21时使用下面的方法
if (Build.VERSION.SDK_INT >= 21) {
SoundPool.Builder builder = new SoundPool.Builder();// SoundPool类支持同时播放多个音效
// 设置最多容纳的流数
builder.setMaxStreams(2);
AudioAttributes.Builder attrBuilder = new AudioAttributes.Builder();
attrBuilder.setLegacyStreamType(AudioManager.STREAM_MUSIC);
builder.setAudioAttributes(attrBuilder.build());
spool = builder.build();
} else {
spool = new SoundPool(2, AudioManager.STREAM_MUSIC, 0);
}
// 加载滑动碎片时的音频文件,返回音频文件的id
int id = spool.load(getApplicationContext(), R.raw.slip, 1);
return id;
}
private void startTimer() { // 开始计时器
time = -1;
mTimer = new Timer(true);
task = new TimerTask() {
@Override
public void run() {
Message message = new Message();
message.what = 2;
time++;
mHandler.sendMessage(message);// 更新时间
}
};
mTimer.schedule(task, 0, 1000);// 定时器间隔1s调用一次run方法
}
初始化游戏的主要界面——拼图网格。使用ImagesUtil类处理并分割图片,GameUtil类则用于将分割后的图片块进行随机打乱。接着设置ScrollGridView的适配器,定义网格的列数,并将打乱后的图片块显示在网格中。最后,为网格设置一个接口,用于在游戏过程中监测拼图是否完成和记录玩家的操作步数。
private void initGridView(Uri imageUri) {
gridView = (ScrollGridView) findViewById(R.id.grid_list);
try {
InputStream inputStream = getContentResolver().openInputStream(imageUri);
mBitmap = BitmapFactory.decodeStream(inputStream);
inputStream.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
Toast.makeText(this, "图片文件未找到", Toast.LENGTH_SHORT).show();
return;
} catch (IOException e) {
e.printStackTrace();
Toast.makeText(this, "读取图片出错", Toast.LENGTH_SHORT).show();
return;
}
ImagesUtil.createInitBitmaps(this, GAME_TYPE, mBitmap); // 对传入图片进行处理,分割
GameUtil.getPuzzleGenertor(); // 对分割完的图片进行打乱处理
adapter = new PuzzleAdapter(this, GameUtil.mImgBeans);
gridView.setNumColumns(GAME_TYPE);
gridView.setAdapter(adapter);
gridView.setPosition(GameUtil.blankPosition);
gridView.setInterface(new GameInterface() {
@Override
public void isSuccessful() {
Message message = new Message();
message.what = 1;
mHandler.sendMessage(message);
}
@Override
public void addStep() {
addStepCount();
Log.d("TAG", "操作x1");
}
});
}
设置一个一个手势监听器,专门用于处理用户在屏幕上的快速滑动动作。计算手势的起点和终点坐标差,判断用户的滑动方向(左、右、上、下)。根据滑动的方向,代码会调整游戏界面上的拼图块位置,并播放相应的声音效果,从而增强游戏的交互性。
// 手势识别器
GestureDetector.OnGestureListener listener = new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2,
float velocityX, float velocityY) {
// e1为(手势起点)第一次按下坐标,e2为(手势终点)手指离开屏幕时坐标
// 安卓的坐标原点是在左上角的,向下是y正轴,向右是x正轴
float x = e2.getX() - e1.getX();
float y = e2.getY() - e1.getY();
float x_abs = Math.abs(x);// 返回绝对值
float y_abs = Math.abs(y);
int gesture = 0;
if (x_abs >= y_abs) { // 如果x轴上的位移计算结果是比y轴上大
if (x_abs >= screenWidth / 5) { // 如果滑动距离过短则判定无效
if (x > 0) {
gesture = ScrollGridView.Gesture_RIGHT; // 水平向右移动
} else {
gesture = ScrollGridView.Gesture_LEFT; // 水平向左滑动
}
}
} else {
if (y_abs >= screenWidth / 5) {
if (y > 0) {
gesture = ScrollGridView.Gesture_DOWN; // 向下滑动
} else {
gesture = ScrollGridView.Gesture_Top; // 向上滑动
}
}
}
gridView.setGesture(gesture);
spool.play(id, 1, 1, 0, 0, 1);
return true;
}
};
在非休闲模式,把不同难度等级的游戏步数设置为128,256,512。
private void addStepCount() {
step++;
if (flag) { // 非休闲模式才执行
switch (GAME_TYPE) {
case 3:
if (step >= 128) {
Message message = new Message();
message.what = 5;
mHandler.sendMessage(message);
}
break;
case 4:
if (step >= 256) {
Message message = new Message();
message.what = 5;
mHandler.sendMessage(message);
}
break;
case 5:
if (step >= 512) {
Message message = new Message();
message.what = 5;
mHandler.sendMessage(message);
}
break;
}
}
Message message = new Message();
message.what = 3;
mHandler.sendMessage(message);
}
帮助按钮:显示一个对话框,提供游戏操作的帮助信息,包括背景音乐控制、原图帮助、难度选择和生成新游戏的说明。
重开按钮:显示一个确认对话框,询问用户是否确定要重新生成游戏。如果用户点击确定,则调用restartGames方法重新开始游戏。
静音按钮:控制背景音乐的播放和暂停。如果当前有音乐播放,则将其暂停并更改按钮图标为静音图标;如果当前是静音状态,则开始播放音乐并更改按钮图标为非静音图标。
难度选择按钮:显示一个单选对话框,让用户选择游戏的难度(3x3, 4x4, 5x5)。用户选择后,更新游戏难度并通过消息通知Handler处理难度更改。
活动返回按钮:停止并释放背景音乐播放器,关闭当前活动,并跳转回欢迎页面。
public void onClick(View v) {
if (v.getId() == R.id.help) {
AlertDialog.Builder builder1 = new AlertDialog.Builder(PuzzlePage.this);
builder1.setMessage("游戏图标操作功能依次为背景音乐控制、原图帮助、难度选择、生成新游戏," +
"其中原图帮助需长按图标方能显示图片,松开后原图消失。" +
"进入打乱的图片页面,将空白处周围的图片移向空白处,继续这样的操作,直到拼完。");
builder1.setTitle("游戏帮助");
builder1.create().show();
} else if (v.getId() == R.id.fanhui) {
new AlertDialog.Builder(this).setTitle("确定要重新生成游戏么?")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
restartGames();
}
})
.setNegativeButton("返回", null).show();
} else if (v.getId() == R.id.nosilence) {
ImageButton bgm;
bgm = (ImageButton) findViewById(R.id.nosilence);// 设置 对应的静音按钮
if (playerbgstate == 1) { // 如果此时是有背景音乐则把图标切换为静音资源
bgm.setBackgroundResource(R.drawable.silence);
// bgm.setBackgroundDrawable(getResources().getDrawable(R.drawable.silence));
playerbgstate = -1;
playerbg.pause();// 暂停播放
} else if (playerbgstate == -1) {
bgm.setBackgroundResource(R.drawable.nosilence);// 如果点击按钮之前是暂定播放(静音)状态 则更新图标
playerbgstate = 1;
playerbg.start();// 开始播放
}
} else if (v.getId() == R.id.difficulty) {
AlertDialog.Builder builder = new AlertDialog.Builder(PuzzlePage.this);
builder.setTitle("难度选择:");
builder.setSingleChoiceItems(new String[] { "3X3", "4X4", "5X5" }, 0,
(dialog, which) -> {
switch (which) {
case 0:
PuzzlePage.GAME_TYPE = 3;
break;
case 1:
PuzzlePage.GAME_TYPE = 4;
break;
case 2:
PuzzlePage.GAME_TYPE = 5;
break;
}
}).setPositiveButton("确定", (dialog, which) -> {
Message message = new Message();
message.what = 4;
mHandler.sendMessage(message);
}).show();
} else if (v.getId() == R.id.left_btn) {
if (playerbg != null) {
playerbg.stop();
playerbg.release();
playerbg = null;
}
finish();
Intent intent = new Intent(PuzzlePage.this, WelcomePage.class);
startActivity(intent);
}
}
拼不出来!!还是得加一个原图提示功能,帮助玩家在拼图时有一个参考,可以在需要时查看原图,释放后原图消失,不影响游戏的进行。
creatMirrorImage 方法用于创建并显示原图的镜像预览。首先通过AlphaAnimation实现一个从不透明到完全透明的渐变动画,使得拼图网格(gridView)在动画结束时变得透明,从而不干扰镜像的显示。然后设置一个新的mImageView,并将其添加到窗口管理器中,显示在屏幕上。
当用户完成原图的查看并释放按钮时,调用deleteMirrorImage 方法来移除镜像预览。首先检查mImageView是否存在,如果存在,则通过另一个AlphaAnimation从透明过渡到不透明,恢复gridView的可见性。然后,从窗口管理器中移除mImageView,恢复游戏界面到原始状态。
// 图像(镜像)预览
private void creatMirrorImage() {
AlphaAnimation animation = new AlphaAnimation(1, 0);// 从不透明到完全透明的渐变过程,改变组件的透明度
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
gridView.setAlpha(0);
}// 拼图列表nxn变为透明
@Override
public void onAnimationRepeat(Animation animation) {
}
});
animation.setDuration(800);// 渐变0.8s内完成,(完成动画的时长)
gridView.startAnimation(animation);
mWindowLayoutParams = new WindowManager.LayoutParams();
mWindowLayoutParams.format = PixelFormat.TRANSLUCENT;// 设置窗口格式为半透明
mWindowLayoutParams.gravity = Gravity.TOP | Gravity.LEFT; // 设置窗口停靠,并不改变其大小
mWindowLayoutParams.x = gridView.getLeft();
mWindowLayoutParams.y = gridView.getTop();
mWindowLayoutParams.width = gridView.getWidth();
mWindowLayoutParams.height = gridView.getHeight();
mWindowLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
mImageView = new ImageView(this);
mImageView.setImageBitmap(mBitmap);
mWindowManager.addView(mImageView, mWindowLayoutParams);// 绘制(添加)“预览”视图显示
}
// “关闭”图像预览,“恢复”拼图界面
private void deleteMirrorImage() {
if (mImageView != null) {
AlphaAnimation animation = new AlphaAnimation(0, 1);// 设置从透明变为不透明
animation.setDuration(500);
gridView.startAnimation(animation);
gridView.setAlpha(1);// (显示原本内容)不透明
mWindowManager.removeView(mImageView); // 移出“预览”视图显示
}
}
用户想要放弃当前游戏进度并重新开始游戏时,点击界面上的“重开”按钮并在随后出现的确认对话框中选择“确定”时触发restartGames方法。
restartGames方法重置计时器和取消当前任务。动画结束后,重新生成并打乱拼图,更新网格视图,并通过另一个从透明到不透明的动画逐渐显示新的游戏界面。在这个过程中,还重置了步数并重新启动了计时器,确保游戏状态完全刷新。
private void restartGames() {
AlphaAnimation animation = new AlphaAnimation(1, 0);// 从不透明到,完全透明
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
time = -1; // 计时器重置
task.cancel();
}
@Override
public void onAnimationEnd(Animation animation) {
addStepCount();// 开始步数统计
GameUtil.getPuzzleGenertor();// 调用之前的打乱函数即可
gridView.setPosition(GameUtil.blankPosition);
adapter.notifyDataSetChanged(); // 刷新ListView
AlphaAnimation animation1 = new AlphaAnimation(0, 1);// 从透明再到不透明
animation1.setAnimationListener(new Animation.AnimationListener() {// 新动画
@Override
public void onAnimationStart(Animation animation) {
startTimer();
step = 0;
}
@Override
public void onAnimationEnd(Animation animation) {
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
animation1.setDuration(1000);
gridView.startAnimation(animation1);
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
animation.setDuration(1000);
gridView.startAnimation(animation);
}
因为项目可以播放背景音乐,所以对背景音乐要有控制。通过处理音量键和返回键的事件,提供音量控制和导航回主界面的功能。
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
// TODO Auto-generated method stub
switch (keyCode) {
case KeyEvent.KEYCODE_VOLUME_UP:// 增大音量
audio.adjustStreamVolume(
AudioManager.STREAM_MUSIC,
AudioManager.ADJUST_RAISE,
AudioManager.FLAG_PLAY_SOUND | AudioManager.FLAG_SHOW_UI);
return true;
case KeyEvent.KEYCODE_VOLUME_DOWN:// 减小音量
audio.adjustStreamVolume(
AudioManager.STREAM_MUSIC,
AudioManager.ADJUST_LOWER,
AudioManager.FLAG_PLAY_SOUND | AudioManager.FLAG_SHOW_UI);
return true;
case KeyEvent.KEYCODE_BACK:// 返回按钮
if (playerbg != null) {// 如果有声音则停止MediaPlayer
playerbg.stop();
playerbg.release();// 释放这个MediaPlayer
playerbg = null;
}
finish();
Intent intent = new Intent(PuzzlePage.this, WelcomePage.class);
startActivity(intent);
return true;
}
return super.onKeyDown(keyCode, event);
}
实验当中遇到了很多问题,这里找两个花费我时间比较久的问题。
完成这一次综合实践针真的是一次非常宝贵的学习和实践经验。从项目的构思、设计到最终的实现,每一个步骤都充满了挑战,也让我学到了许多综合性知识。我更加深入学习了Android开发的多个方面,包括UI设计、事件处理、动画效果、音频管理等。通过实际应用这些技术,我不仅理解了它们的工作原理,还学会了如何将它们融合到一个完整的应用程序中。特别是在处理图像和手势操作时,我遇到了不少问题,如何高效地处理和显示大量的图像数据,以及如何确保手势操作的流畅性和准确性,这些我都通过不断查询资料和反复测试才逐步找到了问题的解决方法。
开发过程中遇到的问题迫使我不断寻找解决方案,这个过程极大地锻炼了我的问题解决能力和创新思维。在实现原图预览功能时,我最初的方案是使用一个新的Activity来显示原图,直接导致了体验不连贯。后来,我上网查了下资料,改用Overlay的方式在当前Activity上直接显示原图,这样不仅解决了问题,还提升了用户体验。
临近期末,通过这个项目,我也提高了合理安排时间的能力,确保在完成项目开发的同时不落下复习。项目开发完成,内心的成就感还是很大的。
时间飞逝,转眼间,《移动平台开发与实践》这门课程已经画上了圆满的句号。回首过去的一个学期,从最初的Android开发环境搭建,到最后一个综合性项目的完成,每一步都见证了我的成长与进步。这段充实而富有挑战的学习经历,不仅丰富了我对于移动平台开发的专业知识,更让我收获了许多宝贵的心得与体会。
在课程的初期,我们自学了如何安装和配置Android Studio,创建第一个简单的Android应用。这一阶段虽然看似简单,但对于初次接触Android开发的我来说,是一个不小的挑战。通过不断地尝试和调试,我终于成功运行了第一个应用程序,这让我感受到了移动平台开发不同于传统PC端开发的乐趣和成就感。随着课程的深入,我们学习了Android的四大组件:Activity、Service、BroadcastReceiver和ContentProvider。每个组件都有其独特的作用和使用场景。通过实际项目的开发,我也学会了如何灵活运用这些组件,实现复杂的功能。在完成本次综合实践的时候,随着处理图片的切割和拼接、管理游戏的状态和计时等功能不断被添加,我也更加深刻理解了Activity的生命周期和事件处理机制。
数据存储是移动应用开发中的重要一环。通过SQLite数据库的学习和实践,我掌握了如何在应用中存储和管理数据。同时,我还学习了ContentProvider的使用,通过内容提供者实现应用间的数据共享。这不仅提升了应用的功能性,也让我理解了数据安全和隐私保护的重要性。这次综合实践美中不足的一点也是没有使用到数据库,可以考虑把每次获胜记录添加到数据库中,也可以考虑添加以下用户登录的功能。
网络通信是现代应用程序不可或缺的一部分。通过Socket编程的学习,我掌握了客户端与服务器之间的数据通信原理,并且在一个实验中实现了一个简单的聊天应用,通过Socket实现实时的消息传递和接收。这是我跟同学一块儿做的,当时没什么经验,一直做到了凌晨。这一过程中,我深刻体会到了团队合作和有效沟通的重要性。通过分工合作,才能提高工作效率,也相互学习、共同进步。编程不仅仅是一个人的战斗,更需要团队的力量。
现代移动应用往往需要调用各种第三方SDK,以实现更丰富的功能。通过学习和实践,我掌握了如何在Android项目中集成和使用第三方SDK。在之前的实验中我集成了百度地图SDK,实现了位置服务和地图显示功能;集成了科大讯飞语音识别SDK实现了语音识别功能。这些实践经验让我更好地理解了SDK的工作原理和最佳实践。
经过一学期的学习,我也体会到,理论知识固然重要,但只有通过实践,才能真正掌握和应用这些知识。在每一次实验中,我都遇到了许多实际问题,但正是通过解决这些问题,我加深了对理论知识的理解,提高了编程能力。
随着技术的不断发展,编程语言和开发工具也在不断更新。在这门课程中,我也学会了如何查找和利用最新的技术资源,不断学习和提升自己。同时,在解决实际问题时,我也学会了如何创新思维,提出和实现新的解决方案。
《移动平台开发与实践》为我打开了Android开发的大门,让我从一个对移动开发一无所知的新手,成长为能够独立完成基本应用开发的开发者。这段学习经历不仅丰富了我的知识体系,更培养了我解决问题的能力和创新思维。感谢老师的悉心指导和同学们对我的帮助,这些都将成为我未来学习工作中的宝贵财富。课程的结束并不是学习的终点,而是新的起点。我将带着这段宝贵的经历,继续在编程的道路上前行,迎接更多的挑战!