Android打地鼠课设:音效开关、自定义View与状态保存全解析

发布时间:2026/9/16 9:04:44

Android打地鼠课设:音效开关、自定义View与状态保存全解析 简介一份基于 Android Studio 的“打地鼠”课程设计完整工程面向 Android 初学者与正在完成移动开发课设的学生覆盖游戏交互、音效开关、界面布局等核心知识点。压缩包共 184 个文件约 24.97MB包含 13 个 Java 源码、39 个 XML 布局与配置、若干 WAV/MP3/Ogg 音频资源、可安装的 APK 文件以及一份课设报告 docx工程结构便于直接导入调试。已有 1860 人学习下载适合作为课设模板或 Android 入门实践参考。除了完整源码还提供打包好的 app-debug.apk可快速查看运行效果报告部分梳理了触摸事件监听、MediaPlayer/SoundPool 音频管理、SharedPreferences 保存用户偏好等实现思路并包含遇到问题时的排查方向帮助读者减少踩坑快速完成自己的版本。1. 做课设打地鼠之前先把音效开关当成一个正经设置来做课设题是“简单的打地鼠”但真正拉开分数差距的从来不是打地鼠本身。会用 Handler 延迟切图三小时就能让地鼠从洞里冒出来而“包括音效开关设置功能”这半句话才是评分老师区分“做了个作业”和“做了个完整小应用”的关键。很多人把开关做成一个 ImageButton点一下没声再点一下有声页面一退下次进来又恢复默认——这不叫设置叫静音按钮。这篇按一回完整课设走一遍先画洞口和地鼠再写随机出现的主循环然后把点击判定、计分和音效开关接到一起最后补上状态保存和答辩细节。语言用 JavaKotlin 只是语法差异目标就一个交上去能跑答辩时每个参数都讲得清。2. 打地鼠界面先行布局容器、洞口与地鼠绘制2.1 用 LinearLayout 定骨架三个“横向行”排开 9 个洞位打地鼠的界面只有两块顶部一条信息栏下面 3x3 的洞口网格。顶部用水平 LinearLayout 放分数、倒计时和音效开关下面用三层水平 LinearLayout每层放 3 个洞位。选这个方案而不是 ConstraintLayout 手排 9 个位置原因很直接洞位天然等宽等高LinearLayout 嵌套加 layout_weight 就能自动分配行列屏幕适配交给权重XML 写起来不容易错如果用 ConstraintLayout9 个洞每个至少 6 条约束屏幕宽度一变还有概率重叠。LinearLayout android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical LinearLayout android:layout_widthmatch_parent android:layout_height48dp android:orientationhorizontal android:gravitycenter_vertical TextView android:idid/scoreView android:layout_width0dp android:layout_heightwrap_content android:layout_weight1 android:text分数0 android:textSize18sp / TextView android:idid/timeView android:layout_width0dp android:layout_heightwrap_content android:layout_weight1 android:gravitycenter android:text60s android:textSize18sp / ImageButton android:idid/soundToggle android:layout_width40dp android:layout_height40dp android:layout_marginEnd12dp android:background?attr/selectableItemBackgroundBorderless android:contentDescription音效开关 / /LinearLayout LinearLayout android:layout_widthmatch_parent android:layout_height0dp android:layout_weight1 android:orientationhorizontal com.course.mole.MoleView android:idid/mole0 android:layout_width0dp android:layout_heightmatch_parent android:layout_weight1 / com.course.mole.MoleView android:idid/mole1 android:layout_width0dp android:layout_heightmatch_parent android:layout_weight1 / com.course.mole.MoleView android:idid/mole2 android:layout_width0dp android:layout_heightmatch_parent android:layout_weight1 / /LinearLayout !-- 第二行、第三行照抄id 分别从 mole3 到 mole8 -- /LinearLayout几个参数说明。信息栏三个子控件用 0dp 加 weight1 均分宽度分数、倒计时各占一份避免不同屏幕下挤在一起洞位的三行用外层 vertical LinearLayout 的 weight1 均分高度行内 3 个 MoleView 再用 weight1 均分宽度这样 9 个洞在任何分辨率下都会铺满可用区域。ImageButton 的 contentDescription 是给无障碍服务读的答辩被问“为什么有这个属性”时可以答出语义标注这一层。也可以用 GridLayout 或自定义一个 View 画 9 个洞三者的取舍如下表方案洞位排布成本屏幕适配课设评价ConstraintLayout 9 组约束每个洞至少 6 条约束XML 冗长中等工程量大容易约束冲突LinearLayout 嵌套 weight9 个洞行列权重自动均分好推荐零额外依赖自定义 View 里手算 9 个洞坐标要自己算洞心、热区差灵活但容易错不推荐2.2 用自定义 View 画地鼠不依赖图片资源课设最常翻车的点之一是用了网上下载的地鼠 PNG结果路径写错或图片放大后糊成一团。自己画一个并不难核心就是三层椭圆洞的深色内圈、地鼠身体、耳朵和眼睛。代码如下足够撑起一场十分钟答辩。public class MoleView extends View { public static final int STATE_HOLE 0; public static final int STATE_MOLE 1; private int state STATE_HOLE; private final Paint paint new Paint(Paint.ANTI_ALIAS_FLAG); public MoleView(Context context, AttributeSet attrs) { super(context, attrs); setClickable(true); } public void setState(int state) { this.state state; invalidate(); } public int getState() { return state; } Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); float w getWidth(); float h getHeight(); float cx w / 2f; float cy h / 2f h * 0.12f; paint.setColor(0xFF6D4C41); canvas.drawOval(cx - w * 0.42f, cy - h * 0.28f, cx w * 0.42f, cy h * 0.32f, paint); paint.setColor(0xFF3E2723); canvas.drawOval(cx - w * 0.30f, cy - h * 0.18f, cx w * 0.30f, cy h * 0.26f, paint); if (state STATE_MOLE) { paint.setColor(0xFFA1887F); canvas.drawOval(cx - w * 0.26f, cy - h * 0.62f, cx w * 0.26f, cy h * 0.05f, paint); paint.setColor(0xFF8D6E63); canvas.drawCircle(cx - w * 0.20f, cy - h * 0.58f, w * 0.09f, paint); canvas.drawCircle(cx w * 0.20f, cy - h * 0.58f, w * 0.09f, paint); paint.setColor(0xFF212121); canvas.drawCircle(cx - w * 0.10f, cy - h * 0.42f, w * 0.025f, paint); canvas.drawCircle(cx w * 0.10f, cy - h * 0.42f, w * 0.025f, paint); } } }这段代码有两个关键设计。洞心 cy 取“略低于 View 中心”是为了让地鼠上半身露在洞外时在视觉上站得住而不是半截悬空。所有尺寸都用 w 和 h 的比例算不写死 dp屏幕缩放不会变形。setClickable(true) 必须写默认 View 的 clickable 是 false即使 setOnClickListener 能触发也会影响点击热区的稳定性。state 用 int 而不用 boolean是为了以后扩展“被打中的地鼠”第三态时只加一个常量不用改调用方。如果想让“打中”更有反馈可以加一个 STATE_HIT 态onDraw 里把身体颜色改成灰色再 setState 切回 STATE_HOLE。这个动作会和第 4 章的点击判定写在一起成本很低答辩时是一个很好的加分演示点。提示自定义 View 在 XML 预览里全黑先检查项目主题是不是深色背景或者在布局根节点加 android:background#F5F5F5。预览问题不影响真机运行。2.3 洞位坐标交给布局点击判定绑在控件上网格和绘制都定了洞位坐标不需要自己算。每个 MoleView 就是布局里的一个单元格setOnClickListener 直接绑在控件上点击坐标系是 View 自己的局部坐标。这比“用一个 SurfaceView 自己算 9 个洞的位置”省掉一整类 bug不同分辨率下洞心错位。后续读取“点了哪个洞”直接通过 view.getId() 或者遍历 9 个 MoleView 引用数组的下标判断两种方式都不涉及像素计算。3. 地鼠出没的主循环随机、Handler 与状态机3.1 一个 Handler 跑全场而不是三个 Timer 各管一段打地鼠的核心逻辑是“地鼠随机出现、停留一段时间、然后消失”。有些课设写成三个 Timer一个管出现一个管消失一个管倒计时。三个定时器的回调都挤在主线程执行顺序一旦错位就会出现“新地鼠刚出来就被上一个消失回调按回去”。我一般用一个 Handler 配一个 Runnable 维护出现节奏地鼠的消失再用一个隐藏 Runnable 按洞口保存引用换洞时先移除旧回调。public class GameController { private final Handler handler new Handler(Looper.getMainLooper()); private final Random random new Random(); private MoleView[] moles; private Runnable hideTask; private int visibleIndex -1; private long minDelay 450; private long currentDelay 1100; public void start() { hideTask null; currentDelay 1100; scheduleNext(); } private void scheduleNext() { handler.postDelayed(this::showRandomMole, currentDelay); } private void showRandomMole() { if (visibleIndex 0) { moles[visibleIndex].setState(MoleView.STATE_HOLE); } int index random.nextInt(moles.length); moles[index].setState(MoleView.STATE_MOLE); visibleIndex index; long stayTime Math.max(450, 900 - currentDelay / 2); hideTask () - { moles[index].setState(MoleView.STATE_HOLE); visibleIndex -1; scheduleNext(); }; handler.postDelayed(hideTask, stayTime); } }这段代码保证了整个出现链路是串行的showRandomMole 里先隐藏上一只再随机选新洞然后注册 hideTaskhideTask 执行时再调度下一次出现。因此不会出现两个洞口同时处于“地鼠状态”。换洞时上一只地鼠直接 setState 关掉不用等它的 hideTask 执行也不会残留“换了洞旧洞的隐藏回调把新地鼠盖住”的隐患。参数上currentDelay 初始 1100 毫秒对应一颗新地鼠约 1.1 秒出现一次手速正常的人能反应过来minDelay 下限 450 毫秒防止后期快到完全点不中。stayTime 用900 - currentDelay / 2当前间隔越短地鼠停留越短难度从“出现频率”和“停留时长”两个维度同时爬升。答辩时可以直接改这个系数演示手感变化。如果项目用的 minSdk 不支持 lambda把this::showRandomMole和箭头改成匿名 Runnable 即可逻辑不变。3.2 用 GameState 管住运行、暂停、结束只靠 Runnable 控制流程会有个问题Activity 的 onPause 发生时Handler 里的回调还在跑地鼠还在变但用户已经切到别的应用。标准做法是定义一个状态枚举所有回调执行前先检查状态。四种状态是 IDLE、RUNNING、PAUSED、OVER单向推进不能回退。public enum GameState { IDLE, RUNNING, PAUSED, OVER } public void pause() { if (gameState GameState.RUNNING) { gameState GameState.PAUSED; handler.removeCallbacksAndMessages(null); if (visibleIndex 0) { moles[visibleIndex].setState(MoleView.STATE_HOLE); visibleIndex -1; } } } public void resume() { if (gameState GameState.PAUSED) { gameState GameState.RUNNING; scheduleNext(); } }pause 里removeCallbacksAndMessages(null)是清理整条队列。这里不能只 removeCallbacks(hideTask)因为“下一次出现”的 Runnable 你并没有保存引用统一清空消息再靠状态机恢复是最干净的做法。最容易写错的是把 OVER 当成 PAUSED导致倒计时归零后还能继续点地鼠加分。倒计时结束的判定顺序要固定先置 OVER再 removeCallbacks最后关掉全部洞口。顺序反了会出现分数已停但地鼠还在冒的现象。提示Handler 的非静态内部类会隐式持有外层 Activity 引用。课设可以不写复杂的弱引用封装但务必在 onDestroy 里调用 removeCallbacksAndMessages(null)养成习惯。3.3 难度参数表出现间隔、停留时长与倒计时联动倒计时不建议单独再起 CountDownTimer可以让每秒 Runnable 与出现 Runnable 共用同一个 Handler两条消息仍排在一条队列里onPause 时一起被清掉。难度参数集中放在控制器顶部答辩现场调参也方便参数初始值变化规则效果currentDelay1100ms每次命中减 50ms下限 450ms控制整体节奏stayTime约 650ms随 currentDelay 缩短自动下降后期地鼠更难打游戏总时长60s固定结束后置 OVER便于演示4. 点击判定与音效开关SoundPool、持久化与即时反馈4.1 音效组件选型这题用 SoundPool不用 MediaPlayer打地鼠的音频需求是“短促、低延迟、允许并发”三个关键词对应 Android 不同的音频组件。MediaPlayer 每次 play 要重新 prepare几百毫秒延迟会让“点击”和“发声”脱节SoundPool 初始化时把音频一次性解码进内存play 基本零开销。背景音乐可以继续用 MediaPlayer音效用 SoundPool两条路径互不干扰但开关要共用一个。组件延迟并发课设评价MediaPlayer高启动慢不支持播短音效有明显延迟不推荐SoundPool低预加载支持多流首选代码量小AudioTrack极低需自己混音绕远路没必要音效文件放 res/raw 目录命名只能用小写字母和下划线建议格式用 wav 或 ogg体积控制在 100KB 以内。没有现成素材可以先放一个空文件占位逻辑跑通后再替换。4.2 开关要做成设置项SharedPreferences 持久化 图标即时切换标题里“音效开关设置功能”要拆成两层开关是 UI设置是持久化。只做 UI 不做持久化就是第 1 章说的“静音按钮”。完整做法是点击时翻转 SoundManager.enabled 并写进 SharedPreferences同时立刻换图标启动游戏时从 Preferences 读回状态。public class SoundManager { private final SoundPool pool; private final Context context; private boolean enabled; private int hitId; private int missId; public SoundManager(Context context) { this.context context.getApplicationContext(); pool new SoundPool.Builder().setMaxStreams(2).build(); hitId pool.load(this.context, R.raw.hit, 1); missId pool.load(this.context, R.raw.miss, 1); enabled prefs().getBoolean(sound_enabled, true); } public void toggle() { enabled !enabled; prefs().edit().putBoolean(sound_enabled, enabled).apply(); } public void playHit() { play(hitId); } public void playMiss() { play(missId); } private void play(int soundId) { if (!enabled) { return; } pool.play(soundId, 1f, 1f, 1, 0, 1f); } private SharedPreferences prefs() { return context.getSharedPreferences(game_setting, Context.MODE_PRIVATE); } }几个参数容易讲不清setMaxStreams(2) 表示最多混两条音效流打地鼠场景同时发声最多是“打中 打空”两条设大了占内存设小了连续命中时后面的音效会被丢。pool.play 的六个参数依次是音效 id、左音量、右音量、优先级、循环次数、播放速率循环填 0 表示播一遍速率 1f 是正常速度。把速率改成 1.5f 能让音调变高做一个“暴击音效”答辩时可以现场切参数演示。开关关闭时直接 return而不是把音量设成 0音量设 0 意味着每次 play 仍在驱动底层音频管线重新打开开关时要记得重置音量容易漏状态。直接拦在入口语义最清晰。点击判定要与音效联动mole.setOnClickListener(v - { if (gameState ! GameState.RUNNING) { return; } if (mole.getState() MoleView.STATE_MOLE) { score 10; scoreView.setText(分数 score); soundManager.playHit(); mole.setState(MoleView.STATE_HOLE); } else { soundManager.playMiss(); } });判定必须读 MoleView 的 state而不是“点了洞就是打中”因为地鼠可能刚好在这一帧被隐藏回调收回去点到的只是个空洞口。分数只在 RUNNING 状态累加避免倒计时结束后继续加分。图标切换放在同一个 OnClickListener 里实现“声音和图标同步翻转”。4.3 第一声没音效SoundPool 异步加载的预加载方案SoundPool.load 是异步的音频还在解码时 play 会被忽略表现就是“游戏开始的前一两声没音效”。demo 演示时最尴尬的就是第一次点击没声评委以为音效是坏的。常见做法是进入页面时加一个预加载监听。public void prepare() { pool.setOnLoadCompleteListener((pool, sampleId, status) - { // 加载完成后什么都不用做load 成功 play 就有效 }); }设置这个监听本身不触发加载真正的加载在构造 SoundPool 时已经调用 pool.loadOnLoadCompleteListener 只是通知“现在可以播了”。如果还是缺第一声可以在游戏开始倒计时 3、2、1 的那一秒静默调用一次 playHit让音频管道先热起来。两种方案选一个演示时第一声就能稳定出来。5. 交课设前的加固状态保存、编码与演示细节5.1 屏幕旋转不丢分数onSaveInstanceState 补全三个字段打地鼠课设默认竖屏能跑但把模拟器旋转 90 度后 Activity 重建分数清零、倒计时从头开始主循环还因为 onPause 里的 removeCallbacks 停摆。能撑住旋转的课设至少要保存三个字段字段类型说明scoreint当前分数旋转后原样显示timeRemainingint剩余秒数不能从 60 重来gameStateString旋转瞬间是 RUNNING 还是 PAUSEDOverride protected void onSaveInstanceState(Bundle outState) { outState.putInt(score, score); outState.putInt(time, timeRemaining); outState.putString(state, gameState.name()); super.onSaveInstanceState(outState); } Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); score savedInstanceState.getInt(score, 0); timeRemaining savedInstanceState.getInt(time, 60); String state savedInstanceState.getString(state); if (state ! null) { gameState GameState.valueOf(state); } }恢复时机在 onStart 之后、onResume 之前如果恢复后发现地鼠出现间隔错乱把 resume() 挪到 onResume 里再调用或者用 handler.post 包一层等界面绘制完成再恢复调度。5.2 演示环境检查编码、模拟器与音频驱动提交课设经常出问题的不在游戏逻辑而在资源和环境。Java 文件带中文注释换电脑打开变乱码检查 File 菜单里的 File Encoding 是否为 UTF-8。模拟器优先挑不带 Google Play 字样的系统镜像这类镜像开硬件加速后资源占用更低笔记本演示选 1080p 分辨率机型。Windows 上模拟器偶尔会用默认声卡驱动导致音效延迟 300 毫秒以上真机演示这一项最稳。5.3 低成本加两个提分功能命中率与连击奖励功能稳定后可以在现有状态机上加两个低成本提分点。一是在 STATE_HOLE 被点击时触发 playMiss同时记录脱靶次数结束界面上显示命中率二是连续命中三次后每次成功额外加 5 分miss 清空连击。这两项都复用已有的 MoleView 状态和 SoundManager 接口不用改主循环。注意连击计数在 pause 之后要清零否则用户切后台回来带着连击继续加分逻辑上讲不通。把 onSaveInstanceState 三个字段补齐旋转屏幕分数和剩余时间都不丢这一处至少能让答辩少一次翻车把命中率显示出来则是从“会写循环”到“会设计游戏平衡”的可见跃迁。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/9/16 9:04:44

Git常用命令实战:从代码提交到版本回退的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/16 9:04:44

对抗性时尚:用服装干扰AI视觉监控的物理层防御

1. 项目概述:当时尚成为对抗算法凝视的战术装备“Adversarial Fashion Makes a Statement on AI Panopticon”——这个标题乍看像一场艺术展的策展名,实则是一次技术、社会学与身体政治的精密合谋。它不是在讲AI如何设计衣服,而是在说&#x…

2026/9/16 8:59:43

SSM+Vue煤矿安全管理信息系统设计与实现

1. 项目背景与需求分析煤矿安全管理信息系统是煤炭行业数字化转型的重要组成部分,旨在通过信息化手段提升煤矿安全生产管理水平。2026届毕业设计选择"SSMVue煤矿安全管理信息系统"作为课题,反映了当前煤炭行业对智能化安全管理的迫切需求。煤矿…

2026/9/16 9:59:52

FPGA驱动超声波与激光测距:从状态机到时序约束的完整设计

简介:这是一个将激光测距与超声波测距驱动整合于同一FPGA工程的驱动资源,面向FPGA开发学习者和电子设计竞赛参赛者。工程支持按键切换,将不同传感器测距结果显示在数码管上,配套4段实操演示视频,涵盖0~4米、…

2026/9/16 9:59:52

Q-dex掌上宝可梦图鉴:Arduino UNO Q实现离线AI识别

1. 项目概述:这不是玩具,是嵌入式AI在掌心的第一次呼吸Q-dex — A Real Handheld Pokdex with On-Device AI,光看标题就让人手指发痒。它不是用手机App模拟的电子图鉴,也不是3D打印外壳里塞块树莓派的“概念验证”,而是…

2026/9/16 9:59:52

AI辅助年终总结PPT工具评测与使用技巧

1. 为什么需要AI辅助的年终总结PPT工具每到年底,职场人最头疼的就是年终总结汇报。传统制作方式需要经历数据收集、内容整理、设计排版等多个环节,往往耗费大量时间却难以达到理想效果。我经历过连续熬夜三天改PPT的痛苦,也见过同事因为汇报效…

2026/9/16 9:59:52

车载制动安全预检系统:硬件级信号验证与ASIL-B级设计

1. 这不是“智能刹车检测仪”,而是一套可量产的车载安全前置验证系统你可能在短视频里见过那种“按一下按钮,车就自己检查刹车”的演示——灯亮、蜂鸣、手机弹出“Brake OK”字样。但真正跑在实车上、能通过ISO 26262 ASIL-B级功能安全预审、且成本压进3…

2026/9/16 9:59:52

OpenMontage:开源Agentic协作框架深度解析

1. OpenMontage 不是视频剪辑软件,而是一个被严重误读的开源智能体协作框架最近在多个技术社区和开发者群聊里,频繁看到有人问“OpenMontage下载后如何使用”,甚至有用户把它和Premiere、DaVinci Resolve混为一谈,搜“openmontage…

2026/9/16 9:54:51

STM32 GPIO驱动NMOS管:气泵与电磁阀开关量控制方案详解

简介:一份面向STM32F103RCT6单片机的工程包,演示如何控制气泵与电磁阀的开关,适合嵌入式初学者、单片机开发者和工业控制项目人员。方案从继电器与MOS管对比入手,说明继电器动作有噪声且体积较大,MOS管更合适&#xff…

2026/9/15 4:54:30

拯救者Y7000黑屏故障排查与维修实战指南

1. 项目概述:一台黑屏的拯救者Y7000,到底卡在哪一步? 联想拯救者Y7000系列笔记本,从2018年第一代搭载i5-8300H开始,到后来的i7-9750H、i7-10750H、i5-11400H,再到2023年款的R7-7840HS,它始终是学…

2026/9/16 0:04:09

PHP源码部署实战:从环境配置到运行情侣游戏全攻略

简介:这是一套面向情侣互动场景的PHP完整源码,集成情侣飞行棋、真心话大冒险、情趣骰子等玩法,并内置完整分销制度,可自定义多种返佣比例,源码完全开源无加密,支持微信无感自动授权登录与第三方授权&#x…

2026/9/15 14:22:53

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/15 21:31:11

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/15 11:42:23

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码