1、什么是事件传递
可以理解为触摸事件在Activity和控件之间,控件和控件之间的传递过程。
2、学习完有什么用处
自定义控件处理控件之间的冲突,明白点击事件由哪个对象发出,经过哪些对象,最终达到哪个对象并最终得到处理。
3、学习之前应该了解什么
3.1 什么是ViewGroup
集成的View,可以充当其他view的容器
3.1 什么是View
单一控件,例如 textview ,button等等
3.2 事件传递的重要方法,事件分发的三个方法
(1)dispathTouchEvent 事件分发
(2)onInterceptTouchEvent 阻拦机制
(3)onTouchEvent 消费机制
3.3 明白 MotionEvent 的几个处理事件
(1)MotionEvent.ACTION_DOWN:按下View(所有事件的开始)
(2)MotionEvent.ACTION_MOVE:滑动View
(3)MotionEvent.ACTION_CANCEL:非人为原因结束本次事件
(4)MotionEvent.ACTION_UP:抬起View(与DOWN对应)
4、知道事件传递的基本过程
图片总是比文字更容易让人理解,这里我们就选择文字来让我们开始理解事件传递的基本顺序,如下图布局
我们可以把图片打比方作为下图
下图就是基本的传递过程
Android事件分发是先传递到ViewGroup,再由ViewGroup传递到View,在ViewGroup中通过onInterceptTouchEvent()对事件传递进行拦截onInterceptTouchEvent方法返回true代表拦截事件,即不允许事件继续向子View传递,然后就会调用自己本身的onTouchEvent事件,进行处理,此时的onTouchEvent也具有false & True的选择,True就代表事件已经被我们的view所消化掉,false就会往上传递,一直到activity中;onInterceptTouchEvent() 返回false代表不拦截事件,即允许事件继续向子View传递;(默认返回false),子View中如果将传递的事件消费掉,ViewGroup中将无法接收到任何事件。
5、知道各种点击事件的排列顺序
onTouch和onClick事件同时发生
ontouch的执行先于onclick的执行
6、onTouch 以及 onTouchEvent 的区别
这两个方法都是在View的dispatchTouchEvent中调用的,onTouch优先于onTouchEvent执行。如果在onTouch方法中通过返回true将事件消费掉,onTouchEvent将不会再执行。
另外需要注意的是,onTouch能够得到执行需要两个前提条件,第一mOnTouchListener的值不能为空,第二当前点击的控件必须是enable的。因此如果你有一个控件是非enable的,那么给它注册onTouch事件将永远得不到执行。对于这一类控件,如果我们想要监听它的touch事件,就必须通过在该控件中重写onTouchEvent方法来实现。
7、知道view点击的调用过程
8、onTouch 以及 onTouchEvent 的关系、深入
根据 view中 的 dispatchTouchEvent 源码我们可以知道outouch执行的三个必须条件。
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
1、mOnTouchListener != null,(mViewFlags & ENABLED_MASK) == ENABLED和mOnTouchListener.onTouch(this, event)这三个条件都为真,就返回true,否则就去执行onTouchEvent(event)方法并返回。
2、第二个条件(mViewFlags & ENABLED_MASK) == ENABLED是判断当前点击的控件是否是enable的,按钮默认都是enable的,因此这个条件恒定为true。
3、第三个条件就比较关键了,mOnTouchListener.onTouch(this, event),其实也就是去回调控件注册touch事件时的onTouch方法。
也就是说如果我们在onTouch方法里返回true,就会让这三个条件全部成立,从而整个方法直接返回true。如果我们在onTouch方法里返回false,就会再去执行onTouchEvent(event)方法。
所以总结说若要执行ouTouch事件,必须满足以经被设置点击事件,还有满足可点击,如 button 就是可以点击的view,imageview就是不可点击的view。第三个就是注册了这个ontouch事件的view。 如果是那些没有支持点击的我们可以选择使用ontouchevent事件来进行处理。
9、为什么onTouch执行比onclick的执行先
onclick事件执行在onTouchEvent里面
我们可以得知在我们outouch中返回true得时候,我们得onclick事件就会无法被执行到,由此我们可以判断出,onclick事件是执行在onTouchEvent事件当中的。也因此我们可知onTouch执行比onclick的执行先
在onTouchEvent中们可以找到在一个事件下执行力下列操作
public boolean performClick() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
if (mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}
现在我们就可以得知原因。
10、viewgroup的点击调用过程
根据下列源码我们得知
点击viewgroup时候,我们会先去寻找viewgroup下面的所有子view,知道找到view为当前点击的区域在进行调用child.dispatchTouchEvent(ev).
for( int i = count - 1; i >=0;i--) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
if (frame.contains(scrolledXInt, scrolledYInt)) {
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
if (child.dispatchTouchEvent(ev)) {
mMotionTarget = child;
return true;
}
}
}
}
当你点击了某个控件,首先会去调用该控件所在布局的dispatchTouchEvent方法,然后在布局的dispatchTouchEvent方法中找到被点击的相应控件,再去调用该控件的dispatchTouchEvent方法。如果我们点击了MyLayout中的按钮,会先去调用MyLayout的dispatchTouchEvent方法,可是你会发现MyLayout中并没有这个方法。那就再到它的父类LinearLayout中找一找,发现也没有这个方法。那只好继续再找LinearLayout的父类ViewGroup,你终于在ViewGroup中看到了这个方法,按钮的dispatchTouchEvent方法就是在这里调用的。修改后的示意图如下所示: button存在于mylayout当中
我们通过此也来看一下ViewGroup中的dispatchTouchEvent方法的源码,如下
public boolean dispatchTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
final float xf = ev.getX();
final float yf = ev.getY();
final float scrolledXFloat = xf + mScrollX;
final float scrolledYFloat = yf + mScrollY;
final Rect frame = mTempRect;
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (action == MotionEvent.ACTION_DOWN) {
if (mMotionTarget != null) {
mMotionTarget = null;
}
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
ev.setAction(MotionEvent.ACTION_DOWN);
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
if (frame.contains(scrolledXInt, scrolledYInt)) {
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
if (child.dispatchTouchEvent(ev)) {
mMotionTarget = child;
return true;
}
}
}
}
}
}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
final View target = mMotionTarget;
if (target == null) {
ev.setLocation(xf, yf);
if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
}
return super.dispatchTouchEvent(ev);
}
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
ev.setAction(MotionEvent.ACTION_CANCEL);
ev.setLocation(xc, yc);
if (!target.dispatchTouchEvent(ev)) {
}
mMotionTarget = null;
return true;
}
if (isUpOrCancel) {
mMotionTarget = null;
}
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setLocation(xc, yc);
if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
mMotionTarget = null;
}
return target.dispatchTouchEvent(ev);
}
可以看到一个条件判断,如果disallowIntercept和!onInterceptTouchEvent(ev)两者有一个为true,就会进入到这个条件判断中。disallowIntercept是指是否禁用掉事件拦截的功能,默认是false,就是不抑制,可以传递给子view,否则相反也可以通过调用requestDisallowInterceptTouchEvent方法对这个值进行修改。那么当第一个值为false的时候就会完全依赖第二个值来决定是否可以进入到条件判断的内部,第二个值是什么呢?竟然就是对onInterceptTouchEvent方法的返回值取反!也就是说如果我们在onInterceptTouchEvent方法中返回false,就会让第二个值为true,从而进入到条件判断的内部,如果我们在onInterceptTouchEvent方法中返回true,就会让第二个值为false,从而跳出了这个条件判断。
11、(注意)
我们都知道如果给一个控件注册了touch事件,每次点击它的时候都会触发一系列的ACTION_DOWN,ACTION_MOVE,ACTION_UP等事件。这里需要注意,如果你在执行ACTION_DOWN的时候返回了false,后面一系列其它的action就不会再得到执行了。简单的说,就是当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发后一个action。
参考着我们前面分析的源码,点击可以允许点击的控件的时候 。首先在onTouch事件里返回了false,就一定会进入到onTouchEvent方法中,然后我们来看一下onTouchEvent方法的细节。由于我们点击了按钮,就会进入到第14行这个if判断的内部,然后你会发现,不管当前的action是什么,最终都一定会走到第89行,返回一个true。
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
if ((mPrivateFlags & PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (!mHasPerformedLongPress) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
mPrivateFlags |= PRESSED;
refreshDrawableState();
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
break;
case MotionEvent.ACTION_DOWN:
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPrivateFlags |= PREPRESSED;
mHasPerformedLongPress = false;
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
break;
case MotionEvent.ACTION_CANCEL:
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
removeTapCallback();
break;
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
// Be lenient about moving outside of buttons
int slop = mTouchSlop;
if ((x < 0 - slop) || (x >= getWidth() + slop) ||
(y < 0 - slop) || (y >= getHeight() + slop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
// Need to switch from pressed to not pressed
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
}
break;
}
return true;
}
return false;
}
viewGroup与view中的dispatchTouchEvent中的区别是 viewgroup会先判断当前 上级view的阻拦事件,以及请求不阻拦事件是否有发生的前提下,在进行深入的查找当前点击的子view,根据点击位置找到子view的时候,再去执行子view的dispatchTouchEvent,然后就与view的dispatchTouchEvent的操作为相同的操作。
12、为什么我们在onTouchEvent中返回ture不会执行onclick事件呢
OnClickListener.onClick()
是在原生的View.onTouchEvent()
方法里面回调的, 你自己重写了这个方法, 而且不调用super.onTouchEvent(event)
的话当然就不会回调onClick()
方法了.
13、如果子控件被限制住了,应该如何进行使他的操作没有被限制?
根据上面源码我们可以,重写onTouchEvent ,使父控件不会限制住,更改二选一的条件,是父控件寻找子控件。
getParent().requestDisallowInterceptTouchEvent(true);
14、demo案例 解决 scrollview嵌套 listview无法滚动的情形
效果图
<?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"
android:orientation="vertical">
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="100dp"
android:gravity="center"
android:text="上部分" />
<com.example.jie.foreverdemo.Fragment.ListViewCustom
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"></com.example.jie.foreverdemo.Fragment.ListViewCustom>
<TextView
android:layout_width="match_parent"
android:layout_height="100dp"
android:gravity="center"
android:text="下部分" />
</LinearLayout>
</ScrollView>
</LinearLayout>
重写listview
package com.example.jie.foreverdemo.Fragment;
import android.content.Context;
import android.util.AttributeSet;
import android.util.EventLog;
import android.view.MotionEvent;
import android.widget.ListView;
/**
* Created by jie on 2018/10/10.
*/
public class ListViewCustom extends ListView {
public ListViewCustom(Context context) {
super(context);
}
public ListViewCustom(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ListViewCustom(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/*** @param event
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_UP:
getParent().requestDisallowInterceptTouchEvent(false);
break;
case MotionEvent.ACTION_MOVE:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_CANCEL:
getParent().requestDisallowInterceptTouchEvent(false);
break;
default:
break;
}
//执行父类原先逻辑
super.onTouchEvent(event);
return true;
}
}
重点是重写 onTouchEvent 使用 getParent().requestDisallowInterceptTouchEvent(true);来去除限制,允许找到当前控件
执行父类原先逻辑,才不会让clcik事件消失。
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_UP:
getParent().requestDisallowInterceptTouchEvent(false);
break;
case MotionEvent.ACTION_MOVE:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_CANCEL:
getParent().requestDisallowInterceptTouchEvent(false);
break;
default:
break;
}
//执行父类原先逻辑
super.onTouchEvent(event);
return true;
}
activity类
public class DispatchDemoActivity extends Activity {
@BindView(R.id.list)
ListView list;
private List<Integer> listdata = new ArrayList<Integer>();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_dispatch_demo);
ButterKnife.bind(this);
initdata();
initview();
}
private void initdata() {
for (int i = 0; i <= 20; i++) {
listdata.add(i);
}
}
private void initview() {
list.setAdapter(new ArrayAdapter<Integer>(this, android.R.layout.simple_list_item_1, listdata));
setListViewHeightBasedOnChildren(list);
}
/**
* 2* @param listView
*/
public void setListViewHeightBasedOnChildren(ListView listView) {
// 获取ListView对应的Adapter
ListAdapter listAdapter = listView.getAdapter();
if (listAdapter == null) {
return;
}
int totalHeight = 0;
for (int i = 0, len = listAdapter.getCount(); i < len; i++) {
// listAdapter.getCount()返回数据项的数目
View listItem = listAdapter.getView(i, null, listView);
// 计算子项View 的宽高
listItem.measure(0, 0);
// 统计所有子项的总高度
totalHeight += listItem.getMeasuredHeight();
}
ViewGroup.LayoutParams params = listView.getLayoutParams();
params.height = (totalHeight + (listView.getDividerHeight() * (listAdapter.getCount() - 1))) / 2;
// listView.getDividerHeight()获取子项间分隔符占用的高度
// params.height最后得到整个ListView完整显示需要的高度
listView.setLayoutParams(params);
}
}
出现此类问题的都是皆是因为父类控件中断了事件的传递给下一层的子类控件
相同的例子呢还有viewpager多重嵌套问题,解决的方法呢,也是跟我上面举例的demo是一样的
thanks!!!
参考文章:https://blog.csdn.net/guolin_blog/article/details/9097463
https://blog.csdn.net/guolin_blog/article/details/9153747
https://segmentfault.com/q/1010000007080819