Android Studio 复刻微信主界面:BottomNavigationView 与 RecyclerView 实战

发布时间:2026/10/8 20:09:04
Android Studio 复刻微信主界面:BottomNavigationView 与 RecyclerView 实战
简介这是一份面向Android初学者与界面开发爱好者的实战项目源码围绕「用Android Studio制作微信界面」展开帮助读者在真实工程中理解移动端UI搭建流程。资源包共1041个文件约24.25MB涵盖426个flat资源、237个json配置、88个xml布局、73个png图片以及20个java源码、28个dex与26个class等完整保留了Android工程从布局、资源到编译产物的目录结构。内容涉及ConstraintLayout布局、BottomNavigationView底部导航、RecyclerView聊天列表、Material Design组件、自定义聊天气泡视图、Adapter数据绑定、Glide图片加载、VectorDrawable图标、styles.xml主题配色以及ObjectAnimator过渡动画等知识点并包含Git版本控制痕迹与更新记录。已有3102人学习下载适合希望从零梳理微信风格界面实现思路、对照源码查漏补缺的开发者参考。1. 用 Android Studio 复刻微信界面从“看着像”到“跑得动”的分水岭很多人第一次打开 Android Studio想做的第一件事就是照着微信的样子搭一套界面顶部一条深色导航栏中间是会话列表底部四个 Tab 来回切。听起来简单但真正动手就会发现光是一个“像”字就分出了三六九等——有人用 LinearLayout 硬堆屏幕一旋转布局全乱有人把图标切成一堆 PNG装到不同分辨率手机上糊成一片还有人把微信控件当成静态图片摆上去点一下毫无反应。这篇笔记要解决的就是怎么用 Android Studio 把微信主界面从零搭出来让它不只是“截图看着像”而是能点击、能滑动、能适配不同屏幕、能继续往上叠业务逻辑。适合刚学完 Activity 和基础控件、想找一个完整案例练手的 Android 新手也适合从其他平台转过来、想快速摸清 Android 布局体系的开发者。下面按“先立结构、再填内容、最后调细节”的顺序走一遍每一步都给出可复制的代码和参数说明。2. 拆解微信主界面四个 Tab 加一个会话列表的布局骨架2.1 为什么选 BottomNavigationView Fragment 而不是自己画微信主界面最显眼的结构是底部四个 Tab微信、通讯录、发现、我。每个 Tab 对应一个独立页面页面之间切换时底部栏保持不动。这个模式在 Android 里最标准的实现就是BottomNavigationView配合Fragment。自己用 LinearLayout 加 ImageView 也能画出来但选中态切换、水波纹反馈、图标着色这些细节全要手写而且和 Material Design 的交互规范对不齐。BottomNavigationView自带这些行为只需要提供菜单资源和图标剩下的交给它。选Fragment而不是Activity的理由更直接Tab 切换时如果每次启动新 Activity底部栏会跟着重建视觉上闪一下体验很割裂。Fragment活在同一个 Activity 里切换只是替换中间的内容区域底部栏纹丝不动。微信本身也是这个思路四个主页面共享一个宿主切换时只换内容。2.2 建工程与依赖配置Gradle 里必须加的那几行新建工程时选 “Empty Views Activity”语言选 Java 或 Kotlin 都行下面以 Kotlin 为例。打开app/build.gradle.kts在dependencies块里加上 Material 组件库和 Fragment 的 KTX 扩展dependencies { // Material 组件库BottomNavigationView 在这里面 implementation(com.google.android.material:material:1.11.0) // Fragment KTX简化 fragment 事务写法 implementation(androidx.fragment:fragment-ktx:1.6.2) // 约束布局后面搭会话列表要用 implementation(androidx.constraintlayout:constraintlayout:2.1.4) }加完后点右上角 “Sync Now”。如果同步报错说找不到某个版本大概率是本地 Gradle 缓存没更新在终端跑一次./gradlew --refresh-dependencies再同步。版本号不用死记Material 库跟着 AndroidX 走写一个近期稳定版即可Android Studio 会提示可升级的版本。2.3 主 Activity 布局给底部栏和内容区划好地盘activity_main.xml用ConstraintLayout做根布局上面放一个FragmentContainerView占满剩余空间下面贴一个BottomNavigationView?xml version1.0 encodingutf-8? androidx.constraintlayout.widget.ConstraintLayout xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:apphttp://schemas.android.com/apk/res-auto android:layout_widthmatch_parent android:layout_heightmatch_parent !-- 内容区四个 Fragment 轮流住进来 -- androidx.fragment.app.FragmentContainerView android:idid/fragment_container android:layout_width0dp android:layout_height0dp app:layout_constraintTop_toTopOfparent app:layout_constraintBottom_toTopOfid/bottom_nav app:layout_constraintStart_toStartOfparent app:layout_constraintEnd_toEndOfparent / !-- 底部导航栏菜单资源在 res/menu 下 -- com.google.android.material.bottomnavigation.BottomNavigationView android:idid/bottom_nav android:layout_width0dp android:layout_heightwrap_content android:backgroundandroid:color/white app:menumenu/menu_bottom_nav app:labelVisibilityModelabeled app:layout_constraintBottom_toBottomOfparent app:layout_constraintStart_toStartOfparent app:layout_constraintEnd_toEndOfparent / /androidx.constraintlayout.widget.ConstraintLayoutFragmentContainerView的高度设为0dp配合约束意思是“撑满底部栏以上的所有空间”这样不同屏幕高度下内容区自动伸缩。labelVisibilityModelabeled让四个 Tab 的文字始终显示微信就是这种效果如果设成auto选中项显示文字、未选中只显示图标那是另一种风格。2.4 菜单资源与图标四个 Tab 的声明式定义在res/menu/下新建menu_bottom_nav.xml?xml version1.0 encodingutf-8? menu xmlns:androidhttp://schemas.android.com/apk/res/android item android:idid/nav_chats android:icondrawable/ic_chat android:title微信 / item android:idid/nav_contacts android:icondrawable/ic_contacts android:title通讯录 / item android:idid/nav_discover android:icondrawable/ic_discover android:title发现 / item android:idid/nav_me android:icondrawable/ic_me android:title我 / /menu图标建议用 Vector Asset 导入 Material 图标不要直接切 PNG。Vector 图标在任何分辨率下都清晰而且可以用app:itemIconTint统一控制选中和未选中的颜色。微信的选中色是绿色#07C160未选中是灰色#999999在BottomNavigationView上加一行app:itemIconTintcolor/bottom_nav_tint然后在res/color/下建一个 selector 文件分别指定两种状态的颜色即可。2.5 Fragment 切换逻辑三行代码管住四个页面在MainActivity里初始化底部栏并绑定切换监听class MainActivity : AppCompatActivity() { // 四个 Fragment 懒加载避免每次切换都重建 private val chatsFragment by lazy { ChatsFragment() } private val contactsFragment by lazy { ContactsFragment() } private val discoverFragment by lazy { DiscoverFragment() } private val meFragment by lazy { MeFragment() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val bottomNav findViewByIdBottomNavigationView(R.id.bottom_nav) // 默认选中第一个 Tab if (savedInstanceState null) { supportFragmentManager.beginTransaction() .replace(R.id.fragment_container, chatsFragment) .commit() } // 切换监听根据菜单 id 决定显示哪个 Fragment bottomNav.setOnItemSelectedListener { item - val target when (item.itemId) { R.id.nav_chats - chatsFragment R.id.nav_contacts - contactsFragment R.id.nav_discover - discoverFragment R.id.nav_me - meFragment else - returnsetOnItemSelectedListener false } supportFragmentManager.beginTransaction() .replace(R.id.fragment_container, target) .commit() true } } }这里用by lazy让 Fragment 只在第一次访问时创建切换回来时复用同一个实例避免重复初始化。savedInstanceState null的判断是为了防止屏幕旋转后 Fragment 被系统重建、又手动添加一次导致重叠。这个坑很常见旋转屏幕后界面出现两层内容就是没加这个判断。3. 会话列表页用 RecyclerView 把微信聊天列表跑起来3.1 列表项布局头像、昵称、消息、时间四件套微信会话列表每一行的结构很固定左边圆角头像中间上下两行分别是昵称和消息预览右边上方是时间、下方可能有未读红点。用ConstraintLayout搭这一行?xml version1.0 encodingutf-8? androidx.constraintlayout.widget.ConstraintLayout xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:apphttp://schemas.android.com/apk/res-auto android:layout_widthmatch_parent android:layout_height72dp android:background?android:attr/selectableItemBackground android:paddingHorizontal16dp !-- 头像圆形通过 CardView 或自定义 ShapeableImageView 实现 -- com.google.android.material.imageview.ShapeableImageView android:idid/iv_avatar android:layout_width48dp android:layout_height48dp android:scaleTypecenterCrop app:shapeAppearanceOverlaystyle/CircleImage app:layout_constraintTop_toTopOfparent app:layout_constraintBottom_toBottomOfparent app:layout_constraintStart_toStartOfparent / !-- 昵称 -- TextView android:idid/tv_name android:layout_width0dp android:layout_heightwrap_content android:textSize16sp android:textColor#191919 android:maxLines1 android:ellipsizeend app:layout_constraintTop_toTopOfid/iv_avatar app:layout_constraintStart_toEndOfid/iv_avatar app:layout_constraintEnd_toStartOfid/tv_time android:layout_marginStart12dp / !-- 消息预览 -- TextView android:idid/tv_message android:layout_width0dp android:layout_heightwrap_content android:textSize14sp android:textColor#999999 android:maxLines1 android:ellipsizeend app:layout_constraintTop_toBottomOfid/tv_name app:layout_constraintStart_toStartOfid/tv_name app:layout_constraintEnd_toEndOfid/tv_name android:layout_marginTop4dp / !-- 时间 -- TextView android:idid/tv_time android:layout_widthwrap_content android:layout_heightwrap_content android:textSize12sp android:textColor#B2B2B2 app:layout_constraintTop_toTopOfid/tv_name app:layout_constraintEnd_toEndOfparent / /androidx.constraintlayout.widget.ConstraintLayoutShapeableImageView配合shapeAppearanceOverlay可以把任意图片裁成圆形比用CardView包一层更轻量。selectableItemBackground给整行加上点击水波纹这是微信列表点击时的反馈效果。昵称和消息预览的宽度设为0dp并约束左右边界这样长文本会自动截断成省略号不会把时间挤出屏幕。3.2 数据模型与 Adapter把假数据灌进列表先定义一个简单的数据类data class ChatItem( val avatarRes: Int, // 头像资源 id val name: String, // 昵称 val message: String, // 消息预览 val time: String // 时间字符串 )Adapter 继承RecyclerView.Adapter在onBindViewHolder里把数据填进视图class ChatAdapter(private val data: ListChatItem) : RecyclerView.AdapterChatAdapter.VH() { class VH(view: View) : RecyclerView.ViewHolder(view) { val avatar: ShapeableImageView view.findViewById(R.id.iv_avatar) val name: TextView view.findViewById(R.id.tv_name) val message: TextView view.findViewById(R.id.tv_message) val time: TextView view.findViewById(R.id.tv_time) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH { val view LayoutInflater.from(parent.context) .inflate(R.layout.item_chat, parent, false) return VH(view) } override fun onBindViewHolder(holder: VH, position: Int) { val item data[position] holder.avatar.setImageResource(item.avatarRes) holder.name.text item.name holder.message.text item.message holder.time.text item.time } override fun getItemCount() data.size }在ChatsFragment的onViewCreated里初始化 RecyclerViewoverride fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val rv view.findViewByIdRecyclerView(R.id.rv_chats) rv.layoutManager LinearLayoutManager(requireContext()) // 模拟数据实际项目里从数据库或网络拿 val mockData listOf( ChatItem(R.drawable.avatar_1, 张三, 晚上一起吃饭, 18:30), ChatItem(R.drawable.avatar_2, 李四, [图片], 17:12), ChatItem(R.drawable.avatar_3, 产品群, 王五需求文档已更新, 16:45) ) rv.adapter ChatAdapter(mockData) }LinearLayoutManager是垂直列表的默认管理器不用额外设置方向。mockData只是占位真实场景里数据来自 Room 数据库或接口返回Adapter 本身不用改只换数据源即可。3.3 分割线与点击事件两个容易漏掉的细节微信列表项之间有一条细分割线从昵称左侧开始、不到屏幕最左边。在 RecyclerView 上加app:divider不行因为那条线要缩进。常见做法是在 item 布局底部加一个 1dp 高的 View左边距设为头像宽度加间距View android:layout_width0dp android:layout_height0.5dp android:background#E5E5E5 app:layout_constraintStart_toStartOfid/tv_name app:layout_constraintEnd_toEndOfparent app:layout_constraintBottom_toBottomOfparent /点击事件在 Adapter 里通过接口回调暴露出去不要在 Adapter 内部直接启动 Activity否则 Adapter 和具体页面耦合太紧。定义一个onItemClick: (Int) - Unit的 lambda在onBindViewHolder里给itemView.setOnClickListener绑定位置参数Fragment 里传入具体跳转逻辑。4. 避坑与排查微信界面复刻里最容易翻车的五件事4.1 底部栏图标显示为灰色方块现象BottomNavigationView的图标位置显示为纯色方块不是预期的图标形状。原因Vector Asset 导入时选了 “Launcher Icons” 类型生成的图标带背景层或者图标本身的fillColor被写死成某个颜色和itemIconTint冲突。解决导入时选 “Action Bar and Tab Icons”生成的 vector 里fillColor用android:color/white让itemIconTint去着色。如果已经导入错了打开 vector xml 把path的fillColor改成#FFFFFFFF即可。4.2 切换 Tab 后 Fragment 重叠现象点了几次底部 Tab再旋转屏幕发现两个 Fragment 的界面叠在一起。原因FragmentManager在 Activity 重建时会自动恢复之前的 Fragment代码里又手动replace了一次导致同一位置有两个 Fragment。解决在onCreate里加savedInstanceState null判断只有首次创建时才手动添加默认 Fragment。或者改用FragmentContainerView的android:name属性指定默认 Fragment让系统自己管理恢复。4.3 列表滑动卡顿、头像加载慢现象会话列表快速滑动时掉帧头像要等一会儿才显示。原因在onBindViewHolder里做了耗时操作比如从网络加载图片、解析大图。RecyclerView 的绑定是在主线程执行的任何耗时都会直接反映为卡顿。解决图片加载交给 Glide 或 Coil在onBindViewHolder里只写一行Glide.with(holder.itemView).load(url).into(holder.avatar)。如果头像是本地资源setImageResource本身很快但图片尺寸不要超过实际显示尺寸否则解码开销白白浪费。4.4 深色模式下文字看不清现象系统切到深色模式后会话列表的昵称变成黑底黑字几乎看不见。原因布局里把textColor写死成#191919背景色又跟随系统变成深色对比度消失。解决颜色统一放到res/values/colors.xml和res/values-night/colors.xml里布局中引用color/text_primary。微信本身没有跟随系统深色模式但如果你要做这是必须处理的一步。4.5 Gradle 同步报资源重复错误现象Android resource compilation failed提示某个资源文件重复。原因从别处拷贝布局或图标时文件名和已有资源冲突或者res/values/strings.xml里定义了同名的 string。解决看报错信息里给出的两个路径保留一个、重命名另一个。Android Studio 的 “Refactor → Rename” 可以连带更新引用比手动改安全。如果是从网上下的 demo 工程先把res/下所有文件过一遍删掉用不到的。5. 让界面“活”起来从静态复刻到可交互原型的三个进阶技巧第一个技巧是用ViewPager2替换Fragment的手动replace。微信主界面左右滑动可以在四个 Tab 之间切换这个效果用ViewPager2配合BottomNavigationView很容易实现。把ViewPager2放在内容区Adapter 用FragmentStateAdapter然后在onPageSelected里同步底部栏的选中项在底部栏的setOnItemSelectedListener里调viewPager.setCurrentItem。两边互相监听时注意加一个标志位防止循环触发这是血泪经验不加标志位点一下 Tab 会来回跳好几次。第二个技巧是给会话列表加ItemTouchHelper实现侧滑。微信列表项左滑会出现“标为未读”和“删除”两个按钮。ItemTouchHelper的onChildDraw里根据滑动距离绘制背景和按钮onSwiped里执行删除逻辑。删除后记得调adapter.notifyItemRemoved(position)并且把数据源里对应项也移除否则下次绑定会越界。这个功能看起来简单但滑动阈值、按钮宽度、回弹动画三个参数要一起调建议先在纸上画一下滑动距离和按钮出现时机的关系。第三个技巧是用SharedElementTransition做头像放大过渡。从会话列表点进聊天详情页时列表里的头像平滑放大到详情页顶部这个效果在微信里也有。核心是给两个页面的头像 View 设置相同的transitionName然后在FragmentTransaction里加addSharedElement。注意transitionName必须在两个 Fragment 的布局里都写而且值要唯一用avatar_${item.id}这种格式。如果过渡时图片闪一下检查两个 ImageView 的scaleType是否一致不一致会导致裁剪区域跳变。最后说一个验证方法把布局里的所有硬编码尺寸换成dp和sp后在 Android Studio 的 Layout Inspector 里连真机跑一遍看每个 View 的实际测量宽高是否符合预期。我习惯在开发者选项里打开“显示布局边界”一眼就能看出哪个 View 多占了空间、哪个约束没生效。这个习惯帮我省掉了大量“为什么这里多了一条缝”的排查时间。希望帮到你。本文还有配套的精品资源点击获取