> 文档中心 > ViewPager2使用和探究

ViewPager2使用和探究


1.ViewPager2简单介绍

    ViewPage2是Jetpack中的其中一个组件,可以实现滑动切换页面的效果,通常可以搭配其他组件实现banner切换、以及类似于抖音短视频上下滑动切换播放的效果。
ViewPager2是基于RecyclerView实现的,自然继承了RecyclerView的众多优点,并且针对ViewPager存在的问题做了优化。

  • 支持垂直方向的滑动且实现极其简单。
  • 完全支持RecyclerView的相关配置功能。
  • 支持多个PageTransformer。
  • 支持DiffUtil,局部数据刷新和Item动画
  • 支持模拟用户滑动与禁止用户操作。

ViewPager 与 ViewPager2 部分对比

ViewPager ViewPager 2
PagerAdapter RecyclerView.Adapter
FragmentStatePagerAdapter FragmentStateAdapter
addPageChangeListener registerOnPageChangeCallback
从右到左 (RTL) 的布局支持
垂直方向支持
停用用户输入的功能(setUserInputEnabled、isUserInputEnabled)

常见API

//刷新Viewpager 同样支持recyclerView的局部刷新notifyDataSetChanged() setUserInputEnabled(false);//禁止手动滑动 setCurrentItem(0, false);//跳转到指定页面,false不带滚动动画 setCurrentItem(0);//跳转到指定页面,带滚动动画 addItemDecoration()//设置分割线 同RecyclerView setOffscreenPageLimit();//设置预加载数量 setOrientation();//设置方向 fakeDragBy(offsetPx)//代码模拟用户滑动页面。支持通过编程方式滚动。 setPageTransformer()//设置滚动动画,参数可传 CompositePageTransformer,PageTransformer

依赖引入

    目前最新版本还是1.0.0,而且使用ViewPager2项目必须迁移到Androidx。
implementation ‘androidx.viewpager2:viewpager2:1.0.0’
implementation ‘androidx.recyclerview:recyclerview:1.2.1’ // ViewPager 2 需要使用 RecycleView 的 adapter

2.常见使用

2.1.简单水平及横向滑动

    这种功能的实现和Recyclerview一模一样,都是需要定义一个适配器,并且在适配器中编写Viewholder并填充数据,水平和竖直方向的变化只需要在布局文件中修改viewpager2的属性即可如下:

布局文件

//主布局文件    
//viewpager2组件中的item布局,这里其实可以完全把viewpager2理解为一个recyclerview    

适配器

package com.example.myapplicationimport android.view.LayoutInflaterimport android.view.Viewimport android.view.ViewGroupimport android.widget.LinearLayoutimport android.widget.TextViewimport androidx.recyclerview.widget.RecyclerViewclass LineAdapter(val data: List) : RecyclerView.Adapter() {    inner class LineViewHolder(view: View) : RecyclerView.ViewHolder(view) { val linerLayout = view.findViewById(R.id.line1) val textView = view.findViewById(R.id.text1)    }    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LineViewHolder { return LineViewHolder(     LayoutInflater.from(parent.context).inflate(R.layout.item_line, parent, false) )    }    override fun onBindViewHolder(holder: LineViewHolder, position: Int) { holder.linerLayout.setBackgroundColor(data[position]) holder.textView.text = "这是第${position}个View"    }    override fun getItemCount(): Int { return data.size    }}

主体代码

package com.example.myapplicationimport android.os.Bundleimport com.example.myapplication.databinding.ActivityLineBindingclass LineActivity : BaseActivity() {    override val bind by getBind()    private lateinit var backgrounds: ArrayList    override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (!::backgrounds.isInitialized) {     backgrounds = ArrayList()     backgrounds.add(android.R.color.holo_blue_bright);     backgrounds.add(android.R.color.holo_red_dark);     backgrounds.add(android.R.color.holo_green_dark);     backgrounds.add(android.R.color.holo_orange_light);     backgrounds.add(android.R.color.holo_purple); } bind.vp1.adapter = LineAdapter(backgrounds)    }}

2.2.搭配RadioGroup

布局文件

               

适配器

fragment相关需要继承FragmentStateAdapter并重写里面的方法package com.example.myapplicationimport androidx.fragment.app.Fragmentimport androidx.fragment.app.FragmentActivityimport androidx.viewpager2.adapter.FragmentStateAdapterclass RadioAdapter(fragmentActivity: FragmentActivity, val data: List) :    FragmentStateAdapter(fragmentActivity) {    override fun getItemCount(): Int { return data.size    }    override fun createFragment(position: Int): Fragment { return data[position]    }}

主体代码

在这个里面可以通过registerOnPageChangeCallback来注册监听逻辑package com.example.myapplicationimport androidx.appcompat.app.AppCompatActivityimport android.os.Bundleimport android.util.Logimport android.widget.RadioGroupimport androidx.viewpager2.widget.ViewPager2import com.example.myapplication.databinding.ActivityLineBindingimport com.example.myapplication.databinding.ActivityRadioBindingclass RadioActivity : BaseActivity() {    override val bind by getBind()    private lateinit var frglist: ArrayList    override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (!::frglist.isInitialized) {     frglist = ArrayList()     frglist.add(BlankFragment("111"))     frglist.add(BlankFragment("222"))     frglist.add(BlankFragment("333")) } bind.vpRg.adapter = RadioAdapter(this, frglist) bind.vpRg.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {     override fun onPageSelected(position: Int) {  Log.d("RadioActivity", "onPageSelected: ${position}")  when (position) {      0 -> bind.rbHome.isChecked = true      1 -> bind.rbMsg.isChecked = true      2 -> bind.rgMy.isChecked = true  }     } }) bind.rgVp.setOnCheckedChangeListener(object : RadioGroup.OnCheckedChangeListener {     override fun onCheckedChanged(p0: RadioGroup?, p1: Int) {  Log.d("RadioActivity", "onCheckedChanged: ${p1}")  when (p1) {      R.id.rb_home -> bind.vpRg.currentItem = 0      R.id.rb_msg -> bind.vpRg.currentItem = 1      R.id.rg_my -> bind.vpRg.currentItem = 2  }     } })    }}

2.3.搭配Tablayout来使用

布局文件

                   

适配器

    和2.2用的一样

主体代码

重要的其实就是使用TabLayoutMediator绑定到一起就可以了。package com.example.myapplicationimport androidx.appcompat.app.AppCompatActivityimport android.os.Bundleimport com.example.myapplication.databinding.ActivityRadioBindingimport com.example.myapplication.databinding.ActivityTabLayoutBindingimport com.google.android.material.tabs.TabLayoutMediatorclass TabLayoutActivity : BaseActivity() {    override val bind by getBind()    private lateinit var frglist: ArrayList    override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) initPage()    }    fun initPage() { if (!::frglist.isInitialized) {     frglist = ArrayList()     frglist.add(BlankFragment("111"))     frglist.add(BlankFragment("222"))     frglist.add(BlankFragment("333")) } bind.myViewPager.adapter = RadioAdapter(this, frglist) TabLayoutMediator(     bind.mTabLayout,     bind.myViewPager,     TabLayoutMediator.TabConfigurationStrategy { tab, position ->  when (position) {      0 -> tab.text = "aaa"      1 -> tab.text = "bbb"      2 -> tab.text = "ccc"  }     }).attach()    }}

3.PageTransformer动画效果

    setPageTransformer是ViewPager中就提供的方法,用于设置页面切换动画效果。相较于ViewPager在ViewPager2中setPageTransformer的功能要更加的强大,除了可以设置页面切换动画,还可以用来设置页面边距而且支持同时设置多个PageTransformer。

MarginPageTransformer

    ViewPager2中取消了在ViewPager中的setPageMargin()方法,改为通过提供的MarginPageTransformer来设置页面间距。

bind.vp1.setPageTransformer(MarginPageTransformer(20))CompositePageTransformer与自定义Transformer实现放缩换页ViewPager2支持设置多个Transformer就是通过CompositePageTransformer实现的。CompositePageTransformer类中通过List来维护多个Transformer。val compositePageTransformer = CompositePageTransformer()compositePageTransformer.addTransformer(MarginPageTransformer(20))compositePageTransformer.addTransformer(MyTransformer())bind.vp1.setPageTransformer(compositePageTransformer)

    MyTransformer是自定义的变换类,自定义Transformer的实现也很简单跟ViewPager中的实现基本一致,只不过需要继承实现的接口变成ViewPager2.PageTransformer,同时实现transformPage方法即可,transformPage方法有两个参数:第一个是发生改变的页面对象,第二个是偏移量。
    View page 发生改变的页面对象。
    float position 页面偏移量,取值范围(-1,1)当前页面显示位置值为0,页面向左滑动时,当前页面从屏幕位置向左移动时取值从0变为-1,完全不可见时取值趋向-1,右侧页面向左移动到当前屏幕位置取值从1变为0,页面右滑时同理。

自定义实现放大缩小变化

inner class MyTransformer() : ViewPager2.PageTransformer {    val DEFAULT_MIN_SCALE = 0.85f    val DEFAULT_CENTER = 0.5f    private val mMinScale = DEFAULT_MIN_SCALE    override fun transformPage(page: View, position: Float) { val pageWidth = page.width val pageHeight = page.height //动画锚点设置为View中心 //动画锚点设置为View中心 page.pivotX = (pageWidth / 2).toFloat() page.pivotY = (pageHeight / 2).toFloat() if (position < -1) {     //屏幕左侧不可见时     page.scaleX = mMinScale     page.scaleY = mMinScale     page.pivotY = (pageWidth / 2).toFloat() } else if (position <= 1) {     if (position < 0) {  //屏幕左侧  //(0,-1)  val scaleFactor: Float = (1 + position) * (1 - mMinScale) + mMinScale  page.scaleX = scaleFactor  page.scaleY = scaleFactor  page.pivotX = pageWidth.toFloat()     } else {  //屏幕右侧  //(1,0)  val scaleFactor: Float = (1 - position) * (1 - mMinScale) + mMinScale  page.scaleX = scaleFactor  page.scaleY = scaleFactor  page.pivotX = pageWidth * ((1 - position) * DEFAULT_CENTER)     } } else {     //屏幕右侧不可见     page.pivotX = 0f     page.scaleY = mMinScale     page.scaleY = mMinScale }    }}

一屏多页效果

    使用setOffscreenPageLimit来设置ViewPager2至少预加载左右各一个页面,否则可能存在左右页面未初始化,导致不显示问题。

viewPager2.setOffscreenPageLimit(1);//一屏多页 val recyclerView: View = bind.vp1.getChildAt(0) if (recyclerView is RecyclerView) {     recyclerView.setPadding(100, 0, 100, 0)     (recyclerView as RecyclerView).clipToPadding = false }

4.fragemnt懒加载

    在使用ViewPager显示Fragment时,最麻烦的可能就是ViewPager的预加载问题了,最常用的解决方案有以下几种:

  1. 通过重写ViewPager来解除setOffscreenPageLimit(int)必须大于等于1的限制。
  • ViewPager提供了setOffscreenPageLimit(int)可以用来控制左右预加载页面的数量,但是限制了至少预加载一页。
  • 可以通过拷贝ViewPager源码,重写setOffscreenPageLimit方法来解除限制,从而实现ViewPager不进行预加载。
  • 此方法只能以某一版本的ViewPager源码为基础进行修改,适配性很差。且需要重写ViewPager部分逻辑。
  • 不推荐使用。
  1. 通过Fragment的setUserVisibleHint(boolean)判断Fragment可见性,从而实现懒加载。
  • 这种方法之前详细介绍过了参考: Fragment懒加载实现
  1. adnroidx之后可以使用Lifecycle实现更简洁的懒加载实现方式。

    ViewPager2也是通过上面提到的第三条实现方式来实现懒加载的。ViewPager2需要使用FragmentStateAdapter,不会调用Fragment的setUserVisibleHint(在Android X中已经被废弃),所以不能依靠setUserVisibleHint 来判断Fragment是否可见。
    FragmentStateAdapter 会自动销毁不再用的Fragment(打log发现销毁倒数第三个),如果需要 首次加载后不再进行接口请求,则需要设置ViewPager的offscreenPageLimit

/** * Created by Yangxy on 2020-01-13 * description -- */abstract class LazyFragment : Fragment() {    private var isFirstLoad = true    override fun onResume() { super.onResume() if (isFirstLoad) {     isFirstLoad = false     lazyLoad() }    }    abstract fun lazyLoad()}//设置offscreenPageLimit 为列表总数vp_node.offscreenPageLimit = nodeList.size

参考文献:懒加载

5.Viewpager2缓存分析

详解Android ViewPager2中的缓存和复用机制