还在用 ViewPager?是时候替换成 ViewPager2 了!
BATcoder技术群,让一部分人先进大厂
大家好,我是刘望舒,腾讯TVP,著有三本业内知名畅销书,连续四年蝉联电子工业出版社年度优秀作者,谷歌开发者社区特邀讲师,百度百科收录的高级技术专家。
前华为技术专家,现大厂技术总监。
想要加入 BATcoder技术群,公号回复BAT
即可。
ViewPager2 已经发布很久了,你是否已经用 ViewPager2 去改造你项目的 ViewPager 呢?
一、ViewPager2的新特性
ViewPager2 从名字就可以看出来它是 ViewPager 的升级版,既然是升级版那么它相比 ViewPager 有哪些新功能和哪些 API 变化呢?
我们接着往下看。
1.1 ViewPager2新特性
基于 RecyclerView 实现。这意味着 RecyclerView 的优点,将会被 ViewPager2 所继承; 支持竖直滑动。只需要一个参数就可以改变滑动方向; 支持关闭用户输入。通过 setUserInputEnabled()
来设置是否禁止用户滑动页面;支持通过编程方式滚动。通过 fakeDragBy(offsetPx)
代码模拟用户滑动页面;CompositePageTransformer 支持同时添加多个 PageTransformer; 支持 DiffUtil ,可以添加数据集合改变的 item 动画; 支持 RTL (right-to-left) 布局。我觉得这个功能对国内开发者来说可能用处不大;
1.2 对比 ViewPager
ViewPager2 相比 ViewPager 做了哪些改变呢?研究了一番之后我大概列出以下几点:
ViewPager2 与 ViewPager 同是继承自 ViewGrop,但是 ViewPager2 被声明成了 final。意味着,我们不可能再像 ViewPager 一样,通过继承来修改 ViewPager2 的代码; FragmentStatePagerAdapter 被 FragmentStateAdapter 替代; PagerAdapter 被 RecyclerView.Adapter 替代; addPageChangeListener()
被registerOnPageChangeCallback()
替代。我们知道 ViewPager 的addPageChangeListener()
接收的是一个 OnPageChangeListener 的接口,而这个接口中有三个方法,当想要监听页面变化时,需要重写这三个方法。而 ViewPager2 的registerOnPageChangeCallback()
方法接收的是一个叫 OnPageChangeCallback 的抽象类,因此我们可以选择性的重写需要的方法即可;移除了 setPargeMargin()
方法;关于 offScreenPageLimit()
离屏加载新特性;
以上所罗列的新特性和 API 可能并不完整,如有疏漏可以留言补充。
二、开启ViewPager2之旅
ViewPager2 位于 androidx 包下,也就是它不像 ViewPager 一样被内置在系统源码中。因此,使用 ViewPager2 需要额外的添加依赖库。
另外,android support 中不包含 ViewPager2,也就是要使用 ViewPager2 必须迁移到 androidx 才可以。
2.1 添加依赖
目前ViewPager2的最新版本是1.0.0。
dependencies {
implementation "androidx.viewpager2:viewpager2:1.0.0"
}
2.2 ViewPager2 布局文件
在 Layout 中声明 ViewPage2。
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
2.3 声明 Adapter
因为 ViewPager2 内部封装的是 RecyclerView,因此它的 Adapter 也就是 RecyclerView 的 Adapter。
class MyAdapter : RecyclerView.Adapter<MyAdapter.PagerViewHolder>() {
private var mList: List<Int> = ArrayList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PagerViewHolder {
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.item_page, parent, false)
return PagerViewHolder(itemView)
}
override fun onBindViewHolder(holder: PagerViewHolder, position: Int) {
holder.bindData(mList[position])
}
fun setList(list: List<Int>) {
mList = list
}
override fun getItemCount(): Int {
return mList.size
}
class PagerViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val mTextView: TextView = itemView.findViewById(R.id.tv_text)
private var colors = arrayOf("#CCFF99","#41F1E5","#8D41F1","#FF99CC")
fun bindData(i: Int) {
mTextView.text = i.toString()
mTextView.setBackgroundColor(Color.parseColor(colors[i]))
}
}
}
item_page 中代码如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<TextView
android:id="@+id/tv_text"
android:background="@color/colorPrimaryDark"
android:layout_width="match_parent"
android:layout_height="280dp"
android:gravity="center"
android:textColor="#ffffff"
android:textSize="22sp" />
</LinearLayout>
2.4 绑定 ViewPager 与 Adapter
在Activity中为ViewPager设置Adapter。
val viewPager2 = findViewById<ViewPager2>(R.id.view_pager)
val myAdapter = MyAdapter()
myAdapter.setList(data)
viewPager2.adapter = myAdapter
很简单就完成了一个 ViewPager 的功能,来看下效果怎么样:
2.5 ViewPager2 竖直滑动
接下来我们通过一行代码为其设置竖直滑动。
viewPager2.orientation = ViewPager2.ORIENTATION_VERTICAL
竖直滑动用 ViewPager 是很难实现的,而通过 ViewPager2 只需要设置一个参数即可。来看下效果:
2.6 页面滑动事件监听
上文已经提到过了,我们为 ViewPager 设置页面滑动的监听事件,需要重写三个方法。
而为 ViewPager2 设置监听事件,只需要重写需要的方法即可,因为 ViewPager2 中 OnPageChangeCallback 是一个抽象类。
viewPager2.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
Toast.makeText(this@MainActivity, "page selected $position", Toast.LENGTH_SHORT).show()
}
})
2.7 控制切换效果
涉及 API:setUserInputEnabled()
与 fakeDragBy()
。
我们知道,在使用 ViewPager 的时候想要禁止用户滑动需要重写 ViewPager 的 onInterceptTouchEvent()
。而 ViewPager2 被声明为了 final,我们无法再去继承 ViewPager2。
那么我们应该怎么禁止 ViewPager2 的滑动呢?其实在 ViewPager2 中已经为我们提供了这个功能,只需要通过 setUserInputEnabled()
即可实现。
viewPager2.isUserInputEnabled = false
同时 ViewPager2 新增了一个 fakeDragBy()
的方法。通过这个方法可以来模拟拖拽。在使用 fakeDragBy()
前需要先 beginFakeDrag()
方法来开启模拟拖拽。
fakeDragBy()
会返回一个 boolean 值:
true 表示有 fake drag 正在执行; false 表示当前没有 fake drag 在执行。
我们通过代码来尝试下:
fun fakeDragBy(view: View) {
viewPager2.beginFakeDrag()
if (viewPager2.fakeDragBy(-310f))
viewPager2.endFakeDrag()
}
需要注意到是 fakeDragBy()
接受一个 float 的参数,当参数值为正数时表示向前一个页面滑动,当值为负数时表示向下一个页面滑动。
下面来看下效果图:
演示图中禁止了用户输入,通过按钮点击可以模拟用户滑动。
2.8 offScreenPageLimit()
offScreenPageLimit()
在 ViewPager 中就已经存在,这个参数用来控制 ViewPager 左右两端预加载页面的个数。
为了保证 ViewPager 的流畅性,offScreenPageLimit()
被强制规定为大于 0 的数,即使我们将其设置为 0,ViewPager 内部也会将其改为 1。
因此 ViewPager 就被强制左右两边至少加载一个页面,这也是一直被广大开发者所诟病的一个问题。
而在 ViewPager2 中针对这一问题做了优化。我们点开 ViewPager2 的源码来看下:
# VewPager2
private @OffscreenPageLimit int mOffscreenPageLimit = OFFSCREEN_PAGE_LIMIT_DEFAULT;
/**
* Value to indicate that the default caching mechanism of RecyclerView should be used instead
* of explicitly prefetch and retain pages to either side of the current page.
* @see #setOffscreenPageLimit(int)
*/
public static final int OFFSCREEN_PAGE_LIMIT_DEFAULT = -1;
/** @hide */
@SuppressWarnings("WeakerAccess")
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Retention(SOURCE)
@IntDef({OFFSCREEN_PAGE_LIMIT_DEFAULT})
@IntRange(from = 1)
public @interface OffscreenPageLimit {
}
可以看到在 ViewPager2 中 offScreenPageLimit()
的默认值被设置为了 -1,而且 offScreenPageLimit()
这个成员变量被一个名为 @OffscreenPageLimit
的注解所修饰,而在这个注解,强制要求 int 的范围是大于等于 1 的。
什么?ViewPager2 的预加载页面,难道也必须大于等于 1?那这相比 ViewPager 有什么区别呢?
先别着急,其实最大的区别就在这个 OFFSCREEN_PAGE_LIMIT_DEFAULT
上,这个值被设置为 - 1,那么它代表什么意思呢?我们可以从 ViewPager2 源码的注释中找出一些端倪
/**
* <p>Set the number of pages that should be retained to either side of the currently visible
* page(s). Pages beyond this limit will be recreated from the adapter when needed. Set this to
* {@link #OFFSCREEN_PAGE_LIMIT_DEFAULT} to use RecyclerView's caching strategy. The given value
* must either be larger than 0, or {@code #OFFSCREEN_PAGE_LIMIT_DEFAULT}.</p>
*
* <p>Pages within {@code limit} pages away from the current page are created and added to the
* view hierarchy, even though they are not visible on the screen. Pages outside this limit will
* be removed from the view hierarchy, but the {@code ViewHolder}s will be recycled as usual by
* {@link RecyclerView}.</p>
*
* <p>This is offered as an optimization. If you know in advance the number of pages you will
* need to support or have lazy-loading mechanisms in place on your pages, tweaking this setting
* can have benefits in perceived smoothness of paging animations and interaction. If you have a
* small number of pages (3-4) that you can keep active all at once, less time will be spent in
* layout for newly created view subtrees as the user pages back and forth.</p>
*
* <p>You should keep this limit low, especially if your pages have complex layouts. By default
* it is set to {@code OFFSCREEN_PAGE_LIMIT_DEFAULT}.</p>
*
* @param limit How many pages will be kept offscreen on either side. Valid values are all
* values {@code >= 1} and {@link #OFFSCREEN_PAGE_LIMIT_DEFAULT}
* @throws IllegalArgumentException If the given limit is invalid
* @see #getOffscreenPageLimit()
*/
public void setOffscreenPageLimit(@OffscreenPageLimit int limit) {
if (limit < 1 && limit != OFFSCREEN_PAGE_LIMIT_DEFAULT) {
throw new IllegalArgumentException(
"Offscreen page limit must be OFFSCREEN_PAGE_LIMIT_DEFAULT or a number > 0");
}
mOffscreenPageLimit = limit;
// Trigger layout so prefetch happens through getExtraLayoutSize()
mRecyclerView.requestLayout();
}
从这段对 setOffscreenPageLimit(int)
方法的注释中,我们可以看到,当 setOffscreenPageLimit()
被设置为 OFFSCREEN_PAGE_LIMIT_DEFAULT
时候会使用 RecyclerView 的缓存机制。那么我们就来在 ViewPager2 中尝试下加载 Fragment 是一种怎样的效果吧。
首先我们在 ViewPager 中添加多个 Fragment,并且 setOffscreenPageLimit()
使用默认值,然后再 Fragment 声明周期中打印出日志,代码不再贴出,直接看日志打印的内容:
从日志中可以看出来,初始化的只有第一个 Fragment。当我们滑动到第二个页面的时候,打印日志如下:
可以看到,第二个页面在滑动的时候才被初始化,由此我们可以看出在 ViewPager2 中默认的 offscreenPageLimit
是不会进行页面预加载的。
接下来我们将 offscreenPageLimit
值改为 1,再来看下输出日志:
此时可以看到 offscreenPageLimit
设置为 1 后,会预加载进来一个页面,和 ViewPager 几乎是一样的效果。
总之,ViewPager2 对于 ViewPager 的预加载机制做了优化,使得体验上变得更好。
三、PageTransformer
相比 ViewPager,ViewPager2 的 Transformer 功能有了很大的扩展。
ViewPager2 不仅可以通过 PageTransformer 用来设置页面动画,还可以用 PageTransformer 设置页面间距,以及同时添加多个 PageTransformer。接下来我们就来认识下 ViewPager2 的 PageTransformer 吧!
3.1 setPageMargin
在第一章中我们提到了 ViewPager2 移除了 setPageMargin()
方法,那么怎么为 ViewPager2 设置页面间距呢?
其实在 ViewPager2 中为我们提供了 MarginPageTransformer,我们可以通过 ViewPager2 的 setPageTransformer 方法来设置页面间距。代码如下:
viewPager2.setPageTransformer(MarginPageTransformer(resources.getDimension(R.dimen.dp_10).toInt()))
上述代码我们为 ViewPager2 设置了 10dp 的页面间距。
效果如下:
3.2 认识 CompositePageTransformer
这个时候我们应该有个疑问,为 ViewPager2 设置了页面间距后,如果还想设置页面动画的 Transformer 怎么办呢?这时候就该 CompositePageTransformer 出场了。
从名字上也可以看出来,它是一个组合的 PageTransformer。没错,CompositePageTransformer 实现了 PageTransformer 接口,同时在其内部维护了一个 List 集合,我们可以将多个 PageTransformer 添加到 CompositePageTransformer 中。
val compositePageTransformer = CompositePageTransformer()
compositePageTransformer.addTransformer(ScaleInTransformer())
compositePageTransformer.addTransformer(MarginPageTransformer(resources.getDimension(R.dimen.dp_10).toInt()))
viewPager2.setPageTransformer(compositePageTransformer)
上述代码中我们通过 CompositePageTransformer 为 ViewPager 设置了 MarginPageTransformer 和一个页面缩放的 ScaleInTransformer。来看下效果:
3.3 ViewPager2 中的 PageTransformer
PageTransformer 是一个位于 ViewPager2 中的接口,因此 ViewPager2 的 PageTransformer 是独立于 ViewPager 的,它与 ViewPager 的 PageTransformer 没有任何关系。
虽然如此,却不必担心。因为 ViewPager2 的 PageTransformer 和 ViewPager 的 PageTransformer 实现方式一模一样。我们看下上一小节中用到的 ScaleInTransformer:
class ScaleInTransformer : ViewPager2.PageTransformer {
private val mMinScale = DEFAULT_MIN_SCALE
override fun transformPage(view: View, position: Float) {
view.elevation = -abs(position)
val pageWidth = view.width
val pageHeight = view.height
view.pivotY = (pageHeight / 2).toFloat()
view.pivotX = (pageWidth / 2).toFloat()
if (position < -1) {
view.scaleX = mMinScale
view.scaleY = mMinScale
view.pivotX = pageWidth.toFloat()
} else if (position <= 1) {
if (position < 0) {
val scaleFactor = (1 + position) * (1 - mMinScale) + mMinScale
view.scaleX = scaleFactor
view.scaleY = scaleFactor
view.pivotX = pageWidth * (DEFAULT_CENTER + DEFAULT_CENTER * -position)
} else {
val scaleFactor = (1 - position) * (1 - mMinScale) + mMinScale
view.scaleX = scaleFactor
view.scaleY = scaleFactor
view.pivotX = pageWidth * ((1 - position) * DEFAULT_CENTER)
}
} else {
view.pivotX = 0f
view.scaleX = mMinScale
view.scaleY = mMinScale
}
}
companion object {
const val DEFAULT_MIN_SCALE = 0.85f
const val DEFAULT_CENTER = 0.5f
}
}
3.4 ViewPager2的一屏多页效果
在 ViewPager2 的官方 Sample 上看到了 ViewPager2 的一屏多页可以通过为 RecyclerView 设置 Padding 来实现。代码如下:
viewPager2.apply {
offscreenPageLimit=1
val recyclerView= getChildAt(0) as RecyclerView
recyclerView.apply {
val padding = resources.getDimensionPixelOffset(R.dimen.dp_10) +
resources.getDimensionPixelOffset(R.dimen.dp_10)
setPadding(padding, 0, padding, 0)
clipToPadding = false
}
}
val compositePageTransformer = CompositePageTransformer()
compositePageTransformer.addTransformer(ScaleInTransformer())
compositePageTransformer.addTransformer(MarginPageTransformer(resources.getDimension(R.dimen.dp_10).toInt()))
viewPager2.setPageTransformer(compositePageTransformer)
最后,我们来看下效果
四、ViewPager2 与 Fragment
我们前面也已经提到了 ViewPager2 中新增的 FragmentStateAdapter,替代了 ViewPager 的 FragmentStatePagerAdapter。那么来我们就用 ViewPager2 来实现一个 Activity 中嵌套 Fragment 的实例。
4.1 Activity的layout中添加ViewPager2
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/vp_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@id/rg_tab" />
4.2 实现 FragmentStateAdapter
class AdapterFragmentPager(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) {
override fun createFragment(position: Int): Fragment {
return when (position) {
PAGE_HOME -> HomeFragment.getInstance();
PAGE_FIND -> PageFragment.getInstance();
PAGE_INDICATOR -> IndicatorFragment.getInstance();
PAGE_OTHERS -> OthersFragment.getInstance();
else -> EmptyFragment.getInstance();
}
}
override fun getItemCount(): Int {
return 4
}
companion object {
const val PAGE_HOME = 0
const val PAGE_FIND = 1
const val PAGE_INDICATOR = 2
const val PAGE_OTHERS = 3
}
}
4.3 在Activity中为ViewPager2设置FragmentStateAdapter
vp_fragment.adapter = AdapterFragmentPager(this)
vp_fragment.offscreenPageLimit = 3
vp_fragment.isUserInputEnabled=false
五、ViewPager2与TabLayout
TabLayout 也是项目中经常用到的一个控件,它通常会与 ViewPager 一起出现。那么对于 ViewPager2 应该怎么使用 Tablayout 呢?
这需要我们认识一个新类 TabLayoutMediator,这个类是在 material-1.2.0 中新增的一个类,目前 material 包的最新版本是 1.2.0-alpha03,因此需要我们单独引入这个包,依赖如下:
implementation 'com.google.android.material:material:1.2.0-alpha03'
TabLayoutMediator 的构造方法接收三个参数,第一个参数为 TabLayout;第二个参数为 ViewPager2;第三个参数是 TabConfigurationStrategy,这是一个接口,该接口中有一个方法 onConfigureTab(@NonNull TabLayout.Tab tab, int position), 第一个参数是当前 Tab, 第二个当前 position,源码如下:
public interface TabConfigurationStrategy {
/**
* Called to configure the tab for the page at the specified position. Typically calls {@link
* TabLayout.Tab#setText(CharSequence)}, but any form of styling can be applied.
*
* @param tab The Tab which should be configured to represent the title of the item at the given
* position in the data set.
* @param position The position of the item within the adapter's data set.
*/
void onConfigureTab(@NonNull TabLayout.Tab tab, int position);
}
接下来我们便可以通过 TabLayoutMediator 将 TabLayout 与 ViewPager2 关联起来了。
TabLayoutMediator(tab_layout, view_pager) { tab, position ->
// 为Tab设置Text
tab.text = Card.DECK[position].toString()
}.attach()
使用起来非常简单,实现效果如下图所示:
六、小结
本篇文章我们认识了 ViewPager2 的新特性以及其用法。
总得来说 ViewPager2 相比 ViewPager 不管在性能上,还是在功能上,都有了很大的提升。因此,我相信在不久的未来,ViewPager2 必定会取代 ViewPager。
那么,你是否已经考虑将 ViewPager2 用到你的项目中了呢?
最后再来给大家推荐一下 BannerViewPager。这是一个基于 ViewPager 实现的具有强大功能的无限轮播库。现在 BannerViewPager 3.0 版本中用 ViewPager2 来重构代码。欢迎大家到 GitHub 关注 BannerViewPager。
BannerViewPager
https://github.com/zhpanvip/BannerViewPager
推荐阅读
• 耗时2年,Android进阶三部曲第三部《Android进阶指北》出版!
为了防止失联,欢迎关注我的小号
微信改了推送机制,真爱请星标本公号👇