还在用 ViewPager?是时候替换成 ViewPager2 了!

刘望舒

共 21338字,需浏览 43分钟

 ·

2021-10-01 00:00

 BATcoder技术群,让一部分人先进大厂

大家,我是刘望舒,腾讯TVP,著有三本业内知名畅销书,连续四年蝉联电子工业出版社年度优秀作者,谷歌开发者社区特邀讲师,百度百科收录的高级技术专家。

前华为技术专家,现大厂技术总监。


想要加入 BATcoder技术群,公号回复BAT 即可。


我赌一包辣条 | 作者
https://zhpanvip.gitee.io/2019/12/14/24.Know about ViewPager2 | 原文


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(0as 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


·················END·················

推荐阅读

耗时2年,Android进阶三部曲第三部《Android进阶指北》出版!

『BATcoder』做了多年安卓还没编译过源码?一个视频带你玩转!

写一本技术书到底有多赚?实话告诉你200万轻轻松松!

重生!进阶三部曲第一部《Android进阶之光》第2版 出版!

为了防止失联,欢迎关注我的小号

  微信改了推送机制,真爱请星标本公号👇
浏览 83
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报