效果展现
前景提要:本文的阅览需求基于NestedScroll
嵌套滑动的基本知识有所了解,并且简单自定义过Behavior
效果定格图片
- 底下是一张背景图,状况栏作了沉溺效果,然后下面是一个RecyclerView
- 跟着手指向上推进,Rv(RecyclerView)也随之上推,顶部的header的透明度逐渐改动,直到RV的上鸿沟达到header的下鸿沟停止,header展现结束
- 继续向上推进,header跟着RV一同上推,并联动顶部TitleBar发生透明度改动,一同header发生了scale改动
- 当header没(mo)入TitleBar后,标题随之展现,RV继续滑动,假如向下拉的话,当RV翻滚量用完之后,会带着header一块回去,一如GIF中预览的效果一般
效果完结
思路剖析
其实在定格图片中,现已剖析了一部分了,接着从代码角度继续剖析一下设计思路:
- RV需求和header有滑动关系,那么抱负来说,他们最好是同级的,能够经过CoordinatorLayout来和谐,由于其间有同级嵌套滑动的分发能够使用
- TitleBar的联动依赖于header的推进状况,那其实能够依据header的移动,对外显露监听,使其能够随之改动,那它与(Rv+header).CoordinatorLayout是同级的,且是线性笔直排布的
- Rv的上滑能够拆分为三个阶段
-
从下方到header下鸿沟: 这个阶段Rv的高度在不断改动,假如真的一向改动高度的话那整个的测量就会变得十分频繁,并且手动setLayoutParams按滑动的频率可能发生抖动。
换个思路Rv的初始y轴在下方,然后逐渐回到0,所以能够用translationY来操作这个效果
-
和header一向上推: 这个阶段header需求依据Rv发生的滑动,作同步的改动,上滑1dp,两者一同上滑1dp,依据上一点的思路,这儿咱们也用translationY来操作,关于header上推便是从
0~ -height
的改动 - 当header没入后: 这个阶段便是单纯的本身滑动的进程了,没有任何压力
-
从下方到header下鸿沟: 这个阶段Rv的高度在不断改动,假如真的一向改动高度的话那整个的测量就会变得十分频繁,并且手动setLayoutParams按滑动的频率可能发生抖动。
- Rv的下滑也能够拆分为三个阶段
-
当header没入后: 这个阶段也是Rv本身滑动的阶段,所以能够经过
computeVerticalScrollOffset
判别本身是否有能够下滑的量,假如够用,那就自己滑就能够了,假如不够,那就需求将本身和header一同下推
-
header还未固定: 这时便是上面的第二种状况,header需求一向滑动到本身
translationY为0停止
- header固定后: 这时便是一开端的相反状况,调整Rv的Y轴就好了
-
当header没入后: 这个阶段也是Rv本身滑动的阶段,所以能够经过
- 由滑动剖析能够得出两个定论
- Rv的最大高度应该是从TitleBar以下的悉数
- header是初始固定在TitleBar下的
- 然后还有一些小细节需求注意
- 滑动阻尼,也便是认为滑动不到位,需求复位,假如到位就需求协助触达。其实便是在阶段1时,松手的状况,不期望Rv停留在该方位,而是只有两种状况:
打开|收缩
- Rv滑动到最下面就不能滑动了,类似于
BottomSheet的Peek
差不多 - ……
- 滑动阻尼,也便是认为滑动不到位,需求复位,假如到位就需求协助触达。其实便是在阶段1时,松手的状况,不期望Rv停留在该方位,而是只有两种状况:
布局
- 最外层是一个CoordinatorLayout,当然这个没必要,替换成FrameLayout也是相同的
- 背景图就一张铺满的图片
- TitleBar简单一点是个TextView,这儿固定高度了,由于下面需求MarginTop做的笔直排布,所以最外层改成LinearLayout也是能够的
- CoordinatorLayout来担任header和Rv的滑动和谐
- 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)
}
}