ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android 纵向滑动页面实战:四种方案选型与性能优化详解

Android 纵向滑动页面实战:四种方案选型与性能优化详解 简介这是一份面向 Android 开发者的纵向滑动页面实现资源重点讲解如何利用 ViewPager 完成上下滑动翻页、页码显示以及滑动细节优化适合需要实现滚动列表、轮播图或翻页阅读器的中高级开发者。资源包内含 61 个文件包括 Java 源码、XML 布局与资源配置、class 编译产物、jar 依赖库、图片资源以及可直接安装运行的 APK 与完整工程文件整体约 1.31MB目录结构清晰便于直接导入开发工具验证效果。内容覆盖 ViewPager 的依赖配置、XML 布局设计、Fragment 适配器编写、TabLayout 关联页码等关键步骤并针对 FlingPage 自定义滑动逻辑与手势检测进行了深入拆解可帮助读者理解纵向滑动背后的实现原理。这份资源既能提供可运行的 Demo 演示又能作为代码参考帮助读者根据自身需求扩展修改。目前已有 3776 人学习下载是一份兼顾代码参考与原理剖析的实用资料。1. 项目背景与方案选型思考做Android开发绕不开一个基础交互——纵向滑动页面。不管是Feed流、商品列表、设置页面还是详情页上下滑动几乎是每个App的标配操作。这个项目看起来只是实现一个“上下滑动效果”但真正落地时会发现滑动方案的选择直接决定了后续的扩展空间、性能表现和维护成本。先说结论Android里实现纵向滑动至少有四条路可走分别是ScrollView、RecyclerView、NestedScrollView以及基于ViewPager2的纵向翻页。我在实际项目中都试过每种方案都有自己的适用场景选错方案的代价往往不是当时能立刻发现的而是在业务迭代到一半时突然卡住不得不返工。如果只是单纯展示一段固定内容比如用户协议、隐私政策、活动规则ScrollView是最简单直接的选择一个TextView套进ScrollView五分钟搞定。但如果内容是动态数据数量不确定甚至还需要下拉刷新、加载更多、点击跳转RecyclerView几乎是唯一正解。它天生为大数据量列表设计ViewHolder回收复用机制保证了滑动流畅性不会因为item增多而卡顿。NestedScrollView则是一个比较特殊的存在它是ScrollView的增强版核心价值在于支持嵌套滚动机制。当页面同时存在纵向滑动和内部横向滑动或者需要协同处理父布局与子布局的滚动事件时NestedScrollView是首选。经典场景就是CoordinatorLayout配合AppBarLayout实现折叠头部效果列表滚动时标题栏逐渐收起这个组合在项目里出现频率极高。ViewPager2纵向化是比较容易被忽略的方案。它的默认方向是横向但通过一行配置就能切换为纵向翻页适合那种整页整页滑动的场景比如短视频上下切换、小说阅读器的翻页体验。它和上述三种“连续滚动”的本质区别在于——ViewPager2是分页式滑动一页就是一屏松手后自动吸附到最近的页面。这个项目之所以值得单独写一篇文章是因为很多初学者在实现纵向滑动时只关注“能滚就行”忽略了滚动嵌套冲突、滑动监听、性能优化这些真正影响用户体验的细节。接下来我会从滚动机制的原理说起再到具体实现、常见问题排查把纵向滑动页面这件事讲透。2. 纵向滑动的核心机制与原理解读2.1 触摸事件分发与滚动消费的底层逻辑理解Android滑动绕不开事件分发机制。用户手指在屏幕上滑动时系统会依次调用dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent这三个方法它们构成了滚动消费的完整链路。我习惯用一个比喻来解释事件分发就像公司审批流程。手指按下ACTION_DOWN相当于提交申请先到父布局管理层那里父布局如果决定自己处理onInterceptTouchEvent返回true子View就收不到这个事件了如果父布局不拦截事件就交给子View自己处理onTouchEvent子View处理完了流程结束处理不了就再往上抛。具体到纵向滑动场景核心逻辑是当子View比如RecyclerView判断出手指滑动的位移量主要发生在Y轴方向且自身内容还有滚动空间就消费掉这个事件执行列表滚动如果子View已经滚到底部了父容器比如NestedScrollView或CoordinatorLayout会接管余下的滚动距离实现联动效果。这里有个关键概念叫“滑动距离分配”。在NestedScrollView嵌套RecyclerView的场景中系统通过NestedScrollingChild和NestedScrollingParent接口协调两个View的滚动行为。子View先消费一部分滚动距离消费不完的交给父View处理。这种协作机制避免了传统事件拦截方案中“父子互斥”的问题让嵌套滚动成为可能。2.2 为什么RecyclerView比ListView更流畅老项目里很多还在用ListView但新项目基本都转向RecyclerView了。从实现纵向滑动的角度看RecyclerView的流畅性优势来自三个方面。第一是ViewHolder机制。RecyclerView强制要求使用ViewHolder来缓存item的引用滑动时只需复用缓存里的View不需要重复findViewById。而ListView虽然也有convertView的概念但没有强制约束很多开发者偷懒直接重新创建View性能自然差一截。第二是布局差异。ListView只支持垂直方向的线性排列。RecyclerView通过LayoutManager抽象出布局策略LinearLayoutManager支持纵向和横向GridLayoutManager支持网格StaggeredGridLayoutManager支持瀑布流。这是纵向滑动方案选型时的一个隐藏优势后续想从列表改成双列瀑布流只需要替换LayoutManager不需要重写Adapter。第三是动画支持。RecyclerView内置了ItemAnimator机制item增删改时会自动播放动画而ListView需要手动实现。滑动场景下的体验差异虽然不明显但一旦涉及数据动态更新有没有动画就是两个层级的体验。2.3 嵌套滚动的核心NestedScrolling接口体系ScrollView和RecyclerView直接嵌套会有经典的滑动冲突问题——手指滑动时不知道应该滚外层还是内层。NestedScrollView的出现就是为了解决这个痛点。NestedScrolling的运作机制可以理解成“先让子View滚滚不动了再告诉父View帮忙”。具体流程是手指滑动时子View通过dispatchNestedPreScroll先把滚动距离通知父View问父View要不要先消费一部分比如AppBarLayout的收起动作父View消费完剩下的距离后子View自己再滚子View滚到底了通过dispatchNestedScroll把剩余距离返回给父View让父View继续滚。这个过程是双向协调的和传统的onInterceptTouchEvent拦截机制完全不同。onInterceptTouchEvent是父View单方面决定“我要拦截”而NestedScrolling是父子协商分配滚动距离。这也是为什么CoordinatorLayout AppBarLayout RecyclerView能做到那种丝滑的联动效果——头部折叠、列表滚动、底部加载是同一个滑动手势完成的三阶段动作。3. 核心实操三种纵向滑动页面的完整实现3.1 基于ScrollView的简单纵向滚动最基础的纵向滑动页面用ScrollView就能实现。XML布局定义一个ScrollView内部放一个LinearLayout承载内容。ScrollView android:idid/scrollView android:layout_widthmatch_parent android:layout_heightmatch_parent android:fillViewporttrue android:scrollbarsvertical LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationvertical !-- 内容区域 -- TextView android:layout_widthmatch_parent android:layout_heightwrap_content android:padding16dp android:text这是一段可以滚动的长文本内容 / /LinearLayout /ScrollView有两个参数值得解释。fillViewporttrue的作用是让ScrollView内部布局的高度至少填充满整个屏幕避免内容不够一屏时背景色或占位布局显示不完整。很多人在使用ScrollView做底部按钮固定时遇到问题底部的按钮总是跟着内容一起滚走就是没有理解fillViewport属性的作用正确做法是让内部布局高度撑满视口再把按钮放在布局底部。ScrollView还有一个隐藏特性——它只能有一个直接子View。如果你在ScrollView里直接写了两个平行的TextView运行时会直接崩溃。解决办法是先套一层LinearLayout或ConstraintLayout再往里填充内容。这是新手最容易踩的坑之一。如果你需要监听ScrollView的滚动位置来触发某些逻辑比如滚动到顶部显示返回按钮需要调用setOnScrollChangeListener从回调里拿到当前滚动的Y坐标。scrollView.setOnScrollChangeListener(new View.OnScrollChangeListener() { Override public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) { if (scrollY dp2px(300)) { // 显示返回顶部按钮 } else { // 隐藏返回顶部按钮 } } });3.2 基于RecyclerView的Feed流纵向滑动RecyclerView适合数据量大的列表场景。构建过程分五步添加依赖、定义item布局、创建Adapter、设置LayoutManager、绑定数据。首先在build.gradle里添加依赖不同版本号对应不同AGP版本这里用普遍兼容的版本implementation androidx.recyclerview:recyclerview:1.3.2Adpter是RecyclerView的核心这个示例展示了一个包含内容文本的简单itempublic class FeedAdapter extends RecyclerView.AdapterFeedAdapter.FeedViewHolder { private final ListString mData; public FeedAdapter(ListString data) { this.mData data; } NonNull Override public FeedViewHolder onCreateViewHolder(NonNull ViewGroup parent, int viewType) { View view LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_feed, parent, false); return new FeedViewHolder(view); } Override public void onBindViewHolder(NonNull FeedViewHolder holder, int position) { holder.tvContent.setText(mData.get(position)); holder.itemView.setOnClickListener(v - { // 处理item点击事件 }); } Override public int getItemCount() { return mData null ? 0 : mData.size(); } static class FeedViewHolder extends RecyclerView.ViewHolder { TextView tvContent; FeedViewHolder(NonNull View itemView) { super(itemView); tvContent itemView.findViewById(R.id.tvContent); } } }然后是Activity或Fragment里的设置RecyclerView recyclerView findViewById(R.id.recyclerView); LinearLayoutManager layoutManager new LinearLayoutManager(this); layoutManager.setOrientation(LinearLayoutManager.VERTICAL); recyclerView.setLayoutManager(layoutManager); recyclerView.setAdapter(new FeedAdapter(dataList));关键点在于setLayoutManager这一步。LinearLayoutManager指定了列表的排列方式这个类非常核心除了设置方向还能控制是否自动测量setAutoMeasureEnabled、是否可以滑动setSmoothScrollbarEnabled。这里我建议加上一个优化如果item高度是固定的设置setHasFixedSize(true)可以避免RecyclerView重复测量布局提升滑动性能。但要注意只有确定item高度不随内容变化时才能设置这个属性否则会导致item显示异常这个坑我踩过一次排查了半天才发现是这里的问题。3.3 基于NestedScrollView的复杂页面纵向滚动实际项目中最常见的一种页面结构是顶部是轮播Banner中间是几个快捷入口图标下面是一个信息流列表。这种页面如果整个用ScrollView包裹数据量一大必然卡顿如果只用RecyclerView混合布局又很麻烦。NestedScrollView嵌套RecyclerView的组合刚好解决这个问题。androidx.core.widget.NestedScrollView android:idid/nestedScrollView android:layout_widthmatch_parent android:layout_heightmatch_parent android:fillViewporttrue LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationvertical !-- Banner区域 -- androidx.viewpager2.widget.ViewPager2 android:idid/bannerPager android:layout_widthmatch_parent android:layout_height180dp / !-- 功能入口区域 -- LinearLayout android:idid/quickEntryLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationhorizontal android:padding16dp / !-- 信息流列表 -- androidx.recyclerview.widget.RecyclerView android:idid/innerRecyclerView android:layout_widthmatch_parent android:layout_heightwrap_content android:nestedScrollingEnabledfalse / /LinearLayout /androidx.core.widget.NestedScrollView这里有个非常关键的属性android:nestedScrollingEnabledfalse。如果不在RecyclerView上关闭嵌套滚动会出现滑动卡顿、阻尼感异常的问题。原因在于RecyclerView默认会尝试自己处理滚动事件而外层NestedScrollView又希望接管滚动两个滚动容器互相竞争结果就是滚动不平滑。关闭RecyclerView的嵌套滚动后列表不再响应滑动而是把滚动事件全部交给外层的NestedScrollView处理整个页面只有一个滚动容器事件流变得清晰滑动自然就流畅了。但这样做有一个副作用RecyclerView的所有item会一次性全部加载失去懒加载的优势。如果列表数据量特别大会出现页面加载慢的问题。我的建议是列表项控制在50条以内时用这种方式完全没有问题超过50条建议使用一个RecyclerView 多ItemType的方案替代嵌套结构。3.4 基于ViewPager2的纵向翻页效果ViewPager2纵向化实现起来非常简单因为它本身就支持设置方向。核心代码就一行ViewPager2 viewPager findViewById(R.id.viewPager); viewPager.setOrientation(ViewPager2.ORIENTATION_VERTICAL);设置完方向后Adapter的写法和横向ViewPager2完全一样不用做任何修改。这里值得展开的是它的LayoutManager和RecyclerView其实是同一个——ViewPager2内部本身就是基于RecyclerView实现的它通过RecyclerView的LinearLayoutManager管理页面再通过PagerSnapHelper实现吸附效果。这意味着ViewPager2天然继承了RecyclerView的性能优势同时也意味着它的每个页面都是一个独立的“item”。如果你想实现类似TikTok的上下滑动切换视频用ViewPager2是最合理的方案。不过我建议把ViewPager2的offscreenPageLimit属性一并设置默认只缓存当前页面两侧各一页如果视频加载有延迟可以适当增大缓存页数来提前预加载viewPager.setOffscreenPageLimit(2); // 预加载当前页前后各2页对应的Adapter示例这里演示了页面间通过Fragment来承载不同内容public class VerticalPagerAdapter extends FragmentStateAdapter { private static final int PAGE_COUNT 10; public VerticalPagerAdapter(NonNull FragmentActivity activity) { super(activity); } NonNull Override public Fragment createFragment(int position) { return PageFragment.newInstance(position); } Override public int getItemCount() { return PAGE_COUNT; } }FragmentStateAdapter是androidx专门为ViewPager2封装的Adapter和旧的FragmentPagerAdapter相比它在页面销毁时能释放Fragment实例节省内存适合页数较多的场景。如果每个页面内容比较简单也可以直接用RecyclerView.Adapter搭配Item布局来实现性能会更好因为没有Fragment的开销。4. 纵向滑动场景的工具选型与性能优化4.1 RecyclerView的diffing机制与刷新优化纵向滑动列表在数据刷新时有一个常见的性能问题数据返回后直接调用notifyDataSetChanged()整个列表全部重绘即使只有一条数据变化。在大列表场景下这会造成明显的卡顿和闪烁。合理的做法是使用DiffUtil或者它的协程版本ListAdapter。DiffUtil的核心原理是在后台线程对比新旧数据集的差异生成一个最小更新集包括新增、删除、移动、修改操作然后只对这些变化的位置执行动画更新而不是全量刷新。class FeedDiffCallback extends DiffUtil.ItemCallbackFeedItem() { Override public boolean areItemsTheSame(FeedItem oldItem, FeedItem newItem) { return oldItem.getId() newItem.getId(); } Override public boolean areContentsTheSame(FeedItem oldItem, FeedItem newItem) { return oldItem.equals(newItem); } }关键点是areItemsTheSame和areContentsTheSame两个方法的语义差别。areItemsTheSame比较的是“是不是同一条数据”一般用id判断areContentsTheSame比较的是“同一条数据的内容有没有变化”一般用equals判断。如果id相同但内容不同DiffUtil会执行item更新操作如果id都不同才会执行新增或删除。我在项目中习惯把业务数据封装成带equals方法的data class这样areContentsTheSame直接调用equals即可。上面的Callback配合AsyncListDiffer使用或者直接继承ListAdapterpublic class FeedListAdapter extends ListAdapterFeedItem, FeedListAdapter.VH { public FeedListAdapter() { super(new FeedDiffCallback()); } // 其他方法和普通Adapter一样 }然后刷新数据时只需要一行adapter.submitList(newDataList);submitList内部会自动做diff计算和UI更新整个过程异步执行不会阻塞主线程滑动体验会提升一个档次。4.2 滑动监听的性能陷阱实现纵向滑动页面时很多场景需要监听滚动位置比如实现“滚动到一定距离显示悬浮按钮”或者“首次滚动触发埋点上报”。最常见的做法是在OnScrollListener里计算当前滚动的Y坐标。recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { Override public void onScrolled(NonNull RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); LinearLayoutManager lm (LinearLayoutManager) recyclerView.getLayoutManager(); if (lm ! null) { int firstVisible lm.findFirstVisibleItemPosition(); int lastVisible lm.findLastVisibleItemPosition(); int totalCount lm.getItemCount(); // 判断是否滚动到底部 if (lastVisible totalCount - 1) { // 触发加载更多 } } } });这里有一个性能陷阱onScrolled回调在每次滚动像素变化时都会触发如果在回调里做复杂的计算或IO操作比如写入数据库、上传埋点数据会造成严重的卡顿。正确做法是加一个节流机制比如累计滚动距离超过一定阈值才执行操作或者用Handler延迟执行避免频繁触发。另外findFirstVisibleItemPosition和findFirstCompletelyVisibleItemPosition是两个容易被混淆的方法。前者只要item任何一个像素出现在屏幕上就会返回后者要求item完全可见才返回。做“加载更多”判断时应该用findLastVisibleItemPosition因为触底加载的时机是最后一个item刚露出来就触发而不是完全显示后才触发。4.3 FastScroll与滚动条的自定义如果你的列表比较长建议开启FastScroll快速滚动让用户可以通过右侧的滑块快速定位。RecyclerView原生支持这个功能但需要通过FastScrollLinearLayoutManager配合实现public class FastScrollLinearLayoutManager extends LinearLayoutManager { public FastScrollLinearLayoutManager(Context context) { super(context); } Override public void scrollToPositionWithOffset(int position, int offset) { // 支持快速滚动定位 super.scrollToPositionWithOffset(position, offset); } }同时RecyclerView可以设置fastScrollEnabled和fastScrollStyle属性配合一个自定义的FastScrollPopup显示当前位置。这个功能在联系人列表、文件管理器这类长列表场景非常实用。实现方法不复杂核心是设置横竖滑块背景和字体样式。相对而言如果列表只有几十条数据FastScroll的意义不大反而会占用屏幕空间。建议列表超过一屏且数据项超过200条时再开启。4.4 坐标系与scrollY的兼容问题ScrollView和NestedScrollView获取滚动位置时用的是getScrollY()但RecyclerView没有这个用法它需要找LayoutManager拿第一个可见item的位置和偏移量。这导致在做一些跨View类型的通用逻辑时比如“页面滚动到多少像素后上报阅读进度”需要区分处理不同的滚动容器。int getScrollY(View scrollView) { if (scrollView instanceof NestedScrollView) { NestedScrollView nsv (NestedScrollView) scrollView; return nsv.getScrollY(); } else if (scrollView instanceof RecyclerView) { RecyclerView rv (RecyclerView) scrollView; LinearLayoutManager lm (LinearLayoutManager) rv.getLayoutManager(); if (lm ! null) { int firstVisiblePosition lm.findFirstVisibleItemPosition(); View firstVisibleView lm.findViewByPosition(firstVisiblePosition); int offsetY firstVisibleView null ? 0 : firstVisibleView.getTop(); return firstVisiblePosition * lm.getHeight() offsetY; } } return 0; }这种跨界面的兼容处理在组件化工程里尤其重要因为一个公共的滚动进度上报组件可能被多个页面复用而不同页面使用的容器可能完全不同。4.5 坐标系与行为联动协调布局如果你要做的是一个复杂页面头部有折叠效果列表滚动时头部逐渐收起那么CoordinatorLayout和AppBarLayout是标配。AppBarLayout通过layout_scrollFlags属性控制折叠行为常用的标志位组合是scroll|enterAlways|snap。androidx.coordinatorlayout.widget.CoordinatorLayout android:layout_widthmatch_parent android:layout_heightmatch_parent com.google.android.material.appbar.AppBarLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:idid/appBarLayout androidx.appcompat.widget.Toolbar android:layout_widthmatch_parent android:layout_height?attr/actionBarSize app:layout_scrollFlagsscroll|enterAlways|snap / /com.google.android.material.appbar.AppBarLayout androidx.recyclerview.widget.RecyclerView android:layout_widthmatch_parent android:layout_heightmatch_parent app:layout_behaviorstring/appbar_scrolling_view_behavior / /androidx.coordinatorlayout.widget.CoordinatorLayout这里面的行为机制是RecyclerView通过layout_behavior指定了appbar_scrolling_view_behavior当列表滚动时这个Behavior会接收到滚动事件的回调然后根据scrollFlags计算出AppBarLayout的偏移量。Toolbar设置了scroll|enterAlways意味着向下滑动时Toolbar可以先于列表进入视野enterAlways列表停止滚动时Toolbar会自动吸附到展开或收起状态snap。这个机制实现起来代码很简单但很多人不理解为什么RecyclerView滚动时AppBarLayout会跟着动。核心就在于那个layout_behavior属性它告诉CoordinatorLayout“这个View是AppBarLayout的滚动伙伴”。去掉这个属性AppBarLayout就失去联动效果了这是排查联动失效bug时的第一个检查点。4.6 工具链选型与AGP版本适配在Android Studio中创建项目时AGP版本和Gradle版本的匹配是一个经典配置问题。构建失败提示“Could not load compiled classes for settings file”或者AGP版本不兼容时可以先检查gradle-wrapper.properties里的Gradle版本是否与AGP匹配。记录一下我常用的版本对照和选择思路AGP 8.0要求Gradle 8.0以上AGP 8.1对应Gradle 8.0AGP 8.2对应Gradle 8.2。如果你在Android Studio Hedgehog2023.1.1 Patch 2上创建项目它默认支持AGP 8.0~8.1系列如果用Tag太新的AGP版本比如8.5可能出现兼容性问题。如果遇到构建卡在下载依赖的问题优先检查是否使用了国内镜像仓库可以在settings.gradle里配置阿里云镜像pluginManagement { repositories { maven { url https://maven.aliyun.com/repository/google } maven { url https://maven.aliyun.com/repository/gradle-plugin } maven { url https://maven.aliyun.com/repository/public } mavenCentral() google() } }纵向滑动页面的开发不涉及特别新的API所以即使你的开发环境版本较旧核心方案依然可以用。但如果要做ViewPager2的纵向翻页需要确保依赖版本在1.0.0以上。至于RecyclerView和NestedScrollView这些都是androidx里的基础组件兼容性比较好不同版本间差异不大可以直接使用。5. 常见问题与排查技巧实录5.1 滑动冲突的三种经典表现和解决方案滑动冲突在纵向滑动页面中是最常见的问题总结下来有三种表现形式第一种是“外层要滚内层也要滚”。典型场景是NestedScrollView嵌套了RecyclerView手指滑动时页面卡顿、跳动两个滚动容器在抢事件。解决办法是内层RecyclerView设置nestedScrollingEnabled为false把滚动权交给外层或者统一使用CoordinatorLayout AppBarLayout的联动方案。第二种是“横向事件被纵向容器抢走”。比如RecyclerView的item里有一个横向滑动的Banner手指横向滑动时却被外层纵向列表拦截了。解决办法是让外层容器在判断出水平滑动距离大于垂直滑动距离时不拦截事件通过onInterceptTouchEvent里的滑动角度判断来实现。Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (ev.getAction() MotionEvent.ACTION_MOVE) { float dx ev.getX() - lastX; float dy ev.getY() - lastY; if (Math.abs(dx) Math.abs(dy)) { // 横向滑动不拦截 return false; } } return super.onInterceptTouchEvent(ev); }第三种是“ListView嵌套在ScrollView里无法滚动”。这是老项目中常见的问题本质上是ListView自身没有测量高度在ScrollView里被无限撑开。如果是新项目直接换RecyclerView然后设置nestedScrollingEnabled为false即可如果必须用ListView可以自定义一个不可滑动的ListView高度设置为wrap_content让外层ScrollView接管滚动。5.2 RecyclerView滑动卡顿的定位思路滑动卡顿是纵向列表的高频问题排查思路一般从三条线展开。第一条线是布局层级。item布局层级过深会导致measure和layout耗时变长。可以用Layout Inspector查看item的视图层级原则是能拍平就拍平所有不需要嵌套的布局尽量用flat结构合理使用ConstraintLayout控制层级深度。第二条线是主线程耗时操作。onBindViewHolder里不要做耗时操作图片加载要使用异步库Glide或Coil不要用BitmapFactory直接加载大图。如果你的列表滑动时出现掉帧优先排查onBindViewHolder里的代码。第三条线是item高度是否固定。如果item高度固定设置setHasFixedSize(true)可以跳过列表变化时的重新测量如果item高度不固定不要在onBindViewHolder里频繁修改布局参数这会导致同一item被反复measure。滑动卡顿有一个共性经验用Android Studio自带的Profiler录制一段滑动的CPU和GPU调用看到主线程有长时间任务的基本就是onBindViewHolder里的重复创建对象或磁盘操作导致的。5.3 底部按钮或ViewPager2的滑动冲突底部固定按钮和纵向滑动的组合很常见比如详情页底部有一个“立即购买”按钮页面内容可以滚动按钮固定在底部不跟着滚。这个需求的实现方式是根据内容容器选择不同的策略。如果外层是ScrollView内部布局设置fillViewport为true高度撑满屏幕按钮放在内部布局底部就能实现“内容滚动、按钮固定”的效果。如果外层是RecyclerView固定底部按钮和列表滚动之间的冲突比较少见最常见的是ViewPager2嵌套在NestedScrollView内部时出现的滑动问题。ViewPager2内部也是RecyclerView和NestedScrollView嵌套同样会引发滚动冲突处理方法也是在ViewPager2上关闭nestedScrollingEnabled但关闭后ViewPager2的页面切换手势会受影响需要把ViewPager2的高度设置为wrap_content并对其内容进行测量。这个场景我建议做适配处理不要直接把两个滚动容器嵌套而是重构页面结构外层用CoordinatorLayoutViewPager2作为独立区域下方的内容列表单独使用RecyclerView通过Behavior建立联动关系。这样既保留了纵向滑动的体验又避免了嵌套滚动冲突。5.4 为何adapter.notifyDataSetChanged()后列表没反应这个问题的出现频率很高第一次遇到时容易一头雾水。排查方向优先级首先确认数据源是否真的更新了——有同事在adapter外新建了一个List把新数据赋给这个新List但adapter内部持有的还是旧List引用notifyDataSetChanged后自然没有任何变化。解决办法是用adapter的setData方法更新数据源并通知刷新或者让数据源List的引用保持不变直接修改其内容。第二个可能的原因是onCreateViewHolder里返回了错误的布局导致数据更新后显示的还是旧布局。这种错误不易发现建议通过把viewType参数传入加载逻辑来规避。第三个可能性比较隐蔽——数据更新发生在子线程直接调用了notifyDataSetChanged而RecyclerView必须在主线程操作。Android会抛出 CalledFromWrongThreadException但有些项目里异常被吞掉表现为列表没刷新。规范做法是子线程更新数据后通过runOnUiThread或Handler切回主线程再刷新。5.5 滑动到底部加载更多的正确姿势很多人在RecyclerView上实现加载更多时会写这样的逻辑在onScrolled回调里判断最后可见item和总item数的关系。这个思路本身没错但有几个注意点。第一每次onScrolled都会触发判断需要加一个isLoadingMore标志位防止重复请求。第二网络请求是异步的请求回来后要重新判断列表状态如果数据已经更新要正确计算新增数据后的总item数。这里我推荐一个更省心的方案用ListAdapter配合Paging 3库。Paging 3是Google官方的分页加载组件它把加载状态、重试、占位item都封装好了只需要实现DataSource和配置PagingConfig即可。纵向滑动列表的分页加载逻辑会简单很多。如果不想引入Paging 3的重型依赖自己实现加载更多的模板代码也不复杂// 在Adapter中增加一个footer item type private boolean mIsLoadingMore false; private static final int TYPE_FOOTER 0x01; Override public int getItemViewType(int position) { if (position getItemCount() - 1) { return TYPE_FOOTER; } return super.getItemViewType(position); }footer item可以显示一个加载中的进度条或者“没有更多数据”的提示文案这是比较常见的交互设计。6. 实际项目中的体会与扩展建议做了这么多年的Android开发我对纵向滑动页面最大的体会是永远不要把一个滑动方案固定为唯一解。每个项目都有自己的业务特征、数据量级和交互预期选型时要综合评估。如果团队里都是新人优先选择最稳妥的方案——RecyclerView LinearLayoutManager约定好Adapter的写法减少花活儿。如果是处理复杂页面NestedScrollView RecyclerView是一个短期见效最快的方案但要注意控制列表数据量。如果产品要求极高的滑动性能和复杂的联动效果可以投入成本做自定义Behavior和布局优化。项目后期如果要加入新的交互比如下拉刷新、上拉加载更多可以在现有方案上集成SwipeRefreshLayout或者第三方库SmartRefreshLayout它们的核心都是通过嵌套滚动机制与RecyclerView协作。如果你理解了NestedScrolling的工作流程这些库的适配会非常顺手。最后分享一个小技巧纵向滑动页面在真机上的测试非常重要模拟器无法还原真机的触摸采样率和屏幕刷新率差异。尤其是快速甩动手指时列表的惯性滑动是否跟手、是否掉帧这些体感问题必须上真机才能发现。我习惯在测试时把开发者选项里的“显示布局边界”和“GPU渲染分析”打开能快速定位布局过度绘制和渲染耗时的问题对滑动流畅度的提升有直接帮助。本文还有配套的精品资源点击获取
返回列表