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的属性即可如下:
布局文件
//主布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".LineActivity">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/vp1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" />
</LinearLayout>
//viewpager2组件中的item布局,这里其实可以完全把viewpager2理解为一个recyclerview
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/line1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/text1"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
适配器
package com.example.myapplication
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
class LineAdapter(val data: List<Int>) : RecyclerView.Adapter<LineAdapter.LineViewHolder>() {
inner class LineViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val linerLayout = view.findViewById<LinearLayout>(R.id.line1)
val textView = view.findViewById<TextView>(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.myapplication
import android.os.Bundle
import com.example.myapplication.databinding.ActivityLineBinding
class LineActivity : BaseActivity() {
override val bind by getBind<ActivityLineBinding>()
private lateinit var backgrounds: ArrayList<Int>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (!::backgrounds.isInitialized) {
backgrounds = ArrayList<Int>()
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
布局文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".RadioActivity">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/vp_rg"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/rg_vp" />
<RadioGroup
android:id="@+id/rg_vp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_gravity="bottom"
android:orientation="horizontal">
<RadioButton
android:id="@+id/rb_home"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:button="@null"
android:checked="true"
android:drawableTop="@drawable/ic_launcher_foreground"
android:drawablePadding="5dp"
android:gravity="center"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:text="111"
android:textSize="16sp" />
<RadioButton
android:id="@+id/rb_msg"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:button="@null"
android:drawableTop="@drawable/ic_launcher_background"
android:drawablePadding="5dp"
android:gravity="center"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:text="222"
android:textSize="16sp" />
<RadioButton
android:id="@+id/rg_my"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:button="@null"
android:drawableTop="@drawable/ic_launcher_foreground"
android:drawablePadding="5dp"
android:gravity="center"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:text="333"
android:textSize="16sp" />
</RadioGroup>
</RelativeLayout>
适配器
fragment相关需要继承FragmentStateAdapter并重写里面的方法
package com.example.myapplication
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
class RadioAdapter(fragmentActivity: FragmentActivity, val data: List<Fragment>) :
FragmentStateAdapter(fragmentActivity) {
override fun getItemCount(): Int {
return data.size
}
override fun createFragment(position: Int): Fragment {
return data[position]
}
}
主体代码
在这个里面可以通过registerOnPageChangeCallback来注册监听逻辑
package com.example.myapplication
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.RadioGroup
import androidx.viewpager2.widget.ViewPager2
import com.example.myapplication.databinding.ActivityLineBinding
import com.example.myapplication.databinding.ActivityRadioBinding
class RadioActivity : BaseActivity() {
override val bind by getBind<ActivityRadioBinding>()
private lateinit var frglist: ArrayList<BlankFragment>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (!::frglist.isInitialized) {
frglist = ArrayList<BlankFragment>()
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来使用
布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".TabLayoutActivity">
<com.google.android.material.tabs.TabLayout
android:id="@+id/mTabLayout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="0.1">
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Monday" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Tuesday" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Wednesday" />
</com.google.android.material.tabs.TabLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/myViewPager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
</androidx.viewpager2.widget.ViewPager2>
</LinearLayout>
适配器
和2.2用的一样
主体代码
重要的其实就是使用TabLayoutMediator绑定到一起就可以了。
package com.example.myapplication
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.example.myapplication.databinding.ActivityRadioBinding
import com.example.myapplication.databinding.ActivityTabLayoutBinding
import com.google.android.material.tabs.TabLayoutMediator
class TabLayoutActivity : BaseActivity() {
override val bind by getBind<ActivityTabLayoutBinding>()
private lateinit var frglist: ArrayList<BlankFragment>
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的预加载问题了,最常用的解决方案有以下几种:
- 通过重写ViewPager来解除setOffscreenPageLimit(int)必须大于等于1的限制。
- ViewPager提供了setOffscreenPageLimit(int)可以用来控制左右预加载页面的数量,但是限制了至少预加载一页。
- 可以通过拷贝ViewPager源码,重写setOffscreenPageLimit方法来解除限制,从而实现ViewPager不进行预加载。
- 此方法只能以某一版本的ViewPager源码为基础进行修改,适配性很差。且需要重写ViewPager部分逻辑。
- 不推荐使用。
- 通过Fragment的setUserVisibleHint(boolean)判断Fragment可见性,从而实现懒加载。
- 这种方法之前详细介绍过了参考: Fragment懒加载实现
- 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
参考文献:懒加载