Android事件传递分析-滑动冲突解决(内部处理原理)
前言
Android事件传递分析-OnTouchListener、onTouchEvent、OnClickListener关系
这是事件传递分析的第三章,前两篇我们对事件的传递和消费进行了大致的分析,看过的读者应该能大致明白android事件机制了,现在我们开始处理滑动冲突的问题,其实写到这里很多人都会说滑动冲突解决网上一大把,但是我看了很多文章发现滑动冲突是能解决,但是并没有详细的讲解原理,这篇文章就对滑动冲突内部解决原理做一个简单的分析
事件传递机制
由于我总结的事件传递机制跟网上很多是相似的,所以这里借鉴 放码过来的总结,先做一个预热!
- 同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以down事件开始,中间含有数量不定的move事件,最终以up事件结束
- 正常情况下,一个事件序列只能被一个View拦截且消耗。因为一旦一个元素拦截了某此事件,那么同一个事件序列内的所有事件都会直接交给它处理,因此同一个事件序列中的事件不能分别由两个View同时处理,但是通过特殊手段可以做到,比如一个View将本该自己处理的事件通过onTouchEvent强行传递给其他View处理。
- 某个View一旦决定拦截,那么这一个事件序列都只能由它来处理(如果事件序列能够传递给它的话),并且它的onInterceptTouchEvent不会再被调用。这条也很好理解,就是说当一个View决定拦截一个事件后,那么系统会把同一个事件序列内的其他方法都直接交给它来处理,因此就不用再调用这个View的onInterceptTouchEvent去询问它是否要拦截了。
- 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。意思就是事件一旦交给一个View处理,那么它就必须消耗掉,否则同一事件序列中剩下的事件就不再交给它来处理了。
- 如果View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理。
- ViewGroup默认不拦截任何事件。Android源码中ViewGroup的onInterceptTouch-Event方法默认返回false。
- View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用。
- View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable 和longClickable同时为false)。View的longClickable属性默认都为false,clickable属性要分情况,比如Button的clickable属性默认为true,而TextView的clickable属性默认为false。
- View的enable属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只要它的clickable或者longClickable有一个为true,那么它的onTouchEvent就返回true。
- onClick会发生的前提是当前View是可点击的,并且它收到了down和up的事件。
- 事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。
场景构建
我们这里做一个viewPager包含了HorizontalScrollView的场景,两个都是横向滑动所以会产生一个滑动冲突,我们重写HorizontalScrollView进行内部拦截处理这个滑动冲突,我们让HorizontalScrollView滑动到最右边或者最左边才触发viewpager的滑动事件
布局代码
其中viewpager中的fragment的布局文件如下
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:gravity="center"
android:layout_height="match_parent">
<com.android.base.weight.MyHorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:ignore="UselessParent">
<TextView android:layout_width="match_parent"
android:textSize="20sp"
android:background="@color/colorPrimary"
android:textColor="@color/colorAccent"
android:maxLines="1"
android:gravity="center"
android:text="滑动冲突解决~!滑动冲突解决~!滑动冲突解决~!滑动冲突解决~!滑动冲突解决~!滑动冲突解决~!滑动冲突解决~!滑动冲突解决~!滑动冲突解决~!滑动冲突解决~!滑动冲突解决~!滑动冲突解决~!滑动冲突解决~!滑动冲突解决~!滑动冲突解决~!滑动冲突解决~!滑动冲突解决~!滑动冲突解决~!"
android:layout_height="wrap_content"
android:paddingTop="100dp"
android:paddingBottom="100dp"/>
</com.android.base.weight.MyHorizontalScrollView>
</RelativeLayout>
viewpager的代码很常规!就是设置适配器,这里就不多写代码了
运行时界面如下:
image解决方案
因为横向滑动的viewpager与横向滑动的HorizontalScrollView是有冲突的,我们这里重写HorizontalScrollView来进行内部拦截方法进行处理滑动冲突,我们这里采用的是requestDisallowInterceptTouchEvent方式来通知父组件是否进行onInterceptTouchEvent拦截处理!
整个代码如下:
public class MyHorizontalScrollView extends HorizontalScrollView {
public MyHorizontalScrollView(Context context) {
super(context);
}
public MyHorizontalScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyHorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); }
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public MyHorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); }
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
/**
* 在dispatchTouchEvent这方法中ACTION_DOWN的时候设置requestDisallowInterceptTouchEvent为true或者false都没得影响,
* 因为ACTION_DOWN事件只要父类不手动拦截都会传入他的子view,这里设置为true子view自己处理这个down,设置为false让父组件
* 可以进行拦截,但是父组件也不会拦截所以也是传递下来子view处理,所以这里设置false或者true并没有影响,关键是看onTouchEvent中对这个
* down事件的返回值才是关键,因为onTouchEvent返回值直接影响后续事件需不需要这个子view处理
*
*/
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if (isScrollToRight() || isScrollToLeft()) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
getParent().requestDisallowInterceptTouchEvent(false);
default:
}
return super.dispatchTouchEvent(ev);
}
/**
* 是否已经滑到了最右边
*/
private boolean isScrollToRight() {
return getChildAt(getChildCount() - 1).getRight() == getScrollX() + getWidth();
}
/**
* 是否已经滑到了最左边
*/
private boolean isScrollToLeft() {
return getScrollX() == 0;
}
}
这里代码很简单,就是在dispatchTouchEvent的时候调用根据事件的类型来调用requestDisallowInterceptTouchEvent进行处理,因为我们在滑动到最左边最右边的时候触发viewpager来滑动,所以这里我们做了一个判断
if (isScrollToRight() || isScrollToLeft()) {
getParent().requestDisallowInterceptTouchEvent(false);
}
这里设置成false就是通知父组件这个事件你可以根据你的规则进行拦截,但是其他的事件都默认是true表示我当前自己来处理!这样就很简单的完成了滑动的处理,因为我们继承的是HorizontalScrollView所以这里控件已经帮我们做了onTouchEvent的处理了!这里看不出什么问题,因为只是做了一个简单的通知父组件的操作,想看清楚详细的操作,那我们自己定义一个横向滑动的自定义view吧
自定义横向滑动的view
自定义的具体过程不多讲述,我们这里给出一个界面可以详细的看出我们自定义的样子就行!
image这里我们做的是拖动小圆球进行左右移动,因为是左右移动所以跟viewpager是产生了滑动冲突的,我们照样重写dispatchTouchEvent方法来进行通知父组件拦截处理
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
getParent().requestDisallowInterceptTouchEvent(true);
break;
default:
getParent().requestDisallowInterceptTouchEvent(false);
}
return super.dispatchTouchEvent(event);
}
疑问解决:为什么我们不做MotionEvent.ACTION_DOWN的requestDisallowInterceptTouchEvent处理呢?
答:跟上边解释的一样,在dispatchTouchEvent这方法中ACTION_DOWN的时候设置requestDisallowInterceptTouchEvent为true或者false都没得影响,因为ACTION_DOWN事件只要父类不手动拦截都会传入他的子view,这里设置为true子view自己处理这个down,设置为false让父组件可以进行拦截,但是父组件也不会拦截所以也是传递下来子view处理,所以这里设置false或者true并没有影响,关键是看onTouchEvent中对这个 down事件的返回值才是关键,因为onTouchEvent返回值直接影响后续事件需不需要这个子view处理
重要的onTouchEvent
因为我们自定义view是继承自viwe的
public class CustomView5 extends View {
............省略............
}
这里的自定义view不像上边的继承自HorizontalScrollView,因为继承HorizontalScrollView里面已经处理好了onTouchEvent事件,但是我们这里需要自己手动处理!
我们继续分析,因为前面自定义view前面已经处理好了dispatchTouchEvent,这里我们开始正式我们滑动事件消费的处理,因为消费是在onTouchEvent中,所这里我们要分类型
public boolean onTouchEvent(MotionEvent event) {
float oldX;
switch (event.getAction()) {
//问题1
case MotionEvent.ACTION_DOWN:
//拖动小圆球滑动的处理
oldX = event.getX();
setProgressIndex(oldX);
break;
case MotionEvent.ACTION_MOVE:
//拖动小圆球滑动的处理
setProgressIndex(event.getX());
//问题2
return false;
default:
}
return super.onTouchEvent(event);
}
看到这里应该很多人都会说这个跟正常的onTouchEvent有什么大的区别吗?却是没什么大的区别。但是这里隐藏了很多原理
问题1
因为onTouchEvent方法中只返回了一个super.onTouchEvent,因为大家都知道down事件如果返回为false,那么后续事件就接收不到了,拖动小圆球就不会起作用了,怎么保证我能接受到后面的事件呢?
答:问到这里却是是这样的,但是我们进入super.onTouchEvent的源码里面可以分析:
/**
* Implement this method to handle touch screen motion events.
* <p>
* If this method is used to detect click actions, it is recommended that
* the actions be performed by implementing and calling
* {@link #performClick()}. This will ensure consistent system behavior,
* including:
* <ul>
* <li>obeying click sound preferences
* <li>dispatching OnClickListener calls
* <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
* accessibility features are enabled
* </ul>
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
。。。省略代码。。。
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
。。。省略代码。。。
}
return true;
}
return false;
}
为了方便起见,我这里省略了很多代码,只看返回值的条件
clickable || (viewFlags & TOOLTIP) == TOOLTIP
因为是||所以很简单,满足一个为true即可,我们看到clickable是不是很熟悉?加上前面两片文章我有介绍onTouchEvent会调用OnClickListener来消耗事件,所以为了让down的时候返回为true只需要设置点击事件即可。所以
view.CustomView5.setOnClickListener { }
问题解决!
我们再来看下HorizontalScrollView的onTouchEvent源码会发现,里面返回的也是true!
@Override
public boolean onTouchEvent(MotionEvent ev) {
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
final int action = ev.getAction();
switch (action & MotionEvent.ACTION_MASK) {
。。。。。省略。。。。。
}
return true;
}
问题2
前面我们解决了down事件返回为true,后续事件可以接受到,但是为什么在ACTION_MOVE的时候返回为false呢?返回为false那这个事件没有处理咋办?
答:这里我只是做了一个代码的埋点,就是让大家看到这里我返回的是fasle,返回true表示消费了这里就不多说了,因为事件传递的过程中,如果子view接受了down事件,那么后续的事件都该由此view来处理完成,但是这里我们在move的时候返回为false不处理,按照道理他应该返回给父组件来处理,但是这里是有问题的,他并不会给viewpager来处理,所以这里返回false也不会触发viewpager的onTouchEvent而导致滑动冲突,他会直接返回到顶层的容器里面处理或者忽略掉,这是很关键的一点,并且下面的up事件也是一样,所以我这里值返回一个super.onTouchEvent(event)来满足down事件,至于后续事件返回true或者false都不影响我的拖动操作!当然个别情况下是move的返回值的话 要自己手动处理!
总结
滑动事件处理只要懂原理什么都可以迎刃而解,外部拦截是最简单的操作,只需要重写onInterceptTouchEvent根绝自己的情况来判断返回为true拦截即可!内部拦截的话一般requestDisallowInterceptTouchEvent是写在dispatchTouchEvent里面进行分发的操作,然后在onTouchEvent里面进行消耗操作,需要注意的是down事件一定要消耗,至于move或者up事件看自己业务需求而定,像我上边拖动小圆球操作就没得什么特别的要求,只需要走那段代码就行,不需要必须消耗掉那个move事件!冲突解决就写到这里!
需要源码的朋友可以发送请求到邮箱 imkobedroid@gmail.com 文章与代码有待改进!希望可以交流