Android Studio仿微信界面:底部导航、Fragment与RecyclerView实战
简介面向 Android 入门与中级开发者的微信风格界面实战项目使用 Android Studio 完成类微信 App UI涵盖底部导航、聊天列表、个人信息页等核心模块。压缩包共 1041 个文件、约 24.25MB以 XML 布局、Java 源码、Gradle 构建脚本为主并附 PNG 图片、APK 安装包及配置和资源文件便于对照工程学习。已有 3102 人学习下载。项目从 ConstraintLayout 布局、BottomNavigationView 与 RecyclerView 组件到 Adapter 适配器、Glide 图片加载、VectorDrawable 图标、主题配色和动画过渡均有完整示例还包含 .git 版本控制目录与更新记录可看到界面美化迭代过程。读者可拿到可运行工程与成品 APK既适合课程设计、自学实训也可作为求职作品原型帮助理解 Android 界面搭建与优化全流程。1. 仿微信界面不是切图九宫格先看懂这四块 Tab 背后的组件选型同样是“用 Android Studio 制作微信的界面”有人两天交出一套能安装的 App 壳子有人卡在第一天因为把界面当成一张大图画在布局文件里堆控件堆到一半发现加一个搜索框要动十几个地方的尺寸。仿微信界面真正要练的不是截图而是把微信拆成底部导航、Fragment 容器、列表页三块底部导航解决四个 Tab 怎么切换Fragment 容器决定每个页面怎么独立存活RecyclerView 负责把会话记录和联系人高效地铺到屏幕上。这个技术方向适合刚开始学 Android 布局的人、做移动端课程设计的同学以及想快速给团队做内部工具壳的开发者。做完你会得到一个能编译、能安装、能点击跳转的 App 骨架而不是一张静态原型图。2. 用 Android Studio 建项目并搭起底部导航先让四个 Tab 能切换2.1 新建工程空 Activity、最低 SDK 和语言怎么选先说我踩过的第一个坑新建项目时选错模板后面全是麻烦。在 Android Studio 的欢迎页选择 New Project 后会看到空 Activity、Empty Views Activity、Compose 多个模板。仿微信界面这种传统布局一定选 Empty Views Activity它的英文直译是“空视图活动”生成的是经典的 XML 布局加 Activity 结构网上绝大多数微信仿制教程都基于这套写法。如果你手滑选了 Compose 模板后面所有代码都变成 Compose 状态驱动的写法虽然也能做界面但和你要找的“微信界面”参考代码完全对不上。最低 SDK 建议选 API 21。别追求新版本也别为了兼容老机型拉低到十几API 21 覆盖了绝大多数存量设备而且 BottomNavigationView、RecyclerView 这些控件在这个版本上表现稳定。语言选 Java 还是 Kotlin 看你手头的资料后续代码我按 Java 写Kotlin 的语法等价照着改就行。新装的 Android Studio 默认是英文界面想省事可以在 Settings 里搜插件 Chinese Language Pack 装一个中文语言包不影响下面任何操作。2.2 底部导航栏的菜单与样式文件最外层布局就是一个纵向 LinearLayout上面是放 Fragment 的容器下面是 BottomNavigationView。微信四个 Tab 正好对应底部导航的四个菜单项菜单项少于三个时 Material 组件会强制切换成“位移模式”微信恰好是四个不会触发这个限制。LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:apphttp://schemas.android.com/apk/res-auto android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical FrameLayout 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:labelVisibilityModelabeled app:menumenu/bottom_nav_menu / /LinearLayoutFrameLayout 的layout_height0dp加layout_weight1是 LinearLayout 里“占满剩余高度”的标准写法这样底部导航高度固定内容区自动填满其余空间。labelVisibilityModelabeled表示四个 Tab 始终同时显示图标和文字微信的导航栏就是这种样式。再在res/menu目录下新建bottom_nav_menu.xml菜单项的顺序和微信保持一致微信、通讯录、发现、我。menu xmlns:androidhttp://schemas.android.com/apk/res/android item android:idid/nav_chat android:icondrawable/ic_tab_chat android:title微信 / item android:idid/nav_contacts android:icondrawable/ic_tab_contacts android:title通讯录 / item android:idid/nav_find android:icondrawable/ic_tab_find android:title发现 / item android:idid/nav_me android:icondrawable/ic_tab_me android:title我 / /menu这里的四个 icon 不需要去网上下载 PNG。在 Android Studio 里右键res/drawable目录选择 New 下的 Vector Asset系统自带一套 Material 图标库搜索“chat”“contact”“explore”“person”就能生成对应的矢量图。矢量图不会像位图一样在不同分辨率下糊掉这也是微信这类 App 的图标选择逻辑。提示如果底部导航运行后只显示一条灰线或者图标消失先检查菜单 item 里的android:icon和android:title是否都写了缺一项都会导致显示异常这个问题在第 5 章还会展开讲。2.3 Fragment 容器与 Tab 切换逻辑为什么我用 show/hide 而不是 replace新建四个 Fragment 类比如ChatFragment、ContactsFragment、FindFragment、MeFragment每个类先写一个最小实现public class ChatFragment extends Fragment { Override public View onCreateView(NonNull LayoutInflater inflater, Nullable ViewGroup container, Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_chat, container, false); } }container是 FragmentManager 传入的父容器最后一个参数传false表示不要把布局立刻添加到 container而是交给系统统一管理。新手最容易在这里误传true导致 Fragment 被重复添加切换几次后界面叠加。MainActivity 里的切换逻辑我一般这样写public class MainActivity extends AppCompatActivity implements BottomNavigationView.OnItemSelectedListener { private final ChatFragment chatFragment new ChatFragment(); private final ContactsFragment contactsFragment new ContactsFragment(); private final FindFragment findFragment new FindFragment(); private final MeFragment meFragment new MeFragment(); private Fragment currentFragment; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); BottomNavigationView bottomNav findViewById(R.id.bottom_nav); bottomNav.setOnItemSelectedListener(this); bottomNav.setSelectedItemId(R.id.nav_chat); } Override public boolean onNavigationItemSelected(NonNull MenuItem item) { int id item.getItemId(); if (id R.id.nav_chat) { return switchTo(chatFragment); } else if (id R.id.nav_contacts) { return switchTo(contactsFragment); } else if (id R.id.nav_find) { return switchTo(findFragment); } else if (id R.id.nav_me) { return switchTo(meFragment); } return false; } private boolean switchTo(Fragment target) { if (currentFragment target) { return true; } FragmentTransaction transaction getSupportFragmentManager().beginTransaction(); if (currentFragment ! null) { transaction.hide(currentFragment); } if (!target.isAdded()) { transaction.add(R.id.fragment_container, target); } else { transaction.show(target); } transaction.commit(); currentFragment target; return true; } }这里的核心是hide()加show()的组合。网上很多教程用replace()每次切换都把旧 Fragment 销毁、新 Fragment 重建表面看没问题但会话列表滚动到一半再切回来就回到顶部了Fragment 内部的临时状态也全丢。show/hide方案保留所有实例切换成本低微信那种“切走再切回来位置不变”的体验靠的就是这套机制。setOnItemSelectedListener是新版 Material 库的推荐接口旧项目里见过setOnNavigationItemSelectedListener效果一样别被两个名字搞混。3. 复刻“微信”会话列表RecyclerView Adapter 的完整落地3.1 TopBar 布局与状态栏配色骨架搭好后先从信息量最大的“微信”Tab 开始。微信的顶部栏是一个白底、高度约 56dp、左边标题右边加号的横条。传统写法是在 Fragment 布局里直接画一个 LinearLayout 当标题栏不依赖系统 ActionBar因为微信的标题栏是自定义样式ActionBar 反而碍事。LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:background#EDEDED android:orientationvertical LinearLayout android:layout_widthmatch_parent android:layout_height56dp android:backgroundandroid:color/white android:gravitycenter_vertical android:orientationhorizontal TextView android:layout_width0dp android:layout_heightwrap_content android:layout_marginStart16dp android:layout_weight1 android:text微信 android:textColor#111111 android:textSize18sp android:textStylebold / TextView android:layout_widthwrap_content android:layout_heightwrap_content android:padding16dp android:text android:textColor#111111 android:textSize24sp android:gravitycenter / /LinearLayout androidx.recyclerview.widget.RecyclerView android:idid/rv_chat android:layout_widthmatch_parent android:layout_heightmatch_parent / /LinearLayout顶部栏里右侧的加号先用一个空格占位后面接状态栏问题时再补图标。这里关键的是给根布局设置浅灰背景#EDEDED这是微信界面“列表白、背景灰”的分层逻辑列表和背景有了对比界面才不糊成一片。TopBar 用layout_weight1把标题推到左侧右侧控件自动靠右。状态栏配色这块容易翻车。微信的顶部栏是白色状态栏应该也是白色并且状态栏上的时间、信号图标要用深色否则白底白字什么都看不见。在values/themes.xml里把状态栏颜色改掉style nameTheme.WeChatMocker parentTheme.Material3.DayNight.NoActionBar item nameandroid:statusBarColorandroid:color/white/item item nameandroid:windowLightStatusBartrue/item /stylestatusBarColor设置状态栏背景色windowLightStatusBar设置为true后状态栏图标变深色。如果项目主题是旧的 Material 组件库把 parent 换成对应的Theme.MaterialComponents.DayNight.NoActionBar即可效果一致。我把这里单独拿出来说是因为状态栏和 TopBar 颜色不一致时仿出来的界面一眼假。3.2 会话的数据模型与示例数据写列表页的第一步是先定义数据模型再写死一组数据驱动界面。先建一个ChatItem类字段对齐微信会话列表展示的内容昵称、最后一条消息、时间、头像资源。public class ChatItem { private final String name; private final String message; private final String time; private final int avatarRes; public ChatItem(String name, String message, String time, int avatarRes) { this.name name; this.message message; this.time time; this.avatarRes avatarRes; } public String getName() { return name; } public String getMessage() { return message; } public String getTime() { return time; } public int getAvatarRes() { return avatarRes; } }再写一个MockData类提供三天数据。头像先用 Vector Asset 生成的默认图标占位这阶段的目标是把布局跑起来不需要纠结真实图片。public class MockData { public static ListChatItem chatItems() { ListChatItem list new ArrayList(); list.add(new ChatItem(Android 开发助手, 构建成功APK 已生成, 09:41, R.drawable.ic_avatar_1)); list.add(new ChatItem(文件传输助手, [图片], 09:38, R.drawable.ic_avatar_2)); list.add(new ChatItem(产品设计群, 周报记得提交哈, 昨天, R.drawable.ic_avatar_3)); list.add(new ChatItem(老妈, 周末回来吃饭吗, 昨天, R.drawable.ic_avatar_4)); return list; } }写死数据的意义在于界面开发阶段不依赖后端数据源只是内存里的一个 List布局错了改布局逻辑错了改逻辑。等到接真数据时只需要把ListChatItem换成接口返回的列表Adapter 完全不用动。这比一开始就接数据库或网络接口省力得多。3.3 RecyclerView 的 Adapter 与条目复用逻辑先写会话列表的单条布局item_rv_chat.xml这是整个列表页的地基布局没调好后面 Adapter 写得再漂亮也白搭。LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_height72dp android:backgroundandroid:color/white android:gravitycenter_vertical android:orientationhorizontal android:paddingHorizontal12dp ImageView android:idid/iv_avatar android:layout_width48dp android:layout_height48dp android:background#CCCCCC android:scaleTypecenterCrop / LinearLayout android:layout_width0dp android:layout_heightwrap_content android:layout_marginStart12dp android:layout_weight1 android:orientationvertical LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationhorizontal TextView android:idid/tv_name android:layout_width0dp android:layout_heightwrap_content android:layout_weight1 android:textColor#111111 android:textSize16sp android:textStylebold / TextView android:idid/tv_time android:layout_widthwrap_content android:layout_heightwrap_content android:textColor#999999 android:textSize12sp / /LinearLayout TextView android:idid/tv_message android:layout_widthmatch_parent android:layout_heightwrap_content android:layout_marginTop4dp android:ellipsizeend android:maxLines1 android:textColor#666666 android:textSize14sp / /LinearLayout /LinearLayout条目高度固定 72dp这是微信会话常驻高度的视觉规律比这个高显得拥挤低了点按不方便。头像固定 48dp 并且scaleTypecenterCrop保证图片等比例裁剪填满而不变形。消息预览的maxLines1配合ellipsizeend让超长文本显示成省略号跟微信最多显示一行的行为一致。Adapter 是列表的灵魂注意几个细节public class ChatAdapter extends RecyclerView.AdapterChatAdapter.ViewHolder { private final ListChatItem data; private final OnItemClickListener listener; public interface OnItemClickListener { void onItemClick(ChatItem item); } public ChatAdapter(ListChatItem data, OnItemClickListener listener) { this.data data; this.listener listener; } NonNull Override public ViewHolder onCreateViewHolder(NonNull ViewGroup parent, int viewType) { View view LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_rv_chat, parent, false); return new ViewHolder(view); } Override public void onBindViewHolder(NonNull ViewHolder holder, int position) { ChatItem item data.get(position); holder.ivAvatar.setImageResource(item.getAvatarRes()); holder.tvName.setText(item.getName()); holder.tvMessage.setText(item.getMessage()); holder.tvTime.setText(item.getTime()); holder.itemView.setOnClickListener(v - { if (listener ! null) { listener.onItemClick(item); } }); } Override public int getItemCount() { return data null ? 0 : data.size(); } static class ViewHolder extends RecyclerView.ViewHolder { ImageView ivAvatar; TextView tvName; TextView tvMessage; TextView tvTime; ViewHolder(NonNull View itemView) { super(itemView); ivAvatar itemView.findViewById(R.id.iv_avatar); tvName itemView.findViewById(R.id.tv_name); tvMessage itemView.findViewById(R.id.tv_message); tvTime itemView.findViewById(R.id.tv_time); } } }ViewHolder的作用是缓存 item 里所有 View 的引用onBindViewHolder直接拿引用改数据不用每次滑动都findViewById这是 RecyclerView 不卡的关键。点击监听器通过构造参数传入而不是在onBindViewHolder里 new避免滚动时反复创建对象。onCreateViewHolder只负责 inflate 布局不做数据绑定这条规则能保证列表滑动流畅。在ChatFragment的onViewCreated里把 Adapter 交给 RecyclerViewRecyclerView recyclerView view.findViewById(R.id.rv_chat); recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); recyclerView.setHasFixedSize(true); ChatAdapter adapter new ChatAdapter(MockData.chatItems(), item - { // 这里先留空第 6 章接跳转 }); recyclerView.setAdapter(adapter);LinearLayoutManager控制列表纵向排列setHasFixedSize(true)告诉 RecyclerView 条目尺寸固定省去重新测量布局的开销。如果你之后想加分割线用一个DividerItemDecoration就可以不需要改 Adapter。4. 通讯录与发现页同一套列表思路的复用与差异4.1 通讯录用多 ItemViewType固定入口区加联系人列表通讯录页在视觉上和会话列表一样是白底列表但多了一块“新的朋友、群聊、标签、公众号”四个固定入口。新手最常见的做法是把四个入口写死在 Activity 布局里下面再接 RecyclerView结果入口区和列表之间隔着一条难看的边线滚动时入口区也不会跟着列表走。微信的这四个入口其实是列表的一部分跟着整个列表一起滚动。用 RecyclerView 的多 ItemViewType 来实现把列表第一条数据当成 Header第二条开始才是联系人。public class ContactsAdapter extends RecyclerView.AdapterRecyclerView.ViewHolder { private static final int TYPE_HEADER 0; private static final int TYPE_CONTACT 1; private final ListString contacts; public ContactsAdapter(ListString contacts) { this.contacts contacts; } Override public int getItemViewType(int position) { return position 0 ? TYPE_HEADER : TYPE_CONTACT; } NonNull Override public RecyclerView.ViewHolder onCreateViewHolder(NonNull ViewGroup parent, int viewType) { if (viewType TYPE_HEADER) { View header LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_contacts_header, parent, false); return new HeaderViewHolder(header); } else { View itemView LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_contact, parent, false); return new ContactViewHolder(itemView); } } Override public void onBindViewHolder(NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceof HeaderViewHolder) { return; } ContactViewHolder contactHolder (ContactViewHolder) holder; String name contacts.get(position - 1); contactHolder.tvName.setText(name); } Override public int getItemCount() { return contacts.size() 1; } static class HeaderViewHolder extends RecyclerView.ViewHolder { HeaderViewHolder(NonNull View itemView) { super(itemView); } } static class ContactViewHolder extends RecyclerView.ViewHolder { TextView tvName; ContactViewHolder(NonNull View itemView) { super(itemView); tvName itemView.findViewById(R.id.tv_contact_name); } } }getItemViewType根据 position 返回不同的类型onCreateViewHolder按类型 inflate 不同的布局onBindViewHolder里用instanceof区分 ViewHolder。这段逻辑里最容易被忽略的是数据下标偏移Header 占了第 0 位所以联系人数据从position - 1取getItemCount也要在联系人数量上加 1漏掉任何一个都会导致列表多一条空行或崩溃。通讯录需要右侧字母索引条但那属于进阶能力静态界面阶段先不做。真要加的时候在 Fragment 根布局的 FrameLayout 右侧叠一个竖排 TextView监听触摸滑动的 position 映射到联系人分组思路不复杂就是需要花时间调坐标。4.2 发现页用 ScrollView 加条目组为什么这里不硬套 RecyclerView发现页是微信四个 Tab 里唯一不适合 RecyclerView 的页面。它一共就十几个固定入口数据量不会随着用户操作增长也没有动态增删的场景。硬套 RecyclerView 意味着你要为每一个小组写一套多类型的 Adapter代码量翻倍还看不出任何收益。我的做法是 ScrollView 包 LinearLayout发现页的界面结构就是两个分组块第一组“朋友圈、扫一扫、搜一搜”第二组“小程序、视频号”。每个分组块用一个纵向 LinearLayout 装条目块与块之间用一个 8dp 高度的灰色 View 隔开。ScrollView xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:background#EDEDED LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationvertical View android:layout_widthmatch_parent android:layout_height8dp / LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:backgroundandroid:color/white android:orientationvertical LinearLayout stylestyle/WeChatMenuItem android:onClickonFindItemClick TextView stylestyle/WeChatMenuText android:text朋友圈 / /LinearLayout !-- 扫一扫、搜一搜同理 -- /LinearLayout View android:layout_widthmatch_parent android:layout_height8dp / LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:backgroundandroid:color/white android:orientationvertical LinearLayout stylestyle/WeChatMenuItem android:onClickonFindItemClick TextView stylestyle/WeChatMenuText android:text小程序 / /LinearLayout /LinearLayout /LinearLayout /ScrollView这里看到了重复的条目样式我直接抽到styles.xml里避免在同页面里复制粘贴三次。样式文件是 Android 布局的“后悔药”通篇复用的尺寸、颜色都应该抽出来改起来只动一处。style nameWeChatMenuItem item nameandroid:layout_widthmatch_parent/item item nameandroid:layout_height52dp/item item nameandroid:backgroundandroid:color/white/item item nameandroid:gravitycenter_vertical/item item nameandroid:orientationhorizontal/item /style style nameWeChatMenuText item nameandroid:layout_widthwrap_content/item item nameandroid:layout_heightwrap_content/item item nameandroid:layout_marginStart16dp/item item nameandroid:textSize16sp/item item nameandroid:textColor#111111/item /style一个最常见的反面写法是ScrollView 里再套 RecyclerView。因为 RecyclerView 自己就是可滚动容器嵌套进去后内部列表的高度永远计算不对要么只显示一两行要么滑动冲突导致界面卡顿。固定条目用 ScrollView 包 LinearLayout 是最安全的。判断标准其实很简单数据是否动态变化动态的交给 RecyclerView固定的直接用 LinearLayout。4.3 把列表 Fragment 抽成模板四个 Tab 的骨架统一做完通讯录和发现页你应该已经发现微信、通讯录这两个 Tab 的骨架高度重合顶部一个 56dp 的标题栏下面一个列表只是数据和条目布局不同。如果四个 Fragment 各写各的之后要统一加下拉刷新或者统一改背景色就要改四个文件。我一般会抽一个BaseListFragment当基类public abstract class BaseListFragment extends Fragment { protected RecyclerView recyclerView; Override public View onCreateView(NonNull LayoutInflater inflater, Nullable ViewGroup container, Nullable Bundle savedInstanceState) { View view inflater.inflate(getLayoutResId(), container, false); recyclerView view.findViewById(R.id.rv_common); return view; } Override public void onViewCreated(NonNull View view, Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); RecyclerView.LayoutManager layoutManager onCreateLayoutManager(); if (layoutManager ! null) { recyclerView.setLayoutManager(layoutManager); } recyclerView.setAdapter(onCreateAdapter()); } protected int getLayoutResId() { return R.layout.fragment_common_list; } protected abstract RecyclerView.LayoutManager onCreateLayoutManager(); protected abstract RecyclerView.Adapter? onCreateAdapter(); }子类只需要实现两个抽象方法提供 LayoutManager 和 Adapter公共的布局文件和生命周期节奏全在基类里管好。这个模板化的度要拿捏微信四个 Tab 里还有“我”这个完全不是列表的页面它就不该硬塞进这个基类。抽象是为了减少重复不是为了强制统一。5. 仿微信界面避坑清单五个高频翻车点与排查办法5.1 顶部栏被状态栏盖住标题跑到电池图标下面现象装上应用后“微信”两个字顶到屏幕最上方和状态栏的时间、电量图标叠在一起整个标题栏像被“吃掉”了一截。原因Android 5.0 之后应用窗口默认会绘制到状态栏下面布局不会自动避让。如果没有给窗口设置不透明的状态栏背景也没有给布局加避让属性内容就会直接铺进状态栏区域。解决两条路任选。第一条是在主题里直接设置一个不透明的statusBarColor我在 3.1 节已经写了这是最省事的方法。第二条是在 Fragment 的根布局上添加android:fitsSystemWindowstrue系统会给根布局补一个状态栏高度的 padding把整个内容往下推。注意fitsSystemWindows要加在最外层根节点上加在子布局上是没效果的。这两条只选其一同时用会双重避让顶部出现一块多余的空白。5.2 底部导航只显示图标不显示文字选中颜色也不对现象BottomNavigationView 运行后四个 Tab 只有图标没有文字或者文字和图标全是同一个颜色选中的 Tab 看不出来。原因Material 库的 BottomNavigationView 默认从主题的colorPrimary取选中色从colorOnSurface取未选中色。如果主题没配好或者图标本身带有颜色就会显示成一片黑或一片灰。文字不显示通常是labelVisibilityMode没设成labeled默认的auto模式在 Tab 数量较多时会把文字隐藏。解决给 BottomNavigationView 显式设置图标和文字的颜色选择器。在res/color下新建tab_selector.xmlselector xmlns:androidhttp://schemas.android.com/apk/res/android item android:color#111111 android:state_checkedtrue / item android:color#999999 / /selector然后在布局里给底部导航加上app:itemIconTintcolor/tab_selector和app:itemTextColorcolor/tab_selector同时确认labelVisibilityModelabeled。选中态用深色未选中态用浅灰这是微信导航的标准配色逻辑。5.3 头像被拉伸变形图片变成椭圆形现象列表里设置的头像图片被左右或上下拉宽圆形或方形的头像变成了椭圆严重时图片盖住了旁边的文字。原因ImageView 的宽高设置不当。常见写法是高度写wrap_content宽度写match_parent图片为了填满控件就被拉伸了。另一个原因是scaleType默认是fitCenter某些情况下配合adjustViewBounds会导致控件尺寸跟随图片比例变化。解决ImageView 固定宽高比如 48dp 乘 48dpscaleTypecenterCrop。centerCrop的意思是图片按比例缩放然后裁剪掉多余部分完整填满控件且不变形。这是列表头像的标准配置大部分头像变形问题都是少写了这一行属性。5.4 列表页滑动掉帧UI 界面卡顿明显现象会话列表或通讯录快速滑动时页面一顿一顿帧率明显下降严重时滑动时整个界面跟手性变差。原因常见的有三个。一是条目布局嵌套层级过深系统在测量布局时反复计算嵌套的 LinearLayout消耗大量性能二是在onBindViewHolder里做了耗时操作比如 new 对象、读取资源、解析字符串三是碎片切换用了replace()每次切换都重新 inflate 整个页面。解决条目布局能用 ConstraintLayout 压成一层就压成一层我的item_rv_chat.xml用了三层 LinearLayout 其实是偏保守的写法层级每少一层卡顿风险就低一分。在onBindViewHolder里只做赋值不 new 对象不写循环。碎片切换统一用 2.3 节的show/hide方案。排查时用 Android Studio 自带的 Layout Inspector 打开运行中的应用看每个 View 的层级树超过十层就值得优化。这个问题的排查思路是“布局层级做减法”不是靠玄学调手机。5.5 onBackPressed 返回键拦不住直接退出了应用现象在聊天页想按返回键回到会话列表结果应用直接退到桌面或者你在 Activity 里重写了onBackPressed()方法发现断点根本不进来。原因从 Android 13 开始Activity.onBackPressed()被官方标记为废弃系统的返回事件统一交给OnBackPressedDispatcher处理。很多旧教程教你重写onBackPressed()在新版本上这个方法完全不会被调用。这是个典型的新版系统干掉老写法的案例。解决在 Fragment 里注册OnBackPressedCallbackrequireActivity().getOnBackPressedDispatcher().addCallback( getViewLifecycleOwner(), new OnBackPressedCallback(true) { Override public void handleOnBackPressed() { // 在这里写返回时的逻辑比如回到列表页 if (needHandle) { // 你的处理逻辑 setEnabled(false); requireActivity().getOnBackPressedDispatcher().onBackPressed(); } else { setEnabled(false); requireActivity().getOnBackPressedDispatcher().onBackPressed(); } } } );用getViewLifecycleOwner()而不是this是为了让回调跟着 Fragment 的视图生命周期走Fragment 销毁时回调自动解绑防止内存泄漏。setEnabled(false)之后调用系统默认行为就能把事件继续往下传递。这条踩坑记录送给所有在旧教程里复制过onBackPressed()的同学。6. 把界面从“像”调到“真”细节打磨与验证方法6.1 圆角头像与红点数字细节决定仿制的成败。微信的头像是圆角方块普通 ImageView 是直角一眼就能看出来。Material 库提供了ShapeableImageView专门处理这类形状裁剪。把 3.3 节的头像 ImageView 换成ShapeableImageView然后在styles.xml里定义一个圆角样式style nameCircleImageStyle item namecornerFamilyrounded/item item namecornerSize24dp/item /style头像宽高是 48dp一个角 24dp 的圆角刚好把上下左右四个角收成半圆形成微信那种“圆角方块”效果。布局里的引用写法是com.google.android.material.imageview.ShapeableImageView android:idid/iv_avatar android:layout_width48dp android:layout_height48dp app:shapeAppearanceOverlaystyle/CircleImageStyle app:srcCompatdrawable/ic_avatar_1 /红点数字是另一个辨识度极高的细节。底部导航的“微信”Tab 上那个未读消息数用 Material 的BadgeDrawable可以几行代码完成BottomNavigationView bottomNav findViewById(R.id.bottom_nav); BadgeDrawable badge bottomNav.getOrCreateBadge(R.id.nav_chat); badge.setVisible(true); badge.setNumber(12);getOrCreateBadge只在第一次调用时创建实例之后重复调用只是改数字不会叠加出多个红点。这个方法比手绘一个 TextView 叠在导航上省心得多位置和大小由系统适配。6.2 三种验证方法界面做完不是“看着像”就行。我习惯用三种方式验证Layout Inspector 检查层级多分辨率模拟器检查适配系统字体调大检查溢出。Layout Inspector 在 Android Studio 的 Tools 菜单里选择正在运行的进程能看到每个页面的完整视图树。检查目标是列表条目的 View 层级主内容区整体深度超过十二层就要考虑压层级同时检查有没有控件宽高为 0 或者超出屏幕。多分辨率模拟器验证用两个配置一个 Pixel 2 的 1080 乘 1920一个 Pixel 5 的 1080 乘 2340覆盖了最主流的屏幕比例。重点看底部导航是否紧贴屏幕底部、顶部栏是否和状态栏和谐、列表条目在不同宽度下名字和时间有没有重叠。字体溢出验证容易被忽略。在模拟器的系统设置里把“字体大小”和“显示大小”都调到最大回应用里看标题栏文字是否被截断、会话预览是否变成两行。哪个控件在这时变形哪个控件的尺寸就没写对。6.3 补上界面跳转让静态界面活起来最后一个收尾动作是给会话列表补上点击跳转让整个项目从“静态界面”变成“可交互原型”。在 3.3 节的 Adapter 里已经留好了点击监听的接口在ChatFragment里接上ChatAdapter adapter new ChatAdapter(MockData.chatItems(), item - { Intent intent new Intent(getActivity(), ChatActivity.class); intent.putExtra(name, item.getName()); startActivity(intent); });putExtra把会话名字传给ChatActivity聊天页的标题栏就可以动态变成对应的会话名。这是界面跳转里最基础的一种Intent 传值加startActivity也是让整个仿制项目从“能看”变成“能点”的关键一步。我早期照葫芦画瓢做过一版仿微信头像全被拉伸成椭圆被同事笑了一周。后来养成的习惯是把“能否通过布局层级检查”当完成标准不再只看截图像不像。这个方向值不值得投入如果目标是练手它是性价比最高的 Android UI 入门题如果是给正式产品做原型仿界面只是第一步后面接业务逻辑才是大头。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →