通过分析源码来学习嵌套滑动机制——就是这么简单

嵌套滑动的预备知识

Scroller的用法

1.scroll方法

mScroller.startScroll(0, 0, -300, -300, 2000)
postInvalidate()//触发重绘

override fun computeScroll() {//重写computeScroll方法
    if (mScroller.computeScrollOffset()) {
        Log.d("vivi", "currX:${mScroller.currX}-----currY:${mScroller.currY}")
        scrollTo(-mScroller.currX, -mScroller.currY)
        postInvalidate()
    }
}
复制代码

2.fling方法

mScroller.fling(0, 0, mVelocityTracker.xVelocity.toInt(), mVelocityTracker.yVelocity.toInt(), 0, width, 0, height)
postInvalidate()
复制代码

同样也需要重写computeScroll方法,fling方法需要传xy方向的速度单位是像素/秒,获取速度就需要用到VelocityTracker

VelocityTracker的用法
private val mVelocityTracker: VelocityTracker = VelocityTracker.obtain()//获取速度跟踪器

override fun onTouchEvent(event: MotionEvent?): Boolean {
    event?.also {
        mVelocityTracker.addMovement(event)//每次处理触摸事件时,我们将触摸事件通过addMovement方法传递给它
        when (it.action) {
            MotionEvent.ACTION_DOWN -> {
            }
            MotionEvent.ACTION_MOVE -> {
            }
            MotionEvent.ACTION_UP -> 
                mVelocityTracker.computeCurrentVelocity(1000)//up的时候计算此时的速度,参数为单位(此处返回的是以1000毫秒为单位的像素),并通过get方法获取xy方向的速度
                mScroller.fling(0, 0, mVelocityTracker.xVelocity.toInt(), mVelocityTracker.yVelocity.toInt(), 0, width, 0, height)
                postInvalidate()
            }
        }
    }
    return true
}
复制代码

同样也需要重写computeScroll方法

嵌套滑动的相关总结
  • 嵌套滑动机制的由来

    大家都知道touch事情的分发机制是一锤子买卖,要么不消费,要么把整个事件消费掉,如果需要处理嵌套滑动,那么就需要引入嵌套滑动

  • 嵌套滑动机制相关接口与工具 NestedScrollingParent、NestedScrollingParentHelper、NestedScrollingChild、NestedScrollingChildHelper NestedScrollingParent、NestedScrollingChild是两个接口,需要支持嵌套滑动的父控件需要实现NestedScrollingParent,需要支持嵌套滑动的子控件需要实现NestedScrollingChild,NestedScrollingParentHelper、NestedScrollingChildHelper是两个帮助类,封装嵌套滑动机制的一些实现,我们只需要实现嵌套滑动接口接口,将接口定义的api用帮助进行代理,基本就可以实现嵌套滑动事件的传递了(具体参考下面的示例代码)

  • 相关接口的调用执行流程

    NestedChildView----setNestedScrollingEnabled

    NestedChildView----startNestedScroll

    NestedParentLayout---onStartNestedScroll

    NestedParentLayout---onNestedScrollAccepted

    NestedChildView----dispatchNestedPreScroll (分发嵌套滑动前的事件,由child调用,会触发onNestedPreScroll)

    NestedParentLayout---onNestedPreScroll (当嵌套滑动前)

    NestedChildView----dispatchNestedScroll (分发嵌套滑动后的事件,由child调用,会触发onNestedScroll)

    NestedParentLayout---onNestedScroll (当嵌套滑动后)

    NestedChildView----dispatchNestedPreFling (分发Fling前的事件,由child调用,会触发onNestedPreFling)

    NestedParentLayout---onNestedPreFling (当Fling前)

    NestedChildView----dispatchNestedFling (分发Fling后的事件,由child调用,会触发onNestedFling)

    NestedParentLayout---onNestedFling (当Fling滑动后)

    NestedChildView----stopNestedScroll

    NestedParentLayout---onStopNestedScroll

图片源自网络,如有侵权,请联系删之

child端示例代码

package com.hotniao.project.nestedscrolldemo.view

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.widget.Scroller

/**
 * 项目名称:NestedScrollDemo
 * 类描述:
 * 创建人:kevinxie
 *
 */
class NestedChild(context: Context?, attrs: AttributeSet?) : View(context, attrs), NestedScrollingChild {

    private val mHelper: NestedScrollingChildHelper = NestedScrollingChildHelper(this)
    private val mVelocityTracker: VelocityTracker = VelocityTracker.obtain()
    private val mScroller = Scroller(context)

    private var mLastX: Float = 0.toFloat()//手指在屏幕上最后的x位置
    private var mLastY: Float = 0.toFloat()//手指在屏幕上最后的y位置

    private var mDownX: Float = 0.toFloat()//手指第一次落下时的x位置(忽略)
    private var mDownY: Float = 0.toFloat()//手指第一次落下时的y位置

    private val consumed = IntArray(2)//消耗的距离
    private val offsetInWindow = IntArray(2)//窗口偏移

    init {
        isNestedScrollingEnabled = true
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        event?.also {
            val x = it.x
            val y = it.y
            when (it.action) {
                MotionEvent.ACTION_DOWN -> {
                    mDownX = x
                    mDownY = y
                    mLastX = x
                    mLastY = y
                    startNestedScroll(ViewCompat.SCROLL_AXIS_HORIZONTAL or ViewCompat.SCROLL_AXIS_VERTICAL)
                }
                MotionEvent.ACTION_MOVE -> {
                    val dx = x - mDownX
                    val dy = y - mDownY
                    var consumedDx = 0f
                    var consumedDy = 0f
                    if (dispatchNestedPreScroll(dx.toInt(), dy.toInt(), consumed, offsetInWindow)) {
                        consumedDx = dx - consumed[0]
                        consumedDy = dy - consumed[1]
                        offsetLeftAndRight(consumedDx.toInt())
                        offsetTopAndBottom(consumedDy.toInt())
                    } else {
                        consumedDx = dx
                        consumedDy = dy
                        offsetLeftAndRight(dx.toInt())
                        offsetTopAndBottom(dy.toInt())
                    }
                    if (dispatchNestedScroll(consumedDx.toInt(), consumedDy.toInt(), (dx - consumedDx).toInt(), (dy - consumedDy).toInt(), offsetInWindow)) {

                    }
                }
                MotionEvent.ACTION_UP -> {
                    mVelocityTracker.computeCurrentVelocity(1000)
                    if (dispatchNestedPreFling(mVelocityTracker.xVelocity, mVelocityTracker.yVelocity)) {
                    }
                    mScroller.fling(scrollX, scrollY, mVelocityTracker.xVelocity.toInt(), mVelocityTracker.yVelocity.toInt(), -1000, 1000, -1000, 1000)
                    if (dispatchNestedFling(mVelocityTracker.xVelocity, mVelocityTracker.yVelocity, true)) {
                    }
                    postInvalidate()
                }
            }
            mLastX = x
            mLastY = y
            mVelocityTracker.addMovement(event)
        }
        return true
    }

    override fun dispatchNestedScroll(dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, offsetInWindow: IntArray?): Boolean {
        Log.d("vivi", "NestedChild---dispatchNestedScroll-----dxConsumed:$dxConsumed----dyConsumed:$dyConsumed----dxUnconsumed:$dxUnconsumed----dyUnconsumed:$dyUnconsumed")
        return mHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow)
    }

    override fun isNestedScrollingEnabled(): Boolean {
        Log.d("vivi", "NestedChild---isNestedScrollingEnabled-----")
        return mHelper.isNestedScrollingEnabled
    }

    override fun dispatchNestedPreScroll(dx: Int, dy: Int, consumed: IntArray?, offsetInWindow: IntArray?): Boolean {
        Log.d("vivi", "NestedChild---dispatchNestedPreScroll-----dx:$dx---dy:$dy----consumed[0]:${consumed?.get(0)}----consumed[1]:${consumed?.get(1)}")
        return mHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow)
    }

    override fun stopNestedScroll() {
        Log.d("vivi", "NestedChild---stopNestedScroll-----")
        mHelper.stopNestedScroll()
    }

    override fun hasNestedScrollingParent(): Boolean {
        Log.d("vivi", "NestedChild---hasNestedScrollingParent-----")
        return mHelper.hasNestedScrollingParent()
    }

    override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float): Boolean {
        Log.d("vivi", "NestedChild---dispatchNestedPreFling-----")
        return mHelper.dispatchNestedPreFling(velocityX, velocityY)
    }

    override fun setNestedScrollingEnabled(enabled: Boolean) {
        Log.d("vivi", "NestedChild---setNestedScrollingEnabled-----")
        mHelper.isNestedScrollingEnabled = enabled
    }

    override fun dispatchNestedFling(velocityX: Float, velocityY: Float, consumed: Boolean): Boolean {
        Log.d("vivi", "NestedChild---dispatchNestedFling-----")
        return mHelper.dispatchNestedFling(velocityX, velocityY, consumed)
    }

    override fun startNestedScroll(axes: Int): Boolean {
        Log.d("vivi", "NestedChild---startNestedScroll-----")
        return mHelper.startNestedScroll(axes)
    }

    private var mFlingLastX = 0
    private var mFlingLastY = 0

    override fun computeScroll() {
        if (mScroller.computeScrollOffset()) {
            Log.d("vivi", "currX:${mScroller.currX}-----currY:${mScroller.currY}")
            offsetLeftAndRight(mScroller.currX - mFlingLastX)
            offsetTopAndBottom(mScroller.currY - mFlingLastY)
            //scrollTo(mScroller.currX, mScroller.currY)
            mFlingLastX = mScroller.currX
            mFlingLastY = mScroller.currY
            postInvalidate()
        } else {
            mFlingLastX = 0
            mFlingLastY = 0
            stopNestedScroll()
        }
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        mHelper.onDetachedFromWindow()
    }
}
复制代码

parent端示例代码

package com.hotniao.project.nestedscrolldemo.view

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.View
import android.widget.FrameLayout

/**
 * 创建人:kevinxie
 */
class NestedParent(context: Context?, attrs: AttributeSet?) : FrameLayout(context, attrs), NestedScrollingParent {
    private val mHelper: NestedScrollingParentHelper = NestedScrollingParentHelper(this)

    override fun onNestedScrollAccepted(child: View, target: View, axes: Int) {
        Log.d("vivi", "NestedParent---onNestedScrollAccepted-----")
    }

    override fun onStartNestedScroll(child: View, target: View, axes: Int): Boolean {
        Log.d("vivi", "NestedParent---onStartNestedScroll-----")
        return true
    }

    override fun onNestedFling(target: View, velocityX: Float, velocityY: Float, consumed: Boolean): Boolean {
        Log.d("vivi", "NestedParent---onNestedFling-----velocityX:$velocityX------velocityY:$velocityY----consumed:$consumed")
        return true
    }

    override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean {
        Log.d("vivi", "NestedParent---onNestedPreFling-----velocityX:$velocityX------velocityY:$velocityY")
        return true
    }

    override fun getNestedScrollAxes(): Int {
        Log.d("vivi", "NestedParent---getNestedScrollAxes-----")
        return mHelper.nestedScrollAxes
    }

    override fun onNestedPreScroll(target: View, dxs: Int, dys: Int, consumed: IntArray) {
        Log.d("vivi", "NestedParent---onNestedPreScroll-----dx:$dxs---dy:$dys----consumed[0]:${consumed[0]}----consumed[1]:${consumed[1]}")
        var dx = dxs
        var dy = dys
        if (dx > 0) {
            if (target.right + dx > width) {
                dx = target.right + dx - width//多出来的
                offsetLeftAndRight(dx)
                consumed[0] += dx//父亲消耗
            }

        } else {
            if (target.left + dx < 0) {
                dx += target.left
                offsetLeftAndRight(dx)
                consumed[0] += dx//父亲消耗
            }

        }

        if (dy > 0) {
            if (target.bottom + dy > height) {
                dy = target.bottom + dy - height
                offsetTopAndBottom(dy)
                consumed[1] += dy
            }
        } else {
            if (target.top + dy < 0) {
                dy += target.top
                offsetTopAndBottom(dy)
                consumed[1] += dy//父亲消耗
            }
        }
    }

    override fun onNestedScroll(target: View, dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int) {
        Log.d("vivi", "NestedParent---onNestedScroll-----dxConsumed:$dxConsumed----dyConsumed:$dyConsumed----dxUnconsumed:$dxUnconsumed----dyUnconsumed:$dyUnconsumed")
    }

    override fun onStopNestedScroll(target: View) {
        Log.d("vivi", "NestedParent---onStopNestedScroll-----")
    }
}
复制代码

嵌套滑动的原理分析

按照上面的相关api的执行流程,以及示例代码,我们来一起阅读源码,首先我们需要在child调用setNestedScrollingEnabled方法,来设置是否支持嵌套滑动,我们一起看看helper里面的实现

public void setNestedScrollingEnabled(boolean enabled) {
    if (mIsNestedScrollingEnabled) {
        ViewCompat.stopNestedScroll(mView);
    }
    mIsNestedScrollingEnabled = enabled;
}
复制代码

前面有个判断,如果嵌套滑动已经可用,会先停止嵌套滑动,最后跟进去

public void stopNestedScroll(View view) {
    if (view instanceof NestedScrollingChild) {
        ((NestedScrollingChild) view).stopNestedScroll();
    }
}
复制代码

其实就是调用了child的stopNestedScroll方法,那我们接着再看startNestedScroll方法

public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
    if (hasNestedScrollingParent(type)) {
        // Already in progress
        return true;
    }
    if (isNestedScrollingEnabled()) {
        ViewParent p = mView.getParent();
        View child = mView;
        while (p != null) {
            if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
                setNestedScrollingParentForType(type, p);
                ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
                return true;
            }
            if (p instanceof View) {
                child = (View) p;
            }
            p = p.getParent();
        }
    }
    return false;
}
复制代码

先看hasNestedScrollingParent方法,直接调用了getNestedScrollingParentForType方法,继续跟

public boolean hasNestedScrollingParent(@NestedScrollType int type) {
    return getNestedScrollingParentForType(type) != null;
}

private ViewParent getNestedScrollingParentForType(@NestedScrollType int type) {
    switch (type) {
        case TYPE_TOUCH:
            return mNestedScrollingParentTouch;
        case TYPE_NON_TOUCH:
            return mNestedScrollingParentNonTouch;
    }
    return null;
}
复制代码

很显然就是看有没有嵌套滑动的父控件,如有就直接返回true,表示有嵌套滑动的父控件来处理此次嵌套滑动,反之我们继续startNestedScroll,调用isNestedScrollingEnabled,如果不支持嵌套滑动就返回false,但是我们一开始就设置过支持嵌套滑动,可以看到进行遍历view树来寻找是否有嵌套滑动的父控件,ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)里面进行了判断是否是NestedScrollingParent,然后直接调用其onStartNestedScroll方法,回到startNestedScroll,如果找到嵌套滑动的父控件,就调用了setNestedScrollingParentForType方法,跟getNestedScrollingParentForType对应,避免了每次都去遍历整个树,然后又继续调用了父控件的onNestedScrollAccepted方法,看来前几步跟我们打印的流程完全一致,开始嵌套滑动,父控件也收到了嵌套滑动的开始了,那我们就需要通过子控件去分发自己的滑动事件了,先看dispatchNestedPreScroll

public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
        @Nullable int[] offsetInWindow, @NestedScrollType int type) {
    if (isNestedScrollingEnabled()) {
        final ViewParent parent = getNestedScrollingParentForType(type);
        if (parent == null) {
            return false;
        }

        if (dx != 0 || dy != 0) {
            int startX = 0;
            int startY = 0;
            if (offsetInWindow != null) {
                mView.getLocationInWindow(offsetInWindow);
                startX = offsetInWindow[0];
                startY = offsetInWindow[1];
            }

            if (consumed == null) {
                if (mTempNestedScrollConsumed == null) {
                    mTempNestedScrollConsumed = new int[2];
                }
                consumed = mTempNestedScrollConsumed;
            }
            consumed[0] = 0;
            consumed[1] = 0;
            ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);

            if (offsetInWindow != null) {
                mView.getLocationInWindow(offsetInWindow);
                offsetInWindow[0] -= startX;
                offsetInWindow[1] -= startY;
            }
            return consumed[0] != 0 || consumed[1] != 0;
        } else if (offsetInWindow != null) {
            offsetInWindow[0] = 0;
            offsetInWindow[1] = 0;
        }
    }
    return false;
}
复制代码

代码稍微有点多,其实也就是做了一些健壮性的判断,最后还是调用了ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type),里面还是一样,调用了父控件的onNestedPreScroll,再继续看onNestedScroll

public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
        int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
        @NestedScrollType int type) {
    if (isNestedScrollingEnabled()) {
        final ViewParent parent = getNestedScrollingParentForType(type);
        if (parent == null) {
            return false;
        }

        if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
            int startX = 0;
            int startY = 0;
            if (offsetInWindow != null) {
                mView.getLocationInWindow(offsetInWindow);
                startX = offsetInWindow[0];
                startY = offsetInWindow[1];
            }

            ViewParentCompat.onNestedScroll(parent, mView, dxConsumed,
                    dyConsumed, dxUnconsumed, dyUnconsumed, type);

            if (offsetInWindow != null) {
                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;
}
复制代码

跟dispatchNestedPreScroll代码相似,就是调用了ViewParentCompat.onNestedScroll(parent, mView, dxConsumed,dyConsumed, dxUnconsumed, dyUnconsumed, type),从而调用了父控件的onNestedScroll,那dispatchNestedPreFling和dispatchNestedFling应该也类似吧,那我们继续看看dispatchNestedPreFling

public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
    if (isNestedScrollingEnabled()) {
        ViewParent parent = getNestedScrollingParentForType(TYPE_TOUCH);
        if (parent != null) {
            return ViewParentCompat.onNestedPreFling(parent, mView, velocityX,
                    velocityY);
        }
    }
    return false;
}
复制代码

猜的没错,也还是调用ViewParentCompat.onNestedPreFling(parent, mView, velocityX,velocityY),其实dispatchNestedFling也是如此,也就是将一些事件进行传递给父控件,最后我们来看看

public void stopNestedScroll(@NestedScrollType int type) {
    ViewParent parent = getNestedScrollingParentForType(type);
    if (parent != null) {
        ViewParentCompat.onStopNestedScroll(parent, mView, type);
        setNestedScrollingParentForType(type, null);
    }
}
复制代码

很简单,就是调用了onStopNestedScroll,后面还将之前设置的parent进行置空,整个嵌套滑动机制也就是这样一个流程,总结下来感觉就是child通过接口把自己的滑动事件发送给父控件,根据父控件的消费的事件来做相应的处理。恩,如果我们把事件发送出去,通过其他的流程应该也可以仿写出其他的滑动机制...

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值