CoordinatorLayout,嵌套滑动,自定义Behavi

2018-12-02  本文已影响298人  暴走的小青春

上文说了CoordinatorLayout其子view的measure和layout后,要探究其最重要的滑动机制了,我们都知道在appbarlayout的子view设置了scroll_flag为scroll属性时会出现滑动现象那首先提一个问题:
1.为何触摸到appBarlayout时,nestScrollview会跟着appBarLayout就行滑动

这两个问题如果在linearlayout里当然用不着问,但是细细一想在coordinatorLayout里nestScrollview在appBarlayout下方本来就是onlayout里设置的,那滑动肯定的话肯定也做了特殊的处理,当然牵扯到滑动就要进行事件分发了,不了解的朋友可以看下事件分发之结论篇。当然默认先从coordinatorLayout的onInterceptTouchEvent说起:

@Override
   public boolean onInterceptTouchEvent(MotionEvent ev) {
       MotionEvent cancelEvent = null;

       final int action = ev.getActionMasked();

       // Make sure we reset in case we had missed a previous important event.
       if (action == MotionEvent.ACTION_DOWN) {
           resetTouchBehaviors(true);
       }

       final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);

       if (cancelEvent != null) {
           cancelEvent.recycle();
       }

       if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
           resetTouchBehaviors(true);
       }

       return intercepted;
   }

很明显重点在于performIntercept方法,让我们来看一下

private boolean performIntercept(MotionEvent ev, final int type) {
        boolean intercepted = false;
        boolean newBlock = false;

        MotionEvent cancelEvent = null;

        final int action = ev.getActionMasked();

        final List<View> topmostChildList = mTempList1;
        getTopSortedChildren(topmostChildList);

        // Let topmost child views inspect first
        final int childCount = topmostChildList.size();
        for (int i = 0; i < childCount; i++) {
            final View child = topmostChildList.get(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            final Behavior b = lp.getBehavior();

            if ((intercepted || newBlock) && action != MotionEvent.ACTION_DOWN) {
                // Cancel all behaviors beneath the one that intercepted.
                // If the event is "down" then we don't have anything to cancel yet.
                if (b != null) {
                    if (cancelEvent == null) {
                        final long now = SystemClock.uptimeMillis();
                        cancelEvent = MotionEvent.obtain(now, now,
                                MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
                    }
                    switch (type) {
                        case TYPE_ON_INTERCEPT:
                            b.onInterceptTouchEvent(this, child, cancelEvent);
                            break;
                        case TYPE_ON_TOUCH:
                            b.onTouchEvent(this, child, cancelEvent);
                            break;
                    }
                }
                continue;
            }

            if (!intercepted && b != null) {
                switch (type) {
                    case TYPE_ON_INTERCEPT:
                        intercepted = b.onInterceptTouchEvent(this, child, ev);
                        break;
                    case TYPE_ON_TOUCH:
                        intercepted = b.onTouchEvent(this, child, ev);
                        break;
                }
                if (intercepted) {
                    mBehaviorTouchView = child;
                }
            }

            // Don't keep going if we're not allowing interaction below this.
            // Setting newBlock will make sure we cancel the rest of the behaviors.
            final boolean wasBlocking = lp.didBlockInteraction();
            final boolean isBlocking = lp.isBlockingInteractionBelow(this, child);
            newBlock = isBlocking && !wasBlocking;
            if (isBlocking && !newBlock) {
                // Stop here since we don't have anything more to cancel - we already did
                // when the behavior first started blocking things below this point.
                break;
            }
        }

        topmostChildList.clear();

        return intercepted;
    }

这里获取到topmostChildList然后进行循环,对mBehaviorTouchView就行了赋值也就是这个coordinatorLayout的事件交给了哪个子view的behavior进行处理,先来看下nestScrollview处理了没有,很明显在其headScrollbehavior里没有对其进行复写,也就说取得就是默认值也就是false,然后看下appBarlayout的headBehavior

@Override
   public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
       if (mTouchSlop < 0) {
           mTouchSlop = ViewConfiguration.get(parent.getContext()).getScaledTouchSlop();
       }

       final int action = ev.getAction();

       // Shortcut since we're being dragged
       if (action == MotionEvent.ACTION_MOVE && mIsBeingDragged) {
           return true;
       }

       switch (ev.getActionMasked()) {
           case MotionEvent.ACTION_DOWN: {
               mIsBeingDragged = false;
               final int x = (int) ev.getX();
               final int y = (int) ev.getY();
               if (canDragView(child) && parent.isPointInChildBounds(child, x, y)) {
                   mLastMotionY = y;
                   mActivePointerId = ev.getPointerId(0);
                   ensureVelocityTracker();
               }
               break;
           }

           case MotionEvent.ACTION_MOVE: {
               final int activePointerId = mActivePointerId;
               if (activePointerId == INVALID_POINTER) {
                   // If we don't have a valid id, the touch down wasn't on content.
                   break;
               }
               final int pointerIndex = ev.findPointerIndex(activePointerId);
               if (pointerIndex == -1) {
                   break;
               }

               final int y = (int) ev.getY(pointerIndex);
               final int yDiff = Math.abs(y - mLastMotionY);
               if (yDiff > mTouchSlop) {
                   mIsBeingDragged = true;
                   mLastMotionY = y;
               }
               break;
           }

           case MotionEvent.ACTION_CANCEL:
           case MotionEvent.ACTION_UP: {
               mIsBeingDragged = false;
               mActivePointerId = INVALID_POINTER;
               if (mVelocityTracker != null) {
                   mVelocityTracker.recycle();
                   mVelocityTracker = null;
               }
               break;
           }
       }

       if (mVelocityTracker != null) {
           mVelocityTracker.addMovement(ev);
       }

       return mIsBeingDragged;
   }

这里我们看到在down的时候coordinatorLayout和appBarlayout都是返回的false,也就是说是交给appBarlayout的ontouchEvent处理的,然后在move的时候,如果当手指落在appBarlayout范围内且移动了,appBarlayout的拦截事件返回了true,那也就是说此时coordinatorLayout的拦截事件也返回了true,并会给appBarlayout的onTouchEvent事件发送一个cancel事件,就表明此事件要自己处理了,也就是说当在move的时候会调用coordinatorLayout的ontouchevent方法,我们来看下

@Override
    public boolean onTouchEvent(MotionEvent ev) {
        boolean handled = false;
        boolean cancelSuper = false;
        MotionEvent cancelEvent = null;

        final int action = ev.getActionMasked();

        if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) {
            // Safe since performIntercept guarantees that
            // mBehaviorTouchView != null if it returns true
            final LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams();
            final Behavior b = lp.getBehavior();
            if (b != null) {
                handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
            }
        }

        // Keep the super implementation correct
        if (mBehaviorTouchView == null) {
            handled |= super.onTouchEvent(ev);
        } else if (cancelSuper) {
            if (cancelEvent == null) {
                final long now = SystemClock.uptimeMillis();
                cancelEvent = MotionEvent.obtain(now, now,
                        MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
            }
            super.onTouchEvent(cancelEvent);
        }

        if (!handled && action == MotionEvent.ACTION_DOWN) {

        }

        if (cancelEvent != null) {
            cancelEvent.recycle();
        }

        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
            resetTouchBehaviors(false);
        }

        return handled;
    }

这里mBehaviorTouchView肯定是有的,也就是appBarlayout,进入这个if语句,发现其还是交给appBarlayout的onTouchEvent处理的

@Override
    public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
        if (mTouchSlop < 0) {
            mTouchSlop = ViewConfiguration.get(parent.getContext()).getScaledTouchSlop();
        }

        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN: {
                final int x = (int) ev.getX();
                final int y = (int) ev.getY();

                if (parent.isPointInChildBounds(child, x, y) && canDragView(child)) {
                    mLastMotionY = y;
                    mActivePointerId = ev.getPointerId(0);
                    ensureVelocityTracker();
                } else {
                    return false;
                }
                break;
            }

            case MotionEvent.ACTION_MOVE: {
                final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
                if (activePointerIndex == -1) {
                    return false;
                }

                final int y = (int) ev.getY(activePointerIndex);
                int dy = mLastMotionY - y;

                if (!mIsBeingDragged && Math.abs(dy) > mTouchSlop) {
                    mIsBeingDragged = true;
                    if (dy > 0) {
                        dy -= mTouchSlop;
                    } else {
                        dy += mTouchSlop;
                    }
                }

                if (mIsBeingDragged) {
                    mLastMotionY = y;
                   

                    // We're being dragged so scroll the ABL
                    scroll(parent, child, dy, getMaxDragOffset(child), 0);
                }
                break;
            }

            case MotionEvent.ACTION_UP:
                if (mVelocityTracker != null) {
                    mVelocityTracker.addMovement(ev);
                    mVelocityTracker.computeCurrentVelocity(1000);
                    float yvel = mVelocityTracker.getYVelocity(mActivePointerId);
                    fling(parent, child, -getScrollRangeForDragFling(child), 0, yvel);
                }
                // $FALLTHROUGH
            case MotionEvent.ACTION_CANCEL: {
                mIsBeingDragged = false;
                mActivePointerId = INVALID_POINTER;
                if (mVelocityTracker != null) {
                    mVelocityTracker.recycle();
                    mVelocityTracker = null;
                }
                break;
            }
        }

        if (mVelocityTracker != null) {
            mVelocityTracker.addMovement(ev);
        }

        return true;
    }

到这里相信大家能叫看出来了,move的时候简单来说就调用了scroll方法最终也是调用了其

@Override
       int setHeaderTopBottomOffset(CoordinatorLayout coordinatorLayout,
                                    AppBarLayout appBarLayout, int newOffset, int minOffset, int maxOffset) {
           final int curOffset = getTopBottomOffsetForScrollingSibling();
        
           int consumed = 0;

           if (minOffset != 0 && curOffset >= minOffset && curOffset <= maxOffset) {
               // If we have some scrolling range, and we're currently within the min and max
               // offsets, calculate a new offset
               newOffset = MathUtils.clamp(newOffset, minOffset, maxOffset);
               if (curOffset != newOffset) {
                   final int interpolatedOffset = appBarLayout.hasChildWithInterpolator()
                           ? interpolateOffset(appBarLayout, newOffset)
                           : newOffset;

                   final boolean offsetChanged = setTopAndBottomOffset(interpolatedOffset);

                   // Update how much dy we have consumed
                   consumed = curOffset - newOffset;
               
                   // Update the stored sibling offset
                   mOffsetDelta = newOffset - interpolatedOffset;

                   if (!offsetChanged && appBarLayout.hasChildWithInterpolator()) {
                       // If the offset hasn't changed and we're using an interpolated scroll
                       // then we need to keep any dependent views updated. CoL will do this for
                       // us when we move, but we need to do it manually when we don't (as an
                       // interpolated scroll may finish early).
                       coordinatorLayout.dispatchDependentViewsChanged(appBarLayout);
                   }

                   // Dispatch the updates to any listeners
                   appBarLayout.dispatchOffsetUpdates(getTopAndBottomOffset());

                   // Update the AppBarLayout's drawable state (for any elevation changes)
                   updateAppBarLayoutDrawableState(coordinatorLayout, appBarLayout, newOffset,
                           newOffset < curOffset ? -1 : 1, false);
               }
           } else {
               // Reset the offset delta
               mOffsetDelta = 0;
           }

           return consumed;
       }

这个consumed也就是相对于每次滑动多少距离,当然向下滑动的话
consumed是>0的,向上反之。
setTopAndBottomOffset方法最终也是调用了ViewOffsetHelper的updateOffsets

 private void updateOffsets() {

       ViewCompat.offsetTopAndBottom(mView, mOffsetTop - (mView.getTop() - mLayoutTop));

       ViewCompat.offsetLeftAndRight(mView, mOffsetLeft - (mView.getLeft() - mLayoutLeft));
   }

可以看到并不是通过scroller滑动的,直接的改变了位置,而mLayoutTop就是我们一开始传进来初始的appBarlayout的位置

那我们分析到现在,也没有找到问题一的答案,为何appBarlayout滑动了,nestScrollview会滑动?

其实啊当调用ViewCompat.offsetTopAndBottom方法后,大家可以看下其源码会进行一次重绘,这并不会调用次view的draw方法,因为源码里其设置的invalidateCache参数是false,也就是说少了一个标志,
但是对整个view树会执行performTraversals方法,那有啥监听可以监听到么,其实是有的也就是preDrawListener

boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;

      if (!cancelDraw && !newSurface) {
          if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
              for (int i = 0; i < mPendingTransitions.size(); ++i) {
                  mPendingTransitions.get(i).startChangingAnimations();
              }
              mPendingTransitions.clear();
          }

          performDraw();
      } else {
}

也就是在执行performDraw的上面会执行dispatchOnPreDraw也就是会调用onPreDraw的监听,我们再来看下coordinatorLayout的onpreDraw

 class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
        @Override
        public boolean onPreDraw() {
            onChildViewsChanged(EVENT_PRE_DRAW);
            return true;
        }
    }

很明显又回到了这里,也就是第一篇文章分析的那里,其实又回调了onDependentViewChanged方法,这才导致了nestScrollview的滑动
也就真相大白了,想不到一个简单的滑动在coordinatorLayout竟如此绕,这也是它可以定制化的魅力所在吧

上一篇 下一篇

猜你喜欢

热点阅读