Android华容道开发实战:Java状态管理与动态UI实现
发布时间:2026/10/1 9:11:34来源:尧图网络
简介本资源是一份面向Java初学者与HarmonyOS应用开发入门者的完整项目实践素材聚焦数字图片华容道益智游戏的移动端实现解决从环境搭建、逻辑设计到UI交互的全流程开发问题。压缩包共38个文件含9个核心Java源码实现游戏状态管理、滑动逻辑与胜负判定、6个XML布局与配置文件定义页面结构与权限、6个PNG/JPG图片资源含游戏图块与界面图标以及Gradle构建脚本、ProGuard混淆配置等工程必需文件整体大小5.01MB结构规范适合作为DeveCo StudioJava双技术栈的实训参考。已有409人学习下载资源提供可直接运行的完整项目工程包含清晰的目录组织如entry/src/main/java与res路径、典型HarmonyOS UI组件调用示例及游戏算法关键注释助读者快速理解二维数组棋盘建模、空格位置追踪、合法移动校验等核心实现逻辑。1. 数字图片华容道不是玩具是移动应用开发里检验 Java 基础、UI 控制力与状态管理能力的“压力测试场”你可能在技能大赛现场见过它一块 4×4 的网格15 张带编号的小图块加一个空格拖动相邻图块滑入空位最终按数字顺序或原图还原。表面看是怀旧小游戏但落到移动应用开发实操中它是一套极简却全量覆盖核心链路的验证载体——从 Deveco Studio 工程搭建、Java 类设计含状态封装、移动逻辑、胜利判定、XML 布局动态生成、触摸事件拦截与手势响应到 Activity 生命周期管理、资源加载优化甚至后续扩展的计时、步数统计、存档恢复全部能塞进这个小框架里锤炼。它不依赖网络、不调用复杂 SDK却能把 Java 面向对象设计比如 Tile 类的封装、Board 状态机的抽象、Android 视图树操作ViewGroup 动态 addView/removeView、事件分发机制onTouchEvent vs GestureDetector这些“看不见但天天踩坑”的点逼你亲手写出来、调通、再重构。尤其对广东高职移动应用开发赛项备赛者、刚学完 Java 基础想落地第一个完整 App 的同学它比“Hello World”真实比“计算器”有深度是少有的、能让你在 3 天内从“会写语法”变成“会控 UI、懂状态、能 debug”的实战锚点。2. 用 Deveco Studio 搭建 Java 工程避开模板陷阱手动配齐华容道所需的最小依赖集Deveco Studio 默认新建 Java 项目时常默认勾选“Empty Ability”模板看似干净实则埋了两个坑一是 Activity 主类继承自Ability鸿蒙 API而非 Android 的AppCompatActivity二是 Gradle 依赖默认指向ohos-sdk而非 AndroidX 兼容库。华容道作为标准 Android 应用非鸿蒙原生必须切回 Android 开发路径。这不是版本问题而是开发范式错位——强行用鸿蒙模板写 Android 逻辑后期连findViewById都会报红。2.1 创建纯 Android Java 工程从 New Project 对话框开始就选对根目录提示Deveco Studio 2.4 版本已支持 Android 模板但需手动开启。不要用“HarmonyOS”分类下的任何选项。启动 Deveco Studio →File → New → New Project在弹窗中左侧选择Android分类非 HarmonyOS 或 Other右侧模板选Empty Activity注意不是 Empty Ability填写包名如com.example.slidingpuzzleMinimum SDK 设为 API 21Android 5.0—— 华容道无需高版本特性设低可覆盖更多设备关键一步取消勾选 Use androidx.artifacts 下方的 Include Kotlin support*我们只用 Java点击 Finish等待 Gradle 同步完成同步成功后检查app/build.gradle文件顶部是否含apply plugin: com.android.application且dependencies块中存在implementation androidx.appcompat:appcompat:1.6.1—— 这是后续使用AppCompatActivity和 Material 组件的基础。2.2 替换默认 Activity 为 Java 实现并确认继承链正确Deveco Studio 生成的MainActivity.java默认继承AppCompatActivity这是正确的。但需手动验证并清理冗余代码// app/src/main/java/com/example/slidingpuzzle/MainActivity.java package com.example.slidingpuzzle; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; public class MainActivity extends AppCompatActivity { Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 此处 layout 尚未创建先留空 } }✅ 正确点import androidx.appcompat.app.AppCompatActivity表明使用 AndroidX 兼容库❌ 删除点移除所有import ohos.*、import com.huawei.*相关导入Deveco Studio 有时残留⚠️ 注意setContentView(R.layout.activity_main)中的R.layout.activity_main是占位符下一章将替换为动态生成的华容道网格布局此处不提前创建 XML 文件避免静态布局与动态逻辑冲突。2.3 添加华容道专用依赖仅需 1 个轻量级工具库应对图片加载华容道需加载本地图片并切割为 151 个小图块。Android 原生BitmapFactory可完成但易 OOM内存溢出。推荐引入glide—— 它不是“必须”但能规避新手最常翻车的图片缩放崩溃问题在app/build.gradle的dependencies块末尾添加implementation com.github.bumptech.glide:glide:4.16.0 annotationProcessor com.github.bumptech.glide:compiler:4.16.0参数说明glide:4.16.0是当前稳定版2024 年主流项目采用兼容 API 21annotationProcessor用于编译期生成 GlideModule提升运行时性能。不要用 5.x 版本——其 API 改动大与 Deveco Studio 内置 Gradle 插件兼容性差易触发NoClassDefFoundError。同步 Gradle 后在AndroidManifest.xml的application标签下追加权限声明虽本地图片无需网络但 Glide 内部可能触发存储访问application android:usesCleartextTraffictrue ... !-- 其他配置 -- /application此配置仅用于调试阶段正式发布需移除或改用 HTTPS 资源。3. Java 核心逻辑实现从 Tile 类封装到 Board 状态机把“滑动规则”写成可测试的 POJO华容道的业务逻辑本质是一个二维数组记录图块位置一个空格坐标标记可移动区域每次拖动只允许与空格相邻的图块交换位置。把它拆成 Java 类不是炫技而是为后续扩展如撤销、AI 求解、难度配置打基础。我一般会建三个类Tile单个图块、Board整个棋盘状态、PuzzleManager游戏控制器。它们之间无 Android SDK 依赖纯 Java可直接单元测试。3.1 Tile 类用 final 字段锁死不可变性用 toString() 辅助 debug每个图块需携带唯一 ID0~150 表示空格、原始图片资源 ID、当前行列坐标。关键约束ID 不可变坐标可变空格 ID 固定为 0。// app/src/main/java/com/example/slidingpuzzle/model/Tile.java package com.example.slidingpuzzle.model; public class Tile { public final int id; // 图块唯一标识0空格1~15数字图块 public final int resourceId; // 对应图片资源ID如 R.drawable.tile_1 public int row; // 当前行0~3 public int col; // 当前列0~3 public Tile(int id, int resourceId, int row, int col) { this.id id; this.resourceId resourceId; this.row row; this.col col; } Override public String toString() { return String.format(Tile{id%d, pos(%d,%d), res%d}, id, row, col, resourceId); } }✅final id和final resourceId保证图块身份不变避免因误赋值导致状态错乱⚠️row/col不设为 final因滑动时需实时更新坐标但必须通过Board统一修改禁止外部直接赋值下一节用 package-private 控制toString()是血泪经验Logcat 打印board.getTiles()时一眼看清每个图块在哪比断点单步快 10 倍。3.2 Board 类用二维数组 空格坐标实现 O(1) 移动判断封装所有游戏规则Board是状态核心。它持有Tile[4][4]数组和emptyRow/emptyCol坐标所有移动逻辑在此闭环// app/src/main/java/com/example/slidingpuzzle/model/Board.java package com.example.slidingpuzzle.model; import java.util.Random; public class Board { private final Tile[][] tiles; // 4x4 图块数组 private int emptyRow; // 空格行坐标 private int emptyCol; // 空格列坐标 public Board() { this.tiles new Tile[4][4]; initSolvedState(); // 初始化为终局状态1~15 顺序排列 shuffle(100); // 随机打乱100步确保可解 } private void initSolvedState() { int id 1; for (int i 0; i 4; i) { for (int j 0; j 4; j) { if (i 3 j 3) { // 右下角设为空格 tiles[i][j] new Tile(0, 0, i, j); emptyRow i; emptyCol j; } else { // 其他位置按顺序填 1~15 int resId getResourceId(id); tiles[i][j] new Tile(id, resId, i, j); id; } } } } // 根据图块ID返回对应图片资源ID实际项目中可从 assets 加载 private int getResourceId(int id) { // 示例tile_1 ~ tile_15 对应 R.drawable.tile_1 ~ R.drawable.tile_15 return switch (id) { case 1 - R.drawable.tile_1; case 2 - R.drawable.tile_2; // ... case 3~14 case 15 - R.drawable.tile_15; default - R.drawable.tile_blank; }; } // 执行一次滑动若 (row, col) 与空格相邻则交换 public boolean move(int row, int col) { // 检查是否越界 if (row 0 || row 3 || col 0 || col 3) return false; // 检查是否为空格自身 if (tiles[row][col].id 0) return false; // 检查是否与空格相邻曼哈顿距离为1 int distance Math.abs(row - emptyRow) Math.abs(col - emptyCol); if (distance ! 1) return false; // 交换图块与空格 Tile temp tiles[row][col]; tiles[row][col] tiles[emptyRow][emptyCol]; tiles[emptyRow][emptyCol] temp; // 更新空格坐标 emptyRow row; emptyCol col; // 更新被移动图块的坐标 temp.row emptyRow; temp.col emptyCol; tiles[emptyRow][emptyCol].row row; tiles[emptyRow][emptyCol].col col; return true; } // 判断是否胜利除空格外其余图块ID按行优先顺序排列 public boolean isSolved() { int expectedId 1; for (int i 0; i 4; i) { for (int j 0; j 4; j) { if (i 3 j 3) continue; // 跳过空格 if (tiles[i][j].id ! expectedId) return false; expectedId; } } return true; } // 获取指定位置图块供 UI 层调用 public Tile getTile(int row, int col) { return tiles[row][col]; } // 随机打乱确保可解只做合法移动 private void shuffle(int steps) { Random random new Random(); for (int i 0; i steps; i) { // 随机选一个与空格相邻的位置 int[] directions {-1, 0, 1, 0, -1}; // 上右下左 int dir random.nextInt(4); int newRow emptyRow directions[dir]; int newCol emptyCol directions[dir 1]; if (newRow 0 newRow 4 newCol 0 newCol 4) { move(newRow, newCol); } } } }✅move()方法用曼哈顿距离Math.abs(row - emptyRow) Math.abs(col - emptyCol) 1判断相邻比写四个 if 更简洁可靠⚠️shuffle()不用Collections.shuffle()—— 随机排列未必可解必须基于合法移动打乱否则用户永远赢不了isSolved()逻辑严格只检查 ID 顺序不依赖图片内容避免因资源加载失败导致误判。3.3 PuzzleManager解耦 UI 与逻辑暴露清晰接口给 ActivityPuzzleManager是桥梁它持有一个Board实例对外提供move(),isSolved(),getTile()等方法内部不碰 View不写findViewById// app/src/main/java/com/example/slidingpuzzle/controller/PuzzleManager.java package com.example.slidingpuzzle.controller; import com.example.slidingpuzzle.model.Board; import com.example.slidingpuzzle.model.Tile; public class PuzzleManager { private final Board board; public PuzzleManager() { this.board new Board(); } public boolean move(int row, int col) { return board.move(row, col); } public boolean isSolved() { return board.isSolved(); } public Tile getTile(int row, int col) { return board.getTile(row, col); } public void reset() { // 重新初始化 Board this.board new Board(); } }✅ 所有方法签名清晰Activity 只需调用manager.move(2,3)无需知道Board如何实现⚠️reset()方法重建Board实例而非调用initSolvedState()—— 避免状态残留这是新手常漏的“后悔药”。4. UI 层实现用 LinearLayout 动态生成网格用 GestureDetector 拦截拖动让 Java 逻辑真正驱动视图华容道 UI 的难点不在美观而在精准映射 Java 状态到 View并把用户触摸准确转为 Board 坐标。静态 XML 布局无法适配不同屏幕密度的图块尺寸而GridLayout在 Deveco Studio 中对android:layout_columnWeight支持不稳定。最佳实践是用 Java 代码动态创建LinearLayout嵌套结构每个图块用ImageView尺寸由代码计算触摸事件用GestureDetector统一处理。4.1 在 MainActivity 中动态构建 4×4 网格用 LinearLayout 嵌套实现像素级控制删除activity_main.xml在MainActivity.onCreate()中直接生成 UI// app/src/main/java/com/example/slidingpuzzle/MainActivity.java package com.example.slidingpuzzle; import androidx.appcompat.app.AppCompatActivity; import android.graphics.Color; import android.os.Bundle; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.Toast; import com.example.slidingpuzzle.controller.PuzzleManager; import com.example.slidingpuzzle.model.Tile; public class MainActivity extends AppCompatActivity { private PuzzleManager manager; private LinearLayout boardLayout; private ImageView[][] imageViews; // 缓存 ImageView 引用便于后续更新 private GestureDetector gestureDetector; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 1. 创建根布局垂直 LinearLayout LinearLayout rootLayout new LinearLayout(this); rootLayout.setOrientation(LinearLayout.VERTICAL); rootLayout.setBackgroundColor(Color.LTGRAY); // 2. 创建棋盘布局4行 LinearLayout boardLayout new LinearLayout(this); boardLayout.setOrientation(LinearLayout.VERTICAL); LinearLayout.LayoutParams boardParams new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f); // 占满剩余高度 boardLayout.setLayoutParams(boardParams); // 3. 初始化 PuzzleManager manager new PuzzleManager(); // 4. 动态生成 4×4 网格 imageViews new ImageView[4][4]; int size calculateTileSize(); // 计算单个图块宽高见下文 for (int i 0; i 4; i) { LinearLayout rowLayout new LinearLayout(this); rowLayout.setOrientation(LinearLayout.HORIZONTAL); LinearLayout.LayoutParams rowParams new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, size); rowLayout.setLayoutParams(rowParams); for (int j 0; j 4; j) { ImageView imageView new ImageView(this); LinearLayout.LayoutParams imgParams new LinearLayout.LayoutParams( size, size); imageView.setLayoutParams(imgParams); imageView.setScaleType(ImageView.ScaleType.CENTER_CROP); imageView.setPadding(2, 2, 2, 2); // 微调边距防粘连 // 设置点击监听备用主逻辑走 onTouch imageView.setOnClickListener(v - handleTileClick(i, j)); rowLayout.addView(imageView); imageViews[i][j] imageView; } boardLayout.addView(rowLayout); } // 5. 添加棋盘到根布局 rootLayout.addView(boardLayout); setContentView(rootLayout); // 6. 初始化手势检测器 gestureDetector new GestureDetector(this, new TileGestureListener()); updateUI(); // 首次渲染 } private int calculateTileSize() { // 获取屏幕宽度减去左右 padding除以4 int screenWidth getResources().getDisplayMetrics().widthPixels; return (screenWidth - 32) / 4; // 32 左右各16dp padding } private void updateUI() { // 遍历所有图块根据 Board 状态设置图片 for (int i 0; i 4; i) { for (int j 0; j 4; j) { Tile tile manager.getTile(i, j); if (tile.id 0) { imageViews[i][j].setImageResource(android.R.color.transparent); } else { imageViews[i][j].setImageResource(tile.resourceId); } } } } private void handleTileClick(int row, int col) { if (manager.move(row, col)) { updateUI(); if (manager.isSolved()) { Toast.makeText(this, 恭喜完成, Toast.LENGTH_SHORT).show(); } } } // 手势监听器见 4.2 节 private class TileGestureListener extends GestureDetector.SimpleOnGestureListener { Override public boolean onDown(MotionEvent e) { return true; // 必须返回 true 才能接收后续事件 } Override public boolean onSingleTapUp(MotionEvent e) { // 将触摸点转换为网格坐标 float x e.getX(); float y e.getY(); int size calculateTileSize(); int col (int) (x / size); int row (int) (y / size); if (row 0 row 4 col 0 col 4) { handleTileClick(row, col); } return true; } } }✅calculateTileSize()动态计算图块尺寸适配不同屏幕避免 XML 中 hardcodedp导致小屏溢出、大屏留白⚠️imageViews[i][j]二维数组缓存所有ImageViewupdateUI()中直接复用避免findViewById性能损耗onSingleTapUp()中用e.getX()/e.getY()除以size得到行列索引比MotionEvent.getActionIndex()更稳定后者在多指触控时易错。4.2 为什么不用 OnClickListener—— GestureDetector 是处理“点击即移动”的唯一可靠方案新手常犯错误给每个ImageView设置setOnClickListener认为“点哪就移哪”。但实际体验中用户手指稍有滑动onClick就不触发导致操作挫败。GestureDetector的onSingleTapUp在手指抬起瞬间触发且自动过滤滑动是 Android 官方推荐的点击识别方式。注意onDown()必须返回true否则onSingleTapUp()不会被回调。这是文档里没明说但必踩的坑。4.3 图片资源准备按命名规范放入 drawable-mdpi用 Glide 加载防 OOM将 15 张图块图片tile_1.png~tile_15.png和一张空白图tile_blank.png放入app/src/main/res/drawable-mdpi/。尺寸建议统一为200×200 px足够清晰又不撑爆内存。若需加载高清图如用户相册图片在updateUI()中替换setImageResource()为 Glide 加载// 替换原 updateUI() 中的 setImageResource 行 Glide.with(this) .load(tile.resourceId) // 或传 Uri .centerCrop() .into(imageViews[i][j]);✅centerCrop()保证图片填满ImageView且不失真⚠️Glide自动处理内存缓存和生命周期绑定Activity.onDestroy()时自动释放避免OutOfMemoryError。5. 避坑指南华容道开发中最常遇到的 4 个玄学问题与血泪解决方案华容道看似简单但每个环节都藏着让新手调试到凌晨的坑。以下是我在广东高职赛项指导中学生提交代码里出现频率最高的 4 类问题附带现象、根因和一招解决法5.1 现象App 启动黑屏 3 秒后才显示棋盘Logcat 无报错原因calculateTileSize()在onCreate()中调用但此时DisplayMetrics尚未完全初始化widthPixels返回 0导致size0ImageView宽高为 0所有图块不可见。解决将calculateTileSize()移至onWindowFocusChanged(true)中执行并重绘 UIOverride public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (hasFocus imageViews ! null) { int size calculateTileSize(); for (int i 0; i 4; i) { for (int j 0; j 4; j) { LinearLayout.LayoutParams params (LinearLayout.LayoutParams) imageViews[i][j].getLayoutParams(); params.width size; params.height size; imageViews[i][j].setLayoutParams(params); } } updateUI(); } }5.2 现象点击图块无反应handleTileClick()从不执行原因ImageView默认clickablefalse即使设置了setOnClickListeneronTouchEvent也不向下传递GestureDetector接收不到事件。解决在创建ImageView后显式设为可点击imageView.setClickable(true); // 关键 imageView.setOnClickListener(v - handleTileClick(i, j));5.3 现象打乱后永远无法还原isSolved()总返回 false原因shuffle()方法中随机方向数组directions {-1, 0, 1, 0, -1}写错dir1越界导致newRow/newCol计算错误空格坐标错乱move()实际未生效。解决用明确的四方向数组避免索引计算int[][] directions {{-1,0}, {0,1}, {1,0}, {0,-1}}; // 上右下左 int dir random.nextInt(4); int newRow emptyRow directions[dir][0]; int newCol emptyCol directions[dir][1];5.4 现象切换横竖屏后图块错位或imageViews数组空指针原因Activity旋转时默认销毁重建imageViews二维数组在onCreate()中重新创建但旧引用未清空updateUI()调用时部分ImageView为 null。解决在AndroidManifest.xml中锁定屏幕方向或重写onSaveInstanceState()保存状态!-- 在 AndroidManifest.xml 的 activity 标签内添加 -- android:screenOrientationportrait若必须支持横竖屏则需在onSaveInstanceState()中序列化Board状态在onCreate()中恢复 —— 但这超出华容道最小可行范围初学者优先用portrait锁定。6. 进阶技巧用 Java 反射注入自定义图片让华容道从“数字游戏”变成“个性化相册拼图”华容道的核心魅力在于可扩展性。当基础版跑通后最值得投入的进阶点不是加音效或动画而是让玩家用自己的照片生成图块。这不需要改 UI 或逻辑只需在Board类中增加一个loadFromBitmap()方法用 Java 反射绕过 Android 资源系统限制直接切割 Bitmap。这是 Java 基础反射、Bitmap 操作与 Android 实战的完美结合点。6.1 准备工作添加存储权限与文件选择器在AndroidManifest.xml中添加uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE / uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE /并在MainActivity中请求运行时权限API 23private static final int REQUEST_CODE_PERMISSION 1001; private void requestStoragePermission() { if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) ! PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, REQUEST_CODE_PERMISSION); } else { openImagePicker(); } }6.2 核心实现用反射调用Bitmap.createBitmap()切割图片注入 Board在Board类中新增方法需传入Bitmap和图块数量支持 3×3、4×4、5×5// app/src/main/java/com/example/slidingpuzzle/model/Board.java public void loadFromBitmap(Bitmap bitmap, int gridSize) { // 1. 确保 bitmap 尺寸可被 gridSize 整除 int size bitmap.getWidth() / gridSize; if (bitmap.getHeight() / gridSize ! size) { throw new IllegalArgumentException(Bitmap must be square and divisible by gridSize); } // 2. 创建新 tiles 数组 this.tiles new Tile[gridSize][gridSize]; // 3. 切割 bitmap 并生成 Tile int id 1; for (int i 0; i gridSize; i) { for (int j 0; j gridSize; j) { if (i gridSize - 1 j gridSize - 1) { // 最后一个作为空格 this.tiles[i][j] new Tile(0, 0, i, j); this.emptyRow i; this.emptyCol j; } else { // 切割子图 Bitmap subBitmap Bitmap.createBitmap(bitmap, j * size, i * size, size, size); // 将 Bitmap 转为资源 ID需反射 int resourceId createDrawableFromBitmap(subBitmap); this.tiles[i][j] new Tile(id, resourceId, i, j); id; } } } } // 反射生成 Drawable 资源 ID替代 R.drawable.xxx private int createDrawableFromBitmap(Bitmap bitmap) { try { // 获取 Resources 实例 Resources resources this.getClass().getClassLoader() .loadClass(android.content.res.Resources) .getDeclaredMethod(getSystem).invoke(null); // 调用 BitmapDrawable 构造函数 Class? bitmapDrawableClass Class.forName(android.graphics.drawable.BitmapDrawable); Object drawable bitmapDrawableClass.getConstructor(Resources.class, Bitmap.class) .newInstance(resources, bitmap); // 由于无法直接获取 ID此处返回一个临时标记 // 实际项目中可将 bitmap 保存到 cache 目录用 Uri 加载 return 0; // 占位符真实项目中用 Glide 加载 Uri } catch (Exception e) { e.printStackTrace(); return R.drawable.tile_blank; } }⚠️ 注意createDrawableFromBitmap()是简化示意。生产环境绝不应反射调用Resources.getSystem()—— 它返回的是系统资源无法访问 App 资源。真实做法是将切割后的Bitmap保存为临时文件用Uri.fromFile()传给 Glide// 替换 createDrawableFromBitmap() 为 private Uri saveBitmapToCache(Bitmap bitmap) { File cacheDir getApplicationContext().getCacheDir(); File file new File(cacheDir, puzzle_ System.currentTimeMillis() .png); try (FileOutputStream out new FileOutputStream(file)) { bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); } catch (IOException e) { e.printStackTrace(); } return Uri.fromFile(file); }然后在updateUI()中用 Glide 加载该Uri。6.3 验证效果三步完成“我的相册华容道”用户点击“选择图片” → 调用Intent.ACTION_PICK选取相册图片onActivityResult()中获取Uri→ 用BitmapFactory.decodeStream()加载为Bitmap调用board.loadFromBitmap(bitmap, 4)→updateUI()自动刷新此时16 张图块全是用户照片的局部空格在右下角滑动逻辑完全复用原有 Java 代码。没有新增 Activity没有重写 View只是把Board的输入源从R.drawable.xxx换成了Bitmap—— 这就是面向对象设计的力量依赖抽象Tile而非具体实现资源 ID。我带学生做这个功能时常强调一句华容道的终点不是“做完”而是“证明你能把任意图片喂给这套逻辑”。当你能用 Java 把一张 JPG 切成 16 块、再用move()方法控制它们滑动你就真正吃透了移动应用开发里最硬核的部分——状态与视图的精确映射。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网