
简介这份Android源码完整实现了经典“会说话的Tom猫”应用适合正在学习Android音频处理、多媒体播放或休闲游戏开发的初中级开发者。包内共16个文件均为Java源码压缩包仅18KB代码结构围绕主界面、录音管理、音频处理、UI交互与工具类展开可通过Android Studio导入研读。已有1256人学习下载说明其具有一定的参考价值。借助这份源码读者可以理清从麦克风录音、临时文件保存到音频变调处理及AudioTrack播放的完整链路同时了解MediaRecorder、JNI封装、线程调度和权限声明等关键环节描述中涉及音频处理线程、工具类及录音状态管理也为深入剖析实时音频处理提供了清晰的切入点。无论是作为毕业设计参考还是用于理解Android底层多媒体API与高频调用场景都是一份精简而典型的学习样本。 想当年“会说话的Tom猫”刚出来的时候几乎所有Android手机用户都装过它。点一下Tom的胳膊他会惨叫戳一下肚子他会笑得打滚对着麦克风说句话他会用可爱的变声复读出来。这个App在智能手机早期几乎是“互动娱乐”的代名词很多开发者也是因为它才萌生了“我也要做一个这样的App”的念头。这篇文章就基于“会说话的Tom猫 Android源码”这个主题从项目拆解、核心技术点、实现思路到一个可用的复刻版本该怎么做完整梳理一遍。无论你是刚学Android想找个练手项目的初学者还是想搞明白语音交互、动画驱动原理的开发者这篇内容都能给你一些可落地的参考。1. 项目本质与整体功能拆解1.1 表象是游戏本质是事件驱动型互动应用“会说话的Tom猫”看起来像个游戏但拆开看它本质上是一个事件驱动的互动娱乐应用。核心交互方式非常统一触摸屏幕触发事件Tom做出对应的动画和音效反馈按住麦克风说话Tom复读你的声音。这里面没有复杂的物理引擎、没有关卡设计全部的功能都围绕“用户输入→状态切换→表现反馈”这条链路运转。理解了这个本质你会发现复刻它并不需要高深的技术。用Android原生开发核心组件就三类触摸事件监听OnTouchListener、动画控制属性动画或帧动画、语音录制与播放MediaRecorder AudioTrack / SoundPool。难点不在某个单一技术而在如何把这三者流畅地组织起来让用户觉得Tom是“活”的而不是一个呆板的木偶。提示如果你打算用Unity或者Cocos来做复刻思路也完全一致只是把动画系统换成引擎自带的Animator把触摸监听换成引擎的Input系统。核心的事件驱动模型不变。1.2 功能清单与优先级划分从源码结构和用户可感知的功能出发一个标准的Tom猫App至少包含以下几个功能模块。我按开发优先级排了个序优先级功能模块核心作用涉及技术点P0触摸互动反馈点身体部位有对应反应惨叫、笑、晕多点触控、区域判断、帧动画P0语音复读变声录音后变调播放营造“Tom在说话”的错觉MediaRecorder录音、音高/速度变换P1动作循环播放闲置时自动做动作打呼噜、伸懒腰Handler定时任务、动画队列P1道具互动摸肚子、拍头等特殊交互坐标区域绑定、状态互斥P2背景场景切换切换不同房间背景View层级切换、资源管理先保证P0的两条主链路跑通App就已经“能玩”了。P1和P2是锦上添花。很多初学者一上来就想把所有功能都做全结果光素材就找了一星期代码一个字没写。我从实际经验出发强烈建议先做触摸惨叫语音复读这两条垂直链路跑通了再横向扩展其他功能。2. 核心技术点与方案选型解析2.1 动画驱动帧动画优于属性动画Tom猫的可爱大部分功劳要归给他那夸张又流畅的表情和肢体动作。我们看到的“会说话”效果本质上是一张张表情不同的静态图片连续播放形成的。这个场景下传统属性动画比如移动、缩放、旋转完全派不上用场因为Tom的动作不是规则的几何变换而是“张嘴、闭眼、抬手”这类非线性的形变。最合适的方案是Drawable动画帧动画。把一组按序列命名的图片放到res/drawable目录下用AnimationDrawable来播放。实现方式如下// Xml 中定义帧动画资源 res/drawable/tom_laugh.xml animation-list xmlns:androidhttp://schemas.android.com/apk/res/android android:oneshotfalse item android:drawabledrawable/tom_laugh_01 android:duration80/ item android:drawabledrawable/tom_laugh_02 android:duration80/ item android:drawabledrawable/tom_laugh_03 android:duration80/ /animation-list// 代码中控制 ImageView tomView findViewById(R.id.tom_view); tomView.setImageResource(R.drawable.tom_laugh); AnimationDrawable anim (AnimationDrawable) tomView.getDrawable(); anim.start();帧动画的优点在于实现简单、表情表现力强缺点也明显素材量巨大且容易OOM。一张1024x1024的PNG图片在无压缩的情况下占内存接近4MB一组20帧的动画就要80MB。所以复刻时通常会把图片压缩到512x512以内并且把动作控制在15帧左右。这一点在源码的资源目录里也能看到素材切图都是经过严格压缩的尽量复用同一套底图只替换嘴型和眼睛的局部贴图。2.2 语音复读录音变声并不复杂“复读”是Tom猫的灵魂功能。用户说“你好”Tom用搞笑的声音也说“你好”。从技术上拆解这个功能分两步录音然后变调播放。早期实现版本使用的是MediaRecorder录制成.amr或.mp4文件然后用SoundPool加载并用setPlaybackParams调整播放速率。这种方案实现快但音质损失严重而且不同手机上延迟差异很大。后来的开源实现里更多采用AudioRecord采集PCM裸流拿到原始音频数据后用**重叠相加算法WSOLA或者共振峰偏移Formant Shift**做变调最后用AudioTrack播放。这里有个很关键的产品设计点Tom的声音比原声“尖”且“快”所以变声处理时要把音调升高大约提升2到4个半音播放速度加快到1.2倍左右。这样就营造出一种“Tom在学你说话”的滑稽感。注意录音权限属于敏感权限。Android 6.0以上需要动态申请RECORD_AUDIO权限Android 12以上如果还想同时录制系统内部声音需要申请MediaProjection服务。复刻源码时这里是最容易出兼容性问题的位置后面我会单独说。2.3 交互区域判定用矩形还是用路径触摸Tom的不同部位反应不同。这就涉及到一个很基础的问题怎么判断用户点到了哪里最简单的方案是给Tom的每个部位创建一个透明View覆盖在Tom整体ImageView的上层。利用Android的触摸分发机制最上层的View优先响应事件。但这样做有个问题Tom是一张不规则图片透明View是矩形会出现“点在空白处也有反应”的违和感。更好的方案是自定义View在onTouchEvent里手动做坐标区域判定。比如Tom的头部中心坐标是(300, 200)头部半径是80dp当手指落点满足Math.sqrt((x-300)^2 (y-200)^2) 80时就判定为“摸头”。源码里一般用的是更灵活的多边形判定预先在资源文件里定义好各个热区的顶点坐标用射线法判断点是否在多边形内。这个方案会更加精准但代码复杂度高一些。对于复刻项目用圆形矩形组合判定已经够用了。3. 从零实现核心模块代码实战3.1 搭建项目骨架与资源目录规划新建Android Studio项目时包名建议用com.example.talkingcat。项目结构按照功能模块来分不要所有类都堆在一个包下app/src/main/java/com/example/talkingcat/ ├── activity/ // 主Activity和设置页面 ├── view/ // 自定义TomView核心绘制与触摸逻辑 ├── audio/ // 录音、变声、播放管理器 ├── anim/ // 动画管理器负责动作切换 └── util/ // 工具类素材目录也要清晰每个动作一个子目录res/drawable/ ├── tom_idle/ // 闲置动画帧 ├── tom_laugh/ // 大笑动画帧 ├── tom_scream/ // 惨叫动画帧 ├── tom_sleep/ // 睡觉动画帧 └── tom_talk/ // 说话动画帧嘴部动作序列好的资源规划能让你写代码时心情愉悦。我见过太多项目所有图片堆在一个drawable目录里命名还是1.png、2.png后期维护非常痛苦找一张图得挨个点开看。3.2 触摸互动模块的实现自定义TomView继承AppCompatImageView在onTouchEvent中处理交互。这里的关键设计是互斥状态机。Tom同一时间只能处于一个动作状态不能在惨叫的同时大笑否则动画会串台。public class TomView extends AppCompatImageView { private enum ActionState { IDLE, LAUGH, SCREAM, TALK, SLEEP } private ActionState currentState ActionState.IDLE; private AnimatorManager animManager; private AudioPlayer audioPlayer; public TomView(Context context, Nullable AttributeSet attrs) { super(context, attrs); animManager new AnimatorManager(this); audioPlayer new AudioPlayer(context); } Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() MotionEvent.ACTION_DOWN) { handleTouch(event.getX(), event.getY()); } return true; } private void handleTouch(float x, float y) { // 判断触摸点落在哪个热区 if (isInCircle(x, y, headX, headY, headRadius)) { triggerAction(ActionState.SCREAM, R.raw.scream); } else if (isInRect(x, y, bellyLeft, bellyTop, bellyRight, bellyBottom)) { triggerAction(ActionState.LAUGH, R.raw.laugh); } else { triggerAction(ActionState.IDLE, R.raw.giggle); } } private void triggerAction(ActionState state, int rawResId) { if (currentState state) { return; } // 同一动作不重复触发 currentState state; animManager.play(state); // 切换动画 audioPlayer.playEffect(rawResId); // 播放音效 // 动画结束后自动回到闲置状态用Handler做延时恢复 postDelayed(() - { currentState ActionState.IDLE; animManager.play(ActionState.IDLE); }, animManager.getDuration(state)); } }这段代码有两个细节值得注意。一是状态互斥通过currentState判断避免动作叠加。二是动画与音效的同步播放惨叫动画时同时播放惨叫音效并用同一个延时回调一起结束保证视觉上的同步感。3.3 语音录制变声与播放链路录音模块的核心类是AudioRecorderManager。为了避免把代码写得绕来绕去我把录制和播放封装成两个独立的方法startRecording()和stopRecordingAndPlay()。录制的核心参数配置private void startRecording() { // 44100Hz采样率、单声道、16位PCM兼容性最好的配置 int sampleRate 44100; int channelConfig AudioFormat.CHANNEL_IN_MONO; int audioFormat AudioFormat.ENCODING_PCM_16BIT; int bufferSize AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat); audioRecord new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRate, channelConfig, audioFormat, bufferSize); recordingBuffer new byte[bufferSize]; audioRecord.startRecording(); // 子线程持续读取录音数据写入PCM文件 executorService.execute(() - { FileOutputStream fos null; try { fos new FileOutputStream(pcmFile); while (isRecording) { int read audioRecord.read(recordingBuffer, 0, recordingBuffer.length); if (read 0) { fos.write(recordingBuffer, 0, read); } } } catch (IOException e) { e.printStackTrace(); } finally { // 关闭资源 } }); }停止录音后要做变声处理。最简单的实现是变调不变速用SoundPool的setPlaybackParams来变调或者直接用AudioTrack设置播放速率来同时变调变速。Tom猫的滑稽感来自“变调变速一起做”所以直接用AudioTrack更省事private void playRecordedVoice() { AudioTrack audioTrack new AudioTrack.Builder() .setAudioFormat(new AudioFormat.Builder() .setSampleRate(44100) .setEncoding(AudioFormat.ENCODING_PCM_16BIT) .setChannelMask(AudioFormat.CHANNEL_OUT_MONO) .build()) .setTransferMode(AudioTrack.MODE_STREAM) .build(); audioTrack.setPlaybackParams(new PlaybackParams() .setPitch(1.6f) // 音调升高60%声音变尖 .setSpeed(1.25f)); // 语速加快25%更显活泼 audioTrack.play(); // 从PCM文件读取数据并写入AudioTrack // 具体写入逻辑略注意使用流式写入避免内存溢出 }这里pitch1.6f和speed1.25f这两个参数是我实测后觉得效果最接近Tom猫原版听感的。pitch太高会变成花栗鼠叫太低则不够搞笑speed太快会听不清原词太慢又没有活泼感。实际开发时建议做成可调节项方便不同设备上微调。提示setPlaybackParams要求设备支持相关的音频处理能力。大多数中高端设备都支持但有部分低端设备忽略这个参数。备用方案是用Ringdroid库或者libsoundtouch这类开源变速变调库预先在录制的PCM数据上处理好音频再直接播放。3.4 闲置AI行为与动作调度Tom不会傻站着他会自己打哈欠、伸懒腰、摸摸尾巴。这个功能需要一个闲置动作调度器。我在源码里看到过不少方案最简单的就是Handler发延时消息private void scheduleIdleAction() { idleHandler.removeCallbacksAndMessages(null); idleHandler.postDelayed(() - { // 随机选择一个闲置动作 int rand new Random().nextInt(3); switch (rand) { case 0: triggerAction(ActionState.SLEEP, R.raw.snore); break; case 1: triggerAction(ActionState.LAUGH, R.raw.giggle); break; case 2: triggerAction(ActionState.IDLE, R.raw.yawn); break; } // 每次动作结束后重新调度下一次闲置动作 if (currentState ActionState.IDLE) { scheduleIdleAction(); } }, 5000 new Random().nextInt(5000)); // 5~10秒后触发 }这个逻辑虽然简单但是要注意一个陷阱闲置调度和用户触摸触发之间的竞争条件。假如用户正在摸Tom闲置调度器刚好触发了睡觉动作就会打断用户的交互。解决方法是每次执行闲置动作前检查currentState只有处于IDLE状态时才允许执行。4. 工程化细节与常见问题排查4.1 音频焦点与后台限制处理Tom猫App的播放场景比较特殊既要播放短音效点击惨叫又要播放长录音复读还可能在后台被用户切走。如果不做音频焦点管理用户正在听音乐时打开App就会出现音乐和Tom声音混杂的糟糕体验。Android的音频焦点机制AudioFocus是解决这个问题的标准方案。在启动录音前申请音频焦点录音结束或播放结束时释放AudioManager am (AudioManager) getSystemService(Context.AUDIO_SERVICE); AudioAttributes attrs new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .build(); AudioFocusRequest focusRequest new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT) .setAudioAttributes(attrs) .build(); int result am.requestAudioFocus(focusRequest); if (result AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { // 获得焦点开始录音 }录音结束后abandonAudioFocusRequest(focusRequest)释放焦点。实测下来不做音频焦点管理的话在微信语音播放或者音乐App播放时Tom的复读声会被系统直接掐断体验非常割裂。4.2 素材OOM与内存优化策略这是复刻汤姆猫项目里最容易踩的坑。帧动画如果直接把所有帧加载进内存在低端机上一秒就崩。我也遇到过类似的情况切到大笑动作时App直接黑屏退出Logcat里全是OutOfMemoryError。三个有效的优化手段第一压缩素材体积。把图片从ARGB_8888降到RGB_565内存占用直接减半。在BitmapFactory解码时设置BitmapFactory.Options options new BitmapFactory.Options(); options.inPreferredConfig Bitmap.Config.RGB_565;第二动态加载与复用。不要一次性初始化所有动画帧。只在需要播放某个动作时才把对应的帧序列加载进内存播放结束后立即回收。用LruCache缓存最近使用的帧容量控制在16MB以内。第三局部换嘴实现说话效果。Tom说话时脸是不动的只有嘴在动。所以说话动画只需要加载3到5张嘴巴的局部贴图叠加在静态的Tom脸上即可。这样极大减少说话动画的素材量。源码里“说话”动作的素材远少于其他动作也是这个原因。4.3 录制播放延迟的排查记录一个经常被用户吐槽的问题是“我按着录音键说完话唐僧都转世了Tom才开口。”这个延迟来自两个地方一是MediaRecorder录完后落盘的时间二是AudioTrack从文件读数据、做变调处理的耗时。优化思路是从“录完再处理”改成“边录边预处理”。用AudioRecord在子线程持续读取PCM数据每次读取完一帧比如50ms的音频就把数据块丢到内存队列里。停止录音后直接从队列中拼接PCM数据省掉了文件读写IO。在低端机上这个优化能把从“松手”到“出声”的延迟从800ms以上降到300ms以内基本感知不到明显卡顿。4.4 常见问题速查表问题现象可能原因排查与解决点击Tom无任何反应触摸事件被父View拦截检查父容器onInterceptTouchEvent或在TomView中调用getParent().requestDisallowInterceptTouchEvent(true)按键录音无声音录入未授予录音权限动态申请RECORD_AUDIO权限并在onRequestPermissionsResult里处理拒绝逻辑变声后音质沙哑PCM采样率或声道配置不匹配统一录音和播放的采样率、声道数建议固定为44100Hz和单声道动画播放时有卡顿停顿首帧直接加载大图导致阻塞在onPreDraw时先预加载第一帧其余帧用后台线程加载到内存缓存App在后台崩溃没有处理权限和生命周期在onPause中停止录音和动画在onResume中恢复闲置调度避免后台占用麦克风5. 源码学习路线与二次开发方向5.1 源码阅读顺序建议如果你手头有“会说话的Tom猫 Android源码”不要拿到就从头到尾看。源码规模大逐行读容易迷失方向。我建议按照下面这条主线来读第一步读MainActivity搞清楚整体流程setContentView到findViewById再到绑定监听30分钟内能建立起App的全貌。第二步读TomView或核心视图类这是整个项目的神经中枢所有动作分发都经过这里。第三步读音频管理器弄清录音、变声、播放的完整链路。第四步读动画部分理解帧动画或者属性动画是怎么和音频同步的。最后再回过头来看资源文件此时你会对“哪些图对应哪些动作”一目了然。5.2 可以扩展的实用方向复刻版跑通后有几个方向是改造成本低、但效果提升明显的AI对话接入把语音复读从“变声播放”升级为“语义理解回复”。用户说“你好”Tom听完后用变声回复“你好呀”。在服务端接入LLM接口做一个简单的流式对话即可。这一改动能让Tom从“复读机”进化成“陪伴伙伴”可玩性提升一个档次。表情状态机升级当前状态机是单层互斥行为简单。可以引入FSM有限状态机框架加入“饥饿值”“心情值”等属性让Tom根据数值自动切换行为。比如心情值低于30%时摸了也不笑只会叹气。桌面宠物模式利用Android的SYSTEM_ALERT_WINDOW悬浮窗权限把Tom做成桌面上随时互动的小宠物。这块功能我在自己的项目里实现过技术难点在于悬浮窗触摸事件与桌面操作的冲突处理用FLAG_NOT_TOUCH_MODAL加手势滑动区域判定能解决大部分问题。在我实际动手做的过程中最大的体会是这类项目看起来简单真正想要做到流畅自然工作量一半在代码一半在素材和细节调试。动画要调帧率、动作切换要计时长、变声要挑参数每一样都在打磨“手感”。如果你也想复刻一版建议不要贪多先把“触摸惨叫语音复读”这条链路做到顺滑加再多花哨功能都不如这条基础链路体验好。这样一版做完你对Android的事件分发、音频处理、动画机制和内存优化的理解会比写十个登录页面都深刻。本文还有配套的精品资源点击获取