效果展现

丝滑体验:Android自定义Behavior实践

前景提要:本文的阅览需求基于NestedScroll嵌套滑动的基本知识有所了解,并且简单自定义过Behavior

效果定格图片

  1. 底下是一张背景图,状况栏作了沉溺效果,然后下面是一个RecyclerView

丝滑体验:Android自定义Behavior实践

  1. 跟着手指向上推进,Rv(RecyclerView)也随之上推,顶部的header的透明度逐渐改动,直到RV的上鸿沟达到header的下鸿沟停止,header展现结束

丝滑体验:Android自定义Behavior实践

  1. 继续向上推进,header跟着RV一同上推,并联动顶部TitleBar发生透明度改动,一同header发生了scale改动

丝滑体验:Android自定义Behavior实践

  1. 当header没(mo)入TitleBar后,标题随之展现,RV继续滑动,假如向下拉的话,当RV翻滚量用完之后,会带着header一块回去,一如GIF中预览的效果一般

丝滑体验:Android自定义Behavior实践

效果完结

思路剖析

其实在定格图片中,现已剖析了一部分了,接着从代码角度继续剖析一下设计思路:

  1. RV需求和header有滑动关系,那么抱负来说,他们最好是同级的,能够经过CoordinatorLayout来和谐,由于其间有同级嵌套滑动的分发能够使用
  2. TitleBar的联动依赖于header的推进状况,那其实能够依据header的移动,对外显露监听,使其能够随之改动,那它与(Rv+header).CoordinatorLayout是同级的,且是线性笔直排布的
  3. Rv的上滑能够拆分为三个阶段
    • 从下方到header下鸿沟: 这个阶段Rv的高度在不断改动,假如真的一向改动高度的话那整个的测量就会变得十分频繁,并且手动setLayoutParams按滑动的频率可能发生抖动。换个思路Rv的初始y轴在下方,然后逐渐回到0,所以能够用translationY来操作这个效果
    • 和header一向上推: 这个阶段header需求依据Rv发生的滑动,作同步的改动,上滑1dp,两者一同上滑1dp,依据上一点的思路,这儿咱们也用translationY来操作,关于header上推便是从0~ -height的改动
    • 当header没入后: 这个阶段便是单纯的本身滑动的进程了,没有任何压力
  4. Rv的下滑也能够拆分为三个阶段
    • 当header没入后: 这个阶段也是Rv本身滑动的阶段,所以能够经过computeVerticalScrollOffset判别本身是否有能够下滑的量,假如够用,那就自己滑就能够了,假如不够,那就需求将本身和header一同下推
    • header还未固定: 这时便是上面的第二种状况,header需求一向滑动到本身translationY为0停止
    • header固定后: 这时便是一开端的相反状况,调整Rv的Y轴就好了
  5. 由滑动剖析能够得出两个定论
    • Rv的最大高度应该是从TitleBar以下的悉数
    • header是初始固定在TitleBar下的
  6. 然后还有一些小细节需求注意
    • 滑动阻尼,也便是认为滑动不到位,需求复位,假如到位就需求协助触达。其实便是在阶段1时,松手的状况,不期望Rv停留在该方位,而是只有两种状况:打开|收缩
    • Rv滑动到最下面就不能滑动了,类似于BottomSheet的Peek差不多
    • ……

布局

  1. 最外层是一个CoordinatorLayout,当然这个没必要,替换成FrameLayout也是相同的
  2. 背景图就一张铺满的图片
  3. TitleBar简单一点是个TextView,这儿固定高度了,由于下面需求MarginTop做的笔直排布,所以最外层改成LinearLayout也是能够的
  4. CoordinatorLayout来担任header和Rv的滑动和谐
  5. header是一个比较简单的组合
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ImageView
        android:id="@+id/map"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="fitXY"
        android:src="@drawable/img_1" />
    <TextView
        android:id="@+id/titleBar"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_gravity="top"
        android:alpha="0"
        android:background="@color/color_yellow"
        android:gravity="center|bottom"
        android:paddingBottom="10dp" />
    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:id="@+id/scrollView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="bottom"
        android:layout_marginTop="50dp"
        android:orientation="vertical">
        <LinearLayout
            android:id="@+id/orderStatusLine"
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:layout_gravity="top"
            android:alpha="0"
            android:background="@color/white"
            android:gravity="center_vertical"
            android:orientation="vertical"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent">
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="配送中"
                android:textColor="@color/common_text_main_black"
                android:textSize="@dimen/Big_text_size"
                android:textStyle="bold" />
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="骑手正在快马加鞭的配送中,请您耐性等候"
                android:textColor="@color/common_text_main_black"
                android:textSize="@dimen/Big_text_size"
                android:textStyle="bold" />
        </LinearLayout>
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="top"
            android:nestedScrollingEnabled="true"
            android:orientation="vertical"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/orderStatusLine" />
    </androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

代码

给Rv填充数据和一些根底操作就不写了,直接上正文内容。总共分为两部分,一部分是初始化时布局中的设置,另一部分是担任和谐的自定义Behavior

// 这个便是最大的PEEK
view.recyclerView.translationY = OrderStatusBehavior.MAX_PEEK
// 这个放下一段代码
val behavior = OrderStatusBehavior(this)
// 这是个自定义的监听
behavior.listener = object : OrderStatusBehavior.OrderStatusListener {
    // 这儿便是TitleBar和header的互动
    private val AIM_PERCENT = 0.7f
    override fun onHeaderMove(percent: Float, title: String) {
        // 这个监听望文生义一下,header的移动程度,经过percent表示,上推进程中percent逐渐变大到1,下滑最小到固定时为0
        // 这儿便是TitleBar中何时显示文字了,这儿的阈值判别是header移动到70%
        if (percent >= AIM_PERCENT && view.titleBar.text.isEmpty()) {
            view.titleBar.text = title
        } else if (percent < AIM_PERCENT && view.titleBar.text.isNotEmpty()) {
            view.titleBar.text = ""
        }
        // 这是透明度和谐
        view.titleBar.alpha = percent
    }
}
// 这儿绑定behavior,当然xml中也是相同能够绑定的(原理:依据途径反射实例化并绑定),但横竖还要设置监听,那就放代码里吧
(view.orderStatusLine.layoutParams as? CoordinatorLayout.LayoutParams)?.behavior = behavior

然后便是重头戏,自定义Behavior,很多人对这玩意儿很惧怕,搞不清楚它的原理,一开端我也是,但自己上手写一下后发现还挺有意思的,终究的Behavior贴在终究,先跟着我一步步渐渐写吧

一开端,十分简单,三个办法,其间最为重要的便是layoutDependsOn决定了与谁进行和谐,这儿简单经过类型进行判别一下就好。然后已然要和谐滑动,那便是嵌套滑动中两个陈词滥调的办法,何时开端:onStartNestedScroll,只要是笔直方向的,咱们都要;第一次询问,预翻滚onNestedPreScroll,咱们的思路便是在预翻滚阶段处理咱们需求手动判别的,而正式翻滚阶段就由Rv自己做就好了,咱们无须关心

class OrderStatusBehavior @JvmOverloads constructor(context: Context, attributeSet: AttributeSet? = null) : CoordinatorLayout.Behavior<View>(context, attributeSet), Animator.AnimatorListener {
    override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View): Boolean {
        return dependency is RecyclerView
    }
    // child是本身;target是和谐的方针view;dx\dy是x\y轴的滑动,向右为x轴u正方向,向下为y轴正方向,能够测验画图辅助了解
    // consumed是消费数组,[x,y]记载了x\y轴的滑动消费状况,假如需求消费,那就需求记载
    // 假如不消费的话,那么不论你怎样滑,Rv本身在后续环节还会本身滑动,由于没有消费完
    override fun onNestedPreScroll(coordinatorLayout: CoordinatorLayout, child: View, target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
        if (dy > 0) {
            // 上滑
            ……
        } else {
            // 下拉
            ……
        }
    }
    // child是本身,directTargetChild建议嵌套滑动的view,target也是
    override fun onStartNestedScroll(coordinatorLayout: CoordinatorLayout, child: View, directTargetChild: View, target: View, axes: Int, type: Int): Boolean {
        // 位运算,取vertical位,即笔直滑动
        return axes.and(ViewCompat.SCROLL_AXIS_VERTICAL) != 0
    }
}

然后咱们开端测验填充onNestedPreScroll中的内容,先是上滑。主旨思想便是translationY位移和consumed[1]消费。当然有些代码也能够再优化一下,这儿仅仅跟着写的思路一块过一遍

if (dy > 0) {
    // 上滑
    // 初始y为0,上推进程中会逐渐变到-height
    // 初始,y被设置为一个常量,MAX_PEEK = 1300f
    val y = target.translationY
    // 上推时,translationY会<0,所以以此判别header是否还固定在原位
    if (child.translationY >= 0 && y > child.height) {
        // 假如header固定时,那childY便是第一阶段中,rv的上界
        if (dy < y - child.height) {
            // 滑动间隔不足以使得rv达到上界,即滑动间隔 < rv与header的之间的间隔
            // 此刻,使得rv改动Y轴即可
            target.translationY = y - dy
            // 记载消费
            consumed[1] += dy
        } else {
            // 假如一次滑动量很大,那就先让rv抵达header处,并消费悉数
            // 这儿其实是个简化,理论上 下一个分发阶段需求处理,这儿偷懒直接疏忽
            target.translationY = child.height.toFloat()
            consumed[1] += dy
        }
    } else {
        // 预备一同推
        if (y > 0) {
            // 还没把header推完
            if (y - dy >= 0) {
                // 也还推不倒头,就一同动
                // 这儿target.translationY -= dy是相同的,我是由于已然y都记载了,干脆用了
                target.translationY = y - dy
                child.translationY -= dy
                consumed[1] += dy
            } else {
                // 先把剩下的推推完
                // header其实也能够直接设置-child.height,当然这儿-y是异曲同工
                child.translationY -= y
                // rv推到头,便是y位移为0
                target.translationY = 0f
                // 这儿是重头戏啊,由于一同推的间隔是rv剩下的y位移,剩下多余的是需求交给下一轮让rv自行去推的
                // 所以这也是为什么header为什么-y更好也更恰当
                consumed[1] += y.toInt()
            }
            // ……这是一同推的阶段,还需求header进行一些scale和对外位移状况的显露,先不重视
        } else {
            // 推完了剩下就自己滑就好了
        }
    }
}

接着是下拉的进程,这块就没有上推时那么多状况了,直接开干。其间强调了一个概念:过度消费,尽管过度欠好,但是这时是咱们所期望的,由于fling也会带来滑动,假如太丝滑,滑动的阶段性就无法体现

else {
    // 下拉
    (target as? RecyclerView)?.let {
        val offsetY = it.computeVerticalScrollOffset()
        if (offsetY > 0) {
            // 说明本来现已滑动过了,由于前面的推进都是translationY改动,影响不到它本身
            // 这儿写了两个判别,但是没作处理,是由于…做处理的话就会太丝滑了,在fling状况下就会忽闪忽闪的
            // 所以咱们的思路是,过度消费,也就全全由rv自己先去滑,由于它最多也就滑到header消失时间的状况
            if (offsetY + dy < 0) {
                // 滑动的多了
            } else {
                // target自己能够处理
            }
        } else {
            if (target.translationY >= MAX_PEEK) {
                // 现已究竟了,不允许继续下拉了,你能够测验不加这个,看看效果Hh
                return
            }
            if (target.translationY - dy > MAX_PEEK) {
                // 拉过头就没了,这个同上,都是对PEEK_HEIGHT的兜底
                // 对了,关于这个PEEK需求设置多少,你能够经过rv的height-需求显露的height得出
                target.translationY = MAX_PEEK
                return
            }
            // header的translationY标志着它的状况
            if (child.translationY < 0) {
                // 需求把header一块滑下来
                if (child.translationY < dy) { // 由于带有方向,所以这两个都是负数,你需求了解成间隔会更加合适
                    // 滑动间隔不足以滑完header,那就一同动
                    child.translationY -= dy
                    target.translationY -= dy
                    consumed[1] += dy
                } else {
                    // 假如够滑完的话,header就需求固定住了,把剩下的translationY滑掉
                    // 这儿也是过度消费的思路,由于滑动间隔过剩了,但咱们期望先拉到固定贴合的状况先
                    // 而不是直接就下去了,太丝滑会不太好
                    // 不信邪的能够试试hhh
                    target.translationY -= child.translationY
                    child.translationY = 0f
                    consumed[1] += dy
                }
                // ……这是一同推的阶段,还需求header进行一些scale和对外位移状况的显露,先不重视
            } else {
                // header现已固定好了,那就自己滑好了
                target.translationY -= dy
                consumed[1] += dy
            }
        }
    }
}

把主体完结之后,header和rv的和谐现已完结了,接着完结一些其他的互动。前面在一同推的上下两处留下了注释,现在填进去吧

companion object {
    const val MAX_PEEK = 1300f
    const val ALPHA_SPEED = 3f * 100
    const val ANIM_DURATION = 300L
    const val SCALE_PERCENT = 0.15f
}
var listener: OrderStatusListener? = null
interface OrderStatusListener {
    fun onHeaderMove(percent: Float, title: String)
}
// 上推
val percent = -child.translationY / child.height
child.scaleX = 1 - percent * SCALE_PERCENT
listener?.onHeaderMove(percent, "配送中")
// 下拉
val percent = -child.translationY / child.height
child.scaleX = 1 - percent * SCALE_PERCENT
listener?.onHeaderMove(percent, "配送中")

还有一个header的透明度突变,为了避免onNestedPreScroll中的复杂度,将其抽离到onDependentViewChanged中,当然写在滑动的地方也是相同的。由于透明度改动是关于上推\下拉均需处理,所以干脆抽象为关于rv的移动

override fun onDependentViewChanged(parent: CoordinatorLayout, child: View, dependency: View): Boolean {
    if (child.translationY >= 0) {
        // header固定状况下
        // diff得出的便是rv顶部与header下鸿沟的相差间隔,也便是还差多少能够进入下一阶段
        // ALPHA_SPEED是一个阈值间隔,便是多少间隔开端进入突变状况
        val diff = dependency.translationY - child.height
        if (diff < ALPHA_SPEED && diff >= 0) {
            // 这儿转化为百分比
            child.alpha = (ALPHA_SPEED - diff) / ALPHA_SPEED
        } else if (diff >= ALPHA_SPEED) {
            child.alpha = 0f
        } else {
            child.alpha = 1f
        }
    }
    return true
}

做到了这一步,那剩下便是第一阶段滑动但未进入下一阶段时松手的问题了,这需求借助onStopNestedScroll的协助。依据滑动结束时的方位判别,需求履行何种动画,并符号动画状况

override fun onStopNestedScroll(coordinatorLayout: CoordinatorLayout, child: View, target: View, type: Int) {
    if (type == ViewCompat.TYPE_TOUCH) {
        // 仅处理touch,差异与not_touch,如fling
        super.onStopNestedScroll(coordinatorLayout, child, target, type)
        val childY = child.height.toFloat()
        val y = target.translationY
        if (y < MAX_PEEK && y > childY) {
            // 处于在中间状况中,即第一阶段状况
            // 这儿判别阈值设置了一半,也能够依据需求自行调整
            val mid = (MAX_PEEK + childY) / 2f
            if (y > mid) {
                // 回缩
                peekViewAnim(target, y, MAX_PEEK)
            } else {
                // 打开
                peekViewAnim(target, y, childY)
            }
        }
    }
}
private fun peekViewAnim(view: View, start: Float, end: Float) {
    if (animaState) {
        return
    }
    animaState = true
    val anim = ObjectAnimator.ofFloat(view, "translationY", start, end)
    anim.duration = ANIM_DURATION
    anim.addListener(this)
    anim.start()
}
private var animaState = false
override fun onAnimationStart(animation: Animator?) {
}
override fun onAnimationEnd(animation: Animator?) {
    animaState = false
}
override fun onAnimationCancel(animation: Animator?) {
}
override fun onAnimationRepeat(animation: Animator?) {
}

为什么需求符号动画状况,这是一个十分有意思的出题。由于当你履行动画时,尽管touch结束了,但如fling的not_touch还会触发,假如它继续走入onNestedPreScroll那就会发生画面的抖动,到这儿你现已能够运行试试了。那怎么进行屏蔽呢,巧用过度消费的理念

override fun onNestedPreScroll(coordinatorLayout: CoordinatorLayout, child: View, target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
    if (animaState) {
        // 动画正在履行中,一切滑动悉数吞掉
        consumed[1] += dy
        return
    }
}

是不是很简单。当然,思考的进程是曲折的,我一开端测验onStartNestedScroll关于动画状况return false,但效果并不抱负。由于不进行和谐滑动,不代表它本身不进行滑动,所以一开端咱们选择对一切笔直方向滑动全盘接纳进行干预

然后这样弥补了之后,仍是存在fling当快速甩动上滑时,会直接顺滑进入一同推进的状况,所以处理的思路仍是如出一辙,进行干预堵塞

if (type != ViewCompat.TYPE_TOUCH) {
    if (child.translationY >= 0) {
        // 假如顶部header还在,那就屏蔽fling
        consumed[1] += dy
        return
    }
}

终究的Behavior

class OrderStatusBehavior @JvmOverloads constructor(context: Context, attributeSet: AttributeSet? = null) : CoordinatorLayout.Behavior<View>(context, attributeSet), Animator.AnimatorListener {
    companion object {
        const val MAX_PEEK = 1300f
        const val ALPHA_SPEED = 3f * 100
        const val ANIM_DURATION = 300L
        const val SCALE_PERCENT = 0.15f
    }
    var listener: OrderStatusListener? = null
    override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View): Boolean {
        return dependency is RecyclerView
    }
    override fun onDependentViewChanged(parent: CoordinatorLayout, child: View, dependency: View): Boolean {
        if (child.translationY >= 0) {
            // header固定状况下
            // diff得出的便是rv顶部与header下鸿沟的相差间隔,也便是还差多少能够进入下一阶段
            // ALPHA_SPEED是一个阈值间隔,便是多少间隔开端进入突变状况
            val diff = dependency.translationY - child.height
            if (diff < ALPHA_SPEED && diff >= 0) {
                // 这儿转化为百分比
                child.alpha = (ALPHA_SPEED - diff) / ALPHA_SPEED
            } else if (diff >= ALPHA_SPEED) {
                child.alpha = 0f
            } else {
                child.alpha = 1f
            }
        }
        return true
    }
    // child是本身;target是和谐的方针view;dx\dy是x\y轴的滑动,向右为x轴u正方向,向下为y轴正方向,能够测验画图辅助了解
    // consumed是消费数组,[x,y]记载了x\y轴的滑动消费状况,假如需求消费,那就需求记载
    // 假如不消费的话,那么不论你怎样滑,Rv本身在后续环节还会本身滑动,由于没有消费完
    override fun onNestedPreScroll(coordinatorLayout: CoordinatorLayout, child: View, target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
        if (animaState) {
            // 动画正在履行中,一切滑动悉数吞掉
            consumed[1] += dy
            return
        }
        if (type != ViewCompat.TYPE_TOUCH) {
            if (child.translationY >= 0) {
                // 假如顶部header还在,那就屏蔽fling
                consumed[1] += dy
                return
            }
        }
        if (dy > 0) {
            // 上滑
            // 初始y为0,上推进程中会逐渐变到-height
            // 初始,y被设置为一个常量,MAX_PEEK = 1300f
            val y = target.translationY
            // 上推时,translationY会<0,所以以此判别header是否还固定在原位
            if (child.translationY >= 0 && y > child.height) {
                // 假如header固定时,那childY便是第一阶段中,rv的上界
                if (dy < y - child.height) {
                    // 滑动间隔不足以使得rv达到上界,即滑动间隔 < rv与header的之间的间隔
                    // 此刻,使得rv改动Y轴即可
                    target.translationY = y - dy
                    // 记载消费
                    consumed[1] += dy
                } else {
                    // 假如一次滑动量很大,那就先让rv抵达header处,并消费悉数
                    // 这儿其实是个简化,理论上 下一个分发阶段需求处理,这儿偷懒直接疏忽
                    target.translationY = child.height.toFloat()
                    consumed[1] += dy
                }
            } else {
                // 预备一同推
                if (y > 0) {
                    // 还没把header推完
                    if (y - dy >= 0) {
                        // 也还推不倒头,就一同动
                        // 这儿target.translationY -= dy是相同的,我是由于已然y都记载了,干脆用了
                        target.translationY = y - dy
                        child.translationY -= dy
                        consumed[1] += dy
                    } else {
                        // 先把剩下的推推完
                        // header其实也能够直接设置-child.height,当然这儿-y是异曲同工
                        child.translationY -= y
                        // rv推到头,便是y位移为0
                        target.translationY = 0f
                        // 这儿是重头戏啊,由于一同推的间隔是rv剩下的y位移,剩下多余的是需求交给下一轮让rv自行去推的
                        // 所以这也是为什么header为什么-y更好也更恰当
                        consumed[1] += y.toInt()
                    }
                    // ……这是一同推的阶段,还需求header进行一些scale和对外位移状况的显露,先不重视
                    val percent = -child.translationY / child.height
                    child.scaleX = 1 - percent * SCALE_PERCENT
//                    child.scaleY = 1 - percent
                    listener?.onHeaderMove(percent, "配送中")
                } else {
                    // 推完了剩下就自己滑就好了
                }
            }
        } else {
            // 下拉
            (target as? RecyclerView)?.let {
                val offsetY = it.computeVerticalScrollOffset()
                if (offsetY > 0) {
                    // 说明本来现已滑动过了,由于前面的推进都是translationY改动,影响不到它本身
                    // 这儿写了两个判别,但是没作处理,是由于…做处理的话就会太丝滑了,在fling状况下就会忽闪忽闪的
                    // 所以咱们的思路是,过度消费,也就全全由rv自己先去滑,由于它最多也就滑到header消失时间的状况
                    if (offsetY + dy < 0) {
                        // 滑动的多了
                    } else {
                        // target自己能够处理
                    }
                } else {
                    if (target.translationY >= MAX_PEEK) {
                        // 现已究竟了,不允许继续下拉了,你能够测验不加这个,看看效果Hh
                        return
                    }
                    if (target.translationY - dy > MAX_PEEK) {
                        // 拉过头就没了,这个同上,都是对PEEK_HEIGHT的兜底
                        // 对了,关于这个PEEK需求设置多少,你能够经过rv的height-需求显露的height得出
                        target.translationY = MAX_PEEK
                        return
                    }
                    // header的translationY标志着它的状况
                    if (child.translationY < 0) {
                        // 需求把header一块滑下来
                        if (child.translationY < dy) { // 由于带有方向,所以这两个都是负数,你需求了解成间隔会更加合适
                            // 滑动间隔不足以滑完header,那就一同动
                            child.translationY -= dy
                            target.translationY -= dy
                            consumed[1] += dy
                        } else {
                            // 假如够滑完的话,header就需求固定住了,把剩下的translationY滑掉
                            // 这儿也是过度消费的思路,由于滑动间隔过剩了,但咱们期望先拉到固定贴合的状况先
                            // 而不是直接就下去了,太丝滑会不太好
                            // 不信邪的能够试试hhh
                            target.translationY -= child.translationY
                            child.translationY = 0f
                            consumed[1] += dy
                        }
                        // ……这是一同推的阶段,还需求header进行一些scale和对外位移状况的显露,先不重视
                        val percent = -child.translationY / child.height
                        child.scaleX = 1 - percent * SCALE_PERCENT
//                        child.scaleY = 1 - percent
                        listener?.onHeaderMove(percent, "配送中")
                    } else {
                        // header现已固定好了,那就自己滑好了
                        target.translationY -= dy
                        consumed[1] += dy
                    }
                }
            }
        }
    }
    // child是本身,directTargetChild建议嵌套滑动的view,target也是
    override fun onStartNestedScroll(coordinatorLayout: CoordinatorLayout, child: View, directTargetChild: View, target: View, axes: Int, type: Int): Boolean {
        // 位运算,取vertical位,即笔直滑动
        return axes.and(ViewCompat.SCROLL_AXIS_VERTICAL) != 0
    }
    override fun onStopNestedScroll(coordinatorLayout: CoordinatorLayout, child: View, target: View, type: Int) {
        if (type == ViewCompat.TYPE_TOUCH) {
            // 仅处理touch,差异与not_touch,如fling
            super.onStopNestedScroll(coordinatorLayout, child, target, type)
            val childY = child.height.toFloat()
            val y = target.translationY
            if (y < MAX_PEEK && y > childY) {
                // 处于在中间状况中,即第一阶段状况
                // 这儿判别阈值设置了一半,也能够依据需求自行调整
                val mid = (MAX_PEEK + childY) / 2f
                if (y > mid) {
                    // 回缩
                    peekViewAnim(target, y, MAX_PEEK)
                } else {
                    // 打开
                    peekViewAnim(target, y, childY)
                }
            }
        }
    }
    private fun peekViewAnim(view: View, start: Float, end: Float) {
        if (animaState) {
            return
        }
        animaState = true
        val anim = ObjectAnimator.ofFloat(view, "translationY", start, end)
        anim.duration = ANIM_DURATION
        anim.addListener(this)
        anim.start()
    }
    private var animaState = false
    override fun onAnimationStart(animation: Animator?) {
    }
    override fun onAnimationEnd(animation: Animator?) {
        animaState = false
    }
    override fun onAnimationCancel(animation: Animator?) {
    }
    override fun onAnimationRepeat(animation: Animator?) {
    }
    interface OrderStatusListener {
        fun onHeaderMove(percent: Float, title: String)
    }
}