简单理解Android中的滑动

说起Android中的滑动,我们第一时间肯定会想到scrollTo scrollBy然后是滑动冲突及事件分发,先简单的回顾一下Android事件分发机制及滑动冲突的处理,然后了解一下Android中的嵌套滑动。

事件分发
我们都知道,Android中的页面都是由Activity、Fragment组成的,然后他们内部的View是按照树形结构一层 层排列下来的,View里面可以包含View及ViewGroup,里面又可以接着包含View,当我们手指从屏幕上按下时,肯定是由手机硬件将事件传递给最外层的Activity,然后一层层分发到最下级View,因此父View可以对事件进行拦截处理,不让子View处理,一个很经典的例子就是公司领导安排任务,然后下发到部门经理,再到小组领导,最后到我们程序员,当然到小组领导这可以直接自己处理而不继续发给程序员。默认父View为ViewGroup,子View为View,ViewGroup中对方法处理就是:dispatchTouchEvent(分发)、onInterceptTouchEvent(拦截)、onTouchEvent(消费),而View里则没有onInterceptTouchEvent拦截事件的方法,只有dispatchTouchEvent(分发)、onTouchEvent(消费);下面对这几个方法进行简单的说明:

  1. dispatchTouchEvent
    首先调用的方法,用于处理事件的分发,ViewGroup会遍历他的子View,并挨个执行他们的dispatchTouchEvent方法,而子View又为ViewGroup时,又会继续调用dispatchTouchEvent,返回Boolean类型,当DOWN为true的时候,顺序下发会中断,此时只会走自己的dispatchTouchEvent和onTouchEvent,代表自己处理了事件,自己的onInterceptTouchEvent则不会进入; 当DOWN为false的时候,就可以分发到下一层继续分发,这里要注意一下,我们不能直接返回false,因为super.dispatchTouchEvent(ev)中做了很多其他分发事件的操作,因此如果我们要在dispatchTouchEvent中测试要记得调用super.dispatchTouchEvent(ev);子View为View时,决定是否自己处理onTouch事件,一般情况下,对于子View而言,我们不需要在View的dispatchTouchEvent方法里处理什么逻辑,因为onTouchEvent的返回值会影响dispatchTouchEvent的返回值,因此在onTouchEvent里面处理就好了。
  2. onInterceptTouchEvent
    这个是只有ViewGroup才有的方法,用于拦截事件。假如我们在一个ViewGroup的onInterceptTouchEvent方法中,将DOWN事件返回true,则代表该ViewGroup要拦截事件,自己处理,接着调用自己的onTouchEvent,其子View不会接受到事件。若我们在DOWN事件返回false,MOVE UP事件返回true,则DOWN事件还是会发送给子View,若子View不处理返回false,则父View自己处理,若子ViewDOWN返回true,则由于事件是先到父View再到子View,因此父View会先自己进行处理,然后给子View一个其他类型的MotionEvent,如ACTION_CANCEL。
  3. onTouchEvent
    我们一般都在这个方法里面,写处理滑动的逻辑。一般一次滑动事件由一个DOWN事件,一个或者多个MOVE事件及一个UP事件组成。需要注意的是,由于我们是DOWN事件开始,因此如果我们要处理滑动,必须在DOWN事件接收到的时候返回true,才代表我们要处理这个事件,接下去的MOVE、UP事件才可以收到,否则收不到。而且需要注意的是,如果我们在该View的DOWN事件返回true了,本次剩余的事件会直接交给他处理,不会再让其他View处理,好比是一锤子买卖。
    一段经典的伪代码可以很好的描述:
public boolean dispatchTouchEvent(MotionEvent ev) {
      boolean handled = false;
        if (onInterceptTouchEvent(ev)) {
            handled = onTouchEvent(ev);
        } else {
            handled = child.dispatchTouchEvent(ev)
        }
        return handled;
}

滑动冲突
简单的了解了这几个方法之后,我们就可以对事件分发体系有一个大概的认识了,有了滑动,就会有滑动冲突的产生,即父子关系为两个可滑动的View,这个时候如果我们要滑动的话是子View处理呢还是父View处理呢,不处理的话就会产生滑动冲突,最常见的莫过于ScrollView嵌套listView、RecyclerView这样,这样的场景解决起来也挺简单,直接禁用掉listView、RecyclerView的滑动就行。滑动的冲突处理方法分为两种,分别为内部处理法和外部处理法。

  1. 内部处理法(子View处理)
    比如ViewPager嵌套ViewPager,这时候就需要我们自己进行自定义处理了,逻辑就主要写在上面的三个方法中,处理滑动冲突,应该介绍一个view的方法requestParentDisallowIntercept,顾名思义,这个方法的作用就是请求父View不要拦截事件,交给子View处理,有一个Boolean类型的入参代表父View是否拦截,因为滑动事件肯定会先到父View再到子View,我们用这个方法就可以自己先处理,比如ViewPager嵌套ViewPager,我们可以在子ViewPager的里面重写onTouchEvent,并在DOWN事件中判断,如果可以滑动,我们就requestParentDisallowIntercept(true),然后自己处理滑动事件,如果子ViewPager已经不能滑动了(第一页和最后一页),就不处理事件,返回给父View处理,这只是一个简单的例子,其他的话要根据实际情况进行判断处理。
  2. 外部处理法(父View处理)
    还是刚刚那个例子,如果我们在父ViewPager处理的话,逻辑其实大同小异,只不过方法变了,在父ViewPager中处理的话,我们需要在onInterceptTouchEvent的DOWN事件中进行判断,这里我们获取子ViewPager处理要比较好一点,还是和上面一样的逻辑,如果子ViewPager 可以滑动,就返回false,然后在子ViewPager的onTouchEvent返回true,子ViewPager处理,反之则在自己的onInterceptTouchEvent返回true,自己处理。

项目中的简单应用
在最近的项目中,就遇到了这样类似的场景,完成一个日历功能可以左右滑动,挺好的,可以用ViewPager直接实现,但是由于月份的日历我并没有用recyclerView实现,而是自己自定义了一个View绘制的日期,同时,日期需要处理点击事件,因此,这就复杂了点。。一般的点击事件处理不了了,于是,我在日历的View里面重写了onTouchEvent,根据按下的坐标判断处理点击事件,但是因为ViewPager可以滑动,而View又需要处理onTouchEvent,因此我在滑动的过程中就会触发View的touch事件,导致了一种不好的体验,这个时候就需要手动处理滑动事件了,解决方法也是挺简单,按照上面的两种方法就可以完成,简单的描述一下,外部拦截:ViewPager处理,若横向滑动距离超过某个值,比如8px,则视为滑动,此时ViewPager处理,反正则不处理。内部拦截:子View请求父View不要拦截事件,如果子View横向滑动小于8px,则视为点击,此时自己处理,反之,父View处理。

嵌套滑动
在刚开始接触嵌套滑动的时候,看别人的博客简直一脸懵逼,完全不知道讲的什么鬼,当然这里不是说别人的博客不够好,只不过是自己之前经验太少,没能看懂,一看NestedScrollingChild、NestedScrollingParent、NestedScrollingChildHelper、
NestedScrollingParentHelper四个类,然后继承要实现一大堆方法,方法里面一大堆参数,然后helper里面一大堆方法,同样一大堆参数,当时就心里就发毛了,真鸡儿复杂,博客基本看一点就放弃了,也没有仔细去看,学习就是这样,当你遇到了难点越看越看不下去的时候,不如试着换个方式,先停在这,等过几天再看,等几天过去了你就会发现,这个事已经完全忘记了。。。这几天抽空仔细看了下,才慢慢发现了他们的用法,也不是太复杂嘛,只是一开始看的时候有点吓人而已。下面来简单介绍一下嵌套滑动。
上面写的滑动机制,就等于一锤子买卖,若View处理了DOWN事件,则接下来的MOVE UP事件都由他处理,不能再分发给其他的View了,而嵌套滑动则有点不太一样,他的主要逻辑是这样的:
前面说过,滑动是首先到父View再到子View,嵌套滑动则等于又对父View加了一个更严格的判断条件,假定父View设置为移动5px则自己处理,则子View的滑动则判断为小于5px,这样,滑动的处理就先到子View上了,子View在滑动之前会先问父View要不要滑动,之后子View进行处理,如果子View滑动距离没有消费完,还可以继续问父View要不要滑动,然后再自己处理,这样就行成了我们的嵌套滚动的过程。(至于源码的实现,我还暂时没有看,担心一看又几天过去了)比如常见的顶部悬浮功能,首先是父View滑动,滑动到要悬浮的View到达页面顶部的时候,此时父View停止滑动,子View继续滑动,从而实现了悬浮的功能。下面我们对几个主要的方法进行说明:
3. NestedScrollingChild
该方法是子View需要实现的,并重写多个方法

//开始嵌套滚动
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);

public boolean startNestedScroll(int axes);
执行一些嵌套滚动准备前的工作,当找到了执行嵌套滚动的父View时,返回true
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
这个是子View滑动之前的方法,传入dx、dy,consumed为父View消费的滑动距离,offsetInWindow为父view的偏移量,之前说过,子View滑动之前可以先让父View处理,然后自己再处理,我们传入dx、dy滑动的距离,父View消费的话会把消费的距离放入consumed里面传回来以及offsetInWindow,如果父View消费了(部分或全部)则返回true
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
这个是子View滑动之后,继续询问父View要不要消费,dxConsumed,dyConsumed为已经消费的距离,dxUnconsumed、dyUnconsumed为剩下的距离,这四个参数我们传入,offsetInWindow为父view的偏移量,父View返回。返回值true就是事件被父View消费。
public boolean dispatchNestedPreFling(float velocityX, float velocityY);
下面两个就是惯性滑动,和之前两个类似,分别为x、y的加速度,返回值true代表父View消费。
public void stopNestedScroll();
停止嵌套滑动
4. NestedScrollingParent
该方法是嵌套滚动的父View需要实现的接口,方法如下:

//当开启、停止嵌套滚动时被调用
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);

由于子View会先询问父View,因此有如下对应关系:
子startNestedScroll返回true才会调用后面的父
从上图可以看出,滑动之前child的startNestedScroll会先被调用,寻找有没有匹配的父parent,此时父View的onStartNestedScroll会被调用,返回true的话,onNestedScrollAccepted接着会被调用;然后滑动即将开始的时候,child调用dispatchNestedPreScroll询问父View是否要进行滑动,调用parent的onNestedPreScroll,我们可以在这个方法里处理相应的逻辑,之后,child再调用自己的dispatchNestedScroll进行滑动,结束的时候,child会停止滑动,然后parent也停止。大体的滑动流程就是这样,虽然看起来挺多方法,仔细想想的话,还是可以理解的,下面,我们用一个小Demo演示一下嵌套滑动,简单的实现一个View悬浮的效果。
这是我们自定义的布局文件:

<?xml version="1.0" encoding="utf-8"?>
<com.civaonline.mvvmdemo.widget.nestedScroll.MyNestedScrollParent
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".ui.activity.NestedScrollActivity">

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

    <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/colorAccent"
            android:textColor="@color/white"
            android:padding="16dp"
            android:text="这是需要悬浮的View"/>

    <com.civaonline.mvvmdemo.widget.nestedScroll.MyNestedScrollChild
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
        <TextView
                android:id="@+id/tv"
                android:text="123\n456\n789\n111\n222\n333\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"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"/>
    </com.civaonline.mvvmdemo.widget.nestedScroll.MyNestedScrollChild>

</com.civaonline.mvvmdemo.widget.nestedScroll.MyNestedScrollParent>

对应的UI显示如下,在这里插入图片描述
简单起见,MyNestedScrollParent继承LinearLayout并实现NestedScrollingParent接口:

class MyNestedScrollParent @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr), NestedScrollingParent {

    private val TAG = "MyNestedScrollParent"
    private var mNestedScrollingParentHelper: NestedScrollingParentHelper = NestedScrollingParentHelper(this)
    private lateinit var mNestedScrollChild: MyNestedScrollChild
    private var imageHeight = 0

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        super.onLayout(changed, l, t, r, b)
        imageHeight = getChildAt(0).height
        Log.e(TAG,"onLayout图片的高度-> $imageHeight")
    }

    override fun getNestedScrollAxes(): Int {
        return mNestedScrollingParentHelper.nestedScrollAxes
    }

    /**
     * 开始嵌套滑动,返回true代表自己消费了 此时调用onNestedScrollAccepted
     * 在此判断target是哪一个子View及滚动的方向
     *  child滑动的子View
     *  比如这里只判断子View是MyNestedScrollChild,当然也可以加上滑动的方向axes一起判断
     */
    override fun onStartNestedScroll(child: View, target: View, axes: Int): Boolean {
        return target is MyNestedScrollChild
    }

    /**
     * 停止嵌套滚动
     */
    override fun onStopNestedScroll(target: View) {
        mNestedScrollingParentHelper.onStopNestedScroll(target)
    }

    /**
     * 开始滚动
     */
    override fun onNestedScrollAccepted(child: View, target: View, axes: Int) {
        mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes)
    }

    /**
     * 优先于Child滚动
     * 获取滚动的距离,根据情况决定是否需要自己滚动,最后将滚动距离存在consumed中
     * dx dy 接受到的子View传过来的距离 consumed自己消费的距离
     */
    override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray) {
        Log.e(TAG,"onNestedPreScroll 子View传递过来的-> $dx,$dy")
        val childScrollY = target.scrollY
        //这里做个假设 如果需要展示图片的话就自己消费
        if(dy<0 && scrollY<imageHeight){
            //向上滑动,距离最大是图片的高度的话就自己消费 超过图片的高度就让子View自己消费
            scrollBy(0,-dy)
            //告诉child消费了多少
            consumed[1] = dy
        }else if (dy>0){
            if (childScrollY==0 && scrollY>0){
                //向下滑动,子View回到原位 (距离最大为图片高度)
                scrollBy(0,-dy)
                consumed[1] = dy
            }
        }
        Log.e(TAG,"自己滚动了多少-> $childScrollY,子View滚动了多少-> $childScrollY")

    }

    //在child滑动之后
    override fun onNestedScroll(target: View, dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int) {
        Log.e(TAG,"onNestedScroll 子View传递过来的-> dxConsumed:$dxConsumed,dyConsumed:$dyConsumed," +
                "dxUnconsumed:$dxUnconsumed,dyUnconsumed:$dyUnconsumed")
        super.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed)
    }

    //返回值,是否消费了惯性滑动
    override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean {
        return false
    }

    //返回值,是否消费了惯性滑动
    override fun onNestedFling(target: View, velocityX: Float, velocityY: Float, consumed: Boolean): Boolean {
        return false
    }

    //重写该方法可以限制scroll值
    override fun scrollTo(x: Int, y: Int) {
        Log.e(TAG,"scrollTo-> x:$x,y:$y")
        val maxY = imageHeight
        var moveY = if (y>maxY){
            maxY
        }else {
            y
        }
        if (y<0){
            moveY = 0
        }
        super.scrollTo(x, moveY)
    }
}

注释写了挺多,简单的讲一下逻辑,我们先实例化一个NestedScrollingParentHelper ,然后在onLayout里面获取图片的高度,即为parent要上滑隐藏或者下滑显示的距离,再通过使用NestedScrollingParentHelper 里面的几个方法来重写我们自己的方法,他会自动帮我们进行一些判断处理。我们自己实现的方法如下:
onStartNestedScroll(child,target,axes)
这个方法里面我们仅仅判断了target的类型是不是MyNestedScrollChild,如果是的话,就返回true,代表可以支持嵌套滚动,child是包含target的直接子View
onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray)
我们主要在这里实现判断图片的逻辑操作:

  1. 上滑,而且距离没有超过图片的高度,就自己消费
  2. 下滑,当子View回到原位时,此时再移动距离最大为图片的高度,此时自己消费
    然后我们需要把自己消费的数据放在consumed数组中,长度为2,分别为宽 高 滑动的距离,由于我们这个滑动比较简单,因此就在onNestedScroll这个方法child滑动之后,直接返回false,代表不需要自己处理了,简单起见,onNestedPreFling 及onNestedFling这两个惯性滑动方法也直接返回false,代表不支持惯性嵌套滑动,重写scrollTo方法是为了限制滑动最大距离,不然会有显示的小bug。
    接着在MyNestedScrollChild中:
class MyNestedScrollChild @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr),NestedScrollingChild {

    private val TAG = "MyNestedScrollChild"

    private var showHeight = 0

    private var mNestedScrollingChildHelper:NestedScrollingChildHelper = NestedScrollingChildHelper(this).apply {
        //支持嵌套滑动
        isNestedScrollingEnabled = true
    }

    //设置是否支持嵌套滑动
    override fun setNestedScrollingEnabled(enabled: Boolean) {
        mNestedScrollingChildHelper.isNestedScrollingEnabled = enabled
    }

    //获取嵌套滑动是否可用
    override fun isNestedScrollingEnabled(): Boolean {
        return mNestedScrollingChildHelper.isNestedScrollingEnabled
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        showHeight = measuredHeight
        Log.e(TAG,"showHeight-> $showHeight")
        super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0,MeasureSpec.UNSPECIFIED))
    }

    private var downY = 0f
    private var moveY = 0f
    //消费
    private var consumed = IntArray(2)
    //偏移
    private var offset = IntArray(2)

    override fun onTouchEvent(event: MotionEvent): Boolean {
        when(event.action){
            MotionEvent.ACTION_DOWN->{
                downY = event.rawY
                Log.e(TAG,"DOWN-> $downY")
            }
            //判断如果父View消费了,则自己消费剩下的,若父View没消费,则自己滑动全部
            MotionEvent.ACTION_MOVE->{
                moveY = event.rawY
                Log.e(TAG,"MOVE-> $moveY")
                val dy = (moveY - downY).toInt()
                downY = moveY
                Log.e(TAG,"DELTA-> $dy")
                //若 找到了父View(竖直轴滚动) && 父View滑动了
                if (startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL) &&
                    dispatchNestedPreScroll(0, dy, consumed, offset)){
                    Log.e(TAG,"父View消费的距离Y-> ${consumed[1]}")
                    //获取剩下的未滑动距离,自己消费
                    val remain = (dy - consumed[1])
                    if (remain!=0){
                        scrollBy(0,-remain)
                    }
                }else{
                    //直接自己消费
                    scrollBy(0,-dy)
                }
            }
            MotionEvent.ACTION_UP->{

            }
        }
        return true
    }


    override fun scrollTo(x: Int, y: Int) {
        //最大移动的距离 就是这个View的底部刚好显示在最底部
//        val maxY = measuredHeight - showHeight - showHeight
//        Log.e(TAG,"scrollTo->测量高度:$measuredHeight,展示高度:$showHeight,x:$x,y:$y")
//        var moveY = if (y>maxY){
//            maxY
//        }else {
//            y
//        }
        val moveY = if (y<0){
            //不能往下移动了
            0
        }else{
            y
        }
        super.scrollTo(x, moveY)
    }

    /**
     * 开始嵌套滑动 找到了嵌套滑动的父View时,返回true
     */
    override fun startNestedScroll(axes: Int): Boolean {
        return mNestedScrollingChildHelper.startNestedScroll(axes)
    }

    /**
     * 停止嵌套滑动
     */
    override fun stopNestedScroll() {
       mNestedScrollingChildHelper.stopNestedScroll()
    }

    //返回是否有一个嵌套的父View
    override fun hasNestedScrollingParent(): Boolean {
        return mNestedScrollingChildHelper.hasNestedScrollingParent()
    }

    /**
     * 在嵌套滑动之前先分发,询问父View是否要滑动
     * dx dy 此次滑动的距离
     * consumed 父View消费滑动的距离 父View传进来,我们只需要用一个数组接收
     * offsetInWindow 父View的偏移量 父View传进来,我们只需要用一个数组接收
     * 返回 若parent消费了滑动,则返回true
     */
    override fun dispatchNestedPreScroll(dx: Int, dy: Int, consumed: IntArray?, offsetInWindow: IntArray?): Boolean {
        return mNestedScrollingChildHelper.dispatchNestedPreScroll(dx,dy,consumed,offsetInWindow)
    }

    /**
     *子View自己进行滑动之后调用 询问父View是否还要进行剩下dyUnconsumed的滑动
     *dxConsumed dyConsumed已经消费的距离 dxUnconsumed dyUnconsumed尚未消费的距离
     * offsetInWindow 父View的偏移量
     * 返回值 true 代表事件被分发 false代表未被分发
     */
    override fun dispatchNestedScroll(
        dxConsumed: Int,
        dyConsumed: Int,
        dxUnconsumed: Int,
        dyUnconsumed: Int,
        offsetInWindow: IntArray?
    ): Boolean {
        return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow)
    }

    //同理,在惯性滑动之前询问父View 返回true代表父View消费了
    override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float): Boolean {
        return mNestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY)
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        mNestedScrollingChildHelper.onDetachedFromWindow()
    }

    /**
     * 分发惯性滑动
     * x y 代表速度 consumed 若子view消费滑动则返回true
     * 返回 若父view消费过或者对该动作有反应
     */
    override fun dispatchNestedFling(velocityX: Float, velocityY: Float, consumed: Boolean): Boolean {
        return mNestedScrollingChildHelper.dispatchNestedFling(velocityX,velocityY,consumed)
    }

}

可以看到,我们实现了NestedScrollingChild接口,并重写了一大堆方法,然而大部分方法不需要我们处理,只需要用NestedScrollingChildHelper去处理这些方法就行,下面主要说一下自己实现的方法:
onTouchEvent
我们在touch事件中记录了按下的位置,在move的时候计算移动的距离,如果找到了嵌套的parent而且parent消费了滑动事件,则我们取剩下未消费的距离自己处理,不然的话就直接自己处理,同样重写了scrollTo方法限制滑动的最大距离

//若 找到了父View(水平轴滚动) && 父View滑动了
                if (startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL) &&
                    dispatchNestedPreScroll(0, dy, consumed, offset)){
                    Log.e(TAG,"父View消费的距离Y-> ${consumed[1]}")
                    //获取剩下的未滑动距离,自己消费
                    val remain = (dy - consumed[1])
                    if (remain!=0){
                        scrollBy(0,-remain)
                    }
                }else{
                    //直接自己消费
                    scrollBy(0,-dy)
                }

可以看到,我们实际上自己处理的uoji并不多,实现的方法倒是挺多的,最终俩个helper类帮我们处理了绝大部分逻辑,忘了说一下,我们在实例化两个Helper类时,必须设置支持嵌套滑动,//支持嵌套滑动 isNestedScrollingEnabled = true
只要知道了他们的几个方法及流程,写代码还是比较好写的,当然,这里只是一个简单的应用,其他的可以根据实际情况自己处理。
这只是NestedScrollingChild NestedScrollingParent 的第一个版本,后面有NestedScrollingChild3 NestedScrollingParent2,然后到AndroidX里面居然有NestedScrollingChild NestedScrollingParent3.。。。说明了里面还是有小bug的,或者说优化的处理,之前第一次使用CoordinatorLayout配合AppbarLayout就发现了滑动不流畅的问题,后面再多了解下其他两个版本吧。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值