Android仿美团外卖菜单:RecyclerView双向联动完整实现
2026/9/17 2:59:38 网站建设 项目流程

简介:这份资源是一份仿美团外卖菜单功能的安卓实战项目,包含完整的工程代码和图片资源,适合希望通过实际项目提升技能的开发者,尤其是安卓初学者,也适合教学中作为案例使用。压缩包内共有541个文件,核心包括工程构建脚本、Java源码、XML布局、JSON配置以及菜品与图标等视觉资源,压缩后约13.53MB,目录结构清晰,其中app模块集中了主要功能代码,res目录对图片资源进行分类管理,便于导入开发环境直接查看或二次开发。已有2319人学习下载,热度较高。通过这个项目,读者可以系统学习安卓项目构建流程、界面布局设计、图片资源管理方法,并加深对事件监听、数据绑定、网络请求等关键知识点的理解;同时参考构建脚本与资源配置文件的用法,体会从源码到安装包生成的完整过程,是一份贴近真实业务场景的综合实战资料。

1. 仿美团外卖菜单,最值得拆解的不是 UI 而是联动逻辑

很多人在 AndroidStudio 里练手都会挑一个“看得见”的项目,仿美团外卖菜单就是典型:左侧一列分类,右侧一道菜图,点分类右侧滚,滑右侧分类跟着切。表面上是两个列表,真正值钱的是它们之间的滚动状态同步和点击反向定位。这份压缩包把整套代码和图片资源都备齐了,适合刚跑通 AndroidStudio 安装教程、想在真实工程里看 RecyclerView 多重嵌套和事件分发的人。拆完你收获的不只是“会写菜单”,而是理解了如何用坐标、位置和 LayoutManager 让两个列表互相驱动。下面从工程结构、构建配置、核心联动实现、资源组织到排错验证,一条线拆开讲。

2. 工程结构与 Gradle 构建配置:先把项目跑起来再看代码

拿到压缩包后不要急着看 Activity,先把目录结构和构建脚本对一遍。这个包里的app/build.gradlesettings.gradlegradle.propertiesgradlew.bat都在,说明是个完整的 Gradle 工程,不是碎片代码。我的习惯是先确认settings.gradle里有没有正确 include:app,再看build.gradle的依赖和 SDK 版本。

2.1 目录布局与各文件职责

解压后典型结构如下:

project/ ├── settings.gradle ├── build.gradle ├── gradle.properties ├── gradlew.bat ├── gradle/wrapper/ ├── app/ │ ├── build.gradle │ ├── src/main/ │ │ ├── java/ │ │ ├── res/ │ │ └── AndroidManifest.xml │ └── .gitignore └── androidResources/

settings.gradle负责声明模块,默认内容通常是:

pluginManagement { repositories { google() mavenCentral() gradlePluginPortal() } } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() } } rootProject.name = "MeiTuanMenu" include ':app'

这段声明了仓库源为 google 和 mavenCentral,include ':app'告诉 Gradle 只构建 app 模块。如果你打开工程报 “Project is not yet configured”,多半是这里缺了 include 或仓库访问不到。

app/build.gradle里重点看 dependencies,仿菜单项目一般会用到 RecyclerView 和图片加载库:

android { compileSdk 34 defaultConfig { applicationId "com.example.meituanmenu" minSdk 21 targetSdk 34 versionCode 1 versionName "1.0" } } dependencies { implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'androidx.recyclerview:recyclerview:1.3.2' implementation 'com.github.bumptech.glide:glide:4.16.0' }

compileSdk决定能用哪些 API,minSdk 21保证了 5.0 以上设备兼容。图片加载这里用的是 Glide,它是处理网络和本地图片最省事的库,后面资源适配还会用它。

2.2 用命令行构建与常见启动问题

Windows 下在工程根目录执行:

gradlew.bat assembleDebug

Linux/macOS 执行./gradlew assembleDebug。第一次构建会下载 Gradle 发行版和依赖,耗时看网络。如果卡在 “Could not resolve com.android.tools.build:gradle”,先检查gradle/wrapper/gradle-wrapper.properties里的 distributionUrl 是否与本地 JDK 版本匹配。建议 JDK 17 + Gradle 8.x 组合,这是现在 AndroidStudio 默认搭配。

如果打开工程后 AndroidStudio 一直提示 “Gradle sync failed”,依次做三件事:看gradle.properties里有没有配置android.useAndroidX=true(这个项目必须开,否则依赖解析会回溯到旧 support 库);关掉代理;在终端手动跑一遍gradlew.bat assembleDebug看真实堆栈。这三步能解决 80% 的导入类问题。

3. 仿美团菜单核心实现:RecyclerView 双向联动机制

这个项目的灵魂在菜单页。仿美团外卖的界面并不复杂:左边一个窄列表放分类名,右边一个宽列表放该分类下的菜品。真正的难点是两边如何保持一致——点击左侧分类,右侧滚到对应区域;右侧滑动时,左侧分类高亮跟着变。下面拆开讲实现。

3.1 数据模型与两级数据设计

菜单页用的是两级结构:一级是分类实体,二级是该分类下的菜品列表。常见的模型设计如下:

public class CategoryBean { private String name; private List<DishBean> dishList; // getter/setter 省略 } public class DishBean { private String dishName; private String price; private int imageRes; // 本地图片资源 id }

把“分类”和“菜品”放在同一个CategoryBean里,是为了让右侧列表可以直接按分类位置分段。右侧数据源其实可以拍平成List<DishBean>,同时记录每个分类在拍平列表中的起始索引。这样用LinearLayoutManager.findFirstVisibleItemPosition()反推当前可见菜品属于哪个分类时,只需要遍历分类的索引区间。

3.2 布局:两级 RecyclerView 嵌套

菜单页主布局用垂直方向的 LinearLayout,内部放两个 RecyclerView:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/left_menu_recycler" android:layout_width="100dp" android:layout_height="match_parent" /> <androidx.recyclerview.widget.RecyclerView android:id="@+id/right_dish_recycler" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" /> </LinearLayout>

左侧宽度固定 100dp,右侧用layout_weight="1"占满剩余空间。右侧 RecyclerView 的onScrollStateChanged回调里做 “被动更新左侧选中项”,左侧 RecyclerView 的onClick做 “主动定位右侧”。两边都用 LinearLayoutManager,不要用 GridLayoutManager,因为联动计算依赖线性坐标。

3.3 点击左侧分类,右侧平滑滚动

左侧列表 item 点击后,需要计算出对应分类在右侧数据源中的真实索引,然后滚动。这里的坑是:右侧数据源是拍平后的菜品列表,分类索引不能直接用,必须从数据模型里累加得到起始偏移。

private void scrollRightToCategory(int categoryIndex) { int targetPos = 0; for (int i = 0; i < categoryIndex; i++) { targetPos += categoryList.get(i).getDishList().size(); } rightLayoutManager.scrollToPositionWithOffset(targetPos, 0); updateLeftSelection(categoryIndex); }

scrollToPositionWithOffsetscrollToPosition更好用,因为它能控制目标 item 停在列表顶部的对齐位置。第二个参数 0 表示 targetPos 对应的 item 出现在右侧列表最顶端。如果不传偏移,RecyclerView 可能只是滚动到让该 item 可见,位置可能停在屏幕中间,看起来就不像美团的效果了。

3.4 右侧滚动,联动左侧分类高亮

这是整个项目里最容易被写坏的部分。常见错误是每收到一次滚动回调就去重画整个左侧列表。正确做法是取右侧第一个可见 item 的位置,找到它属于哪个分类,再对比当前左侧选中的分类,变了才更新。

rightRecycler.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); int firstPos = rightLayoutManager.findFirstVisibleItemPosition(); int currentCategory = findCategoryByDishPos(firstPos); if (currentCategory != currentSelectedCategory) { currentSelectedCategory = currentCategory; leftAdapter.setSelectedPosition(currentCategory); leftLayoutManager.scrollToPosition(currentCategory); } } private int findCategoryByDishPos(int dishPos) { int sum = 0; for (int i = 0; i < categoryList.size(); i++) { sum += categoryList.get(i).getDishList().size(); if (dishPos < sum) return i; } return categoryList.size() - 1; } });

findFirstVisibleItemPosition()返回的是屏幕上可见的最靠上的 item 位置,用这个位置去反向查所属分类,误差最小。注意不要用findLastVisibleItemPosition(),否则右侧滚到分类边界时会提前切换高亮。setSelectedPosition方法里让左侧 item 的选中背景和文字颜色变化,同时用另一个scrollToPosition让左侧选中项始终保持在可视区域内。

3.5 保持联动的两个细节

第一个细节是左侧点击后,要打断右侧滚动引起的回调冲突。我的做法是在scrollRightToCategory里设一个布尔标记isClickDriven = true,然后在右侧onScrolled开头检查这个标记,若是主动滚动,先跳过联动更新,等scrollToPositionWithOffset生效后再重置标记。

第二个细节是右侧列表需要设置setHasFixedSize(true),如果没有,每次数据刷新都会重新测量高度,滚动时会明显卡顿。美团那种实时菜单因为菜品种类多,还会用到 DiffUtil 做局部刷新,但基础版直接一次性 set 全量数据也能用。

4. 图片资源组织与 Glide 加载适配

压缩包里专门有androidResources目录和resources-debug.ap_,说明图片资源是一等公民。仿美团菜单没有真实后端,菜品图大多是本地 drawable 资源或 assets 下的图片。资源放得对不对,直接决定项目能否编译通过。

4.1 drawable 与 mipmap 的摆放规则

Android 的 res 目录有严格的类型限制:mipmap只放应用图标,drawable放普通图片和 xml 形状资源。这个项目的菜品图片应当放在app/src/main/res/drawable/下,如果图片是切图,常见的做法是分密度目录:

目录用途常见场景
drawable-mdpi160dpi 设备老设备兜底
drawable-hdpi240dpi小尺寸图标
drawable-xhdpi320dpi这几年主流
drawable-xxhdpi480dpi现代中高端机
drawable-xxxhdpi640dpi大屏高分辨率

美团外卖这类带实拍感的菜品图,一般只用 xxhdpi 一套就够,因为图片加载库会做缩放。如果你把大图放在 drawable-nodpi(不缩放)里,在 4K 屏上可能出现图片被拉伸的问题;放进 xxhdpi 之后系统会自动按密度折算到其他设备,显示大小基本一致。

4.2 用 Glide 加载本地资源与缓存策略

在 item 布局里给 ImageView 设置数据时,用 Glide 统一加载:

Glide.with(itemView.getContext()) .load(dishBean.getImageRes()) .placeholder(R.drawable.ic_placeholder) .error(R.drawable.ic_error) .centerCrop() .into(ivDishImage);

load()接收 int 资源 id,Glide 会按资源类型解码。centerCrop()会让图片填满 ImageView 并裁掉多余部分,这样菜品图不管原始比例是 4:3 还是 1:1,最终显示都是统一尺寸。如果图片放在 assets 目录,则改成.load("file:///android_asset/dish_001.jpg")

真正影响流畅度的不是加载,而是缓存。本地资源不需要磁盘缓存,但 Glide 默认也会走内存缓存。菜单列表滑动时如果反复加载同一张图,内存缓存命中率越高越顺滑。在 AndroidStudio 中观察 Profiler 的 Memory 曲线,如果内存占用锯齿状大起大落,多半是 Glide 缓存策略没配合适。可以在Glide.with()前用.skipMemoryCache(false)显式保持缓存,避免每次滚动都重新解码。

4.3 图片资源编译产物与资源混淆

压缩包里出现resources-debug.ap_是 Gradle 构建过程中生成的中间资源包,里面是编译后的资源和 manifest。如果你修改了 drawable 目录下的图片文件名,重新构建时报 “Resource not found”,先执行Build -> Clean Project,再执行Rebuild Project,因为旧资源索引可能还留在构建缓存里。另外,图片名必须全小写,只能用字母、数字、下划线,不能是中文或大写——这是 Android 资源命名红线,很多人第一次打包失败就栽在这里。

5. 排错清单与验证联动效果的四个检查点

代码写完不是终点,菜单联动这类需求,肉眼看不出的问题远比编译错误多。我总结了一套从构建到运行的验证流程,直接照做能少走很多弯路。

5.1 编译期常见问题对照表

症状原因处理
Gradle sync 失败,提示 com.android.tools.build:gradle 版本不兼容JDK 版本与 Gradle 不匹配JDK 17 + Gradle 8.2+
编译报错:android:attr/colorAccent not found主题继承自旧 support 库把主题父类改为Theme.AppCompat.Light.NoActionBar
资源文件找不到,引用 R.drawable.xxx 报红图片名非法或未放进 drawable检查文件名小写、无中文,Clean 后 Rebuild
左侧分类点击后右侧不动目标索引计算错误打印 targetPos,核对分类菜品数累加值
右侧滑动时左侧高亮跳变使用了 findLastVisibleItemPosition改为 findFirstVisibleItemPosition

5.2 验证联动流畅度的四个检查点

第一个检查点是左侧选中项是否永远可见。如果左侧列表被点击后滚动到了可见区域之外,需要在setSelectedPosition里调用leftLayoutManager.scrollToPosition并保证该 position 完整可见。如果滚动后左侧选中项只露出一半,可改用smoothScrollToPosition

第二个检查点是右侧滚动到分类边界时左侧是否提前切换。请在两个分类交界处慢速滑动,观察左侧高亮是否在最后一个菜品完全移出屏幕前就变了。理想情况下,只有第一个不可见的菜品属于新分类时才能切换。用findFirstVisibleItemPosition已能避免大部分问题,但如果 item 高度不一致(比如某道菜特别长),需要在onScrolled里计算偏移,可以结合findFirstVisibleItemPosition拿到 View,再使用getTop()判断可见比例。

第三个检查点是点击左侧分类后右侧是否精确停在分类首项。在日志里打印targetPos,手动验证并核对分类菜品数是否匹配数据源。常见错误是在循环累加时从categoryIndex开始而不是从 0 开始,导致偏移少了一个分类的菜品数。

第四个检查点是快速滑动时是否出现白屏或崩溃。白屏多为图片解码耗时,Glide 加载本地图不至于;如果出现RecyclerView has no LayoutManager异常,说明在setAdapter前没有 setLayoutManager,检查 onCreate 里的初始化顺序。

5.3 扩展:把固定数据源改成可加载分页

如果你的场景需要接入真实接口,右侧列表的联动逻辑不变,只需要把数据源换成PagingSource。右侧滚动到底部时,先记录当前可见的菜品位置,再请求下一页,插入后重新计算每个分类的起始索引。

// 伪代码示意:追加下一页数据后重建索引 List<CategoryBean> newData = fetchNextPage(); int base = 0; for (CategoryBean category : categoryList) { category.setStartIndex(base); base += category.getDishList().size(); }

索引重建完以后,用adapter.notifyDataSetChanged()会导致整个右列表闪烁。更好的做法是计算新数据的插入范围,用notifyItemRangeInserted。这样既能保持滚动位置,又不会打断左侧高亮状态。动手改这一版时,建议把左侧分类名和右侧菜品的 id 都建模成 long 类型,避免后续做数据库缓存时还要重构主键。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询