ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Android仿美团外卖菜单:RecyclerView双向联动完整实现

Android仿美团外卖菜单:RecyclerView双向联动完整实现 简介这份资源是一份仿美团外卖菜单功能的安卓实战项目包含完整的工程代码和图片资源适合希望通过实际项目提升技能的开发者尤其是安卓初学者也适合教学中作为案例使用。压缩包内共有541个文件核心包括工程构建脚本、Java源码、XML布局、JSON配置以及菜品与图标等视觉资源压缩后约13.53MB目录结构清晰其中app模块集中了主要功能代码res目录对图片资源进行分类管理便于导入开发环境直接查看或二次开发。已有2319人学习下载热度较高。通过这个项目读者可以系统学习安卓项目构建流程、界面布局设计、图片资源管理方法并加深对事件监听、数据绑定、网络请求等关键知识点的理解同时参考构建脚本与资源配置文件的用法体会从源码到安装包生成的完整过程是一份贴近真实业务场景的综合实战资料。1. 仿美团外卖菜单最值得拆解的不是 UI 而是联动逻辑很多人在 AndroidStudio 里练手都会挑一个“看得见”的项目仿美团外卖菜单就是典型左侧一列分类右侧一道菜图点分类右侧滚滑右侧分类跟着切。表面上是两个列表真正值钱的是它们之间的滚动状态同步和点击反向定位。这份压缩包把整套代码和图片资源都备齐了适合刚跑通 AndroidStudio 安装教程、想在真实工程里看 RecyclerView 多重嵌套和事件分发的人。拆完你收获的不只是“会写菜单”而是理解了如何用坐标、位置和 LayoutManager 让两个列表互相驱动。下面从工程结构、构建配置、核心联动实现、资源组织到排错验证一条线拆开讲。2. 工程结构与 Gradle 构建配置先把项目跑起来再看代码拿到压缩包后不要急着看 Activity先把目录结构和构建脚本对一遍。这个包里的app/build.gradle、settings.gradle、gradle.properties、gradlew.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 和 mavenCentralinclude :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决定能用哪些 APIminSdk 21保证了 5.0 以上设备兼容。图片加载这里用的是 Glide它是处理网络和本地图片最省事的库后面资源适配还会用它。2.2 用命令行构建与常见启动问题Windows 下在工程根目录执行gradlew.bat assembleDebugLinux/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.useAndroidXtrue这个项目必须开否则依赖解析会回溯到旧 support 库关掉代理在终端手动跑一遍gradlew.bat assembleDebug看真实堆栈。这三步能解决 80% 的导入类问题。3. 仿美团菜单核心实现RecyclerView 双向联动机制这个项目的灵魂在菜单页。仿美团外卖的界面并不复杂左边一个窄列表放分类名右边一个宽列表放该分类下的菜品。真正的难点是两边如何保持一致——点击左侧分类右侧滚到对应区域右侧滑动时左侧分类高亮跟着变。下面拆开讲实现。3.1 数据模型与两级数据设计菜单页用的是两级结构一级是分类实体二级是该分类下的菜品列表。常见的模型设计如下public class CategoryBean { private String name; private ListDishBean dishList; // getter/setter 省略 } public class DishBean { private String dishName; private String price; private int imageRes; // 本地图片资源 id }把“分类”和“菜品”放在同一个CategoryBean里是为了让右侧列表可以直接按分类位置分段。右侧数据源其实可以拍平成ListDishBean同时记录每个分类在拍平列表中的起始索引。这样用LinearLayoutManager.findFirstVisibleItemPosition()反推当前可见菜品属于哪个分类时只需要遍历分类的索引区间。3.2 布局两级 RecyclerView 嵌套菜单页主布局用垂直方向的 LinearLayout内部放两个 RecyclerViewLinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationhorizontal androidx.recyclerview.widget.RecyclerView android:idid/left_menu_recycler android:layout_width100dp android:layout_heightmatch_parent / androidx.recyclerview.widget.RecyclerView android:idid/right_dish_recycler android:layout_width0dp android:layout_heightmatch_parent android:layout_weight1 / /LinearLayout左侧宽度固定 100dp右侧用layout_weight1占满剩余空间。右侧 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); }scrollToPositionWithOffset比scrollToPosition更好用因为它能控制目标 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 资源 idGlide 会按资源类型解码。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改为 findFirstVisibleItemPosition5.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。右侧滚动到底部时先记录当前可见的菜品位置再请求下一页插入后重新计算每个分类的起始索引。// 伪代码示意追加下一页数据后重建索引 ListCategoryBean newData fetchNextPage(); int base 0; for (CategoryBean category : categoryList) { category.setStartIndex(base); base category.getDishList().size(); }索引重建完以后用adapter.notifyDataSetChanged()会导致整个右列表闪烁。更好的做法是计算新数据的插入范围用notifyItemRangeInserted。这样既能保持滚动位置又不会打断左侧高亮状态。动手改这一版时建议把左侧分类名和右侧菜品的 id 都建模成 long 类型避免后续做数据库缓存时还要重构主键。本文还有配套的精品资源点击获取
返回列表