一、简介
姓名:ViewPager2
生日:2019年11月20日
地址:androidx.viewpager2.widget
背景:
在2019年的11月20号,Google对外公开了一件大事!让很多Android开发者激动万分~ 那就是公布了大家期待已久的 ViewPager2 正式版。他属于 androidx 组件包中的一员,ViewPager2 可以看做是 ViewPager的升级版,所以很多场景是可以按照ViewPager的使用方式来用的。ViewPager2的进阶处理是内部采用了RecyclerView实现,解决了很多在使用ViewPager过程中的问题同时还增加了一些自身的特性~ 接下来就让我们一起来认识一下他吧~
二、基础
1.差异对比
谷歌在源码中有这样介绍ViewPager2
ViewPager2 replaces ViewPager, addressing most of its predecessor’s pain-points, including right-to-left layout support, vertical orientation, modifiable Fragment collections, etc.
用如下表格清晰的展示对比:
ViewPager | ViewPager2 |
---|---|
PagerAdapter | RecyclerView.Adapter |
FragmentStatePagerAdapter | FragmentStateAdapter |
addPageChangeListener | registerOnPageChangeCallback |
不支持 | 支持RTL |
不支持 | 支持垂直滑动 |
不支持 | 支持停止用户操作 |
个人感觉最爽的改动点在于内部的RecyclerView实现以及支持垂直滚动
2.基本使用
以ViewPager2来实现ViewPager最基础的功能【图片banner】为例:
前提是需要保证项目代码支持androidx的呦~参考官网
2.1 添加依赖(需要单独添加依赖~),
dependencies {
implementation "androidx.viewpager2:viewpager2:version"
}
2.3 布局文件使用
在xml中使用,没有什么特殊的点,类似常见控件使用方式即可。设置宽高、ID、位置等等条件
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager2"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="2:3"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
2.4 设置Adapter
因为内部RecycleRiew实现的,所以ViewPager2的Adapter就需要设置为RecyclerView的Adapter
class VpAdapter : RecyclerView.Adapter<VpAdapter.VpViewHolder>() {
// adapter的数据源
private var data: MutableList<HouseItem> = mutableListOf()
fun setData(list: MutableList<HouseItem>) {
data.clear()
data.addAll(list)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VpViewHolder {
val view =
LayoutInflater.from(parent.context).inflate(R.layout.item_rv2_basic_view, parent, false)
return VpViewHolder(view)
}
override fun onBindViewHolder(holder: VpViewHolder, position: Int) {
holder.render(data[position])
}
override fun getItemCount() = data.size
class VpViewHolder(_itemView: View) : RecyclerView.ViewHolder(_itemView) {
private var pvImage: SimpleDraweeView = itemView.findViewById(R.id.pv_image)
private var tvTitle: TextView = itemView.findViewById(R.id.tv_title)
fun render(bean: HouseItem) {
tvTitle.text = bean.houseTypeName
pvImage.setImageURI(bean.houseTypePic)
}
}
}
布局文件也很简单,单纯的放置了一个SimpleDraweeView+一个TextView用来展示一张图片+一个标题; 可以看到,这跟我们平常使用RecyclerView时写的Adapter没有任何区别。所以在使用的时候是不会有任何阻碍的~
(SimpleDraweeView的使用在此不赘述,感兴趣的小伙伴可参考官网学习SimpleDraweeView)
2.5 配置属性
类似于RecyclerView~ 初始化ViewPager2、初始化Adapter、设置数据源、给ViewPager2设置Adapter,就能实现类似ViewPager的Banner效果了~
adapter = VpAdapter()
adapter.setData(list)
viewPager2.adapter = adapter
效果如图: Video
前边讲述区别的时候,有提到过ViewPager2默认支持垂直滑动,那么我要实现垂直滚动的Banner,该怎么处理呢?
其实很简单,只需要设置ViewPager2的滚动方向属性就行了~
// 设置垂直滚动方向
viewPager2.orientation = ViewPager2.ORIENTATION_VERTICAL
至此,关于ViewPager2基本的使用都介绍完了~
三、进阶
1.页面切换监听
ViewPager2的页面切换监听,提供了 registerOnPageChangeCallback
的方法,传递一个 抽象对象 OnPageChangeCallback
,好处是三个方法不需要一次性列出来了,用到哪个写哪个。
public abstract static class OnPageChangeCallback {
public void onPageScrolled(int position, float positionOffset,
@Px int positionOffsetPixels) {
}
public void onPageSelected(int position) {
}
public void onPageScrollStateChanged(@ScrollState int state) {
}
}
通过以上源码解释,常用的应该就是 onPageSelected(int position)
方法了,监听到滑动到对应索引值的页面。
// 页面切换监听
viewPager2.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
// 选中 回调,核心用于 处理选中索引值相关逻辑
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
Log.e(TAG, "onPageSelected: position = $position")
}
})
2.一屏多页
ViewPager2如果要实现一屏多页的效果,需要借助于 offscreenPageLimit
以及 setPadding
viewPager2.apply {
offscreenPageLimit = 1
val recyclerView = this.getChildAt(0) as RecyclerView // 获取ViewPager2中的 RecyclerView
recyclerView.apply {
val padding = 40.dp
setPadding(padding, 0, padding, 0)
clipToPadding = false
}
}
val recyclerView = this.getChildAt(0) as RecyclerView
针对这个地方,参考源码254行,可以看到,ViewPager2内部是默认将 mRecyclerView添加到ViewGroup中的,索引值固定是0!
attachViewToParent(mRecyclerView, 0, mRecyclerView.getLayoutParams());
3.offscreenPageLimit 预加载
在ViewPager中,会默认加载前后各一个页面,所以处理懒加载就比较麻烦,那么在ViewPager2中,默认去掉了这限制,也就是默认只会加载一个页面, 先来看下 参数
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
设置需要处理一个参数,可以看到有个注解OffscreenPageLimit
表明默认值是需要从1开始的。
接下来我用ViewPager2加载Fragment来证明,默认以Fragment的生命周期日志为参照
3.1 offscreenPageLimit 不设置
E/debug_VpFragment: onCreate: mCurrentPosition = 0
E/debug_VpFragment: onPause: onCreateView = 0
E/debug_VpFragment: onResume: mCurrentPosition = 0
可以看到,默认对此方法不做处理的话,只对第一个Fragment进行了初始化的相关操作。
3.2 offscreenPageLimit = 1
E/debug_VpFragment: onCreate: mCurrentPosition = 0
E/debug_VpFragment: onPause: onCreateView = 0
E/debug_VpFragment: onResume: mCurrentPosition = 0
E/debug_VpFragment: onCreate: mCurrentPosition = 1
E/debug_VpFragment: onPause: onCreateView = 1
设置offscreenPageLimit = 1的时候,对前两个Fragment进行了初始化相关操作。
3.3 offscreenPageLimit = 2
E/debug_VpFragment: onCreate: mCurrentPosition = 0
E/debug_VpFragment: onPause: onCreateView = 0
E/debug_VpFragment: onResume: mCurrentPosition = 0
E/debug_VpFragment: onCreate: mCurrentPosition = 1
E/debug_VpFragment: onPause: onCreateView = 1
E/debug_VpFragment: onCreate: mCurrentPosition = 2
E/debug_VpFragment: onPause: onCreateView = 2
设置offscreenPageLimit = 2的时候,对前三个Fragment进行了初始化相关操作。
4.结合TabLayout
TabLayout结合ViewPager也是比较常用的,但是ViewPager2结合使用的时候,需要多一个TabLayoutMediator
TabLayoutMediator接受三个参数:
参数1:TabLayout
参数2:ViewPager2
参数3:TabConfigurationStrategy (interface)
重点是第三个参数,需要重写onConfigureTab(@NonNull TabLayout.Tab tab, int position)
,所以如果我们想要实现一些自定义的Tab样式,可以直接在此方法中处理,同时可以拿到索引值进行特殊处理。
new TabLayoutMediator(mTabLayout, mViewPager2, new TabLayoutMediator.TabConfigurationStrategy() {
@Override
public void onConfigureTab(@NonNull TabLayout.Tab tab, int position) {
TextView textView = new TextView(mFragmentActivity);
textView.setText(mVpBeans.get(position).getTitle());
tab.setCustomView(textView);
}
}).attach();
项目实战
自如寓,隶属于自如大家庭的重要一员,是一种更加年轻的、社交化的集中式公寓,具有多种社交活动等功能,所以自如寓的管家们会将日常组织的活动中的一些精彩视频、图片记录下来,并上传到“好玩”tab的数据里边,并以瀑布流的形式展示。 基于用户体验以及交互形式,我们考虑针对这些视频、图片做一种上下滑动播放视频、横向滑动展示图片的一种效果,考虑到了上述ViewPager2的特性,尝试使用实现
大致思路
页面容器中,采用ViewPager2来承载每一个切换的Page;Page以Fragment实现;Fragment中区分视频类型/图片类型,分别以视频播放器/图片banner来承载
页面结构
考虑到了ViewPager2的垂直滚动功能,毫不犹豫的进行尝试一下子;页面结构实现简单化,在对应的页面中,整体结构就是ViewPager2+TopView(一些自定义的样式展示,此处忽略),然后VpFragmeng作为每一个Page的承载,内含播放器控件或者轮播控件(展示横滑图片) 大致如图:
Activity中的内容比较简单,主要就是对ViewPager2、Fragment绑定的处理。
private fun initView() {
mAdapter = PageAdapter(this, mModelList)
mAdapter?.setOnAdapterDataChangeListener(object : PageAdapter.OnAdapterDataChangeListener {
override fun onPageChangeListener(selectedPosition: Int) {
mTvCurrentPage?.text = String.format("%d", selectedPosition + 1)
}
override fun onSupportClick(bean: PlayEvaluationListModel.RowsBean?, position: Int, status: Boolean) {
}
override fun onImageClick() {
finish()
}
})
// 设置垂直滚动效果
mViewPager2?.orientation = ViewPager2.ORIENTATION_VERTICAL
mViewPager2?.adapter = mAdapter
mViewPager2?.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
// 处理顶部的指示器数据
//这只当前item的数据
val model = mModelList[position]
// 准备加载更多的数据
if (position == mModelList.size - 2) { // -2 ,是为了在倒数第二个的时候就让他进行分页,提前一个加载
if (!mCanLoadMore) {
return
}
mPresenter?.fillData()
}
}
})
initTitlePosition()
}
Adapter的代码就不贴了,跟demo中类似,绑定Fragment就行了~
PageFragment,作为每一个Page的承载,使用方式跟普通Fragment没什么本质区别;
常规数据初始化
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mFragmentActivity = activity
if (arguments != null) {
mRowsBean = arguments?.getSerializable("rowsBean") as PlayEvaluationListModel.RowsBean
mCurrentPosition = arguments?.getInt("position") ?: 0
}
}
绑定View
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.item_play_evaluation_list, container, false)
initView(view)
return view
}
// 这个是具体的展示逻辑,根据是视频类型还是图片类型,分别展示 视频播放器 或者 图片banner控件;
private fun render() {
val model = mRowsBean ?: return
if (model.isVideo) {
mZvvVideo?.visible = true
mCpPicList?.visible = false
val pic = if (null != model.picList && model.picList.size >= 1) model.picList[0].picUrl else ""
mZvvVideo?.setPlaceImg(pic)
mZvvVideo?.setVideoPath(model.videoUrl)
mZvvVideo?.setOnClickListener { _: View? ->
if (mZvvVideo?.videoView != null && mZvvVideo?.videoView?.isPlaying == true) {
mZvvVideo?.pause()
} else {
mZvvVideo?.startVideo()
}
}
var imgWidth = 720.0
var imgHeight = 1280.0
// 视频和图片的宽高
if (model.picList != null && model.picList.isNotEmpty()) {
val picListBean = model.picList[0]
if (picListBean != null) {
imgWidth = picListBean.width
imgHeight = picListBean.height
}
}
// 手动赋值宽高
val lp: ViewGroup.LayoutParams = mZvvVideo?.layoutParams
?: ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
lp.width = mScreenWidth
var newH = imgHeight
// 竖屏视频
if (imgWidth > 0) {
newH = mScreenWidth * imgHeight / imgWidth
}
lp.height = newH.toInt()
mZvvVideo?.videoView?.setOnVideoSizeChangedListener { _: Int, _: Int ->
mZvvVideo?.videoView?.displayAspectRatio = PLVideoView.ASPECT_RATIO_FIT_PARENT
}
} else {
mZvvVideo?.visible = false
mCpPicList?.visible = true
mCpPicList?.isCanLoop = false
mCpPicList?.setPages({
val detailPlayPicHolder = DetailPlayPicHolder()
detailPlayPicHolder.setOnAdapterDataChangeListener(object : OnAdapterDataChangeListener {
override fun onPageChangeListener(selectedPosition: Int) {}
override fun onSupportClick(bean: PlayEvaluationListModel.RowsBean?, position: Int, status: Boolean) {}
override fun onImageClick() {
mOnAdapterDataChangeListener?.onImageClick()
}
})
detailPlayPicHolder
}, model.picList)
mCpPicList?.onPageChangeListener = object : ViewPager.OnPageChangeListener {
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}
override fun onPageSelected(page: Int) {
mOnAdapterDataChangeListener?.onPageChangeListener(page)
}
override fun onPageScrollStateChanged(state: Int) {}
}
}
// ....省略....
}
考虑到视频播放状态要与用户操作页面状态结合,所以需要在页面切换出去的时候,将对应的视频播放器进行暂停或者停止操作(具体采取何种操作视情况而定),在此处我们采用的是暂停方案,当用户手动切换回一个刚切换出去的Page,可以实现继续播放上次未播放完毕的视频。
override fun onResume() {
super.onResume()
mZvvVideo?.start()
}
override fun onPause() {
super.onPause()
mZvvVideo?.pause()
}
至此,实现了支持上下滑动切换视频/图片、左右切换图集的功能。 以上,仅为本人实战体验,如有更优姿势,欢迎不吝赐教~
本文作者:自如大前端研发中心-邱宴峰