学习的最终目标就是要学以致用,本文所分享的案例都是自己在公司实战开发过程中的真实案例,现在把它分享出来,希望对初学者有所帮助
好久没有写博客了,今天是周末,所以有时间来写一篇,前些天在工作中出现了一个关于滑动冲突的问题,我把解决它的过程记录下来,现在分享出来,以便给大家遇到了类似的问题提供参考。
关于事件分发在三年前曾经写过一个专栏,共有六篇文章,三篇理论,一篇总结,两篇实战,今天再来写一篇关于实战的文章,如果对事件分发流程不熟悉,请先阅读之前我写过的专栏《View事件分发》系列文章,然后再来看这篇文章你会轻松很多。
整个APP首页的布局架构为: BottomNavigationView +TabLayout+ViewPager+SwipeRefreshLayout+NestedScrollView+RecyclerView,
这种结构在现在的app中也是最通用的布局架构方式,打开手机随便点开一个APP几乎都是这种布局方式,不管是无人不用的微信,还是现在火爆的抖音,头条....等无一例外都是这样的布局方式,侧边栏菜单的模式好像对于国人的使用习惯并不合适,滴滴打车APP在前几年也用过侧边栏的方式,不过在后来的版本升级中也改成了大众所熟悉的底边栏菜单。
在APP首页中ViewPager有左右滑动的动作来翻页,在第一页中有个教材学习的横向RecycleView,它也有左右滑动的动作,当在RecycleView中左右滑动的时候,在它在向右滑到头的时候,此时继续向右滑动ViewPager就开始翻页了,ViewPager中已经给我们解决了滑动冲突,看起来貌似没有问题,但是细心的同学可能发现RecycleView在左右滑动的过程中有明显的卡顿,同样的功能和IOS相比较很明显没有IOS那样的丝滑流畅。
具体的问题效果如下:
006.gif
教材学习的RecycleView左右滑动的过程中会明显的感觉到不流畅,有卡顿,这是因为RecycleView在左右滑动的过程中,一部分滑动事件被ViewPager消费了,所以就出现的卡顿,那么只要事件落在教材学习的RecycleView之上就把所有的事件都交给RecycleView来处理即可解决问题,在最顶级的ViewPager可以做如下处理:
首先创建一个方法isBookTouch来判断触摸事件是否在RecycleView中
private fun isBookTouch(event: MotionEvent): Boolean {
slideViewPagerListener?.getHomeBookRecView()?.let {
val rect = Rect()
it.getHitRect(rect)
if (rect.contains(event.x.toInt(), event.y.toInt())) {
KLog.v("MYTAG", "isBookTouch true")
return true
}
}
return false
}
在isBookTouch方法中我们用到了一个getHitRect方法来判断当前触摸点是否在指定的View上,相关联的有四个方法,分别介绍如下:
1.getHitRect: 获取View可点击矩形左、上、右、下边界相对于父View的左顶点的距离(偏移量)
2.getDrawingRect: 获取View的绘制范围,即左、上、右、下边界相对于此View的左顶点的距离(偏移量),即0、0、View的宽、View的高
3.getLocalVisibleRect: 获取View在第一个可滚动的上级View(父View或祖父View或...)中的可见区域相对于此View的左顶点的距离(偏移量)
4.getGlobalVisibleRect: 获取View在第一个可滚动的上级View(父View或祖父View或...)中的可见区域相对于屏幕左顶点的距离(偏移量)
方案1:
在顶级的ViewPager中做如下处理:
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
if (isBookTouch(event)) {
slideViewPagerListener?.getHomeBookRecView()?.let {
return it.dispatchTouchEvent(event)
}
}
return super.dispatchTouchEvent(event)
}
只要判断该滑动事件在在RecycleView中就全部交给RecycleView来处理
001.gif
这样左右滑动非常的流畅,但是出现了一个问题,当滑动起点在教材学习RecycleView上方,下拉出现了卡顿,起点在RecView上,终点在Recview之下,下拉无响应,此操作录屏的无法演示。
方案2:
代码优化如下:
var isBookTouch = false
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
isBookTouch = isBookTouch(event)
}
else -> {
if (isBookTouch) {
slideViewPagerListener?.getHomeBookRecView()?.let {
return it.dispatchTouchEvent(event)
}
}
}
}
return super.dispatchTouchEvent(event)
}
在滑动的时候判断只要是第一个事件在教材学习的RecycleView上那么后序的事件都交给RecycleView来处理,左右滑动很流畅,但是下拉的问题依旧,起点在RecView上,终点在Recview之下,下拉无响应,此操作录屏的无法演示,这是因为事件的在RecycleView之外本应该交给SwipeRefreshView来处理的事件也交给RecycleView来处理了,显然是不合理的。
002.gif
方案3:阈值法
我们要对RecycleView处理的事件要做进一步的限制,上下滑动的时候事件交给SwipeRefreshLayout来处理,左右滑动的事件交给教材学习的RecycleView即可,代码如下:
var x1 = 0f
var isBookTouch = false
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
isBookTouch = isBookTouch(event)
x1 = event.x
}
MotionEvent.ACTION_MOVE -> {
var x2 = event.x
if (isBookTouch && Math.abs(x2 - x1) > 50) {
slideViewPagerListener?.getHomeBookRecView()?.let {
return it.dispatchTouchEvent(event)
}
}
x1 = x2
}
MotionEvent.ACTION_UP -> {
isBookTouch = false
x1 = 0f
}
}
return super.dispatchTouchEvent(event)
}
事件在RecycleView之上,且是左右滑动,通过两次滑动x轴的偏移量来判断,只要offset偏移大于50即可,左右滑动流畅,上下滑动下拉也很流畅。
具体效果如下:
003.gif
下面在介绍另外一种方法也可以解决此问题:
方案4:斜率法
通过x轴滑动距离和y轴滑动距离做比较,只要x轴的offset比y轴的offset大则认为是左右滑动,代码如下:
var x1 = 0f
var y1 = 0f
var isBookTouch = false
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
isBookTouch = isBookTouch(event)
x1 = event.x
y1 = event.y
}
MotionEvent.ACTION_MOVE -> {
var x2 = event.x
var y2 = event.y
val offsetX = Math.abs(x2 - x1)
val offsetY = Math.abs(y2 - y1)
if (isBookTouch && offsetX > offsetY) {
slideViewPagerListener?.getHomeBookRecView()?.let {
return it.dispatchTouchEvent(event)
}
}
x1 = x2
y1 = y2
}
MotionEvent.ACTION_UP -> {
isBookTouch = false
x1 = 0f
y1 = 0f
}
}
return super.dispatchTouchEvent(event)
}
滑动效果如下:
004.gif
左右滑动很流畅,上下滑动下拉刷新也没有问题。
当然在解决这个问题的时候,也出现了一些不该出现的错误,起初用外部拦截法在滑动时间结束的ACTION_UP事件没有给mFirstMotionEvent 变量重新初始化而导致的下拉刷新有偶尔卡死的现象,具体问题代码如下:
var x1 = 0f
var x2 = 0f
val MIN_DISTANCE = 50
var isBookClick = false
var mFirstMotionEvent : MotionEvent? = null
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
//KLog.v("MYTAG", "MainActivity dispatchTouchEvent start")
if(mFirstMotionEvent == null){
KLog.v("MYTAG", "mFirstMotionEvent is Null, touch:"+event.action)
mFirstMotionEvent = event
isBookClick = isBookRect(event)
}
when (event!!.action) {
MotionEvent.ACTION_DOWN ->{
KLog.v("MYTAG", "ACTION_DOWN touch")
x1 = event!!.x
}
MotionEvent.ACTION_MOVE ->{
KLog.v("MYTAG", "ACTION_MOVE touch")
x2 = event!!.x
val deltaX: Float = x2 - x1
if (Math.abs(deltaX) > MIN_DISTANCE) {
if(isBookRect(event) && isBookClick){
mHomeFragment.getBookRecycleView()?.let {
return it.dispatchTouchEvent(event)
}
}
}
}
MotionEvent.ACTION_UP -> {
KLog.v("MYTAG", "ACTION_UP touch")
x2 = event!!.x
val deltaX: Float = x2 - x1
if (Math.abs(deltaX) > MIN_DISTANCE) {
if(isBookRect(event) && isBookClick){
mHomeFragment.getBookRecycleView()?.let {
return it.dispatchTouchEvent(event)
}
}
}
//初始化位置不对,导致下拉卡顿
mFirstMotionEvent = null
isBookClick = false
}
}
KLog.v("MYTAG", "MainActivity super.dispatchTouchEvent start")
return super.dispatchTouchEvent(event)
}
private fun isBookRect(event: MotionEvent?) : Boolean{
if (mCurrFragment is HomeFragment) {
val bookRecycleView = mHomeFragment.getBookRecycleView()
bookRecycleView?.let {
val rect = Rect()
it.getGlobalVisibleRect(rect)
if (rect.contains(event?.x!!.toInt(), event?.y!!.toInt())) {
KLog.v("MYTAG", "bookRecycleView touch")
return true
}
}
}
return false
}
具体卡顿效果如下:
005.gif
下拉的时候偶尔会出现这种卡死的现象,应该在ACTION_UP时候稍加修改即可解决问题:
MotionEvent.ACTION_UP -> {
KLog.v("MYTAG", "ACTION_UP touch")
x2 = event!!.x
val deltaX: Float = x2 - x1
//修改了初始化mFirstMotionEvent的位置
mFirstMotionEvent = null
if (Math.abs(deltaX) > MIN_DISTANCE) {
if(isBookRect(event) && isBookClick){
mHomeFragment.getBookRecycleView()?.let {
return it.dispatchTouchEvent(event)
}
}
}
}
好了,关于RecycleView+ViewPager+SwipeRefreshLayout滑动冲突我们今天就分析到这里,具体的核心算法就是:滑动事件在RecycleView上且是左右滑动,则该系列事件都交给RecycleView来处理,否则则交给系统来自行处理,左右滑动用阈值法和斜率法都可以解决问题,能用外部拦截法就尽量使用外部拦截发来解决问题,这样不但处理方便,而且执行的效率也会很高,希望这篇文章对你有所帮助。
网友评论