
简介一款基于Android Studio实现的经典华容道益智小游戏以移动方块让“曹操”抵达出口为核心玩法面向安卓初学者和需要完成课程设计的学生覆盖从规则设计、界面布局到触摸事件处理、方块移动逻辑的完整开发流程。压缩包共1223个文件包含Java源码、XML布局、Gradle构建脚本、JSON配置文件、PNG/JPG图片资源及可安装的APK压缩后整体大小约11.04MB目录结构清晰方便按需查阅与二次开发。已有373人学习下载。资源提供可直接导入Android Studio运行的项目工程并附有详细的开发过程总结涵盖触摸偏移量判断方向、事件监听与传递、资源管理等关键技术点可帮助读者快速理解经典益智类APP的实现思路。作为安卓大作业模板能显著节省搭建时间并为课程设计或面试作品提供完整范例。1. 用Android Studio做华容道先别急着写界面很多新手选择“华容道小游戏”当安卓课程设计是因为它看起来逻辑简单无非是移动数字块。但真正动手时会发现卡住你的往往不是界面而是三个问题棋盘状态该用什么结构存、点击后怎么判断能不能移动、洗牌之后为什么拼不回去。下面这套方案直接用Android Studio实现一个可运行的华容道App从项目创建、GridView布局、数据模型、洗牌算法到真机调试讲完整覆盖课程设计评审会看的功能点步数、计时、胜利弹窗、重新开局以及可以写进报告里的扩展设计。适合第一次用Android Studio做完整项目的学生也适合想快速了解完整开发链条的Android初学者。2. Android Studio项目搭建华容道的棋盘模型与GridView布局2.1 用二维数组定义华容道棋盘状态华容道数字华容道是一类4x4滑动拼图15个数字块加一个空位。处理它之前先把棋盘抽象成数据结构。我建议用int[][]而不是一维数组因为二维数组的行列索引和界面上格子一一对应判断上、下、左、右时不用做除法换算对新手更友好。定义一个PuzzleBoard类内部保存棋盘状态和空格坐标public class PuzzleBoard { private int[][] board; private int emptyRow; private int emptyCol; private int size 4; public PuzzleBoard() { board new int[size][size]; initOrdered(); } private void initOrdered() { int num 1; for (int i 0; i size; i) { for (int j 0; j size; j) { board[i][j] num; } } board[size - 1][size - 1] 0; // 空格放在最后 emptyRow size - 1; emptyCol size - 1; } }initOrdered()生成的是完成状态移动前需要洗牌但洗牌不能直接对一维数组Collections.shuffle否则容易出现无解局面。emptyRow和emptyCol记录空格位置移动时只需要判断点击位置和空格是否相邻。棋盘类里提供移动方法public boolean move(int row, int col) { if (row 0 || row size || col 0 || col size) return false; if (Math.abs(row - emptyRow) Math.abs(col - emptyCol) ! 1) return false; board[emptyRow][emptyCol] board[row][col]; board[row][col] 0; emptyRow row; emptyCol col; return true; }这段代码的关键是移动条件行差绝对值加列差绝对值等于1代表目标位置是空格的上下左右邻居。返回boolean方便后续调用方决定是否增加步数。用这种写法三个方向的边界情况都被绝对值公式覆盖不用写四个if。2.2 配置gradle镜像和Android SDK路径绕开新手前两道坎Android Studio下载安装后第一步通常是新建项目等待Gradle同步。如果直接使用默认仓库拉取依赖会非常慢很多新手以为卡死了。常见的做法是把仓库地址换成阿里云镜像同时保留Google官方仓库兜底。在项目根目录的settings.gradle.kts中修改pluginManagement { repositories { maven(https://maven.aliyun.com/repository/google) maven(https://maven.aliyun.com/repository/public) maven(https://maven.aliyun.com/repository/gradle-plugin) google() mavenCentral() } } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { maven(https://maven.aliyun.com/repository/public) google() mavenCentral() } }注意repositoriesMode设置成FAIL_ON_PROJECT_REPOS后如果从旧项目复制过来的app/build.gradle里还写了repositories会在同步时报错。解决办法是把模块级配置里的repositories整段删掉统一在settings.gradle.kts里管理。如果你想让Android Studio界面显示中文可以在Settings - Plugins里搜索安装“Chinese Language Pack”安装后重启就是中文界面。这个和项目本身无关但新手环境配置阶段会经常用到。另一个常见问题SDK Manager里的组件无法勾选。先检查File - Project Structure - SDK LocationSDK路径不要包含空格和中文最好放在D:\Android\Sdk这类纯英文目录下。Windows下如果权限不够右键SDK目录选择“属性 - 安全”给当前用户完全控制权限。2.3 选GridView而不是手写Canvas适配华容道的4x4网格棋盘渲染有两种主流思路一是自定义View配合Canvas逐格绘制二是用GridView加Adapter。对课程设计和Android新手来说GridView是最稳妥的选择。Adapter机制天然适合数据列表展示你只需要维护数组数据格子复用、滚动回收都由系统处理不用手动计算每个格子的位置。activity_main.xml里的核心布局GridView android:idid/grid_puzzle android:layout_width0dp android:layout_height0dp android:numColumns4 android:columnWidth90dp android:horizontalSpacing8dp android:verticalSpacing8dp android:gravitycenter android:stretchModecolumnWidth app:layout_constraintDimensionRatio1:1 app:layout_constraintEnd_toEndOfparent app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toBottomOfid/tv_title app:layout_constraintBottom_toTopOfid/ll_info /关键参数说明参数值作用numColumns4固定4列对应4x4棋盘columnWidth90dp每列基准宽度配合stretchMode填满剩余空间horizontalSpacing8dp格子水平间距视觉上更像拼图块layout_constraintDimensionRatio1:1让GridView宽高比保持1:1保证格子是正方形这里用0dp加上dimensionRatio约束棋盘会根据屏幕宽度自动计算高度。如果把宽高写成wrap_contentGridView会按四行item的总高度计算在部分机型上可能超过屏幕高度。子item布局item_number.xml只需要一个居中TextView。Adapter在下一章实现但要注意GridView的item会被复用所以不要在item上保存任何状态所有数字都从board数组读取。3. 华容道核心逻辑洗牌、移动判定与胜负检测3.1 用Adapter渲染二维数组点击后交换并刷新数据模型和UI之间用BaseAdapter桥接。先把二维数组铺平成int[]这样GridView的position和棋盘行列的换算关系就是row position / 4col position % 4。实现一个PuzzleAdapterpublic class PuzzleAdapter extends BaseAdapter { private int[] flatData; private Context context; public PuzzleAdapter(Context context, int[] flatData) { this.context context; this.flatData flatData; } Override public int getCount() { return flatData.length; } Override public Object getItem(int position) { return flatData[position]; } Override public long getItemId(int position) { return position; } Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView null) { convertView LayoutInflater.from(context) .inflate(R.layout.item_number, parent, false); } TextView tvNumber convertView.findViewById(R.id.tv_number); int value flatData[position]; if (value 0) { tvNumber.setText(); convertView.setBackgroundColor(Color.TRANSPARENT); } else { tvNumber.setText(String.valueOf(value)); convertView.setBackgroundResource(R.drawable.bg_block); } return convertView; } public void updateData(int[] newData) { this.flatData newData; notifyDataSetChanged(); } }在Activity中点击事件的处理gridView.setOnItemClickListener((parent, view, position, id) - { int row position / 4; int col position % 4; if (board.move(row, col)) { adapter.updateData(flatten(board.getBoard())); steps; tvSteps.setText(步数 steps); checkWin(); } });flatten()方法把二维数组转一维顺序按行优先。移动成功后才更新AdapternotifyDataSetChanged触发GridView重新渲染点击前后数字块的位置就交换了。这里的move内部已经判断了是否相邻不需要在Activity里再写一遍位置判断。3.2 用逆序数判断洗牌结果是否有解很多新手会在初始化时直接随机打乱数组结果玩到最后两块顺序反了怎么也拼不回去。实际上4x4华容道不是所有排列都可解需要满足逆序数条件。先明确概念把一个排列中每个数字后面比它小的数字个数累加得到逆序数。对于4x4棋盘当空格位于最后一行时只有逆序数为偶数的排列才有解。所以我们可以这样生成有解局面保持空格不动只用前15个数字洗牌然后计算逆序数如果是奇数就交换最后两个数字块。private int[] generateSolvableBoard() { int[] flat new int[16]; for (int i 0; i 15; i) flat[i] i 1; flat[15] 0; ListInteger list new ArrayList(); for (int i 0; i 15; i) list.add(flat[i]); Collections.shuffle(list); int inv 0; for (int i 0; i 15; i) { flat[i] list.get(i); for (int j 0; j i; j) { if (flat[j] flat[i]) inv; } } if (inv % 2 1) { int tmp flat[13]; flat[13] flat[14]; flat[14] tmp; } return flat; }洗牌后把一维数据填入PuzzleBoard同时把空格坐标固定在最后一行。这里不需要在每次洗牌时重新计算空格位置因为空格始终在flat[15]也就是第4行第4列。这样生成的所有局面都在逻辑上可解玩家可以通过有限步数复原。如果你用真机测试时发现某局怎么也拼不回来大概率是随机洗牌没有做逆序数校验。这一点写进课程设计报告能明显体现你理解了算法原理而不是只会调接口。3.3 步数和计时器用Handler还是Chronometer步数用int累加即可难点在计时。Android提供的Chronometer组件可以直接显示跑表但它基于SystemClock暂停和恢复的控制相对麻烦格式化时间也不够自由。课程设计里我一般用Handler配合SystemClock.elapsedRealtime()实现逻辑完全可控。private Handler handler new Handler(Looper.getMainLooper()); private long startTime; private Runnable timerRunnable new Runnable() { Override public void run() { long elapsed SystemClock.elapsedRealtime() - startTime; int seconds (int) (elapsed / 1000); int minutes seconds / 60; seconds seconds % 60; tvTime.setText(String.format(Locale.CHINA, 时间%02d:%02d, minutes, seconds)); handler.postDelayed(this, 1000); } }; private void startTimer() { startTime SystemClock.elapsedRealtime(); handler.postDelayed(timerRunnable, 1000); }elapsedRealtime()记录的是开机以来的毫秒数不受手动改系统时间影响适合计时场景。每次更新秒数后再次postDelayed形成每秒刷新一次的效果。Activity销毁时必须移除回调否则Runnable持有Activity引用会造成内存泄漏Override protected void onDestroy() { super.onDestroy(); handler.removeCallbacksAndMessages(null); }两种计时方案对比方案优点缺点适用场景Handler elapsedRealtime可控性高暂停清零都方便需要手动移除回调课程设计中的正计时、成绩统计Chronometer代码量最少自带时间格式停止和重置要处理组件状态简单跑表演示ValueAnimator可以同时驱动UI动画计时逻辑要维护累计时长需要和时间联动动画时使用关于计时还有一个进阶需求玩家按Home键再回来计时要不要继续默认情况下Activity进入后台但没销毁Handler仍然每秒回调计时继续。如果你希望后台停止计时需要在onPause移除Runnable在onResume重新启动并记录暂停时长这个可以作为报告里的“待完善点”。4. 真机调试与Android Studio中的常见构建报错4.1 连接小米手机调试华容道App先过adb这关模拟器在低配置电脑上容易卡顿真机调试反而更顺畅。以小米手机为例连接步骤是先开启开发者选项进入“设置 - 我的设备 - 全部参数与信息”连续点击“MIUI版本”7次然后回到“设置 - 更多设置 - 开发者选项”打开“USB调试”。用数据线连接电脑后手机上弹出“允许USB调试”时选允许。Android Studio的顶部设备列表通常会自动出现手机型号。如果识别不到打开终端执行adb devices输出结果中有一行设备id且状态是device则连接成功。状态为unauthorized时需要重新插拔数据线并在手机上确认授权。状态为offline时尝试先执行adb kill-server再adb start-server。终端找不到adb命令是因为platform-tools没有加入环境变量。Windows下在“系统环境变量 - Path”中添加Android SDK路径\platform-toolsmacOS/Linux在shell配置文件中加一行export PATH$PATH:$HOME/Library/Android/sdk/platform-tools真机调试时手指点击的精度不如鼠标容易误触到相邻格子。可以在item_number.xml里给TextView设置android:minHeight64dp让可点击区域更大。4.2 Gradle构建慢、AGP版本过高、SDK无法勾选的处理方式新手在Android Studio中遇到的大多数构建问题都能归为三类。第一类是Gradle下载慢。即使仓库换成镜像gradle-wrapper发行版本身还是从官方地址下载。修改gradle/wrapper/gradle-wrapper.propertiesdistributionUrlhttps\://mirrors.cloud.tencent.com/gradle/gradle-8.7-bin.zip这里版本号要和项目使用版本一致。改完以后在Android Studio里执行一次Sync Project后续构建会明显变快。第二类是AGP版本过高或过低。热词里的“android studio agp 9”提醒我们新版AGP对Gradle版本有强制要求。如果项目的build.gradle.kts里声明了com.android.application版本而Gradle发行版不匹配同步时会报错“Minimum supported Gradle version is X”。最简单的做法是参照Android Studio新建一个空项目把新项目的AGP版本和Gradle版本号复制过来。下面是常见的构建问题对照表报错现象触发场景常用解法Minimum supported Gradle version is XAGP与Gradle版本不匹配修改distributionUrl和AGP插件版本tag number over 30 is not supported构建中间产物文件tag超限Clean Project后Rebuild提升minSdkVersion到21以上SDK location not foundSDK路径被修改或不存在Project Structure重选SDK目录Gradle project sync failed镜像切换后缓存残留File - Invalidate Caches and Restart第三类是SDK Manager无法勾选。常见原因是JDK版本不匹配。Android Studio从2021年开始默认内置JBR在Project Structure - SDK Location - JDK Location里选择“Embedded JDK”即可。另一个原因是SDK目录缺少写权限Windows下给用户完全控制权限macOS下把SDK目录从系统保护目录挪到自己用户目录下。4.3 胜利弹窗与重新开始完善课程设计交互游戏拼好后要给出明确的成功反馈。先判断胜利当所有数字块顺序正确且空格在右下角时视为完成public boolean isWin() { for (int i 0; i size; i) { for (int j 0; j size; j) { int expect i * size j 1; if (i size - 1 j size - 1) { if (board[i][j] ! 0) return false; } else if (board[i][j] ! expect) { return false; } } } return true; }胜利后停止计时弹出对话框private void checkWin() { if (!board.isWin()) return; handler.removeCallbacks(timerRunnable); new AlertDialog.Builder(this) .setTitle(拼图完成) .setMessage(步数 steps) .setPositiveButton(再来一局, (dialog, which) - restart()) .setNegativeButton(退出, (dialog, which) - finish()) .setCancelable(false) .show(); }restart()中重新生成有解棋盘、重置步数为0、更新Adapter并重新调用startTimer。注意在启动新线程之前保证旧的Runnable已经被移除否则两个计时器同时运行界面时间会双倍速度刷新。Activity每次从后台恢复时也可以在这里做一次时间校准。5. 课程设计拿高分的三个加分技巧5.1 给华容道加滑动动画替换瞬间跳变基础版点击后数字块直接跳到空格位置功能完整但视觉效果生硬。可以给itemView加translationX和translationY动画块移动的距离正好是一个格子的宽度。在点击事件中拿到view参数根据点击位置和空格的位置差计算偏移View targetView view; float cellWidth gridView.getColumnWidth() gridView.getHorizontalSpacing(); float offsetX (col - emptyCol) * cellWidth; float offsetY (row - emptyRow) * cellWidth; ObjectAnimator animX ObjectAnimator.ofFloat(targetView, translationX, 0, -offsetX); ObjectAnimator animY ObjectAnimator.ofFloat(targetView, translationY, 0, -offsetY); AnimatorSet set new AnimatorSet(); set.setDuration(120); set.playTogether(animX, animY); set.start();注意动画结束后GridView会在notifyDataSetChanged时重建item重建后translation会被重置所以视觉上看起来是数字块滑过去后再刷新到新位置顺序需要调试。课程设计里不必做得太复杂动画时长控制在150毫秒内最自然。5.2 用SharedPreferences保存通关记录评审老师会关注数据持久化能力。通关后把当前步数、耗时和日期存到SharedPreferencesSharedPreferences sp getSharedPreferences(record, MODE_PRIVATE); int bestSteps sp.getInt(best_steps, Integer.MAX_VALUE); if (steps bestSteps) { sp.edit().putInt(best_steps, steps).apply(); } long bestTime sp.getLong(best_time, Long.MAX_VALUE); if (elapsed bestTime) { sp.edit().putLong(best_time, elapsed).apply(); }这里只在成绩更好时才更新记录保证“历史最佳”不被普通成绩覆盖。下次启动时读取并显示在界面上报告里写“基于SharedPreferences实现本地最高分记录”是很实用的功能点。5.3 多难度与自定义背景写进报告更有说服力华容道不只4x4支持3x3、5x5会让课程设计显得完整。实现时把PuzzleBoard里的size从常量改成构造参数所有循环、逆序数判断都基于size生成。需要留意当棋盘边长是偶数时有解性判断同时看逆序数和空格所在行当边长是奇数时只看逆序数奇偶即可。界面添加Spinner选择难度切换时重新初始化棋盘。数字块的样式可以通过drawable资源自定义例如圆角绿色块shape xmlns:androidhttp://schemas.android.com/apk/res/android android:shaperectangle solid android:color#4CAF50 / corners android:radius12dp / padding android:left8dp android:top8dp android:right8dp android:bottom8dp / /shape最后在AndroidManifest.xml中的Activity上加上android:screenOrientationportrait固定竖屏避免旋转屏幕导致棋盘状态丢失。如果还想处理旋转就在onSaveInstanceState里保存二维数组和步数再在onCreate中恢复。本文还有配套的精品资源点击获取