Android NestedScrolling 的使用

自从Lollipop开始,谷歌给我们带来了一套全新的嵌套滑动机制 - NestedScrolling来实现一些普通情况下不容易办到的滑动效果。Lollipop及以上版本的所有View都已经支持了这套机制,Lollipop之前版本可以通过Support包进行向前兼容。

它和我们已熟知的dispatchTouchEvent不太一样。
我们先来看传统的事件分发,它是由父View发起,一旦父View需要自己做滑动效果就要拦截掉事件并通过自己的onTouch进行消耗,这样子View就再没有机会接手此事件,如果自己不拦截交给子View消耗,那么不使用特殊手段的话父View也没法再处理此事件。

// Lollipop及以上版本的View源码多了这么几个方法:

public void setNestedScrollingEnabled(boolean enabled);
public boolean isNestedScrollingEnabled();
public boolean startNestedScroll(int axes);
public void stopNestedScroll();
public boolean hasNestedScrollingParent();
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
public boolean dispatchNestedPreFling(float velocityX, float velocityY);

//Lollipop及以上版本的ViewGroup源码多了这么几个方法:

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();

前面已经说过Lollipop及以上版本的所有View都已经支持了NestedScrolling,Lollipop之前版本需要通过Support包进行向前兼容,需要Support包里的
以下4个类:

NestedScrollingParent   // 接口 
NestedScrollingParentHelper //辅助类
NestedScrollingChild       //接口
NestedScrollingChildHelper //辅助类`

上面NestedScrollingParent和NestedScrollingChild两个接口分别包含了ViewGroup和View中
涉及到NestedScrolling的所有Api.

那要怎么实现接口中辣么多的方法呢?
这就要用到上面的Helper辅助类了,Helper类中已经写好了大部分方法的实现,只需要调用就可以了。

NestedScrolling 相关Api的调用流程分析

1.首先子View需要找到一个支持NestedScrollingParent的父View,告知父View我准备开始和你一起处理滑动事件了,一般情况下都是在onTouchEvent的ACTION_DOWN中调用
public boolean startNestedScroll(int axes)//参数表示方向
axes可传如下参数

 /**
     * Indicates no axis of view scrolling.
     */
    public static final int SCROLL_AXIS_NONE = 0;

    /**
     * Indicates scrolling along the horizontal axis.
     */
    public static final int SCROLL_AXIS_HORIZONTAL = 1 << 0;

    /**
     * Indicates scrolling along the vertical axis.
     */
    public static final int SCROLL_AXIS_VERTICAL = 1 << 1;

2.然后父View就会被回调
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) //返回值表示是否接受嵌套滑动

public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes)//紧接着上面方法之后调用,可做初始化操作

3.然后每次子View在滑动前都需要将滑动细节传递给父View,一般情况下是在
ACTION_MOVE中调用

/**
*
* @param dx x轴滑动距离
* @param dy y轴滑动距离
* @param consumed 子View创建给父View使用的数组,用于保存父View的消费距离
* @param offsetInWindow 子View创建给父View使用的数组,保存了子View滑动前后的坐标偏移量
* @return 返回父View是否有消费距离
*/
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow)

4.然后父View就会被回调

/**
* @param target 子View
* @param dx 子View需要在x轴滑动的距离
* @param dy 子View需要在y轴滑动的距离
* @param consumed 子View传给父View的数组,用于保存消费的x和y方向的距离
**/
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed)

5.父View处理完后,接下来子View就要进自己的滑动操作了,滑动完成后子View还需要调用下面的方法将自己的滑动结果再次传递给父View.
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,int dyUnconsumed, int[] offsetInWindow)

6.然后父View就会被回调

/**
* @param target 子View
* @param dxConsumed x轴被子View消耗的距离
* @param dyConsumed y轴被子View消耗的距离
* @param dxUnconsumed x轴未被子View消耗的距离
* @param dyUnconsumed y轴未被子View消耗的距离
**/
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, 
	int dxUnconsumed, int dyUnconsumed)

这个步骤的前提是:
父View没有将滑动值全部消耗掉,因为父View全部消耗掉,子View就不用再进行滑动了.

7.随着ACTION_UP或者ACTION_CANCEL的到来,子View需要调用
public void stopNestedScroll()//告知父View本次NestedScrollig结束.

8.父View对应的会被回调
public void onStopNestedScroll(View target)//可以在此方法中做一些对应停止的逻辑操作比如资源释放等.

9.如果当子View ACTION_UP时伴随着fling的产生,就需要子View在stopNestedScroll前调用
public boolean dispatchNestedPreFling(View target, float velocityX, float velocityY)

public boolean dispatchNestedFling(View target, float velocityX, float velocityY, boolean consumed)

10.父View对应的会被回调
public boolean onNestedPreFling(View target, float velocityX, float velocityY)

public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed)
这点和之前的scroll处理逻辑是一样的,返回值代表父View是否消耗掉了fling,参数
consumed代表子View是否消耗掉了fling,fling不存在部分消耗,一旦被消耗就是指全部。

流程图如下:

这里写图片描述

下面来一个Demo演示下如何使用的.
效果图:
这里写图片描述

布局文件

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


    <blog.csdn.net.mchenys.demo1.MyNestedScrollParent
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ImageView
            android:id="@+id/imageview"
            android:background="@drawable/icon_default"
            android:layout_width="match_parent"
            android:layout_height="200dp"/>

        <TextView
            android:textColor="#fff"
            android:text="固定栏"
            android:gravity="center"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:background="@color/colorAccent"/>

         <blog.csdn.net.mchenys.demo1.MyNestedScrollChild
             android:orientation="vertical"
             android:layout_width="match_parent"
             android:layout_height="wrap_content">

             <ImageView
                 android:scaleType="fitXY"
                 android:src="@drawable/icon_default"
                 android:layout_gravity="center_horizontal"
                 android:layout_width="250dp"
                 android:layout_height="300dp"/>
             <ImageView
                 android:scaleType="fitXY"
                 android:src="@drawable/icon_default"
                 android:layout_gravity="center_horizontal"
                 android:layout_width="250dp"
                 android:layout_height="300dp"/>
             <ImageView
                 android:scaleType="fitXY"
                 android:src="@drawable/icon_default"
                 android:layout_gravity="center_horizontal"
                 android:layout_width="250dp"
                 android:layout_height="300dp"/>

             <ImageView
                 android:scaleType="fitXY"
                 android:src="@drawable/icon_default"
                 android:layout_gravity="center_horizontal"
                 android:layout_width="250dp"
                 android:layout_height="300dp"/>
             <ImageView
                 android:scaleType="fitXY"
                 android:src="@drawable/icon_default"
                 android:layout_gravity="center_horizontal"
                 android:layout_width="250dp"
                 android:layout_height="300dp"/>
             <ImageView
                 android:scaleType="fitXY"
                 android:src="@drawable/icon_default"
                 android:layout_gravity="center_horizontal"
                 android:layout_width="250dp"
                 android:layout_height="300dp"/>
         </blog.csdn.net.mchenys.demo1.MyNestedScrollChild>


    </blog.csdn.net.mchenys.demo1.MyNestedScrollParent>
</FrameLayout>

MyNestedScrollParent.java

package blog.csdn.net.mchenys.demo1;

import android.content.Context;
import android.support.v4.view.NestedScrollingParent;
import android.support.v4.view.NestedScrollingParentHelper;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.OverScroller;
import android.widget.TextView;


public class MyNestedScrollParent extends LinearLayout implements NestedScrollingParent {
    private static final String TAG = "MyNestedScrollParent";
    private ImageView img;
    private TextView tv;
    private MyNestedScrollChild myNestedScrollChild;
    private NestedScrollingParentHelper mNestedScrollingParentHelper;
    private int imgHeight;
    private int tvHeight;
    private OverScroller mScroller;

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

    public MyNestedScrollParent(Context context, AttributeSet attrs) {
        super(context, attrs);
        mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
        mScroller = new OverScroller(context);
    }

    //获取子view
    @Override
    protected void onFinishInflate() {
        super.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个为输入参数,最后一个是输出参数

    /**
     *
     * @param target 子View
     * @param dx 子View需要在x轴滑动的距离
     * @param dy 子View需要在y轴滑动的距离
     * @param consumed 子View传给父View的数组,用于保存消费的x和y方向的距离
     */
    @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滚动

    /**
     *
     * @param target 子View
     * @param dxConsumed x轴被子View消耗的距离
     * @param dyConsumed y轴被子View消耗的距离
     * @param dxUnconsumed x轴未被子View消耗的距离
     * @param dyUnconsumed y轴未被子View消耗的距离
     */
    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        if (dyUnconsumed > 0) {
            // 如果子View还有为消费的,可以继续消费
            scrollBy(0, -dyUnconsumed);//滚动
        }
    }

    //返回值:是否消费了fling 先于child fling
    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        if (getScrollY() >= 0 && getScrollY() < imgHeight) {
            fling((int) velocityY);
            return true;
        }
        return false;

    }

    //返回值:是否消费了fling,后于child fling
    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        if(!consumed){
            fling((int) velocityY);
            return true;
        }
        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);
    }


    public void fling(int velocityY) {
        Log.e("parent", "velocityY:" + velocityY);
        mScroller.fling(0, getScrollY(), 0, velocityY, 0, 0, 0, imgHeight);
        invalidate();
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            scrollTo(0, mScroller.getCurrY());
            postInvalidate();
        }
    }

    //处理自身的滚动逻辑
    private int lastY;
    private VelocityTracker mVelocityTracker;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);
        switch (event.getAction()) {
            //按下
            case MotionEvent.ACTION_DOWN:
                lastY = (int) event.getRawY();
                if (!mScroller.isFinished()) { //fling
                    mScroller.abortAnimation();
                }
                break;
            //移动
            case MotionEvent.ACTION_MOVE:
                int y = (int) (event.getRawY());
                int dy = y - lastY;
                lastY = y;
                scrollBy(0, -dy);
                break;
            case MotionEvent.ACTION_UP:
                mVelocityTracker.computeCurrentVelocity(1000);
                int vy = (int) mVelocityTracker.getYVelocity();
                fling(-vy);
                break;
        }

        return true;
    }
}

MyNestedScrollChild.java

package blog.csdn.net.mchenys.demo1;

import android.content.Context;
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.VelocityTracker;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.OverScroller;



public class MyNestedScrollChild extends LinearLayout implements NestedScrollingChild {
    private static final String TAG = "MyNestedScrollChild";
    private NestedScrollingChildHelper mNestedScrollingChildHelper;
    private final int[] offset = new int[2]; //偏移量
    private final int[] consumed = new int[2]; //消费
    private int lastY;
    private int maxScrollY;//最大滚动距离
    private OverScroller mScroller;
    private VelocityTracker mVelocityTracker;

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

    public MyNestedScrollChild(Context context, AttributeSet attrs) {
        super(context, attrs);
        mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
        setNestedScrollingEnabled(true);
        mScroller = new OverScroller(context);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int contentHeight = 0;
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            measureChild(view, widthMeasureSpec, heightMeasureSpec);
            contentHeight += view.getMeasuredHeight();//内容高度
        }
        int parentHeight = ((ViewGroup) getParent()).getMeasuredHeight();//父view高度
        int pinTopHeight = (int) (getResources().getDisplayMetrics().density * 50 + 0.5);//固定头的高度
        int visibleHeight = parentHeight - pinTopHeight;//可见高度
        maxScrollY = contentHeight - visibleHeight;

        setMeasuredDimension(getMeasuredWidth(), visibleHeight);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);
        switch (event.getAction()) {
            //按下
            case MotionEvent.ACTION_DOWN:
                lastY = (int) event.getRawY();
                if (!mScroller.isFinished()) { //fling
                    mScroller.abortAnimation();
                }
                break;
            //移动
            case MotionEvent.ACTION_MOVE:
                int y = (int) (event.getRawY());
                int dy = y - lastY;
                lastY = y;
                if (startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)
                        && dispatchNestedPreScroll(0, dy, consumed, offset)) { //父类有消费距离

                    //获取滑动距离
                    int remain = dy - consumed[1];
                    if (remain != 0) {
                        scrollBy(0, -remain);
                        //这个时候由于子View已经全部消费调了剩余的距离,其实可以不用调用下面这个方法了.
                        //dispatchNestedScroll(0, remain, 0, 10, offset);
                    }

                } else {
                    scrollBy(0, -dy);
                }
                break;
            case MotionEvent.ACTION_UP:
                mVelocityTracker.computeCurrentVelocity(1000);
                float vy = mVelocityTracker.getYVelocity();
                if (!dispatchNestedPreFling(0, -vy)) {
                    //父View没有fling,则子View处理
                    fling(-vy);
                    //这句话可以不用调了,因为这子View已经处理了fling
                    //dispatchNestedFling(0, -vy, true);
                }
                break;
        }

        return true;
    }

    //限制滚动范围
    @Override
    public void scrollTo(int x, int y) {
        Log.d(TAG, "Y:" + y + " maxScrollY:" + maxScrollY);
        if (y > maxScrollY) {
            y = maxScrollY;
        }
        if (y < 0) {
            y = 0;
        }
        super.scrollTo(x, y);
    }


    public void fling(float velocityY) {
        mScroller.fling(0, getScrollY(), 0, (int) velocityY, 0, 0, 0, maxScrollY);
        invalidate();
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            scrollTo(0, mScroller.getCurrY());
            postInvalidate();
        }
    }

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

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

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

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

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

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

    /**
     *
     * @param dx x轴滑动距离
     * @param dy y轴滑动距离
     * @param consumed 子View创建给父View使用的数组,用于保存父View的消费距离
     * @param offsetInWindow 子View创建给父View使用的数组,保存了子View滑动前后的坐标偏移量
     * @return 返回父View是否有消费距离
     */
    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        return mNestedScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

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

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

在来看一个例子

这里写图片描述

本例中的布局由三部分组成,一个是带下拉刷新的自定义布局,一个是带嵌套滑动的自定义布局,最后一个是ViewPager,如下图所示:
这里写图片描述

1.红色区域是带下拉刷新的LinearLayout,通过重写onInterceptTouchEvent实现触摸事件拦截,重写onTouchEvent实现刷新头的滑动.
2.绿色区域是带嵌套滚动的LinearLayout,实现了NestedScrollingParent接口处理和子View中的RecycleView的嵌套滑动效果.其内部包含了焦点图、tab导航条、ViewPager.
同时还重写了onInterceptTouchEvent方法拦截事件交给自己的onTouchEvent去处理焦点图的滑动,当焦点图划出屏幕后,该View将不能继续上拉滑动,但是事件已经拦截,如何传递给RecycleView呢?
这里可以巧妙的在其onTouchEvent方法中调用RecycleView的onTouchEvent方法去处理,否则就只能松手后才能滑动RecycleView.
至于当焦点图划出屏幕后,如果RecycleView已经滚动了一段距离,下拉RecycleView不松手也能划出焦点图这个效果就是NestedScrolling 来实现的了.
3.蓝色区域就是ViewPager,它里面展示的是Fragment,而Fragment里面放的才是RecycleView.由于RecycleView是实现了NestedScrollingChild接口的,所以可以完成嵌套滑动.
从这点也可以看出,嵌套滑动的组件,只要有包含关系,无论是否是直接包含都是有效果的.

Demo下载

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值