美文网首页
CoordinatorLayout,嵌套滑动,自定义Behavi

CoordinatorLayout,嵌套滑动,自定义Behavi

作者: 暴走的小青春 | 来源:发表于2018-12-02 14:40 被阅读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竟如此绕,这也是它可以定制化的魅力所在吧

    相关文章

      网友评论

          本文标题:CoordinatorLayout,嵌套滑动,自定义Behavi

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