示例之Activity
public class MainActivity extends Activity implements OnTouchListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_second);
findViewById(R.id.root).setTag("root");
findViewById(R.id.ll_child).setTag("ll_child");
findViewById(R.id.tv).setTag("tv");
findViewById(R.id.tv2).setTag("tv2");
findViewById(R.id.tv3).setTag("tv3");
findViewById(R.id.tv_child).setTag("tv_child");
findViewById(R.id.tv_child2).setTag("tv_child2");
findViewById(R.id.root).setOnTouchListener(this);
findViewById(R.id.ll_child).setOnTouchListener(this);
findViewById(R.id.tv).setOnTouchListener(this);
findViewById(R.id.tv2).setOnTouchListener(this);
findViewById(R.id.tv_child).setOnTouchListener(this);
findViewById(R.id.tv_child2).setOnTouchListener(this);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
//ACTION_DOWN = 0,按下动作;ACTION_UP = 1,离开动作;ACTION_MOVE = 2,移动动作
Log.i("onTouch:", "onTouch--" + v.getTag() + "--" + event.getAction());
return false;
}
}
示例之布局
<com.bqt.MyLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#00f"
android:orientation="vertical"
android:padding="30dp" >
<com.bqt.MyTextView
android:id="@+id/tv"
android:layout_width="match_parent"
android:layout_height="65dp"
android:background="#0ff"
android:gravity="center"
android:text="LinearLayout的子View1" />
<com.bqt.MyTextView
android:id="@+id/tv2"
android:layout_width="match_parent"
android:layout_height="65dp"
android:background="#ff0"
android:gravity="center"
android:text="LinearLayout的子View2" />
<com.bqt.MyLinearLayout
android:id="@+id/ll_child"
android:layout_width="match_parent"
android:layout_height="220dp"
android:background="#000"
android:orientation="vertical"
android:padding="30dp" >
<com.bqt.MyTextView
android:id="@+id/tv_child"
android:layout_width="match_parent"
android:layout_height="65dp"
android:background="#fff"
android:gravity="center"
android:text="子子View1" />
<com.bqt.MyTextView
android:id="@+id/tv_child2"
android:layout_width="match_parent"
android:layout_height="65dp"
android:background="#f00"
android:gravity="center"
android:text="子子View2" />
</com.bqt.MyLinearLayout>
<com.bqt.MyTextView
android:id="@+id/tv3"
android:layout_width="match_parent"
android:layout_height="65dp"
android:background="#0ff"
android:gravity="center"
android:text="LinearLayout的子View3" />
</com.bqt.MyLinearLayout>
示例之MyLinearLayout
package com.bqt;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.LinearLayout;
public class MyLinearLayout extends LinearLayout {
public MyLinearLayout(Context context) {
super(context);
}
public MyLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i("onTouch:", "dispatchTouchEvent--" + getTag() + "--" + event.getAction());
//if (getId() == R.id.ll_child) return true;//在【示例1】时添加
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("onTouch:", "onTouchEvent--" + getTag() + "--" + event.getAction());
//if (getId() == R.id.ll_child) return true;//在【示例1】时增加此行代码没有任何意义。在【示例3】时添加。
return super.onTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
Log.i("onTouch:", "onInterceptTouchEvent--" + getTag() + "--" + event.getAction());
//if (getId() == R.id.ll_child) return true;//在【示例5】时添加。
return super.onInterceptTouchEvent(event);
}
}
示例之MyTextView
public class MyTextView extends TextView {
public MyTextView(Context context) {
super(context);
}
public MyTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i("onTouch:", "dispatchTouchEvent--TextView--" + getTag() + "--" + event.getAction());
//if (getId() == R.id.tv2) return true;//在【示例2】时添加
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("onTouch:", "onTouchEvent--" + getTag() + "--" + event.getAction());
//if (getId() == R.id.tv2) return true;//在【示例2】时增加此行代码没有任何意义。在【示例4】时添加。在【示例5】时增加此行代码没有任何意义。
return super.onTouchEvent(event);
}
}
【示例1,VG的dispatchTE方法】
我们在【子LinearLayout】的dispatchTouchEvent方法中返回true
或者在【子LinearLayout】的dispatchTouchEvent和onTouchEvent方法中都返回true
当触摸根部的LinearLayout时
当触摸根部的子View1时
当触摸子LinearLayout或子子View1、子子View2时
可以看到:
1、触摸事件由Action_Down==0、Action_Move==2、Aciton_UP==1组成,其中一次完整的触摸事件中,Down只有一个、Up有一个或0个、Move有若干个(包括0个),一旦Aciton_UP发生,就表明此次触摸事件已经结束了。
2、不管触摸哪里,不管是触摸事件中的哪个Action(DOWN、MOVE、UP),对于同一个View来说,都是按照下面的顺序执行的:
dispatchTouchEvent --> onTouch(如果有) --> onTouchEvent
3、如果触摸点区域没有一个View的dispatchTouchEvent方法的返回值为true(大前提是:也没有一个View的onTouchEvent方法和onTouch方法的返回值为true),如当触摸根部的子View1时,则在分发完Down事件后(Down事件一定会分发),其余的Move、UP事件将不再分发,也即任何View都不会收到后续的Move、UP事件(后续事件不再分发),因为没有View需要处理。
4、同样是在上面的条件下,即:如果触摸点区域没有一个View的dispatchTouchEvent方法的返回值为true(大前提是:也没有一个View的onTouchEvent方法和onTouch方法的返回值为true),如当触摸根部的子View1时,当Down事件从root逐级下传到最底层的View1后,View1的dispatchTouchEvent又将Down事件传给了自己的onTouchEvent方法,然后是逐级逆向、上传给父View的onTouchEvent方法。
5、一旦某个View(如子LinearLayout)的dispatchTouchEvent方法返回值为true(表明事件分发到此结束),则其子View(如子子View1、子子View2)就不可能会获取到任何Touch事件(包括Down事件);但是此View的所有父View(如root)可以获取到此View能获取到的任何事件(包括Move、UP事件) 。
6、如果一个ViewGroup的dispatchTouchEvent方法返回值为true,则此ViewGroup的onTouchEvent方法和onTouch方法将不会被调用。
【示例2,View的dispatchTE方法】
我们在【子View2】的dispatchTouchEvent方法中返回true
或者在【子View2】的dispatchTouchEvent和onTouchEvent方法中都返回true
当触摸其他View时的过程和上面的情况完全一样
当触摸根部的子View2时
我们发现,示例1中的结论对于View来说也完全适用。
【示例3,VG的onTouchEvent方法】
我们仅在【子LinearLayout】的onTouchEvent方法中返回true
当触摸子LinearLayout时
可以发现,对于事件的分发过程,其和示例1、示例2完全一样。
唯一的区别是:
如果子LinearLayout的onTouchEvent方法返回true,那么子LinearLayout的onTouchEvent方法将不再逐级逆向、上传给父View的onTouchEvent方法,即其父View的onTouchEvent将无法调用。
换句话说就是,如果子LinearLayout的onTouchEvent方法返回true,那么Touch事件将会在这里完全被消耗掉。
当触摸子子View1、子子View2时
可以发现:
1、对于【DOWN事件】,虽然子LinearLayout的onTouchEvent方法返回了true,但是子子View1的dispatchTouchEvent方法仍【会】被调用,所以可以知道,任何View的onTouchEvent方法返回true都不影响DOWN事件的正常分发。
2、同样,对于【DOWN事件】,子子View1的dispatchTouchEvent被调用后会回调自己的onTouchEvent方法,然后子子View1的onTouchEvent方法会回调其父View的onTouchEvent方法。这些都和上面的分析是完全一样的。
3、下面是关键了,我们发现,对于【非DOWN事件】,子子View1的dispatchTouchEvent方法【不会】被调用(当然,子子View1的onTouchEvent方法更不可能会被调用),这意味着分发过程在此之前已经结束了。
4、完整的流程为:一旦某个View的onTouchEvent返回true,当【DOWN】事件按照【正常的分发流程】逐级【下传】到【最底层的View】的【dispatchTouchEvent】后,【最底层的View】将通过其【onTouchEvent】逐级【上传】,直到上传到【此View】的【onTouchEvent】方法之后,DOWN事件将会被消耗掉;然而,此后的【MOVE、UP】事件在按照【正常的分发流程】逐级【下传】到【此View】后将直接结束分发(而不会下传到最底层的View),并且在调用【此View】的onTouchEvent方法后被消耗掉。
5、由此可见,MOVE、UP事件仅onTouchEvent返回true的那个View及其所有父View能接收到,这也是onTouchEvent方法最核心的作用,即用来告诉系统,Touch事件到底应该【传递】给哪个View。注意,不要误解为:MOVE、UP事件仅onTouchEvent返回值为true的那个View才能接收到。而应该理解为:MOVE、UP事件仅onTouchEvent返回值为true的那个View的onTouchEvent方法才会被回调到。
6、简单的说就是:
- 事件【向下】传递过程中,如果中间任一View的dispatchTouchEvent方法返回true,则事件将结束分发(此时不会调用任何View的onTouchEvent方法)
- 事件【向上】传递过程中,如果中间任一View的onTouchEvent方法返回true,除了DOWN事件会正常分发外(会向下分发,dispatchTouchEvent方法会被正常回调,onTouchEvent方法会在回调到此View时结束回调),其他Touch事件在【分发】到此View之后将结束分发(不会再向下分发),且事件会在此View的onTouchEvent中被消费掉(不会再向上传递)。
【示例4,View的onTouchEvent方法】
我们仅在【子View2】的onTouchEvent方法中返回true
当触摸其他View时的过程和上面的情况完全一样
当触摸根部的子View2时
实在没啥好说的,该说的上面的都说完了。
也就是说,示例3所有的结论对于View来说也完全适用。
【示例5,VG的onInterceptTE方法】
我们仅在【子LinearLayout】的onInterceptTouchEvent方法中返回true
或者在【子LinearLayout】的onInterceptTouchEvent方法中返回true,并且在【子View2】的onTouchEvent方法中返回true
当触摸其他View时的过程和上面的情况完全一样
当触摸子LinearLayout时
可以看到,在调用dispatchTouchEvent方法之后立刻调用了View的onInterceptTouchEvent方法,除此之外没任何区别。
当触摸子子View1和子子View2时:
我们发现,事件分发过程中,子子View1、子子View2的任何回调方法都回调不到了,包括万能的DOWN事件。
另外,虽然子View2的onTouchEvent方法返回true,但是也没有任何卵用,因为在dispatchTouchEvent下发DOWN事件的过程中,在DOWN事件分发到它之前就被拦截掉了,后续也更不会收到MOVE、UP事件。
这是onInterceptTouchEvent的一个重要作用:拦截包括Down事件在内的所有事件【向下】分发。
【用onInterceptTE解决滑动冲突的思想】
设想一下在一个ViewPager中,每个Item都是个ImageView,我们需要对这些ImageView做Matrix操作,这不可避免要捕获掉Touch事件,但是我们又需要做到不影响ViewPager翻页效果,这又必须保证ViewPager能捕获到Move事件,我们该怎么做呢?
我们可以对ViewPager的onInterceptTouchEvent接收到的Move事件做一个过滤,当适当条件的Move事件(如持续若干时间或移动若干距离)触发时,会拦截掉,返回子View一个Action_Cancel事件,这个时候子View就没有Up事件了,很多需要在Up中处理的事物要转到Cancel中处理。
【面试题:滑动冲突问题如何解决?】
答:
要解决滑动冲突,其实最主要的就是有一个核心思想:你到底想在一个事件序列中让哪个view 来响应你的滑动?
比如,从上到下滑,是哪个view来处理这个事件,从左到右呢?
业务需求想明白以后,剩下的其实就很好做了。核心的方法就是2个,外部拦截也就是父亲拦截,另外就是内部拦截,也就是子view拦截法。学会这2种,基本上所有的滑动冲突都是这2种的变种,而且核心代码思想都一样。
外部拦截法:
思路就是重写父容器的onInterceptTouchEvent即可,子元素一般不需要管。可以很容易理解,因为这和android自身的事件处理机制逻辑是一模一样的,例如:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN://down事件肯定不能拦截,拦截了后面的就收不到了
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if (你的业务需求) intercepted = true;//如果确定拦截了,就去自己的onTouchEvent里处理拦截之后的操作即可
else intercepted = false;
break;
case MotionEvent.ACTION_UP:
//up事件我们一般都是返回false的,一般父容器都不会拦截他, 因为up是事件的最后一步,这里返回true也没啥意义。
//唯一的意义就是,如果父元素把up拦截了,将导致子元素收不到up事件,
//那子元素就肯定没有onClick事件触发了,这里的小细节 要想明白
intercepted = false;
break;
default:
break;
}
return intercepted;
}
内部拦截法:
内部拦截法稍微复杂一点,就是事件到来的时候,父容器不管,让子元素自己来决定是否处理。如果消耗了就最好,没消耗自然就转给父容器处理了。
子元素代码:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if (如果父容器需要这个点击事件) getParent().requestDisallowInterceptTouchEvent(false);
//否则的话就交给自己本身view的onTouchEvent自动处理了
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}
父亲容器代码也要修改一下,其实就是保证父亲别拦截down事件:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) return false;
return true;
}