Android-从重叠view响应问题到安卓事件分发机制

重叠view事件响应问题

工作当中遇到一个需求:有两个重叠并且全屏的Framelayout,交互逻辑是点击上层的button使得上层消失,下层可见;再点击下层使得下层消失上层出现。但是发现当点击上层非button区域时,下层也会响应点击事件,但是点击button区域则不会。这意味着点击事件穿过上层到达下层。对于安卓事件机制一知半解的我无从下手,上网搜了一下发现在上层FrameLayout下增加:

android:clickable="true"

这样的确可以保证上层点击事件不会到达下层,可是这是为什么呢?最初步的猜想是当上层FrameLayout的clickable属性为true时,点击事件已经被上层拦截了。这个和当你点击button时响应onclick()时的原理是不是一样?这就需要从源码分析了。

安卓事件分发机制基础

分析源码前先大致了解一下安卓事件分发机制。

  • 当点击事件到达一个viewgroup,首先会调用dispatchTouchEvent。如果该viewgroup的onInterceptTouchEvent返回true,表明拦截该事件,接着事件就会交给viewgoup来处理,意味着它的onTouchEvent会进行事件的处理。如果返回false,表明不拦截当前事件。这时事件会继续传递给它的子view,遍历调用子view的dispatchTouchEvent,直到事件被某个子view处理。如果所有子view都没有处理该事件,则事件会传递给父容器的onTouchEvent。以此类推,如果所有元素都不处理该事件,事件最终会传递给Activity处理,即Activity的onTouchEvent;
  • 当一个view调用dispatchTouchEvent进行事件处理时,由于没有子元素无法进行向下传递只能自己处理。首先会判断是否有设置OnTouchListener,如果OnTouchListener的onTouch返回true,那么onTouchEvent就不会调用。
/**
     * 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;
        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;
    }
  • 当view调用onTouchEvent处理事件时,从代码中可以看出只要view的Clickable和long_clickable中有一个为true,那么它就会消耗这个事件,即onTouchEvent返回true.
public boolean onTouchEvent(MotionEvent event) {
   ...
    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
        switch (event.getAction()) {
            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();
                            }
                        }
                    }
   ..
}

结论

综合上述对安卓事件分发的源码分析知道,当在上层FrameLayout中将clickable设置为true时,其实就是
当点击事件到达上层framelayout时,在onTouchEvent中对事件进行拦截,使得点击事件不会继续向下传递。当然,除了这个方法以外,也可以重写FrameLayout的onInterceptTouchEvent,在将这个函数的返回值为true,也可以实现拦截事件的作用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容