Android事件分发机制——View(一)

版权声明:本文为博主原创文章,欢迎转载但需注明出处谢谢! https://blog.csdn.net/dongxianfei/article/details/83820222

在分析分发机制之前,我们来了解一些预备知识。

  • onTouch是优先于onClick执行,事件传递的顺序是先经过onTouch,再传递到onClick;
  • MotionEvent对象的四种状态:
MotionEvent.ACTION_DOWN:手指按下屏幕的瞬间。
MotionEvent.ACTION_MOVE:手指在屏幕上移动
MotionEvent.ACTION_UP:手指离开屏幕瞬间
MotionEvent.ACTION_CANCEL:取消手势
  • Android事件响应机制是“由外到内”分发、“由内到外”处理的形式实现的;
  • Android中的事件onClick、onLongClick、onScroll等,都是由多个Touch事件(一个ACTION_DOWN,多个ACTION_MOVE,一个ACTION_UP)组成;
  • Android事件分发机制主要由“事件分发”—>“事件拦截”—>“事件响应”这三步来进行逻辑控制的;
  • ViewGroup中的分发机制三个方法:
public boolean dispatchTouchEvent(MotionEvent ev)public boolean onInterceptTouchEvent(MotionEvent ev);    
public boolean onTouchEvent(MotionEvent ev);
  • View中的分发机制两个方法:
public boolean dispatchTouchEvent(MotionEvent ev)public boolean onTouchEvent(MotionEvent ev);

案例

public class MyButton extends Button {

    private static String TAG = MyButton.class.getSimpleName();

    public MyButton(Context context) {
        super(context);
    }

    public MyButton(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @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);
    }

    @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);
    }
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    tools:context=".MainActivity">

    <com.example.dispatchevent.MyButton
        android:id="@+id/my_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="click me"/>

</LinearLayout>
public class MainActivity extends AppCompatActivity {

    private static String TAG = "MyButton";
    private Button mButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mButton = findViewById(R.id.my_button);

        mButton.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {

                int action = motionEvent.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;
                    default:
                        break;
                }
                return false;
            }
        });

        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.e(TAG, "onclick");
            }
        });

        mButton.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                Log.e(TAG, "longclick");
                return false;
            }
        });
    }
}

好了,跟View事件相关一般就三个地方,一个onTouchEvent,一个dispatchTouchEvent,一个setOnTouchListener;
我们来看一下日志输出:

01-07 22:20:38.992 24590-24590/com.example.dispatchevent E/MyButton: dispatchTouchEvent ACTION_DOWN
01-07 22:20:38.992 24590-24590/com.example.dispatchevent E/MyButton: onTouch ACTION_DOWN
01-07 22:20:38.992 24590-24590/com.example.dispatchevent E/MyButton: onTouchEvent ACTION_DOWN
01-07 22:20:39.016 24590-24590/com.example.dispatchevent E/MyButton: dispatchTouchEvent ACTION_MOVE
01-07 22:20:39.016 24590-24590/com.example.dispatchevent E/MyButton: onTouch ACTION_MOVE
01-07 22:20:39.016 24590-24590/com.example.dispatchevent E/MyButton: onTouchEvent ACTION_MOVE
01-07 22:20:39.019 24590-24590/com.example.dispatchevent E/MyButton: dispatchTouchEvent ACTION_UP
01-07 22:20:39.020 24590-24590/com.example.dispatchevent E/MyButton: onTouch ACTION_UP
01-07 22:20:39.020 24590-24590/com.example.dispatchevent E/MyButton: onTouchEvent ACTION_UP
01-07 22:20:39.029 24590-24590/com.example.dispatchevent E/MyButton: onclick

好了,可以看到,不管是DOWN,MOVE,UP都会按照下面的顺序执行:

  1. dispatchTouchEvent
  2. setOnTouchListener的onTouch
  3. onTouchEvent

源码分析
首先你需要知道一点,只要你触摸到了任何一个控件,就一定会调用该控件的dispatchTouchEvent方法,由于Button中没有会一直向上寻找,最终发现View.dispatchTouchEvent。
View.dispatchTouchEvent

View.dispatchTouchEvent

/**
     * 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 (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
            mOnTouchListener.onTouch(this, event)) {
        return true;
    }
    return onTouchEvent(event);
}

   /**
     * Register a callback to be invoked when a touch event is sent to this view.
     * @param l the touch listener to attach to this view
     */
    public void setOnTouchListener(OnTouchListener l) {
        mOnTouchListener = l;
    }

首先判断mOnTouchListener不为null,并且view是enable的状态,然后 mOnTouchListener.onTouch(this, event)返回true,这三个条件如果都满足,直接return true,也就是下面的onTouchEvent(event)不会被执行了;

我们在看一个简单的示例:

mButton.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                int action = motionEvent.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;
                    default:
                        break;
                }
                return true;
				//return false;
            }
        });
01-07 04:23:35.132 14251-14251/com.example.dispatchevent E/MyButton: dispatchTouchEvent ACTION_DOWN
01-07 04:23:35.134 14251-14251/com.example.dispatchevent E/MyButton: onTouch ACTION_DOWN
01-07 04:23:35.154 14251-14251/com.example.dispatchevent E/MyButton: dispatchTouchEvent ACTION_MOVE
01-07 04:23:35.156 14251-14251/com.example.dispatchevent E/MyButton: onTouch ACTION_MOVE
01-07 04:23:35.163 14251-14251/com.example.dispatchevent E/MyButton: dispatchTouchEvent ACTION_UP
01-07 04:23:35.164 14251-14251/com.example.dispatchevent E/MyButton: onTouch ACTION_UP

从这个示例中我们看到,如果onTouch中返回true,从而使onTouchEvent就不会执行并且onClick也没有执行,从而得出结论onTouch要优先于onClick之前被执行,并且onClick是在onTouchEvent中被执行的。

View.onTouchEvent

/**
     * Implement this method to handle touch screen motion events.
     *
     * @param event The motion event.
     * @return True if the event was handled, false otherwise.
     */
    public boolean onTouchEvent(MotionEvent event) {
        final int viewFlags = mViewFlags;
 
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            // 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;
            }
        }
 		//(1)此处肯定返回true
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
            switch (event.getAction()) {
            	//(4)MotionEvent.ACTION_UP操作
                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 (!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) {
                            mPrivateFlags |= PRESSED;
                            refreshDrawableState();
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }
                        removeTapCallback();
                    }
                    break;
 				//(2)MotionEvent.ACTION_DOWN操作
                case MotionEvent.ACTION_DOWN:
                    if (mPendingCheckForTap == null) {
                        mPendingCheckForTap = new CheckForTap();
                    }
                    mPrivateFlags |= PREPRESSED;
                    mHasPerformedLongPress = false;
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    break;
 
                case MotionEvent.ACTION_CANCEL:
                    mPrivateFlags &= ~PRESSED;
                    refreshDrawableState();
                    removeTapCallback();
                    break;
 				//(3)MotionEvent.ACTION_MOVE状态判断
                case MotionEvent.ACTION_MOVE:
                    final int x = (int) event.getX();
                    final int y = (int) event.getY();
 
                    // Be lenient about moving outside of buttons
                    int slop = mTouchSlop;
                    if ((x < 0 - slop) || (x >= getWidth() + slop) ||
                            (y < 0 - slop) || (y >= getHeight() + slop)) {
                        // 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;
    }

分析:
(1)
首先我们需要注意的是在23行,onTouchEvent肯定是返回true的,接下来就是 switch (event.getAction())了,判断事件类型,DOWN,MOVE,UP等;

if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
	//...
	return true;
}

(2)MotionEvent.ACTION_DOWN

case MotionEvent.ACTION_DOWN:
	if (mPendingCheckForTap == null) {
		mPendingCheckForTap = new CheckForTap();
	}
	mPrivateFlags |= PREPRESSED;
	mHasPerformedLongPress = false;
	postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
	break;
  1. 先将mPrivateFlags设置一个PREPRESSED的标识
  2. 并且设置mHasPerformedLongPress=false,表示长按事件还未触发
  3. 最后发送一个延迟为ViewConfiguration.getTapTimeout()的延迟消息,到达延时时间后会执行CheckForTap()里面的run方法
private final class CheckForTap implements Runnable {
        public void run() {
        	//取消mPrivateFlags的PREPRESSED,然后设置PRESSED标识
            mPrivateFlags &= ~PREPRESSED;
            mPrivateFlags |= PRESSED;
            //刷新背景
            refreshDrawableState();
            //如果View支持长按事件,则再发一个延时消息,检测长按
            if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
                postCheckForLongClick(ViewConfiguration.getTapTimeout());
            }
        }
    }

 private void postCheckForLongClick(int delayOffset) {
        mHasPerformedLongPress = false;
 
        if (mPendingCheckForLongPress == null) {
            mPendingCheckForLongPress = new CheckForLongPress();
        }
        mPendingCheckForLongPress.rememberWindowAttachCount();
        //发送延迟消息,执行CheckForLongPress中run方法,500ms之后触发performLongClick(),之前都算是performOnClick()
        postDelayed(mPendingCheckForLongPress,
                ViewConfiguration.getLongPressTimeout() - delayOffset);
    }

class CheckForLongPress implements Runnable {
 
        private int mOriginalWindowAttachCount;
 
        public void run() {
            if (isPressed() && (mParent != null)
                    && mOriginalWindowAttachCount == mWindowAttachCount) {
                //调用长按事件,当返回为true时,将mHasPerformedLongPress设置为true,从而使Move_Up中的performOnClick无法执行
                if (performLongClick()) {
                    mHasPerformedLongPress = true;
                }
            }
        }
  1. 如果此时设置了长按的回调,则执行长按时的回调,且如果长按的回调返回true,才把mHasPerformedLongPress置为ture;
  2. 否则,如果没有设置长按回调或者长按回调返回的是false,则mHasPerformedLongPress依然是false;

(3)MotionEvent.ACTION_MOVE
简单来说就是先拿到当前触摸的x,y坐标,然后判断触摸是否移出当前View,如果移出了将清除之前的标识以及removeCallbacks等操作;

(4)MotionEvent.ACTION_UP

//之前如果没有执行performLongClick或者返回值为false,mHasPerformedLongPress将为false;
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();
		}
	}
}

 public boolean performClick() {
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
 
        if (mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            mOnClickListener.onClick(this);
            return true;
        }
        return false;
    }

之前也说了,如果没有设置长按回调或者长按回调返回的是false,则mHasPerformedLongPress依然是false,此时将满足条件,从而会执行performClick()。

另外最后还会执行UnsetPressedState.run();

private final class UnsetPressedState implements Runnable {
        @Override
        public void run() {
            setPressed(false);
        }
    }

public void setPressed(boolean pressed) {
        final boolean needsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED);
        if (pressed) {
            mPrivateFlags |= PFLAG_PRESSED;
        } else {
            mPrivateFlags &= ~PFLAG_PRESSED;
        }
        if (needsRefresh) {
            refreshDrawableState();
        }
        dispatchSetPressed(pressed);
    }

//可以重新此方法
protected void dispatchSetPressed(boolean pressed) {
    }

总结

(1)View的事件转发流程
View.dispatchTouchEvent->View.setOnTouchListener->View.onTouchEvent

在dispatchTouchEvent中会进行OnTouchListener的判断,如果OnTouchListener不为null且返回true,则表示事件被消费,onTouchEvent不会被执行,否则执行onTouchEvent。

(2)Down时

  1. 首先设置标志为PREPRESSED,设置mHasPerformedLongPress=false,然后发出一个115ms后的mPendingCheckForTap;
  2. 如果115ms内没有触发UP,则将标志置为PRESSED,清除PREPRESSED标志,同时发出一个延时为500-115ms的,检测长按任务消息;
  3. 如果500ms内(从DOWN触发开始算),则会触发LongClickListener;
  4. 超过500ms,此时如果LongClickListener不为null,则会执行回调,同时如果LongClickListener.onClick返回true,才把mHasPerformedLongPress设置为true,否则mHasPerformedLongPress依然为false;

(3)Move时
主要就是检测用户是否划出控件,如果划出将清除标识以及removeCallback等;

(4)Up时

  1. 如果115ms内,触发UP,此时标志为PREPRESSED,则执行UnsetPressedState,setPressed(false),会把setPress转发下去,可以在View中复写dispatchSetPressed方法接收;
  2. 如果是115ms-500ms间,即长按还未发生,则首先移除长按检测,执行onClick回调;
  3. 如果超过500ms以后,设置了onLongClickListener,且onLongClickListener.onClick返回true,则点击事件OnClick事件无法触发,如果没有设置onLongClickListener或者onLongClickListener.onClick返回false,则点击事件OnClick事件依然可以触发;
  4. 最后执行mUnsetPressedState.run(),将setPressed传递下去;

最后一个问题

问:setOnLongClickListener和setOnClickListener是否只能执行一个?

答:不是的,只要setOnLongClickListener中的onLongClick返回false,则两个都会执行;返回true则会屏蔽setOnClickListener。

猜你喜欢

转载自blog.csdn.net/dongxianfei/article/details/83820222