一直对View的事件分发机制不太明白,在项目开发中也遇到过,在网上也找到一些解决问题方法,但是其原理并不太了解,现在辞职了有时间,今天写写View的事件分发,结合android源码一起来学习下,如果讲的不对,往指出一起学习提高,言归正传。
新建一个android项目,里面只有一个activity,有一个button,我们给Button设置setOnClickListener(),setOnTouchListener(),通过log看看结果:
btnClick.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Log.i("com.example.demo","button click "); } });
Button的Touch事件:
<span style="font-size:18px;">btnClick.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { Log.i("com.example.demo","button touch "+event.getAction()); return false; } });</span>
log:
com.example.demo(30220): button touch 0 com.example.demo(30220): button touch 2 com.example.demo(30220): button touch 2 com.example.demo(30220): button touch 2 com.example.demo(30220): button touch 2 com.example.demo(30220): button touch 2 com.example.demo(30220): button touch 1 com.example.demo(30220): button click
可以看到onTouch优先于OnClick,而且OnTouch执行了好几次,因为OnTouch事件由DWON,MOVE,UP这三部分构成,所以onTouch执行了好几次,那么为什么执行的顺序是先onTouch后onClick呢?
观察onClick和onTouch会发现onTouch()方法有返回值,默认是返回false,如果我们改为返回true,会有什么不同,点击打印log看看:
com.example.demo(3280): button touch 0 com.example.demo(3280): button touch 2 com.example.demo(3280): button touch 2 com.example.demo(3280): button touch 2 com.example.demo(3280): button touch 2 com.example.demo(3280): button touch 1
通过log结果发现Onclick事件没有执行,我们可以理解onTouch返回true时,Button的事件被消费了,就相当于把view的事件拦截了就不会再继续向下传递,因此OnClick事件没有执行,
首先知道一点,只要你触摸到了界面上的任何一个控件,就一定会调用该控件的dispatchTouchEvent方法。这个方法优先于onTouch和onClick先执行,当我们去点击按钮的时候,就会去调用Button类里的dispatchTouchEvent方法,那我们去Button源码中找这个方法,Button源码很少,没有这个方法,Button源码如下:
虽然没有这个方法,但我们看出Button继承了TextView,那就到TextView中取找,但是在TextView中并没有找到dispatchTouchEvent()方法,那就只能找TextView的父类了,而TextView的父类就是View对象了,那在View源码中找dispatchTouchEvent()方法看看它执行逻辑:
我们首先翻译下这个方法的说明:
@param event The motion event to be dispatched,事件动作事件派遣
@return True
if the event was handled by the view, false otherwise.如果这个事件被处理了就返回true,否则会返回false,
现在看下dispatchTouchEvent()方法里的代码,看源码得有个方法,不是所有的代码都要看懂,
在dispatchTouchEvent()方法中重点是看
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) { return true; } if (onTouchEvent(event)) { return true; }
首先看第一个if语句,
mOnTouchListener变量在什么时候初始化呢?我们追踪下,发现它的初始化时在
<pre name="code" class="java"> public void setOnTouchListener(OnTouchListener l) { mOnTouchListener = l; }
因此这个是我们在setOntouchListener的时候,mOnTouchListener就可以赋值了,因此这个变量不会为null,
第二个条件(mViewFlags & ENABLED_MASK) == ENABLED是判断当前点击的控件是否是enable的,按钮默认都是enable的,因此这个条件恒定为true
if条件的第三个条件mOnTouchListener.onTouch(this, event)我们点击onTouch()方法里发现:
public interface OnTouchListener { /** * Called when a touch event is dispatched to a view. This allows listeners to * get a chance to respond before the target view. * * @param v The view the touch event has been dispatched to. * @param event The MotionEvent object containing full information about * the event. * @return True if the listener has consumed the event, false otherwise. */ boolean onTouch(View v, MotionEvent event); }
发现它是一个接口中的方法,用于回调的,其实就是我们设置onTouchListener方法的返回值,
而我们返回的是true,因此这个if条件判断返回的是true,那么就不会执行下面的语句了
if (onTouchEvent(event)) { return true; }
我们看看onTouchEvent(event)方法的逻辑:源码如下:
public boolean onTouchEvent(MotionEvent event) { final int viewFlags = mViewFlags; if ((viewFlags & ENABLED_MASK) == DISABLED) { if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PRESSED) != 0) { mPrivateFlags &= ~PRESSED; refreshDrawableState(); } // 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 (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. mPrivateFlags |= PRESSED; refreshDrawableState(); } 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) { postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); } else if (!post(mUnsetPressedState)) { // If the post failed, unpress right now mUnsetPressedState.run(); } removeTapCallback(); } 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 |= PREPRESSED; if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // Not inside a scrolling container, so show the feedback right away mPrivateFlags |= PRESSED; refreshDrawableState(); checkForLongClick(0); } 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 if (!pointInView(x, y, mTouchSlop)) { // 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; }
我们发现这方法里面的源码太多了,但是onTouchEvent()方法其实我们只要看case MotionEvent.ACTION_UP里面的代码,因为我们手触摸到最后倒是要执行这里,在经过种种判断之后,代码会走到这里:
if (!post(mPerformClick)) { performClick(); }
然后执行performClick()方法,performClick方法里面的代码:
public boolean performClick() { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); if (mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); mOnClickListener.onClick(this); return true; } return false; }
首先看if条件,mOnClickListener这个变量就是我们点击的时候设置的,因此不会为null,然后我们看一个重要的方法,也是回调方法,
mOnClickListener.onClick(this);
这就是设置view的点击事件,通过源码我们现在应该明白了最初我们设置onClick和onTouch事件的传递顺序,
总结:
1:如果view对象setOnTouchListener方法返回true,那么view对象就不会执行click事件,如果setTouchListener设置为false,view才会执行click事件,
还有一个重要的知识,我们知道touch事件由DWON,MOVE,UP组成,如下:
btnClick.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { if(event.getAction()==MotionEvent.ACTION_DOWN){ Log.i("com.example.demo","ACTION_DOWN"); return false; }else if(event.getAction()==MotionEvent.ACTION_MOVE){ Log.i("com.example.demo","ACTION_MOVE"); }else{ Log.i("com.example.demo","ACTION_UP"); } return false; } });
运行结果:
晚上继续