首页 > 代码库 > Android 触摸事件 点击事件的分发机制 详解

Android 触摸事件 点击事件的分发机制 详解

最近发现团队里有些员工在做一些自定义控件的时候感觉比较吃力。尤其是做触摸事件这种东西的时候。很多人对机制并不理解。因为百度出来的东西都太理论化了。确实不好理解。

今天带大家坐几个小demo。帮助理解一下。

先从简单的view 的事件分发机制开始解释。

我们首先自定义一个工程

package com.example.testtouch;import android.app.Activity;import android.os.Bundle;import android.util.Log;import android.view.MotionEvent;import android.view.View;import android.view.View.OnClickListener;import android.view.View.OnTouchListener;import android.widget.ImageView;import android.widget.TextView;public class MainActivity extends Activity {    private TextView tv;    private ImageView iv;    @Override    protected void onCreate(Bundle savedsInstanceState) {        super.onCreate(savedsInstanceState);        setContentView(R.layout.activity_main);        tv = (TextView) this.findViewById(R.id.tv);        iv = (ImageView) this.findViewById(R.id.iv);        iv.setOnTouchListener(new OnTouchListener() {            @Override            public boolean onTouch(View v, MotionEvent event) {                if (event.getAction() == MotionEvent.ACTION_DOWN) {                    Log.v("test", "iv event down ==" + MotionEvent.ACTION_DOWN);                }                if (event.getAction() == MotionEvent.ACTION_UP) {                    Log.v("test", "iv event up ==" + MotionEvent.ACTION_UP);                }                return false;            }        });        tv.setOnTouchListener(new OnTouchListener() {            @Override            public boolean onTouch(View v, MotionEvent event) {                // TODO Auto-generated method stub                if (event.getAction() == MotionEvent.ACTION_DOWN) {                    Log.v("test", "tv event down ==" + MotionEvent.ACTION_DOWN);                }                if (event.getAction() == MotionEvent.ACTION_UP) {                    Log.v("test", "tv event up ==" + MotionEvent.ACTION_UP);                }                return false;            }        });        tv.setOnClickListener(new OnClickListener() {            @Override            public void onClick(View v) {                // TODO Auto-generated method stub                Log.v("test", "on click");            }        });    }}

然后运行起来以后 点击一下那个textview。

 

输出日志如下:

 

10-31 02:54:40.456: V/test(1430): tv event down ==0
10-31 02:54:40.523: V/test(1430): tv event up ==1
10-31 02:54:40.526: V/test(1430): on click

 

可以看出来 ontouch事件是在onclick事件以前被调用的。

如果我们把 ontouch事件的返回值 更改为true的话。那么就会发现 onclick 事件就不会执行了。

 

可以理解成 如果ontouch事件 返回的值为true的话 那么剩下的触摸操作全部都被拦截了。

 

换句话说 只要这个方法返回值 为true 那么剩下的操作就全部都没有了。

 

我们可以看源代码 去验证一下。首先我们得知道一个前提。 那就是view的事件分发都是由 dispatchTouchEvent

这个方法来控制的,这个方法 在view的类里面定义 我们去看一下源代码。

 

/**     * Pass the touch screen motion event down to the target view, or this     * view if it is the target.     *     * @param event The motion event to be dispatched.     * @return True if the event was handled by the view, false otherwise.     */    public boolean dispatchTouchEvent(MotionEvent event) {        if (mInputEventConsistencyVerifier != null) {            mInputEventConsistencyVerifier.onTouchEvent(event, 0);        }        if (onFilterTouchEventForSecurity(event)) {            //noinspection SimplifiableIfStatement            if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&                    mOnTouchListener.onTouch(this, event)) {                return true;            }            if (onTouchEvent(event)) {                return true;            }        }        if (mInputEventConsistencyVerifier != null) {            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);        }        return false;    }

主要看这个部分

if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}

if (onTouchEvent(event)) {
return true;
}

 

如果mOnTouchListener  这个不为空 并且 (mViewFlags & ENABLED_MASK) == ENABLED(代表这个控件可以被点击)

并且mOnTouchListener.onTouch(this, event) 这个也返回true  那么dispatchTouchEvent就返回true了。

否则就执行 onTouchEvent(event) 这个方法 并且返回true。

 

分析完这个 就比较好理解了,这边是先调用的OnTouchListener.onTouch(this, event)  这个事件,所以我们能看到log输出 ontouch事件

 

是在onclick时间之前的。并且这个方法 如果返回false  才会走到onTouchEvent 这个事件里面去。如果返回true 那么dispatchTouchEvent

 

这个函数就直接返回了。onTouchEvent  就永远不会得到执行。这也就解释了为什么我们如果把onTouch事件的值设置成true ,onClick方法

就不会得到执行。

当然也可以看出来 oncLIClick事件是在onTouchEvent里执行的。哪我们继续看源代码onTouchEvent 是怎么做的。

 

  1 /**  2      * Implement this method to handle touch screen motion events.  3      *  4      * @param event The motion event.  5      * @return True if the event was handled, false otherwise.  6      */  7     public boolean onTouchEvent(MotionEvent event) {  8         final int viewFlags = mViewFlags;  9  10         if ((viewFlags & ENABLED_MASK) == DISABLED) { 11             if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PRESSED) != 0) { 12                 mPrivateFlags &= ~PRESSED; 13                 refreshDrawableState(); 14             } 15             // A disabled view that is clickable still consumes the touch 16             // events, it just doesn‘t respond to them. 17             return (((viewFlags & CLICKABLE) == CLICKABLE || 18                     (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)); 19         } 20  21         if (mTouchDelegate != null) { 22             if (mTouchDelegate.onTouchEvent(event)) { 23                 return true; 24             } 25         } 26  27         if (((viewFlags & CLICKABLE) == CLICKABLE || 28                 (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { 29             switch (event.getAction()) { 30                 case MotionEvent.ACTION_UP: 31                     boolean prepressed = (mPrivateFlags & PREPRESSED) != 0; 32                     if ((mPrivateFlags & PRESSED) != 0 || prepressed) { 33                         // take focus if we don‘t have it already and we should in 34                         // touch mode. 35                         boolean focusTaken = false; 36                         if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { 37                             focusTaken = requestFocus(); 38                         } 39  40                         if (prepressed) { 41                             // The button is being released before we actually 42                             // showed it as pressed.  Make it show the pressed 43                             // state now (before scheduling the click) to ensure 44                             // the user sees it. 45                             mPrivateFlags |= PRESSED; 46                             refreshDrawableState(); 47                        } 48  49                         if (!mHasPerformedLongPress) { 50                             // This is a tap, so remove the longpress check 51                             removeLongPressCallback(); 52  53                             // Only perform take click actions if we were in the pressed state 54                             if (!focusTaken) { 55                                 // Use a Runnable and post this rather than calling 56                                 // performClick directly. This lets other visual state 57                                 // of the view update before click actions start. 58                                 if (mPerformClick == null) { 59                                     mPerformClick = new PerformClick(); 60                                 } 61                                 if (!post(mPerformClick)) { 62                                     performClick(); 63                                 } 64                             } 65                         } 66  67                         if (mUnsetPressedState == null) { 68                             mUnsetPressedState = new UnsetPressedState(); 69                         } 70  71                         if (prepressed) { 72                             postDelayed(mUnsetPressedState, 73                                     ViewConfiguration.getPressedStateDuration()); 74                         } else if (!post(mUnsetPressedState)) { 75                             // If the post failed, unpress right now 76                             mUnsetPressedState.run(); 77                         } 78                         removeTapCallback(); 79                     } 80                     break; 81  82                 case MotionEvent.ACTION_DOWN: 83                     mHasPerformedLongPress = false; 84  85                     if (performButtonActionOnTouchDown(event)) { 86                         break; 87                     } 88  89                     // Walk up the hierarchy to determine if we‘re inside a scrolling container. 90                     boolean isInScrollingContainer = isInScrollingContainer(); 91  92                     // For views inside a scrolling container, delay the pressed feedback for 93                     // a short period in case this is a scroll. 94                     if (isInScrollingContainer) { 95                         mPrivateFlags |= PREPRESSED; 96                         if (mPendingCheckForTap == null) { 97                             mPendingCheckForTap = new CheckForTap(); 98                         } 99                         postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());100                     } else {101                         // Not inside a scrolling container, so show the feedback right away102                         mPrivateFlags |= PRESSED;103                         refreshDrawableState();104                         checkForLongClick(0);105                     }106                     break;107 108                 case MotionEvent.ACTION_CANCEL:109                     mPrivateFlags &= ~PRESSED;110                     refreshDrawableState();111                     removeTapCallback();112                     break;113 114                 case MotionEvent.ACTION_MOVE:115                     final int x = (int) event.getX();116                     final int y = (int) event.getY();117 118                     // Be lenient about moving outside of buttons119                     if (!pointInView(x, y, mTouchSlop)) {120                         // Outside button121                         removeTapCallback();122                         if ((mPrivateFlags & PRESSED) != 0) {123                             // Remove any future long press/tap checks124                             removeLongPressCallback();125 126                             // Need to switch from pressed to not pressed127                             mPrivateFlags &= ~PRESSED;128                             refreshDrawableState();129                         }130                     }131                     break;132             }133             return true;134         }135 136         return false;137     }

 

 

看62行 有一个   performClick() 我们去看看这个方法。

 /**     * Call this view‘s OnClickListener, if it is defined.     *     * @return True there was an assigned OnClickListener that was called, false     *         otherwise is returned.     */    public boolean performClick() {        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);        if (mOnClickListener != null) {            playSoundEffect(SoundEffectConstants.CLICK);            mOnClickListener.onClick(this);            return true;        }        return false;    }

 

 看到没,我们的点击事件回调就是在这里做的!

 

回过头来 我们再看看 onTouchEvent 那个switch语句,我们会发现 最终 他们的返回值 都是true!!! 无论是什么事件 都是return true。

然后回过头来 看  dispatchTouchEvent 这个方法。

前面两行代码。

if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}

大家都知道 触摸事件是由dispatchTouchEvent  这个函数去进行分发的。无论是什么触摸事件,都是手指先点下去 也就是 action down事件。action down的值是0.

 

所以你看 这边的代码意思就是如果action down的返回事件是true 那就分发。如果action down的返回onTouchEvent都是false的话 哪后面的事件就全部拦截了。

 

可以理解成。对于dispatchTouchEvent 来说 如果ACTION_DOWN事件返回true,就说明它需要处理这个事件,就让它接收所有的触屏事件,否则,说明它不用处理,也就不让它接收后续的触屏事件了。

 

Android 触摸事件 点击事件的分发机制 详解