The distribution mechanism of Android events

It is important to understand the distribution mechanism of Andorid events. Event distribution is generally for a group of events, namely ACTION_DOWN > ACTION_UP or ACTION_DOWN > ACTION_MOVE… ACTION_UP, where the main methods of dispatchTouchEvent(MotionEvent event), onInterceptTouchEvent(MotionEvent event) (ViewGroup has, View does not have), onTouchEvent(MotionEvent Event), and event distribution is passed from top to bottom, i.e. first to parent, then to child. Here simply take a View wrapped inside the ViewGroup as an example. Analyze the event distribution process (ignore Activity, Window)

Events will first to a ViewGroup. DispatchTouchEvent (MotionEvent event), then make ViewGroup. OnInterceptTouchEvent (MotionEvent event) return values:

1. If false is returned, the event will be passed to view.dispatchTouchEvent (MotionEvent event). OnTouchEvent (MotionEvent event) is passed to the View’s view. onTouchEvent(MotionEvent Event). If the View does not consume the event, The event is returned to viewGroup.onTouchEvent (MotionEvent Event). If view.onTouchEvent (MotionEvent Event) consumes the event, the event is not returned to ViewGroup. This event distribution is over.

2. If the ViewGroup. OnInterceptTouchEvent (MotionEvent event) return values are true, namely intercept events, the event will be ViewGroup. OnTouchEvent (MotionEvent event), This event distribution is over.

3. A View can prevent a ViewGroup from intercepting an event, even if the event is blocked by the ViewGroup. Can through the getParent (). RequestDisallowInterceptTouchEvent (true).

General flow chart

I said that a View can prevent a ViewGroup from intercepting events, but with the exception of ACTION_DOWN, that is, for a set of events, with the exception of ACTION_DOWN, View can be in ViewGroup. OnInterceptTouchEvent (MotionEvent event) returns true, obtain discretion of events, the following screenshots android25 source code.

final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget ! = null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) ! = 0; if (! disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { intercepted = false; } } else { // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true; }Copy the code
@Override public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) ! = 0)) { // We're already in this state, assume our ancestors are too return; } if (disallowIntercept) { mGroupFlags |= FLAG_DISALLOW_INTERCEPT; } else { mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } // Pass it up to our parent if (mParent ! = null) { mParent.requestDisallowInterceptTouchEvent(disallowIntercept); }}Copy the code

When actionMasked = = MotionEvent. ACTION_DOWN | | mFirstTouchTarget! =null, disallowIntercept is determined by mGroupFlags, because FLAG_DISALLOW_INTERCEPT isa constant 0x80000, While mGroupFlags assignment is requestDisallowInterceptTouchEvent to change. When requestDisallowInterceptTouchEvent (true), disallowIntercept = true, go while the onInterceptTouchEvent (ev), intercepted=false to prevent ViewGroup interception.


Second, take RecyclerView drop-down refresh and pull-up loading as an example to analyze sliding conflict and solve

Take recyclerView as an example, you can also change listView. By default, the red part is the visible part, that is, the top and bottom are hidden and invisible. Here, we choose FrameLayout as the container. Therefore, FrameLayout and RecyclerView will produce co-sliding conflicts. Only when recyclerView content slides to the top and the gesture is down, the header will slowly slide into the visual range, or recyclerView content slides to the bottom and the gesture is up, the footer will slowly slide up into the visual range.


private boolean intercept; private float lastInterceptY; @Override public boolean onInterceptTouchEvent(MotionEvent ev) { float curInterceptY = ev.getY(); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: intercept = false; break; case MotionEvent.ACTION_MOVE: if (isRefreshing || isLoadMore) { intercept = false; } else { boolean isHeaderShow = headerParams.topMargin > -headerHeight; boolean isFooterShow = footerParams.topMargin < height; intercept = touchHelper ! = null && touchHelper.judgeIntercept(curInterceptY, lastInterceptY, isHeaderShow, isFooterShow, allowLoadMore); } break; case MotionEvent.ACTION_UP: intercept = false; break; } lastInterceptY = curInterceptY; return intercept; }Copy the code

    @Override
    public boolean judgeIntercept(float curInterceptY, float lastInterceptY, boolean isHeaderShow, boolean isFooterShow, boolean allowLoadMore) {
         boolean intercept;

         int firstVisiblePos = layoutManager.findFirstVisibleItemPosition();
         View firstView = rv.getChildAt(firstVisiblePos);
         if (firstVisiblePos == 0 && firstView.getTop() == 0) {
             intercept = curInterceptY > lastInterceptY || isHeaderShow;
         } else {
             if (allowLoadMore && layoutManager.findLastVisibleItemPosition() == layoutManager.getItemCount() - 1) {
                 intercept = curInterceptY < lastInterceptY || isFooterShow;
             } else {
                 intercept = false;
             }
         }

         return intercept;
    }Copy the code

ACTION_UP must also return false, because ACTION_UP must pass framelayout.onTouchEvent (ev) for a set of events ending in ACTION_UP. The child View can’t do whatever it needs to do for ACTION_UP.

3. In MotionEvent.ACTION_MOVE, determine whether to block the event based on the actual situation.

private float moveDis; @Override public boolean onTouchEvent(MotionEvent ev) { if (touchHelper ! = null) { float curTouchY = ev.getY(); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: moveDis = curTouchY - lastInterceptY; if (Math.abs(moveDis) < touchSlop) { break; } if (isRefreshing || isLoadMore) { break; } moveDis = moveDis * kMoveFactor; if (touchHelper.isContentSlideToTop()) { updateHeaderMargin(moveDis); } else if (touchHelper.isContentSlideToBottom()) { updateFooterMargin(moveDis); } break; case MotionEvent.ACTION_UP: if (moveDis > 0) { if (touchHelper.isContentSlideToTop()) { if (headerParams.topMargin < 0) { scrollHeaderByAnimator(headerParams.topMargin, -headerHeight); if (header ! = null) { header.onPullToRefresh(moveDis); } } else { scrollHeaderByAnimator(headerParams.topMargin, 0); if (header ! = null) { header.onRefreshing(); } isRefreshing = true; if (listener ! = null) { listener.onRefresh(); } } } } else { if (touchHelper.isContentSlideToBottom()) { if (footerParams.topMargin > height - footerHeight) { scrollFooterByAnimator(false, footerParams.topMargin, height); if (footer ! = null) { footer.onPullToLoadMore(moveDis); } } else { scrollFooterByAnimator(false, footerParams.topMargin, height - footerHeight); if (footer ! = null) { footer.onLoadMore(); } isLoadMore = true; if (listener ! = null) { listener.onLoadMore(); } } } } break; } } return true; }Copy the code
private void updateHeaderMargin(float moveDis) { moveDis = moveDis < 0 ? 0 : moveDis; headerParams.topMargin = (int) (-headerHeight + moveDis); headerVG.setLayoutParams(headerParams); setChildViewTopMargin((int) moveDis); if (header ! = null) { if (moveDis < headerHeight) { header.onPullToRefresh(moveDis); } else { header.onReleaseToRefresh(moveDis); } } } private void setChildViewTopMargin(int topMargin) { LayoutParams childParams = (LayoutParams) childView.getLayoutParams(); childParams.topMargin = topMargin; childView.setLayoutParams(childParams); } private void updateFooterMargin(float moveDis) { moveDis = moveDis > 0 ? 0 : moveDis; footerParams.topMargin = (int) (height + moveDis); footerVG.setLayoutParams(footerParams); setChildViewBottomMargin((int) Math.abs(moveDis)); scrollContentToBottom((int) -moveDis); if (footer ! = null) { if (Math.abs(moveDis) < footerHeight) { footer.onPullToLoadMore(moveDis); } else { footer.onReleaseToLoadMore(moveDis); } } } private void setChildViewBottomMargin(int bottomMargin) { LayoutParams childParams = (LayoutParams) childView.getLayoutParams(); childParams.bottomMargin = bottomMargin; childView.setLayoutParams(childParams); } private void scrollContentToBottom(int deltaY) { if (childView instanceof RecyclerView) { childView.scrollBy(0, deltaY); } else if (childView instanceof ListView) { ((ListView) childView).smoothScrollBy(deltaY, 0); } else if (childView instanceof ScrollView) { childView.scrollBy(0, deltaY); } } private void scrollHeaderByAnimator(float startY, float endY) { ValueAnimator animator = ValueAnimator.ofFloat(startY, endY); animator.setDuration(kDuration); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float floatValue = (float) animation.getAnimatedValue(); headerParams.topMargin = (int) floatValue; headerVG.setLayoutParams(headerParams); setChildViewTopMargin((int) (headerHeight + floatValue)); }}); animator.start(); } private void scrollFooterByAnimator(final boolean isAuto, float startY, float endY) { ValueAnimator animator = ValueAnimator.ofFloat(startY, endY); animator.setDuration(kDuration); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float floatValue = (float) animation.getAnimatedValue(); footerParams.topMargin = (int) floatValue; footerVG.setLayoutParams(footerParams); int bottomMargin = (int) (height - floatValue); setChildViewBottomMargin(bottomMargin); if (isAuto) { scrollContentToBottom(bottomMargin); if (footer ! = null) { footer.onPullToLoadMore(bottomMargin); if (bottomMargin == footerHeight) { footer.onLoadMore(); }}}}}); animator.start(); }Copy the code

Click open link

Imperfect point still need correction!!