android中view事件传递

作者: 的一幕 | 来源:发表于2019-07-19 19:06 被阅读9次

    其实android的view事件传递一直很少关注源码的分析,这次在做项目的时候为了获取webview是否被点击了,如果被点击了,做一个逻辑的存储,所以会有下面的代码:

     mWebView.setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    //do something
                    return true;
                }
     });
    

    但是在网页里面又有js的按钮点击,此时呢,点击webview的按钮没有任何反应,所以此时,感觉是onTouch事件在搞鬼,因此,这里尝试着在在onTouch返回false尝试下,因此又有下面的代码:

     mWebView.setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    //do something
                    return false;
                }
    });
    

    因此这里想肯定是onTouch返回了true,导致onClick事件无法获取。猜想归猜想,还是得看下源码是怎么来定这个规则的,下面还是按照惯例,写个小的demo验证下:

    public class TestView extends View {
        private static final String TAG = TestView.class.getSimpleName();
    
        public TestView(Context context) {
            super(context);
        }
    
        public TestView(Context context, AttributeSet attrs) {
            super(context, attires
        }
    
        public TestView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        @Override
        public boolean dispatchTouchEvent(MotionEvent event) {
            int action = event.getAction();
            String actionName = "";
            if (action == MotionEvent.ACTION_DOWN) {
                actionName = "action_down";
            } else if (action == MotionEvent.ACTION_MOVE) {
                actionName = "action_move";
            } else if (action == MotionEvent.ACTION_UP) {
                actionName = "action_up";
            }
            Log.d(TAG, "TestButton dispatchTouchEvent-----" + actionName);
            return super.dispatchTouchEvent(event);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            int action = event.getAction();
            String actionName = "";
            if (action == MotionEvent.ACTION_DOWN) {
                actionName = "action_down";
            } else if (action == MotionEvent.ACTION_MOVE) {
                actionName = "action_move";
            } else if (action == MotionEvent.ACTION_UP) {
                actionName = "action_up";
            }
            Log.d(TAG, "TestButton onTouchEvent-----" + actionName);
            return super.onTouchEvent(event);
        }
    }
    

    testView记下了dispatchTouchEvent中三个action,以及返回super.dispatchTouchEvent(event),在ontouchEvent中同样记下了三个action,以及返回super.onTouchEvent(event)

    public class EventActivity extends AppCompatActivity implements View.OnClickListener, View.OnTouchListener {
    
        private static final String TAG = EventActivity.class.getSimpleName();
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            View testView = findViewById(R.id.test_view);
            testView.setOnClickListener(this);
            testView.setOnTouchListener(this);
        }
    
        @Override
        public void onClick(View v) {
            Log.d(TAG, "onClick-----" + v.getClass().getSimpleName());
        }
    
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            int action = event.getAction();
            String actionName = "";
            if (action == MotionEvent.ACTION_DOWN) {
                actionName = "action_down";
            } else if (action == MotionEvent.ACTION_MOVE) {
                actionName = "action_move";
            } else if (action == MotionEvent.ACTION_UP) {
                actionName = "action_up";
            }
            Log.d(TAG, "onTouch-----" + v.getClass().getSimpleName() + ";" + actionName);
            return false;
        }
    }
    

    EventActivity里面分别设置了testViewonClickonTouch的监听,此时我们点击下testView,会发生如下的日志:

    image.png
    从日志上可以看出来先是触发了testViewdispatchTouchEvent--->onTouch--->onTouchEvent--->onClick
    用一张更详细的时序图可以表示如下:
    image.png
    下面再来验证开篇例子中onTouch返回true的情况流程是咋样的:
    image.png
    仅仅改了这么一处,看下如下日志:
    image.png
    从这里可以看出来testViewontouchEvent事件和onClick事件都没有被执行,也就是说:
    当view的setOnTouchListener的onTouch返回true的时候,testView的onTouchEvent和onClick事件都没有被触发,得到此结论后,咋们可以顺着源码找下去。
    首先看下setOnTouchListener把该OnTouchListener给谁了:
    image.png
    ListenerInfo getListenerInfo() {
        if (mListenerInfo != null) {
            return mListenerInfo;
        }
        mListenerInfo = new ListenerInfo();
        return mListenerInfo;
    }
    

    这里获取了一个ListenerInfo对象,在ListenerInfo对象中封装了各种监听器,这里就不一一说了,然后给到它的mOnTouchListener属性,其实setOnClickListener也是同样的做法:

    image.png
    其实setOnClickListener就是将view的clickable属性至为true了,并且将外面传过来的listener交给了ListenerInfo对象

    既然上面从日志上看是先是触发了testView的dispatchTouchEvent那下面看下该方法:

    public boolean dispatchTouchEvent(MotionEvent event) {
        //省略代码
        boolean result = false;
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            //如果li.mOnTouchListener.onTouch(this, event)返回true,该方法的result就直接返回true
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            //如果result=false,才会走下面的onTouchEvent
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        //省略代码
        return result;
    }
    

    注释写得很清楚了,如果我们传入的OnTouchListener中的onTouch返回true的话,就不会执行到onTouchEvent方法,直接返回了result=true,这也就验证了上面说的只会执行到dispatchTouchEventOnTouchListener中的onTouch方法。
    下面继续看onTouch返回false的情况,上面已经看到日志在action_up打印了下onClick的日志,咱们可以猜测onClick事件肯定是在onTouchEventACTION_UP调用的:

    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 (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                        handleTooltipUp();
                    }
                    if (!clickable) {
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
                    }
                    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 long press 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)) {
                                    //调用onClick事件的地方
                                    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;
            }
            return true;
        }
        return false;
    }
    

    上面注释写得很清楚在performClick中调用了OnClickListener中的onClick方法:

    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);
        notifyEnterOrExitForAutoFillIfNeeded(true);
        return result;
    }
    

    所以也正是验证了上面打印日志的顺序,下面再来看下其他的情况:

    • view中onTouchEvent直接返回true,看看日志如何

      image.png
      image.png
      这里可以看出来比默认返回super. onTouchEvent的情况少了onClick事件的回调,其实也很好理解,毕竟在没有调用super方法,肯定不会走里面的up回调的onClick事件。
    • view中onTouchEvent直接返回false,看看日志如何

      image.png
      image.png
      此处可以看到只走了action_down后面就没有了,可想而知,我们的onTouchEvent是view消费事件的核心,只要返回false什么都干不了,这里涉及到ViewGroup到View事件分发的原理,因此该篇先不做不过的陈述。
    • view中dispatchTouchEvent直接返回true,看看日志如何

      image.png
      image.png
      从日志上看,只执行了dispatchTouchEvent的三个action,因为没调用super. dispatchTouchEvent,所以其他的几个方法都不会被执行。
    • view中dispatchTouchEvent直接返回false,看看日志如何

      image.png
      image.png
      从日志上来看,只有dispatchTouchEvent的action_down也被触发了,该处也是ViewGroup到View事件的分发导致的,返回false,直接不让view的move和up事件继续往下走了。

    总结

    • 如果view的setOnTouchListeneronTouch方法返回true,view的onTouchEvent和view的onClick事件都不会被执行
    • view设置的setOnClick事件是在onTouchEvent方法的action_up中执行的
    • view的onTouchEvent事件如果直接返回true,事件相当于消费了,onClick事件不会被执行到,因为onClick是在super.onTouchEvent中的action_up中被执行的
    • view的onTouchEvent事件如果直接返回false,事件相当于传给了它的ViewGroup,这个留到viewGroup到view事件传递的时候讲,现在只需要记住就行
    • view的dispatchTouchEvent直接返回true,事件由于没有调用到super.dispatchTouchEvent,因此后续的setOnTouchListeneronTouch方法不会被执行,onTouchEvent也不会被执行到,以及setOnClickListener中的onClick方法也不会被执行
    • view的dispatchTouchEvent直接返回false,事件就交给了viewgroup,这个留到viewGroup到view事件传递的时候讲,现在只需要记住就行

    相关文章

      网友评论

        本文标题:android中view事件传递

        本文链接:https://www.haomeiwen.com/subject/eluyqctx.html