ViewPager2Demo

Introduction: 关于封装 ViewPager2 的 FragmentStateAdapter 的封装,以最简单的方式实现抖音列表的上下加载,画廊效果,以及 ViewPager2 的各种封装
More: Author   ReportBugs   
Tags:

特点功能

完全脱离 xml,所有效果只需要通过 api 调用

具体功能:
    1. 两句代码实现抖音列表效果
    2. 无感且丝滑,动态从头部或者底部加载数据
    3. 设置上下加载监听,再达到预加载 limit 的时候触发监听
    4. 实现数据源接口,和 Fragment 接口,你会体验到什么是丝滑
    5. 画廊的实现,不再需要在 xml 设置 clipChildren 属性,调用即可实现。
    6. 极限脱离 xml 控制,以简化使用者使用
    7. 支持无线循环模式
    8. 支持自动滚动模式;设置滚动时长,设置循环间隔时长
    9. 循环滚动可绑定页面 lifeCycle 生命周期

SmartViewPager2Adapter 动态

Demo

为录制流畅,截图分辨率比较模糊。可在下方扫描二维码下载 apk


效果展示

为录制流畅,截图分辨率模糊。可下载 apk 查看真机效果

  • 基础功能展示 跳转基础使用文档 ------ 跳转数据加载及监听文档

    |几句代码实现抖音列表|向上或向下加载数据|设置加载监听| |:---:|:---:|:---:| |Sample|Sample|Sample

  • 画廊效果 跳转此文档

    |asGallery 一句代码搞定|3d 画廊| |:---:|:---:| |Sample|Sample

  • 无限循环和自动滚动 跳转此文档

    |无限循环|自动滚动| |:---:|:---:| |Sample|Sample

  • 指示器功能及边缘滑动监听 指示器文档

    |指示器的使用|指示器自动更新|边缘滑动监听| |:---:|:---:|:---:| |Sample|Sample|Sample

  • 3.1.0 及以后无数据源的使用 跳转此文档

    |极速实现 app 的 tab 切换|重写点击事件,判断是否可切换页面| |:---:|:---:| |Sample|Sample

添加依赖

  • 项目 build.gradle 添加如下
    allprojects {
         repositories {
             maven { url 'https://jitpack.io' }
         }
     }
    
  • app build.gradle 添加如下
    dependencies {
             implementation 'com.github.lihangleo2:SmartViewPager2Adapter:3.1.3'
     }
    

一、基本使用

使用此库,你只需在 xml 加上简单的 viewPager2 即可,其他只需调用方法即可

1.1、两句代码实现抖音效果:步骤一:初始化 adapter


    private val mAdapter by lazy {
        //SourceBean 是你使用的数据泛型
        SmartViewPager2Adapter.Builder<SourceBean>(this)
            .addFragment(1, ImageFragment::class.java)
            .addFragment(2, TextFragment::class.java)
            .build(mBinding.viewPager2)
            //可以在这里初始化数据
            .addData(list)
    }


1.2、步骤二:设置给 viewpager2(做完这下就搞定了,你没看错)

mBinding.viewPager2.adapter = mAdapter


1.3、步骤三:数据源对象 bean 要实现接口:SmartFragmentTypeExEntity

public class SourceBean extends SmartFragmentTypeExEntity {
    int type;

    @Override
    public int getFragmentType() {
        return type;
    }
}

数据源 bean 实现此接口后,例如:通过方法.addFragment(1, ImageFragment::class.java)(也就是说 type==1 时生成 ImageFragment,这些逻辑 adapter 帮你操作了)


1.4、步骤四:目标 fragment 要实现接口:SmartFragmentImpl

public class ImageFragment extends Fragment implements SmartFragmentImpl<SourceBean> {
    //....伪代码
    @Override
    public void initSmartFragmentData(SourceBean bean) {
        this.mSourceBean = bean
    }
}

要使用 SmartViewPager2Adapter,你的目标 fragment 必须要实现 SmartFragmentImpl接口,adapter 会将数据回传给 fragment


二、数据加载及监听

2.1、数据类 api

  • 向下无感加载数据

    mAdapter.addData(list)
    
  • 向上无感加载数据

    mAdapter.addFrontData(list)
    
  • 移除数据

    mAdapter.removeData(index: Int)
    
  • 根据 position 获取对象

    //getLastItem() 获取 last item
    //getItemOrNull() 不存在 position 会返回 null
    var item = mAdapter.getItem(index: Int)
    
  • 获取数据源 list

    var list = mAdapter.getData()
    

2.2、监听类 api

使用加载监听可以搭配此设置。不设置则默认是 3

  //设置滑动到 preLoadLimit 触发预加载监听
  .setPreLoadLimit(3)
  • 设置头部加载监听(不设置则不触发)

              mAdapter.setOnRefreshListener(object : OnRefreshListener {
                  override fun onRefresh(smartAdapter: SmartViewPager2Adapter<*>) {
                      //滑动到 preLoadLimit 后触发头部加载监听
    
                  }
              })
    
  • 设置底部加载监听(不设置则不触发)

              mAdapter.setOnLoadMoreListener(object :OnLoadMoreListener{
                  override fun onLoadMore(smartAdapter: SmartViewPager2Adapter<*>) {
                      //滑动到 preLoadLimit 后触发底部加载监听
    
                  }
              })
    
  • 同时设置头部和底部监听(不设置则不触发)

              mAdapter.setOnRefreshLoadMoreListener(object : OnRefreshLoadMoreListener {
                  override fun onRefresh(smartAdapter: SmartViewPager2Adapter<*>) {
    
                  }
    
                  override fun onLoadMore(smartAdapter: SmartViewPager2Adapter<*>) {
    
                  }
    
              })
    
  • 设置 ViewPager2 边缘滑动监听(不设置则不触发)

          mAdapter.setOnSideListener(object :onSideListener{
              override fun onLeftSide() {
                  ToastUtils.showShort("触发左边缘事件")
              }
    
              override fun onRightSide() {
                  ToastUtils.showShort("触发右边缘事件")
              }
    
          })
    


2.3 数据加载其他方法

  • 头部已经不能翻页时,调用。将不再触发头部监听。

    mAdapter.finishRefreshWithNoMoreData()
    
  • 底部已经不能翻页时,调用。将不再触发底部监听。

    mAdapter.finishLoadMoreWithNoMoreData()
    
  • 结束头部刷新状态,继续触发监听

    //1.注意调用 mAdapter.addData(list)和 mAdapter.addFrontData(list)也会触发此效果,无须主动调用
    //2.此方法针对调用加载接口时,接口异常等无数据情况下需要主动调用
    mAdapter.finishRefresh()
    
  • 结束底部刷新状态,继续触发监听

    //同上
    mAdapter.finishLoadMore()
    


三、画廊效果

画廊只需要加上如下代码,无需在 xml 里写 clipChildren="false"这些代码,解放 xml

    private val mAdapter by lazy {
        SmartViewPager2Adapter.Builder<SourceBean>(this)
            //实现画廊功能,参数为左右间距
            .asGallery(ConvertUtils.dp2px(50f),ConvertUtils.dp2px(50f))
            //设置滑动效果
            .setPagerTransformer(SmartTransformer.TRANSFORMER_ALPHA_SCALE)
            .addFragment(1,ImageFragment::class.java)
            .addFragment(2,TextFragment::class.java)
            .build(mBinding.viewPager2)
            .addData(list)
    }


四、无线循环和自动滚动

    private val mAdapter by lazy {
        SmartViewPager2Adapter.Builder<SourceBean>(this)
            //实现无线循环模式
            .setInfinite(true)
            //实现自动滚动
            .setAutoLoop(true)
            //自动滚动下绑定页面生命周期:即离开页面暂停滚动,回到页面恢复滚动
            .addLifecycleObserver()
            //滚动间隔时间
            .setLoopTime(3000L)
            //切换轮播滚动速度
            .setScrollTime(600L)
            .addFragment(1, ImageFragment::class.java)
            .addFragment(2, TextFragment::class.java)
            .build(mBinding.viewPager2)
            .addData(list)
    }


五、指示器的使用

指示器的使用也是非常的简单,如下:(demo 里的 IndicatorActivity 有具体用法)。

5.1、api 使用指示器

特别注意,使用 withIndicator(SmartIndicator.CIRCLE) api,必须是 viewPage2 的父控件是 ConstraintLayout 布局,否则建议使用 xml 里的方式,不受父控件约束

    private val mAdapter by lazy {
        SmartViewPager2Adapter.Builder<SourceBean>(this)
            //圆形指示器:SmartIndicator.CIRCLE   线性指示器:SmartIndicator.LINE 
            .withIndicator(SmartIndicator.CIRCLE)

            //以上会默认在居中靠下的位置,设置位置你可以使用以下 api
            //.withIndicator(SmartIndicator.CIRCLE,SmartGravity.LEFT_BOTTOM, ConvertUtils.dp2px(20f),ConvertUtils.dp2px(20f)) 
            .addFragment(1, ImageFragment::class.java)
            .addFragment(2, TextFragment::class.java)
            .build(mBinding.viewPager2)
            .addData(list)
    }


5.2 布局 xml 里使用指示器:

你还可以将指示器放在 xml 里,这样可以更强自定义指示器样式,及把指示器放置你布局里想放置的任何位置,不受父控件影响 xml 里如下:

    <com.smart.adapter.indicator.CircleIndicator
        android:id="@+id/circle_indicator"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginRight="10dp"
        android:layout_marginBottom="10dp"
        app:layout_constraintBottom_toBottomOf="@+id/viewPager2"
        app:layout_constraintRight_toRightOf="parent"
        app:lh_indicator_mode="fill"
        app:lh_indicator_radius="10dp"
        app:lh_indicator_scrollWithViewPager2="true"
        app:lh_indicator_selectColor="#FF0101"
        app:lh_indicator_space="10dp"
        app:lh_indicator_strokeWidth="1.5dp"
        app:lh_indicator_unselectColor="#FFEB3B" />

代码如下:

    private val mAdapter by lazy {
        SmartViewPager2Adapter.Builder<SourceBean>(this)
            //关键代码,xml 里的指示器
            .withIndicator(mBinding.circleIndicator)
            .addFragment(1, ImageFragment::class.java)
            .addFragment(2, TextFragment::class.java)
            .build(mBinding.viewPager2)
            .addData(list)
    }


六、刷新问题和视频的用法问题

本库开源以后,有很多说刷新问题和视频的问题,这个这里详细讲解一下

6.1 刷新问题:

我们首先是通过.addData()去设置数据源的,然后到了我们的 fragment 里。以下代码,在 fragment 里你可以拿到 mSourceBean,注意这里的内存地址和数据源 list 里的内存地址指向一个地方的。记住这一点就好办了。

public class ImageFragment extends Fragment implements SmartFragmentImpl2<SourceBean> {
    private SourceBean mSourceBean;
    //....伪代码
    @Override
    public void initSmartFragmentData(SourceBean bean) {
        this.mSourceBean = bean
    }

    //....伪代码
    private void initView(){
        //一般封装的 baseFragement 都通过一个方法去初始化页面
        //当然你也可以叫 initData()这里随便。拿到 mSourceBean 去初始化页面
    }
}


需求来了:假设你有一个点赞按钮,点击了,从未点赞状态变成了红色点赞状态如下:

btn.setOnClickListener{
    //第一步:(因为你在 fragment 里),你要去改变按钮 ui 的样式
    //第二步:【重点】你要去修改数据源里的点赞字段假设:mSourceBean.isClick = 1
    //执行了第二步以后,你的数据源的 isClick 字段改变了。因为内存地址一样,数据源 list 里的数据也改变了。
    //所以,当你随便滑动你的 viewPager2,即使超过 offscreenPageLimit,
    //页面被销毁了,然后重建,因为数据源在这,样式也会恢复之前的状态。
    //其实就和 RecycleView 里的 Adapter 里的用法是一样的
}

以上是对此库页面刷新的用法。其实很简单

6.2、视频在 fragment 里的用法:

视频用法也是极其简单,我这里放我 baseFragment 里的封装,我相信大部分开发者都会有一个 base,如下:

public abstract BaseFragment{
    //....伪代码
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        //初始化子布局
        initView()
    }

    override fun onResume() {
        super.onResume()
        if (!isLoaded && !isHidden) {
            lazyInit()
            isLoaded = true
        }

        if (!isHidden) {
            onVisible()
        }
    }

    override fun onPause() {
        super.onPause()
        onInVisible()
    }
}

这里有 3 个方法

  • initView() 在这里你可以初始化你的视频数据,甚至 preLoad(因为缓存问题会加载很多个 fragment,只要你不播放视频即可)
  • onVisible() 页面可见时,播放视频
  • onInVisible() 页面不可见时,暂停播放。假设你滑回这个页面你想从第一帧开始播放,你可以将视频 seekTo 到第 0 秒的时间。可以根据你的需求来。

希望我表达清楚了。看懂了也是非常好用!感谢,让我们共同维护这个库,让你在使用 ViewPager2 的时候,是如此简单


方法详解

    private val mAdapter by lazy {
        SmartViewPager2Adapter.Builder<SourceBean>(this) //SourceBean 泛型 bean
            //构造参数(3.0 后部分 api 不可动态改变,规范及正确使用)
            .overScrollNever() //取消 viewPager2 滑动边缘阴影
            .canScroll(false) //viewPager2 不可手势滑动
            .setOffscreenPageLimit(5) //设置 viewPager2 缓存
            .setPreLoadLimit(3) //滑动到 preLoadLimit 触发预加载监听
            .addFragment(1, ImageFragment::class.java) //添加要生成的 fragment
            .addFragment(2, TextFragment::class.java)
            .addDefaultFragment(TextFragment::class.java) //找不到对应 type 的数据时生成的默认 fragment
            .asGallery(50,50) //实现画廊
            .setPagerTransformer(SmartTransformer.TRANSFORMER_ALPHA_SCALE) //设置滑动效果
            .withIndicator(SmartIndicator.CIRCLE) // 添加指示器
            .setVertical(true) //viewPager2 是否竖直方向
            .setInfinite(true) //实现无线循环模式
            .setAutoLoop(true) //实现自动滚动
            .addLifecycleObserver() //自动滚动下绑定页面生命周期:即离开页面暂停滚动,回到页面恢复滚动
            .setLoopTime(3000L) //滚动间隔时间
            .setScrollTime(600L) //切换轮播滚动速度
            .build(mBinding.viewPager2) //绑定 viewPager2

            //方法
            .addData(list) //添加数据
            .addFrontData(list) //添加头部数据
            .removeData(index) //移除数据
            .addLifecycleObserver() //自动滚动下绑定页面生命周期:即离开页面暂停滚动,回到页面恢复滚动
            .removeLifecycleObserver() //移除页面监听
            //结束头部刷新状态,继续触发监听(addData,addFrontData 默认调用此方法),此方法针对数据加载失败时需要主动调用
            .finishRefresh() 
            .finishLoadMore() //同上
            .finishRefreshWithNoMoreData() //头部已经不能翻页时,调用。将不再触发头部监听。
            .finishLoadMoreWithNoMoreData() //底部已经不能翻页时,调用。将不再触发底部监听。
            .canScroll(false) //viewPager2 不可手势滑动
            .overScrollNever() //取消 viewPager2 边缘阴影
            .setPagerTransformer(SmartTransformer.TRANSFORMER_ALPHA_SCALE)//设置滑动效果
            .setLoopTime(3000L) //设置滚动间隔时间
            .setScrollTime(600L) //设置轮播切换速度


            //监听
            .setOnRefreshLoadMoreListener(object :OnRefreshLoadMoreListener{ //加载监听
                override fun onRefresh(smartAdapter: SmartViewPager2Adapter<*>) {
                    //滑动到 preLoadLimit 后触发头部加载监听
                }

                override fun onLoadMore(smartAdapter: SmartViewPager2Adapter<*>) {
                    //滑动到 preLoadLimit 后触发底部加载监听
                }

            })
            .setOnSideListener(object : onSideListener { //左右边界滑动监听
                override fun onLeftSide() {
                    //触发左边缘事件

                }
                override fun onRightSide() {
                    //触发右边缘事件
                }

            })
    }


七、无数据源的使用

作者在使用时发现,长长会出现,无数据源的情况,比如我们大部分 app 的首页,我的这些页面切换功能,故增添了此 api

7.1、几行代码快速实现切换

注意这里是没数据源的,此刻的 fragment 是不需实现上面的接口
步骤一:

    private val mAdapter by lazy {
        SmartViewPager2Adapter.NoDataBuilder(this)
            //如果你不需要按钮联动,也可不用这个
            .bindViews(mBinding.tabHome, mBinding.tabFile, mBinding.tabManager, mBinding.tabMine)
            .build(mBinding.viewPager2)
            //也支持 List<Fragment>
            .addData(ImageFragment(),TextFragment(),NoDataFragment(),TestApiFragment())
    }


步骤二:设置给 viewpager2(做完这下就搞定了,你没看错)

mBinding.viewPager2.adapter = mAdapter


7.2、点击事件处理

如果你点击按钮 1 需要实现额外逻辑,只需重写以下监听.setOnClickListener(callback)

    private val mAdapter by lazy {
        SmartViewPager2Adapter.NoDataBuilder(this)
            //如果你不需要按钮连动,也可不用这个
            .bindViews(mBinding.tabHome, mBinding.tabFile, mBinding.tabManager, mBinding.tabMine)
            .build(mBinding.viewPager2)
            //也支持 List<Fragment>,里面是你 new 出来的 fragment 实例
            .setFragmentList(HomeFragment(),FileFragment(),ManagerFragment(),MineFragment())
            .setOnClickListener(object :SmartNoDataAdapter.OnClickListener{
                override fun onClick(v: View): Boolean {
                    when(v.id){
                        R.id.tab_mine->{
                            //假如有些 app 可先不登录,但是点击"我的"tab 按钮,判断没有登录去登录并返回 false,
                            //那么 adapter 就不会去切换 tab
                            if (!isLogin){
                                //跳转登录页
                                //伪代码...
                                //返回 false 不会选中 tab
                                return false
                            }
                            //登录后的操作...
                        }
                    }
                    return true
                }
            })
    }


7.3、获取 Fragment 实例

    private fun getRealFragment() {
        //泛型直接获取,【注意】:前提是 setFragmentList() 时,fragment 类型必须唯一,
        //如,不能有 2 个 HomeFragment,否则会报错
        var mHomeFragment = mAdapter.getFragment<HomeFragment>()

        //也可以不使用泛型获取
        var mHomeFragment = mAdapter.getFragment(0) as HomeFragment
    }

无数据源 Adapter 方法详解

无数据源 Adapter 大部分 api 和有数据源的 Adapter 一样,除了舍去了跟数据有关的 api,新增的方法如下:

    private val mAdapterOther by lazy {
        SmartViewPager2Adapter.NoDataBuilder(this)
            .overScrollNever() //去掉阴影
            .canScroll(false) //viewPager2 不可手势滑动
            .smoothScroll(false) //点击 tab 后,viewPager2 直接切换,不带滚动动画
            //与按钮联动,如果不需要可不写
            .bindViews(mBinding.tabHome, mBinding.tabFile, mBinding.tabManager, mBinding.tabMine) 
            .build(mBinding.viewPager2)
            //设置 fragment 实例
            .setFragmentList(HomeFragment(),FileFragment(),ManagerFragment(),MineFragment())
            //重写联动按钮点击事件,返回 false,不切换 tab
            .setOnClickListener(object : SmartNoDataAdapter.OnClickListener { 
                override fun onClick(v: View): Boolean {
                    return true
                }
            })
    }


赞赏

如果你喜欢 SmartViewPager2Adapter 的功能,感觉 SmartViewPager2Adapter 帮助到了你,可以点右上角 "Star" 支持一下 谢谢! ^_^ 你也还可以扫描下面的二维码~ 请作者喝一杯咖啡。或者遇到工作中比较难实现的需求请作者帮忙。

如果在捐赠留言中备注名称,将会被记录到列表中~ 如果你也是 github 开源作者,捐赠时可以留下 github 项目地址或者个人主页地址,链接将会被添加到列表中

捐赠列表


其他作品

万能阴影布局


关于作者。

Android 工作多年了,如果你在学习的路上也感觉孤独,请和我一起。让我们在学习道路上少些孤独

  • QQ 群: 209010674 android 交流群(点击图标,可以直接加入)


Licenses

MIT License

Copyright (c) 2023 leo

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Apps
About Me
GitHub: Trinea
Facebook: Dev Tools