简介:这是一份面向Android开发者的RecyclerView嵌套RecyclerView实战案例工程,聚焦主列表内再嵌入网格或子列表的多层数据展示场景,适合需要处理复杂列表结构、嵌套滚动、刷新交互与性能优化的中级开发者参考。工程共65个文件,以30个xml布局与配置、10个java源码为核心,辅以9张png界面素材以及gradle、properties等构建文件,压缩包约260KB,目录组织紧凑,可直接导入开发工具后对照阅读。案例覆盖内外层适配器与LayoutManager配置、在ViewHolder中初始化子RecyclerView、ItemDecoration间隔处理,以及SwipeRefreshLayout上拉下拉刷新和NestedScrolling事件传递思路,并涉及DiffUtil与视图复用等优化方向。已有1248人学习,可作为梳理嵌套列表数据绑定、滑动冲突协调与刷新逻辑的完整代码样本。
1. 需求确定:嵌套的 RecyclerView 为什么在第二次滑动时才出问题
一个商品详情页,顶部横滑一排推荐位,下面竖滑评论区。第一天写完跑得挺顺,第三天测试拿千元机过来,说滑到第十屏就一顿一顿的。拆开看,外层 RecyclerView 的每个 item 里都塞了一个 RecyclerView,两者互不协调:内层用 wrap_content 把全部条目一次性测量完,内层各自的 RecycledViewPool 又互相独立,滚动事件在两个方向上互相抢占。嵌套 RecyclerView 的难点在于嵌套完成后的三件事:滚动由谁控制、高度由谁决定、复用池怎么共享;能不能嵌套本身并不难。这套案例围绕这三个问题给出三种可落地的结构,适合正在做列表页课程设计、或者要把首页楼层拆成独立组件的开发者直接对照改。
2. 总体结构:三种嵌套形态与一张选型判据表
动手写布局之前,先把「嵌套」这个词拆开。同样是外层里放内层,横着嵌和竖着嵌,实现成本差一个量级;内层只有五六条和内层有上百条,选型也完全不同。我见过太多项目一上来就套「内层 wrap_content + 禁用滑动」这个模板,横滑楼层跑得好好的,换成纵向评论区立刻翻车。所以这一章先把形态分类、模块职责、选型判据三件事定下来,后面两章的代码才有地方放。
2.1 三种嵌套形态,先对号入座
形态一,纵向列表里嵌横向列表。典型是首页楼层:外层竖滑,每个楼层内部横滑一排卡片。两个方向正交,手势冲突最小,真正的坑集中在高度测量和复用池上——内层 wrap_content 会让外层把内层所有子项都量一遍,楼层一多,首次布局就肉眼可见地慢。
形态二,纵向列表里嵌纵向列表。典型是评论区、订单明细、课程章节目录。方向相同,手势直接打架,最常见的表现是手指从内层区域起滑,外层不动、内层滑到底之后外层也不接管,用户以为页面卡死了。这种形态要么让内层彻底交出滚动权,要么干脆摊平成一层。
形态三,同向且内层很长,需要独立滚动。比如可展开的二级列表,展开后内层自己滚一段。这类需求我一般不推荐真嵌套:两层各自的滚动位置、各自的空状态、各自的加载更多,维护成本会翻倍。通常的做法是展开时把子项摊平进外层数据源,视觉上仍然是「分组」,实现上只有一个 RecyclerView。
判断自己属于哪一种,看两个问题就够了:内层和外层的滚动方向是否一致,内层条目数是否超过一屏。方向一致且超过一屏,优先考虑摊平;方向正交,可以放心用真嵌套,但要把高度和复用池处理好。
2.2 模块划分:宿主页、装配层、两层 Adapter 各管一段
宿主页只做三件事:拿数据、管生命周期、接收事件。它不应该知道内层 Adapter 长什么样,也不应该在 onBindViewHolder 里去拼装业务字段。这条边界如果在一开始没有划清,后面 ViewHolder 里会逐渐长出一个包含上百行业务逻辑的巨型方法。
数据装配层负责把后端返回的 DTO 转成 UI 直接可用的条目模型。哪些数据属于同一楼层、楼层按什么顺序排、空楼层要不要过滤,这些判断全部在进入 Adapter 之前算完。好处是外层 Adapter 只认「条目类型」,不认业务含义,加一个楼层类型只需要加一个子类和一支 ViewHolder。
外层 Adapter 管类型分发和 ViewHolder 缓存,内层 Adapter 管自己那份 List 的渲染。两层之间通过回调通信,内层把点击事件往上抛,不持有宿主页或者 Fragment 的引用。这一点在项目后期特别重要:内层 Adapter 一旦持有了 Activity,页面销毁时的内存泄漏排查起来就是纯纯的玄学问题。
2.3 选型判据:数据量、滚动归属、复用池三个维度
把上面的判断整理成一张表,评审或者答辩的时候可以直接拿出来讲清楚「为什么选这个结构」。
| 场景特征 | 内层条目量 | 滚动归属 | 推荐结构 | 主要代价 |
|---|---|---|---|---|
| 竖向楼层 + 横向卡片 | 5~20 | 内层横向独立滚 | 真嵌套 + 共享复用池 | 需要固定高度或最大高度 |
| 竖向楼层 + 竖向短列表 | ≤ 8,不超过一屏 | 全部交给外层 | 真嵌套 + 内层禁用滑动 | 内层不能独立滚动 |
| 竖向楼层 + 竖向长列表 | > 20,需要独立滚动 | 内层自己滚 | 摊平为单层多类型 | 数据装配层要做扁平化 |
| 多个独立模块拼一页 | 各自独立 | 各自独立 | ConcatAdapter 组合 | 模块间共享状态要额外设计 |
| 分组可展开 | 展开后较长 | 全部交给外层 | 摊平 + 分组头 | 展开动画要自己处理 |
表里没有「永远正确」的一列,只有代价不同的选择。横向楼层那条之所以还能用真嵌套,是因为方向正交,用户几乎不会同时触发两层滚动;纵向长列表那条之所以建议摊平,是因为同向嵌套下滚动接管逻辑怎么写都会有边界情况,与其去调边界,不如从结构上消除边界。
选型定完之后,再往下才是代码。结构错了,后面调多少个参数都是白费。
3. 数据契约与 Adapter 实现:多类型 Item 加内层数据装配
嵌套结构最容易出问题的地方,是让外层 Adapter 去「临时创建」内层 Adapter。onBindViewHolder 里每次绑定都 new 一个内层 Adapter 再 setAdapter,看起来能跑,实际上每次复用都在丢弃内层的滚动位置和缓存池,滑两屏就开始卡。这一章把数据契约和两层的创建时机定下来:外层用 sealed class 收敛类型,内层 Adapter 在 ViewHolder 构造时创建一次,bind 只换数据。
3.1 用 sealed class 把外层条目类型固定下来
外层条目的类型是有限的,用 sealed class 表达最直接。新增一种楼层就是新增一个子类,when 分支会强制你把对应的处理补上,编译器帮你兜底,比用 int 常量加注释可靠得多。
// 外层条目类型:新增楼层只加子类,when 分支会强制补齐 sealed class FeedItem { abstract val stableId: Long data class Header(val title: String) : FeedItem() { override val stableId: Long = title.hashCode().toLong() } // 横向商品楼层:内层是横向 RecyclerView data class ProductRow( val rowId: String, val products: List<Product> ) : FeedItem() { override val stableId: Long = rowId.hashCode().toLong() } // 纵向评论楼层:内层是纵向 RecyclerView data class CommentBlock( val blockId: String, val comments: List<Comment> ) : FeedItem() { override val stableId: Long = blockId.hashCode().toLong() } }stableId的作用是给 DiffUtil 和 RecyclerView 一个稳定的身份标识。这里用业务字段的哈希,实际项目里更稳妥的做法是让服务端直接下发楼层 ID,避免字符串哈希碰撞导致「明明是两个不同楼层,却被判成同一个」这种难以排查的问题。Product和Comment是普通数据类,字段按业务需要定义,这里不展开。
内层列表直接用List<T>挂在条目上,而不是让 ViewHolder 自己去查。这样装配层在构造FeedItem的时候就把「这一楼层有哪些数据」确定下来了,ViewHolder 只负责渲染,不做数据请求。
3.2 外层 Adapter 的 viewType 分发与 ViewHolder 骨架
外层 Adapter 的职责边界很清楚:拿到FeedItem,判断类型,交给对应的 ViewHolder。它不应该知道商品卡片有几个按钮,也不应该知道评论要不要显示回复数。
class FeedAdapter( private val onProductClick: (Product) -> Unit ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { private val items = mutableListOf<FeedItem>() fun submit(newItems: List<FeedItem>) { items.clear() items.addAll(newItems) notifyDataSetChanged() // 示例先用全量刷新,正式项目换 DiffUtil } // 类型分发:viewType 只在这里出现一次 override fun getItemViewType(position: Int): Int = when (items[position]) { is FeedItem.Header -> TYPE_HEADER is FeedItem.ProductRow -> TYPE_PRODUCT_ROW is FeedItem.CommentBlock -> TYPE_COMMENT } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val inflater = LayoutInflater.from(parent.context) return when (viewType) { TYPE_HEADER -> HeaderVH(inflater.inflate(R.layout.item_header, parent, false)) TYPE_PRODUCT_ROW -> ProductRowVH( inflater.inflate(R.layout.item_product_row, parent, false), onProductClick ) else -> CommentBlockVH(inflater.inflate(R.layout.item_comment_block, parent, false)) } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (val item = items[position]) { is FeedItem.Header -> (holder as HeaderVH).bind(item) is FeedItem.ProductRow -> (holder as ProductRowVH).bind(item) is FeedItem.CommentBlock -> (holder as CommentBlockVH).bind(item) } } override fun getItemCount(): Int = items.size companion object { private const val TYPE_HEADER = 0 private const val TYPE_PRODUCT_ROW = 1 private const val TYPE_COMMENT = 2 } }getItemViewType里返回的整型值必须和onCreateViewHolder里的分支一一对应,错一个就是一个类型转换异常。submit这里用了全量刷新,是为了让示例短一点;正式项目里换成ListAdapter加DiffUtil.ItemCallback,把stableId作为 areItemsTheSame 的判据,能省掉大量无意义的重绘。onProductClick通过构造参数传入而不是让 ViewHolder 自己去找宿主,是为了让内层 Adapter 不持有页面引用,这一条在 3.3 会继续贯彻。
3.3 内层 Adapter 在 ViewHolder 里只建一次,bind 时只换数据
这是整个嵌套结构里最关键的一处。内层 Adapter 和 LayoutManager 应该在 ViewHolder 构造时创建,绑定阶段只做数据替换和滚动位置恢复。
class ProductRowVH( itemView: View, onProductClick: (Product) -> Unit ) : RecyclerView.ViewHolder(itemView) { // 关键:内层 Adapter 随 ViewHolder 创建一次,复用时不重建 private val innerAdapter = ProductAdapter(onProductClick) private val innerList: RecyclerView = itemView.findViewById(R.id.inner_list) init { innerList.apply { layoutManager = LinearLayoutManager( context, LinearLayoutManager.HORIZONTAL, false ) // 与外层共享复用池,横向卡片不再反复 inflate recycledViewPool = sharedPool adapter = innerAdapter setHasFixedSize(true) } } fun bind(row: FeedItem.ProductRow) { // 只换数据,不动 adapter 和 layoutManager innerAdapter.submit(row.products) // 复用时要恢复横向滚动位置,否则每次滑回来都回到最左边 val firstVisible = row.scrollOffset if (firstVisible > 0) { (innerList.layoutManager as LinearLayoutManager) .scrollToPositionWithOffset(firstVisible, 0) } } companion object { // 尺寸相同的横向卡片才可互相复用,注意 viewType 要一致 private val sharedPool = RecyclerView.RecycledViewPool().apply { setMaxRecycledViews(0, 8) } } }setMaxRecycledViews(viewType, max)的第一个参数是内层 Adapter 的 viewType,示例里内层只有一种卡片所以传 0;第二个参数是缓存上限,8 意味着最多留 8 个离屏 ViewHolder 供复用。这个值不要设得太大,缓存池本质上是拿内存换 inflate 时间,横向楼层通常 5~10 就够。
scrollOffset需要在内层滚动时记录回FeedItem.ProductRow,可以在内层加一个OnScrollListener把 firstVisibleItemPosition 写回去。如果不做这一步,用户横滑到第五个商品、继续往下滑再滑回来,卡片会跳回第一个——这个现象在验收时很容易被当成「复用错乱」,实际是滚动位置没有持久化。
sharedPool之所以放在 companion object 里,是为了让同一个页面里所有横向楼层共用一份缓存。前提是这些楼层的卡片布局完全一致、viewType 相同,否则复用会拿到错误的 ViewHolder 类型,直接崩溃。
4. 滚动与高度:嵌套滑动的三种解法与 wrap_content 的代价
滚动和高度是嵌套结构里两个绑在一起的问题。内层一旦用 wrap_content,外层就必须把内层所有子项测量一遍才能确定自己的高度,条目越多测量越久;而内层如果保留自己的滚动能力,同向嵌套下手指一进去就出不来。这一章给三种解法,分别对应不同数据量,最后单独说高度怎么定。
4.1 方案A:内层交出滚动权,全部交给外层
这是改动最小的一种做法,适合内层条数少、不需要独立滚动的场景,比如订单详情里的商品清单、评论区的前三条精选。
// 方案A:内层放弃嵌套滚动,手势全部冒泡给外层 innerList.apply { isNestedScrollingEnabled = false layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) adapter = innerAdapter setHasFixedSize(true) }isNestedScrollingEnabled = false的作用是让内层不再消费嵌套滚动事件,手指从内层区域起滑时,外层会直接接管。配套的 XML 里内层高度必须写成wrap_content,否则内层会自己截断成一个固定框,多出来的条目看不见。
这个方案的代价很明确:内层条目数一旦超过一屏,用户就得先滑到底再滑外层,体验会变得很奇怪。所以它只适合短列表,判断标准就是「能不能一屏放下」。另外,wrap_content会触发全量测量,条数控制在 8 条以内一般感觉不到卡顿,超过这个量级就要考虑方案B。
4.2 方案B:摊平成单层多类型,数据量大的首选
同向嵌套且内层较长时,最省心的做法是把内层数据在装配阶段摊平,最终只渲染一个 RecyclerView。视觉上仍然是「分组」,实现上没有任何嵌套。
// 装配层:把「楼层 + 子项」摊平成一条条独立条目 fun flatten(raw: List<FloorDto>): List<FeedItem> { val result = mutableListOf<FeedItem>() raw.forEach { floor -> when (floor.type) { // 楼层头单独占一条 "comment_block" -> { result.add(FeedItem.Header(floor.title)) floor.comments.forEach { c -> // 子项复用同一个数据类,靠 itemType 区分渲染 result.add(FeedItem.CommentItem(c.id, c.content, c.author)) } } else -> Unit } } return result }摊平之后,外层 Adapter 需要多一个CommentItem类型,内层 Adapter 可以直接删掉。好处是滚动只有一层,复用池只有一份,滚动位置、加载更多、空状态全部按单列表处理,不需要任何嵌套边界逻辑。
代价在装配层:分组头、分组间距、最后一项的圆角这些视觉效果,都要在 ViewHolder 里根据位置自己判断。通常的做法是给条目加一个isFirstOfGroup/isLastOfGroup标记,在bind里控制间距和圆角。这部分工作量比方案A大,但换来的是后面几个月的省心。
4.3 方案C:ConcatAdapter 组合,适合模块化拆分
页面由几个互相独立的模块拼成,每个模块有自己的 Adapter 和刷新逻辑,用 ConcatAdapter 把它们串起来比硬塞进一个 Adapter 干净得多。RecyclerView 1.2.0 之后提供了这个类,可以直接把多个 Adapter 顺序拼接。
// 各模块 Adapter 独立维护,ConcatAdapter 负责拼接 val concatAdapter = ConcatAdapter( ConcatAdapter.Config.Builder() // 隔离各子 Adapter 的 viewType,避免编号冲突 .setIsolateViewTypes(true) // 各子 Adapter 内部稳定 ID 互不干扰 .setStableIdMode(ConcatAdapter.Config.StableIdMode.ISOLATED_STABLE_IDS) .build(), headerAdapter, productRowAdapter, commentAdapter ) outerList.adapter = concatAdaptersetIsolateViewTypes(true)解决的是多个子 Adapter 各自从 0 开始编 viewType 导致的冲突,不开这个开关,两个模块的 viewType 撞在一起会崩。ISOLATED_STABLE_IDS则是让每个子 Adapter 的稳定 ID 在自己的命名空间里生效,适合各模块数据来源不同的情况。
ConcatAdapter 的边界也很清楚:它解决的是「组合」,不解决「嵌套」。如果某个模块内部本身还是一个需要独立滚动的长列表,那仍然要回到方案B去摊平。我一般把它用在「首页由轮播、金刚区、推荐流三块拼成,三块刷新节奏不同」这类场景,而不是用来处理纵向嵌套。
4.4 高度怎么定:wrap_content、固定行高与最大行数
无论选哪个方案,只要页面里还有真嵌套,高度就必须明确。三种写法各有适用面:
wrap_content最直观,但测量成本是 O(n),内层条目全部量完才能确定高度,首屏渲染时间随条目数线性增长。条数少可以接受,条数多必须换。
固定行高最省,内层每一项高度写死,外层直接按「行数 × 行高」算,测量是 O(1)。缺点是内容长度不可控,商品标题两行变三行就会截断,需要在 ViewHolder 里做最大行数限制。
最大高度加内部滚动是折中方案:内层设一个maxHeight,超过就自己滚。这种写法在同向嵌套下会重新引入手势冲突,用之前想清楚用户到底该滚哪一层。
提示:内层用 wrap_content 时,外层 RecyclerView 建议关掉
setHasFixedSize(true),否则内容变化后高度不会重新计算,表现为「数据刷新了但列表高度没变」。
5. 常见问题排查:五类最容易翻车的嵌套行为
嵌套结构的问题有个共同点:现象看起来像 A,原因其实在 B。这一章按「现象 → 原因 → 解决」整理五类高频问题,都是我在实际项目里真实遇到过的,验收前照着过一遍能省掉大量返工。
5.1 高度类:内层只显示一行或整块高度塌陷
现象:内层 RecyclerView 明明有十条数据,界面上只露出第一条,剩下的被裁掉;或者整个楼层高度变成 0,什么都看不见。
原因通常有两个。一是内层高度写成了match_parent或者固定值,在wrap_content的外层里测量约束传递不下去,内层拿到的高度是 0。二是外层用了LinearLayoutManager加wrap_content,但没有给内层设置正确的 LayoutManager 方向,横向数据配了纵向管理器,测量结果自然不对。
解决:内层高度统一写wrap_content,LayoutManager 的方向和数据方向保持一致,横向楼层必须是LinearLayoutManager.HORIZONTAL。如果外层本身是NestedScrollView包裹,还要给外层 RecyclerView 设isNestedScrollingEnabled = false,避免两层滚动容器互相测量。改完之后用 Layout Inspector 看一眼实际测量高度,比反复改 XML 猜要快得多。
5.2 复用类:内层数据错位与点击错乱
现象:滑到下面几屏,某个横向楼层里出现了上一个楼层的商品;或者点击第三个商品,跳转的却是第五个的详情。
原因:内层 Adapter 在onBindViewHolder里被重新创建,或者内层 Adapter 复用了同一个实例但没清空旧数据。点击错乱多半是 ViewHolder 里用了position,而position在复用后是变化的,闭包里捕获的还是旧值。
解决:内层 Adapter 只在 ViewHolder 构造时创建一次,bind里只做数据替换;点击回调传数据对象本身而不是 position,比如onProductClick(product)而不是onProductClick(adapterPosition)。如果确实需要 position,用bindingAdapterPosition并在返回NO_POSITION时直接 return,不要硬转。
5.3 滑动类:内外层抢手势,滑到一半停住
现象:手指从内层区域起滑,外层不动;内层滑到底之后继续滑,外层还是不接管,用户以为页面卡死。
原因:同向嵌套下,内层默认会消费掉所有滚动事件,isNestedScrollingEnabled为 true 时事件不会冒泡给外层。这是 Android 嵌套滚动机制的正常行为,并不是 bug,但对用户来说就是「滑不动」。
解决:短列表按方案A把内层的isNestedScrollingEnabled设为 false,滚动权全部交给外层;长列表按方案B摊平成单层,从结构上消除冲突。不建议自己去重写onTouchEvent做事件分发,这类代码后期极难维护,而且不同厂商 ROM 上的表现还不一致,属于典型的自找麻烦。
5.4 性能类:复用池不共享,越滑越慢
现象:列表滑到后面越来越卡,Profiler 里能看到大量 inflate 和 measure 调用,内存也一直在涨。
原因:每个内层 RecyclerView 都有自己独立的RecycledViewPool,横向楼层滑出屏幕后,里面的 ViewHolder 跟着一起被回收销毁,下一个楼层进来又要重新 inflate。楼层越多,重复创建越频繁。
解决:给所有布局一致的横向楼层共享同一个RecycledViewPool,按 3.3 的写法在 companion object 里建一份,setMaxRecycledViews设一个合理上限。共享的前提是 viewType 一致、布局完全一样,如果不同楼层的卡片样式不同,要拆成不同的池,否则会拿到错误的 ViewHolder 直接崩。
// 共享池的复用边界:只有 viewType 相同的 ViewHolder 才会被复用 val pool = RecyclerView.RecycledViewPool().apply { // 参数1:内层 Adapter 的 viewType;参数2:该类型的缓存上限 setMaxRecycledViews(TYPE_PRODUCT, 10) setMaxRecycledViews(TYPE_BANNER, 4) }两个类型分开设上限,是因为商品卡和 banner 的尺寸、创建成本不一样,用同一个数值要么浪费内存要么不够用。改完用 Profiler 对比一下 inflate 次数,通常会有明显下降。
5.5 状态类:内层滚动位置在复用时被重置
现象:用户横滑到第五个商品,继续往下滑两屏再滑回来,卡片回到了最左边。评论区的展开状态在滑出屏幕后也被收起了。
原因:内层 RecyclerView 的滚动位置存在 ViewHolder 里,ViewHolder 被回收后状态就丢了;数据模型里也没有保存这份状态,重新绑定自然从 0 开始。
解决:把内层滚动位置和展开状态写回数据模型,bind时读出来恢复。滚动位置监听onScrollStateChanged,在IDLE时记录findFirstVisibleItemPosition();展开状态在点击回调里直接改数据对象并notifyItemChanged。这部分代码不多,但漏掉就会变成一个「说不上哪里不对、但用户就是觉得别扭」的问题,验收时也很难复现,属于典型的血泪经验。
6. 验收与答辩:三层验证清单和一次现场演示的走法
嵌套列表的验收不能只看「能不能滑」,要分三层走:结构层看类型分发和复用池,交互层看滚动接管和状态恢复,性能层看测量次数和内存曲线。这三层各自有可量化的检查点,答辩时按顺序讲,评委很容易跟上。
结构层我一般用一张清单过:
| 检查项 | 合格标准 | 验证方式 |
|---|---|---|
| 类型分发 | 每个 viewType 有唯一 ViewHolder | 断点打在 onCreateViewHolder,看分支命中 |
| 内层 Adapter 创建次数 | 与 ViewHolder 创建次数一致,不随 bind 增长 | 构造方法里打日志计数 |
| 复用池共享 | 同类型楼层共用一个池 | 连续滑过 5 个楼层,观察 inflate 次数 |
| 数据装配 | 空楼层被过滤,顺序与接口一致 | 用构造数据跑一遍装配函数 |
交互层重点验三个动作:从内层区域起滑,外层能否正常接管;内层滑到底继续滑,页面是否还有响应;横滑到中间位置,上下滑两屏再回来,位置是否还在。这三个动作覆盖了绝大多数嵌套翻车点,演示时按这个顺序做,比随机滑一遍有说服力。
性能层不用讲太复杂,打开 Profiler 录一段「连续下滑十屏」的轨迹,展示两件事就够了:inflate 次数是否随滑动线性增长,内存曲线是否在滑动停止后回落。如果两条都正常,基本可以认为复用池和缓存策略没问题。反过来,如果 inflate 一直涨、内存不回落,回去查 5.4 的共享池。
答辩时最容易被问的是「为什么不用 NestedScrollView 一把梭」。这个问题要正面回答:NestedScrollView 会把所有子项一次性测量并全部保留,条目一多首屏就慢,而且它不解决复用问题,只解决了「能滚」。把这句话和 4.4 的测量成本对上,追问基本就结束了。
还有一个演示技巧:提前准备一份「故障版」和「修复版」两个分支,现场切一下,让评委看到内层滑不动、数据错位这些现象长什么样,再切回修复版对比。这比口头解释有效得多,也显得你真的踩过这些坑。我第一次带这种嵌套页面的时候没做对比分支,讲到最后评委还是一脸茫然,从那以后每次做列表类方案,我都强制自己先跑一遍「起滑、滑到底、滑回来」这三步再提交。希望帮到你。
本文还有配套的精品资源,点击获取