从实战开始的NestScrolling学习

1.开篇

网上相关的NestScrolling介绍有很多,而NestScrolling本身也是很简单的,说白了就是几个接口,里面定义了很多方法,而这些方法需要我们自己传递和处理。
我们平常能使用NestScrollViewReyclerView等进行嵌套滑动,这些都是官方已经帮我们把接口处理完毕了

本篇打算自己实现接口完成简单的Demo练习,而不借助NestScrollView等已经实现接口的View的继承,用最基本的几大布局LinearLayoutFrameLayout等来实现如下的效果

本篇涉及的点包含NestScrollParent,NestScrollChild以及自定义Behavior的机制
在这里插入图片描述

2.Behavior

上面看起来很复杂,其实说白了也就是手势的处理和分发了,手势分发我们都特别属性,那么嵌套滑动的核心就是通过NestScrolling相关的接口了,如果要使用behavior那么就会大大减少操作的成本,因为behavior一般会处理依赖关系,如果一个view的操作以及确定,而其他view依赖于这个view的变化,那么只要实时监控view的变化,根据变动处理即可,这块通常会在ViewTreeObserver.OnPreDrawListener中进行监听,这是绘制前的监听,方法走到这里一般测量和布局已经完成了

比如上面gif中的顶层下滑view和缩放的滑块都是通过behavior依赖关系实现的

同时我们在LayoutInflate的源码中可以知道

void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
        ...... 
         final View view = createViewFromTag(parent, name, context, attrs);
         final ViewGroup viewGroup = (ViewGroup) parent;
         final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
         rInflateChildren(parser, view, attrs, true);
         viewGroup.addView(view, params);
        ......
    }

解析布局的时候,会把子view添加LayoutParmas,而这个参数是通过generateLayoutParams完成的,这里传入了attrs,也就是定义view的键值对属性,比如layout_width="match_parent"就是一个默认命名空间的属性对,那么我们就可以自定义属性在这里进行拓展,实际上google也是这么做的

比如我们定义IBehavior

interface IBehavior<T : View> {
    fun dependsOn(parent: View, selfView: T, denpendView: View): Boolean
    fun onDependViewChanged(parent: View, selfView: T, dependView: View): Boolean
    fun fliping(target: T, velocityY: Float): Boolean
    fun canScrollDown(target: T): Boolean
    fun tryScrollTo(target: T, delx: Int, delY: Int)
}

我在这里定义了常用的dependsOn,以及变化的onDependViewChanged方法 后面这几个是我这里后面要用到的拓展方法
然后定义一个attr属性,里面指定字符串,其实就是具体实现类的全类名

  <declare-styleable name="My_Nest_Parent">
        <attr name="myBehavior" format="string|reference"/>
    </declare-styleable>

然后在我们后面要定义的NestParent内中添加代码

class MNestParent @JvmOverloads constructor(
    context: Context,
    attributeSet: AttributeSet? = null,
    intStyle: Int = 0
) : FrameLayout(context, attributeSet, intStyle), NestedScrollingParent2 {

 inner class MLayoutParams : FrameLayout.LayoutParams {
        constructor(width: Int, height: Int) : super(width, height)
        constructor(c: Context, attrs: AttributeSet?) : super(c, attrs)
        var behavior: IBehavior<View>? = null
    }

 override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
        val parentParams: MLayoutParams = MLayoutParams(context, attrs)
        attrs?.apply {
            val types = context.obtainStyledAttributes(attrs, R.styleable.My_Nest_Parent)
            val behavior = types.getString(R.styleable.My_Nest_Parent_myBehavior)
            parentParams.behavior = behavior?.run {
                parseBehavior(this)
            }
        }
        return parentParams
    }

    fun parseBehavior(clazzName: String): IBehavior<View>? {
        try {
            val constructor = Class.forName(clazzName).getDeclaredConstructor()
            constructor?.apply {
                isAccessible = true
                return constructor.newInstance() as IBehavior<View>
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
        return null
    }
    }

代码也很简单,我自定义了一个MLayoutParams,里面添加了一个IBehavior的属性,而这个属性是通过attr获取,然后把全类名反射为一个Behavior的实例
那么我们在xml就可以这么使用

 <TextView
            android:layout_width="match_parent"
            android:background="#0af"
            android:text="最上面的"
            android:gravity="center"
            android:textColor="#fff"
            app:myBehavior="com.view.zero.learn.views.nestscroll.MBehaviorImp3"
            android:layout_height="30dp"/>

是不是有种熟悉的感觉,MBehaviorImp3就是我定义的实现类了

class MBehaviorImp3() : IBehavior<TextView> {
    override fun dependsOn(parent: View, selfView: TextView, denpendView: View): Boolean {
        if (denpendView is MAppTopChild) {
            selfView.layout(0,-selfView.measuredHeight,parent.measuredWidth,0)
            return true
        }
        return false
    }

    override fun onDependViewChanged(parent: View,selfView: TextView,dependView: View): Boolean {
        val scrollRange = (dependView as MAppLayoutInterface).getScrollRange()
        val scrollY = parent.scrollY
        val rate = scrollY.toFloat() / scrollRange
        val translateY = selfView.measuredHeight * rate +parent.scrollY
        selfView.translationY = translateY
        return false
    }
   	......   
}

可以看出上面的滑块处理就是这么简单,这里有几个类后面会提到,大概有个流程概念就行

那么有依赖就得监听依赖的变化,上面提到的onPreDrawListener就派上用场了,同样在MNestParent中添加代码

  private var preListenerAdded = false
  private val dependRefList: MutableList<DependViewInfo> = ArrayList()
  private var appLayout: MAppLayoutInterface? = null
  private val preListener = ViewTreeObserver.OnPreDrawListener{
        onDenpendViewStateChanged()
        true //kotlin最后一行就是返回值
    }

  override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        super.onLayout(changed, left, top, right, bottom)
        prepareChildren()
        ensurePreListener()
    }
	override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        for (i in 0 until childCount) {
            val childView = getChildAt(i)
            if (childView is MAppLayoutInterface) appLayout = childView
            if (childView !is MAppLayout && appLayout != null) {
                val childHeightSpec = ViewGroup.getChildMeasureSpec(
                    heightMeasureSpec,
                    appLayout!!.getPinedHeight(),
                    childView.layoutParams.height
                )
                val childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec,0,childView.layoutParams.width)
                childView.measure(childWidthSpec, childHeightSpec)
            }
        }
        if (appLayout != null) {
            totalHeight = appLayout!!.getScrollRange() + measuredHeight
        } else {
            totalHeight = measuredHeight
        }
    }

  fun prepareChildren() {
        dependRefList.clear()
        for (i in 0 until childCount) {
            val childView = getChildAt(i)
            val params = childView.layoutParams as MLayoutParams
            if (params.behavior == null) continue
            for (j in 0 until childCount) {
                if (i == j) continue
                val dependView = getChildAt(j)
                if (params.behavior!!.dependsOn(this, childView, dependView)) {
                    dependRefList.add(DependViewInfo(childView, dependView))
                    params.behavior!!.onDependViewChanged(this, childView, dependView)
                    break
                }
            }
        }
    }
    
	fun ensurePreListener() {
        if (preListenerAdded) return
        viewTreeObserver.addOnPreDrawListener(preListener)
        preListenerAdded = true
    }
    
	 fun onDenpendViewStateChanged() {
        if (dependRefList.size == 0) return
        for (i in dependRefList.indices) {
            val dependInfo = dependRefList[i]
            val childParams = dependInfo.selfview.layoutParams as MLayoutParams
            val behavior = childParams.behavior
            behavior!!.onDependViewChanged(this, dependInfo.selfview, dependInfo.dependview)
        }
    }

这里我在onLayout中完成view的绑定关系,这里的布局已经完成可以做依赖关系的处理,然后我在onMeasure中处理的view的重新测量,因为有了吸顶,子view占据的高度需要减去这一块。
然后我定义了appLayout实现了MAppLayoutInterface的接口,这里是模仿coordinlayout,在我定义的appLayout是个LinearLayout,其中会以里面的最后一个view进行吸顶固定,这里的scrollRange是该view的总高度减去吸顶的高度,也就是可以可以滑动多少距离

最后监听有变化,通知所有的有关联的view即可,使用也很简单

到这里Behavior的基本思路也就完成了,印象中那么难其实原理很简单

3.NestedScrollingParent

其实NestedScrollingParent这个其实没多大意义,因为ViewGroup中本身就有同名的方法(API>21),甚至可以在NestedScrollingChildHelper方法中看到一些痕迹

 public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
            int nestedScrollAxes, int type) {
        if (parent instanceof NestedScrollingParent2) {
            return ((NestedScrollingParent2) parent).onStartNestedScroll(child, target,
                    nestedScrollAxes, type);
        } else if (type == ViewCompat.TYPE_TOUCH) {
            if (Build.VERSION.SDK_INT >= 21) {
                try {
                    return parent.onStartNestedScroll(child, target, nestedScrollAxes);
                } catch (AbstractMethodError e) {
                }
            } else if (parent instanceof NestedScrollingParent) {
                return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
                        nestedScrollAxes);
            }
        }
        return false;
    }

ViewParent本身就有相关的方法,当你自定义一个ViewGroup去实现NestedScrollingParent接口,会发现不会提示未实现的方法
NestedScrollingParent2NestedScrollingParent3则是对NestedScrollingParent的拓展,新增了一些接口,而这些就是我们必须实现的
虽说默认不需要实现,但为了实现功能,该重写的还是得重写

下面简单讲一下这里的机制了

NestedScrollingParentNestedScrollingChild是一对"父子"关系,更像是一个上下级关系
NestedScrollingChild会发出一系列请求,这些请求最终会通知到NestedScrollingParent中,NestedScrollingParent会判断是否对这些请求进行处理,比如消耗,处理完毕后再把剩下的返给NestedScrollingChild
简单的说就是NestedScrollingChild做什么都要先去获得NestedScrollingParent的批准,如果NestedScrollingParent不关心该操作那就自己处理,否则上级先处理完再把处理完的结果给下级

从方法名字上就可以窥见一些痕迹,NestedScrollingChild很多方法都是dispatch开头,表示分发。NestedScrollingParent很多方法都是on开头,表示接收。也就是起点都是NestedScrollingChild

比如startNestedScroll方法

 public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
        if (hasNestedScrollingParent(type)) {
            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;
    }

这里会通过onStartNestedScroll通知上级,这个方法就是NestedScrollingParent中的。如果Parent想要处理,也就是返回true,然后会调用setNestedScrollingParentForType设置操作的类型和对应的view,这个方法很重要

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

    private void setNestedScrollingParentForType(@NestedScrollType int type, ViewParent p) {
        switch (type) {
            case TYPE_TOUCH:
                mNestedScrollingParentTouch = p;
                break;
            case TYPE_NON_TOUCH:
                mNestedScrollingParentNonTouch = p;
                break;
        }
    }

后面会根据这个进行依据判断,如果为null,那么就认为没有上级要处理,完全交给自己处理
比如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) {
                    consumed = getTempNestedScrollConsumed();
                }
                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;
    }

这里重点关注consumed这个数组,这里会先把数组的数据全部置0,然后通过onNestedPreScroll交给上级,上级如果想要处理,那么就给这个数组中的数据进行赋值,当值发生变化,也就是consumed[0] != 0 || consumed[1] != 0;成立,那么下级再把上级消耗的减去,就是自己真实要滑动的距离了

比如子View手势判断滑动距离是10,把这个通知给父View,父View接收后处理了8,那么最终反馈给子View就剩下2了,然后再去处理真实滑动即可。如果父View把这些全处理,那么子View就压根滑不动了,这很好理解。

有了这些理解,我们就可以处理NestedScrollingParent了,在上面定义的MNestParent中添加nest相关处理

 //这个方法只是为了在setNestedScrollingParentForType把当前view的父view设置
    // 后续可以通过getNestedScrollingParentForType获取到viewparent
    // 后续的dispatchNestedPreScroll等都会通过这个进行判断,没有就返回false 也就是不处理
    override fun onStartNestedScroll( child: View,target: View,nestedScrollAxes: Int,type: Int): Boolean {
        return nestedScrollAxes and ViewCompat.SCROLL_AXIS_VERTICAL != 0
    }

    override fun onNestedPreScroll(target: View,dx: Int,dy: Int,consumed: IntArray, type: Int) {
        var curScrollY = scrollY
        var afterScrollY = curScrollY + dy
        //当前是吸顶状态或者原始状态,需要判断子View是否都已经滑动到原始状态(scrollY=0),才能下滑父View
        if (curScrollY == 0 || curScrollY == appLayout!!.getScrollRange()) {
            if (dy < 0) {
                for (i in 0 until childCount) {
                    val paraView = getChildAt(i)
                    var paramas = paraView.layoutParams as MLayoutParams
                    val behavior = paramas.behavior
                    //如果当前view不能下滑,也是就有子view的scrollY没有到0,通过behavior滑动子View
                    if (behavior != null && !behavior.canScrollDown(paraView)) {
                        behavior.tryScrollTo(paraView, 0, dy)
                        return
                    }
                }
            }
        }
		//区间内 全部由父亲接收
        if (afterScrollY > 0 && afterScrollY < appLayout!!.getScrollRange()) {
            if (target.scrollY == 0) { 
                consumed[1] = dy //全部消耗,一点都不留给子view,只滑动父view
                scrollTo(0, afterScrollY)
            }
        } else {
            if (curScrollY == 0 || curScrollY == appLayout!!.getScrollRange()) return
            //强制滑动到0
            if (afterScrollY < 0) {
                afterScrollY = 0
                consumed[1] = dy + afterScrollY
                scrollTo(0, 0)
			//强制滑动到吸顶状态
            } else if (afterScrollY > appLayout!!.getScrollRange()) {
                afterScrollY = appLayout!!.getScrollRange()
                consumed[1] = dy + (afterScrollY - appLayout!!.getScrollRange())
                scrollTo(0, appLayout!!.getScrollRange())
            }
        }
    }
//这里可能实现不同子view的惯性联动,也是子view->A 惯性滑动到头后 ,交给父View,父view再把速度传给子view->B ,实现B的惯性滑动,也就是上面gif中的,在AppLayout中的惯性滑动最终在吸顶后反馈给下面的view,继续惯性滑动
override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean {
        for (i in 0 until childCount) {
            val target = getChildAt(i)
            val params = target.layoutParams as MLayoutParams
            params.behavior?.apply {
                params!!.behavior!!.fliping(target, velocityY)
            }
        }
        return false
    }

这里涉及到一个Behavior,我先贴出来

class MBehaviorImp() : IBehavior<MNestChild> { 
    override fun dependsOn(parent: View, selfView: MNestChild, denpendView: View): Boolean {
        if (denpendView is MAppLayout) {
            selfView.layout(0,denpendView.measuredHeight,parent.measuredWidth,
                selfView.measuredHeight + denpendView.measuredHeight
            )
            return true
        }
        return false
    }
    override fun onDependViewChanged(parent: View,selfView: MNestChild,dependView: View): Boolean {
        return false
    }

    override fun fliping(target: MNestChild, velocityY: Float): Boolean {
        target.fling(velocityY.toInt())
        return true
    }

    override fun canScrollDown(target: MNestChild): Boolean {
        if (target.scrollY != 0) return false //如果不为0,那么就不能下滑
        return true
    }

    override fun tryScrollTo(target: MNestChild, delx: Int, delY: Int) {
        target.tryScrollTo(delx, delY)
    }
}

这个是我下面要定义的MNestChild的,AppLayout会反馈到父view中,再通过Behavior反馈给MNestChild

4.NestedScrollingChild

NestedScrollingChild的处理一遍需要借助一个工具类NestedScrollingChildHelper,这个也只是分发的工具类,逻辑并不复杂,通过这个通知上级即可

Child的处理稍微复杂一点,因为需要处理手势和惯性滑动,这里就要借助无敌的Scroller了,处理丝滑的滑动就靠这个了,不理解Scroller的可以去查一下资料,这个可以认为就是一个算法工具,给定时间或速度,它就能给你算出相应时间对应的值,通过computeScrollOffset进行当前时间值的计算,根据这个值进行滑动处理即可

那么开始定义MNestChild,我这里直接继承了LinearLayout,注意嵌套滑动需要调setNestedScrollingEnabled设置可用

class MNestChild @JvmOverloads constructor(
    context: Context,
    attributeSet: AttributeSet? = null,
    intStyle: Int = 0
) : LinearLayout(context, attributeSet, intStyle), NestedScrollingChild {

    private val mChildHelper: NestedScrollingChildHelper = NestedScrollingChildHelper(this)

    init {
        mChildHelper.isNestedScrollingEnabled = true
    }

    private var totalHeight: Int = 0
    private var velocityTracker = VelocityTracker.obtain()
    private val maxVelocityY = ViewConfiguration.get(context).scaledMaximumFlingVelocity
    private val minVelocityY = ViewConfiguration.get(context).scaledMinimumFlingVelocity
    private val mScroller = Scroller(context)
    private val consumed = intArrayOf(0, 0)
    private var lastTouchedY = 0
    private var lastFlingY = 0

  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        totalHeight = 0
        for (i in 0 until childCount) {
            val childView = getChildAt(i)
            totalHeight += childView.measuredHeight
        }
    }
	 override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        velocityTracker?.recycle()
    }
}

先定义一些基本变量,包括总高度,速度捕捉,上一次的按下位置等,然后再onMeasure中算出总共的高度,后面算滑动边界要用,最后要记得回收velocityTracker
接着处理手势

 override fun onTouchEvent(event: MotionEvent): Boolean {
        val actionMask = event.actionMasked
        when (actionMask) {
            MotionEvent.ACTION_DOWN -> {
            	//如果上次计算未完成,强制终止
                if (!mScroller.isFinished) {
                    mScroller.abortAnimation()
                    mScroller.forceFinished(true)
                }

				//这里的Type默认会有两种
				//TYPE_TOUCH  手势拖动
				//TYPE_NON_TOUCH  非手势,一般指惯性滑动
                mChildHelper.stopNestedScroll(ViewCompat.TYPE_TOUCH)
                mChildHelper.stopNestedScroll(ViewCompat.TYPE_NON_TOUCH)

                //其实就是设置setNestedScrollingParentForType,如果父亲有处理的意愿就设置,否则父亲就不处理
                mChildHelper.startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL,
                    ViewCompat.TYPE_TOUCH
                )
                //这里按照getRawY进行处理,因为父View滑动可能会有些影响
                lastTouchedY = event.rawY.toInt()
            }
            MotionEvent.ACTION_MOVE -> {
            	//追踪这次的速度操作
                velocityTracker.addMovement(event)
                val curEventY = event.rawY.toInt()
                var delY = (lastTouchedY - curEventY).toInt()

				//分发,返回true表示距离被上级消耗了一部分,减去消耗的计算剩下的
                if (mChildHelper.dispatchNestedPreScroll(0, delY, consumed, null)) {
                    delY -= consumed[1]
                }

				//如果还有剩下的
                if (delY != 0) {
                    val curScrollY = scrollY
                    tryScrollTo(0, delY)
                    val resScrollY = scrollY
                    //子view滑动可能到边界,那么这些距离可能还会剩余一点,然后把这个再通知给上级
                    val unconsumeY = delY - (resScrollY - curScrollY)
                    //其实这个方法我上级并没有处理,实际传递方式也差不多是这样
                    mChildHelper.dispatchNestedScroll(0,0,
                        delY,unconsumeY,
                        null,ViewCompat.TYPE_TOUCH)
                }
                consumed[1] = 0
                lastTouchedY = curEventY
            }
            MotionEvent.ACTION_UP -> {
            	//计算当前惯性的速度
                velocityTracker.computeCurrentVelocity(1000)
                var velocityY = velocityTracker.yVelocity
                if (Math.abs(velocityY) > minVelocityY) {
                    if (Math.abs(velocityY) > maxVelocityY) {
                   		//sign方法获取当前值的符号 比如 -5就是-1  6就是1
                        velocityY = velocityY.sign * maxVelocityY
                    }
                    //通知父view惯性滑动发生
                    if (!mChildHelper.dispatchNestedPreFling(0f, -velocityY)) {
                        //fling(velocityY.toInt()) //速度这个一般和我们滑动的偏移是相反的	
                        //因为有behvaior处理,这里就不需要调用fliping了
                    }
                }
                //通知终止,终止的类型是TYPE_TOUCH
                mChildHelper.stopNestedScroll(ViewCompat.TYPE_TOUCH)
            }
        }
        return true
    }
    
    //滑动到指定位置
	fun tryScrollTo(delX: Int, dexY: Int) {
        if (dexY == 0) return
        var currentScrollY = scrollY
        var targetScrollY = currentScrollY + dexY
        if (targetScrollY < 0) targetScrollY = 0
        if (targetScrollY > totalHeight - measuredHeight) targetScrollY =
            totalHeight - measuredHeight
        scrollTo(0, targetScrollY)
    }

下面就是Scroller相关的操作

  fun fling(velocityY: Int) {
        if (childCount > 0) {
        	//开始滑动,类型为TYPE_NON_TOUCH
            mChildHelper.startNestedScroll(
                ViewCompat.SCROLL_AXIS_VERTICAL,
                ViewCompat.TYPE_NON_TOUCH
            )
            lastFlingY = scrollY
            //fling一般都会指定无界,传递速度会帮我们自动计算
            mScroller.fling(
                scrollX, scrollY,
                0, -velocityY,
                0, 0, Int.MIN_VALUE, Int.MAX_VALUE
            )
            invalidate()
        }
    }

    override fun computeScroll() {
    	//这里通过是否结束进行判断
    	//如果直接用computeScrollOffset判断,如果父view把手势全部消耗,那么这里会很快返回false,但实际计算并没有停止
        if (mScroller.isFinished) return
        mScroller.computeScrollOffset()
        var curY = mScroller.currY
        var delY = lastFlingY - curY  
        //继续通知父亲滑动操作
        if (mChildHelper.dispatchNestedPreScroll(0,delY,consumed,null,
                ViewCompat.TYPE_NON_TOUCH)) {
            delY -= consumed[1]
        }
        tryScrollTo(0, delY)
        invalidate()
        lastFlingY = curY
    }

到这里应该基本使用思路就应该都了解了

最后的AppLayout其实和MNestChild实现差不多,就直接贴出代码了

class MAppLayout @JvmOverloads constructor(
    context: Context,
    attributes: AttributeSet? = null,
    intStyle: Int = 0
) : LinearLayout(context, attributes, intStyle), MAppLayoutInterface, NestedScrollingChild {

    private lateinit var lastChildPined: View
    private var pinedHeight = 0

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        //把最后一个当成固定的头部
        lastChildPined = getChildAt(childCount - 1)
        pinedHeight = lastChildPined.measuredHeight
    }

    override fun getPinedHeight(): Int {
        return pinedHeight
    }

    override fun getPinedView(): View {
        return lastChildPined
    }

    override fun getContentHeight(): Int {
        return measuredHeight
    }

    override fun getScrollRange(): Int {
        return measuredHeight - pinedHeight
    }

    private val mChildHelper: NestedScrollingChildHelper = NestedScrollingChildHelper(this)

    init {
        mChildHelper.isNestedScrollingEnabled = true
    }

    private var velocityTracker = VelocityTracker.obtain()
    private val maxVelocityY = ViewConfiguration.get(context).scaledMaximumFlingVelocity
    private val minVelocityY = ViewConfiguration.get(context).scaledMinimumFlingVelocity
    private val mScroller = Scroller(context)

    private val consumed = intArrayOf(0, 0)

    private var lastTouchedY = 0

    override fun dispatchTouchEvent(event: MotionEvent): Boolean {
        val actionMask = event.actionMasked
        when (actionMask) {
            MotionEvent.ACTION_DOWN -> {
                if (!mScroller.isFinished) {
                    mScroller.abortAnimation()
                    mScroller.forceFinished(true)
                }

                mChildHelper.stopNestedScroll(ViewCompat.TYPE_TOUCH)
                mChildHelper.stopNestedScroll(ViewCompat.TYPE_NON_TOUCH)
                mChildHelper.startNestedScroll(
                    ViewCompat.SCROLL_AXIS_VERTICAL,
                    ViewCompat.TYPE_TOUCH
                )
                lastTouchedY = event.rawY.toInt()
            }
            MotionEvent.ACTION_MOVE -> {
                velocityTracker.addMovement(event)

                val curEventY = event.rawY.toInt()
                var delY = (lastTouchedY - curEventY).toInt()
                if (mChildHelper.dispatchNestedPreScroll(0, delY, consumed, null)) {
                    delY -= consumed[1]
                }
                lastTouchedY = curEventY
            }
            MotionEvent.ACTION_UP -> {
                velocityTracker.computeCurrentVelocity(1000)
                var velocityY = velocityTracker.yVelocity
                if (Math.abs(velocityY) > minVelocityY) {
                    if (Math.abs(velocityY) > maxVelocityY) {
                        velocityY = velocityY.sign * maxVelocityY
                    }
                    if (!mChildHelper.dispatchNestedPreFling(0f, -velocityY)) {
                        invalidate()
                    }
                }
                mChildHelper.stopNestedScroll(ViewCompat.TYPE_TOUCH)

            }
        }
        return super.dispatchTouchEvent(event)
    }
}

interface MAppLayoutInterface {
    fun getPinedHeight(): Int
    fun getPinedView(): View
    fun getContentHeight():Int
    fun getScrollRange():Int
}

一直以为NestScrolling很复杂,也一直很抗拒去处理,其实扒开代码,自己实现也不算很复杂,就是如果自定义,很多细节问题需要考虑周全,这里只是一个简单的学习Demo,不可避免会有很多问题,多多包涵

最后是布局文件了,也很简单

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <com.view.zero.learn.views.nestscroll.MNestParent
        android:id="@+id/mNestParent"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        >
        <com.view.zero.learn.views.nestscroll.MAppLayout
            android:layout_width="match_parent"
            android:orientation="vertical"
            android:layout_height="wrap_content">
            <TextView
                android:id="@+id/header_1"
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:gravity="center"
                android:text="头部内容1"
                android:textSize="30sp"
                android:onClick="onClickViews"
                />
            <TextView
                android:id="@+id/header_2"
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:gravity="center"
                android:text="头部内容2"
                android:textSize="30sp"
                android:onClick="onClickViews"
                />
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="130dp"
                android:layout_marginTop="-30dp"
                >
                <TextView
                    android:id="@+id/pin_1"
                    android:layout_marginTop="30dp"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:gravity="center"
                    android:text="固定内容"
                    android:background="#0aa"
                    android:textColor="#fff"
                    android:onClick="onClickViews"
                    android:textSize="30sp"
                    />
            </LinearLayout>
        </com.view.zero.learn.views.nestscroll.MAppLayout>
        <com.view.zero.learn.views.nestscroll.MNestChild
            android:id="@+id/mNestChild"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:myBehavior="com.view.zero.learn.views.nestscroll.MBehaviorImp"
            android:orientation="vertical">
        </com.view.zero.learn.views.nestscroll.MNestChild>
        <TextView
            android:id="@+id/float_1"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:background="#06a"
            android:text="Dep"
            android:onClick="onClickViews"
            android:textSize="20sp"
            android:gravity="center"
            android:textColor="#fff"
            app:myBehavior="com.view.zero.learn.views.nestscroll.MBehaviorImp2"
            />
        <TextView
            android:layout_width="match_parent"
            android:background="#0af"
            android:text="最上面的"
            android:gravity="center"
            android:textColor="#fff"
            app:myBehavior="com.view.zero.learn.views.nestscroll.MBehaviorImp3"
            android:layout_height="30dp"/>
    </com.view.zero.learn.views.nestscroll.MNestParent>
</LinearLayout>

5.recyclerView嵌套

因为Recyclerview本身就实现了NestScrolling相关的操作接口,我这里直接把我的MNestChild替换成RecyclerView,如果可以正常进行嵌套操作,那么就说明上述的理解和流程是正确的,这里需要重新定义一下Behavior,我这里新增一个开始滑动的监听

class MRecyclerViewBehaviorImp() : IBehavior<RecyclerView> {
    override fun dependsOn(parent: View, selfView: RecyclerView, denpendView: View): Boolean {
        if (denpendView is MAppLayout) {
            selfView.layout(
                0,
                denpendView.measuredHeight,
                parent.measuredWidth,
                selfView.measuredHeight + denpendView.measuredHeight
            )
            return true
        }
        return false
    }

    override fun onDependViewChanged(
        parent: View,
        selfView: RecyclerView,
        dependView: View
    ): Boolean {
        return false
    }

    private var fliping = false

    override fun fliping(target: RecyclerView, velocityY: Float): Boolean {
        if (fliping) return true
        fliping = true
        target.fling(0, velocityY.toInt())
        return true
    }

    override fun canScrollDown(target: RecyclerView): Boolean {
        val can = target.canScrollVertically(-1)
        return !can
    }

    override fun tryScrollTo(target: RecyclerView, delx: Int, delY: Int) {
        target.scrollBy(delx, delY)
    }

    override fun onStartScroll() {
        fliping = false
    }
}

RecyclerView可以通过canScrollVertically判断是否已经滑动到了顶端
把布局中的Behavior和子View进行替换

  <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:myBehavior="com.view.zero.learn.views.nestscroll.MRecyclerViewBehaviorImp"
            />

下面是实际的展示效果,实际也是可以正常执行的,那么以后我们自定义嵌套NestScrolling就可以根据这个作为大概的流程参考,其实也蛮好玩的
在这里插入图片描述
不仅是RecyclerViewViewPager也是可以弄的,只要修改下Behavior就行,讲真,到这里就可以看出Behavior这个设计是真的叼
在这里插入图片描述

最后附上项目地址
https://github.com/gouptosee/ZeroViewLearns

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值