Coordinatorlayout嵌套滑动,自定义Behavior,听我来讲讲?


前言

嵌套滑动,顾名思义,嵌套嵌套就一定有父容器和子容器。如何能让子容器滑动能带动父容器(或父容器包含的其他子容器)滑动?什么样的子容器有这种能力?这种关联如何形成,以及被关联的容器如何响应,这种响应的逻辑在哪里定义?
相信你在对本文的阅读之后会有一定的了解


提示:以下是本篇文章正文内容

分析

1、父容器子容器

了解安卓开发的同学对这个概念再熟悉不过了,父容器是容器布局,子容器(控件)则是被这个父容器包含的容器布局(控件)。如:

<FrameLayout>
	<RelativeLayout>
	...
    </RelativeLayout>
    <View/>
</FrameLayout>

这里的FrameLayout就是父容器,这里的RelativeLayoutView就是子容器(控件)。

2、如何形成关联,谁是发起者

父容器实现NestedScrollingParent接口,
子容器实现NestedScrollingChild接口。

查看代码示例

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout>

    <android.support.design.widget.AppBarLayout/>

    <FrameLayout
        app:layout_behavior="android.support.design.widget.AppBarLayout$ScrollingViewBehavior">
        
        <android.support.v7.widget.RecyclerView/>
        <!--悬浮条-->
        <RelativeLayout>
			...
        </RelativeLayout>
    </FrameLayout
    	app:layout_behavior=".ScaleBehavior">

    <android.support.design.widget.FloatingActionButton/>
</android.support.design.widget.CoordinatorLayout>

示例中的CoordinatorLayout默认实现了NestedScrollingParent接口,而RecyclerView控件默认实现了NestedScrollingChild接口。

所以在RecyclerView滑动的时候,CoordinatorLayout一直能收到相应的回调。比如说这时候这个控件不是RecyclerView而是ListView的话,那这个回调自然是没有的,理由就是ListView并没有默认实现NestedScrollingChild接口。

实现了NestedScrollingChild接口的控件,其实也就是整个事件的发起者,而父容器便是接受的一方。

扫描二维码关注公众号,回复: 12640778 查看本文章

3、NestedScrollingParent和NestedScrollingChild对应

这两个接口的回调方法api,如下:

SCROLL_STATE_IDLE        0, 最后是RecyclerView滚动停止状态。
SCROLL_STATE_DRAGGING    1, 先是手指拖拽的状态
SCROLL_STATE_SETTLING    2,再是手指松开但是RecyclerView还在滑动

/**
*父容器实现的接口
*/
public interface NestedScrollingParent {
     
     
    /**
     * 开始滑动回调
     * @param child 该父View 的子View
     * @param target 支持嵌套滑动的 VIew
     * @param nestedScrollAxes 滑动方向
     * @return 是否支持 嵌套滑动
     */
    boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int nestedScrollAxes);

    void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int nestedScrollAxes);

    void onStopNestedScroll(@NonNull View target);
    /**
     * 这里 传来了 x y 方向上的滑动距离
     * 并且 先与 子VIew  处理滑动,  并且 consumed  中可以设置相应的 除了的距离
     * 然后 子View  需要更具这感觉, 来处理自己滑动
     *
     * @param target
     * @param dx
     * @param dy
     * @param consumed
     */
    void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed);
    /**
     * 这里 主要处理 dyUnconsumed dxUnconsumed 这两个值对应的数据
     * @param target
     * @param dxConsumed
     * @param dyConsumed
     * @param dxUnconsumed
     * @param dyUnconsumed
     */
    void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);

    boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY);

    boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed);


    int getNestedScrollAxes();
}

/**
*子容器(控件)实现的接口
*/
public interface NestedScrollingChild {
     
     
    //设置允许嵌套滑动 true表示允许
    void setNestedScrollingEnabled(boolean enable);

    boolean isNestedScrollingEnabled();

    //开始嵌套滑动 这里需要返回true 否在后续事件不会再触发
    boolean startNestedScroll(int axes);//坐标轴

    //结束嵌套滑动
    void stopNestedScroll();

    //判断NestedParent的onStartNestedScroll是否返回true 只有为true后续的事件才能继续一系列的嵌套滑动
    boolean hasNestedScrollingParent();

    //子view消费了拖动事件之前通知父view,dx dy是将要消费的距离,如果父view要消费可通过
    //设置consumed[0]=x .consumed[1]=y来分别消费x,y。然后子view继续处理剩下的位移(即dx-x,dy-y)
    boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow);

    //子View消费滑动事件后通知父View
    boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow);

    //子view消费了滑动事件之前通知父view
    boolean dispatchNestedPreFling(float var1, float var2);

    //子view消费了滑动事件之后通知父view
    boolean dispatchNestedFling(float var1, float var2, boolean var3);
}

父接口的回调和子接口的回调,两者的方法有明显的对应关系。

这样实现了子接口就可以在需要的时候调用接口的方法,如stopNestedScroll,这样对应在父接口onStopNestedScroll也就会被回调。

4、响应者

上文说了父容器是接受的一方,但它并不是真正意义上的响应者,响应者是谁取决于Behavior的定义。

本例中的父容器是CoordinatorLayout,我们就可以自定义一个Behavior继承CoordinatorLayout.Behavior,然后在对应的父容器回调方法中加入自己想要的逻辑。

值得一提的是,你即可以指定设置了Behavior的控件本身响应,也可以指定该父容器下的其他子容器(控件)响应,无论这个Behavior设置给哪个子容器(控件)。

示例如下:

public class ScaleBehavior<V extends View> extends CoordinatorLayout.Behavior<V> {
     
     
    @Override
    public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
     
     
        //只有返回true 后续的动作才会触发
        return axes == ViewCompat.SCROLL_AXIS_VERTICAL;//垂直滚动
    }

    @Override
    public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
     
     
        super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
        ...
    }
}

当然Android本身也有很多定义好的Behavior可以直接使用,这里就不赘述了。

最后将这个Behavior设置到布局中,就可以正常使用了。

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout>

    <android.support.design.widget.AppBarLayout/>

    <FrameLayout
        app:layout_behavior="android.support.design.widget.AppBarLayout$ScrollingViewBehavior">
        
        <android.support.v7.widget.RecyclerView/>
        <!--悬浮条-->
        <RelativeLayout>
			...
        </RelativeLayout>
    </FrameLayout
    	app:layout_behavior=".ScaleBehavior">

    <android.support.design.widget.FloatingActionButton/>
</android.support.design.widget.CoordinatorLayout>

示例

本例主要

  • 使用RecyclerView做示范。(将项目启动页改为MainActivity查看)
  • 自定义了一个实现了NestedScrollingChild接口的ListView做示范。
  • 自定义Behavior示范。

效果如下
在这里插入图片描述
总布局代码如下

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".SecondActivity">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <android.support.v7.widget.Toolbar
            android:layout_width="match_parent"
            android:layout_height="?actionBarSize"
            app:layout_scrollFlags="scroll|enterAlways"
            app:title="ToolBar" />
    </android.support.design.widget.AppBarLayout>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="android.support.design.widget.AppBarLayout$ScrollingViewBehavior">

        <pers.owen.recyclerview.NestedListView
            android:id="@+id/list_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@android:color/white"
            tools:listitem="@layout/item_feed" />
        <!--悬浮条-->
        <RelativeLayout
            android:id="@+id/suspension_bar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@android:color/white">

            <pers.owen.recyclerview.CircleImageView
                android:id="@+id/iv_avatar"
                android:layout_width="44dp"
                android:layout_height="44dp"
                android:padding="8dp"
                android:src="@drawable/avatar1" />

            <TextView
                android:id="@+id/tv_nickname"
                android:layout_width="wrap_content"
                android:layout_height="44dp"
                android:layout_marginLeft="8dp"
                android:layout_toRightOf="@id/iv_avatar"
                android:gravity="center_vertical"
                android:text="粥可温"
                android:textSize="12sp" />

            <View
                android:id="@+id/top_divider"
                android:layout_width="match_parent"
                android:layout_height="0.2dp"
                android:layout_below="@id/tv_nickname"
                android:background="#33000000" />


        </RelativeLayout>
    </FrameLayout>

    <android.support.design.widget.FloatingActionButton
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:layout_gravity="bottom|right"
        android:layout_margin="16dp"
        app:layout_behavior=".ScaleBehavior" />
</android.support.design.widget.CoordinatorLayout>

NestedListView代码如下

public class NestedListView extends ListView implements NestedScrollingChild {
     
     
    //1初始化获取ChildHelper
    private NestedScrollingChildHelper mChildHelper;
    private int mLastY;
    private final int[] mScrollOffset = new int[2];//滑动偏移
    private final int[] mScrollConsumed = new int[2];//滑动消费
    private int mNestedOffsetY;//嵌套偏移

    public NestedListView(Context context) {
     
     
        super(context);
        init();
    }

    public NestedListView(Context context, AttributeSet attrs) {
     
     
        super(context, attrs);
        init();
    }

    public NestedListView(Context context, AttributeSet attrs, int defStyleAttr) {
     
     
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
     
     
        mChildHelper = new NestedScrollingChildHelper(this);
        setNestedScrollingEnabled(true);
    }

    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
     
     
        mChildHelper.setNestedScrollingEnabled(enabled);

    }

    @Override
    public boolean isNestedScrollingEnabled() {
     
     
        return mChildHelper.isNestedScrollingEnabled();
    }

    @Override
    public boolean startNestedScroll(int axes) {
     
     
        return mChildHelper.startNestedScroll(axes);
    }

    @Override
    public void stopNestedScroll() {
     
     
        mChildHelper.stopNestedScroll();
    }

    @Override
    public boolean hasNestedScrollingParent() {
     
     
        return mChildHelper.hasNestedScrollingParent();
    }

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) {
     
     
        return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow) {
     
     
        return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
     
     
        return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
    }

    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
     
     
        return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
     
     
        int action = ev.getAction();
        int y = (int) ev.getY();
        ev.offsetLocation(0, mNestedOffsetY);
        switch (action) {
     
     
            case MotionEvent.ACTION_DOWN:
                mLastY = y;
                mNestedOffsetY = 0;
                this.startNestedScroll((ViewCompat.SCROLL_AXIS_VERTICAL));//开始嵌套滑动
                break;

            case MotionEvent.ACTION_MOVE:
                int dy = mLastY - y;//Y的拖动距离
                int oldY = getScrollY();//注意一般一直为0
                //在自己消费前先分发给父容器
                if (dispatchNestedPreScroll(0, dy, mScrollConsumed, mScrollOffset)) {
     
     
                    dy -= mScrollConsumed[1];//剩余
                    ev.offsetLocation(0, -mScrollOffset[1]);
                    mNestedOffsetY += mScrollOffset[1];
                }
                mLastY = y - mScrollOffset[1];
                int newScrollY = oldY + dy;
                dy -= newScrollY - oldY;//全部消费完
                //自己消费
                if (dispatchNestedScroll(0, newScrollY - dy, 0, dy, mScrollOffset)) {
     
     
                    ev.offsetLocation(0, mScrollOffset[1]);
                    mNestedOffsetY += mScrollOffset[1];
                    mLastY -= mScrollOffset[1];
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                stopNestedScroll();
                break;
        }
        return super.onTouchEvent(ev);
    }
}

ScaleBehavior代码如下

public class ScaleBehavior<V extends View> extends CoordinatorLayout.Behavior<V> {
     
     
    private Interpolator interpolator;
    private boolean isRunning;

    public ScaleBehavior(Context context, AttributeSet attrs) {
     
     
        super(context, attrs);
        interpolator = new AccelerateDecelerateInterpolator();
    }

    @Override
    public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
     
     
        //只有返回true 后续的动作才会触发
        return axes == ViewCompat.SCROLL_AXIS_VERTICAL;//垂直滚动
    }

    @Override
    public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
     
     
        super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
        Log.e("test", dyConsumed + "  " + dyUnconsumed);
        if (dyConsumed > 0 && !isRunning && child.getVisibility() == View.VISIBLE) {
     
     
            //上滑 缩小隐藏 动画
            scaleHide(child);
        } else if (dyConsumed < 0 && !isRunning && child.getVisibility() == View.INVISIBLE) {
     
     
            //下滑 放大显示
            scaleShow(child);
        }
    }

    private void scaleShow(final V child) {
     
     
        child.setVisibility(View.VISIBLE);
        ViewCompat.animate(child).alpha(1).scaleX(1).scaleY(1).setInterpolator(interpolator)
                .setListener(new ViewPropertyAnimatorListener() {
     
     
                    @Override
                    public void onAnimationStart(View view) {
     
     
                        isRunning = true;
                    }

                    @Override
                    public void onAnimationEnd(View view) {
     
     
                        isRunning = false;
                    }

                    @Override
                    public void onAnimationCancel(View view) {
     
     
                        isRunning = false;
                    }
                }).setDuration(500).start();
    }

    private void scaleHide(final V child) {
     
     
        ViewCompat.animate(child).alpha(0).scaleX(0).scaleY(0).setInterpolator(interpolator)
                .setListener(new ViewPropertyAnimatorListener() {
     
     
                    @Override
                    public void onAnimationStart(View view) {
     
     
                        isRunning = true;
                    }

                    @Override
                    public void onAnimationEnd(View view) {
     
     
                        isRunning = false;
                        child.setVisibility(View.INVISIBLE);
                    }

                    @Override
                    public void onAnimationCancel(View view) {
     
     
                        isRunning = false;
                    }
                }).setDuration(500).start();
    }
}

推荐阅读

Android 11新特性,Scoped Storage又有了新花样

About

本文Demo

UI系列文章一览

猜你喜欢

转载自blog.csdn.net/u014158743/article/details/114287779