自己造轮子--AppBarLayout的enterAlways效果未达预期?那就自定义一个Behavior

236 阅读3分钟

效果需求:顶栏在列表下滑的时候先行滑出,上滑时先行滑入

看到后快速实现的想法集中在CoordinatorLayoutAppbarLayout上,只需要配置上app:layout_scrollFlags="scroll|enterAlways"就能实现该效果。

效果图1

简易效果有了然后把title替换成目标内容,开始出现违和感了。首先AppbarLayout会为自己自动添加一个背景色,另一个问题是ScrollingViewBehavior的实现是下拉的时候优先将AppbarLayoutRecyclerView一起向下移动,直到AppbarLayout完全展示才将滑动事件交给RecyclerView处理,上滑同理。在这个过程中,两个View一直是纵向上的上下关系,无法做到下图中的效果。

效果图2

效果未达预期 自己动手写一个

因为CoordinatorLayout帮我们做了很多事情,所以我们只需要用Behavior做滑动的处理就行了。

首先重写方法layoutDependsOn确定需要依赖的View(如果只是处理滑动的话,这一步不是必须的,但是有另外的打算)

override fun layoutDependsOn(parent: CoordinatorLayout,child: View,dependency: View): Boolean {
    //暂时先直接依赖RecyclerView,这里是可以随机应变的
    return dependency is RecyclerView    
}

然后重写方法onDependentViewChanged。说一下在这里需要处理的事情:记录bar的高度,并设置RecyclerViewtranslationY

题外话:为什么ScrollingViewBehavior能直接在xml布局中展示出RecyclerViewAppbarLayout下方的效果?因为ScrollingViewBehavior是作用于RecyclerView依赖于AppbarLayout,其中重写了方法onChildLayout直接对RecyclerView进行重新布局,而目前实现方案是Behavior作用于bar依赖于RecyclerView,无法对RecyclerView的位置进行重新布局。

override fun onDependentViewChanged(parent: CoordinatorLayout,child: View,dependency: View): Boolean {
    //无法通过layout方法调整RecyclerView的位置,那么只能在这里做出调整
    //因为该方法在绑定的时候必定会回调一次
    if (floatViewHeight < 0) {
        //获取bar的高度
        floatViewHeight = (child.measuredHeight + child.marginVertical()).toFloat()
        //关于fitsSystemWindows的处理
        //减去view底部被系统填充的底部padding
        // (虽然也会减去自己设置的padding,所以这个view尽量不要设置纵向的padding)
        if (ViewCompat.getFitsSystemWindows(child)) floatViewHeight -= child.paddingBottom
        //为RecyclerView设置偏移
        dependency.translationY = floatViewHeight
        return true
    }
    return false
}

剩下的就是在方法onNestedPreScroll做滑动预处理,这个方法涉及到了NestedScrolling的机制,目前个人也只知道个大概,还无法进行灵活地使用。

override fun onNestedPreScroll(coordinatorLayout: CoordinatorLayout, child: View, target: View,dx: Int,dy: Int,consumed: IntArray,type: Int) {
    super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type)
    //当view绑定的时候,recyclerview中不一定有数据,recyclerview不一定拥有高度
    //所以需要在滑动的时候才能知道recyclerview的高度
    //才能计算出活动范围
    if (offsetLimit < 0) {
        offsetLimit =
            (target.measuredHeight + target.marginVertical() + floatViewHeight - coordinatorLayout.measuredHeight)
                .coerceIn(0f, floatViewHeight)
    }
    //向上滑动
    if (dy > 0) {
        //判断bar能否移动
        if (child.translationY > -offsetLimit) {
            child.translationY = (child.translationY - dy).coerceIn(-offsetLimit, 0F)
        }
        //判断recyclerview是否需要移动
        //向上滑动时,recyclerview需要先消耗完自己的translationY才能滑动列表
        if (target.translationY > floatViewHeight - offsetLimit) {
            target.translationY =
                (target.translationY - dy).coerceIn(floatViewHeight - offsetLimit, floatViewHeight)
            //这里其实是有误差的,因为在上一行中不一定消耗了dy,可能会比dy少,不过问题不大
            consumed[1] = dy
        }
    }
    //向下滑动
    if (dy < 0) {
        //判断bar能否移动
        if (child.translationY < 0) {
            child.translationY = (child.translationY - dy).coerceIn(-floatViewHeight, 0F)
        }
        //这里判断增加了一个条件,recyclerview需要无法下拉之后再进行移动
        if (!target.canScrollVertically(-1) && target.translationY < floatViewHeight) {
            target.translationY =
                (target.translationY - dy).coerceIn(floatViewHeight - offsetLimit, floatViewHeight)
            //这里同上,同样存在误差,不过问题不大
            consumed[1] = dy
        }
    }
}

结语

整个behavior不是很复杂,核心代码全在上面,最终就能完成上面图二的效果。自己动手一遍基本上能了解behavior的用法,同时也了解到了CoordinatorLayout与behavior的组合是个很强大而且很有意思的东西。最后希望自己能把自己造轮子这个系列写下去,这个系列不会提供完整的源码,但是会提到自己实现过程中的细节,目标是看完+动手,自己也能写。分享给屏幕前的你,记录给在未来的我。