深入了解ViewPager2

ViewPager2从名字就可以看出来它是ViewPager的升级版,既然是升级版那么它相比ViewPager有哪些新变化呢?
添加依赖,目前ViewPager2的最新稳定版本是1.0.0(20210525)。1.1.0-alpha01 不稳定

implementation "androidx.viewpager2:viewpager2:1.0.0"

先来看看目录结构


viewpager2目录结构.jpg

1.adapter

可以看到,熟悉的FragmentStatePagerAdapter被FragmentStateAdapter 替代。

(1) StatefulAdapter
/**
 * {@link ViewPager2} adapters should implement this interface to be called during
 * {@link View#onSaveInstanceState()} and {@link View#onRestoreInstanceState(Parcelable)}
 */
public interface StatefulAdapter {
    /** Saves adapter state */
    @NonNull Parcelable saveState();

    /** Restores adapter state */
    void restoreState(@NonNull Parcelable savedState);
}

注释上很明确,是用来处理 onSaveInstanceState 和 onRestoreInstanceState的。基本上就等于是横竖屏切换时的状态的保存和恢复。暂时可以不管

(2) FragmentViewHolder
public final class FragmentViewHolder extends ViewHolder {
    ......
    @NonNull static FragmentViewHolder create(@NonNull ViewGroup parent) {
        FrameLayout container = new FrameLayout(parent.getContext());
        container.setLayoutParams(
                new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                        ViewGroup.LayoutParams.MATCH_PARENT));
        container.setId(ViewCompat.generateViewId());
        container.setSaveEnabled(false);
        return new FragmentViewHolder(container);
    }
    .....
}

很明显,继承自 RecyclerView.ViewHolder,并且 create 一个空的 FrameLayout

(3) FragmentStateAdapter

这是重头戏,代码过长,有些就不贴出来了
首先一个,它继承自 RecyclerView.Adapter,这意味着RecyclerView的优点和功能将会被 FragmentStateAdapter 拥有.看一下 onBindViewHolder

    @Override
    public final void onBindViewHolder(final @NonNull FragmentViewHolder holder, int position) {
        ......
        ensureFragment(position);//***************

        /** Special case when {@link RecyclerView} decides to keep the {@link container}
         * attached to the window, but not to the view hierarchy (i.e. parent is null) */
        final FrameLayout container = holder.getContainer();
        if (ViewCompat.isAttachedToWindow(container)) {
            if (container.getParent() != null) {
                throw new IllegalStateException("Design assumption violated.");
            }
            container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
                @Override
                public void onLayoutChange(View v, int left, int top, int right, int bottom,
                        int oldLeft, int oldTop, int oldRight, int oldBottom) {
                    if (container.getParent() != null) {
                        container.removeOnLayoutChangeListener(this);
                        placeFragmentInViewHolder(holder);//***************
                    }
                }
            });
        }

        gcFragments();
    }

主要是两个地方。一个是 ensureFragment,一个是 placeFragmentInViewHolder。ensureFragment 里面调用了 createFragment,placeFragmentInViewHolder 里面调用了 beginTransaction add 来添加。
整个代码其实不难理解,主要是有个内部类 FragmentMaxLifecycleEnforcer。先来看一下注释

    /**
     * Pauses (STARTED) all Fragments that are attached and not a primary item.
     * Keeps primary item Fragment RESUMED.
     */

翻译过来就是把当前 Fragment 设置为 RESUMED,其他的设置为 STARTED。具体实现在 updateFragmentMaxLifecycle 函数里面。很明显就是控制生命周期的。我们知道,以前使用Fragment时,如果使用show/hide切换Fragment显示,由于Fragment都attach了,当Activity生命周期走到onResume的时候,会触发所有Fragment执行onResume。显然没必要这么做。于是就多了这么一个生命周期控制函数。具体怎么操作呢。先看代码
先定义两个Fragment,如下(FirstFragment SecondFragment 用类似的log)

class FirstFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_first, container, false)
    }

    override fun onHiddenChanged(hidden: Boolean) {
        super.onHiddenChanged(hidden)
        Log.e("Fragment", "FirstFragment - onHiddenChanged")
    }

    override fun onPause() {
        super.onPause()
        Log.e("Fragment", "FirstFragment - onPause")
    }

    override fun onStart() {
        super.onStart()
        Log.e("Fragment", "FirstFragment - onStart")
    }

    override fun onResume() {
        super.onResume()
        Log.e("Fragment", "FirstFragment - onResume")
    }
}

接下来我们用下面这段代码测试

        findViewById<TextView>(R.id.single).setOnClickListener {
            val beginTransaction = supportFragmentManager.beginTransaction()
            beginTransaction.hide(firstFragment)
            beginTransaction.hide(secondFragment)

            //beginTransaction.setMaxLifecycle(firstFragment, androidx.lifecycle.Lifecycle.State.STARTED)
            beginTransaction.show(firstFragment)
            beginTransaction.commit()
        }

当没有给 firstFragment 设置 STARTED 的时候,进入二级页面会调用 onPause,然后返回时会调用 onResume。当设置以后这两个函数都不会掉用。因此,我们可以简化每个Fragment里面的函数调用。其实 FragmentStateAdapter 内部就是这样实现的,也就是它的注释。当前Fragment执行到onResume,非当前Fragment执行到onStart

2.widget

(1) OnPageChangeCallback

viewpager2中改为了 registerOnPageChangeCallback

(2) PageTransformer

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

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

当然也可以设置多个 PageTransformer。

val compositePageTransformer = CompositePageTransformer()
compositePageTransformer.addTransformer(MarginPageTransformer(resources.getDimension(R.dimen.dp_20).toInt()))
viewPager2.setPageTransformer(compositePageTransformer)
(3) 一屏多页

可以通过为RecyclerView设置Padding来实现。

        viewPager2.apply {
            offscreenPageLimit = 1
            val recyclerView = getChildAt(0) as RecyclerView
            recyclerView.apply {
                val padding = 50
                // setting padding on inner RecyclerView puts overscroll effect in the right place
                setPadding(padding, 0, padding, 0)
                clipToPadding = false
            }
        }
        val compositePageTransformer = CompositePageTransformer()
        compositePageTransformer.addTransformer(MarginPageTransformer(50))
        viewPager2.setPageTransformer(compositePageTransformer)

3.零碎功能

(1) 竖直滑动
viewPager2.orientation = ViewPager2.ORIENTATION_VERTICAL
(2) 禁止用户滑动

我们知道,在使用ViewPager的时候想要禁止用户滑动需要重写ViewPager的onInterceptTouchEvent。而ViewPager2被声明为了final,我们无法再去继承ViewPager2。那么我们应该怎么禁止ViewPager2的滑动呢?其实在ViewPager2中已经为我们提供了这个功能,只需要通过setUserInputEnabled即可实现

viewPager2.isUserInputEnabled = false
(3) 模拟拖拽

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的参数,当参数值为正数时表示向前一个页面滑动,当值为负数时表示向下一个页面滑动。
具体实现在 FakeDrag 里面

(4) 与TabLayout结合使用
implementation 'com.google.android.material:material:1.4.0-alpha01'

里面有个TabLayoutMediator。用法也简单,这里就不展开了

4 总结

和ViewPager最大的不同就是多了 FragmentMaxLifecycleEnforcer ,可以控制Fragment的生命周期。因此,如果用 FragmentStateAdapter 的话,一定要注意生命周期

参考文献
学不动也要学!深入了解ViewPager2

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容