说明
本篇所讲的View不包括Viewgroup
事件分发在解决滑动冲突的时候很重要。如果想做出完美的自定义控件。事件分发的掌握是必须的。
例子
我们继承Button写一个自定义的CustomButton,复写事件分发相关的方法,并打上log
public class CustomButton extends Button {
private static final String TAG = CustomButton.class.getSimpleName();
public CustomButton(Context context) {
super(context);
}
public CustomButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action)
{
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "dispatchTouchEvent ACTION_UP");
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action)
{
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "onTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "onTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "onTouchEvent ACTION_UP");
break;
default:
break;
}
return super.onTouchEvent(event);
}
}
然后添加到Activity中
public class Main3Activity extends AppCompatActivity{
private static final String TAG = Main3Activity.class.getSimpleName();
private CustomButton customButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main3);
customButton = (CustomButton)findViewById(R.id.cbtn);
customButton.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
int action = event.getAction();
switch (action){
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "onTouch ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "onTouch ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "onTouch ACTION_UP");
break;
}
return false;
}
});
}
}
点击button并稍微移动一下可以看到如下log:
07-31 10:38:57.710 31587-31587/com.fcott.customview E/CustomButton: dispatchTouchEvent ACTION_DOWN
07-31 10:38:57.711 31587-31587/com.fcott.customview E/Main3Activity: onTouch ACTION_DOWN
07-31 10:38:57.711 31587-31587/com.fcott.customview E/CustomButton: onTouchEvent ACTION_DOWN
07-31 10:38:57.906 31587-31587/com.fcott.customview E/CustomButton: dispatchTouchEvent ACTION_MOVE
07-31 10:38:57.906 31587-31587/com.fcott.customview E/Main3Activity: onTouch ACTION_MOVE
07-31 10:38:57.906 31587-31587/com.fcott.customview E/CustomButton: onTouchEvent ACTION_MOVE
07-31 10:38:57.906 31587-31587/com.fcott.customview E/CustomButton: dispatchTouchEvent ACTION_UP
07-31 10:38:57.906 31587-31587/com.fcott.customview E/Main3Activity: onTouch ACTION_UP
07-31 10:38:57.906 31587-31587/com.fcott.customview E/CustomButton: onTouchEvent ACTION_UP
执行顺序一目了然,按照顺序来一一了解
dispatchTouchEvent
下面是dispatchTouchEvent方法的源码
public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
if (event.isTargetAccessibilityFocus()) {
// We don't have focus or no virtual descendant has it, do not handle the event.
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// We have focus and got the event, then use normal event dispatch.
event.setTargetAccessibilityFocus(false);
}
boolean result = false;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
//未被其他窗口遮盖
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
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;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
我们只看重要部分,result 它用来标识接收事件的 view 是否消耗了当前事件。
//未被其他窗口遮盖
if (onFilterTouchEventForSecurity(event)) {
//判断控件是否可用以及当前这个事件是否为滚动条拖动,不必关心
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
//这里的 li.mOnTouchListener就是我们调用setOnTouchListener设置进去的listener
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;
}
}
由上我们可以看出,onTouchEvent和我们设置的onTouch事件都是在dispatchTouchEvent内部调用的。
首先判断li和li.mOnTouchListener不为null(li是view管理所有监听事件的一个类),并且view是enable的状态,然后 mOnTouchListener.onTouch(this, event)返回true,这三个条件如果都满足,直接result为true ; 也就是下面的onTouchEvent(event)不会被执行了;
也就是说:如果我们设置了setOnTouchListener,并且return true,那么View自己的onTouchEvent就不会被执行了
接下来看看onTouchEvent这东西
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();
//如果当前view处于不可用状态依然会消耗这个事件,只是不会有任何回应。
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// 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)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
//如果设置了代理,则执行代理的onTouchEvent事件。(代理的作用是可以扩大view的反应区域而不改变view本身大小)
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
//重点,如果我们的View可以点击或者可以长按,则最终一定return true ;
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
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)) {
performClick();
}
}
}
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:
mHasPerformedLongPress = false;
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:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
setPressed(false);
}
}
break;
}
return true;
}
return false;
}
上面的关键部分我用注释解释了一下大概的作用,接下来着重看一下重点部分
MotionEvent.ACTION_DOWN
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
//表示鼠标右击 不用管
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;
先设置mHasPerformedLongPress=false;表示长按事件还未触发;
然后判读isInScrollingContainer(),isInScrollingContainer的注释已经写得很明白了,就是遍历整个View树来判断当前的View是不是在一个滚动的容器中.因为对于触碰事件的处理,不能把滑动当成点击.所以先判断是不是在一个可滑动的容器中.
如果不在滑动容器中
执行
setPressed(true, x, y);
checkForLongClick(0, x, y);
说明:
PFLAG_PREPRESSED 表示在一个可滚动的容器中,要稍后才能确定是按下还是滚动.
PFLAG_PRESSED 已经可以确定按下这一操作.
setPressed(true, x, y)的主要作用是给mPrivateFlags设置一个PRESSED的标识表示已经按下,并刷新背景
checkForLongClick(0, x, y)的作用是检测是否是长点击事件
private void checkForLongClick(int delayOffset) {
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();
}
mPendingCheckForLongPress.rememberWindowAttachCount();
postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout() - delayOffset);
}
}
发送一个延迟为ViewConfiguration.getTapTimeout() - delayOffset(500- 0ms)的延迟消息,到达延时时间后会执行CheckForLongPress()里面的run方法.
class CheckForLongPress implements Runnable {
private int mOriginalWindowAttachCount;
public void run() {
if (isPressed() && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
if (performLongClick()) {
mHasPerformedLongPress = true;
}
}
}
public void rememberWindowAttachCount() {
mOriginalWindowAttachCount = mWindowAttachCount;
}
}
因为等待形成长按的过程中,界面可能发生变化如Activity的pause及restart,这个时候,长按应当失效.
View中提供了mWindowAttachCount来记录View的attach次数.当检查长按时的attach次数与长按到形成时.
的attach一样则处理,否则就不应该再当前长按. 所以在将检查长按的消息添加时队伍的时候,要记录下当前的windowAttachCount.
如果在滑动容器中
1.给mPrivateFlags设置一个PREPRESSED的标识
2.发送一个延迟为ViewConfiguration.getTapTimeout()(100毫秒)的延迟消息,到达延时时间后会执行CheckForTap()里面的run方法.
说明:
在给定的tapTimeout(100ms)时间之内,用户的触摸没有移动,就当作用户是想点击,而不是滑动.
具体的做法是,将 CheckForTap的实例mPendingCheckForTap添加时消息队例中,延迟执行.
如果在这tagTimeout之间用户触摸移动了,则删除此消息.否则:执行按下状态.然后检查长按.
CheckForTap
private final class CheckForTap implements Runnable {
public float x;
public float y;
@Override
public void run() {
mPrivateFlags &= ~PFLAG_PREPRESSED;
setPressed(true, x, y);
checkForLongClick(ViewConfiguration.getTapTimeout(), x, y);
}
}
在run方法里面取消mPrivateFlags的PREPRESSED,执行setPressed(true, x, y);方法和checkForLongClick(ViewConfiguration.getTapTimeout(), x, y);
和上面不在滑动容器中的情况一样,唯一的区别是这里的delayOffset时间不是0ms,而是100ms,因为在判断滑动还是按下的时候已经消耗了100ms.
我们来总结一下上面的分析,可以看到,当用户按下,首先会判断是否在滑动容器中,如果在滑动容器中则设置标识为PREPRESSED,如果100ms后,没有抬起,会将View的PREPRESSED标识去掉并且设置为PRESSED,然后发出一个检测长按的延迟任务,延时为:ViewConfiguration.getLongPressTimeout() - 100ms,这个100ms刚好时检测额PREPRESSED时间;也就是用户从DOWN触发开始算起,如果500ms内没有抬起则认为触发了长按事件:
1、如果此时设置了长按的回调,则执行长按时的回调,且如果长按的回调返回true;才把mHasPerformedLongPress置为ture;
2、否则,如果没有设置长按回调或者长按回调返回的是false;则mHasPerformedLongPress依然是false;
MotionEvent.ACTION_MOVE
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
setPressed(false);
}
}
break;
判断手指是否移出view,如果移出则:
1、执行removeTapCallback();
2、然后判断是否包含PRESSED标识,如果包含,移除长按的检查:removeLongPressCallback();
3、最后把mPrivateFlags中PRESSED标识去除,刷新背景;
MotionEvent.ACTION_UP
case MotionEvent.ACTION_UP:
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;
//看是否需要获得焦点及用变量focusTaken设置是否获得了焦点.
//如果我们还没有获得焦点,但是我们在触控屏下又可以获得焦点,那么则请求获得焦点.
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
//如果手指触摸屏幕不到100ms就放开,设置为按下
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);
}
// 如果mHasPerformedLongPress 返回false,并且不忽略本次up事件
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.
//如果mPerformClick为null,初始化一个实例
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
//立即通过handler添加到消息队列尾部,如果添加失败则直接执行 performClick()
//总之无论如都会执行performClick();
if (!post(mPerformClick)) {
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
//如果触摸未满100ms就放开,则延迟64ms执行mUnsetPressedState
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
}
//如果触摸满100ms,立即通过handler添加到消息队列尾部
//如果添加失败则直接执行 mUnsetPressedState.run();
else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
上面关键部分已经用注释说明。需要特别说明的是:
1.performClick()
public boolean performClick() {
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);
return result;
}
可以看到,performClick()执行的就是我们调用setOnclickListener设置的点击监听。说明onClick是在MotionEvent.ACTION_UP里执行的。
2.mUnsetPressedState
private final class UnsetPressedState implements Runnable {
@Override
public void run() {
setPressed(false);
}
}
只有一句代码,把我们的mPrivateFlags中的PRESSED取消,然后刷新背景,把setPress转发下去。
3.mHasPerformedLongPress
我们在MotionEvent.ACTION_DOWN的时候获取了一个变量mHasPerformedLongPress,上面没讲这个东西是干嘛的。这里可以看到:
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.
//如果mPerformClick为null,初始化一个实例
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
//立即通过handler添加到消息队列尾部,如果添加失败则直接执行 performClick()
//总之无论如都会执行performClick();
if (!post(mPerformClick)) {
performClick();
}
}
}
只有当mHasPerformedLongPress 为false的时候,我们才能触发performClick()事件。
也就是说,如果设置了onLongClickListener,且onLongClickListener.onClick返回true,则点击事件OnClick事件无法触发;
如果没有设置onLongClickListener或者onLongClickListener.onClick返回false,则点击事件OnClick事件依然可以触发;
总结
1、整个View的事件转发流程是:
View.dispatchEvent->View.setOnTouchListener->View.onTouchEvent
在dispatchTouchEvent中会进行OnTouchListener的判断,如果OnTouchListener不为null且返回true,则表示事件被消费,onTouchEvent不会被执行;否则执行onTouchEvent。
2、onTouchEvent中的DOWN,MOVE,UP
DOWN时(只讲在滑动容器内,如果不在滑动容器内,则跳过a部分即可):
a、首先设置mHasPerformedLongPress=false;然后设置标志为PREPRESSED ,最后发出一个100ms后的mPendingCheckForTap;
b、如果100ms内没有触发UP,则将标志置为PRESSED,清除PREPRESSED标志,同时发出一个延时为500-100ms的,检测长按任务消息;
c、如果按下500ms后(从DOWN触发开始算),则会触发LongClickListener:
此时如果LongClickListener不为null,则会执行回调,同时如果LongClickListener.onClick返回true,才把mHasPerformedLongPress设置为true;否则mHasPerformedLongPress依然为false;
MOVE时:
主要就是检测用户是否划出控件,如果划出了:
100ms内,直接移除mPendingCheckForTap;
100ms后,则将标志中的PRESSED去除,同时移除长按的检查:removeLongPressCallback();
UP时:
a、如果100ms内,触发UP,此时标志为PREPRESSED,先设置setPressed(true, x, y)确定按下操作,再执行UnsetPressedState,setPressed(false);会把setPress转发下去,可以在View中复写dispatchSetPressed方法接收;
b、如果是100ms-500ms间(mHasPerformedLongPress一定为false),即长按还未发生,则首先移除长按检测,执行onClick回调;
c、如果是500ms以后,那么有两种情况:
i.设置了onLongClickListener,且onLongClickListener.onClick返回true,则点击事件OnClick事件无法触发;
ii.没有设置onLongClickListener或者onLongClickListener.onClick返回false,则点击事件OnClick事件依然可以触发;