笔记:事件分发机制(一):View的事件分发

hackernew
发布于 2021-2-24 09:42
浏览
0收藏

说明


一直以来虽然对事件分发机制多少有些了解,看过一些博客,但自己没有真正从源码层次分析过。我觉得心中模糊的知识,不去加以验证和分析就不算真正明白,而不能纯熟运用的就不算真正熟练。


其实知道的知识点有很多,但大多数只知其然,多少会用一点,但不知其所以然,所以一直是庸手,因为知之而不精,用之而不能游刃。
所以,需要由浅入深的渗透学习。把知道的和会用的变成精通的,再广泛涉猎。此所谓,会别人之不会,用别人之不明,知别人之不知,是为高手。

郭大神的博客由浅入深比较好懂,所以主要根据他的博客做下笔记。

 

View的事件分发


基础


写一个简单的demo,demo中只有一个Activity,Activity中只有一个按钮。为按钮注册点击事件:

 

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Log.i(TAG, "OnClickListener: onClick...");
    }
});

 

实现点击监听的onClick()方法,就可执行点击的响应事件了。
如果再给这个按钮添加一个触摸事件,只需调用:

button.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        Log.i(TAG, "OnTouchListener: onTouch... === action: " + event.getAction());
        return false;
    }
});

 

onTouch中所能处理的事件比onClick中要多一些,如手指按下、抬起、移动、取消等事件:
MotionEvent.ACTION_DOWN (=0)
MotionEvent.ACTION_UP (=1)
MotionEvent.ACTION_MOVE (=2)
MotionEvent.ACTION_CANCEL (=3) 等
如果同时注册两个监听,那个会先执行呢?看一下log打印:

笔记:事件分发机制(一):View的事件分发-鸿蒙开发者社区

可以看出onTouch是先于onClick执行的,并且onTouch执行了两次,一次ACTION_DOWN,一次ACTION_UP。这是纯点击。

当我们手指有移动时,还会执行一次或多次ACTION_MOVE。笔记:事件分发机制(一):View的事件分发-鸿蒙开发者社区

当我们在按钮上按下,移动,在按钮外抬起手指,执行一次ACITON_DOWN,多次ACTION_MOVE,一次ACTION_UP。不执行点击事件。笔记:事件分发机制(一):View的事件分发-鸿蒙开发者社区

由上面可以看出,onTouch 先于 onClick 执行,并且onClick在手指抬起后执行。因此,事件传递顺序是先经过onTouch,然后到onClick。

 

仔细一看,发现onTouch是有返回值的,默认是false。尝试将它改为true,运行结果如下:

笔记:事件分发机制(一):View的事件分发-鸿蒙开发者社区

发现onClick不再执行了。这是为什么?可以先理解为当onTouch返回true,事件先被onTouch消费了,因此不再向下传递了。

 

源码分析


以上基本知识早就了解。现在需要做的是从源码角度去深入地探索和了解一下出现上述现象的原理。

首先要了解的是触摸任何一个控件,就一定会调用dispatchTouchEvent

我们创建一个View:MyButton,
继承 android.support.v7.widget.AppCompatButton ,
重写 dispatchTouchEvent。

public class MyButton extends android.support.v7.widget.AppCompatButton {

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

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

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

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.i("MainActivity", "MyButton: dispatchTouchEvent... ");
        return super.dispatchTouchEvent(event);
    }
}

 

Ctrl + 左键 点进 super.dispatchTouchEvent(event),发现
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) {
    ......
    boolean result = false;
    ......
    //按下时停止嵌套滑动
    final int actionMasked = event.getActionMasked();
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        // Defensive cleanup for new gesture
        stopNestedScroll();
    }
    //为了安全起见,过滤触触摸事件,如果窗口被遮挡,停止触摸。
    //见方法onFilterTouchEventForSecurity()
    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        //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;
}

 

从ListenerInfo li = mListenerInfo;这一行开始看起。
往下看,会发现:
如果 li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED&& li.mOnTouchListener.onTouch(this, event) 四个条件都为true,执行完OnTouchListener的onTouch方法(li.mOnTouchListener.onTouch(this, event))后返回true;否则执行 onTouchEvent(event) 后返回true。

 

ListenerInfo 是View的一个静态内部类(static class ListenerInfo),保存的是View的一些监听事件。可由getListenerInfo()创建和获取。

我们从第一个条件看,li就是mListenerInfo,而mListenerInfo由getListenerInfo()创建和获取。下面是getListenerInfo() 源码:

ListenerInfo getListenerInfo() {
    if (mListenerInfo != null) {
        return mListenerInfo;
    }
    mListenerInfo = new ListenerInfo();
    return mListenerInfo;
}

 

如果mListenerInfo已经存在,直接返回,否则创建一个返回。

 

第二个条件li.mOnTouchListener != null, 找mOnTouchListener,发现:

public void setOnTouchListener(OnTouchListener l) {
    getListenerInfo().mOnTouchListener = l;
}

 

嗯,很熟悉,这不就是注册触摸事件监听的方法吗?getListenerInfo()表示创建或拿到View的mListenerInfo,所以只要我们注册了触摸监听事件,那么,前两个条件成立了。

 

第三个条件(mViewFlags & ENABLED_MASK) == ENABLED表示当前View是否enable,按钮默认是enable,这个条件为true。

 

第四个条件li.mOnTouchListener.onTouch(this, event)比较关键,这是回调了触摸监听的onTouch方法,也就是说,我们在onTouch返回true,四个条件组成的表达式就为true。如果在onTouch返回false,表达式就是false,就会执行onTouchEvent(event)。

 

现在我们就可以结合前面的例子来分析一下,首先在dispatchTouchEvent中最先执行的是onTouch ,因此 onTouch 肯定是优先于 onClick执行的,与之前打印结果一致。如果我们在onTouch中返回了true,就会让dispatchTouchEvent直接返回true,不再继续向下执行,与之前结果onClick不再执行符合。

 

上面的源码分析从原理上验证了前面例子的运行结果。同时还暗示了一个重要信息,onTouch之后没有onClick调用,只有一个View的onTouchEvent(event)调用,所以onClick的调用在ontouchEvent()中。那我们就看一下onTouchEvent的源码:

public boolean onTouchEvent(MotionEvent event) {
    final float x = event.getX();
    final float y = event.getY();
    final int viewFlags = mViewFlags;
    final int action = event.getAction();

    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. //disable但可点击的控件仍然消费触摸时间只是不作出响应。

        return (((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
    }
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }

    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
            (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
        switch (action) {
            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;

            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, x, y);
                }
                break;

            case MotionEvent.ACTION_CANCEL:
                setPressed(false);
                removeTapCallback();
                removeLongPressCallback();
                mInContextButtonPress = false;
                mHasPerformedLongPress = false;
                mIgnoreNextUpEvent = false;
                break;

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

        return true;
    }

    return false;
}

 

先从23行开始看,如果View是可点击的,那就进入action判断。之前我们知道了,点击事件是在onTouchEvent()中被调用。接着往下看。从54行上面的注释开始读,

 

// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.

 

使用一个Runnable并post执行这个Runnable,而不是直接调用performClick。这使View的其他视觉状态在点击动作开始执行前更新。

也就是post执行一个Runnable更新视觉状态,然后再执行点击事件,而不是直接调用performClick()。我们就看一下performClick源码:

/**
 * Call this view's OnClickListener, if it is defined.  Performs all normal
 * actions associated with clicking: reporting accessibility event, playing
 * a sound, etc.
 * 调用这个控件的OnClickListener,如果已经定义了OnClickListener。执行所有与点击有关的      
 * 正常动作:声明可访问事件,播放声音,等等。
 * @return True there was an assigned OnClickListener that was called, false
 *         otherwise is returned.
 */
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和li.mClickListener就都不为空,OnClickListener的onClick(this)就会被执行,并且把控件自己作为参数传给onClick。

 

mClickListener赋值当然是在我们熟悉的方法setOnClickListener中执行:

public void setOnClickListener(@Nullable OnClickListener l) {
    if (!isClickable()) {
        setClickable(true);
    }
    getListenerInfo().mOnClickListener = l;
}

 

getListenerInfo()方法创建或拿到一个ListenerInfo对象。

 

所以调用setOnClickListener方法注册点击监听,在performClick()方法中执行点击事件onClick, 而performClick是在View的onTouchEvent中手指抬起动作后执行。

现在整个View的事件分发流程已经搞清楚了。还有一个重要的知识点,就是touch事件的层级传递。如果注册了touch监听,每次点击都会触发一系列的

 

ACTION_DOWN、ACTION_UP、ACTION_MOVE等触摸事件。这里需要注意,如果在ACTION_DOWN之后返回了false,后面一系列的action都将得不到执行了。简单说就是dispatchTouchEvent在事件分发的时候,只有前一个action返回true,后一个action才能被执行。


下面做一个简单的验证:
在上面自定义的MyButton中重写onTouchEvent方法,在ACTION_DOWN之后我们返回false,代码如下

public class MyButton extends android.support.v7.widget.AppCompatButton {

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

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

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

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.i("MainActivity", "MyButton: dispatchTouchEvent... ");
        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        Log.i("MainActivity", "MyButton: onTouchEvent...action: " + event.getAction());
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            return false;
        }
        return true;
    }
}

 

布局文件替换原来的Button,在Activity中设置触摸监听和点击监听

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Log.i(TAG, "OnClickListener: onClick...");
    }
});
button.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        Log.i(TAG, "OnTouchListener: onTouch... === action: " + event.getAction());
        return false;
    }
});

 

手指做按下,移动,抬起动作,运行结果为笔记:事件分发机制(一):View的事件分发-鸿蒙开发者社区

发现,首先执行dispatchTouchEvent,进行事件分发
然后,执行OnTouchListener的onTouch方法,返回false(会执行onTouchEvent)
接着,执行onTouchEvent,在按下事件结束后,我们返回了false
然后,没有了,不再执行ACTION_MOVE和ACTION_UP,当然也不会执行点击事件。

 

如果我们在ACTION_DOWN之后我们返回true,

@Override
public boolean onTouchEvent(MotionEvent event) {
    super.onTouchEvent(event);
    Log.i("MainActivity", "MyButton: onTouchEvent...action: " + event.getAction());
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
        return true; //false->true
    }
    return true;
}

 

重新来一遍手指做按下,移动,抬起动作,打印结果如下笔记:事件分发机制(一):View的事件分发-鸿蒙开发者社区

 

从上到下可以解释为:


注action: 0 -> ACTION_DOWN, 1-> ACTION_UP, 2 -> ACTION_MOVE

 

—– 手指按下,ACTION_DOWN(action: 0)


1,执行dispatchTouchEvent(①),进行事件分发
2,执行OnTouchListener的onTouch方法,返回false(会执行onTouchEvent)
3,执行onTouchEvent
—– 手指移动—1,ACTION_MOVE(action: 2)
4,执行dispatchTouchEvent(②),新的触摸事件会重新事件分发
5,执行OnTouchListener的onTouch方法,返回false(会执行onTouchEvent)
6,执行onTouchEvent
—– 手指移动—2,ACTION_MOVE(action: 2)
7,执行dispatchTouchEvent(③),新的触摸事件会重新事件分发
8,执行OnTouchListener的onTouch方法,返回false(会执行onTouchEvent)
9,执行onTouchEvent
—– 手指抬起,ACTION_UP(action: 1)
10,执行dispatchTouchEvent(④),新的触摸事件会重新事件分发
11,执行OnTouchListener的onTouch方法,返回false(会执行onTouchEvent)
12,执行onTouchEvent
13,执行onClick事件
——结束


对比两次结果,发现,
① 每次有新的触摸事件产生,就会进行一次事件分发
② 先执行onTouchListener的onTouch方法,(返回false)后执行View的onTouchEvent方法
③ 只有前一个事件(action)执行完返回true时,下一个事件(action)才能被执行

 

这里可能就会疑惑了,明明我们在OnTouchListener中的onTouch方法中返回了false,为什么之后的事件依然执行了呢?之前在判断四个条件的时候,发现只要设置了触摸监听,是否执行onTouchEvent就由onTouch的返回值决定了。onTouch返回true,表示事件被触摸监听消费,四个条件判断成立,不再执行View的

 

onTouchEvent;返回false,表示事件没有被消费,继续使用View本身的onTouchEvent处理触摸事件。
而在onTouchEvent的源码中,经过switch判断action,处理过事件以后始终会返回true,使得下一个触摸时间可以继续执行。

 

从23行的if判断中,可知如果控件可点击(clickable),就会进入switch的action判断,最后返回true,控件如果不可点击,直接返回false,下一个action不再执行。

 

if (((viewFlags & CLICKABLE) == CLICKABLE ||
        (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
        (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
    switch(action){
        ......
    }
    retrun true;

}
retrun false;

 

如果换一个控件,将MyButton换成ImageView,把点击监听注释掉,运行后结果为:笔记:事件分发机制(一):View的事件分发-鸿蒙开发者社区

在ACTION_DOWN之后的一系列action都不再执行了。这是为什么?原来ImageView与Button不同,Button默认是clickable。而ImageView默认不可点击,所以不会进入switch的action判断,直接返回了false,后续的触摸事件将不再执行。

 

我们可以在xml中给ImageView设置android:clickable="true",或添加代码imageView.setClickable(true)。

但是为什么我们不设置clickable,ImageView仍然可以有点击事件呢?点击事件不是在onTouchEvent中的ACTION_UP case中执行的吗?看一下setOnClickListener的源码就清楚了:

/**
 * Register a callback to be invoked when this view is clicked. If this view is not
 * clickable, it becomes clickable.
 * 为这个控件注册一个callback供这个控件被点击时调用。如果这个控件不是可点击的,让他可点击。
 * @param l The callback that will run
 *
 * @see #setClickable(boolean)
 */
public void setOnClickListener(@Nullable OnClickListener l) {
    if (!isClickable()) {
        setClickable(true);
    }
    getListenerInfo().mOnClickListener = l;
}

 

源码中在设置点击监听时,如果View不可点击,会先把这个View设置为可点击。

关于View的事件分发的笔记就到此结束了。郭大神的博客给了我很多启发,也给我做笔记看源码提供了一个明确的路线,使得我轻松了不少,同时也收获了很多,感谢郭大神。
何俊林大神的博客更加深入一些,比较系统,建议看完郭大神的博客,再看何大神的博客。

分类
已于2021-2-24 09:42:05修改
收藏
回复
举报
回复
    相关推荐