学不动也要学!深入了解ViewPager2

码个蛋(codeegg) 第 859 次推文

作者:一包纯牛奶
链接:https://juejin.im/post/5df4aabe6fb9a0161104c8eb

码妞看世界

上次发过一篇:

《学不动也要学!探究Fragment延迟加载的前世今生》还记得嘛?

今天带来ViewPage2。什么?你还不知道ViewPager2?那么请你马上系好安全带,本篇文章将带你一览ViewPager2的风采。

一、ViewPager2的新特性bt

ViewPager2从名字就可以看出来它是ViewPager的升级版,既然是升级版那么它相比ViewPager有哪些新功能和哪些API变化呢?我们接着往下看。

1. ViewPager2新特性

  • 基于RecyclerView实现。这意味着RecyclerView的优点将会被ViewPager2所继承。

  • 支持竖直滑动。只需要一个参数就可以改变滑动方向。

  • 支持关闭用户输入。通过setUserInputEnabled来设置是否禁止用户滑动页面。

  • 支持通过编程方式滚动。通过fakeDragBy(offsetPx)代码模拟用户滑动页面。

  • CompositePageTransformer 支持同时添加多个PageTransformer。

  • 支持DiffUtil ,可以添加数据集合改变的item动画。

  • 支持RTL (right-to-left)布局。我觉得这个功能对国内开发者来说可能用处不大..


2. 相比ViewPager变化的API

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之旅bt

ViewPager2位于androidx包下,也就是它不像ViewPager一样被内置在系统源码中。因此,使用ViewPager2需要额外的添加依赖库。另外,android support中不包含ViewPager2,也就是要使用ViewPager2必须迁移到androidx才可以。

1. 添加依赖,目前ViewPager2的最新版本是1.0.0:

dependencies {
    implementation "androidx.viewpager2:viewpager2:1.0.0"
}


2. ViewPager2布局文件:

<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" />

3. ViewPager2的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
    }
  //  ViewHolder需要继承RecycleView.ViewHolder
    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>

4. 在Activity中为ViewPager设置Adapter:

val viewPager2 = findViewById<ViewPager2>(R.id.view_pager)
    val myAdapter = MyAdapter()
    myAdapter.setList(data)
        viewPager2.adapter = myAdapter

很简单就完成了一个ViewPager的功能,来看下效果怎么样:

5. ViewPager2竖直滑动

接下来我们通过一行代码为其设置竖直滑动

viewPager2.orientation = ViewPager2.ORIENTATION_VERTICAL

竖直滑动用ViewPager是很难实现的,而通过ViewPager2只需要设置一个参数即可。来看下效果:

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()
    }
})

7. 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的参数,当参数值为正数时表示向前一个页面滑动,当值为负数时表示向下一个页面滑动。下面来看下效果图:

演示图中禁止了用户输入,通过按钮点击可以模拟用户滑动。

8. ViewPager2的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的预加载机制做了优化,使得体验上变得更好。关于ViewPager2的offScreenLimit在本篇文章中不再深究,我会在下篇文章中深入探讨。

三、ViewPager2的PageTransformerbt


相比ViewPager,ViewPager2的Transformer功能有了很大的扩展。ViewPager2不仅可以通过PageTransformer用来设置页面动画,还可以用PageTransformer设置页面间距以及同时添加多个PageTransformer。

接下来我们就来认识下ViewPager2的PageTransformer吧!

1. setPageMargin

在第一章中我们提到了ViewPager2移除了setPageMargin方法,那么怎么为ViewPager2设置页面间距呢?其实在ViewPager2中为我们提供了MarginPageTransformer,我们可以通过ViewPager2的setPageTransformer方法来设置页面间距。代码如下:

viewPager2.setPageTransformer(MarginPageTransformer(resources.getDimension(R.dimen.dp_10).toInt()))

上述代码我们为ViewPager2设置了10dp的页面间距。效果如下:

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. 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
    }
}

最后,我们来看下效果

四、ViewPager2与Fragmentbt


我们前面也已经提到了ViewPager2中新增的FragmentStateAdapter 替代了ViewPager的FragmentStatePagerAdapter。那么来我们就用ViewPager2来实现一个Activity中嵌套Fragment的实例。

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" />

2. 实现FragmentStateAdapter

class AdapterFragmentPager(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) {


    private val fragments: SparseArray<BaseFragment> = SparseArray()


    init {
        fragments.put(PAGE_HOME, HomeFragment.getInstance())
        fragments.put(PAGE_FIND, PageFragment.getInstance())
        fragments.put(PAGE_INDICATOR, IndicatorFragment.getInstance())
        fragments.put(PAGE_OTHERS, OthersFragment.getInstance())
    }


    override fun createFragment(position: Int): Fragment {
        return fragments[position]
    }


    override fun getItemCount(): Int {
        return fragments.size()
    }


    companion object {


        const val PAGE_HOME = 0


        const val PAGE_FIND = 1


        const val PAGE_INDICATOR = 2


        const val PAGE_OTHERS = 3
    }
}

3. 在Activity中为ViewPager2设置FragmentStateAdapter

vp_fragment.adapter = AdapterFragmentPager(this)
        vp_fragment.offscreenPageLimit = 3
        vp_fragment.isUserInputEnabled=false

五、小结

本篇文章我们认识了ViewPager2的新特性以及其用法。总得来说ViewPager2相比ViewPager不管在性能上还是在功能上都有了很大的提升。因此,我相信在不久的未来ViewPager2必定会取代ViewPager。那么,你是否已经考虑将ViewPager2用到你的项目中了呢?

相关文章:

今日问题:

ViewPager2你用了嘛?

专属升级社区:《这件事情,我终于想明白了》 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值