Android 触屏事件 OnTouch onClick onTouchEvent对于触屏事件的处理和分发
做项目的时候经常遇到需要事件分发,很多时候我们发现当我们触发了onTouch却触发不了onClick。或者触发了View的事件却触发不了ViewGroup的事件。那么他们之间到底是什么关系呢,其实最终他们涉及的只是两个问题
OnTouch 、onClick 、onTouchEvent 之间的关系
OnTouch 、onClick 、onTouchEvent 之间的处理顺序
这里,我做了简单的例子来看看他们之间的关系。
<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:background="#FFFFFF"> <com.example.empty.MyFrameLayout android:id="@+id/mFrame" android:layout_width="300dp" android:layout_gravity="center" android:layout_height="300dp" android:background="#00FFFF" > <com.example.empty.MyImageView android:id="@+id/mImage" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/image_128" android:background="#00FF00" android:layout_gravity="center" /> </com.example.empty.MyFrameLayout> </FrameLayout>
package com.example.empty; import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnTouchListener; import android.view.MotionEvent; import android.widget.FrameLayout; public class MyFrameLayout extends FrameLayout implements OnClickListener,OnTouchListener{ private static final String TAG = "Event"; public MyFrameLayout(Context context, AttributeSet attrs) { super(context, attrs); // TODO Auto-generated constructor stub Log.d(TAG,"MyFrameLayout init"); setOnClickListener(this); setOnTouchListener(this); } @Override public boolean dispatchTouchEvent(MotionEvent event) { Log.d(TAG,"MyFrameLayout dispatchTouchEvent"); return super.dispatchTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { Log.d(TAG,"MyFrameLayout onTouchEvent"); return super.onTouchEvent(event); } @Override public void onClick(View view) { // TODO Auto-generated method stub Log.d(TAG,"MyFrameLayout onClick"); } @Override public boolean onTouch(View view, MotionEvent event) { // TODO Auto-generated method stub Log.d(TAG,"MyFrameLayout onTouch"); return false; } }
package com.example.empty; import android.content.Context; import android.util.AttributeSet; 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; public class MyImageView extends ImageView implements OnClickListener,OnTouchListener{ private static final String TAG = "Event"; public MyImageView(Context context, AttributeSet attrs) { super(context, attrs); // TODO Auto-generated constructor stub Log.d(TAG,"MyImageView init"); setOnClickListener(this); setOnTouchListener(this); } @Override public boolean dispatchTouchEvent(MotionEvent event) { Log.d(TAG,"MyImageView dispatchTouchEvent"); return super.dispatchTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { Log.d(TAG,"MyImageView onTouchEvent"); return super.onTouchEvent(event); } @Override public boolean onTouch(View arg0, MotionEvent arg1) { // TODO Auto-generated method stub Log.d(TAG,"MyImageView onTouch"); return false; } @Override public void onClick(View arg0) { // TODO Auto-generated method stub Log.d(TAG,"MyImageView onClick"); } }
例子很简单,下面我们让Log来告诉我们一些东西
当我们点击我们ImageView时
当我们再点击我们FrameLayout
从这个Log 我们可以得出2点结论
一、执行顺序来讲
dispatchTouchEvent > onTouch > onTouchEvent > onClick
二、事件分发顺序
点击图片时 我们触发了而且还是首先触发了FrameLayout的dispatchTouchEvent
这里我们知道在Android中我们所有的控件都源自View甚至
public abstract class ViewGroup extends View
所以从底层来看我们的所有事件最终都是交给我们的View
目录(源码目录/frameworks/base\core\java\android\view/View.java)
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource { ...... public interface OnClickListener { /** * Called when a view has been clicked. * * @param v The view that was clicked. */ void onClick(View v); } ...... 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); } ...... public boolean dispatchTouchEvent(MotionEvent event) { if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(event, 0); } if (DBG_MOTION) { Xlog.d(VIEW_LOG_TAG, "(View)dispatchTouchEvent: event = " + event + ",this = " + this); } if (onFilterTouchEventForSecurity(event)) { //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { return true; } if (onTouchEvent(event)) { return true; } } if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); } return false; } /** * Filter the touch event to apply security policies. * * @param event The motion event to be filtered. * @return True if the event should be dispatched, false if the event should be dropped. * * @see #getFilterTouchesWhenObscured */ public boolean onFilterTouchEventForSecurity(MotionEvent event) { //noinspection RedundantIfStatement if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0 && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) { // Window is obscured, drop this touch. return false; } return true; } ..... public boolean onTouchEvent(MotionEvent event) { final int viewFlags = mViewFlags; if ((viewFlags & ENABLED_MASK) == DISABLED) { /// M: we need to reset the pressed state or remove prepressed callback either up or cancel event happens. final int action = event.getAction(); if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { if ((mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } else if ((mPrivateFlags & PFLAG_PREPRESSED) != 0) { Xlog.d(VIEW_LOG_TAG, "View onTouch event, if view is DISABLED & PFLAG_PREPRESSED, remove callback mPrivateFlags = " + mPrivateFlags + ", this = " + this); removeTapCallback(); } } // 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 & PFLAG_PREPRESSED) != 0; if (DBG_MOTION) { Xlog.d(VIEW_LOG_TAG, "(View)Touch up: prepressed = " + prepressed + ",this = " + this); } 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); } 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(); if (DBG_MOTION) { Xlog.d(VIEW_LOG_TAG, "(View)Touch down: isInScrollingContainer = " + isInScrollingContainer + ",this = " + this); } // 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(); } postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // Not inside a scrolling container, so show the feedback right away setPressed(true); checkForLongClick(0); } break; case MotionEvent.ACTION_CANCEL: if (DBG_MOTION) { Xlog.d(VIEW_LOG_TAG, "(View)Touch cancel: this = " + this); } setPressed(false); removeTapCallback(); removeLongPressCallback(); break; case MotionEvent.ACTION_MOVE: final int x = (int) event.getX(); final int y = (int) event.getY(); if (DBG_MOTION) { Xlog.d(VIEW_LOG_TAG, "(View)Touch move: x = " + x + ",y = " + y + ",mTouchSlop = " + mTouchSlop + ",this = " + this); } // 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; } }
有了源码,其他自然一目了然。在这里我们发现onTouchListener 和onClickListener接口
很明显他们事件确定之后的回调(我自定义的View都实现了这两个接口)。
那么很明显他们和我们dispatchTouchEvent不会具有可比性
那么接下来我们把目光对准dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) { if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(event, 0); } if (DBG_MOTION) { Xlog.d(VIEW_LOG_TAG, "(View)dispatchTouchEvent: event = " + event + ",this = " + this); } if (onFilterTouchEventForSecurity(event)) { //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { return true; } if (onTouchEvent(event)) { return true; } } if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); } return false; } /** * Filter the touch event to apply security policies. * * @param event The motion event to be filtered. * @return True if the event should be dispatched, false if the event should be dropped. * * @see #getFilterTouchesWhenObscured */ public boolean onFilterTouchEventForSecurity(MotionEvent event) { //noinspection RedundantIfStatement if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0 && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) { // Window is obscured, drop this touch. return false; } return true; }
这里重点说明一下onFilterTouchEventForSecurity这个函数
它通过判断我们的窗口Window是否被遮蔽来判断是否舍弃本次事件
所以这就是为什么我们点击我们的ImageView时我们的FrameLayout也执行了dispatchTouchEvent但是却没有向下走了
那么我们继续回归dispatchTouchEvent中不是ViewGroup的情形
接下来,系统会自动判断我们是否实现了onTouchListener 这里就开始有分支了
当我们实现了onTouchListener
那么下一步我们的事件叫交给了onTouchListener .onTouch来处理
这里就又开始了分支
如果我们在onTouch中返回了true,那么就表明我们的onTouchListener
已经消化掉了本次的事件,本次事件完结。这就是为什么我们在onTouch中返回去就永运不会执行onClick,onLongClick了
如果我们在onTouch中返回了false,那么很明显了我们的事件就会被onTouchEvent处理
同理,当我们没有实现了onTouchListener,很明显了我们的事件就会被onTouchEvent处理
殊途同归,最终如果我们的事件没有被干掉,最终都交给了onTouchEvent
那么接下来我们继续来看onTouchEvent
那么我们的onTouchEvent又是用来干什么的呢(这里既然已经有onTouchListener了,他们似乎一模一样啊)
其实不然,说白了我们的onTouchEvent最终会用来分发onClick和onLongClick事件
如果你个人在这里还是不能理解onTouchEvent函数
推荐阅读
Android中onTouch方法的执行过程以及和onClick执行发生冲突的解决办法
希望对你有帮助
这里我需要提到的是注意onTouchEvent对于View的State的判断和处理