自定义控件技能——嵌套滑动机制

Android中的嵌套滑动机制

Android5.0开始提供嵌套滑动机制,用于给子view与父view滑动互动提供更好的交互。
因为在原来的事件分发机制中,如果让子view开始处理事件后,父view有需要在某一个条件下处理事件,只能把子view的事件拦截,在接下来的一个完整的时间系类中,父view就无法继续给子view分发事件了,除非重写dispatchTouchEvent方法,但是我们知道重写这个方法还是比较有难度的。

在最新的V4包等兼容库中Android都对嵌套滑动提供了支持,主要类如下:

V4

  • NestedScrollingParent 嵌套滑动中父view接口
  • NestedScrollingParentHelper 嵌套滑动中父view接口的代理实现
  • NestedScrollingChild 嵌套滑动中子view接口
  • NestedScrollingChildHelper 嵌套滑动中子view接口的代理实现
  • NestedScrollView 支持嵌套滑动的ScrollView

design

  • CoordinatorLayout 协调器布局
  • CoordinatorLayout.Behavior

注意点

  1. 在嵌套滑动中的一些规则:子view是嵌套滑动的发起者,父view是嵌套滑动的处理者
  2. 在使用调用嵌套滑动相关的方法时,应该总是使用:ViewCompat,ViewGroupCompat, ViewParentCompat的静态方法来实现兼容
  3. 应该使用NestedScrollingParentHelper或NestedScrollingChildHelper的对应方法来实现NestedScrollingParent或NestedScrollingChild接口中的方法。

学习嵌套滑动机制

接下来对上述的一些类进行介绍

NestedScrollingChild与NestedScrollingParent

public interface NestedScrollingChild {
    public void setNestedScrollingEnabled(boolean enabled);
    public boolean isNestedScrollingEnabled();
    public boolean startNestedScroll(int axes);
    public boolean hasNestedScrollingParent();
    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);
    public void stopNestedScroll();



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实现,一个需要被嵌套滑动中的子view实现。

一个嵌套滑动的完成流程应该是这样的。

  1. 在一个可以滑动的子view中开启嵌套滑动setNestedScrollingEnabled
  2. 如果要开始一次嵌套滑动,首先应该调用startNestedScroll方法(比如在ACTION_DOWN中),通知父view开始一次嵌套滑动,方法的参数应该是ViewCompatSCROLL_AXIS_HORIZONTAL(横向)或ViewCompatSCROLL_AXIS_VERTICAL(竖向)或者他们的and/or值。这时父view的onStartNestedScroll方法将会被回调,如果父view返回true表示配合此次嵌套滑动,并且父view的onNestedScrollAccepted被调用
  3. 在子view开始滑动之前,应该先问父view许否需要先滑动,也就是调用dispatchNestedPreScroll方法,这个方法接收三个四个参数:
    • dxConsumed 表示子view此次滑动期间将要消耗的水平方法的距离
    • dyConsumed 表示子view此次滑动期间将要消耗的垂直方法的距离
    • consumed 一个两个长度的数组,这个数组传递给父view,如果父view要先行滑动,将会把消耗的距离通过此数据返回给子view
    • offsetInWindow 父view先完成一个滑动后子view在窗口中的偏移值。
    • 上面参数可以理解为:dxConsumed和dyConsumed是总的滑动值,传给父view,如果父view需要滑动有消耗掉一些距离,然后把消耗的距离放在consumed中,返回给子view,返回子view根据父view消耗的距离重新计算自己需要滑动的距离,进行滑动。这个过程发送在父view的onNestedPreScroll方法中。
  4. 子view在根据dispatchNestedPreScroll的返回值,然后计算被父view消耗的距离,根据需要位置
  5. 子view重新计算自己的滑动距离进行滑动之后,需要调用dispatchNestedScroll方法,此方法接收五个参数
    • int dxConsumed 子view在滑动中水平方向消耗的距离
    • int dyConsumed 子view在滑动中垂直方向消耗的距离
    • int dxUnconsumed 子view在滑动中水平方向没有消耗的距离
    • int dyUnconsumed 子view在滑动中垂直方向没有消耗的距离
    • int[] offsetInWindow 返回值。父view完成一个滑动后子view在窗口中的偏移值。
  6. 在完成一系列滑动后,如果需要停止滑动,则子view调用stopNestedScroll然后父view的onStopNestedScroll方法被回调

NestedScrollingParentHelper和NestedScrollingChildHelper分析

NestedScrollingParentHelper和NestedScrollingChildHelper是两个辅助类,分别对象上面分析的两个接口。系统已经给我们封装好了,我们只需要在对应的接口的方法中调用这些辅助类的实现即可。

NestedScrollingChildHelper

public class NestedScrollingChildHelper {
    private final View mView;//嵌套滑动中的子view
    private ViewParent mNestedScrollingParent;//嵌套滑动中的父view接口
    private boolean mIsNestedScrollingEnabled;//嵌套滑动是否可用
    private int[] mTempNestedScrollConsumed;

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

    //......省略一部分方法

    public boolean startNestedScroll(int axes) {
        if (hasNestedScrollingParent()) {//如果正在进行嵌套滑动,无需处理
            // Already in progress
            return true;
        }
        if (isNestedScrollingEnabled()) {//否则如果嵌套滑动时开启的,遍历查找可以配合嵌套滑动的父view
            ViewParent p = mView.getParent();
            View child = mView;
            while (p != null) {
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {//这里调用了父view的onStartNestedScroll询问是否配合嵌套滑动
                //如果配合的话,给mNestedScrollingParent赋值,再调用父view的onNestedScrollAccepted。
                    mNestedScrollingParent = p;
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
                    return true;//找到了就返回
                }
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }

    //停止嵌套滑动,就是调用父view的onStopNestedScroll,然后mNestedScrollingParent置为null
    public void stopNestedScroll() {
        if (mNestedScrollingParent != null) {
            ViewParentCompat.onStopNestedScroll(mNestedScrollingParent, mView);
            mNestedScrollingParent = null;
        }
    }


    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) {//判断输入值

            /*记录子view滑动前在窗口中的位置*/
                int startX = 0;
                int startY = 0;
                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    startX = offsetInWindow[0];
                    startY = offsetInWindow[1];
                }

                //子view滑动后,告诉父view滑动的距离
                ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed,
                        dyConsumed, dxUnconsumed, dyUnconsumed);

                if (offsetInWindow != null) {
                 //计算父view滑动后,子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;
    }

    //分发嵌套滑动,在子view开始滑动之前
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
            if (dx != 0 || dy != 0) {//判断 dx 与 dy
                /*记录子view滑动前在窗口中的位置*/
                int startX = 0;
                int startY = 0;
                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    startX = offsetInWindow[0];
                    startY = offsetInWindow[1];
                }

                if (consumed == null) {//处理==null的情况
                    if (mTempNestedScrollConsumed == null) {
                        mTempNestedScrollConsumed = new int[2];
                    }
                    consumed = mTempNestedScrollConsumed;
                }
                consumed[0] = 0;
                consumed[1] = 0;
                //让父view先滑动。
                ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);
                //计算父view滑动后,子view在窗口中的偏移值
                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    offsetInWindow[0] -= startX;
                    offsetInWindow[1] -= startY;
                }
                return consumed[0] != 0 || consumed[1] != 0;//如果父view消耗了一部分距离就返回ture
            } 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;
    }

    //......省略一些方法
}

NestedScrollingParentHelper

public class NestedScrollingParentHelper {
    private final ViewGroup mViewGroup;
    private int mNestedScrollAxes;
    public NestedScrollingParentHelper(ViewGroup viewGroup) {
        mViewGroup = viewGroup;
    }

    public void onNestedScrollAccepted(View child, View target, int axes) {
        mNestedScrollAxes = axes;
    }
    public int getNestedScrollAxes() {
        return mNestedScrollAxes;
    }
    public void onStopNestedScroll(View target) {
        mNestedScrollAxes = 0;
    }
}

NestedScrollingParentHelper就是记录NestedScrollAxes。

实战

开始实战之前,我也不知道到底那些消耗值改如何写,但是v4包中有个NestedScrollView可以拿来参考。

然后我们可以可以根据嵌套滑动写一个简单的demo,效果如下:

自定义控件技能——嵌套滑动机制_第1张图片

代码实现很简单:

嵌套滑动中的子view:

public class NestChildView extends View implements NestedScrollingChild {

    private static final String TAG = NestChildView.class.getSimpleName();

    private float mLastX;//手指在屏幕上最后的x位置
    private float mLastY;//手指在屏幕上最后的y位置

    private float mDownX;//手指第一次落下时的x位置(忽略)
    private float mDownY;//手指第一次落下时的y位置


    private int[] consumed = new int[2];//消耗的距离
    private int[] offsetInWindow = new int[2];//窗口偏移


    private NestedScrollingChildHelper mScrollingChildHelper;

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

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

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

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


    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        float x = ev.getX();
        float y = ev.getY();

        int action = ev.getAction();

        switch (action) {
            case MotionEvent.ACTION_DOWN: {

                mDownX = x;
                mDownY = y;
                mLastX = x;
                mLastY = y;
                //当开始滑动的时候,告诉父view
                startNestedScroll(ViewCompat.SCROLL_AXIS_HORIZONTAL | ViewCompat.SCROLL_AXIS_VERTICAL);
                break;
            }

            case MotionEvent.ACTION_MOVE: {
                /*
                mDownY:293.0
                mDownX:215.0
                 */

                int dy = (int) (y - mDownY);
                int dx = (int) (x - mDownX);

                //分发触屏事件给父类处理
                if (dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow)) {
                    //减掉父类消耗的距离
                    dx -= consumed[0];
                    dy -= consumed[1];
                    Log.d(TAG, Arrays.toString(offsetInWindow));
                }

                offsetTopAndBottom(dy);
                offsetLeftAndRight(dx);


                break;
            }

            case MotionEvent.ACTION_UP: {
                stopNestedScroll();
                break;
            }
        }
        mLastX = x;
        mLastY = y;
        return true;
    }


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

    @Override
    public boolean isNestedScrollingEnabled() {
        return mScrollingChildHelper.isNestedScrollingEnabled();

    }

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

    @Override
    public void stopNestedScroll() {
        mScrollingChildHelper.stopNestedScroll();

    }

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

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

    /**
     * @param dx       水平滑动距离
     * @param dy       垂直滑动距离
     * @param consumed 父类消耗掉的距离
     * @return
     */
    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        return mScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

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

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


}

嵌套滑动中的父view:

public class NestParentLayout extends FrameLayout implements NestedScrollingParent {

    private static final String TAG = NestParentLayout.class.getSimpleName();
    private NestedScrollingParentHelper mScrollingParentHelper;

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

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

    public NestParentLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mScrollingParentHelper = new NestedScrollingParentHelper(this);
    }


    /*
    子类开始请求滑动
     */
    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        Log.d(TAG, "onStartNestedScroll() called with: " + "child = [" + child + "], target = [" + target + "], nestedScrollAxes = [" + nestedScrollAxes + "]");

        return true;
    }


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


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

    @Override
    public void onStopNestedScroll(View child) {
        mScrollingParentHelper.onStopNestedScroll(child);
    }

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        Log.d(TAG, "onNestedPreScroll() called with: " + "dx = [" + dx + "], dy = [" + dy + "], consumed = [" + Arrays.toString(consumed) + "]");
        final View child = target;
        if (dx > 0) {
            if (child.getRight() + dx > getWidth()) {
                dx = child.getRight() + dx - getWidth();//多出来的
                offsetLeftAndRight(dx);
                consumed[0] += dx;//父亲消耗
            }


        } else {
            if (child.getLeft() + dx < 0) {
                dx = dx + child.getLeft();
                offsetLeftAndRight(dx);
                Log.d(TAG, "dx:" + dx);
                consumed[0] += dx;//父亲消耗
            }


        }

        if (dy > 0) {
            if (child.getBottom() + dy > getHeight()) {
                dy = child.getBottom() + dy - getHeight();
                offsetTopAndBottom(dy);
                consumed[1] += dy;
            }
        } else {
            if (child.getTop() + dy < 0) {
                dy = dy + child.getTop();
                offsetTopAndBottom(dy);
                Log.d(TAG, "dy:" + dy);
                consumed[1] += dy;//父亲消耗
            }
        }


    }
}

你可能感兴趣的:(自定义控件技能——嵌套滑动机制)