1.需求分析
从需求特点来看,这些功能都是比较常见的功能,普遍对应的解决方案如下
2.具体实现
2.1效果展示
CoordinatorLayout :
CoordinatorLayout.Behavior:Behavior 是 CoordinatorLayout 的一个抽象内部类
主要是通过为 CoordinatorLayout 设置 CoordinatorLayout.Behavior ,在 CoordinatorLayout.Behavior 的一系列回调方法中,操作 CoordinatorLayout 中包含的子 View ,实现想要的交互效果
嵌套滚动两种情况下( 抬起时无甩动动作 及 抬起时有甩动动作 ),滚动相关回调方法触发顺序
强调上述内容,是为了更好的处理手指快速滑动时,CoordinatorLayout 内的子 View 交互
2.2布局分析
XML代码如下,将各部分分别抽成成一个 View(点击跳转查看源码:activity_shop_details.xml、ShopDiscountLayout、ShopContentLayout、ShopTitleLayout、ShopPriceLayout)
<?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"
android:id="@+id/cl_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white">
<!--顶部 店铺信息+优惠活动 内容-->
<com.ziwenl.meituan_detail.ui.shop.ShopDiscountLayout
android:id="@+id/layout_discount"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!--中下部 点菜/评论/商家 内容-->
<com.ziwenl.meituan_detail.ui.shop.ShopContentLayout
android:id="@+id/layout_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="@dimen/top_min_height"
app:layout_behavior=".ui.shop.ShopContentBehavior" />
<!--顶部 标题栏 内容-->
<com.ziwenl.meituan_detail.ui.shop.ShopTitleLayout
android:id="@+id/layout_title"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<!--底部 满减神器、满减优惠、价格费用-->
<com.ziwenl.meituan_detail.ui.shop.ShopPriceLayout
android:id="@+id/layout_price"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
2.3代码分析
2.3.1自定义 CoordinatorLayout.Behavior
自定义 ShopContentBehavior (点击查看源码) 继承于 CoordinatorLayout.Behavior ,并将 ShopContentLayout 视图设置为其使用者
class ShopContentBehavior(private val context: Context, attrs: AttributeSet?) :
CoordinatorLayout.Behavior<ShopContentLayout>(context, attrs) {
......
}
声明 xml 布局中 CoordinatorLayout 内需要根据滚动进行交互的子 view,并分别在 onLayoutChild、layoutDependsOn 方法中得到它们的实例
/**
* 顶部标题栏:返回、搜索、收藏、更多
*/
private lateinit var mShopTitleLayoutView: ShopTitleLayout
/**
* 中上部分店铺信息:配送时间、描述、评分、优惠及公告
*/
private lateinit var mShopDiscountLayoutView: ShopDiscountLayout
/**
* 中下部分:点菜(广告、菜单)、评价、商家
*/
private lateinit var mShopContentLayoutView: ShopContentLayout
/**
* 底部价格:满减神器、满减优惠、选中价格
*/
private lateinit var mShopPriceLayoutView: ShopPriceLayout
override fun onLayoutChild(
parent: CoordinatorLayout,
child: ShopContentLayout,
layoutDirection: Int
): Boolean {
if (!this::mShopContentLayoutView.isInitialized) {
mShopContentLayoutView = child
......
}
return super.onLayoutChild(parent, child, layoutDirection)
}
override fun layoutDependsOn(
parent: CoordinatorLayout,
child: ShopContentLayout,
dependency: View
): Boolean {
when (dependency.id) {
R.id.layout_title -> mShopTitleLayoutView = dependency as ShopTitleLayout
R.id.layout_discount -> mShopDiscountLayoutView = dependency as ShopDiscountLayout
R.id.layout_price -> mShopPriceLayoutView = dependency as ShopPriceLayout
else -> return false
}
return true
}
解决嵌套滚动冲突问题:在 onNestedPreScroll 方法中,根据子 View 是否可以滚动的回调方法判断是否为内部 View 设置偏移
实现滚动过程中,各部分子 View 随着滚动程度进行相应变化:主要是在 onNestedPreScroll 方法中,根据滚动距离对内部 View 设置属性(透明度、偏移量、缩放等),实现嵌套滚动交互效果,配合工具类 ViewState(点击查看源码) 实现(记录 View 的起始状态和目标状态及对应状态下的属性,再根据滚动进度动态设置目标 View 的相关属性,达到指定 View 样式随滚动程度变化的目的)
/**
* 嵌套滑动进行中,要监听的子 View 将要滑动,滑动事件即将被消费(但最终被谁消费,可以通过代码控制)
* @param type = ViewCompat.TYPE_TOUCH 表示是触摸引起的滚动 = ViewCompat.TYPE_NON_TOUCH 表示是触摸后的惯性引起的滚动
*/
override fun onNestedPreScroll(
coordinatorLayout: CoordinatorLayout,
child: ShopContentLayout,
target: View,
dx: Int,
dy: Int,
consumed: IntArray,
type: Int
) {
if (mIsScrollToHideFood) {
consumed[1] = dy
return // scroller 滑动中.. do nothing
}
mVerticalPagingTouch += dy
if (mVpMain.isScrollable && abs(mVerticalPagingTouch) > mPagingTouchSlop) {
mVpMain.isScrollable = false // 屏蔽 pager横向滑动干扰
}
if (type == ViewCompat.TYPE_NON_TOUCH && mIsFlingAndDown) {
//当处于惯性滑动时,有触摸动作进入,屏蔽惯性滑动,以防止滚动错乱
consumed[1] = dy
return
}
if (type == ViewCompat.TYPE_NON_TOUCH) {
mIsScrollToFullFood = true
}
mHorizontalPagingTouch += dx
if ((child.translationY < 0 || (child.translationY == 0F && dy > 0))
&& !child.getScrollableView().canScrollVertically(-1)
) {
val effect = mShopTitleLayoutView.effectByOffset(dy)
val transY = -mSimpleTopDistance * effect
mShopDiscountLayoutView.translationY = transY
if (transY != child.translationY) {
child.translationY = transY
consumed[1] = dy
}
} else if ((child.translationY > 0 || (child.translationY == 0F && dy < 0))
&& !child.getScrollableView().canScrollVertically(-1)
) {
if (mIsScrollToFullFood) {
child.translationY = 0F
} else {
child.translationY -= dy
mShopDiscountLayoutView.effectByOffset(child.translationY)
mShopPriceLayoutView.effectByOffset(child.translationY)
}
consumed[1] = dy
} else {
//折叠状态
if (child.getRootScrollView() != null
//这个判断是防止按着bannerView滚动时导致scrollView滚动速度翻倍
&& (child.getScrollableView() is RecyclerView)
) {
if (dy > 0) {
child.getRootScrollView()!!.scrollY += dy
}
}
}
}
实现点击指定 View 展开/收缩布局:同样是通过工具类 ViewState(点击查看源码)内的拓展函数 Any?.statesChangeByAnimation () 借由属性动画去更新指定 View 的属性
// ViewState 内提供的拓展函数
/**
* 通过属性动画更新指定 View 状态
*/
fun Any?.statesChangeByAnimation(
views: Array<View>,
startTag: Int,
endTag: Int,
start: Float = 0F,
end: Float = 1F,
updateCallback: AnimationUpdateListener? = null,
updateStateListener: AnimatorListenerAdapter? = null,
duration: Long = 400L,
startDelay: Long = 0L
): ValueAnimator {
return ValueAnimator.ofFloat(start, end).apply {
this.startDelay = startDelay
this.duration = duration
interpolator = AccelerateDecelerateInterpolator()
addUpdateListener { animation ->
val p = animation.animatedValue as Float
updateCallback?.onAnimationUpdate(startTag, endTag, p)
for (it in views) it.stateRefresh(startTag, endTag, animation.animatedValue as Float)
}
updateStateListener?.let { addListener(it) }
start()
}
}
// ShopDiscountLayout 中点击展开和收缩时的调用示例
/**
* 展开/收缩当前布局
*/
fun switch(
expanded: Boolean,
byScrollerSlide: Boolean = false
) {
if (mIsExpanded == expanded) {
return
}
sv_main.scrollTo(0, 0)
mIsExpanded = expanded // 目标
val start = effected
val end = if (expanded) 1F else 0F
statesChangeByAnimation(
animViews(), R.id.viewStateStart, R.id.viewStateEnd, start, end,
null, if (!byScrollerSlide) internalAnimListener else null, 500
)
}
2.3.2自定义 RecyclerView.ItemDecoration
通过自定义 RecyclerView.ItemDecoration 实现列表 Item 吸顶过渡替换效果(点击跳转查看源码)
3.最后
要通过 CoordinatorLayout + 自定义 Behavior 实现多重嵌套滚动交互效果,主要还是要了解自定义 Behavior 中嵌套滚动时触发的相关方法的具体调用时机和作用,然后通过为子 View 去设置相关 View 属性,从而实现滚动交互效果。该 Demo 都是业务代码,也没什么需要细讲的地方,具体实现可参考查阅源码。
- 源码及 Demo 地址:https://github.com/ziwenL/MeituanDetailDemo
- 实现过程中借鉴参考的博客:https://blog.csdn.net/bfbx5173/article/details/80624322
- 如有更好的见解或建议,欢迎留言