简介这份资源面向具备一定 Java 基础、希望借助 Android Studio 实践界面开发的初学者与进阶学习者围绕仿微信客户端界面这一典型场景帮助读者理解从布局搭建到交互细节的完整实现思路。压缩包共 1041 个文件约 24.25MB以 flat、json、xml、png 等资源与配置文件为主辅以 dex、class、java 源码及 jar、gradle 构建文件并包含 apk 成品与 Git 版本控制痕迹便于对照运行与阅读。已有 3103 人学习下载说明其在同类练习中具备一定参考价值。内容覆盖底部导航栏、聊天列表、个人信息页等模块涉及 ConstraintLayout 布局、BottomNavigationView 与 RecyclerView 组件、自定义聊天气泡视图、适配器数据绑定、Glide 图片加载、VectorDrawable 图标、主题配色及过渡动画等知识点读者可据此理解仿微信界面的组织方式与优化方向。1. 从一份“高仿微信”工程说起Android Studio 界面还原到底在练什么很多人第一次打开这份工程时以为它只是个“画界面”的练习跑起来才发现里面藏着不少真东西底部四个 Tab 的切换、消息列表的 RecyclerView 复用、聊天页的软键盘顶起输入框、朋友圈的九宫格图片排布几乎把 Android 原生 UI 开发里最容易翻车的几个点都踩了一遍。它适合两类人一类是刚学完四大组件、想找个完整项目练手的 Android 新手另一类是做过业务、但想回头把布局、适配、状态管理这些基本功重新捋一遍的老手。用 Android Studio 打开、编译、跑模拟器你就能看到一个结构清晰、可继续往上叠功能的微信界面骨架而不是一堆散落的 Demo 代码。2. 工程结构与技术选型一个原生 Android 项目是怎么搭起来的2.1 目录结构从 Gradle 配置到 res 资源的分层拿到这份工程第一件事不是急着点运行而是先把目录扫一遍。它用的是标准的 Android Studio 工程结构没有引入奇怪的第三方框架纯原生 Java/Kotlin XML 布局这对想读懂每一行代码的人反而是好事。核心目录大致是这样WeChatUIDemo/ ├── app/ │ ├── build.gradle # 模块级构建脚本依赖和编译配置都在这 │ └── src/main/ │ ├── AndroidManifest.xml # 四大组件注册、权限声明 │ ├── java/com/example/wechatui/ │ │ ├── activity/ # 各页面 Activity │ │ ├── adapter/ # RecyclerView 适配器 │ │ ├── fragment/ # 底部 Tab 对应的 Fragment │ │ ├── model/ # 数据实体类 │ │ └── widget/ # 自定义 View │ └── res/ │ ├── layout/ # 页面与 item 布局 │ ├── drawable/ # 图标、圆角背景、selector │ ├── values/ # 颜色、字符串、主题、尺寸 │ └── mipmap/ # 启动图标 ├── build.gradle # 项目级构建脚本 ├── settings.gradle # 模块声明 └── gradle.properties # 全局构建参数这个分层的关键在于activity和fragment的职责划分主界面是一个容器 Activity底部 Tab 切换的是四个 Fragment而不是四个 Activity 来回跳。这样做的好处是切换时状态保留、动画顺滑也符合现在主流 App 的导航结构。adapter单独拎出来是因为消息列表、联系人列表、朋友圈列表都要复用同一套列表逻辑抽出来之后改一处就能影响全局。res/drawable里通常会有大量 selector 和 shape 文件比如底部 Tab 的选中/未选中状态、消息气泡的圆角背景、输入框的边框。这些不是“装饰”而是界面还原度的核心——微信那种灰白配色和细分割线靠的就是这些 XML 资源而不是硬编码在布局里。2.2 技术栈与依赖为什么选原生而不是跨平台这份工程的技术选型很克制基本就是 Android 官方那一套层面选型说明语言Java 或 Kotlin工程内以 Java 为主部分文件可能是 Kotlin布局XML ConstraintLayout复杂页面用约束布局简单 item 用 LinearLayout列表RecyclerView Adapter消息、联系人、朋友圈统一用列表导航Fragment BottomNavigationView底部 Tab 切换图片Glide 或原生 ImageView头像、朋友圈图片加载构建Gradle标准 Android 构建流程为什么不用 Flutter 或 React Native因为这个项目的目标就是练 Android 原生 UI。用跨平台框架确实能一套代码跑两端但底部导航、软键盘避让、列表复用这些细节恰恰是原生开发里最需要亲手写一遍的东西。你在这个工程里踩过的坑换成任何原生 App 都会再遇到。build.gradle里常见的依赖大概长这样dependencies { implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.android.material:material:1.9.0 implementation androidx.constraintlayout:constraintlayout:2.1.4 implementation androidx.recyclerview:recyclerview:1.3.0 implementation com.github.bumptech.glide:glide:4.15.1 }这里每个依赖都有明确用途appcompat提供兼容主题和 Activity 基类material提供 BottomNavigationView 和卡片样式constraintlayout是复杂布局的基础recyclerview负责所有列表glide处理图片加载和缓存。版本号不用死记打开工程后 Android Studio 会提示可升级项但第一次跑建议先别动保持和工程一致避免因为版本差异引入编译错误。提示如果 Gradle 同步卡住先检查gradle-wrapper.properties里的 distributionUrl 是否指向可访问的地址以及本地是否已经缓存了对应版本的 Gradle。国内网络环境下依赖下载慢是常见问题可以在项目级build.gradle里配置镜像仓库但不要随意改动工程原有的仓库顺序。2.3 主界面容器BottomNavigationView 与 Fragment 的联动主界面是整个工程的入口通常是一个MainActivity布局里放一个FrameLayout作为 Fragment 容器底部一个BottomNavigationView。核心逻辑是监听底部菜单的选中事件切换对应的 FragmentBottomNavigationView nav findViewById(R.id.bottom_nav); nav.setOnItemSelectedListener(item - { Fragment target; int id item.getItemId(); if (id R.id.tab_chat) { target new ChatListFragment(); // 微信消息列表 } else if (id R.id.tab_contacts) { target new ContactsFragment(); // 通讯录 } else if (id R.id.tab_discover) { target new DiscoverFragment(); // 发现朋友圈入口 } else { target new ProfileFragment(); // 我 } getSupportFragmentManager() .beginTransaction() .replace(R.id.fragment_container, target) .commit(); return true; });这段代码的逻辑很直白每次选中底部菜单就新建一个 Fragment 并替换容器里的内容。replace会销毁上一个 Fragment 的视图适合这种四个 Tab 互相独立的场景。参数上R.id.fragment_container是布局里 FrameLayout 的 id必须和 XML 里一致commit()是异步提交如果在onSaveInstanceState之后调用会抛异常所以正式项目里一般用commitAllowingStateLoss()或者判断状态但作为界面练习commit()足够。这里有个容易忽略的点每次切换都new一个 Fragment会导致重复创建、状态丢失。更稳妥的做法是用addshow/hide缓存四个 Fragment 实例或者用ViewPager2配合BottomNavigationView。工程里如果用的是replace你二次开发时想保留列表滚动位置就得改成缓存方案这是后面改造章节会展开的内容。3. 核心界面拆解消息列表、聊天页与朋友圈的实现细节3.1 消息列表RecyclerView 的 Adapter 与 ViewHolder微信主界面最显眼的就是消息列表每一行有头像、昵称、最后一条消息、时间可能还有未读红点。这个列表用 RecyclerView 实现核心在 Adapter 和 ViewHolder 的配合public class ChatListAdapter extends RecyclerView.AdapterChatListAdapter.VH { private ListChatItem data; public ChatListAdapter(ListChatItem data) { this.data data; } static class VH extends RecyclerView.ViewHolder { ImageView avatar; TextView name, lastMsg, time; TextView badge; VH(View itemView) { super(itemView); avatar itemView.findViewById(R.id.iv_avatar); name itemView.findViewById(R.id.tv_name); lastMsg itemView.findViewById(R.id.tv_last_msg); time itemView.findViewById(R.id.tv_time); badge itemView.findViewById(R.id.tv_badge); } } Override public VH onCreateViewHolder(ViewGroup parent, int viewType) { View v LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_chat, parent, false); return new VH(v); } Override public void onBindViewHolder(VH holder, int position) { ChatItem item data.get(position); holder.name.setText(item.getName()); holder.lastMsg.setText(item.getLastMsg()); holder.time.setText(item.getTime()); // 未读数大于 0 才显示红点否则占位隐藏 if (item.getUnread() 0) { holder.badge.setVisibility(View.VISIBLE); holder.badge.setText(String.valueOf(item.getUnread())); } else { holder.badge.setVisibility(View.GONE); } Glide.with(holder.avatar.getContext()) .load(item.getAvatarRes()) .circleCrop() .into(holder.avatar); } Override public int getItemCount() { return data null ? 0 : data.size(); } }逐段看ViewHolder里把一行里所有子 View 的引用缓存下来避免每次bind都findViewById这是 RecyclerView 性能的基本盘。onCreateViewHolder负责把item_chat.xml膨胀成 Viewparent传进去是为了让布局的layout_width/layout_height生效传null会导致宽高失效这是新手常翻的车。onBindViewHolder里做数据绑定未读红点用VISIBLE/GONE控制而不是设成空字符串因为GONE不占位布局不会错位。头像用 Glide 的circleCrop()裁成圆形省去自己写圆形 ImageView 的麻烦。ChatItem这个实体类一般包含name、lastMsg、time、unread、avatarRes几个字段数据源可以是硬编码的假数据也可以从本地数据库或接口来。作为界面练习硬编码在Fragment里初始化一个List就够了但要注意列表数据变化后调用notifyDataSetChanged()否则界面不刷新。注意notifyDataSetChanged()会重绘所有可见 item数据量大时性能差。二次开发如果要做“收到新消息插入顶部”用notifyItemInserted(0)更合适配合scrollToPosition(0)体验更好。3.2 聊天页软键盘避让与输入框顶起点进某个会话就是聊天页。这个页面最容易出问题的地方不是消息气泡而是软键盘弹出时输入框被挡住。工程里通常会在AndroidManifest.xml给聊天 Activity 设置windowSoftInputModeactivity android:name.activity.ChatActivity android:windowSoftInputModeadjustResize /adjustResize的意思是软键盘弹出时系统会缩小 Activity 的可用高度让底部的输入框跟着顶上来。但光设这个还不够布局的根节点如果是ScrollView或ConstraintLayout还要保证输入框在底部且不被其他元素挤压。常见做法是把消息列表放在RecyclerView里输入框固定在底部整体用ConstraintLayout约束androidx.constraintlayout.widget.ConstraintLayout ... androidx.recyclerview.widget.RecyclerView android:idid/rv_messages android:layout_width0dp android:layout_height0dp app:layout_constraintTop_toTopOfparent app:layout_constraintBottom_toTopOfid/layout_input app:layout_constraintStart_toStartOfparent app:layout_constraintEnd_toEndOfparent / LinearLayout android:idid/layout_input android:layout_width0dp android:layout_heightwrap_content android:orientationhorizontal app:layout_constraintBottom_toBottomOfparent app:layout_constraintStart_toStartOfparent app:layout_constraintEnd_toEndOfparent !-- 输入框、表情按钮、发送按钮 -- /LinearLayout /androidx.constraintlayout.widget.ConstraintLayout这里RecyclerView的高度是0dp加上下约束意思是“占满输入框以上的所有空间”输入框高度wrap_content贴底。软键盘弹出后adjustResize让根布局高度变小RecyclerView自动收缩输入框仍然贴底可见。如果换成adjustPan整个窗口会上移标题栏被顶出去体验很差这是选型上的关键差别。消息气泡本身一般用LinearLayout左右对齐对方消息靠左、自己消息靠右气泡背景用shape画圆角矩形颜色区分。时间戳居中显示用TextView加灰色小字。这些细节不复杂但堆起来就是还原度。3.3 朋友圈九宫格图片与列表嵌套的取舍朋友圈页面是这份工程里布局最复杂的部分一条动态包含头像、昵称、正文、图片网格、时间、点赞评论入口。图片网格常见做法是用GridLayout或者一个横向RecyclerView但更简单可控的是根据图片数量动态设置列数int count item.getImages().size(); GridLayout grid holder.grid; grid.setColumnCount(count 1 ? 1 : 3); // 单图一行多图三列 grid.removeAllViews(); for (int i 0; i count; i) { ImageView iv new ImageView(grid.getContext()); GridLayout.LayoutParams lp new GridLayout.LayoutParams(); lp.width dp2px(grid.getContext(), 100); lp.height dp2px(grid.getContext(), 100); lp.setMargins(dp2px(grid.getContext(), 2), 2, 2, 2); iv.setLayoutParams(lp); iv.setScaleType(ImageView.ScaleType.CENTER_CROP); Glide.with(iv).load(item.getImages().get(i)).into(iv); grid.addView(iv); }逻辑说明单图时列数为 1图片可以大一些多图时固定三列每张 100dp 见方CENTER_CROP保证不变形。dp2px是自己写的工具方法把 dp 转成像素避免不同屏幕密度下大小不一致。setMargins给每张图留 2dp 间隙形成网格感。这里有个取舍朋友圈整体是一个RecyclerView每条动态是一个 itemitem 内部又嵌了图片网格。如果图片网格也用RecyclerView就会出现嵌套滚动冲突滑动手感很怪。所以工程里用GridLayout动态添加ImageView牺牲一点复用性换取滚动流畅这是常见做法。二次开发如果图片很多可以考虑用FlexboxLayout或者自己算行高但别轻易上嵌套RecyclerView。4. 跑起来与避坑从 Gradle 同步到模拟器适配的排查清单4.1 首次运行环境、SDK 与 Gradle 同步把工程导入 Android Studio 后第一步是等 Gradle 同步完成。同步失败最常见的原因是 SDK 版本不匹配工程build.gradle里compileSdk、targetSdk写的版本你本地没装。打开 SDK Manager把对应版本的 Android SDK Platform 和 Build-Tools 装上再同步一次。第二个坑是 JDK 版本。新版 Android Studio 自带 JDK但老工程可能指定了 Java 8 或 11。在File Project Structure SDK Location里确认 Gradle JDK 和工程要求一致否则会报Unsupported class file major version之类的错。第三个坑是依赖下载。build.gradle里的仓库地址如果访问慢同步会卡在Downloading。可以先检查本地 Gradle 缓存目录一般在用户目录下的.gradle/caches确认依赖是否已缓存没有的话配置一个可用的镜像仓库但注意不要打乱原有仓库顺序否则可能拉到不兼容的版本。同步通过后选一个模拟器或真机点运行。模拟器建议选 API 30 以上的版本分辨率用 Pixel 系列这样底部导航和列表的显示效果最接近真机。如果模拟器启动慢可以开硬件加速或者在 AVD Manager 里调大内存。4.2 常见问题排查界面错位、图片不显示、点击无响应跑起来之后问题才真正开始。下面几条是这类界面工程里高频出现的现象一底部导航栏被系统导航条挡住。原因是没有处理沉浸式状态栏和导航栏布局直接贴底。解决办法是在主题里配置windowTranslucentNavigation或者在代码里用WindowInsets给根布局加底部 padding。简单做法是给BottomNavigationView加android:layout_marginBottom但更规范的是用fitsSystemWindows配合CoordinatorLayout。现象二消息列表头像不显示。先看 Glide 有没有加网络权限如果头像是网络图再看ImageView的宽高是不是wrap_content导致加载不出来。Glide 在wrap_content下也能工作但如果父布局约束有问题图片可能被压成 0。检查item_chat.xml里头像的layout_width/layout_height是否给了具体值或约束。现象三点击列表项没反应。RecyclerView 的 item 点击事件不是自带的需要在onBindViewHolder里给itemView设OnClickListener或者用接口回调把 position 传出去。如果只在ViewHolder构造里设了监听但没绑数据点击后拿不到正确的位置就会“点了没反应”或者“点谁都跳同一个页面”。现象四聊天页输入框被键盘挡住。前面说过adjustResize但如果 Activity 的主题是Fullscreen或者根布局用了ScrollView包裹整个页面adjustResize可能失效。检查主题里有没有windowFullscreen以及布局根节点是不是可收缩的。现象五Fragment 切换后数据丢失。用replace切换 Fragment 会销毁视图回来时重新创建列表滚动位置和输入内容都没了。如果业务要求保留状态改成addshow/hide或者用ViewPager2的offscreenPageLimit控制缓存。提示排查界面问题最有效的手段是打开开发者选项里的“显示布局边界”一眼就能看出哪个 View 占了多余空间、哪个 View 宽高为 0。比盲猜快得多。4.3 真机与模拟器的差异字体、密度与状态栏模拟器上看着正常的界面到真机上可能就歪了。最常见的是字体缩放用户在系统设置里调大了字体TextView里的文字变大把固定高度的行撑破。解决办法是给关键TextView设maxLines和ellipsize或者用autoSizeTextType让文字自适应。屏幕密度差异也会影响dp转px的结果。工程里如果有硬编码的px值在不同密度设备上大小会不一致。检查布局文件里有没有android:layout_width100px这种写法统一改成dp。状态栏颜色是另一个差异点。模拟器默认状态栏可能是深色真机可能是浅色导致状态栏图标看不清。在主题里显式设置statusBarColor和windowLightStatusBar保证两种设备上一致。5. 二次开发改造把界面骨架变成能用的聊天 Demo5.1 从假数据到本地存储给消息列表接上数据库工程里的消息列表大概率是硬编码的假数据想让它“活”起来第一步是把数据源换成本地数据库。Room 是官方推荐的方案轻量且和 RecyclerView 配合好。先定义实体和 DAOEntity(tableName chat_item) public class ChatEntity { PrimaryKey(autoGenerate true) public int id; public String name; public String lastMsg; public String time; public int unread; } Dao public interface ChatDao { Query(SELECT * FROM chat_item ORDER BY time DESC) ListChatEntity getAll(); Insert void insert(ChatEntity item); Query(UPDATE chat_item SET lastMsg :msg, time :time WHERE name :name) void updateLastMsg(String name, String msg, String time); }然后在Fragment里观察数据变化把ListChatEntity喂给 Adapter。这样每次发消息、收消息只要更新数据库列表就自动刷新。改造时注意把原来的ChatItem字段映射到ChatEntityAdapter 的泛型也要跟着换别漏改onBindViewHolder里的取值方法。5.2 聊天页发消息本地回显与时间戳处理聊天页的输入框和发送按钮接上逻辑后点发送应该立刻在列表里追加一条自己的消息并滚动到底部。核心代码大概是这样btnSend.setOnClickListener(v - { String text etInput.getText().toString().trim(); if (text.isEmpty()) return; Message msg new Message(text, System.currentTimeMillis(), true); messageList.add(msg); adapter.notifyItemInserted(messageList.size() - 1); rvMessages.scrollToPosition(messageList.size() - 1); etInput.setText(); });这里notifyItemInserted比notifyDataSetChanged更精准只刷新新增的那一项配合scrollToPosition让新消息可见。时间戳用System.currentTimeMillis()显示时格式化成“HH:mm”超过一天显示日期。如果想做“对方自动回复”可以加一个Handler.postDelayed模拟延迟但记得在onDestroy里移除回调否则页面销毁后回调还在跑容易内存泄漏。5.3 改造验证三个必须回归的检查点改完之后别急着提交至少回归这三处第一切到其他 Tab 再切回来消息列表的滚动位置和数据是否还在如果用了replace就会丢需要改成缓存方案第二软键盘弹出再收起输入框和列表有没有错位第三旋转屏幕后Activity 重建数据是否恢复用ViewModel保存列表状态或者加configChanges但后者不推荐。这三处过了基本算能用了。我自己的习惯是每次动完布局或导航逻辑都强制在模拟器和真机上各跑一遍上面三个检查点再顺手打开布局边界看一眼有没有多余的占位。血泪经验是界面问题往往不是代码写错而是某个layout_height写成了match_parent把兄弟 View 挤没了。希望这份拆解能帮你少走几步弯路把这份微信界面工程真正跑起来、改下去。本文还有配套的精品资源点击获取