Android Studio 复刻微信界面:单Activity多Fragment架构与避坑指南
简介这份资源面向具备一定 Android 基础、希望从零搭建仿微信界面的开发者与学习者围绕 Android Studio 开发环境系统演示一个微信风格 App 的界面实现路径。压缩包共 1041 个文件约 24.25MB以 flat、json、xml 布局与资源文件、png 图片素材、dex 与 class 编译产物、java 源码及 gradle 构建脚本为主完整保留了工程结构与依赖配置便于直接导入运行和逐层拆解。内容覆盖底部导航栏、聊天列表、个人信息页等核心模块涉及 ConstraintLayout 布局、BottomNavigationView 与 RecyclerView 等 Material Design 组件、自定义聊天气泡视图、适配器数据绑定、Glide 图片加载、VectorDrawable 图标、主题配色以及页面切换动画等知识点并附带版本控制与更新记录痕迹。目前已有 3104 人学习适合作为界面仿写练习与组件用法查阅的实战样本。1. 需求先立住为什么用 Android Studio 复刻微信界面值得认真做一遍很多人拿到「用 Android Studio 制作微信的界面」这个题目第一反应是照着截图堆布局觉得把四个 Tab 摆出来就完事了。真动手才发现微信的界面难点根本不在单个页面好不好看而在于底部导航切换时状态不能丢、消息列表滚动要顺、聊天页输入法弹起时布局不能乱跳。这三件事任何一件翻车答辩现场演示就会当场露馅。这套方案要解决的就是「一个可运行、可讲解、可验收的微信主界面复刻工程」底部四 Tab 导航、消息列表、联系人列表、发现页、我的页加上一个能发消息的聊天详情页。适合正在做 Android 课程设计、移动开发毕设的学生也适合想拿一个结构干净的 Android 项目练手的从业者。下面按需求、架构、模块实现、避坑、验收的顺序讲透代码和配置都能直接抄。2. 总体架构设计单 Activity 加多 Fragment 的骨架怎么搭2.1 为什么选单 Activity 多 Fragment 而不是多 Activity微信主界面本质是四个平级入口来回切用多 Activity 实现的话每次切换都要走一遍生命周期重建消息列表的滚动位置、未读红点状态全得自己存纯属给自己找麻烦。单 Activity 承载一个 FragmentContainerView四个 Fragment 用 show/hide 切换状态天然保留这是这个项目最省心的骨架。工程目录我一般这样分app/src/main/java/com/example/wechatui/ ├── MainActivity.java # 唯一宿主 Activity管底部导航 ├── fragment/ │ ├── ChatListFragment.java # 消息列表 │ ├── ContactFragment.java # 通讯录 │ ├── DiscoverFragment.java # 发现 │ └── MineFragment.java # 我的 ├── chat/ │ └── ChatActivity.java # 聊天详情页 ├── adapter/ │ ├── ChatListAdapter.java │ └── MessageAdapter.java └── model/ ├── ChatItem.java └── Message.java这个分层的好处是每个 Fragment 只关心自己的列表和点击事件MainActivity 只负责导航切换职责边界清楚答辩时被问「为什么这么分」也有话讲。2.2 底部导航的布局与切换逻辑底部导航用 BottomNavigationView 配 menu 资源四个 item 的图标和文字对齐微信。关键在切换时用 FragmentTransaction 的 show/hide 而不是 replace避免 Fragment 反复重建。!-- activity_main.xml 核心结构 -- LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical androidx.fragment.app.FragmentContainerView android:idid/fragment_container android:layout_widthmatch_parent android:layout_height0dp android:layout_weight1 / com.google.android.material.bottomnavigation.BottomNavigationView android:idid/bottom_nav android:layout_widthmatch_parent android:layout_heightwrap_content app:menumenu/bottom_nav_menu / /LinearLayout// MainActivity.java 切换逻辑 private Fragment currentFragment; private void switchFragment(Fragment target) { if (target currentFragment) return; FragmentTransaction ft getSupportFragmentManager().beginTransaction(); if (currentFragment ! null) ft.hide(currentFragment); // 隐藏而非销毁 if (!target.isAdded()) { ft.add(R.id.fragment_container, target); } else { ft.show(target); // 已添加则直接显示状态保留 } currentFragment target; ft.commit(); }这里hide和show是核心replace会把旧 Fragment 的视图销毁列表滚动位置和输入框内容全丢。isAdded()判断保证 Fragment 只 add 一次后续都走 show。BottomNavigationView 的setOnItemSelectedListener里根据 itemId 映射到对应 Fragment 即可。2.3 主题与沉浸式状态栏的配置微信顶部是深色状态栏配浅色内容区这个效果靠主题配置加状态栏颜色设置。在themes.xml里用 NoActionBar 主题代码里设置状态栏颜色style nameTheme.WeChatUI parentTheme.MaterialComponents.Light.NoActionBar item namecolorPrimary#07C160/item item nameandroid:statusBarColor#EDEDED/item item nameandroid:windowLightStatusBartrue/item /stylewindowLightStatusBar设为 true 让状态栏图标变深色配浅灰背景才看得清。这个细节不做的话状态栏图标和背景撞色一眼就看出是半成品。3. 核心页面实现消息列表、聊天页与通讯录的落地细节3.1 消息列表RecyclerView 加 ViewHolder 的标准写法消息列表是微信的门面每行有头像、昵称、最后一条消息、时间、未读红点。用 RecyclerView 配自定义 Adapteritem 布局用 ConstraintLayout 摆位。// ChatListAdapter.java 核心部分 public class ChatListAdapter extends RecyclerView.AdapterChatListAdapter.VH { private final ListChatItem data; private final OnItemClickListener listener; public interface OnItemClickListener { void onClick(ChatItem item); } public ChatListAdapter(ListChatItem data, OnItemClickListener listener) { this.data data; this.listener listener; } NonNull Override public VH onCreateViewHolder(NonNull ViewGroup parent, int viewType) { View v LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_chat_list, parent, false); return new VH(v); } Override public void onBindViewHolder(NonNull VH holder, int position) { ChatItem item data.get(position); holder.name.setText(item.getName()); holder.lastMsg.setText(item.getLastMessage()); holder.time.setText(item.getTime()); // 未读红点大于 0 才显示超过 99 显示 99 if (item.getUnread() 0) { holder.badge.setVisibility(View.VISIBLE); holder.badge.setText(item.getUnread() 99 ? 99 : String.valueOf(item.getUnread())); } else { holder.badge.setVisibility(View.GONE); } holder.itemView.setOnClickListener(v - listener.onClick(item)); } Override public int getItemCount() { return data.size(); } static class VH extends RecyclerView.ViewHolder { TextView name, lastMsg, time, badge; VH(View v) { super(v); name v.findViewById(R.id.tv_name); lastMsg v.findViewById(R.id.tv_last_msg); time v.findViewById(R.id.tv_time); badge v.findViewById(R.id.tv_badge); } } }onBindViewHolder里红点的显隐判断是重点未读为 0 时必须GONE否则会出现空红点。99的截断逻辑微信就是这么处理的加上去答辩时是个加分细节。点击事件通过接口回调给 FragmentFragment 里启动 ChatActivity 并传对方昵称。3.2 聊天详情页消息气泡与输入法适配聊天页的难点是消息气泡左右对齐和输入法弹起时列表自动滚到底部。气泡用 9-patch 背景图或者 shape drawable 做圆角左右靠 gravity 区分。!-- item_message.xml 消息气泡 -- LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationhorizontal android:padding8dp ImageView android:idid/iv_avatar android:layout_width40dp android:layout_height40dp android:srcdrawable/ic_avatar_default / TextView android:idid/tv_content android:layout_widthwrap_content android:layout_heightwrap_content android:layout_marginStart8dp android:backgrounddrawable/bg_bubble_left android:maxWidth240dp android:padding10dp android:textColor#000000 / /LinearLayout左右气泡的区分在 Adapter 里根据消息类型设置 gravity 和背景自己发的消息把头像和气泡顺序调换、背景换成绿色bg_bubble_right。输入法适配在 AndroidManifest 里给 ChatActivity 配windowSoftInputModeadjustResize再监听布局变化滚到底// ChatActivity.java 输入法弹起时滚到底部 private void scrollToBottom() { if (messageList.size() 0) { recyclerView.scrollToPosition(messageList.size() - 1); } } // 监听根布局高度变化输入法弹起时触发 rootView.getViewTreeObserver().addOnGlobalLayoutListener(() - { int heightDiff rootView.getRootView().getHeight() - rootView.getHeight(); if (heightDiff 200) { // 经验值超过 200px 视为输入法弹起 scrollToBottom(); } });adjustResize让内容区被压缩而不是被输入法盖住heightDiff判断是常见的土办法阈值 200 是踩过坑调出来的太小会误判、太大反应迟钝。3.3 通讯录与发现页分组列表和宫格布局通讯录用带字母索引的分组列表简单做法是 RecyclerView 加两种 viewType分组标题和联系人条目。发现页是宫格用 GridLayout 或者 RecyclerView 配 GridLayoutManager 都行我一般用后者方便统一处理点击。// ContactFragment.java 分组数据构建 private void buildGroupedData() { MapString, ListContact grouped new TreeMap(); // TreeMap 自动按字母排序 for (Contact c : allContacts) { String letter c.getPinyin().substring(0, 1).toUpperCase(); grouped.computeIfAbsent(letter, k - new ArrayList()).add(c); } // 展开成扁平列表标题 条目 for (Map.EntryString, ListContact entry : grouped.entrySet()) { items.add(new ContactItem(true, entry.getKey(), null)); for (Contact c : entry.getValue()) { items.add(new ContactItem(false, null, c)); } } adapter.notifyDataSetChanged(); }TreeMap保证字母顺序computeIfAbsent省掉判空。拼音首字母这块如果不想引第三方库可以给测试数据直接写好拼音字段答辩演示够用别在这上面耗太久。4. 避坑与排查这个界面项目最容易翻车的五个点4.1 底部导航切换后列表滚动位置丢失现象从消息列表切到通讯录再切回来列表回到顶部。原因用了replace或者每次切换都new一个 Fragment。解决统一用show/hideFragment 实例在 MainActivity 里只创建一次并缓存切换时复用。4.2 聊天页输入法弹起后布局被顶飞现象点输入框整个页面往上窜顶部标题栏跑出屏幕。原因Activity 的windowSoftInputMode没配或者配成了adjustPan。解决Manifest 里改成adjustResize同时根布局用LinearLayout的layout_weight让列表区自适应压缩别用固定高度。4.3 RecyclerView 的 item 点击无响应现象列表能显示点了没反应。原因item 根布局里放了可点击的 Button 或 ImageView 抢了焦点或者 item 布局高度写成wrap_content但内部有match_parent冲突。解决给 item 根布局设android:clickabletrue和android:focusabletrue内部控件设focusablefalse点击事件统一在 Adapter 的itemView.setOnClickListener里处理。4.4 头像圆角处理导致列表卡顿现象消息列表滑动掉帧。原因在onBindViewHolder里每次都重新裁剪 Bitmap 做圆角。解决用 Glide 的circleCrop()或者提前把圆角头像做成 drawable 资源绑定阶段只做 setImageResource别在主线程做图像运算。4.5 深色模式下一片惨白或一片漆黑现象系统切深色模式界面文字看不见。原因颜色全写死成#000000没走主题属性。解决文字颜色用?attr/colorOnBackground这类主题属性或者在values-night下单独配一套颜色。课程设计不要求深色模式的话至少在themes.xml里把forceDarkAllowed设为 false锁死浅色避免演示时被系统设置坑到。5. 验收与答辩怎么证明这套界面真的能跑、能讲、能改验收分三层从「能跑」到「能讲」再到「能改」逐层过一遍基本不会翻车。第一层功能验收按清单逐项点验收项操作通过标准底部导航依次点四个 Tab页面正确切换无闪退状态保留消息列表滑到中间切走再切回滚动位置不变聊天跳转点消息列表任一条进入聊天页标题显示对方昵称发消息输入文字点发送气泡出现在右侧列表滚到底输入法适配点输入框标题栏不动列表区压缩未读红点查看有未读的条目红点显示99 截断正确第二层代码讲解答辩老师最爱问的三个问题提前准备好答案为什么用 Fragment 不用 Activity状态保留、切换轻量RecyclerView 比 ListView 好在哪ViewHolder 复用、局部刷新adjustResize和adjustPan的区别压缩内容区 vs 整体上移。这三个答顺了基本盘就稳了。第三层可扩展性留一两个口子证明这工程不是死代码。比如消息数据从静态 List 换成 Room 数据库、聊天页接入本地存储答辩时说一句「当前数据层是内存 List接口已经抽出来了换数据源只改一个类」比空谈架构有说服力。我自己的血泪经验是答辩前一天晚上一定要在真机上完整走一遍模拟器上adjustResize的表现和真机经常不一致我见过不止一个项目在模拟器上好好的换真机输入法一弹布局就乱。从那以后我每次交项目前都强制在真机上把验收清单过一遍模拟器只用来写代码。希望这套结构和清单能帮到你照着搭一遍界面能跑、代码能讲、答辩能过。本文还有配套的精品资源点击获取