(Android面试必知必会系列)Android事件分发

本篇文章主要结合面试中的问题,从以下几个方面分析Android事件分发,为方便理解,源码分析尽量点到为止,避免深入源码不可自拔。

  1. 通过重写dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent三个方法分析事件分发的流程
  2. 结合源码分析为什么是这样的
  3. 根据面试中常见的事件分发问题做一个回答
盗一张图

一、场景

activity 中放一个ViewGroup(红色的RelativeLayout),
ViewGroup中放一个View(灰色的TextView)

下文提到的ViewGroup指的是图中的RelativeLayout
下文提到的View指的是图中的TextView

image.png

二、重写方法进行分析

(假设你对事件分发的三个主要方法的意思已经清楚 )

Activity:
dispatchTouchEvent()
onTouchEvent

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        LogHelper.d("Activity->dispatchTouchEvent:" + getEventName(ev));
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        LogHelper.d("Activity->onTouchEvent:" + getEventName(event));
        return super.onTouchEvent(event);
    }

    private String getEventName(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            return "ACTION_DOWN";
        } else if (event.getAction() == MotionEvent.ACTION_UP) {
            return "ACTION_UP";
        } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
            return "ACTION_MOVE";
        } else {
            return event.getAction() + "";
        }
    }

ViewGroup:
dispatchTouchEvent()
onInterceptTouchEvent()
onTouchEvent()

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        LogHelper.d("ViewGroup->dispatchTouchEvent:" + getEventName(ev));
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        LogHelper.d("ViewGroup->onInterceptTouchEvent:" + getEventName(ev));
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        LogHelper.d("ViewGroup->onTouchEvent:" + getEventName(event));
        return super.onTouchEvent(event);
    }

    private String getEventName(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            return "ACTION_DOWN";
        } else if (event.getAction() == MotionEvent.ACTION_UP) {
            return "ACTION_UP";
        } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
            return "ACTION_MOVE";
        } else {
            return event.getAction() + "";
        }
    }

View:
dispatchTouchEvent()
onTouchEvent()
(注意:没有onInterceptTouchEvent()方法)

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        LogHelper.d("View->dispatchTouchEvent:" + getEventName(event));
        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        LogHelper.d("View->onTouchEvent:" + getEventName(event));
        return super.onTouchEvent(event);
    }

    private String getEventName(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            return "ACTION_DOWN";
        } else if (event.getAction() == MotionEvent.ACTION_UP) {
            return "ACTION_UP";
        } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
            return "ACTION_MOVE";
        } else if (event.getAction() ==MotionEvent.ACTION_CANCEL) {
            return "ACTION_CANCEL";
        } else {
            return event.getAction() + "";
        }
    }

三、开始分析,打印日志

1. 默认返回值

点击Activity空白处

D: Activity->dispatchTouchEvent:ACTION_DOWN
D: Activity->onTouchEvent:ACTION_DOWN
D: Activity->dispatchTouchEvent:ACTION_MOVE
D: Activity->onTouchEvent:ACTION_MOVE
D: Activity->dispatchTouchEvent:ACTION_MOVE
D: Activity->onTouchEvent:ACTION_MOVE
D: Activity->dispatchTouchEvent:ACTION_UP
D: Activity->onTouchEvent:ACTION_UP

由于点击空白处,事件到了Activity之后,没有传到里面的ViewGroup里,所以最终没人处理,就只能调用自己的onTouchEvent()方法,

点击ViewGroup外层

D: Activity->dispatchTouchEvent:ACTION_DOWN
D: ViewGroup->dispatchTouchEvent:ACTION_DOWN
D: ViewGroup->onInterceptTouchEvent:ACTION_DOWN
D: ViewGroup->onTouchEvent:ACTION_DOWN
D: Activity->onTouchEvent:ACTION_DOWN
D: Activity->dispatchTouchEvent:ACTION_MOVE
D: Activity->onTouchEvent:ACTION_MOVE
D: Activity->dispatchTouchEvent:ACTION_UP
D: Activity->onTouchEvent:ACTION_UP

首先是down事件,从Activity开始,传到ViewGroup,经过ViewGroup的dispatchTouchEvent->onInterceptTouchEvent->onTouchEvent,ViewGroup默认没有消费事件,所以down事件还给Activity处理,既然你不处理down事件,那么后续的Move,Up事件不再询问ViewGroup,直接给Activity处理了。

点击View

D: Activity->dispatchTouchEvent:ACTION_DOWN
D: ViewGroup->dispatchTouchEvent:ACTION_DOWN
D: ViewGroup->onInterceptTouchEvent:ACTION_DOWN
D: View->dispatchTouchEvent:ACTION_DOWN
D: View->onTouchEvent:ACTION_DOWN
D: ViewGroup->onTouchEvent:ACTION_DOWN
D: Activity->onTouchEvent:ACTION_DOWN
D: Activity->dispatchTouchEvent:ACTION_MOVE
D: Activity->onTouchEvent:ACTION_MOVE
D: Activity->dispatchTouchEvent:ACTION_UP
D: Activity->onTouchEvent:ACTION_UP

View没有消费down事件,所以down事件往上传递给ViewGroup,ViewGroup也不消费,所以最终交给Activity处理,之后的Move、Up事件都直接给Activity处理,没有调用ViewGroup的dispatchTouchEvent和onInterceptTouchEvent

2. ViewGroup、View消费事件的情况

View设置点击事件

view.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                LogHelper.d("view onClick");
            }
        });

看下日志

D: Activity->dispatchTouchEvent:ACTION_DOWN
D: ViewGroup->dispatchTouchEvent:ACTION_DOWN
D: ViewGroup->onInterceptTouchEvent:ACTION_DOWN
D: View->dispatchTouchEvent:ACTION_DOWN
D: View->onTouchEvent:ACTION_DOWN
D: Activity->dispatchTouchEvent:ACTION_MOVE
D: ViewGroup->dispatchTouchEvent:ACTION_MOVE
D: ViewGroup->onInterceptTouchEvent:ACTION_MOVE
D: View->dispatchTouchEvent:ACTION_MOVE
D: View->onTouchEvent:ACTION_MOVE
D: Activity->dispatchTouchEvent:ACTION_UP
D: ViewGroup->dispatchTouchEvent:ACTION_UP
D: ViewGroup->onInterceptTouchEvent:ACTION_UP
D: View->dispatchTouchEvent:ACTION_UP
D: View->onTouchEvent:ACTION_UP
D: view onClick

down、move、up事件都从上到下传递,由于View设置了点击监听,消费了事件,所以事件到View的onTouhEvent就结束了。

点击事件源码分析

看下设置点击事件发生了什么

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

public boolean onTouchEvent(MotionEvent event) {

      ...
     switch(action) {
    ...
     case MotionEvent.ACTION_UP:
    ...
          performClick(); //ACTION_UP 的时候才响应点击事件
    ...
    }

   if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        //如果是可点击的状态,返回true,消费事件
       return true;
   }
}

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;
        } 
      ...
        return result;
    }

可以看到performClick是在onTouchEvent 的Up事件中回调的,也就是在事件的最尾部,然后如果控件是可点击的,就返回true消费事件。

ViewGroup设置点击事件

viewGroup.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                LogHelper.d("ViewGroup onClick");
            }
        });

点击View(设置了点击事件)的情况日志如下

D: Activity->dispatchTouchEvent:ACTION_DOWN
D: ViewGroup->dispatchTouchEvent:ACTION_DOWN
D: ViewGroup->onInterceptTouchEvent:ACTION_DOWN
D: View->dispatchTouchEvent:ACTION_DOWN
D: View->onTouchEvent:ACTION_DOWN
D: Activity->dispatchTouchEvent:ACTION_UP
D: ViewGroup->dispatchTouchEvent:ACTION_UP
D: ViewGroup->onInterceptTouchEvent:ACTION_UP
D: View->dispatchTouchEvent:ACTION_UP
D: View->onTouchEvent:ACTION_UP
D: view onClick

黑人问号,并没有调用ViewGroup的onClick???

因为ViewGroup默认是没有拦截事件的,看下源码

ViewGroup#onInterceptTouchEvent

public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getX(), ev.getY())) {
            return true;
        }
        //默认返回false
        return false;
    }

事件传到View(可点击)的时候被消费掉了。我们要让ViewGroup处理点击事件要怎么办?

答案就是重写onInterceptTouchEvent 方法,返回true,拦截事件

public boolean onInterceptTouchEvent(MotionEvent ev) {
        LogHelper.d("ViewGroup->onInterceptTouchEvent:" + getEventName(ev));
//        return super.onInterceptTouchEvent(ev);
        return true;
    }

点击View,看下日志

D: Activity->dispatchTouchEvent:ACTION_DOWN
D: ViewGroup->dispatchTouchEvent:ACTION_DOWN
D: ViewGroup->onInterceptTouchEvent:ACTION_DOWN
D: ViewGroup->onTouchEvent:ACTION_DOWN
D: Activity->dispatchTouchEvent:ACTION_MOVE
D: ViewGroup->dispatchTouchEvent:ACTION_MOVE
D: ViewGroup->onTouchEvent:ACTION_MOVE
D: Activity->dispatchTouchEvent:ACTION_UP
D: ViewGroup->dispatchTouchEvent:ACTION_UP
D: ViewGroup->onTouchEvent:ACTION_UP
D: ViewGroup onClick

可以看到
onInterceptTouchEvent 返回true,拦截了事件,表示自己要处理,事件就不会向下传递

onInterceptTouchEvent 返回值意义知道了,那dispatchTouchEvent 的返回值代表什么呢?将ViewGroup的 dispatchTouchEvent 返回true看下结果

D: Activity->dispatchTouchEvent:ACTION_DOWN
D: ViewGroup->dispatchTouchEvent:ACTION_DOWN
D: Activity->dispatchTouchEvent:ACTION_MOVE
D: ViewGroup->dispatchTouchEvent:ACTION_MOVE
D: Activity->dispatchTouchEvent:ACTION_MOVE
D: ViewGroup->dispatchTouchEvent:ACTION_MOVE
D: Activity->dispatchTouchEvent:ACTION_UP
D: ViewGroup->dispatchTouchEvent:ACTION_UP

dispatchTouchEvent 返回true使得 onInterceptTouchEvent 没有被调用,事件就结束了,看下ViewGroup 的 dispatchTouchEvent 方法

ViewGroup#dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev) {
        ...
        if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                // 1、调用requestDisallowInterceptTouchEvent(true)会改变mGroupFlags,disallowIntercept会为true,
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    //2、如果是down事件,调用onInterceptTouchEvent,询问是否拦截
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            }
              //3.如果子view调用 requestDisallowInterceptTouchEvent ,intercepted就为false,事件会
              if (!canceled && !intercepted) {
              ...
                        for (int i = childrenCount - 1; i >= 0; i--) {
              ...
                                dispatchTransformedTouchEvent
                        }
              }
             ...
            if (mFirstTouchTarget == null) {
                // 4.反回值由这个方法决定
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            }
        ...
        return handled;
}


private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        // Canceling motions is a special case.  We don't need to perform any transformations
        // or filtering.  The important part is the action, not the contents.
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            //没有子孩子则调用父类(View)的dispatchTouchEvent
            //否则调用子view的dispatchTouchEvent
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }
}

小结一下,ViewGroup#dispatchTouchEvent,内部是这样的:

1.如果是down事件,调用onInterceptTouchEvent,询问是否拦截
2.特殊情况,子view调用requestDisallowInterceptTouchEvent(true),将intercepted设置为false, 此时onInterceptTouchEvent失效,事件会传给子view,看注释1
3.常规情况,如果判断子view为空,就调用父类(View)的dispatchTouchEvent;如果有子view,那么返回值由子view的dispatchTouchEvent决定

看下View的dispatchTouchEvent。

View#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;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                //1
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                //2
                result = true;
            }
        }
        ...
        return result;
}

注释1:如果View设置了OnTouchListener并且onTouch返回true,则dispatchTouchEvent返回true,onTouchEvent没机会调用

注释2:如果没有设置onTouch返回true,那么dispatchTouchEvent的返回值由onTouchEvent决定。

3. onTouchEvent() 返回值不同的情况

前面已经分析了dispatchTouchEvent,onInterceptTouchEvent,剩下一个onTouchEvent

让View 的 onTouchEvent 返回true

D: Activity->dispatchTouchEvent:ACTION_DOWN
D: ViewGroup->dispatchTouchEvent:ACTION_DOWN
D: ViewGroup->onInterceptTouchEvent:ACTION_DOWN
D: View->dispatchTouchEvent:ACTION_DOWN
D: View->onTouchEvent:ACTION_DOWN
D: Activity->dispatchTouchEvent:ACTION_MOVE
D: ViewGroup->dispatchTouchEvent:ACTION_MOVE
D: ViewGroup->onInterceptTouchEvent:ACTION_MOVE
D: View->dispatchTouchEvent:ACTION_MOVE
D: View->onTouchEvent:ACTION_MOVE
D: Activity->dispatchTouchEvent:ACTION_UP
D: ViewGroup->dispatchTouchEvent:ACTION_UP
D: ViewGroup->onInterceptTouchEvent:ACTION_UP
D: View->dispatchTouchEvent:ACTION_UP
D: View->onTouchEvent:ACTION_UP

View是有设置点击事件的,onTouchEvent 返回true之后点击事件没有走,因为
performClick是在父类View的onTouchEvent的Up事件调用的,现在super.onTouchEvent没有调用,所以不会走onClick。

4. onTouch() 返回值不同的情况


        view.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                LogHelper.d("child onTouch");
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        break;
                    case MotionEvent.ACTION_MOVE:
                        break;
                    case MotionEvent.ACTION_UP:
                        return true;
//                        break;
                }
                /**
                 * onTouch 返回true 就不会走onClick,因为OnTouchEvet不会走
                 * 见源码 dispachTouchEvent()
                 * */
                return false;
            }
        });

ACTION_UP返回true,看下结果

D: Activity->dispatchTouchEvent:ACTION_DOWN
D: ViewGroup->dispatchTouchEvent:ACTION_DOWN
D: ViewGroup->onInterceptTouchEvent:ACTION_DOWN
D: View->dispatchTouchEvent:ACTION_DOWN
D: child onTouch
D: View->onTouchEvent:ACTION_DOWN
D: Activity->dispatchTouchEvent:ACTION_MOVE
D: ViewGroup->dispatchTouchEvent:ACTION_MOVE
D: ViewGroup->onInterceptTouchEvent:ACTION_MOVE
D: View->dispatchTouchEvent:ACTION_MOVE
D: child onTouch
D: View->onTouchEvent:ACTION_MOVE
D: Activity->dispatchTouchEvent:ACTION_UP
D: ViewGroup->dispatchTouchEvent:ACTION_UP
D: ViewGroup->onInterceptTouchEvent:ACTION_UP
D: View->dispatchTouchEvent:ACTION_UP
D: child onTouch

可看到 onTouchEvent 的 ACTION_UP没有调用,原因就是因为 dispachTouchEvent 里面判断如果设置了TouchListener,先调用了onTouch,如果onTouch 返回true,就不会调用OnTouchEvent。忘记了吗,看下代码:

View#dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent event) {
        boolean result = false;
...
            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;
            }
...
}

到此我们对事件分发就有了一个大概的理解,接下来主要针对面试中的问题做一个分析。

三、面试中的事件分发问题

1. 说一下事件分发的流程

事件从Activity开始分发,看下Activity的dispatchTouchEvent

Activity#dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        //交给window
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
}

Activity先把事件交给Window,Window一个抽象类,唯一的实现类是PhoneWindow

PhoneWindow#superDispatchTouchEvent

 public boolean superDispatchTouchEvent(MotionEvent event) {
        return this.mDecor.superDispatchTouchEvent(event);
}

PhoneWindow把事件交给DecorView

DecorView#superDispatchTouchEvent

public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
}

小结:DecorView是一个FrameLayout,所以事件就从Activity传到PhoneWindow再传到ViewGroup里了,然后就是ViewGroup的事件分发流程

ViewGroup主要三个方法:
分发(dispatchTouchEvent)、拦截(onInterceptTouchEvent)、处理(onTouchEvent)

View没有拦截(onInterceptTouchEvent)的方法,收到事件,dispatchTouchEvent肯定会被调用,返回true或者false代表是否自己要处理事件。

这里可能有疑问了,PhoneWindow是怎么跟Activity关联的,DecorView又是怎么跟Window关联的?

2.Activity、Window、DecorView的关系

简单来说就是Activity里有一个Window,Window里面有一个DecorView,我们通过setContentView添加的布局就是添加到DecorView里面的FrameLayout中。

以下是部分源码分析,加深理解

Activity#setContentView
public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
 }

我们在Activity的onCrate方法调用setContentView添加的布局最终是调用getWindow() 的setContentView

Activity#getWindow

public Window getWindow() {
        return mWindow;
}

final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback) {
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);
        //实例化
        mWindow = new PhoneWindow(this, window, activityConfigCallback);

}

void makeVisible() {
        //1
        if (!mWindowAdded) {
            ViewManager wm = getWindowManager();
            wm.addView(mDecor, getWindow().getAttributes());
            mWindowAdded = true;
        }
        //2
        mDecor.setVisibility(View.VISIBLE);
}

可以看到,Activity内部有一个Window实例,是在attach方法实例化的,
注释1:在Activity可见的时候,通过WindowManager将DecorView添加到Window
注释2:DecorView设置可见,也就说明了DecorView是Activity根布局

PhoneWindow#setContentView

    public void setContentView(View view, LayoutParams params) {
        //1
        if (this.mContentParent == null) {
            this.installDecor();
        } else if (!this.hasFeature(12)) {
            this.mContentParent.removeAllViews();
        }

        if (this.hasFeature(12)) {
            view.setLayoutParams(params);
            Scene newScene = new Scene(this.mContentParent, view);
            this.transitionTo(newScene);
        } else {
            //2 
            this.mContentParent.addView(view, params);
        }
...
        this.mContentParentExplicitlySet = true;
    }

注释1:如果mContentParent空,则则调用installDecor
注释2:将我们的布局添加到mContentParent

PhoneWindow#installDecor

private void installDecor() {
        this.mForceDecorInstall = false;
        if (this.mDecor == null) {
            //mDecor空则初始化
            this.mDecor = this.generateDecor(-1);
            ...
        } else {
            //绑定Window
            this.mDecor.setWindow(this);
        }

        if (this.mContentParent == null) {
            //mContentParent 初始化,generateLayout里面根据不同主题mContentParent指向的内容会button
            this.mContentParent = this.generateLayout(this.mDecor);
            this.mDecor.makeOptionalFitsSystemWindows();
            //decorContentParent 初始化
            DecorContentParent decorContentParent = (DecorContentParent)this.mDecor.findViewById(16908823);
            int transitionRes;
            //标题栏设置
            if (decorContentParent == null) {
                this.mTitleView = (TextView)this.findViewById(16908310);
                if (this.mTitleView != null) {
                    if ((this.getLocalFeatures() & 2) != 0) {
                        View titleContainer = this.findViewById(16909374);
                        if (titleContainer != null) {
                            titleContainer.setVisibility(8);
                        } else {
                            this.mTitleView.setVisibility(8);
                        }

                        this.mContentParent.setForeground((Drawable)null);
                    } else {
                        this.mTitleView.setText(this.mTitle);
                    }
                }
            } 
}

小结:从上面分析,我们知道Activity、Window、DecorView之间的关系是:
Activity里有一个Window,Window里有一个DecorView,我们添加的布局就放在DecorView里的一个FrameLayout里。

3.ScrollView嵌套RecyclerView,如何解决滑动冲突?

RecyclerView 可以滑动的时候我们希望滑动的是RecyclerView,RecyclerView不能滑动的时候才去滑动ScrollView

滑动冲突的方案一般有两种

  1. 外部拦截法
  2. 内部拦截法

外部拦截

重写ScrollView的onInterceptTouchEven,在需要自己处理的时候返回true拦截事件,其它情况都不拦截

public boolean onInterceptTouchEvent(MotionEvent ev) {
        //拦截条件
        if (needIntercepter(ev)){
            return true;
        }
        return super.onInterceptTouchEvent(ev);
}

    /**
     * 外部拦截的条件
     * @return
     */
    private boolean needIntercept(MotionEvent event) {
        if (mRecyclerView == null){
            return true;
        }

        /**判断RecyclerView到达顶部或者底部,就拦截*/
        // 测试view是否在点击范围内

        /**g etRawX是以屏幕左上角为坐标做预案,获取X坐标轴上的值。*/
        float x = event.getRawX();
        float y = event.getRawY();

        int[] locate = new int[2];
        mRecyclerView.getLocationOnScreen(locate);
        left = locate[0];
        right = left + mRecyclerView.getWidth();
        top = locate[1];
        bottom = top + mRecyclerView.getHeight();

        if (top > y || y > bottom) {
            return true;
        }
        return false;
    }

内部拦截法

1.RecyclerView 通过调用requestDisallowInterceptTouchEvent(true),请求父控件不要拦截我的事件
2.RecyclerView判断不需要的事件,调用requestDisallowInterceptTouchEvent(false),交给ScrollView去处理。

详细步骤

  1. ScrollView 需要重写 onInterceptTouchEvent,不拦截down事件,返回false,同时意味着自己的onTouchEvent的down事件永远不会调用到,所以手动调用onTouchEvent
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        /** 内部拦截法需要父布局不拦截 ACTION_DOWN ,否则所有事件都给父布局了*/
        if (ev.getAction() == MotionEvent.ACTION_DOWN){
            //手动调用onTouchEvent
            onTouchEvent(ev);
            return false;
        }
        return super.onInterceptTouchEvent(ev);
    }
  1. RecyclerView分发到down事件的时候,调用requestDisallowInterceptTouchEvent(true),事件就会传过来。然后在move事件中判断如果不需要处理事件,就调用 requestDisallowInterceptTouchEvent(false),将事件交给ScrollView 处理,因为我们手动调用了ScrollView的onTouchEvent的down事件,所以对ScrollView来说,仍然是一个完整的事件序列。
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN){
            getParent().getParent().requestDisallowInterceptTouchEvent(true);
        }else if (ev.getAction() == MotionEvent.ACTION_MOVE){

            //向上滑动,且RecyclerView到底了
            if (ev.getY() < lastY){
                if (!canScrollVertically(1)){
                    getParent().getParent().requestDisallowInterceptTouchEvent(false);
                }

            //向下滑动,且RecyclerView到顶部了
            }else if(ev.getY() > lastY){
                if (!canScrollVertically(-1)){
                    getParent().getParent().requestDisallowInterceptTouchEvent(false);
                }
            }
        }

        lastY = ev.getY();
        return super.dispatchTouchEvent(ev);
    }

好了,本篇文章到此就结束了,总结一下:

  1. 是什么?
    通过重写事件分发的三个方法,了解事件分发的大概流程

  2. 为什么?
    结合ViewGroup、View事件分发源码进行原理分析

  3. 怎么做?
    列举了面试中的几个问题,最后是事件冲突的解决办法。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容