文章目录
简介
ViewPager2是Google在 androidx 组件包里增加的一个组件,目前已经到了1.0.0的稳定版本。
谷歌为什么要出这个组件呢?官方是这么说的:
源码简单了解
ViewPager2继承ViewGroup,所以跟ViewPager不兼容,内部核心是RecyclerView+LinearLayoutManager,其实就是对RecyclerView封装了一层,所有功能都是围绕着RecyclerView和LinearLayoutManager展开。
我们知道,SnapHelper用于辅助RecyclerView在滚动结束时将Item对齐到某个位置。PagerSnapHelper的作用让滑动结束时使当前Item居中显示,并且限制一次只能滑动一页,不能快速连续滑动,这样就和原来viewpager的交互很像,实现类似交互效果。所以需要设置下SnapHelper:
new PagerSnapHelper().attachToRecyclerView(mRecyclerView);
ViewPager2需要一个adapter用来显示内容,adapter可以是RecyclerView.Adapter或者FragmentStateAdapter,还有ViewPager2是final类,所以无法被拓展。
改动点
新功能
- 支持RTL布局
- 支持竖向滚动
- 支持关闭预加载offscreenPageLimit
- 完整支持notifyDataSetChanged(还有局部刷新)
- 方便启用和禁用用户的滑动 (setUserInputEnabled)
- 引入了MarginPageTransformer,以提供在页面之间增加空隙
- 引入CompositePageTransformer来组合多个PageTransformer
- 因为ViewPager2由Recyclerview支持,所以也支持ItemDecorator、DiffUtil等
API的变动
- FragmentStateAdapter替换了原来的 FragmentStatePagerAdapter
- RecyclerView.Adapter替换了原来的 PagerAdapter
- registerOnPageChangeCallback替换了原来的 addPageChangeListener
注意:不要忘记unregisterOnPageChangeCallback
常用Api
- void setOrientation(int orientation)设置布局方向
- void setUserInputEnabled(boolean enabled)设置是否允许用户输入/触摸
- int getCurrentItem()获取当前Item下标
- void setCurrentItem(int item)设置当前Item下标
- setOffscreenPageLimit(int limit) 设置屏幕外加载页面数量
- setPageTransformer(ViewPager2.PageTransformer transformer) 设置页面滑动时的变换效果
- registerOnPageChangeCallback(OnPageChangeCallback) 注册页面改变回调
- unregisterOnPageChangeCallback(ViewPager2.OnPageChangeCallback callback) 解注册页面改变回调
更多的可以自行前往ViewPager2查看。
引入implementation
dependencies {
implementation "androidx.viewpager2:viewpager2:1.0.0"
}
注意,viewpager2是在androidx里的,而androidx和android support库不能共存,所以项目中还是用support库的注意需要转移到androidx,这里不详述,自己google。
官方demo介绍
ViewPager2 with Views
这个示例用来展示添加views
代码:
XML布局
<LinearLayout 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"
android:orientation="vertical"
tools:background="#FFFFFF">
<include layout="@layout/controls" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
定义RecyclerView.Adapter
viewPager = findViewById(R.id.view_pager)
viewPager.adapter = CardViewAdapter()
class CardViewAdapter : RecyclerView.Adapter<CardViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CardViewHolder {
return CardViewHolder(CardView(LayoutInflater.from(parent.context), parent))
}
override fun onBindViewHolder(holder: CardViewHolder, position: Int) {
holder.bind(Card.DECK[position])
}
override fun getItemCount(): Int {
return Card.DECK.size
}
}
class CardViewHolder internal constructor(private val cardView: CardView) :
RecyclerView.ViewHolder(cardView.view) {
internal fun bind(card: Card) {
cardView.bind(card)
}
}
从上面的代码可以看出adapter和使用RecyclerView是一样的。
ViewPager2 with Fragments
这个示例用来展示添加Fragments
代码:
XML布局
<LinearLayout 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"
android:orientation="vertical"
tools:background="#FFFFFF">
<include layout="@layout/controls" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
使用FragmentStateAdapter
viewPager.adapter = object : FragmentStateAdapter(this) {
override fun createFragment(position: Int): Fragment {
return CardFragment.create(Card.DECK[position])
}
override fun getItemCount(): Int {
return Card.DECK.size
}
}
class CardFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val cardView = CardView(layoutInflater, container)
cardView.bind(Card.fromBundle(arguments!!))
return cardView.view
}
companion object {
/** Creates a Fragment for a given [Card] */
fun create(card: Card): CardFragment {
val fragment = CardFragment()
fragment.arguments = card.toBundle()
return fragment
}
}
}
ViewPager2和Fragment结合使用,需要使用FragmentStateAdapter。FragmentStateAdapter其实是继承RecyclerView.Adapter,有兴趣的可以去看看源码。
ViewPager2 with TabLayout
这个示例用来展示结合TabLayout使用
代码:
XML布局
<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:background="#FFFFFF">
<include layout="@layout/controls" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabMode="scrollable" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
注意:TabLayout与旧版ViewPager集成在一起很简单,只需将其添加为ViewPager的子项,并按设置layout_gravity属性就可以了。
<android.support.v4.view.ViewPager
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.TabLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top" />
</android.support.v4.view.ViewPager>
然而,ViewPager2不接受TabLayout作为子View绑定。
定义RecyclerView.Adapter
class CardViewAdapter : RecyclerView.Adapter<CardViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CardViewHolder {
return CardViewHolder(CardView(LayoutInflater.from(parent.context), parent))
}
override fun onBindViewHolder(holder: CardViewHolder, position: Int) {
holder.bind(Card.DECK[position])
}
override fun getItemCount(): Int {
return Card.DECK.size
}
}
class CardViewHolder internal constructor(private val cardView: CardView) :
RecyclerView.ViewHolder(cardView.view) {
internal fun bind(card: Card) {
cardView.bind(card)
}
}
viewPager.adapter = CardViewAdapter()
TabLayout与 ViewPager2绑定
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
tab.text = Card.DECK[position].toString()
}.attach()
注意:androidx中,TabLayout没有setupWithViewPager(ViewPager2 viewpager2)方法,而是用TabLayoutMediator将TabLayout和ViewPager2结合。
ViewPager2 with PageTransformer
这个示例用来展示给滑动增加自定义动画
代码:
private val mAnimator = ViewPager2.PageTransformer { page, position ->
val absPos = Math.abs(position)
page.apply {
rotation = if (rotateCheckBox.isChecked) position * 360 else 0f
translationY = if (translateY) absPos * 500f else 0f
translationX = if (translateX) absPos * 350f else 0f
if (scaleCheckBox.isChecked) {
val scale = if (absPos > 1) 0F else 1 - absPos
scaleX = scale
scaleY = scale
} else {
scaleX = 1f
scaleY = 1f
}
}
}
viewPager.setPageTransformer(mAnimator)
rotateCheckBox.setOnClickListener { viewPager.requestTransform() }
如果需要在页面直接增加空隙,可以使用MarginPageTransformer,这也是ViewPager2中的新功能。(注意:不能为负数)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BlKSdCth-1576063656447)(index_files/MarginPageTransformer.gif)]
val marginPageTransformer = MarginPageTransformer(50)
如果有多个page transformers,可以用CompositePageTransformer组合它们。
viewPager2.setPageTransformer(CompositePageTransformer().also {
it.addTransformer(marginPageTransformer)
it.addTransformer(translationPageTransformer())
})
ViewPager2 with MultiPages
这个示例用来展示一个页面显示两边item,两边能看到上一个和下一个item
要实现这个功能主要需要注意以下几点:
1.setOffscreenPageLimit(int limit)
设置为1,预加载前后item
2.android:clipToPadding
此属性表示: 用来定义ViewGroup是否允许在padding中绘制。默认情况下为true,为true的情况下, 那么绘制的区域会把padding部分剪裁。若为false,那么控件的绘制区域包含padding部分。所以这里需要设置为false。
3.给ViewPager2设置左右padding,给item设置左右margin
代码:
findViewById<ViewPager2>(R.id.view_pager).apply {
offscreenPageLimit = 1
val recyclerView = getChildAt(0) as RecyclerView
recyclerView.apply {
val padding = resources.getDimensionPixelOffset(R.dimen.halfPageMargin) +
resources.getDimensionPixelOffset(R.dimen.peekOffset)
setPadding(padding, 0, padding, 0)
clipToPadding = false
}
adapter = Adapter()
}
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="@dimen/halfPageMargin"
android:layout_marginRight="@dimen/halfPageMargin"
android:background="#0C2962"
android:padding="6dp"
android:scaleType="fitCenter"
android:src="@drawable/jetpack_logo" />
ViewPager2 with notifyDataSetChanged
用过ViewPager的notifyDataSetChanged的知道,在更新ViewPager中View的个数时会存在一些问题,要通过重写getItemPosition方法使其返回POSITION_NONE才会真正刷新到。
而ViewPager2的notifyDataSetChanged因实际用的是RecyclerView的notifyDataSetChanged,所以不会有这个问题,还支持局部刷新,而且可以利用DiffUtil类来更新(注意:如果用DiffUtil,需要覆盖getItemId()和containsItem()方法)。
示例:
代码:
fun changeDataSet(performChanges: () -> Unit) {
if (checkboxDiffUtil.isChecked) {
/** using [DiffUtil] */
val idsOld = items.createIdSnapshot()
performChanges()
val idsNew = items.createIdSnapshot()
DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun getOldListSize(): Int = idsOld.size
override fun getNewListSize(): Int = idsNew.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
idsOld[oldItemPosition] == idsNew[newItemPosition]
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
areItemsTheSame(oldItemPosition, newItemPosition)
}, true).dispatchUpdatesTo(viewPager.adapter!!)
} else {
/** without [DiffUtil] */
val oldPosition = viewPager.currentItem
val currentItemId = items.itemId(oldPosition)
performChanges()
viewPager.adapter!!.notifyDataSetChanged()
if (items.contains(currentItemId)) {
val newPosition =
(0 until items.size).indexOfFirst { items.itemId(it) == currentItemId }
viewPager.setCurrentItem(newPosition, false)
}
}
}
ViewPager2 with fake drag
这个示例是模拟拖曳动作,主要涉及beginFakeDrag()、fakeDragBy()、endFakeDrag()方法。
示例:
代码:
findViewById<View>(R.id.touchpad).setOnTouchListener { _, event ->
handleOnTouchEvent(event)
}
private fun handleOnTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
lastValue = getValue(event)
viewPager.beginFakeDrag()
}
MotionEvent.ACTION_MOVE -> {
val value = getValue(event)
val delta = value - lastValue
viewPager.fakeDragBy(if (viewPager.isHorizontal) mirrorInRtl(delta) else delta)
lastValue = value
}
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
viewPager.endFakeDrag()
}
}
return true
}
详细代码可以查看ViewPaer2 demo。
关于offscreenPageLimit(预加载)
ViewPager的情况
ViewPager默认情况下的加载示意图如下:
当切换到当前页面时,会默认预加载左右两侧的布局到ViewPager
中,尽管两侧的View并不可见的。由于ViewPager
对offscreenPageLimit
设置了限制,在ViewPager中offscreenPageLimit设置为0无效果其实还是默认值1(看下面源码),这导致页面的预加载是不可避免的,在Fragment
配合ViewPager
使用时也会存在这个问题。
ViewPager2的情况
前面改动点说到ViewPager2支持offscreenPageLimit,即在默认情况或设置默认值时只会加载当前页面。我们来看下ViewPager2的源码
可以看出只支持大于0或默认值(-1)。接着查看源码发现getOffscreenPageLimit()方法在ViewPager2.LinearLayoutManagerImpl
里的calculateExtraLayoutSpace
方法使用到了。
这个calculateExtraLayoutSpace
方法据了解是为了定义布局额外的空间,默认空间等于RecyclerView的宽高空间,定义这个意在可以放大可布局的空间,该方法参数extraLayoutSpace
是一个长度为2的int数组,第一条数据接受左边和上边的额外空间,第二条数据接受右边和下边的额外空间,故上述代码是表明等于默认值时不做处理,不然就是左右和上下各扩大offscreenSpace
;所以OffscreenPageLimit
其实就是放大了LinearLayoutManager
的布局空间。
demo验证
为了验证加载效果,我用了LinearLayout同时展示ViewPager和ViewPager2,设置相同的item和数据,然后用Layout Inspector抓取两者的布局结构,结果如下:
1.默认offscreenPageLimit
2.设置offscreenPageLimit为1