ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
[TOC] # 1. 自定义ViewPager 比如在自定义ViewPager中,中的某个子页面使用了一个scrollView。对于自定义ViewPager这里再次复习一下: * 定义对应的类,继承自ViewGroup,并复写onLayout方法,使得所有的页面在逻辑上是连着的。 * 通过addView来添加子视图,这里直接使用ImageView,然后为其指定Background; * 通过上述步骤后,就可以显示出来一个页面;然后我们需要为这个自定义ViewPager指定手指触摸的滑动事件; * 使用手势识别GestureDetector的onTouchEvent事件来进行事件的拦截,在对应的onScroll方法中进行滑动,这里使用scrollBy进行,当然需要进行边界的判断; * 然后我们需要为他添加一个回弹的动画,这里可以采用自定义,也可以使用系统中提供的android.widget.Scroller来实现,使用scroller.startScroll来开始滑动,使用scroller.computeScrollerOffset判断是否滑动结束。同时,这个类也提供了插值器,所以在最后会有个很好看的平滑效果。 对应代码: ~~~ /** * 使用系统自带Scroller */ class MyViewPager(context: Context?, attrs: AttributeSet?) : ViewGroup(context, attrs) { // 手势识别器 var gestureDetector: GestureDetector? = null var mOnPagerChangerListener: OnPagerChangerListener? = null init { gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() { override fun onScroll( e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float ): Boolean { // X轴移动 if(scrollX + distanceX >= 0 && scrollX + distanceX <= width * (childCount - 1)){ scrollBy(distanceX.toInt(), 0) } return true } }) } override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { for (i in 0..childCount) { val childView = getChildAt(i) childView?.layout(i * width, 0, (i + 1) * width, height) } } var startX = 0f var index = 0 // 使用系统android.widget.Scroller var scroller: Scroller = Scroller(context) override fun onTouchEvent(event: MotionEvent?): Boolean { //3.把事件传递给手势识别器 gestureDetector?.onTouchEvent(event) when (event?.action) { MotionEvent.ACTION_DOWN -> { // 起始位置 startX = event.x } MotionEvent.ACTION_MOVE -> { } MotionEvent.ACTION_UP -> { var tempIndex = index if((startX - event.x) > width / 2){ tempIndex++ }else if((event.x - startX) > width / 2 ){ tempIndex-- } // 非法处理 if(tempIndex < 0) tempIndex = 0 if(tempIndex > childCount - 1) tempIndex = childCount - 1 index = tempIndex // 监听接口调用 mOnPagerChangerListener?.onPageChange(index) // 回弹 scrollToPage(index) } } return true } /** * 按照页面下标进行滚动 */ fun scrollToPage(tempIndex: Int){ scroller.startScroll(scrollX, scrollY, tempIndex*width - scrollX, 0) invalidate() } override fun computeScroll() { if (scroller.computeScrollOffset()) { scrollTo(scroller.currX, 0) invalidate() } } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) // 获取子元素个数 if (childCount == 0) return // 如果是wrap_content,也就是AT_MOST模式 // 如果是match_parents,也就是精确模式 val widthMode = MeasureSpec.getMode(widthMeasureSpec) val heightMode = MeasureSpec.getMode(heightMeasureSpec) // 自适应,也就是取决于最大的子元素的宽和高,这里直接设置为屏幕的宽和高 var height = MeasureSpec.getSize(heightMeasureSpec) var width = MeasureSpec.getSize(widthMeasureSpec) if (widthMode == MeasureSpec.AT_MOST ){ width = resources.displayMetrics.widthPixels } if(heightMode == MeasureSpec.AT_MOST) { height = resources.displayMetrics.heightPixels } setMeasuredDimension(width, height) // 需要测量孩子 for (i in 0 until childCount){ getChildAt(i).measure(width, height) } } // 定义一个页面下标改变的监听接口 interface OnPagerChangerListener{ fun onPageChange(position: Int) } } ~~~ 这里设置监听器是为了关联指示器,指示器使用RadioButton来实现。比如: ~~~ class MainActivity : AppCompatActivity() { val custom_viewpager by lazy { findViewById<MyViewPager>(R.id.custom_viewpager) } val radioGroup by lazy { findViewById<RadioGroup>(R.id.radioGroup) } var imageRes = listOf<Int>(R.drawable.a, R.drawable.b, R.drawable.c) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) for (i in 0 until imageRes.size){ val imageView = ImageView(this) imageView.setBackgroundResource(imageRes.get(i)) custom_viewpager.addView(imageView) } val view = layoutInflater.inflate(R.layout.activity_other, null) custom_viewpager.addView(view, 2) for(i in 0 until custom_viewpager.childCount){ val btn = RadioButton(this) if(i == 0) btn.isChecked = true btn.id = i radioGroup.addView(btn) } radioGroup.setOnCheckedChangeListener(object : RadioGroup.OnCheckedChangeListener { override fun onCheckedChanged(group: RadioGroup?, checkedId: Int) { // 切换页面 custom_viewpager.scrollToPage(checkedId) } }) // 页面切换关联radioButton custom_viewpager.mOnPagerChangerListener = object : MyViewPager.OnPagerChangerListener{ override fun onPageChange(position: Int) { radioGroup.check(position) } } } } ~~~ # 2. 滑动冲突 ## 2.1 环境构建 在上面的代码中我们使用了: ``` val view = layoutInflater.inflate(R.layout.activity_other, null) custom_viewpager.addView(view, 2) ``` 来添加一个页面,在这个页面中使用了ScrollView: ~~~ <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <ScrollView android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:id="@+id/linearlayout" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" > </LinearLayout> </ScrollView> </LinearLayout> ~~~ 如果我们在另外一个Activity中测试: ~~~ class TestActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_other) val linearlayout by lazy { findViewById<LinearLayout>(R.id.linearlayout) } for( i in 0..50){ val textView = TextView(this) textView.text = "文本:${i}" linearlayout.addView(textView) } } } ~~~ 可以发现滑动没有问题: ![](https://img.kancloud.cn/b7/d3/b7d330e413ceb3f8292ad60899afee8a_373x597.png) 但是如果我们将其应用在前面的自定义ViewPager中: ~~~ val view = layoutInflater.inflate(R.layout.activity_other, null) val linearlayout by lazy { view.findViewById<LinearLayout>(R.id.linearlayout) } for( i in 0..50){ val textView = TextView(this) textView.width = resources.displayMetrics.widthPixels textView.gravity = Gravity.CENTER textView.textSize = 22F textView.text = "文本:${i}" linearlayout.addView(textView) } custom_viewpager.addView(view, 0) ~~~ ​那么,按照逻辑这里就会出现滑动冲突。这里我的现象是滚动不了ViewPager,同时ScrollerView也不能滚动。 ## 2.2 环境构建中问题排查 其实这个现象是不应该的,因为按照道理来说滑动冲突,也能有响应发生,故而这里排查一下: > ScrollView内的最外层控件或布局,如果尺寸大小不明确,会导致无法滑动。 但由于我的布局为: ~~~ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <ScrollView android:id="@+id/scrollview" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:id="@+id/linearlayout" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" > </LinearLayout> </ScrollView> </LinearLayout> ~~~ 然后custom_viewpager.addView(view, 0),也就是说可能的原因就在于xml布局中外层LinearLayout的大小没有测量出来。也就是测量孩子的宽高有问题。再次看下前面的测量孩子的代码: ``` // 需要测量孩子 for (i in 0 until childCount){ getChildAt(i).measure(width, height) } ``` 很明显,这里只是手动的测量了直接孩子的大小。所以这里就由系统自己去测量: ~~~ // 测量孩子 measureChildren(widthMeasureSpec, heightMeasureSpec) ~~~ 即: ~~~ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) // 获取子元素个数 if (childCount == 0) return // 测量孩子 measureChildren(widthMeasureSpec, heightMeasureSpec) // 如果是wrap_content,也就是AT_MOST模式 // 如果是match_parents,也就是精确模式 val widthMode = MeasureSpec.getMode(widthMeasureSpec) val heightMode = MeasureSpec.getMode(heightMeasureSpec) // 自适应,也就是取决于最大的子元素的宽和高,这里直接设置为屏幕的宽和高 var height = MeasureSpec.getSize(heightMeasureSpec) var width = MeasureSpec.getSize(widthMeasureSpec) if (widthMode == MeasureSpec.AT_MOST ){ width = resources.displayMetrics.widthPixels } if(heightMode == MeasureSpec.AT_MOST) { height = resources.displayMetrics.heightPixels } setMeasuredDimension(width, height) } ~~~ 然后就解决了前面的现象: > 滚动不了ViewPager,同时ScrollerView也不能滚动 ![](https://img.kancloud.cn/6b/f1/6bf1899406ff3b5d63f3200fa4aa9e1a_242x403.png) 到了一个正常的滑动冲突的现象。此时的现象为: > 可以滚动ScrollView中的文本; > 自定义的ViewPager无法滑动切换; 这里的**冲突也就是常见的非同向冲突。** ## 2.3 滑动冲突解决 这里的滑动冲突解决起来比较简单,因为是两个非同向的冲突,我们只需要判断一下触摸事件的方向,然后决定由谁来处理即可。具体来说涉及到几个方法: * onTouchEvent,判断起始位置,判断用户滑动事件方向; * onInterceptTouchEvent,返回true表示拦截,否则为不拦截; * parent.requestDisallowInterceptTouchEvent(boolean),传入的参数为true表示要求父控件不处理,由自己处理; 首先看下这个方法: ~~~ override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { } ~~~ 根据事件传递规则,如果一开始就直接返回true或者false,那么就会导致两种情况。要么事件直接被当前的VeiwGroup拦截,要么就是使得当前的ViewGroup响应不了事件。所以这里提供的思路就可以有两种: * 根据条件判断,是否要拦截事件; * 传递到子View,由子View决定自己要消费的事件; 当然,这里首先考虑使用第一种,代码如下: ~~~ var interceptorX = 0f var interceptorY = 0f override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { when (ev?.action) { MotionEvent.ACTION_DOWN -> { // 放行Down事件 startX = ev.x // 因为后续接受不了Down事件,所以在这里赋值 // 起始位置 interceptorX = ev.x interceptorY = ev.y } MotionEvent.ACTION_MOVE -> { // MOVE事件选择放行 return abs(ev.x - interceptorX) >= abs(ev.y - interceptorY) } } return false } ~~~ 当然还是来尝试一下使用第二种方式: 由于ViewGroup默认onInterceptTouchEvent返回false,所以是交给子View处理,也就是这里的ScrollView来处理。我们只需要复写一下其方法,然后对需要的事件进行消费,当然还是需要在ViewGroup中对其进行放行Down事件才行: ~~~ var interceptorX = 0f var interceptorY = 0f // 添加到ViewGroup之后执行 scrollview.setOnTouchListener(object : View.OnTouchListener { override fun onTouch(v: View?, event: MotionEvent?): Boolean { when (event?.action) { MotionEvent.ACTION_DOWN -> { Log.e("TAG", "ACTION_DOWN: ${scrollview.parent == null}") // 告诉父控件,自己要处理,不允许拦截 scrollview.parent?.apply { (scrollview.parent as ViewGroup).requestDisallowInterceptTouchEvent(true) } // 起始位置 interceptorX = event.x interceptorY = event.y return true } MotionEvent.ACTION_MOVE -> { Log.e("TAG", "ACTION_MOVE: ") scrollview.parent?.apply { if (abs(event.x - interceptorX) >= abs(event.y - interceptorY)) { // 让父控件拦截 (scrollview.parent as ViewGroup).requestDisallowInterceptTouchEvent( false ) return false // ScrollView不消费 } else { (scrollview.parent as ViewGroup).requestDisallowInterceptTouchEvent( true ) // 交给scrollview处理 scrollview.onTouchEvent(event) return true } } } } return true } }) ~~~ 从代码量来说,在这个场景中第一种方式解决冲突的更好,因为代码量更少。