Android NestedScroll嵌套滑动机制解析

Android NestedScroll嵌套滑动机制解析

目录

Android提供了一个官方的嵌套滑动机制,让子View实现NestedScrollingChild或者NestedScrollingChild2接口,父布局实现NestedScrollingParentNestedScrollingParent2接口,Android官方还提供了NestedScrollingChildHelperNestedScrollingChildHelper两个帮助类,让开发者更容易实现嵌套滑动的逻辑.

最初的接口和接口2的区别

子View的接口有NestedScrollingChildNestedScrollingChild2,NestedScrollingChild2继承于NestedScrollingChild,然后以多态的形式给大部分的方法都加了一个int类型的NestedScrollType,这个int值是用来区分是用户触摸滑动还是其他滑动的,其共有两个类型

    /**
     * Indicates that the input type for the gesture is from a user touching the screen.
     */
    public static final int TYPE_TOUCH = 0;

    /**
     * Indicates that the input type for the gesture is caused by something which is not a user
     * touching a screen. This is usually from a fling which is settling.
     */
    public static final int TYPE_NON_TOUCH = 1;

根据英文释义,TYPE_TOUCH为用户触摸操作的类型,TYPE_NON_TOUCH为非用户触摸操作类型,而且主要用于代码中的惯性操作,比如View滑动时的惯性滑动.

原始接口NestedScrollingChild默认类型为TYPE_TOUCH,如果需要实现子View和父View的惯性嵌套滑动则需要实现NestedScrollingChild2接口

父View接口NestedScrollingParentNestedScrollingParent2和子View一样在大部分方法中添加了NestedScrollType,在此不做赘述.

接口方法介绍

在此只介绍原始接口的方法,对于扩展的第二接口由于只在原基础上加了一个类型,不多做介绍

NestedScrollingChild

public interface NestedScrollingChild {
    /**
     * 启用或者禁止嵌套滑动
     */
    void setNestedScrollingEnabled(boolean enabled);

    /**
     * 用于判断嵌套滑动是否被启用
     */
    boolean isNestedScrollingEnabled();

    /**
     * 开始嵌套滑动,参数为滑动方向,参数有如下几个
     * 
     * 没有滑动方向
     * public static final int SCROLL_AXIS_NONE = 0;
     * 
     * 横向滑动
     * public static final int SCROLL_AXIS_HORIZONTAL = 1 << 0;
     * 
     * 纵向滑动
     * public static final int SCROLL_AXIS_VERTICAL = 1 << 1;
     * 
     * 其返回值代表父View是否接受嵌套滑动,如果不接受返回false,后续的嵌套滑动都将失效
     */
    boolean startNestedScroll(@ScrollAxis int axes);

    /**
     * 是否有实现了NestedScrollingParent的父View
     * 如果父View没有实现接口,此方法返回false,且所有嵌套滑动无效
     */
    boolean hasNestedScrollingParent();

    /**
     * 分发嵌套滑动事件,在子View滑动处理完之后调用
     */
    boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow);

    /**
     * 分发预嵌套滑动事件,在子View滑动处理之前调用
     */
    boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
            @Nullable int[] offsetInWindow);

    /**
     * 分发嵌套滑动的惯性滑动处理,返回值表示是否处理
     */
    boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);

    /**
     * 分发嵌套滑动的惯性滑动预处理,返回值表示是否处理,在子View处理之前调用
     */
    boolean dispatchNestedPreFling(float velocityX, float velocityY);
}

NestedScrollingParent

此接口都是NestedScrollingChild的接口回调,在子View接口方法被调用时便会调用父View的NestedScrollingParent的方法,它们有着一一对应的关系,具体如下

NestedScrollingChild NestedScrollingParent 备注
startNestedScroll onStartNestedScroll 前者的调用会触发后者的调用,然后后者的返回值将决定后续的嵌套滑动事件是否能传递给父View,如果返回false,父View将不处理嵌套滑动事件,一般前者的返回值即后者的返回值
onNestedScrollAccepted 如果onStartNestedScroll返回true,则回调此方法
stopNestedScroll onStopNestedScroll
dispatchNestedScroll onNestedScroll
dispatchNestedPreScroll onNestedPreScroll
dispatchNestedFling onNestedFling
dispatchNestedPreFling onNestedPreFling
getNestedScrollAxes 获得滑动方向,没有回调,为主动调用的方法

使用方法

子View接口的使用

子View接口常用实现

子View的接口通常都是借助NestedScrollingChildHelper通过委派模式实现的,没有直接写在某个嵌套滑动子View里,提升了代码复用性,还是很高明的做法.

具体如下

在类中声明NestedScrollingChildHelper对象

private final NestedScrollingChildHelper mChildHelper;

然后在子View构造函数中实例化

mChildHelper = new NestedScrollingChildHelper(this);

接下来实现NestedScrollingChild接口

  @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, int[] offsetInWindow) {
        return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
                offsetInWindow);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, 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);
    }

子View接口的自我调用

在实现了子View的接口后,其实嵌套滑动并没有效果,因为根本没有哪里调用实现接口的方法.

然后一般,接口方法的调用其实是子View自己来调用的,可以说大部分NestedScrollingChild接口的方法是自用的.

嵌套滑动的实现是通过子View再将触摸事件回传给父View的,所以大部分的嵌套滑动逻辑都会放在子ViewonInterceptTouchEvent或者onTouchEvent中.

其大致有如下流程

在构造函数中启用嵌套滑动

setNestedScrollingEnabled(true);
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                startNestedScroll(SCROLL_AXIS_VERTICAL);
                // handle touch down event here

                break;
            case MotionEvent.ACTION_MOVE:
                if (dispatchNestedPreScroll(dx, dy, comsumed, offsetInWindow)) {

                }
                // handle touch move event here
                if (dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow)) {

                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                if (!dispatchNestedPreFling(velocityX, velocityY)) {
                    dispatchNestedFling(velocityX, velocityY, canScroll);
                }
                stopNestedScroll();
                break;
            default:
                // do nothing
                break;
        }
        return true;
    }

在这里重点讲解下dispatchNestedPreScrolldispatchNestedScroll方法

dispatchNestedPreScroll方法参数
  • dx,dy

这两个参数为上一次触摸点和当前触摸点的x轴和y轴坐标差值.在SDK中的NestedScrollViewRecyclerView的源码中这个值是使用的上一次触摸点的坐标减去当前触摸点的坐标,这和通常逻辑上的dx,dy正好相反,猜测是代指View应当滑动的距离.毕竟手指向上滑动,View的scroll是向下的.在写代码时,应当尽可能遵循其规则,使用上一次的坐标值减去当前值
- consumed

这是一个长度为2的int类型数组,用于储存父View的消耗的长度.然后在子View中处理滑动时需要减去父View的长度消耗,这样才能和真实的滑动的距离相平衡.
- offsetInWindow

这也是一个长度为2的int类型的数组,用于储存子View在父View中的偏移值,这个值一般会等于(comsumed中的值 * -1),但是也有绝对值不相等的时候,就是嵌套滑动不止两层,父View的parent也处理了部分的嵌套滑动.这时comsumed和offsetInWindow值是不相等的.

dispatchNestedScroll方法参数
  • dxConsumed,dyConsumed
@param dxConsumed Horizontal distance in pixels consumed by this view during this scroll step
@param dyConsumed Vertical distance in pixels consumed by this view during this scroll step

对于这两个参数,源码的解释是这样的.在这个滑动阶段中子View的距离消耗.

查阅NestedScrollParentonNestedScroll各View的方法实现,未发现这两个参数的实际使用.一般这两个参数使用也较少.

  • dxUnconsumed,dyUnconsumed

这两个参数是当子View滑动完后剩余应当滑动的距离.这个一般用在子View已经滑动到顶部或者底部时,将滑动事件分发给父View处理.所以这两个是关键父View需要处理的数据.

  • offsetInWindow

这个和dispatchNestedPreScroll一样是一个长度为2的int类型的数组,用于储存子View在父View中的偏移值

具体使用操作流程
  1. 在move事件处理时,先通过dispatchNestedPreScroll将整个的滑动距离dx,dy传递给父View,然后父View选择性处理一部分距离,将处理了的距离储存在consumed数组中,其中consumed[0]为x轴处理距离,consumed[1]为y轴处理距离.
  2. 然后子View根据自己的需要处理剩余的距离.
  3. 如果子View未能将剩余距离消耗掉,通过dispatchNestedScroll将剩余的滑动通过参数dxUnconsumed,dyUnconsumed交给父View处理.一般来说dispatchNestedPreScrolldispatchNestedScroll只有一个会得到实际上的使用.

父View接口的使用

父View接口常用实现

在类中声明NestedScrollingParentHelper对象

private final NestedScrollingParentHelper mParentHelper;

然后在子View构造函数中实例化

mParentHelper = new NestedScrollingParentHelper(this);

接下来实现NestedScrollingParent接口


    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

    @Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {

    }

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

    }

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
            int dyUnconsumed) {

    }

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {

    }

    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {

        return false;
    }

    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        return false;
    }

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

使用举例

原理解释完了,现在来实践一波

主要代码

MainActivity.java

package com.yxf.nestedscrolldemo;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.ListView;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ListView listView = (ListView) findViewById(R.id.list_view);
        String[] data = new String[]{
                "0",
                "1",
                "2",
                "3",
                "4",
                "5",
                "6",
                "7",
                "8",
                "9",
                "0",
                "1",
                "2",
                "3",
                "4",
                "5",
                "6",
                "7",
                "8",
                "9",
                "0",
                "1",
                "2",
                "3",
                "4",
                "5",
                "6",
                "7",
                "8",
                "9",
        };
        ArrayAdapter<String> arrayAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data);
        listView.setAdapter(arrayAdapter);

    }

}

NestedScrollParentLinearLayout.java

package com.yxf.nestedscrolldemo;

import android.content.Context;
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.support.v4.view.NestedScrollingParent;
import android.support.v4.view.NestedScrollingParentHelper;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.LinearLayout;

public class NestedScrollParentLinearLayout extends LinearLayout implements NestedScrollingParent {

    private static final String TAG = NestedScrollParentLinearLayout.class.getSimpleName();
    private final NestedScrollingParentHelper mParentHelper;

    public NestedScrollParentLinearLayout(Context context) {
        this(context, null);
    }

    public NestedScrollParentLinearLayout(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public NestedScrollParentLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setOrientation(VERTICAL);
        mParentHelper = new NestedScrollingParentHelper(this);
    }

    // NestedScrollingParent

    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

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

    }

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

    }

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
                               int dyUnconsumed) {
        int scrollY = getScrollY();
        int step;
        if (scrollY + dyUnconsumed < 0) {
            step = -scrollY;
        } else {
            int height = getChildAt(getChildCount() - 1).getBottom();
            Rect rect = new Rect();
            getLocalVisibleRect(rect);
            int visibleHeight = rect.bottom;
            if (visibleHeight < height && dyUnconsumed > 0) {
                step = Math.min(dyUnconsumed, height - visibleHeight);
            } else if (rect.top > 0 && dyUnconsumed < 0) {
                step = Math.max(dyUnconsumed, -rect.top);
            } else {
                step = 0;
            }
        }
        scrollBy(0, step);
        Log.d(TAG, "onNestedScroll: Y scrollBy : " + step);
    }

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        int targetTop = target.getTop();
        int targetBottom = target.getBottom();
        int scrollY = getScrollY();
        int currentTargetBottom = targetBottom - scrollY;
        if (scrollY < targetTop && dy > 0 && scrollY >= 0) {
            consumed[0] = 0;
            consumed[1] = Math.min(dy, targetTop - scrollY);
            Log.d(TAG, "onNestedPreScroll: Y scrollBy : " + consumed[1]);
        } else if (currentTargetBottom < getBottom() && dy < 0) {
            consumed[0] = 0;
            consumed[1] = Math.max(dy, currentTargetBottom - getBottom());
        } else {
            consumed[0] = 0;
            consumed[1] = 0;
        }
        scrollBy(0, consumed[1]);
    }

    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        if (!consumed) {

            return true;
        }
        return false;
    }

    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        return false;
    }

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

NestedScrollChildListView.java

package com.yxf.nestedscrolldemo;

import android.content.Context;
import android.graphics.Rect;
import android.support.v4.view.NestedScrollingChild;
import android.support.v4.view.NestedScrollingChildHelper;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ListView;

import static android.support.v4.widget.ViewDragHelper.INVALID_POINTER;

public class NestedScrollChildListView extends ListView implements NestedScrollingChild {

    private static final String TAG = NestedScrollChildListView.class.getSimpleName();
    private final NestedScrollingChildHelper mChildHelper;
    private final int[] mScrollConsumed = new int[2];
    private final int[] mScrollOffset = new int[2];

    private int mActivePointerId = INVALID_POINTER;

    private int mNestedYOffset;

    private int mLastScrollerY;

    private int mLastMotionY;

    private int lastY = -1;

    private int oldTop = 0;


    public NestedScrollChildListView(Context context) {
        this(context, null);
    }

    public NestedScrollChildListView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public NestedScrollChildListView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mChildHelper = new NestedScrollingChildHelper(this);
        setNestedScrollingEnabled(true);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        MotionEvent vtev = MotionEvent.obtain(event);

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            mNestedYOffset = 0;
        }


        switch (actionMasked) {
            case MotionEvent.ACTION_DOWN:
                mLastMotionY = (int) event.getY();
                mActivePointerId = event.getPointerId(0);
                oldTop = getTop();
                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                stopNestedScroll();
                mActivePointerId = INVALID_POINTER;
                break;
            case MotionEvent.ACTION_MOVE:
                int currentTop = getTop();
                mNestedYOffset = currentTop - oldTop;
                final int activePointerIndex = event.findPointerIndex(mActivePointerId);
                if (activePointerIndex == -1) {
                    Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
                    break;
                }

                final int y = (int) event.getY(activePointerIndex);
                int deltaY = mLastMotionY - y;
                if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
                    Log.d(TAG, "onTouchEvent: deltaY : " + deltaY + " , mScrollConsumedY : " + mScrollConsumed[1] + " , mScrollOffset : " + mScrollOffset[1]);
                    vtev.offsetLocation(0, mScrollConsumed[1]);
                    deltaY -= mScrollConsumed[1];
                }
                mLastMotionY = y - mScrollOffset[1];
                Rect rect = new Rect();
                if (getLocalVisibleRect(rect)) {
                    Log.d(TAG, "onTouchEvent: rect : " + rect);
                } else {
                    Log.d(TAG, "onTouchEvent: visible rect got failed");
                }
                int consumeY = deltaY;
                if (getFirstVisiblePosition() == 0) {
                    int top = getChildAt(0).getTop();
                    if (rect.top + deltaY < top) {
                        consumeY = top - rect.top;
                    }
                } else if (getLastVisiblePosition() == getCount() - 1) {
                    int bottom = getChildAt(getChildCount() - 1).getBottom();
                    if (rect.bottom + deltaY > bottom) {
                        consumeY = bottom - rect.bottom;
                    }
                }
                if (Math.abs(consumeY) < Math.abs(deltaY)) {
                    deltaY -= consumeY;
                    Log.d(TAG, "onTouchEvent: consumeY :" + consumeY + " , deltaY : " + deltaY);
                    vtev.offsetLocation(0, consumeY);
                    if (dispatchNestedScroll(0, consumeY, 0, deltaY, mScrollOffset)) {

                    }
                }
                break;
            case MotionEvent.ACTION_POINTER_DOWN: {
                final int index = event.getActionIndex();
                mLastMotionY = (int) event.getY(index);
                mActivePointerId = event.getPointerId(index);
                break;
            }
            case MotionEvent.ACTION_POINTER_UP:
                mLastMotionY = (int) event.getY(event.findPointerIndex(mActivePointerId));
                break;
            default:

                break;
        }
        return super.onTouchEvent(vtev);
    }

    // NestedScrollingChild

    @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, int[] offsetInWindow) {
        return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
                offsetInWindow);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, 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);
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<com.yxf.nestedscrolldemo.NestedScrollParentLinearLayout 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="wrap_content"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="150dp"
        android:background="@color/colorAccent" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="150dp"
        android:background="@color/colorPrimaryDark" />

    <com.yxf.nestedscrolldemo.NestedScrollChildListView
        android:layout_width="match_parent"
        android:layout_height="600dp"
        android:id="@+id/list_view"
        >

    </com.yxf.nestedscrolldemo.NestedScrollChildListView>

    <TextView
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:background="@color/colorAccent" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:background="@color/colorPrimaryDark" />
</com.yxf.nestedscrolldemo.NestedScrollParentLinearLayout>

代码分析

嵌套事件分发都写在了NestedScrollChildListView中的onTouchEvent中,
应当注意其中的对于消耗值的处理,在传给父类的onTouchEvent方法之前必须将嵌套滑动的消耗距离减掉,不然滑动会卡顿或者距离不合理.

运行程序会得到一个非常简陋的界面如下

为了获得列表的最大显示效果,当列表的View不能完全占据屏幕时,需要先分发NestedPreScroll事件,在onNestedPreScroll中处理让父View让出屏幕空间.

其效果如下

然后在列表已经滑动到顶部或者底部时,应当将列表推出屏幕让其他的View显示出来,这部分逻辑放在了onNestedScroll中处理.

其效果如下

注意

  • 当前使用的嵌套滑动和Materials Design中使Toolbar上滑的效果实现有相似的地方但是并不一样,在下一篇文章中,将介绍其中使用到的CoordinatorLayout和Behavior原理.
  • 嵌套滑动是从Android 5.0开始引入的,在4.4及以前的系统上说不定会产生一定的bug,不过既然已经了解原理,其实也可以自己尝试自己造轮子实现.

示例源码

NestedScrollDemo

猜你喜欢

转载自blog.csdn.net/dqh147258/article/details/81208889