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)//参数表示方向

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还需要
调用
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow),将自己的滑动结果再次传递给父View.

6.然后父View就会被回调

扫描二维码关注公众号,回复: 43367 查看本文章
/**
* @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_HORIZONTAL)
                        && 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);
    }
}

猜你喜欢

转载自blog.csdn.net/mChenys/article/details/80041618