恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

仿美团外卖菜单实战:数据模型与RecyclerView联动全解析

  • 首页
  • 资讯中心
  • /
  • 仿美团外卖菜单实战:数据模型与RecyclerView联动全解析

相关资讯

Node.js 发布节奏演进深度解读:从一年双版本到年度单版本与 Alpha 通道 2026/9/17 3:23:53
Notepad-- 完整快速上手指南:面向中文开发者的跨平台文本编辑器,文件对比与编码转换一次搞定 2026/9/17 3:23:53
火电机组协调控制Simulink高保真建模与工程落地 2026/9/17 3:18:53

最新资讯

Windows 10安装错误“无法判断”排查指南:从日志到分区表全解决
魔百盒改Linux服务器,从吃灰到SSH连通只要40分钟
Kubernetes SIG Scheduling 2021 年度技术回顾:调度重排队优化、抢占性能提升与调度框架组件配置演进
Home Assistant Insteon 集成:使用 `insteon.print_im_all_link_database` 操作打印调制解调器 All-Link 数据库
使用 EIM GUI 激活 ESP-IDF 开发环境:打开 IDF 终端完整指南
Python第五次作业全解析:函数、文件读写与爬虫入门实战

今日推荐

每日热评|13% 的 Agent 技能带严重漏洞,这个注册表想用“验证+签名”解决信任危机
即梦AI保姆级教程:从生图到数字人,一站式搞定AI视频创作
BERT+LLM混合架构:突破NER长尾实体抽取瓶颈的工程实践

本周热门

AI SDK Harness 依赖更新指南:掌握 harness 包 SDK 依赖的升级、桥接同步与一致性校验
Refine v5 Ant Design NumberField 组件实战:基于 Intl 的本地化数字格式化
Flutter应用改名全指南:从Android到iOS的配置与工具实践

本月精选

自研推理加速器Redwood:两周内实现PyTorch模型高效部署的实战教程
V4L2摄像头采集实战:从camera_client.rar到出图全流程解析
从“谁发明了钢琴键”到知识问答智能体:RAG与记忆工程实践

仿美团外卖菜单实战:数据模型与RecyclerView联动全解析

发布时间:2026/9/17 3:23:53
仿美团外卖菜单实战:数据模型与RecyclerView联动全解析 简介面向Android开发初学者与实战学习者这份资源以仿美团外卖菜单为核心场景提供从项目配置到界面实现的全套代码与图片素材帮助理解Android应用从构建到运行的完整链路。压缩包共541个文件体积约13.53MB内容覆盖Gradle构建脚本、XML布局与资源配置、JSON数据、Java源码、PNG/WebP图片以及可安装APK目录结构遵循Android工程规范适合导入Android Studio后逐层拆解学习。目前已有2319人浏览/学习是较受关注的入门项目。通过该项目可重点练习界面布局设计、事件监听、数据绑定、图片加载与网络交互等常见技能同时了解Gradle工程配置、资源目录规范与Android调试方式掌握从零搭建一个功能模块的完整思路为后续开发完整应用积累实战经验。资源还附带调试安装包与辅助配置文件便于直接运行验证效果无论是课程设计、个人练习还是功能改造参考都能帮助快速落地菜单类应用的开发需求也可作为二次开发的高效起点整体结构简洁值得反复对照和复用。1. 仿美团外卖菜单实战先把数据模型想明白再谈 AndroidStudio 全部代码很多人拿到“仿美团外卖菜单”这个需求第一反应是打开 AndroidStudio 拖两个 RecyclerView左边一个右边一个再找个图片资源放进去调完就交差。真上手后会发现自己被卡在三件事上右侧滚动时左侧选中项不跟着走点击左侧分类右侧滚不到对应位置图片一多列表就明显掉帧。我一般会在动手前先把数据结构画清楚再决定适配器和联动方案因为“全部代码”的重点不是把界面堆出来而是把菜单场景里的数据关系、滚动同步、图片占位这三件事完整跑通。这篇内容按数据模型、布局联动、适配器装配、编译排错、滚动优化展开适合刚做外卖类应用的 Android 开发也适合需要快速搭建“左侧分类 右侧商品”双列表的工程师。5 年以上的老手可以直接跳到最后一章看滚动细节和图片内存控制前面几章用作对照。2. 仿美团外卖菜单的数据结构分类 Bean、菜品 Bean 与图片资源目录设计2.1 先定义可双层嵌套的菜单实体类美团外卖的菜单不是“一个列表”而是“左侧分类 右侧该分类下的一组菜品”。常见做法是定义两个实体分类CategoryBean和菜品DishBean由分类持有菜品列表。这样写虽然不是唯一的思路但最直观后面做右侧滚动同步时也容易定位“当前第一个可见菜品属于哪个分类”。public class CategoryBean { private String name; private ListDishBean dishes; public CategoryBean(String name, ListDishBean dishes) { this.name name; this.dishes dishes; } public String getName() { return name; } public ListDishBean getDishes() { return dishes; } public DishBean getDish(int index) { return dishes null ? null : dishes.get(index); } }public class DishBean { private String dishName; private String desc; private double price; private int imageRes; private int count 0; public DishBean(String dishName, String desc, double price, int imageRes) { this.dishName dishName; this.desc desc; this.price price; this.imageRes imageRes; } public String getDishName() { return dishName; } public String getDesc() { return desc; } public double getPrice() { return price; } public int getImageRes() { return imageRes; } public int getCount() { return count; } public void setCount(int count) { this.count count; } }这里把imageRes设计为int保存的是本地 drawable 的资源 id而不是网络 URL。这样做的好处是不需要先接入图片加载框架项目能直接跑起来后续要替换成网络图片时只需要把imageRes改成图片 URL 字段并换成 Glide 或 Coil 加载即可。为什么不直接用“扁平列表 groupId”而要用双层嵌套因为右侧商品列表需要知道“每个分类从第几条数据开始”左侧点击时要根据这个起始位置去滚动右侧列表。嵌套结构天然能推导分组关系省去在多个列表之间维护一致性的麻烦。要注意的是DishBean.getDish(int index)这个方法是给适配器用的如果分类下菜品可能为空调用前要做空判断否则会出现空指针。2.2 图片资源目录怎么放drawable、mipmap 与占位资源图片资源是“加图片资源”这个需求里最容易被忽略的环节。AndroidStudio 的 res 目录下会预置 mipmap 和 drawable但很多人把所有图都塞进mipmap这其实不是好习惯。下面这张表按本次场景给出建议目录典型用途本次仿美团外卖菜单的使用方式mipmap-hdpi / xhdpi 等应用启动图标、自适应图标不放菜品图片drawable-hdpi / drawable-nodpi位图、shape、selector菜品图、占位图放这里assets原始文件需用 AssetManager 读取不适合 setImageResource 直接使用我习惯把菜品图片放进res/drawable-nodpi因为菜单图片不需要像启动图标那样严格按密度区分nodpi 可以避免 Android 对不同密度自动缩放带来的失真。如果你手上没有真实菜品图不能把imageRes空着否则setImageResource(0)会让 ImageView 直接空白。一个可靠的占位方案是创建一个dish_placeholder.xml放到res/drawable下shape xmlns:androidhttp://schemas.android.com/apk/res/android android:shaperectangle gradient android:startColor#FFEDED android:centerColor#FFD7D7 android:endColor#FFECEC android:angle135 / corners android:radius12dp / /shape这段代码定义了一个浅红渐变的圆角矩形用作菜品占位图。angle135表示渐变方向是左上到右下radius12dp让占位图边缘和常见的圆角商品卡片一致。项目运行时所有没有真实图片的菜品都会显示这个占位图不会出现空白块。2.3 构造模拟数据并保证右侧列表能定位到分类起始位置数据层最后一步是生成测试数据。模拟数据的数量和分类数量都不必完全靠手写用循环生成即可public class DataSource { public static ListCategoryBean buildData() { ListCategoryBean categories new ArrayList(); String[] names {热销, 主食, 小吃, 饮品, 甜品}; for (int i 0; i names.length; i) { ListDishBean dishes new ArrayList(); for (int j 1; j 8; j) { dishes.add(new DishBean( names[i] 菜 j, 新鲜食材约 (200 j * 20) g, 12.5 j, R.drawable.dish_placeholder )); } categories.add(new CategoryBean(names[i], dishes)); } return categories; } }这段代码里分类名数组是可控的每个分类下生成 8 个菜品价格用12.5 j模拟图片资源统一用占位图。你可能注意到价格是 double实际餐饮系统里金额一般用分存储避免浮点精度问题这里为了演示代码简洁才用 double。左侧直接使用ListCategoryBean作为数据源右侧需要使用“所有菜品拼成的平铺列表”还要记下每个分类的起始下标。在 Activity 里这样处理ListCategoryBean categories DataSource.buildData(); ListDishBean dishData new ArrayList(); MapInteger, Integer categoryStartIndex new HashMap(); int index 0; for (int i 0; i categories.size(); i) { categoryStartIndex.put(i, index); dishData.addAll(categories.get(i).getDishes()); index dishData.size(); }categoryStartIndex的 key 是分类下标value 是右侧平铺列表中的起始数据位置。例如“主食”是第 2 个分类它的起始位置就是前一个分类“热销”的菜品总数。右侧 RecyclerView 使用layoutManager.scrollToPositionWithOffset(startPosition, 0)时依赖的就是这张映射表。注意如果你的分类名是动态从接口拿到的建议不要写死在DataSource里而是把buildData()改成“接收分类列表和菜品列表再生成平铺数据和映射表”的工具方法。Data 层的职责是提供干净的索引关系而不是在 Activity 里临时算。3. 仿美团外卖菜单联动主界面双 RecyclerView 的滚动同步与点击定位3.1 主界面布局骨架左边分类、右边菜品布局文件决定双列表的基本行为。常见的仿美团外卖菜单布局是左右两个 RecyclerView 共享同一屏幕高度左侧固定较窄宽度右侧占剩余空间中间加一条分割线。activity_main.xml可以这样写LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationhorizontal androidx.recyclerview.widget.RecyclerView android:idid/rv_category android:layout_width96dp android:layout_heightmatch_parent android:background#F6F6F6 / View android:layout_width1dp android:layout_heightmatch_parent android:background#E8E8E8 / androidx.recyclerview.widget.RecyclerView android:idid/rv_dish android:layout_width0dp android:layout_heightmatch_parent android:layout_weight1 android:background#FFFFFF / /LinearLayout左侧宽度用96dp在绝大多数手机上可以显示 5 个汉字左右如果有“热销爆款”这类四字分类需要适当加宽到104dp。右侧用layout_width0dp配合layout_weight1让 RecyclerView 自动占满剩余宽度这是 LinearLayout 中“按权重分配剩余空间”的标准写法比写死宽度更能适配屏幕尺寸。如果你使用的 AndroidStudio 版本默认生成的是viewBinding或dataBinding这套 id 同样可用。布局确定后Activity 里要分别给两个 RecyclerView 设置 LayoutManager 和 Adapter。注意顺序必须先setLayoutManager再setAdapter否则日志会打印 “No adapter attached; skipping layout” 这类提示。3.2 右侧滚动时左侧选中项跟随更新联动最核心的部分是滚动同步。右侧列表滑动时要实时计算当前第一个可见菜品属于哪个分类然后让左侧列表切换选中背景。我常用的是LinearLayoutManager.findFirstVisibleItemPosition()它返回当前列表第一项在数据源中的位置配合第二节建立的categoryStartIndex映射可以快速反推出分类下标。rvDish.addOnScrollListener(new RecyclerView.OnScrollListener() { Override public void onScrolled(NonNull RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); LinearLayoutManager layoutManager (LinearLayoutManager) recyclerView.getLayoutManager(); if (layoutManager null) return; int firstVisible layoutManager.findFirstVisibleItemPosition(); int categoryPosition findCategoryIndexByDishPosition(firstVisible); if (categoryPosition ! lastCheckedCategory) { lastCheckedCategory categoryPosition; categoryAdapter.setCheckedPosition(categoryPosition); categoryAdapter.notifyDataSetChanged(); rvCategory.smoothScrollToPosition(categoryPosition); } } });findCategoryIndexByDishPosition的实现很简单遍历分类映射表判断这个菜品位置落在哪个分类的起始位置与下一个分类起始位置之间。private int findCategoryIndexByDishPosition(int dishPosition) { for (int i 0; i categories.size(); i) { int start categoryStartIndex.get(i); int end (i categories.size() - 1) ? dishData.size() - 1 : categoryStartIndex.get(i 1) - 1; if (dishPosition start dishPosition end) { return i; } } return 0; }边界判断需要注意最后一个分类的结束位置不能取categoryStartIndex.get(i 1) - 1因为i 1已经越界要用dishData.size() - 1。如果用户的滑动速度很快firstVisible可能还没来得及刷新到新的 item这属于正常现象不需要额外处理。lastCheckedCategory用来避免同一分类重复触发notifyDataSetChanged减少左侧列表的绘制开销。3.3 左侧点击右侧定位到指定分类的起始位置右侧跟随左侧的代码完成后左侧点击也要能反向控制右侧。这里的重点是scrollToPositionWithOffset而不是scrollToPosition前者可以指定列表项的顶部与 RecyclerView 顶部的偏移量。传0表示让被点击分类的第一条菜品对齐右侧列表顶部不会出现从分类中间开始滚动的问题。categoryAdapter.setOnItemClickListener(new CategoryAdapter.OnItemClickListener() { Override public void onItemClick(int position) { Integer startPosition categoryStartIndex.get(position); if (startPosition null) return; rvDish.stopScroll(); layoutManagerDish.scrollToPositionWithOffset(startPosition, 0); categoryAdapter.setCheckedPosition(position); categoryAdapter.notifyDataSetChanged(); } });这里有两个容易被忽视的点。第一个是rvDish.stopScroll()如果右侧列表正在惯性滚动直接调用scrollToPositionWithOffset会被接下来的滚动事件覆盖导致最终位置不对。stopScroll()会中断当前滚动动画和惯性让定位立刻生效。第二个是点击左侧后右侧滚动会触发前面写的onScrolled监听反向把左侧选中项改成它认为正确的分类。因此我通常会再加一个布尔标记在点击左侧时设为true在onScrolled里检测到标记时跳过同步逻辑等右侧滚动停止后再解除标记。private boolean isClickFromLeft false; private void moveToCategory(int position) { int startPosition categoryStartIndex.get(position); isClickFromLeft true; rvDish.stopScroll(); layoutManagerDish.scrollToPositionWithOffset(startPosition, 0); rvDish.postDelayed(new Runnable() { Override public void run() { isClickFromLeft false; } }, 300); }postDelayed的延迟时间不是越大越好。300ms 足够覆盖一次普通点击后的滚动如果延迟太大用户在 300ms 内手动滑动右侧列表时左侧会短暂不跟随。另一个参数点是scrollToPositionWithOffset的第二个参数如果你希望右侧列表顶部保留一个轮播横幅或搜索框可以把偏移量设为这个视图的高度在仿美团外卖菜单里没有顶部悬浮元素时写0最省事。4. 打通仿美团外卖菜单的全部代码Adapter、Activity 装配与 AndroidStudio 编译坑4.1 CategoryAdapter 核心实现选中背景和加粗文字左侧分类适配器的重点不是数据填充而是“当前选中的分类”要有明确的视觉反馈。仿美团外卖的常见效果是选中项白底黑字加粗未选中项灰底灰字。这部分代码量不大但容易写错的地方是notifyDataSetChanged导致左侧整列表重绘。分类数量少时可以接受超过 50 个分类建议改成只刷新前后两个 item。public class CategoryAdapter extends RecyclerView.AdapterCategoryAdapter.VH { private ListCategoryBean data; private int checkedPosition 0; public CategoryAdapter(ListCategoryBean data) { this.data data; } public void setCheckedPosition(int position) { this.checkedPosition position; } NonNull Override public VH onCreateViewHolder(NonNull ViewGroup parent, int viewType) { View view LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_category, parent, false); return new VH(view); } Override public void onBindViewHolder(NonNull VH holder, int position) { CategoryBean bean data.get(position); holder.tvName.setText(bean.getName()); boolean checked position checkedPosition; holder.itemView.setBackgroundColor(checked ? 0xFFFFFFFF : 0xFFF6F6F6); holder.tvName.setTextColor(checked ? 0xFF222222 : 0xFF666666); holder.tvName.setTypeface(null, checked ? Typeface.BOLD : Typeface.NORMAL); } Override public int getItemCount() { return data null ? 0 : data.size(); } static class VH extends RecyclerView.ViewHolder { TextView tvName; VH(View itemView) { super(itemView); tvName itemView.findViewById(R.id.tv_category_name); } } }item_category.xml中只需要一个居中显示的TextView高度建议设置为56dp和美团外卖的分类项高度接近。setTypeface(null, Typeface.BOLD)中第一个参数传null表示使用系统默认字体只修改字重如果你使用了自定义字体要在资源里单独配置多一个 bold 字体文件否则加粗效果可能不生效。4.2 DishAdapter 与加入购物车的数量局部刷新右侧菜品适配器要展示图片、菜名、描述、价格和“加号”按钮实际项目中还会有“减号”。这里的重点是点击加号后不要notifyDataSetChanged否则整行会重新绑定图片造成视觉闪烁和滚动卡顿。正确做法是使用 RecyclerView 的局部刷新机制也就是带 payload 参数的onBindViewHolder。public class DishAdapter extends RecyclerView.AdapterDishAdapter.VH { private ListDishBean data; private OnCountChangeListener listener; public interface OnCountChangeListener { void onCountChanged(DishBean bean, int count); } public void setOnCountChangeListener(OnCountChangeListener listener) { this.listener listener; } Override public void onBindViewHolder(NonNull VH holder, int position) { DishBean bean data.get(position); holder.tvName.setText(bean.getDishName()); holder.tvDesc.setText(bean.getDesc()); holder.tvPrice.setText(¥ bean.getPrice()); holder.ivImage.setImageResource(bean.getImageRes()); holder.tvCount.setText(String.valueOf(bean.getCount())); holder.ivImage.setTag(bean); // 便于事件回调拿到对象 // 这里省略按钮点击的内部实现 } Override public void onBindViewHolder(NonNull VH holder, int position, NonNull ListObject payloads) { if (payloads.isEmpty()) { onBindViewHolder(holder, position); return; } for (Object payload : payloads) { if (count.equals(payload)) { holder.tvCount.setText(String.valueOf(data.get(position).getCount())); } } } }关于holder.ivImage.setTag(bean)这行点击按钮时可以通过view.getTag()拿到 Bean避免再根据 position 做一次列表查询。不过如果列表项复用了 ViewHolderposition是可靠的因为getBindingAdapterPosition()在重绘时也能返回正确位置。真正的性能差异来自 payload 局部刷新图片不会被重新加载整个 item 不会闪一下。按钮改数量后需要让 RecyclerView 只刷新数量不刷新图片。调用代码是adapter.notifyItemChanged(position, count)这里的count是自定义的 payload 字符串。如果数量和购物车角标有联动再通过OnCountChangeListener回调给 Activity。4.3 MainActivity 装配把数据源、适配器和滚动监听接起来前面几段代码分散在多个文件里MainActivity 是最后把它们组装起来的地方。完整的onCreate示例代码如下public class MainActivity extends AppCompatActivity { private ListCategoryBean categories; private ListDishBean dishData; private MapInteger, Integer categoryStartIndex new HashMap(); private CategoryAdapter categoryAdapter; private DishAdapter dishAdapter; private LinearLayoutManager layoutManagerCategory; private LinearLayoutManager layoutManagerDish; private int lastCheckedCategory 0; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); RecyclerView rvCategory findViewById(R.id.rv_category); RecyclerView rvDish findViewById(R.id.rv_dish); categories DataSource.buildData(); dishData new ArrayList(); categoryStartIndex.clear(); int startIndex 0; for (int i 0; i categories.size(); i) { categoryStartIndex.put(i, startIndex); dishData.addAll(categories.get(i).getDishes()); startIndex dishData.size(); } categoryAdapter new CategoryAdapter(categories); dishAdapter new DishAdapter(dishData); layoutManagerCategory new LinearLayoutManager(this); layoutManagerDish new LinearLayoutManager(this); rvCategory.setLayoutManager(layoutManagerCategory); rvDish.setLayoutManager(layoutManagerDish); rvCategory.setAdapter(categoryAdapter); rvDish.setAdapter(dishAdapter); // 右侧滚动同步左侧 rvDish.addOnScrollListener(new RecyclerView.OnScrollListener() { Override public void onScrolled(NonNull RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); if (isClickFromLeft) return; int firstVisible layoutManagerDish.findFirstVisibleItemPosition(); int categoryPosition findCategoryIndexByDishPosition(firstVisible); if (categoryPosition ! lastCheckedCategory) { lastCheckedCategory categoryPosition; categoryAdapter.setCheckedPosition(categoryPosition); categoryAdapter.notifyDataSetChanged(); rvCategory.smoothScrollToPosition(categoryPosition); } } }); // 左侧点击滚动右侧 categoryAdapter.setOnItemClickListener(new CategoryAdapter.OnItemClickListener() { Override public void onItemClick(int position) { moveToCategory(position); } }); } }装配顺序是有讲究的setLayoutManager永远在setAdapter之前即使漏掉也不会立即崩溃但列表首屏无法正确测量如果你的 List 数据比较大建议使用setHasFixedSize(true)告诉 RecyclerView 宽高不会因为内容变化而变化这样可以跳过重新测量。这里的dishData是把所有分类的菜品拼接后的结果右侧列表的 item 总数就是它的 size左侧点击时用categoryStartIndex.get(position)得到起始下标。4.4 AndroidStudio 编译与运行时的常见坑编译和运行阶段有 3 个和本项目直接相关的坑基本都集中在 RecyclerView 和图片资源上。现象可能出现的位置处理方式Cannot perform this operation on a RecyclerView that does not have a LayoutManager点击按钮或点击分类时调用scrollToPositionWithOffset在setAdapter前先检查是否执行了setLayoutManagerNo adapter attached; skipping layout运行日志高频出现确认setAdapter是否在 ActivityonCreate中执行不要在子线程调用图片区域出现 OutOfMemoryError用setImageResource加载大尺寸位图缩小图片尺寸或改用BitmapFactory.Options.inSampleSize第一类报错在 AndroidStudio 里最常见的触发方式是在初始化调用中先执行了rvDish.setAdapter(dishAdapter)而后面的代码又在某种条件下给 RecyclerView 换了 LayoutManager导致 View 被 invalidate 时状态不对。写法上尽量保持“LayoutManager 和 Adapter 只设置一次”。如果项目里用了历史版本下载的 AndroidStudio 或者低版本 Gradle这个错误并不会因为 IDE 新旧而消失它是 RecyclerView 运行时状态问题。第二类报错一般不会导致崩溃但会在 logcat 刷屏。出现原因通常是数据异步加载在界面还没完成 attach 时就去更新 adapter。解决办法是数据回来后判断 RecyclerView 的 adapter 是否为空再调用submitList或notifyDataSetChanged。第三类问题直接关系到图片资源。如果每个菜品图都是 1080x1080 的 PNG8 个商品就能占掉几十 MB 内存列表滑动时必然卡顿。建议在加载前用BitmapFactory.Options做一次降采样而不是直接setImageResource。这部分代码放在第 5 章详细说明因为它是“图片资源”优化最值得看的内容。5. 让菜单更贴近真实外卖滚动细节、吸顶与图片资源的内存控制5.1 左侧选中项滚动范围限制右侧滚动同步左侧时我前文写的是rvCategory.smoothScrollToPosition(categoryPosition)。这个 API 的问题在于如果目标分类在左侧可视区域外RecyclerView 会持续滚动到目标 item 完全可见经常出现“选中项被顶到列表第一行”的效果和美团外卖的“选中项居中偏上”不一致。更接近线上的做法是先判断目标 item 是否已经可见只有不可见时才滚动。int first layoutManagerCategory.findFirstVisibleItemPosition(); int last layoutManagerCategory.findLastVisibleItemPosition(); if (categoryPosition first || categoryPosition last) { rvCategory.stopScroll(); layoutManagerCategory.scrollToPositionWithOffset(categoryPosition, 40); }偏移量40表示选中分类距离左侧列表顶部的距离单位是像素。如果你的分类 item 高度是 56dp在 xxhdpi 屏幕上等于 168px40px 让选中项不会紧贴顶部。这段逻辑放在“右侧滚动同步左侧”的回调里可以大幅减少左侧列表频繁滚动的生硬感。5.2 右侧分组跟随的悬浮分类条真正的外卖应用右侧列表上方会有一个固定不动的分类条比如你滑到“主食”区域顶部悬浮条显示“主食”。这个功能不一定要用复杂的 ItemDecoration一个更轻量的做法是在右侧列表上方放一个TextView然后在滚动监听里根据当前第一个可见菜品更新文本。tvFloatTitle.setText(categories.get(findCategoryIndexByDishPosition(firstVisible)).getName());这个TextView的高度一般设置 32dp背景用淡橙色或白色文字大小 12sp 左右。它的位置完全独立于 RecyclerView所以不会随列表滚动。如果你需要带背景色的吸顶效果再给这个 Text 加一个浅色渐变背景即可。实现成本最低而且不会因为 item 复用产生文字重叠。5.3 图片资源的采样加载与内存回收菜单列表真正的性能瓶颈通常不在代码逻辑而在 Bitmap。直接setImageResource会让系统按原图尺寸加载一个菜单页几十张图很容易触发 OOM。手动采样加载的代码并不复杂适合放在 Adapter 的绑定方法里BitmapFactory.Options opts new BitmapFactory.Options(); opts.inJustDecodeBounds true; BitmapFactory.decodeResource(getResources(), bean.getImageRes(), opts); int sample 1; while (opts.outWidth / sample 720) { sample * 2; } opts.inJustDecodeBounds false; opts.inSampleSize sample; Bitmap bitmap BitmapFactory.decodeResource(getResources(), bean.getImageRes(), opts); holder.ivImage.setImageBitmap(bitmap);参数说明第一次decodeResource只读取图片宽高不分配内存所以inJustDecodeBounds是trueoutWidth / sample 720表示把图片宽度压到 720px 以内因为美团外卖菜单的图片在列表里实际显示宽度通常只有 160dp 左右1080px 原图完全没有必要inSampleSize2时宽高都减半像素总量变为原来的四分之一内存占用因此大幅下降。需要注意这段代码每次绑定都会执行一次decodeResource频繁滚动时仍有开销建议再套一层LruCache缓存 Bitmapkey 用imageRes即可。如果你不想手写缓存引入 Glide 也是合理选择但最终效果完全取决于图片尺寸。无论用哪种方案图片资源进入 drawable 之前应该先压到合适宽高这是“仿美团外卖菜单全部代码”中最容易被忽略的一步也是对这个标题最有价值的补充。本文还有配套的精品资源点击获取

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号