Android/Kotlin:如何按需仅允许ViewPager左滑?
Hey 各位开发者好!我最近遇到了一个ViewPager滑动控制的需求,折腾了好一阵,现在把过程和最终解决方案分享出来~
核心场景需求
我有一个支持左右滑动的ViewPager,其中某一个Fragment页面需要用户完成操作后,才能允许继续向右滑动前进,但必须保留向左滑动返回上一页的功能。
尝试过的无效方案(基于ViewPager1)
首先我自定义了一个LockableViewPager来控制滑动开关,代码如下:
class LockableViewPager : ViewPager { constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet) companion object { private const val TAG = "LockableViewPager" } private var swipeEnabled = true override fun onTouchEvent(event: MotionEvent): Boolean { return when (swipeEnabled) { true -> super.onTouchEvent(event) false -> false // 禁用时不处理触摸事件 } } override fun onInterceptTouchEvent(event: MotionEvent): Boolean { return when (swipeEnabled) { true -> super.onInterceptTouchEvent(event) false -> swipeEnabled } } fun setSwipeEnabled(swipeEnabled: Boolean) { this.swipeEnabled = swipeEnabled } }
但这个实现的问题很明显:它会同时禁用左右两个方向的滑动,没法做到只锁定向右前进的滑动。我尝试给ViewPager添加OnTouchListener来检测滑动方向,但发现只有在ViewPager未锁定时,这个监听器才会生效,完全达不到我要的“只锁右滑、保留左滑”的效果。
后来我想到一个绕路的方案:在包含ViewPager的Activity布局底部加一个全屏的拦截View:
<View android:id="@+id/touch_layer" android:layout_width="match_parent" android:layout_height="match_parent" android:clickable="true" />
然后写了一个抽象的触摸监听器类:
abstract class OverlayTouchListener : View.OnTouchListener { companion object{ private const val TAG = "OverlayTouchListener" } }
在Activity中给这个拦截层设置监听器,试图判断滑动方向后再决定是否把事件传给ViewPager:
var touchLayer = this.findViewById<View>(R.id.touch_layer) touchLayer?.setOnTouchListener(object : OverlayTouchListener() { override fun onTouch(v: View?, event: MotionEvent?): Boolean { Log.d(TAG, "onTouch") if(event != null){ // 这里计划判断滑动方向和ViewPager的锁定状态 // 仅当左滑时才把事件传给ViewPager viewPager?.setSwipeEnabled(true) viewPager?.onTouchEvent(event) } return true } })
但这个方案太繁琐了,感觉不是最优解,所以我转向了ViewPager2。
切换到ViewPager2遇到的嵌套Fragment崩溃问题
改用ViewPager2后,我遇到了子Fragment的布局加载问题。我先自定义了Fragment事务的扩展方法:
inline fun FragmentManager.doTransaction(func: FragmentTransaction.() -> Unit) { Log.d(TAG, "doTransaction") val fragmentTransaction = beginTransaction() fragmentTransaction.func() fragmentTransaction.commit() } fun AppCompatActivity.replaceFragment(frameId: Int, fragment: Fragment) { Log.d(TAG, "replaceFragment") supportFragmentManager.doTransaction { replace(frameId, fragment) } }
然后在某个Fragment的newInstance方法里,尝试嵌套加载子Fragment:
companion object : CustomCompanion() { @JvmStatic override fun newInstance(activity: Activity): IdealsFragment { val fragment = IdealsFragment( activity ) (activity as NewCharacterActivity).replaceFragment( R.id.fragmentIdeals_container_ideals, IdealsRecyclerViewFragment.newInstance( activity ) ) return fragment } }
结果直接抛出了no view found for id异常——我明白这是因为用了Activity的supportFragmentManager而不是Fragment的childFragmentManager。但换成childFragmentManager后,又出现了Fragment has not been attached yet的崩溃,这让我卡了好一阵。
最终可行的解决方案(基于ViewPager2)
后来我想通了,ViewPager2的工作机制和RecyclerView类似,其实可以通过控制适配器的Fragment列表来实现“锁定前进滑动”的需求:
- 在适配器中使用
MutableList<Fragment>来管理页面:
var fragmentList = mutableListOf<Fragment>( BasicInfoFragment.newInstance( activity ), BondsFragment.newInstance( activity ), // 初始只包含需要锁定页面之前的Fragment )
- 初始状态下,列表里只有用户当前能访问的Fragment,后面需要解锁的页面暂时不加入列表;
- 当用户完成指定操作、满足解锁条件后,再把后续的Fragment添加到适配器的列表中,并通知适配器更新:
// 满足条件后添加后续Fragment fragmentList.add(IdealsFragment.newInstance(activity)) fragmentList.add(OtherFragment.newInstance(activity)) adapter.notifyDataSetChanged()
这样ViewPager2就会自动识别新增的页面,允许用户滑动过去,同时天然保留了向左滑动返回的功能,完美解决了我的需求!
内容的提问来源于stack exchange,提问作者Ch4BRN

