Android事件分发机制源码分析之View篇

对于Android事件分发机制,我们在开发的过程中,肯定曾经遇到在最外层添加了ScrollView之后ListView无法正常滑动、我们的图片轮播在左右滑动图片为什么感觉很难控制。这些都是我们用户在屏幕上进行交互的一系列操作,因此深入了解Android事件分发机制是非常的重要。

事件分发的概念

所谓点击事件的事件分发,就是当一个MotionEvent产生了以后,系统需要把这个事件传递给一个具体的View(ViewGroup也继承于View),这个传递的过程就叫做分发过程。Android的触摸事件分发传递过程中,最重要的是可以分为:

  • Activity

    • dispatchTouchEvent(MotionEvent) //事件分开
    • onTouchEvent(MotionEvent) //事件处理
  • ViewGroup

    • dispatchTouchEvent(MotionEvent)
    • onInterceptTouchEvent(MotionEvent) //事件拦截
    • onTouchEvent(MotionEvent)
  • View

    • dispatchTouchEvent(MotionEvent)
    • onTouchEvent(MotionEvent)

然而在用户点下屏幕之后,我们通过下面这张图对触摸事件要有一个整体的了解。

Android事件分发机制源码分析之View篇_第1张图片

示例

大家在有了整体了解之后,我们今次主要分析的是View的事件分发。
我们通过下面简单的代码来了解一下。我们自定以一个CustomButton继承Button,然后把跟View的事件传播有关的方法进行复写,然后再Log打印下。
CustomButton:我们重写一下onTouchEvent和dispatchTouchEvent方法并打印。

public class CustomButton extends Button {
    private static final String TAG = "CustomButton";

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

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                Log.i(TAG, "onTouchEvent ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i(TAG, "onTouchEvent ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.i(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.i(TAG, "dispatchTouchEvent ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i(TAG, "dispatchTouchEvent ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.i(TAG, "dispatchTouchEvent ACTION_UP");
                break;

            default:
                break;
        }
        return super.dispatchTouchEvent(event);
    }
}

简单看一下我们的布局文件:


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">

    <com.example.pc.myapplication.CustomButton
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="button" />
LinearLayout>

最后是我们的MainActivity代码:

public class MainActivity extends Activity {
    private static final String TAG = "MainActivity";
    private CustomButton mbtn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mbtn = (CustomButton) findViewById(R.id.button);
        mbtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.i(TAG, "onClick Event");
            }
        });
        mbtn.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                int action = event.getAction();
                switch (action) {
                    case MotionEvent.ACTION_DOWN:
                        Log.i(TAG, "onTouch ACTION_DOWN");
                        break;
                    case MotionEvent.ACTION_MOVE:
                        Log.i(TAG, "onTouch ACTION_MOVE");
                        break;
                    case MotionEvent.ACTION_UP:
                        Log.i(TAG, "onTouch ACTION_UP");
                        break;
                    default:
                        break;
                }

                return false;
            }
        });
    }

}

好了上面就是大致的代码,我们接着编译运行一下,看看打印的结果:

10-27 14:03:05.264 14668-14668/com.example.pc.myapplication I/CustomButton: dispatchTouchEvent ACTION_DOWN
10-27 14:03:05.264 14668-14668/com.example.pc.myapplication I/MainActivity: onTouch ACTION_DOWN
10-27 14:03:05.264 14668-14668/com.example.pc.myapplication I/CustomButton: onTouchEvent ACTION_DOWN
10-27 14:03:05.273 14668-14668/com.example.pc.myapplication I/CustomButton: dispatchTouchEvent ACTION_MOVE
10-27 14:03:05.273 14668-14668/com.example.pc.myapplication I/MainActivity: onTouch ACTION_MOVE
10-27 14:03:05.273 14668-14668/com.example.pc.myapplication I/CustomButton: onTouchEvent ACTION_MOVE
10-27 14:03:05.308 14668-14668/com.example.pc.myapplication I/CustomButton: dispatchTouchEvent ACTION_UP
10-27 14:03:05.308 14668-14668/com.example.pc.myapplication I/MainActivity: onTouch ACTION_UP
10-27 14:03:05.308 14668-14668/com.example.pc.myapplication I/CustomButton: onTouchEvent ACTION_UP
10-27 14:03:05.326 14668-14668/com.example.pc.myapplication I/MainActivity: onClick EVENT

从打印结果来看,无论是DOWN,MOVE,UP的动作,执行的步骤都是
1. dispatchTouchEvent
2. setOnTouchListener的onTouch
3. onTouchEvent

分析

那我们就根据打印来看一下dispatchTouchEvent的源码。我们在CustomButton类中按CTRL+O,搜索dispatchTouchEvent方法,你会发现dispatchTouchEvent()是直接指向View类中的dispatchTouchEvent(),这是因为Button类没有覆写dispatchTouchEvent(),Button类继承TextView类,而TextView类也没有覆写dispatchTouchEvent(),最后是TextView类继承View类,所以我们CustomButton覆写的dispatchTouchEvent()直接指向它的父类View中。(可能有同学产生疑问虽然是先执行dispatchTouchEvent(),但是为什么呢?还会有Activity–>ViewGroup–>View这样分发下来,那么Activity又是怎么执行到dispatchTouchEvent()的呢?这部分的问题不是我们这篇讨论分析的范围,我们先定性理解先执行dispatchTouchEvent()。)

    public boolean dispatchTouchEvent(MotionEvent event) {
        ···

        boolean result = false;

        ···

        if (onFilterTouchEventForSecurity(event)) {
            //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;
            }
        }

        ···

        return result;
    }

我们直接看重点部分,判断if(onFilterTouchEventForSecurity(event)),这个主要是判断当前事件到来的时候,窗口有没有被遮挡,如果被遮挡则会直接返回false。接着是将mListenerInfo赋给ListenerInfo的li对象,ListenerInfo是什么呢?

static class ListenerInfo {
        ···
        public OnClickListener mOnClickListener;

        protected OnLongClickListener mOnLongClickListener;


        protected OnContextClickListener mOnContextClickListener;

        protected OnCreateContextMenuListener mOnCreateContextMenuListener;

        private OnKeyListener mOnKeyListener;

        private OnTouchListener mOnTouchListener;
        ···
}

是我们平时用到的一些监听,然而mListenerInfo又是从哪里赋值的?继承找

    ListenerInfo getListenerInfo() {
        if (mListenerInfo != null) {
            return mListenerInfo;
        }
        mListenerInfo = new ListenerInfo();
        return mListenerInfo;
    }
    /**
     * 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) {
        getListenerInfo().mOnTouchListener = l;
    }

从给出的注释就可以理解得到,当我们在视图中设置一个setOnTouchListener(),那么就会注册一个回调函数调用。
接着往下有四个条件判断:li != null ; li.mOnTouchListener != null; (mViewFlags & ENABLED_MASK) == ENABLED; li.mOnTouchListener.onTouch(this, event)

第一个li != null,如果我们设置了setOnTouchListener(),在ListenerInfo li = mListenerInfo;中就会被赋值。

第二个li.mOnTouchListener != null,在setOnTouchListener()源码中可以看到赋值。不为空。

第三个(mViewFlags & ENABLED_MASK) == ENABLED,是判断当前点击的控件是否是enable的,按钮默认都是enable的,因此这个条件恒定为true。

第四个,mOnTouchListener.onTouch(this, event),其实也就是去回调控件注册touch事件时的onTouch方法的返回值,默认值是返回false。

上面的四个判断条件都成立整个方法赋值返回true。有一个不成立都会赋值返回false。继续接下下面的判断。!result && onTouchEvent(event)。

第一个根据上面的判断返回值result,如果result为false才会接着执行onTouchEvent(event)。

小总结:上面的源码分析,证明了之前的打印信息顺序。先执行dispatchTouchEvent(),然后执行mOnTouchListener.onTouch(this, event)的回调返回,满足条件之后再调用onTouchEvent()。

我们紧接着看一下onTouchEvent(event)源码:
由于源码较长,这里分段来讲述。

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

如果当前View是Disabled不可用状态状态且是可点击则会消费掉事件(return true);

        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

如果设置了mTouchDelegate,则会将事件交给代理者处理,直接return true;

        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
            ···

            return true;
        }

        return false;

在一开始的判断满足其中一个情况就返回true,否则返回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);
                    }
                    break;

设置mHasPerformedLongPress=false;表示长按事件还未触发;
isInScrollingContainer判断上一层结构,判断是否在一个滚动的容器中;
如果在滚动容器中会做一个短延迟,区分滚动还是长按,接着最终的方法都是差不多,记录横纵坐标和检测长按监听。

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,如果移出了:
执行removeTapCallback();
然后判断是否包含PRESSED标识,如果包含,移除长按的检查:removeLongPressCallback();

其实ACTION_DOWN与ACTION_MOVE都进行了一些必要的设置与置位,重点是ACTION_UP。

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;
                        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;

首先判断了是否被按下 boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;接下来判断是不是可以获得焦点,同时尝试去获取焦点;再处理点击下显示效果;清除长按回调。经过上述的种种判断之后我们重点看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;
    }

终于到看到li.mOnClickListener.onClick(),这里检测了当前View是否设置了onClickListener,如果设置了那么回调它的onClick方法,所以验证了我们一开始打印的数据,onClick()在onTouch()的后面执行,因为onClick()方法是在onTouchEvent内部被调用的。
接下来的是我们处理完performClick()后的一些状态标识、状态的改变和回调的操作。

总结:经过一系列的源码,我们了解到dispatchTouchEvent()、onTouch()、onTouchEvent()。在View的dispatchTouchEvent中调用onTouch()和onTouchEvent(),onTouch优先于onTouchEvent执行。如果在onTouch方法中通过返回true将事件消费掉,onTouchEvent将不会再执行。onTouch能够得到执行需要两个前提条件,第一mOnTouchListener的值不能为空,第二当前点击的控件必须是enable的(ImageView、TextView非enable)。因此如果你有一个控件是非enable的,那么给它注册onTouch事件将永远得不到执行。

但是,我们上述说的只是事件分发的事件流向分发部分,我们将在下一篇Android事件分发机制源码分析之ViewGroup篇 来理解事件分发另一个重点————事件拦截。

注:
- 源码为Android API 23 Platform
- 参考图片
- 鸿洋Android View 事件分发机制 源码解析 (上)

你可能感兴趣的:(Android,android,源码,事件分发,面试)