美文网首页
CoordinatorLayout 与 Behavior

CoordinatorLayout 与 Behavior

作者: yxhuang | 来源:发表于2019-10-15 09:20 被阅读0次

    CoordinatorLayout 与 Behavior

    CoordinatorLayout 的使用

    先看官网对 CoordinatorLayout 的介绍
    CoordinatorLayout is a super_powered FrameLayout。

    CoordinatorLayout is intended for two primary use cases:

    1. As a top-level application decor or chrome layout;

    2. As a container for a specific interaction with one or more child views

    我们常用的是第二种情况居多

    CoordinatorLayout 结合 AppBarLayout,CollapsingToolbarLayout 和 Toolbar 一起使用,可以给我们的应用带来更多的交互效果。
    它们的布局关系

    <android.support.design.widget.CoordinatorLayout...>
        <android.support.design.widget.AppBarLayout...>
            <android.support.design.widget.CollapsingToolbarLayout...>
                <!-- your collapsed view -->
                <View.../>
                <android.support.v7.widget.Toolbar.../>
            </android.support.design.widget.CollapsingToolbarLayout>
        </android.support.design.widget.AppBarLayout>
    
        <!-- Scroll view -->
        <android.support.v7.widget.RecyclerView.../>
    </android.support.design.widget.CoordinatorLayout>
    

    这是效果图


    coor_1.gif

    先看看它们的几个属性常用的几个布局元素

    CoordinatorLayout

    这几个属性都是直接在子布局中使用的

    app:layout_behavior

    指定子 View 的 behavior, 关于 behavior 后面会有详细的论述

    app:layout_anchor

    指定锚点的 View

    app:layout_anchorGravity

    指相对于锚 view 的布局重心

    app:layout_keyline

    AppBarLayout

    AppBarLayout 一般作为 CoordinatorLayout 的直接子类使用;
    AppBarLayout 的子 View 通过设置自身的 srollFlags 进行希望的滑动行为;
    如果把 AppBarLayout 放到普通的 ViewGroup 中而不是 CoordinatorLayout 中,AppBarLayout 的功能将不会起作用

    app:layout_scrollFlags/ setScrollFlags(int)

    app:layout_scrollFlags 标记位是子布局设置是否可滑动

    • scroll: 滑动

    • enterAlways: 获取屏幕外,向下滑,会重新出现

    • exitUntilCollapsed 滑动一定距离出屏幕,会收叠成 minHeight

    • snap: 在活动停止之后,会自动滑动靠边的一侧

    • enterAlwaysCollapsed:要与 minHeight 和 enterAlways 一起使用, 当 View 达到 minHeight 的高度时,CollapsingToolbarLayout 开始展开展开完之后,才会进行滚动

      <android.support.design.widget.CoordinatorLayout
      
       ...
       
       <android.support.design.widget.CollapsingToolbarLayout
               android:id="@+id/collapsing_toolbar_layout"
               android:layout_width="match_parent"
               android:layout_height="220dp"
               android:fitsSystemWindows="true"
               android:minHeight="100dp"
               app:expandedTitleMarginStart="38dp"
               app:layout_scrollFlags="scroll|enterAlwaysCollapsed|enterAlways"/>
              
       .../>
      

    app:expanded

    设置 AppBarLayout 是否展开

    CollapsingToolbarLayout

    CollapsingToolbarLayout 是一个实现了折叠功能包裹 Toolbar 的 View, 它做一位 AppBarLayout 子 View 使用

    app:layout_collapseMode

    • pin 固定,钉住
    • parallax 会呈现视觉差,需要collapseParallaxMultiplier(0.0~1.0之间) 视觉差系数一起配合使用在代码中设置
           <ImageView
                android:id="@+id/img_bg"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:fitsSystemWindows="true"
                android:scaleType="centerCrop"
                android:src="@drawable/coor_bg"
                app:layout_collapseMode="parallax"
                app:layout_collapseParallaxMultiplier="0.7"/>
    

    app:contentScrim

    折叠之后 toolbar 的颜色

    Behavior

    CoordinatorLayout 的子 View 进行一些交互,需要设置 Behavior
    如果是 CoordinatorLayout 内部滑动的 View 需要设置已经为我们提供好的 behavior

    app:layout_behavior="@string/appbar_scrolling_view_behavior"

    behavior 的方法分为几类,

    布局相关的方法

     // 给 Behavior 设置 LayoutParams 时会调用
    public void onAttachedToLayoutParams(...) {}
    
    // LayoutParams 移除时会调用
    public void onDetachedFromLayoutParams() {}
    
    //  CoordinatorLayout 在测量时会回调这个方法
    public boolean onMeasureChild(...) {
        return false;
    }
    
    //  CoordinatorLayout 在布局时会回调这个方法
    public boolean onLayoutChild(...) {
        return false;
    }
    

    事件处理相关的方法

    // 是否拦截 CoordinatorLayout 发过了的点击事件
    public boolean onInterceptTouchEvent(...) {
        return false;
    }
    
    // 接收 CoordinatorLayout 发过了的点击事件
    public boolean onTouchEvent(...) {
        return false;
    }
    

    滑动事件相关的方法

    // 当 CoordinatorLayout 内有 NestedScrollView 开始滑动的时候回调
    public boolean onStartNestedScroll(...) {
        return false;
    }
    
    // 当上面的 onStartNestedScroll 返回 true,会回到改方法
    public void onNestedScrollAccepted(...) {}
    
    // 当 CoordinatorLayout 内有 NestedScrollView 停止滑动的时候回调
    public void onStopNestedScroll(...) {}
    
        
    // 当 CoordinatorLayout 内有 NestedScrollView 滑动过程中的回调
    public void onNestedScroll(...) {}
    
    // 在 onNestedScroll 之前回调该方法
    public void onNestedPreScroll(...) {}
    
     // 是否滑动的惯性事件处理
    public boolean onNestedFling(...) {
        return false;
    }
    
    // 滑动的惯性事件开始的回调
    public boolean onNestedPreFling(...) {
        return false;
    }
    

    依赖 View 相关的方法

    这也是我们在自定义 Behavior 时一定会重写的方法

    // 当前 View 是否依赖指定 View 进行变化
    public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {
        return false;
    }
    
    // 依赖的 View(dependency)变化时的回调
    public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
        return false;
    }
    
    // 依赖的 View 被移除时的回调
    public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {
    }
    

    下面是 behavior 重要的方法

        public static abstract class Behavior<V extends View>{
            public Behavior() {
            }
            public Behavior(Context context, AttributeSet attrs) {}
    
           // 给 Behavior 设置 LayoutParams 时会调用
            public void onAttachedToLayoutParams(@NonNull CoordinatorLayout.LayoutParams params) {}
    
            // LayoutParams 移除时会调用
            public void onDetachedFromLayoutParams() {}
    
                 
            // 是否拦截 CoordinatorLayout 发过了的点击事件
            public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
                return false;
            }
    
            // 接收 CoordinatorLayout 发过了的点击事件
            public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
                return false;
            }
    
            // 设置 Behavior 所在 View 之外的 View 的蒙层颜色
            @ColorInt
            public int getScrimColor(CoordinatorLayout parent, V child) {
                return Color.BLACK;
            }
    
            // 设置蒙层的透明度
            @FloatRange(from = 0, to = 1)
            public float getScrimOpacity(CoordinatorLayout parent, V child) {
                return 0.f;
            }
    
            // 是否对 Behavior 绑定 View 下面的 View 的进行交互,
            // 默认是是根据 getScrimOpacity 的透明度决定的
            public boolean blocksInteractionBelow(CoordinatorLayout parent, V child) {
                return getScrimOpacity(parent, child) > 0.f;
            }
    
            // 当前 View 是否依赖指定 View 进行变化
            public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {
                return false;
            }
    
            // 依赖的 View(dependency)变化时的回调
            public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
                return false;
            }
    
            // 依赖的 View 被移除时的回调
            public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {
            }
    
            //  CoordinatorLayout 在测量时会回调这个方法
            public boolean onMeasureChild(CoordinatorLayout parent, V child,
                    int parentWidthMeasureSpec, int widthUsed,
                    int parentHeightMeasureSpec, int heightUsed) {
                return false;
            }
    
            //  CoordinatorLayout 在布局时会回调这个方法
            public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
                return false;
            }
    
            // 设置 tag
            public static void setTag(View child, Object tag) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                lp.mBehaviorTag = tag;
            }
    
            // 获取 tag
            public static Object getTag(View child) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                return lp.mBehaviorTag;
            }
    
            // 当 CoordinatorLayout 内有 NestedScrollView 开始滑动的时候回调
            public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                    @NonNull V child, @NonNull View directTargetChild, @NonNull View target,
                    @ScrollAxis int axes, @NestedScrollType int type) {
                if (type == ViewCompat.TYPE_TOUCH) {
                    return onStartNestedScroll(coordinatorLayout, child, directTargetChild,
                            target, axes);
                }
                return false;
            }
    
            // 当上面的 onStartNestedScroll 返回 true,会回到改方法
            public void onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout,
                    @NonNull V child, @NonNull View directTargetChild, @NonNull View target,
                    @ScrollAxis int axes, @NestedScrollType int type) {
                if (type == ViewCompat.TYPE_TOUCH) {
                    onNestedScrollAccepted(coordinatorLayout, child, directTargetChild,
                            target, axes);
                }
            }
    
            // 当 CoordinatorLayout 内有 NestedScrollView 停止滑动的时候回调
            public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                    @NonNull V child, @NonNull View target, @NestedScrollType int type) {
                if (type == ViewCompat.TYPE_TOUCH) {
                    onStopNestedScroll(coordinatorLayout, child, target);
                }
            }
    
            // 当 CoordinatorLayout 内有 NestedScrollView 滑动过程中的回调
            public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child,
                    @NonNull View target, int dxConsumed, int dyConsumed,
                    int dxUnconsumed, int dyUnconsumed, @NestedScrollType int type) {
                if (type == ViewCompat.TYPE_TOUCH) {
                    onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed,
                            dxUnconsumed, dyUnconsumed);
                }
            }
    
            // 在 onNestedScroll 之前回调该方法
            public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
                    @NonNull V child, @NonNull View target, int dx, int dy, @NonNull int[] consumed,
                    @NestedScrollType int type) {
                if (type == ViewCompat.TYPE_TOUCH) {
                    onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
                }
            }
    
             // 是否滑动的惯性事件处理
            public boolean onNestedFling(@NonNull CoordinatorLayout coordinatorLayout,
                    @NonNull V child, @NonNull View target, float velocityX, float velocityY,
                    boolean consumed) {
                return false;
            }
    
            // 滑动的惯性事件开始的回调
            public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout,
                    @NonNull V child, @NonNull View target, float velocityX, float velocityY) {
                return false;
            }
    
            // 如果给CoordinatorLayout设置了fitSystemWindow=true,可以在这里自己处理WindowInsetsCompat
            @NonNull
            public WindowInsetsCompat onApplyWindowInsets(CoordinatorLayout coordinatorLayout,
                    V child, WindowInsetsCompat insets) {
                return insets;
            }
    
            // 在CoordinatorLayout的requestChildRectangleOnScreen()中被调用
            public boolean onRequestChildRectangleOnScreen(CoordinatorLayout coordinatorLayout,
                    V child, Rect rectangle, boolean immediate) {
                return false;
            }  
        }
    

    CoordinatorLayout 与 Behavior 的关系

    了解 CoordinatorLayout 与 Behavior 的关系,需要进入 CoordinatorLayout 的源码里面去看看。

    CoordinatorLayout#onMeasure

    CoordinatorLayout#onMeasure 方法里面会调用 Behavior.#onMeasureChild

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 准备工作, 用 DFS 深度遍历算法,对依赖的 View 进行排序
        prepareChildren();
        // 根据情况添加或者移除OnPreDrawListener
        ensurePreDrawListener();
    
        ...
    
        final boolean applyInsets = mLastInsets != null && ViewCompat.getFitsSystemWindows(this);
    
        final int childCount = mDependencySortedChildren.size();
        for (int i = 0; i < childCount; i++) {
            // 从排好续的集合中依次获取Child Vie
            final View child = mDependencySortedChildren.get(i);
            if (child.getVisibility() == GONE) {
                // If the child is GONE, skip...
                continue;
            }
    
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            ....
            
            // 处理 FitsSystemWindows
            int childWidthMeasureSpec = widthMeasureSpec;
            int childHeightMeasureSpec = heightMeasureSpec;
            if (applyInsets && !ViewCompat.getFitsSystemWindows(child)) {
                // We're set to handle insets but this child isn't, so we will measure the
                // child as if there are no insets
                ...
            }
    
            // Behavior 
            final Behavior b = lp.getBehavior();
            // Behavior.onMeasureChild 方法调用
            if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
                    childHeightMeasureSpec, 0)) {
                onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
                        childHeightMeasureSpec, 0);
            }
    
           ...
        }
    
        ...
        // 设置 width,height
        setMeasuredDimension(width, height);
    }
    

    CoordinatorLayout#onLayout

    CoordinatorLayout#onLayout 方法里面会调用 Behavior#onLayoutChild

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int layoutDirection = ViewCompat.getLayoutDirection(this);
        final int childCount = mDependencySortedChildren.size();
        for (int i = 0; i < childCount; i++) {
            final View child = mDependencySortedChildren.get(i);
            if (child.getVisibility() == GONE) {
                // If the child is GONE, skip...
                continue;
            }
    
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            final Behavior behavior = lp.getBehavior();
    
            // 调用 Behavior#onLayoutChild, 如果 behavior 不进行测量,则需要 CoordinatorLayout 自己测量
            if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
                onLayoutChild(child, layoutDirection);
            }
        }
    }
    
    public void onLayoutChild(View child, int layoutDirection) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        if (lp.checkAnchorChanged()) {
            throw new IllegalStateException("An anchor may not be changed after CoordinatorLayout"
                    + " measurement begins before layout is complete.");
        }
        if (lp.mAnchorView != null) {
            // 设置了 AnchorView  时候的布局, app:layout_anchor
            layoutChildWithAnchor(child, lp.mAnchorView, layoutDirection);
        } else if (lp.keyline >= 0) {
            // 设置了 keyline 的布局, app:layout_keyline
            layoutChildWithKeyline(child, lp.keyline, layoutDirection);
        } else {
            // 正常测量,像 FrameLayout 那样布局
            layoutChild(child, layoutDirection);
        }
    }
    

    CoordinatorLayout#onLayout

    CoordinatorLayout#onLayout 方法会调用 Behavior#onInterceptTouchEvent 方法询问是否要拦截,也会调用 调用 Behavior#onTouchEvent 处理点击事件

     @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.
        // 重置 Behavior 
        if (action == MotionEvent.ACTION_DOWN) {
            resetTouchBehaviors(true);
        }
    
        // performIntercept 进行判断是否拦截
        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;
    }
    
    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;
        // View 按照 z-order 进行排序
        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.
                // 如果一个 View 把事件拦截了,则把重叠于它之下的 behavior 事件都取消
                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:
                        // 调用 Behavior#onInterceptTouchEvent 方法询问是否要拦截
                        intercepted = b.onInterceptTouchEvent(this, child, ev);
                        break;
                    case TYPE_ON_TOUCH:
                        // 调用 Behavior#onTouchEvent 处理点击事件 
                        intercepted = b.onTouchEvent(this, child, ev);
                        break;
                }
                if (intercepted) {
                    mBehaviorTouchView = child;
                }
            }
    
            ...
        }
    
        topmostChildList.clear();
    
        return intercepted;
    }
    

    CoordinatorLayout#onTouchEvent

    在 CoordinatorLayout#onTouchEvent 方法里面会调用 Behavior#onTouchEvent 是否进行拦截判断

    @Override
        public boolean onTouchEvent(MotionEvent ev) {
            boolean handled = false;
            boolean cancelSuper = false;
            MotionEvent cancelEvent = null;
    
            final int action = ev.getActionMasked();
    
            // 如果是 Behavior 拦截了,则把点击事件教给 Behavior#onTouchEvent 处理
            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
           // 如果是 Behaivor 不拦截,则交给 CoordinatorLayout 的父类处理,按照事件传递流程处理
            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();
            }
    
            // ACTION_UP 事件重置 Behavior
            if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
                resetTouchBehaviors(false);
            }
    
            return handled;
        }
    

    CoordinatorLayout 的嵌套滑动

    CoordinatorLayout 是实现了 NestedScrollingParent2,NestedScrollingParent2 继承了 NestedScrollingParent

    NestedScrollingParent2.java 的接口

    // This interface should be implemented by ViewGroup  
    // subclasses that wish to support scrolling operations 
    // delegated by a nested child view
    public interface NestedScrollingParent2 extends NestedScrollingParent {
    
         boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes,
                @NestedScrollType int type);
                
         boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes,
                @NestedScrollType int type);
                
         void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes,
                @NestedScrollType int type);
                
        void onStopNestedScroll(@NonNull View target, @NestedScrollType int type);
        
        void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
                int dxUnconsumed, int dyUnconsumed, @NestedScrollType int type);
                
        void onNestedPreScroll(@NonNull View target, int dx, int dy, @Nullable int[] consumed,
                @NestedScrollType int type)
                
    }
    
    

    在 CoordinatorLayout 滑动的时候,实现这些滑动方法,都会直接传入到 Behavior 中对应的方法,例如 CoordinatorLayout#onStartNestedScroll 会分发到 Behavior#onStartNestedScroll

    
    @Override
    public boolean onStartNestedScroll(View child, View target, int axes, int type) {
        boolean handled = false;
    
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            if (view.getVisibility() == View.GONE) {
                // If it's GONE, don't dispatch
                continue;
            }
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                // 分发到 Behavior#onStartNestedScroll
                final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,
                        target, axes, type);
                handled |= accepted;
                lp.setNestedScrollAccepted(type, accepted);
            } else {
                lp.setNestedScrollAccepted(type, false);
            }
        }
        return handled;
    }
    

    关于嵌套滑动,在后续文章中再细说。

    当手指滑动的范围在 AppBarLayout 内滑动,CoordinatorLayout 会通过 NestScrollView 的 Behavior 分发事件,让 NestScrollView 产生滑动;
    当手指滑动的范围在 NestScrollView 内滑动,CoordinatorLayout 会通过 AppBarLayout 的 Behavior 分发事件,让 AppBarLayout 产生滑;AppBarLayout 默认的 AppBarLayout#Behavior, 不需要显式指定 Behavior。

    AppBarLayout 滑动监听

    自定义 AppBarStateChangeListener, 同时定义 AppBarLayout 的状态 EXPANDED 展开,COLLAPSED 折叠 和 IDLE 转态

    public abstract class AppBarStateChangeListener implements AppBarLayout.OnOffsetChangedListener {
    
        private State mCurrentState = State.IDLE;
    
        @Override
        public final void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
            if (verticalOffset == 0) {
                if (mCurrentState != State.EXPANDED) {
                    onStateChanged(appBarLayout, State.EXPANDED, verticalOffset);
                }
                mCurrentState = State.EXPANDED;
            } else if (Math.abs(verticalOffset) >= appBarLayout.getTotalScrollRange()) {
                if (mCurrentState != State.COLLAPSED) {
                    onStateChanged(appBarLayout, State.COLLAPSED, verticalOffset);
                }
                mCurrentState = State.COLLAPSED;
            } else {
                if (mCurrentState != State.IDLE) {
                    onStateChanged(appBarLayout, State.IDLE, verticalOffset);
                }
                mCurrentState = State.IDLE;
    
            }
        }
    
        public abstract void onStateChanged(AppBarLayout appBarLayout, State state, int verticalOffset);
    }
    
    enum State {
        EXPANDED,
        COLLAPSED,
        IDLE
    }
    

    然后在 AppBarLayout 中设置监听

    appBarLayout.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
            @Override
            public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
                // 进行操作
        });
    

    这个回调是在 AppBarLayout$Behavior#setHeaderTopBottomOffset 方法中回调的。当 AppBarLayout 在进行滑动的时候,会调用 setHeaderTopBottomOffset 方法,在 setHeaderTopBottomOffset在中

    @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);
                    }
    
    // 分发到监听
    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;
        }    
    }    
    
    // 分发到所有的监听器
    void dispatchOffsetUpdates(int offset) {
    if (mListeners != null) {
        for (int i = 0, z = mListeners.size(); i < z; i++) {
            final OnOffsetChangedListener listener = mListeners.get(i);
            if (listener != null) {
                listener.onOffsetChanged(this, offset);
            }
        }
    }
    

    注意事项

    1. 如果 CoordinatorLayout 里面使用了 ScrollView 或者 ViewPager, 需要指定 Bebeavior
    <android.support.design.widget.CoordinatorLayout...>
        <android.support.design.widget.AppBarLayout...>
            <android.support.design.widget.CollapsingToolbarLayout...>
                <!-- your collapsed view -->
                <View.../>
                <android.support.v7.widget.Toolbar.../>
            </android.support.design.widget.CollapsingToolbarLayout>
        </android.support.design.widget.AppBarLayout>
    
        <!-- 需要指定 appbar_scrolling_view_behavior -->
       <android.support.v4.view.ViewPager
            android:id="@+id/viewpager"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
    </android.support.design.widget.CoordinatorLayout>
    

    参考

    相关文章

      网友评论

          本文标题:CoordinatorLayout 与 Behavior

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