Android-NestedScrollingParent, NestedScrollingChild父子View 间 的 嵌套滑动

参考:

NestedScrolling事件机制源码解析

NestedScrollingParent, NestedScrollingChild 详解

Android NestedScrolling 实战

简介:

从 Android 5.0 Lollipop 开始提供一套 API 来支持嵌入的滑动效果。同样在最新的 Support V4 包中也提供了前向的兼容。有了嵌入滑动机制,就能实现很多很复杂的滑动效果。在 Android Design Support 库中非常重要的 CoordinatorLayout 组件就是使用了这套机制,实现了 Toolbar 的收起和展开功能,如下图所示:

NestedScrolling 提供了一套父 View 和子 View 滑动交互机制。要完成这样的交互,父 View 需要实现 NestedScrollingParent 接口,而子 View 需要实现 NestedScrollingChild 接口。

NestedScrollingChild (接口)
NestedScrollingChildHelper

NestedScrollingParent (接口)
NestedScrollingParentHelper

Android 就是通过这几个类, 来实现 父子View 间 的 嵌套滑动。

常见的RecyclerView 类实现了NestedScrollingChild 接口,如下:

public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild {...}

CoordinatorLayout 类实现了NestedScrollingParent 接口,如下:

public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent {...}

实现 NestedScrollingChild

首先来说 NestedScrollingChild。如果你有一个可以滑动的 View,需要被用来作为嵌入滑动的子 View,就必须实现本接口。在此 View 中,包含一个 NestedScrollingChildHelper 辅助类。NestedScrollingChild 接口的实现,基本上就是调用本 Helper 类的对应的函数即可,因为 Helper 类中已经实现好了 Child 和 Parent 交互的逻辑。原来的 View 的处理 Touch 事件,并实现滑动的逻辑大体上不需要改变。

需要做的就是,如果要准备开始滑动了,需要告诉 Parent,你要准备进入滑动状态了,调用 startNestedScroll()。
你在滑动之前,先问一下你的 Parent 是否需要滑动,也就是调用 dispatchNestedPreScroll()。
如果父类滑动了一定距离,你需要重新计算一下父类滑动后剩下给你的滑动距离余量。
然后,你自己进行余下的滑动。
最后,如果滑动距离还有剩余,你就再问一下,Parent 是否需要在继续滑动你剩下的距离,也就是调用 dispatchNestedScroll()。

实现 NestedScrollingParent

作为一个可以嵌入 NestedScrollingChild 的父 View,需要实现 NestedScrollingParent,这个接口方法和 NestedScrollingChild 大致有一一对应的关系。同样,也有一个 NestedScrollingParentHelper 辅助类来默默的帮助你实现和 Child 交互的逻辑。滑动动作是 Child 主动发起,Parent 就收滑动回调并作出响应。

从上面的 Child 分析可知,滑动开始的调用 startNestedScroll(),Parent 收到 onStartNestedScroll() 回调,决定是否需要配合 Child 一起进行处理滑动,如果需要配合,还会回调 onNestedScrollAccepted()。

每次滑动前,Child 先询问 Parent 是否需要滑动,即 dispatchNestedPreScroll(),这就回调到 Parent 的 onNestedPreScroll(),Parent 可以在这个回调中“劫持”掉 Child 的滑动,也就是先于 Child 滑动。

Child 滑动以后,会调用 onNestedScroll(),回调到 Parent 的 onNestedScroll(),这里就是 Child 滑动后,剩下的给 Parent 处理,也就是 后于 Child 滑动。

最后,滑动结束,调用 onStopNestedScroll() 表示本次处理结束。

其实,除了上面的 Scroll 相关的调用和回调,还有 Fling 相关的调用和回调,处理逻辑基本一致。

NestedScrollingChild接口

//开始、停止嵌套滚动
public boolean startNestedScroll(int axes); public void stopNestedScroll();
//触摸滚动相关
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
//惯性滚动相关 
public boolean dispatchNestedPreFling(float velocityX, float velocityY);
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);

源码:


package android.support.v4.view;

import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewParent;


public interface NestedScrollingChild {  

    /** 
     * 设置嵌套滑动是否能用
     * 
     *  @param enabled true to enable nested scrolling, false to disable
     */  
    public void setNestedScrollingEnabled(boolean enabled);  

    /** 
     * 判断嵌套滑动是否可用 
     * 
     * @return true if nested scrolling is enabled
     */  
    public boolean isNestedScrollingEnabled();  

    /** 
     * 开始嵌套滑动
     * 
     * @param axes 表示方向轴,有横向和竖向
     *             ViewCompat.SCROLL_AXIS_HORIZONTAL 横向哈东 
     *             ViewCompat.SCROLL_AXIS_VERTICAL 纵向滑动 
     */  
    public boolean startNestedScroll(int axes);  

    /** 
     * 停止嵌套滑动 
     */  
    public void stopNestedScroll();  

    /** 
     * 判断是否有父View 支持嵌套滑动 
     * @return whether this view has a nested scrolling parent
     */  
    public boolean hasNestedScrollingParent();  

    /** 
     * 在子View的onInterceptTouchEvent或者onTouch中,调用该方法通知父View滑动的距离
     *
     * @param dx  x轴上滑动的距离
     * @param dy  y轴上滑动的距离
     * @param consumed 父view消费掉的scroll长度
     * @param offsetInWindow   子View的窗体偏移量
     * @return 支持的嵌套的父View 是否处理了 滑动事件 
     */  
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);  

    /** 
     * 子view处理scroll后调用
     *
     * @param dxConsumed x轴上被消费的距离(横向) 
     * @param dyConsumed y轴上被消费的距离(竖向)
     * @param dxUnconsumed x轴上未被消费的距离 
     * @param dyUnconsumed y轴上未被消费的距离 
     * @param offsetInWindow 子View的窗体偏移量
     * @return  true if the event was dispatched, false if it could not be dispatched.
     */  
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,  
          int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);  



    /** 
     * 滑行时调用 
     *
     * @param velocityX x 轴上的滑动速率
     * @param velocityY y 轴上的滑动速率
     * @param consumed 是否被消费 
     * @return  true if the nested scrolling parent consumed or otherwise reacted to the fling
     */  
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);  

    /** 
     * 进行滑行前调用
     *
     * @param velocityX x 轴上的滑动速率
     * @param velocityY y 轴上的滑动速率 
     * @return true if a nested scrolling parent consumed the fling
     */  
    public boolean dispatchNestedPreFling(float velocityX, float velocityY);  
}

NestedScrollingChildHelper

public class NestedScrollingChildHelper {  
    /** 
     * 嵌套滑动的子View 
     */  
    private final View mView;  

    /** 
     * 支持 嵌套滑动的 父View 
     */  
    private ViewParent mNestedScrollingParent;  

    /** 
     * 是否支持 嵌套滑动 
     */  
    private boolean mIsNestedScrollingEnabled;  

    /** 
     * 是否被消费的一个中变变量 
     */  
    private int[] mTempNestedScrollConsumed;  

    public NestedScrollingChildHelper(View view) {  
        mView = view;  
    }  

    public void setNestedScrollingEnabled(boolean enabled) {  
        if (mIsNestedScrollingEnabled) {  
            ViewCompat.stopNestedScroll(mView);  
        }  
        mIsNestedScrollingEnabled = enabled;  
    }  

    public boolean isNestedScrollingEnabled() {  
        return mIsNestedScrollingEnabled;  
    }  

    public boolean hasNestedScrollingParent() {  
        return mNestedScrollingParent != null;  
    }  

    /** 
     * 开始嵌套滑动 
     * @param axes 滑动方向 
     * @return 是否有父view 支持嵌套滑动 
     */  
    public boolean startNestedScroll(int axes) {  
        if (hasNestedScrollingParent()) {  
            // 如果已经找到 了嵌套滑动的父View  
            // Already in progress  
            return true;  
        }  
        if (isNestedScrollingEnabled()) {  
            ViewParent p = mView.getParent();  
            View child = mView;  
            // 递归向上寻找 支持 嵌套滑动的父View  
            while (p != null) {  
                // 这里会调用 父View 的NestedScrollingParent.onStartNestedScroll 方法  
                // 如果 父View 返回 false  则再次向上寻找父View , 直到找到支持的fuView  
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {  
                    mNestedScrollingParent = p;  
                    // 这里回调 父View 的onNestedScrollAccepted 方法 表示开始接收 嵌套滑动  
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);  
                    return true;  
                }  
                if (p instanceof View) {  
                    child = (View) p;  
                }  
                p = p.getParent();  
            }  
        }  
        // 没有找到 支持嵌套滑动的父View  则返回false  
        return false;  
    }  

    /** 
     * 停止 嵌套滑动, 一般 在 cancel up 事件中 调用 
     */  
    public void stopNestedScroll() {  
        if (mNestedScrollingParent != null) {  
            ViewParentCompat.onStopNestedScroll(mNestedScrollingParent, mView);  
            mNestedScrollingParent = null;  
        }  
    }  

    /** 
     * 
     * @param dxConsumed  x 上被消费的距离 
     * @param dyConsumed  y 上被消费的距离 
     * @param dxUnconsumed  x 上未被消费的距离 
     * @param dyUnconsumed  y 上未被消费的距离 
     * @param offsetInWindow  子View 位置的移动距离 
     * @return 
     */  
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,  
                                        int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {  
        if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {  
            if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {  
                int startX = 0;  
                int startY = 0;  
                if (offsetInWindow != null) {  
                    mView.getLocationInWindow(offsetInWindow);  
                    startX = offsetInWindow[0];  
                    startY = offsetInWindow[1];  
                }  

                // 父View 回调 onNestedScroll 方法, 该放在 主要会处理  dxUnconsumed dyUnconsumed 数据  
                ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed,  
                        dyConsumed, dxUnconsumed, dyUnconsumed);  

                if (offsetInWindow != null) {  
                    // 计算 子View的移动距离  
                    mView.getLocationInWindow(offsetInWindow);  
                    offsetInWindow[0] -= startX;  
                    offsetInWindow[1] -= startY;  
                }  
                return true;  
            } else if (offsetInWindow != null) {  
                // No motion, no dispatch. Keep offsetInWindow up to date.  
                offsetInWindow[0] = 0;  
                offsetInWindow[1] = 0;  
            }  
        }  
        return false;  
    }  

    /** 
     * 
     * consumed[0]  为0 时 表示 x 轴方向上事件 没有被消费 
     *              不为0 时 表示 x 轴方向上事件 被消费了, 值表示 被消费的滑动距离 
     * consumed[1]  为0 时 表示 y 轴方向上事件 没有被消费 
     *              不为0 时 表示 y 轴方向上事件 被消费了, 值表示 被消费的滑动距离 
     * 
     * 
     * @param dx 
     * @param dy 
     * @param consumed 
     * @param offsetInWindow 
     * @return 
     */  
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {  
        if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {  
            if (dx != 0 || dy != 0) {  
                int startX = 0;  
                int startY = 0;  
                // 获取 当前View 初始位置  
                if (offsetInWindow != null) {  
                    mView.getLocationInWindow(offsetInWindow);  
                    startX = offsetInWindow[0];  
                    startY = offsetInWindow[1];  
                }  

                // 初始化是否被消费数据  
                if (consumed == null) {  
                    if (mTempNestedScrollConsumed == null) {  
                        mTempNestedScrollConsumed = new int[2];  
                    }  
                    consumed = mTempNestedScrollConsumed;  
                }  
                consumed[0] = 0;  
                consumed[1] = 0;  

                // 这里回调 父View 的 onNestedPreScroll 方法,  
                // 父View 或许会处理 相应的滑动事件,  
                // 如果 处理了 则 consumed 会被赋予 相应的值  
                ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);  

                if (offsetInWindow != null) {  
                    // 父View 处理了相应的滑动,  很可能导致 子View 的位置的移动  
                    // 这里计算出  父view 消费 滑动事件后,  导致 子View 的移动距离  
                    mView.getLocationInWindow(offsetInWindow);  
                    // 这里 子View 的移动距离  
                    offsetInWindow[0] -= startX;  
                    offsetInWindow[1] -= startY;  
                }  
                // 如果  xy 方向 上 有不为0 的表示消费了 则返回true  
                return consumed[0] != 0 || consumed[1] != 0;  
            } else if (offsetInWindow != null) {  
                offsetInWindow[0] = 0;  
                offsetInWindow[1] = 0;  
            }  
        }  
        return false;  
    }  

    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {  
        if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {  
            return ViewParentCompat.onNestedFling(mNestedScrollingParent, mView, velocityX,  
                    velocityY, consumed);  
        }  
        return false;  
    }  

    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {  
        if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {  
            return ViewParentCompat.onNestedPreFling(mNestedScrollingParent, mView, velocityX,  
                    velocityY);  
        }  
        return false;  
    }  

    public void onDetachedFromWindow() {  
        ViewCompat.stopNestedScroll(mView);  
    }  

    public void onStopNestedScroll(View child) {  
        ViewCompat.stopNestedScroll(mView);  
    }  
}  

NestedScrollingParent接口

//当开启、停止嵌套滚动时被调用
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
public void onStopNestedScroll(View target);
//当触摸嵌套滚动时被调用
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);
//当惯性嵌套滚动时被调用
public boolean onNestedPreFling(View target, float velocityX, float velocityY);
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);

源码:

public interface NestedScrollingParent {

    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);

    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);

    public void onStopNestedScroll(View target);

    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed);

    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);

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

    public boolean onNestedPreFling(View target, float velocityX, float velocityY);

    public int getNestedScrollAxes();
}

你会发现,其实和子view差不多的方法,大致一一对应关系,而且它的具体实现也交给了NestedScrollingParentHelper这个代理类

整个嵌套滑动的流程:

从上面的接口还有方法我们可以得出一些简单的流程
1. 调用child的startNestedScroll()来发起嵌套滑动流程(实质上是寻找能够配合child进行嵌套滚动的parent)。parent的onStartNestedScroll()会被调用,若此方法返回true,则OnNestScrollAccepted()也会被调用。
2. child每次滚动前,可以先询问parent是否要滚动,即调用dispatchNestedScroll(),这时可以回调到parent的OnNestedPreScroll(),parent可以在这个回调中先于child滚动。
3. dispatchNestedPreScroll()之后,child可以进行自己的滚动操作。

子view : startNestedScroll   
父view : onStartNestedScroll、onNestedScrollAccepted

子view : dispatchNestedPreScroll 
父view : onNestedPreScroll

子view : dispatchNestedScroll    
父view : onNestedScroll

子view : stopNestedScroll    
父view : onStopNestedScroll

参考:

Android – NestedScrolling滑动机制

示例:

布局:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.example.lenovo.mysmartrefresh.MyNestedScrollParent
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_alignParentTop="true"
        android:orientation="vertical">

        <ImageView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:src="@mipmap/ic_launcher" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#f0f"
            android:text="上面的图片会被隐藏,而这个文字不会被隐藏" />

        <com.example.lenovo.mysmartrefresh.MyNestedScrollChild
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="@string/strs"
                android:textColor="#f0f"
                android:gravity="center"
                android:textSize="20sp" />
        </com.example.lenovo.mysmartrefresh.MyNestedScrollChild>
    </com.example.lenovo.mysmartrefresh.MyNestedScrollParent>
</RelativeLayout>

res/value/string:

<resources>
    <string name="app_name">MySmartRefresh</string>


    <string name="strs">123\n456\n789\n111\n222\n333\n444\n555\n666\n777\n888\n999\n14\n12\n13
                \n44\n55\n66\n77\n88\n444\n555\n666\n777\n888\n999\n14\n12\n13\n44\n55\n66\n77\n88\n444\n555\n666\n777\n888\n999\n14\n12\n13\n44\n55\n66\
                n77\n88\n444\n555\n666\n777\n888\n999\n14\n12\n13\n44\n55\n66\n77\n88\n444\n555\n666
                \n777\n888\n999\n14\n12\n13\n44\n55\n66\n77\n88\n99\n11\n22\n33\n44\n55\n66\n77\n88\n99\n77\n88\n88\n8\n88\n88\n</string>
</resources>

MyNestedScrollParent

package com.example.lenovo.mysmartrefresh;

import android.annotation.SuppressLint;
import android.content.Context;
import android.support.v4.view.NestedScrollingParent;
import android.support.v4.view.NestedScrollingParentHelper;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

/**
 *  ①、在onStartNestedScroll()中判断参数target是哪一个子view以及滚动的方向,然后决定是否要配合其进行嵌套滚动
 *   ②、在onNestedPreScroll()中获取需要滚动的距离,根据情况决定自己是否要进行滚动,最后还要将自己滚动消费掉的距离存储在consumed数组中回传给child
 *
 * Created by lenovo on 2017/7/19.
 */

public class MyNestedScrollParent extends LinearLayout implements NestedScrollingParent {
    private ImageView img;
    private TextView tv;
    private MyNestedScrollChild myNestedScrollChild;
    private NestedScrollingParentHelper mNestedScrollingParentHelper;
    private int imgHeight;
    private int tvHeight;

    public MyNestedScrollParent(Context context) {
        super(context);
    }

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

    private void init() {
        mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
    }

    //获取子view
    @SuppressLint("MissingSuperCall")
    @Override
    protected void onFinishInflate() {
        img = (ImageView) getChildAt(0);
        tv = (TextView) getChildAt(1);
        myNestedScrollChild = (MyNestedScrollChild) getChildAt(2);
        img.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                if (imgHeight <= 0) {
                    imgHeight = img.getMeasuredHeight();
                }
            }
        });

        tv.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                if (tvHeight <= 0) {
                    tvHeight = tv.getMeasuredHeight();
                }
            }
        });
    }

    //在此可以判断参数target是哪一个子view以及滚动的方向,然后决定是否要配合其进行嵌套滚动
    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        if (target instanceof MyNestedScrollChild) {
            return true;
        }
        return false;
    }


    @Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
        mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
    }

    @Override
    public void onStopNestedScroll(View target) {
        mNestedScrollingParentHelper.onStopNestedScroll(target);
    }

    //先于child滚动
    //前3个为输入参数,最后一个是输出参数
    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        if (showImg(dy) || hideImg(dy)) {//如果需要显示或隐藏图片,即需要自己(parent)滚动
            scrollBy(0, -dy);//滚动
            consumed[1] = dy;//告诉child我消费了多少
        }
    }

    //后于child滚动
    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {

    }

    //返回值:是否消费了fling
    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        return false;
    }

    //返回值:是否消费了fling
    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        return false;
    }

    @Override
    public int getNestedScrollAxes() {
        return mNestedScrollingParentHelper.getNestedScrollAxes();
    }

    //下拉的时候是否要向下滚动以显示图片
    public boolean showImg(int dy) {
        if (dy > 0) {
            if (getScrollY() > 0 && myNestedScrollChild.getScrollY() == 0) {
                return true;
            }
        }

        return false;
    }

    //上拉的时候,是否要向上滚动,隐藏图片
    public boolean hideImg(int dy) {
        if (dy < 0) {
            if (getScrollY() < imgHeight) {
                return true;
            }
        }
        return false;
    }

    //scrollBy内部会调用scrollTo
    //限制滚动范围
    @Override
    public void scrollTo(int x, int y) {
        if (y < 0) {
            y = 0;
        }
        if (y > imgHeight) {
            y = imgHeight;
        }

        super.scrollTo(x, y);
    }
}

MyNestedScrollChild

package com.example.lenovo.mysmartrefresh;

import android.content.Context;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.support.v4.view.NestedScrollingChild;
import android.support.v4.view.NestedScrollingChildHelper;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.LinearLayout;

/**
 * Created by lenovo on 2017/7/19.
 */

public class MyNestedScrollChild extends LinearLayout implements NestedScrollingChild {
    private NestedScrollingChildHelper mNestedScrollingChildHelper;
    private final int[] offset = new int[2]; //偏移量
    private final int[] consumed = new int[2]; //消费
    private int lastY;
    private int showHeight;


    public MyNestedScrollChild(Context context) {
        super(context);
    }

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //第一次测量,因为布局文件中高度是wrap_content,因此测量模式为atmost,即高度不超过父控件的剩余空间
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        showHeight = getMeasuredHeight();

        //第二次测量,对稿哦度没有任何限制,那么测量出来的就是完全展示内容所需要的高度
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            //按下
            case MotionEvent.ACTION_DOWN:
                lastY = (int) event.getRawY();
                break;
            //移动
            case MotionEvent.ACTION_MOVE:
                int y = (int) (event.getRawY());
                int dy = y - lastY;
                lastY = y;
                if (startNestedScroll(ViewCompat.SCROLL_AXIS_HORIZONTAL)
                        && dispatchNestedPreScroll(0, dy, consumed, offset)) //如果找到了支持嵌套滑动的父类,父类进行了一系列的滑动
                {
                    //获取滑动距离
                    int remain = dy - consumed[1];
                    if (remain != 0) {
                        scrollBy(0, -remain);
                    }

                } else {
                    scrollBy(0, -dy);
                }
                break;
        }

        return true;
    }

    //限制滚动范围
    @Override
    public void scrollTo(int x, int y) {
        int maxY = getMeasuredHeight() - showHeight;
        if (y > maxY) {
            y = maxY;
        }
        if (y < 0) {
            y = 0;
        }
        super.scrollTo(x, y);
    }

    //初始化helper对象
    private NestedScrollingChildHelper getScrollingChildHelper() {
        if (mNestedScrollingChildHelper == null) {
            mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
            mNestedScrollingChildHelper.setNestedScrollingEnabled(true);
        }
        return mNestedScrollingChildHelper;
    }

    //实现一下接口
    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        getScrollingChildHelper().setNestedScrollingEnabled(enabled);
    }

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

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

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

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

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

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

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

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

效果图:

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值