简介:这是一份面向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 version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <!-- 内容区:四个 Fragment 轮流住进来 --> <androidx.fragment.app.FragmentContainerView android:id="@+id/fragment_container" android:layout_width="0dp" android:layout_height="0dp" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toTopOf="@id/bottom_nav" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" /> <!-- 底部导航栏:菜单资源在 res/menu 下 --> <com.google.android.material.bottomnavigation.BottomNavigationView android:id="@+id/bottom_nav" android:layout_width="0dp" android:layout_height="wrap_content" android:background="@android:color/white" app:menu="@menu/menu_bottom_nav" app:labelVisibilityMode="labeled" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>FragmentContainerView的高度设为0dp配合约束,意思是“撑满底部栏以上的所有空间”,这样不同屏幕高度下内容区自动伸缩。labelVisibilityMode="labeled"让四个 Tab 的文字始终显示,微信就是这种效果;如果设成auto,选中项显示文字、未选中只显示图标,那是另一种风格。
2.4 菜单资源与图标:四个 Tab 的声明式定义
在res/menu/下新建menu_bottom_nav.xml:
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/nav_chats" android:icon="@drawable/ic_chat" android:title="微信" /> <item android:id="@+id/nav_contacts" android:icon="@drawable/ic_contacts" android:title="通讯录" /> <item android:id="@+id/nav_discover" android:icon="@drawable/ic_discover" android:title="发现" /> <item android:id="@+id/nav_me" android:icon="@drawable/ic_me" android:title="我" /> </menu>图标建议用 Vector Asset 导入 Material 图标,不要直接切 PNG。Vector 图标在任何分辨率下都清晰,而且可以用app:itemIconTint统一控制选中和未选中的颜色。微信的选中色是绿色#07C160,未选中是灰色#999999,在BottomNavigationView上加一行app:itemIconTint="@color/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 = findViewById<BottomNavigationView>(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 -> return@setOnItemSelectedListener false } supportFragmentManager.beginTransaction() .replace(R.id.fragment_container, target) .commit() true } } }这里用by lazy让 Fragment 只在第一次访问时创建,切换回来时复用同一个实例,避免重复初始化。savedInstanceState == null的判断是为了防止屏幕旋转后 Fragment 被系统重建、又手动添加一次导致重叠。这个坑很常见:旋转屏幕后界面出现两层内容,就是没加这个判断。
3. 会话列表页:用 RecyclerView 把微信聊天列表跑起来
3.1 列表项布局:头像、昵称、消息、时间四件套
微信会话列表每一行的结构很固定:左边圆角头像,中间上下两行分别是昵称和消息预览,右边上方是时间、下方可能有未读红点。用ConstraintLayout搭这一行:
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="72dp" android:background="?android:attr/selectableItemBackground" android:paddingHorizontal="16dp"> <!-- 头像:圆形通过 CardView 或自定义 ShapeableImageView 实现 --> <com.google.android.material.imageview.ShapeableImageView android:id="@+id/iv_avatar" android:layout_width="48dp" android:layout_height="48dp" android:scaleType="centerCrop" app:shapeAppearanceOverlay="@style/CircleImage" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" /> <!-- 昵称 --> <TextView android:id="@+id/tv_name" android:layout_width="0dp" android:layout_height="wrap_content" android:textSize="16sp" android:textColor="#191919" android:maxLines="1" android:ellipsize="end" app:layout_constraintTop_toTopOf="@id/iv_avatar" app:layout_constraintStart_toEndOf="@id/iv_avatar" app:layout_constraintEnd_toStartOf="@id/tv_time" android:layout_marginStart="12dp" /> <!-- 消息预览 --> <TextView android:id="@+id/tv_message" android:layout_width="0dp" android:layout_height="wrap_content" android:textSize="14sp" android:textColor="#999999" android:maxLines="1" android:ellipsize="end" app:layout_constraintTop_toBottomOf="@id/tv_name" app:layout_constraintStart_toStartOf="@id/tv_name" app:layout_constraintEnd_toEndOf="@id/tv_name" android:layout_marginTop="4dp" /> <!-- 时间 --> <TextView android:id="@+id/tv_time" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="12sp" android:textColor="#B2B2B2" app:layout_constraintTop_toTopOf="@id/tv_name" app:layout_constraintEnd_toEndOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>ShapeableImageView配合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: List<ChatItem>) : RecyclerView.Adapter<ChatAdapter.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里初始化 RecyclerView:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val rv = view.findViewById<RecyclerView>(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_width="0dp" android:layout_height="0.5dp" android:background="#E5E5E5" app:layout_constraintStart_toStartOf="@id/tv_name" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent" />点击事件在 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 多占了空间、哪个约束没生效。这个习惯帮我省掉了大量“为什么这里多了一条缝”的排查时间。希望帮到你。
本文还有配套的精品资源,点击获取