1. 从ViewPager到ViewPager2:为什么你需要升级?
如果你做过Android开发,肯定对ViewPager不陌生。那个经典的左右滑动组件,承载了多少应用的引导页、轮播图和主页Tab。我刚开始做Android那会儿,ViewPager几乎是实现这类功能的唯一选择,用起来也还算顺手。但用久了,尤其是在一些复杂场景下,它的“脾气”就上来了——比如你想让它垂直滑动,得自己写一堆自定义代码;数据更新了,调用notifyDataSetChanged()有时会出各种奇怪的bug;还有那个让人头疼的RTL(从右到左)布局支持,简直是国际化的噩梦。
所以,当Google在AndroidX里推出ViewPager2时,我第一时间就去尝鲜了。说实话,第一感觉是:这玩意儿不就是个RecyclerView吗? 没错,你说对了!ViewPager2的内部实现,本质上就是一个RecyclerView加上一个LinearLayoutManager。这听起来好像没什么,但正是这个“换心手术”,带来了翻天覆地的变化。这意味着,RecyclerView身上那些我们熟悉的、强大的特性——比如顺滑的动画、灵活的布局方式、高效的视图复用机制,现在ViewPager2全都拥有了。
我举个例子你就明白了。以前用ViewPager做个垂直滚动的效果,你得去继承ViewPager,重写onTouchEvent和onInterceptTouchEvent,计算坐标转换,一不小心就掉坑里。现在呢?一行代码:viewPager2.setOrientation(ViewPager2.ORIENTATION_VERTICAL)。就这么简单,瞬间搞定。这种体验上的提升,是实实在在的。
所以,我的建议是,如果你的项目还在用老旧的ViewPager,尤其是新启动的项目,没有任何理由不直接上ViewPager2。它不仅解决了历史遗留问题,还带来了更现代、更强大的API。接下来,我就带你深入它的内部,看看这个“RecyclerView马甲”到底是怎么工作的,以及如何用它玩出花来。
2. 深入内核:ViewPager2与RecyclerView的共生关系
很多开发者知道ViewPager2基于RecyclerView,但可能没仔细想过这意味着什么。这不仅仅是“实现方式不同”,而是整个设计哲学和能力的跃迁。我们扒开它的源码看看(别怕,我们只看关键部分),你就能理解它的强大之处。
2.1 源码窥探:它真的是个RecyclerView
我们不用去翻完整的源码,只看最核心的初始化方法 initialize,就能一目了然:
private void initialize(Context context, AttributeSet attrs) {
// 1. 核心:内部创建了一个RecyclerView实例
mRecyclerView = new RecyclerView(context) {
// ... 一些Accessibility相关的重写
};
mRecyclerView.setId(ViewCompat.generateViewId());
// 2. 使用LinearLayoutManager来管理布局
mLayoutManager = new LinearLayoutManager(context);
mRecyclerView.setLayoutManager(mLayoutManager);
// 3. 设置方向(水平或垂直)
setOrientation(context, attrs);
// 4. 将这个RecyclerView作为自己的唯一子View添加进来
mRecyclerView.setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
attachViewToParent(mRecyclerView, 0, mRecyclerView.getLayoutParams());
}
看到没?ViewPager2自己继承自ViewGroup,但在它的“肚子”里,只装了一个全屏的RecyclerView。它自己并不直接处理触摸滑动、视图复用这些脏活累活,而是全部委托给了身经百战的RecyclerView。 这是一种非常聪明的设计模式——组合优于继承。ViewPager2只负责定义“页面”这个概念相关的逻辑(比如当前页、页面切换回调),而底层的滚动、动画、性能优化,全部由RecyclerView这个久经考验的框架来保障。
这带来一个直接好处:性能的先天优势。RecyclerView的视图复用池(RecycledViewPool)机制已经非常成熟,它能保证在快速滑动时,内存占用保持稳定,不会因为页面过多而OOM。而老的ViewPager在这方面就需要开发者自己更小心地处理。
2.2 Adapter的进化:从PagerAdapter到RecyclerView.Adapter
这是使用上变化最大,也是最爽的一点。老ViewPager用的是PagerAdapter,你需要重写instantiateItem, destroyItem, isViewFromObject这几个方法,虽然不难,但模板代码不少,而且容易出错,特别是处理Fragment的时候。
ViewPager2的Adapter就是标准的RecyclerView.Adapter。这意味着:
- 写法统一了:如果你已经会写RecyclerView的列表,那你就会写ViewPager2。再也不用单独记另一套Adapter的API。
- 数据更新通知变得强大且精细:这是我最喜欢的一点。老
PagerAdapter只有notifyDataSetChanged(),一调用就是全局刷新,有时候页面状态会丢失,体验不流畅。而RecyclerView.Adapter有一整套notifyItem*方法。notifyItemInserted(position): 优雅地插入一页,伴有动画。notifyItemRemoved(position): 优雅地删除一页。notifyItemChanged(position): 只更新某一页的内容。notifyDataSetChanged(): 当然也还在,但你可能很少需要用它了。
实战场景:假设你做了一个图片浏览器,用户可以从中间删除一张图片。用ViewPager2,你只需要在数据源List中remove掉对应数据,然后调用adapter.notifyItemRemoved(position),当前页面就会平滑地滑走,后面的页面补位上来,动画流畅自然。用老的ViewPager,你可能需要调用notifyDataSetChanged(),然后会发现当前页索引可能错乱,还需要手动纠正,体验生硬。
2.3 FragmentStateAdapter:更智能的生命周期管理
当ViewPager2需要承载Fragment时,我们使用FragmentStateAdapter。它替代了以前的FragmentStatePagerAdapter。别看名字只差一个词,内在联系却从ViewPager跳转到了RecyclerView。
FragmentStateAdapter同样继承自RecyclerView.Adapter,但它内部会帮我们管理Fragment的生命周期。最关键的是,它的生命周期是与RecyclerView的视图复用机制绑定的。
这是什么意思呢?老的FragmentStatePagerAdapter,其生命周期(如onPause, onResume)是与ViewPager的页面“是否对用户可见”强关联的。而FragmentStateAdapter管理的Fragment,其生命周期则与“该Fragment的ItemView是否被附加到RecyclerView上”关联。在RecyclerView的复用机制下,非当前页及相邻页的Fragment可能会被销毁其视图(onDestroyView),但Fragment实例本身可能还在FragmentManager中。这种机制通常更节省内存。
使用技巧:在createFragment方法中,千万不要尝试去缓存或重用Fragment实例。这个方法每次在需要显示一个新位置的Fragment时都会被调用,FragmentStateAdapter内部自己会处理实例的创建和查找。你只需要根据position返回对应的Fragment类即可。
public class MyFragmentStateAdapter extends FragmentStateAdapter {
public MyFragmentStateAdapter(@NonNull FragmentActivity fragmentActivity) {
super(fragmentActivity);
}
@NonNull
@Override
public Fragment createFragment(int position) {
// 简单、直接地根据位置返回新的Fragment实例
// Adapter内部会自己管理这些实例的复用和生命周期
return MyDetailFragment.newInstance(dataList.get(position));
}
@Override
public int getItemCount() {
return dataList.size();
}
}
3. 实战进阶:玩转ViewPager2的高级特性
了解了内核,我们来看看在实际项目中,怎么把ViewPager2的特性用到极致。这里我分享几个我踩过坑后才总结出来的实用技巧。
3.1 实现酷炫的页面变换效果
RecyclerView有一个神器叫ItemDecoration,可以用来画分割线。但ViewPager2通过PageTransformer给了我们一个更强大的玩具,它可以让我们自定义页面滑动时的变换动画。
ViewPager2.setPageTransformer()方法允许你操作每一个页面的View,在滑动时实时改变它的属性,比如透明度、缩放、旋转、平移等。
举个例子,实现一个“深度缩放”效果:当前页最大,左右两边的页面稍小,并且随着滑动动态变化大小。
viewPager2.setPageTransformer { page, position ->
// position解释:
// 0:当前页完全居中
// -1:当前页的左边一页(刚滑走)
// 1:当前页的右边一页(即将滑入)
// -0.5:当前页向左滑动了50%
val minScale = 0.8f
val scaleFactor = Math.max(minScale, 1 - Math.abs(position) * 0.2f)
page.scaleX = scaleFactor
page.scaleY = scaleFactor
// 可以同时结合淡入淡出
page.alpha = minScale + (1 - minScale) * (1 - Math.abs(position))
}
你还可以实现3D旋转、卡片堆叠、视差滚动等非常复杂的效果。网上有很多开源的PageTransformer库,但我的建议是,先自己动手写几个简单的,理解position参数的含义,以后就能随心所欲地创造自己的动画了。
3.2 与TabLayout的优雅联姻
TabLayout依然是ViewPager2的最佳拍档。但连接它们的不再是ViewPager,而是一个叫TabLayoutMediator的“媒人”。
使用起来非常简洁:
val tabLayout = findViewById<TabLayout>(R.id.tab_layout)
val viewPager2 = findViewById<ViewPager2>(R.id.view_pager2)
val adapter = MyViewPager2Adapter() // 你的Adapter
viewPager2.adapter = adapter
TabLayoutMediator(tabLayout, viewPager2) { tab, position ->
tab.text = adapter.getPageTitle(position) // 从Adapter获取标题
// 或者直接设置:tab.text = "Tab $position"
// 你甚至可以在这里设置自定义的Tab View
}.attach()
关键点:TabLayoutMediator的第三个参数是一个lambda,它会在每个Tab创建时被调用。这里一定要确保viewPager2.adapter已经设置好了,否则TabLayoutMediator无法知道有多少个Tab。
高级用法:自定义Tab。有时候产品经理会要求Tab不是简单的文字,而是要带图标,或者特殊的布局。
TabLayoutMediator(tabLayout, viewPager2) { tab, position ->
// 1. 自定义View
val customView = LayoutInflater.from(this).inflate(R.layout.custom_tab, null)
val iconView = customView.findViewById<ImageView>(R.id.tab_icon)
val textView = customView.findViewById<TextView>(R.id.tab_text)
iconView.setImageResource(iconResIds[position])
textView.text = "Page $position"
tab.customView = customView
// 2. 或者使用TabLayout自带的setIcon和setText
// tab.icon = ContextCompat.getDrawable(this, iconResIds[position])
// tab.text = "Page $position"
}.attach()
3.3 处理预加载与页面懒加载
ViewPager2默认会预加载相邻的页面(左右各一页),以保证滑动的流畅性。这通常是个好特性。但有时候,页面内容非常重(比如里面有复杂的图表、视频、大量图片),我们可能希望只有页面真正对用户可见时(即滑动到它)才去加载数据,以节省流量和内存。
这时,我们可以利用ViewPager2的registerOnPageChangeCallback来监听页面选中事件,并结合Fragment的setUserVisibleHint(在FragmentPagerAdapter时代)或Lifecycle(在FragmentStateAdapter时代)来实现懒加载。
对于使用FragmentStateAdapter的情况,更现代的做法是利用Fragment的Lifecycle:
在对应的Fragment中,观察自己的生命周期状态。
class LazyLoadFragment : Fragment() {
private var isDataLoaded = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 初始化UI,但不加载数据
initView()
}
override fun onResume() {
super.onResume()
// 在onResume时尝试加载数据,但配合ViewPager2,可能需要更精确的判断
// 一个更可靠的方法是使用`ViewPager2`的`offscreenPageLimit`和当前页位置来判断
tryLoadData()
}
// 或者,更好的方式是让Activity/Fragment通过接口或ViewModel来通知数据加载
fun tryLoadData() {
if (!isDataLoaded && isVisibleToUser) { // isVisibleToUser需要自己维护或判断
loadHeavyData()
isDataLoaded = true
}
}
}
实际上,在ViewPager2 + FragmentStateAdapter的架构下,由于生命周期与RecyclerView绑定,判断“是否对用户可见”变得有点棘手。一个常见的实践是,在Fragment中暴露一个手动触发加载的方法(如loadDataIfNeeded()),然后在ViewPager2的页面切换回调中,手动调用当前页和相邻页的加载方法。
viewPager2.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
// 获取当前Fragment并触发加载
val currentFragment = supportFragmentManager.findFragmentByTag("f$position")
(currentFragment as? LazyLoadFragment)?.loadDataIfNeeded()
// 也可以预加载相邻页的数据
val nextFragment = supportFragmentManager.findFragmentByTag("f${position+1}")
(nextFragment as? LazyLoadFragment)?.loadDataIfNeeded()
}
})
这种方式给了开发者更精细的控制权,虽然代码量稍多,但在复杂场景下是值得的。
4. 避坑指南与性能优化
用了这么久ViewPager2,我也踩过不少坑。这里总结几个最常见的,帮你省点时间。
4.1 嵌套滚动冲突(Nested Scrolling)
这是最常遇到的问题。比如,ViewPager2内部有一个可以垂直滚动的RecyclerView(或者ScrollView)。当你在这个内部列表上垂直滑动时,很容易不小心触发ViewPager2的水平滑动,导致体验非常糟糕。
解决方案:处理触摸事件冲突。核心思路是判断用户的手势意图是水平滑动还是垂直滑动。
我们可以自定义一个RecyclerView(或者使用NestedScrollView),在onInterceptTouchEvent中做判断:
class VerticalScrollRecyclerView(context: Context, attrs: AttributeSet) : RecyclerView(context, attrs) {
private var startX = 0f
private var startY = 0f
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
when (e.action) {
MotionEvent.ACTION_DOWN -> {
startX = e.x
startY = e.y
parent.requestDisallowInterceptTouchEvent(true) // 先请求父View不要拦截
}
MotionEvent.ACTION_MOVE -> {
val endX = e.x
val endY = e.y
val disX = abs(endX - startX)
val disY = abs(endY - startY)
// 如果水平距离大于垂直距离,认为是水平滑动,交给父View(ViewPager2)处理
if (disX > disY) {
parent.requestDisallowInterceptTouchEvent(false)
}
// 否则,垂直滑动,自己处理
}
}
return super.onInterceptTouchEvent(e)
}
}
然后在你ViewPager2的页面布局里,使用这个自定义的VerticalScrollRecyclerView。这样,当用户意图垂直滚动内部列表时,外层的ViewPager2就不会“抢”走触摸事件了。
4.2 禁止滑动与动态修改数据源
有时候我们需要暂时禁用ViewPager2的滑动,比如在某个页面显示一个模态弹窗时。
// 方法1:通过RecyclerView(不推荐,因为直接操作内部View)
(viewPager2.getChildAt(0) as? RecyclerView)?.isUserInputEnabled = false
// 方法2:自定义ViewPager2(推荐)
class LockableViewPager2 : ViewPager2 {
var isSwipeEnabled: Boolean = true
override fun onTouchEvent(ev: MotionEvent): Boolean {
return isSwipeEnabled && super.onTouchEvent(ev)
}
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
return isSwipeEnabled && super.onInterceptTouchEvent(ev)
}
}
动态修改数据源:这是ViewPager2比ViewPager优秀的地方。记得一定要先修改Adapter背后的数据集合(List),然后再通知Adapter。
// 添加一页
dataList.add(newData)
adapter.notifyItemInserted(dataList.size - 1)
// 删除当前页
val currentItem = viewPager2.currentItem
dataList.removeAt(currentItem)
adapter.notifyItemRemoved(currentItem)
// 重要!删除后,当前页索引会自动指向下一个,但数据源变了,可能需要调整
viewPager2.setCurrentItem(newPosition, false)
// 更新某一页
dataList[position] = updatedData
adapter.notifyItemChanged(position)
千万注意:不要在notifyItem*之后再去修改数据源,这会导致索引错乱。顺序永远是:改数据 -> 通知Adapter。
4.3 内存与性能优化
虽然ViewPager2基于RecyclerView,性能已经不错,但在页面非常复杂(比如每个页面都是一个完整的Fragment,里面又有图片、视频、复杂布局)时,仍需注意。
-
控制
offscreenPageLimit(离屏页面保留数):默认是1,意味着左右各预加载一页。如果你不需要这么高的流畅度,或者页面非常重,可以设置为0。但设置为0后,快速滑动可能会有白屏。需要根据实际情况权衡。viewPager2.offscreenPageLimit = 1 // 默认值,左右各保留一页 -
Fragment的轻量化:确保每个Fragment的布局不要过于复杂,避免过度绘制。使用
ConstraintLayout减少布局嵌套。图片使用高效的库(如Glide、Coil)并进行缓存。 -
利用RecyclerView的复用池:由于ViewPager2内部是RecyclerView,所有页面的视图类型如果相同,它们会共享同一个复用池。这意味着从第5页滑回第1页,第1页的视图很可能被复用了,而不是重新创建。确保你的Adapter正确实现了
getItemViewType(如果需要的话),以充分利用这一特性。 -
避免在
onBindViewHolder或createFragment中执行耗时操作:这些方法调用频繁,应该只做视图绑定和数据填充。网络请求、数据库查询等操作,应该在页面真正可见或用户交互时再触发,也就是我们前面提到的懒加载策略。
在我经历的一个电商App首页项目中,ViewPager2承载了5个不同的Fragment主页。最初没有做懒加载,App启动时所有页面的数据请求同时发出,导致首屏加载缓慢且流量消耗大。后来我们实现了基于页面可见性的懒加载后,不仅启动速度提升了,非首屏页面的数据也做到了按需更新,用户体验和性能指标都有了明显改善。ViewPager2的这套现代化架构,让这类优化变得有章可循。

2万+

被折叠的 条评论
为什么被折叠?



