以一个需求为例浅谈对事件分发机制的理解
Posted 隔壁小王66
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了以一个需求为例浅谈对事件分发机制的理解相关的知识,希望对你有一定的参考价值。
最近看了一些事件分发机制的文章,觉得有必要拿项目中的一些实例,来阐述一下对事件分发机制的理解,增强记忆。
首先看需求
这里由于需要向上滑动展示出一个titlebar的效果,所以我采用了CoordinatorLayout+AppBarLayout+CollapsingToolbarLayout+ViewPager的一个方案。
首先,通过问题来阐述事件分发
问题1:titlebar的样式并不好用toolbar来实现
解决这个问题,我们先看一下CollapsingToolbarLayout,因为CollapsingToolbarLayout是toolbar的父布局
看一下CollapsingToolbarLayout的源码我们可以发现,CollapsingToolbarLayout实际是framelayout的子类,那么我们可以使toolbar透明,在放入一个titlebar的布局加以解决。
运行效果确实解决了这个问题,但是引申出另外一个问题,我们需要在titlebar中添加点击事件,如何让titlebar响应,而不是toolbar呢?这就引入了本文的主题,事件分发
首先我们来看布局
当我们设置titlebar的点击事件时,我们发现titlebar并没有响应,应该是被toolbar消費了,我们来看一下demo测试实例
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<LinearLayout
android:id="@+id/ll"
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#f00"
android:orientation="vertical"
android:layout_gravity="center"/>
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#ff0"
android:layout_gravity="center"/>
</FrameLayout>
findViewById(R.id.toolbar).setOnTouchListener(new View.OnTouchListener()
@Override
public boolean onTouch(View v, MotionEvent event)
return false;
);
findViewById(R.id.ll).setOnClickListener(new View.OnClickListener()
@Override
public void onClick(View v)
Log.i("TAG", "OnClickListener--onClick--"+v);
);
效果:
我们设置setOnTouchListener中的onTouch方法返回false,点击黄色区域,事件还是被消费了,为什么呢?这里我们需要了解一下分发机制消费的优先级
设置了setOnTouchListener的onTouch方法返回fasle,则表示不处理,交给onTouchEvent处理,onTouchEvent返回false,如果有子view,则继续分发,如果没有,则返回父布局的onTouchEvent。
这里设置onTouch方法返回false事件还是被toolbar消费了,那么只有一个原因toolbar中重写了onTouchEvent,而且返回true消费了此事件
看一下toolbar源码,这里稍稍理解阅读源码的重要性:
toolbar确实重写了onTouchEvent方法,并且默认消费返回true,凉凉。
这就某得法了,这时候回顾一下viewgroup的事件分发流程
viewgroup调用dispatchTouchEvent方法进行分发,其内部会调用onInterceptTouchEvent判断是否拦截,返回false不拦截则遍历子view,调用子view的dispatchTouchEvent分发方法,返回true,则代表拦截,调用viewgroup的onTouchEvent方法判断是否消费,onTouchEvent返回true,则消费,返回false,则返回上一层,直到activity消费此事件。
上面也提到了,如果设置setOnTouchListener监听,则onTouch方法优先级高于onTouchEvent。
要解决此问题,就需要在
findViewById(R.id.toolbar).setOnTouchListener(new View.OnTouchListener()
@Override
public boolean onTouch(View v, MotionEvent event)
return false;
);
返回false之前,让ll布局来消费此次事件。
private View.OnTouchListener mToolbarOnTouchListener = new View.OnTouchListener()
@Override
public boolean onTouch(View v, MotionEvent event)
touchIndex = event.getRawX();
return rootView.findViewById(R.id.frame).dispatchTouchEvent(event);
;
这里在返回false之前,调用同级布局frame的dispatchTouchEvent分发方法,将事件流程篡改,正常流程是遍历子view,当前处理完,在遍历其他字view,现在的逻辑是在处理当前子view的时候调用其他子view的分发方法,就修改了消费事件的先后顺序。从而圆满解决此问题。
在去调用
rootView.findViewById(R.id.frame).setOnClickListener(new View.OnClickListener()
@Override
public void onClick(View v)
int[] location = new int[2];
fraAdd.getLocationOnScreen(location);//获取在整个屏幕内的绝对坐标
if (touchIndex < location[0])
Intent intent1 = new Intent(getActivity(), MajorSelectDialogActivity.class);
getActivity().startActivity(intent1);
else
addCourse();
);
就可以解決了。
这里可能会有疑问,frame的view在分发的时候,我们没有去写onTouchEvent方法或者去设置OnTouchListener 监听,那么他是如何消费此事件的???
首先验证,我们分别监听OnTouchListener ,OnClickListener ,并且onTouch方法返回true消费事件
findViewById(R.id.toolbar).setOnTouchListener(new View.OnTouchListener()
@Override
public boolean onTouch(View v, MotionEvent event)
return findViewById(R.id.ll).dispatchTouchEvent(event);
);
findViewById(R.id.ll).setOnClickListener(new View.OnClickListener()
@Override
public void onClick(View v)
Log.i("TAG", "OnClickListener--onClick--"+v);
);
findViewById(R.id.ll).setOnTouchListener(new View.OnTouchListener()
@Override
public boolean onTouch(View v, MotionEvent event)
Log.i("TAG", "OnTouchListener--onTouch--"+v);
return true;
);
点击效果,只走onTouch,不走onclick,改为onTouch返回false,不消费事件,先走onTouch,后走onclick。why????
总结出两点:
1:ontouch 不管返回啥都是先与onClick执行
2:onTouch返回true,onClick不执行,返回false,onClick后于onTouch执行
我们看一下分发事件的部分源码
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event))
result = true;
if (!result && onTouchEvent(event))
result = true;
ListenerInfo 对象包含了一系列监听对象,包括setOnTouchListener,OnClickListener ,设置setOnTouchListener后li.mOnTouchListener不等于空,并调用onTouch方法消费事件,result等于true,则不会走下面的onTouchEvent方法,若是没有设置setOnTouchListener,则li.mOnTouchListener == null,就会走 if (!result && onTouchEvent(event)) ,从而调用onTouchEvent方法,
由此上面的日志输出优先级以及这部分来判断,onClick方法必定在onTouchEvent方法中执行。
我们看一下view中onTouchEvent的源码:
public boolean onTouchEvent(MotionEvent event)
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED)
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0)
setPressed(false);
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
if (mTouchDelegate != null)
if (mTouchDelegate.onTouchEvent(event))
return true;
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP)
switch (action)
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP)
handleTooltipUp();
if (!clickable)
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_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 (prepressed)
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent)
// 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))
performClickInternal();
if (mUnsetPressedState == null)
mUnsetPressedState = new UnsetPressedState();
if (prepressed)
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
else if (!post(mUnsetPressedState))
// If the post failed, unpress right now
mUnsetPressedState.run();
removeTapCallback();
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN)
mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
mHasPerformedLongPress = false;
if (!clickable)
checkForLongClick(0, x, y);
break;
if (performButtonActionOnTouchDown(event))
break;
// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if (isInScrollingContainer)
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null)
mPendingCheckForTap = new CheckForTap();
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
else
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0, x, y);
break;
case MotionEvent.ACTION_CANCEL:
if (clickable)
setPressed(false);
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
break;
case MotionEvent.ACTION_MOVE:
if (clickable)
drawableHotspotChanged(x, y);
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop))
// Outside button
// Remove any future long press/tap checks
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0)
setPressed(false);
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
break;
return true;
return false;
首先clickable 赋值,当我们设置setOnClickListener时,就会setClickable(true)
参考setOnClickListener,setOnClickListener的源码中
public void setOnClickListener(@Nullable OnClickListener l)
if (!isClickable())
setClickable(true);
getListenerInfo().mOnClickListener = l;
我们会发现,第一步:它会设置setClickable(true);,setClickable的源码中
public void setClickable(boolean clickable)
setFlags(clickable ? CLICKABLE : 0, CLICKABLE);
实际上给CLICKABLE 设置了标志位
第二步
给listenerInfo的mOnClickListener 设置监听对象 getListenerInfo().mOnClickListener = l;
走到这里,基本上可以看出设置了mOnClickListener 方法的onTouchEvent中的boolean clickable为true,
DISABLED参数是有什么决定的呢?由setEnable决定,与setClickable类似。我们看一下源码中对其的描述
// A disabled view that is clickable still consumes the touch
// events, it just doesn’t respond to them.
意思是可点击的已禁用视图仍会消耗触摸事件,它只是没有响应它们。view的setClickable方法不会影响 touch事件的触发,仍然会走touch事件,设置enable为false则不会消耗touch事件
基于此, if ((viewFlags & ENABLED_MASK) == DISABLED) 肯定是false,因为我们并没有设置setEnable,如果设置setEnable(false),则会进入DISABLED而非ENABLED
if ((viewFlags & ENABLED_MASK) == DISABLED)
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0)
setPressed(false);
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
返回 return clickable;则不会进入点击事件
由于本例没有设置setEnable,所以不会进入此判断。
下一个判断
if (mTouchDelegate != null)
if (mTouchDelegate.onTouchEvent(event))
return true;
由于setTouchDelegate只有这几个view会设置,本例并没有设置,所以mTouchDelegate ==null,并不会进入此判断
则进入switch中
if (!clickable)
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
setClickable(false)才走,不考虑next,真正调用onClick方法是在
if (!post(mPerformClick))
performClickInternal();
performClickInternal源码:
private boolean performClickInternal()
// Must notify autofill manager before performing the click actions to avoid scenarios where
// the app has a click listener that changes the state of views the autofill service might
// be interested on.
notifyAutofillManagerOnClick();
return performClick();
performClick源码:
public boolean performClick()
// We still need to call this method to handle the cases where performClick() was called
// externally, instead of through performClickInternal()
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null)
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
else
result = false;
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
可以看到 li.mOnClickListener.onClick(this);, li.mOnClickListener是在setOnClickListener中设置,所以不会为空,至此就解释了为啥只调用setOnClickListener就消费了触摸事件,并且为啥onClick方法在ontouch方法后面执行的原因。
创作打卡挑战赛 赢取流量/现金/CSDN周边激励大奖以上是关于以一个需求为例浅谈对事件分发机制的理解的主要内容,如果未能解决你的问题,请参考以下文章