本Demo是使用Kotlin编写,什么都不多说,先来看个效果图Gif :

android fragment嵌套linearlayout fragment嵌套viewpager2_kotlin

类似这样结构的App也有很多。可以看到demo中,底部菜单包含4个按钮分别对应着4个不同fragment 。其中前2个底部菜单按钮fragment 也分别包含他们自己的Tab选项卡,当Tab选项卡滑动到最后一个时,自动切换为下一个底部菜单fragment ,与此同时内部的 Banner  和 RecyclerView 滑动事件不与ViewPager切换冲突的这种效果。

再来看看前2个底部菜单效果页面的布局结构:

android fragment嵌套linearlayout fragment嵌套viewpager2_底部菜单_02

想当时以为这种效果做起来还算简单,需要实现这种滑动切换效果第一时间肯定想到要用ViewPager,但Google最近推出的ViewPager2说是ViewPager的升级版且功能更强大,所以就决定采用ViewPager2来实现效果。底部菜单BottomNavigationView绑定父ViewPager实现底部菜单的滑动切换,子ViewPager绑定TabLayout选项卡实现滑动切换。结果当做起来才发现很多问题,比如ViewPager2嵌套问题导致子ViewPager2不能正常响应滑动,ViewPager2里面的Banner滑动事件又产生冲突等一系列问题。

想到ViewPager2嵌套产生滑动事件冲突,首先我第一想法也是继承ViewPager2重写它的一系列事件分发的方法处理,而ViewPager2是内部是对RecyclerView的封装实现,且是final类型的不能被继承,因此继承ViewPager2这种解决方式就可以pass掉了。

 

第一个坎:如何解决父ViewPager2嵌套子ViewPager2后导致子ViewPager2滑动事件失效?

ViewPager2 提供了一个方法 setUserInputEnabled(boolean enabled) ,此方法的作用是当前ViewPager是否要响应用户输入事件,设置为 setUserInputEnabled(false)表示不处理 ,然后就交给子ViewPager处理了。

由于知道了我们前2个底部菜单Fragment都是包含多个Tab选项卡的,所以我的实现思路是父ViewPager如果当前页面为前两个底部菜单Fragment,滑动事件交给子ViewPager处理Tab滑动

//父ViewPager页面切换监听
mainViewpager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
    override fun onPageSelected(position: Int) {
        //滑动fragment对应底部按菜单钮选中
        bottomNavView.menu.getItem(position).isChecked = true
        //除了前2个bottom fragment有viewpager嵌套不处理滑动,其他都处理滑动
        mainViewpager.isUserInputEnabled =(mainViewpager.currentItem > 1)
    }
})

现在,子ViewPager的Tab选项卡已经可以正常滑动切换了。

But,  现在只是Tab选项卡可以切换和父ViewPager后面两个fragment页面可以滑动切换,前两个父ViewPager fragment 还是不能相互切换的,因此还需要继续完善 :

1.当第一个父ViewPager的Fragment顶部Tab选项卡为最后一个时且继续往右滑,则切换到下一个父ViewPager的Fragment。

2.当第二个父ViewPager的Fragment顶部Tab选项卡为第一个时继续往左滑,则切换到上一个父ViewPager的Fragment;当Tab为最后一个时且继续往右滑,则切换到下一个父ViewPager的Fragment。

问题来了,怎样判断Tab到达最后一个时且继续往右滑,Tab为第一个时继续往左滑?

 

第二个坎:如何判断第一个或最后一个Tab的滑动方向?

这里就需要用到OnPageChangeCallback 回调的2个方法对子ViewPager进行判断了:

滚动状态改变回调:onPageScrollStateChanged(state: Int)  

页面位置改变回调:onPageScrolled( position: Int, positionOffset: Float, positionOffsetPixels: Int )

viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
    private var currentPosition = 0     //当前滑动位置
    private var oldPositon = 0          //上一个滑动位置
    private var currentState = 0        //记录当前手指按下状态
    private var scrolledPixeledList = mutableListOf<Int>() //记录手指滑动时的像素坐标记录
    
    //页面滚动的位置信息回调: position 当前滚动到哪个页面,positionOffset 位置偏移百分比, positionOffsetPixels 当前所在页面偏移量
    //此回调会触发完onPageScrollStateChanged 的 state 值为1时后面才触发回调
    override fun onPageScrolled(position: Int,positionOffset: Float,positionOffsetPixels: Int) {
        currentPosition = position
        if (currentState == 1) {
           //手指按下滑动坐标记录
           scrolledPixeledList.add(positionOffsetPixels)
        }
    }


    // 滚动状态改变回调,state的值分别有0,1,2 ;
    // 0为ViewPager所有事件(1,2)已结束触发
    // 1为在viewPager里按下并滑动触发多次
    // 2是手指抬起触发
    override fun onPageScrollStateChanged(state: Int) {
         currentState = state
         if (state == 0) {
            if (currentPosition == oldPositon) {
                when (currentPosition) {
                     0 -> {
                          if (scrolledPixeledList.size > 1 && scrolledPixeledList.last() == 0 || scrolledPixeledList.last() - scrolledPixeledList[0] > 0) {
                                    //有可能出现滑到一半放弃的情况也是可以出现currentPosition == oldPositon=0,则先判断是否是往右滑时放弃
                                    return
                          }   
                         Log.d("TAG1", "到达最左一个继续往左滑....")
                          //若还有上一个bottom fragment页面则切换
                         mainViewPager.currentItem.takeIf { it > 0 }
                         ?.also { (activity as MainActivity).switchFragment(it - 1) }
                     }

                     (viewPager.adapter as FragmentStateAdapter).itemCount - 1 -> {
                         Log.d("TAG1", "到达最右一个继续往右滑....")
                         //若还有下一个bottom fragment页面则切换
                         mainViewPager.currentItem.takeIf { it < mainViewPager.adapter!!.itemCount - 1 }
                         ?.also { (activity as MainActivity).switchFragment(it + 1) }
                      }
                  }
              }
              oldPositon = currentPosition
              scrolledPixeledList.clear()//清空滑动记录
         }
    }

})

到此为止,终于实现了顶部Tab和底部菜单Fragment联动切换啦。

一般大部分App的首页结构都是包含一个广告栏和列表组成,当我向首页Tab再添加一个Banner和RecyclerView时,不出乎意料Banner与ViewPager2滑动冲突了,这里Banner我用的是github一个库:

com.youth.banner:banner:$version

第三个坎:如何解决Banner滑动冲突?

在项目中一开始我用的是github开源库banner 1.4.10 版本,刚开始尝试将Banner作为RecyclerView的Header View,看下RecyclerView有没有帮我们处理了这个问题,不幸运的是RecyclerView并没有对HeaderView任何优化。 因为我也懒得继承Banner重写一堆事件分发等方法,然后发现 Banner 已经出了2.0版本了,它是基于ViewPager2重构了一番,对此已经对与ViewPager2滑动冲突做了优化 ,因此直接使用Banner 2.0 非常方便的解决了冲突问题。


最后附上部分关键的源码:

  MainActivity

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/mainViewpager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />


    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottomNavView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="?android:attr/windowBackground"
        app:menu="@menu/bottom_nav_menu" />

</LinearLayout>
//AndroidX AppCompat 1.1.0 开始 AppCompatActivity构造函数可直接传入布局ID
class MainActivity : AppCompatActivity(R.layout.activity_main) {

    private val fragmentList = arrayListOf<Fragment>()
    private val homeFragment by lazy { BottomHomeFragment() } // 首页
    private val statisticsFragment by lazy { BottomFamilyFragment() } // 统计
    private val inviteFragment by lazy { BottomInviteFragment() } // 邀请
    private val mineFragment by lazy { BottomMineFragment() } // 我的

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        initView()
    }

    private fun initView() {
        fragmentList.run {
            add(homeFragment)
            add(statisticsFragment)
            add(inviteFragment)
            add(mineFragment)
        }
        initViewPager()
        bottomNavView.setOnNavigationItemSelectedListener {
            when (it.itemId) {
                R.id.home -> switchFragment(0)
                R.id.family -> switchFragment(1)
                R.id.invite -> switchFragment(2)
                R.id.mine -> switchFragment(3)
            }
            true
        }
    }

    fun switchFragment(index: Int) {
        mainViewpager.setCurrentItem(index, true)
    }

    private fun initViewPager() {
        //父ViewPager页面切换监听
        mainViewpager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
            override fun onPageSelected(position: Int) {
                //滑动fragment对应底部按菜单钮选中
                bottomNavView.menu.getItem(position).isChecked = true
                //除了前2个bottom fragment有viewpager嵌套不处理滑动,其他都处理滑动
                mainViewpager.isUserInputEnabled =(mainViewpager.currentItem > 1)
            }
        })
        mainViewpager.offscreenPageLimit = 2
        mainViewpager.adapter = object : FragmentStateAdapter(this) {
            override fun createFragment(position: Int) = fragmentList[position]
            override fun getItemCount() = fragmentList.size
        }
    }

    fun getMainViewPager(): ViewPager2 {
        return mainViewpager
    }

}

 

BottomHomeFragment

<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

  <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="?actionBarSize"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

        <com.google.android.material.tabs.TabLayout
            android:id="@+id/tabLayout"
            android:layout_width="match_parent"
            android:layout_height="?actionBarSize"
            app:tabIndicatorColor="#ADBE107E"
            app:tabGravity="fill"
            app:tabMode="fixed" />

    </com.google.android.material.appbar.AppBarLayout>

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior" />


</androidx.coordinatorlayout.widget.CoordinatorLayout>
/**
 * create by ZhongZihao on 2020/3/31
 *
 * 底部菜单 首页 fragment
 */
class BottomHomeFragment : Fragment(R.layout.fragment_bottom_home) {
    private val tabTitleList = arrayOf("首页", "收租记录", "统计")
    private val tabFragmentList = arrayListOf<Fragment>()
    private val homeFragment by lazy { TabIndexFragment() } // 首页
    private val paymentListFragment by lazy { TabPaymentListFragment() } // 收租记录
    private val statisticsFragment by lazy { TabStatisticsFragment() } // 统计
    private lateinit var mainViewPager: ViewPager2 // 父ViewPager

    init {
        tabFragmentList.run {
            add(homeFragment)
            add(paymentListFragment)
            add(statisticsFragment)
        }
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        initViewPager()
    }


    private fun initViewPager() {
        mainViewPager = (activity as MainActivity).getMainViewPager()
        viewPager.offscreenPageLimit = 1
        viewPager.adapter = object : FragmentStateAdapter(this) {
            override fun createFragment(position: Int) = tabFragmentList[position]

            override fun getItemCount() = tabTitleList.size
        }
        TabLayoutMediator(tabLayout, viewPager) { tab, position ->
            tab.text = tabTitleList[position]
        }.attach()

        viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
            private var currentPosition = 0     //当前滑动位置
            private var oldPositon = 0          //上一个滑动位置
            private var currentState = 0
            private var scrolledPixeledList = mutableListOf<Int>() //记录手指滑动时的像素坐标记录
            
            // 滚动状态改变回调,state的值分别有0,1,2 ;
            // 0为ViewPager所有事件(1,2)已结束触发
            // 1为在viewPager里按下并滑动触发多次
            // 2是手指抬起触发
            override fun onPageScrollStateChanged(state: Int) {
                currentState = state
                Log.d("TAG1","onPageScrollStateChanged $state")

                if (state == 0) {
                    if (currentPosition == oldPositon) {
                        when (currentPosition) {
                            0 -> {
                                Log.d("TAG1", "到达最左一个继续往左滑....")
                                //若还有上一个bottom fragment页面则切换
                                mainViewPager.currentItem.takeIf { it > 0 }
                                    ?.also { (activity as MainActivity).switchFragment(it - 1) }
                            }
                            (viewPager.adapter as FragmentStateAdapter).itemCount - 1 -> {
                                   if (scrolledPixeledList.size > 1 && scrolledPixeledList.last() - scrolledPixeledList[0] < 0) {
                                    //有可能出现向左滑到一点放弃的情况也是可以出现currentPosition == oldPositon,则先判断是否是往左滑时放弃
                                        return
                                    }
                                Log.d("TAG1", "到达最右一个继续往右滑....")
                                //若还有下一个bottom fragment页面则切换
                                mainViewPager.currentItem.takeIf { it < mainViewPager.adapter!!.itemCount - 1 }
                                    ?.also { (activity as MainActivity).switchFragment(it + 1) }
                            }
                        }
                    }
                    oldPositon = currentPosition
                    scrolledPixeledList.clear()//清空滑动记录
                }
            }

            //页面滚动的位置信息回调: position 当前滚动到哪个页面,positionOffset 位置偏移百分比, positionOffsetPixels 当前所在页面偏移量
            //此回调会触发完onPageScrollStateChanged 的 state 值为1时后面才触发回调
            override fun onPageScrolled(
                position: Int,
                positionOffset: Float,
                positionOffsetPixels: Int
            ) {
                currentPosition = position
                if (currentState == 1) {
                    //手指按下滑动状态
                    scrolledPixeledList.add(positionOffsetPixels)
                }
            }
        })
    }
}