1.需求分析
需求特點(diǎn)
- 多重嵌套滾動(dòng)
- 標(biāo)題欄 內(nèi)容跟隨滾動(dòng)變化
- 店鋪信息(店鋪名、描述、評(píng)分、優(yōu)惠信息、公告等)滾動(dòng)時(shí) 折疊隱藏 或 完全展開(kāi)
- “點(diǎn)菜” 、“評(píng)價(jià)” 及 “商家” 欄滾動(dòng)時(shí) 懸停吸頂
- “點(diǎn)菜” 頁(yè)面內(nèi),列表懸停 效果及菜品 列表 Item 吸頂過(guò)渡替換
- 底部滿減神器、滿減優(yōu)惠、選中價(jià)格等內(nèi)容 隨店鋪信息展開(kāi)漸變隱藏
從需求特點(diǎn)來(lái)看,這些功能都是比較常見(jiàn)的功能,普遍對(duì)應(yīng)的解決方案如下
功能分析
- 多重嵌套滾動(dòng):
1.事件分發(fā)
2.NestedScroll嵌套滑動(dòng)機(jī)制
3.CoordinatorLayout 與 Behavior 配合實(shí)現(xiàn)- 內(nèi)容跟隨滾動(dòng)變化
通過(guò)監(jiān)聽(tīng)滾動(dòng)事件,配合屬性動(dòng)畫(huà)實(shí)現(xiàn)- 懸停吸頂
1.繪制兩個(gè)相同的 View,AView隨布局滾動(dòng),BView固定在布局某處,再根據(jù)滾動(dòng)距離,動(dòng)態(tài)隱藏或顯示 BView,造成吸頂假象
2.CoordinatorLayout 與 Behavior 配合實(shí)現(xiàn)- 列表 Item 吸頂過(guò)渡替換:
通過(guò)自定義 RecyclerView.Decoration 實(shí)現(xiàn)
2.具體實(shí)現(xiàn)
2.1效果展示
?主要通過(guò) CoordinatorLayout + 自定義 Behavior 的方式實(shí)現(xiàn)
CoordinatorLayout :
?
CoordinatorLayout 是功能更強(qiáng)大的 FrameLayout
CoordinatorLayout 適用于兩個(gè)主要用例:
作為頂層應(yīng)用程序裝飾或 chrome 布局
作為與一個(gè)或多個(gè)子視圖進(jìn)行特定交互的容器
通過(guò)指定 BehaviorsCoordinatorLayout 的子視圖,您可以在單個(gè)父視圖中提供許多不同的交互,并且這些視圖也可以彼此交互。當(dāng)視圖類(lèi)用作帶 CoordinatorLayout.DefaultBehavior 注釋的 CoordinatorLayout 的子級(jí)時(shí),可以指定默認(rèn)行為 。
CoordinatorLayout.Behavior:Behavior 是 CoordinatorLayout 的一個(gè)抽象內(nèi)部類(lèi)
?
互動(dòng)行為插件,用在位于 CoordinatorLayout 中的子視圖上
行為實(shí)現(xiàn)了用戶可以在子視圖上進(jìn)行的一個(gè)或多個(gè)交互。這些交互可能包括拖動(dòng),滑動(dòng),甩動(dòng)或任何其他手勢(shì)
主要是通過(guò)為 CoordinatorLayout 設(shè)置 CoordinatorLayout.Behavior ,在 CoordinatorLayout.Behavior 的一系列回調(diào)方法中,操作 CoordinatorLayout 中包含的子 View ,實(shí)現(xiàn)想要的交互效果
- 為 CoordinatorLayout 設(shè)置 Behavior(有三種方式):
1.在 xml 布局通過(guò) app:layout_behavior 來(lái)指定
2.在代碼中,通過(guò) child.getLayoutParams().setBehavior() 來(lái)指定
3.在目標(biāo) childView 類(lèi)上,通過(guò) @DefaultBehavior 來(lái)指定
(本文采用最常用的方式1進(jìn)行設(shè)置)- Behavior<V> 包含的主要方法有:
// 確定使用 Behavior 的 View 位置
onLayoutChild ()
// 確定使用 Behavior 的 View 要依賴的 View ,可以在此處得到 CoordinatorLayout 下的其它子 View
layoutDependsOn ()
// 當(dāng)被依賴的 View 狀態(tài)改變時(shí)回調(diào)
onDependentViewChanged ()
// 嵌套滑動(dòng)開(kāi)始,確定 Behavior 是否要監(jiān)聽(tīng)此次事件
onStartNestedScroll ()
// 嵌套滑動(dòng)進(jìn)行中,要監(jiān)聽(tīng)的子 View 將要滑動(dòng),滑動(dòng)事件即將被消費(fèi)(但最終被誰(shuí)消費(fèi),可以通過(guò)代碼控制)
onNestedPreScroll ()
// 接受嵌套滾動(dòng)
onNestedScrollAccepted ()
// 要監(jiān)聽(tīng)的子View即將慣性滑動(dòng)(開(kāi)始非實(shí)際觸摸的慣性滑動(dòng))
onNestedPreFling ()
// 嵌套滑動(dòng)結(jié)束
onStopNestedScroll ()- 與滾動(dòng)動(dòng)作相關(guān)的方法回調(diào)中,都有一個(gè) @NestedScrollType type : Int 參數(shù),像 onStartNestedScroll () 、 onNestedPreScroll () 、 onNestedScrollAccepted () 、 onStopNestedScroll () 等,該 type 是用來(lái)區(qū)分當(dāng)前的滾動(dòng)是由實(shí)際觸摸引起的,還是由觸摸結(jié)束后的慣性引起的。其中:
當(dāng) type = ViewCompat.TYPE_TOUCH 時(shí),表示滾動(dòng)是由實(shí)際觸摸引起的(正在觸摸中)
當(dāng) type = ViewCompat.TYPE_NON_TOUCH 時(shí),表示滾動(dòng)是由慣性引起的(觸摸已經(jīng)結(jié)束,甩動(dòng)動(dòng)作帶動(dòng)的滑動(dòng))
嵌套滾動(dòng)兩種情況下( 抬起時(shí)無(wú)甩動(dòng)動(dòng)作 及 抬起時(shí)有甩動(dòng)動(dòng)作 ),滾動(dòng)相關(guān)回調(diào)方法觸發(fā)順序
?
?
手指抬起時(shí)有甩動(dòng)動(dòng)作引起的慣性嵌套滾動(dòng),是在執(zhí)行完實(shí)際觸摸引起的嵌套滾動(dòng)后執(zhí)行的。也就是上面示意圖1執(zhí)行完之后才會(huì)執(zhí)行示意圖2。
強(qiáng)調(diào)上述內(nèi)容,是為了更好的處理手指快速滑動(dòng)時(shí),CoordinatorLayout 內(nèi)的子 View 交互
2.2布局分析
?
XML代碼如下,將各部分分別抽成成一個(gè) View(點(diǎn)擊跳轉(zhuǎn)查看源碼: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">
<!--頂部 店鋪信息+優(yōu)惠活動(dòng) 內(nèi)容-->
<com.ziwenl.meituan_detail.ui.shop.ShopDiscountLayout
android:id="@+id/layout_discount"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!--中下部 點(diǎn)菜/評(píng)論/商家 內(nèi)容-->
<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" />
<!--頂部 標(biāo)題欄 內(nèi)容-->
<com.ziwenl.meituan_detail.ui.shop.ShopTitleLayout
android:id="@+id/layout_title"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<!--底部 滿減神器、滿減優(yōu)惠、價(jià)格費(fèi)用-->
<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 (點(diǎn)擊查看源碼) 繼承于 CoordinatorLayout.Behavior ,并將 ShopContentLayout 視圖設(shè)置為其使用者
class ShopContentBehavior(private val context: Context, attrs: AttributeSet?) :
CoordinatorLayout.Behavior<ShopContentLayout>(context, attrs) {
......
}
聲明 xml 布局中 CoordinatorLayout 內(nèi)需要根據(jù)滾動(dòng)進(jìn)行交互的子 view,并分別在 onLayoutChild、layoutDependsOn 方法中得到它們的實(shí)例
/**
* 頂部標(biāo)題欄:返回、搜索、收藏、更多
*/
private lateinit var mShopTitleLayoutView: ShopTitleLayout
/**
* 中上部分店鋪信息:配送時(shí)間、描述、評(píng)分、優(yōu)惠及公告
*/
private lateinit var mShopDiscountLayoutView: ShopDiscountLayout
/**
* 中下部分:點(diǎn)菜(廣告、菜單)、評(píng)價(jià)、商家
*/
private lateinit var mShopContentLayoutView: ShopContentLayout
/**
* 底部?jī)r(jià)格:滿減神器、滿減優(yōu)惠、選中價(jià)格
*/
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
}
解決嵌套滾動(dòng)沖突問(wèn)題:在 onNestedPreScroll 方法中,根據(jù)子 View 是否可以滾動(dòng)的回調(diào)方法判斷是否為內(nèi)部 View 設(shè)置偏移
實(shí)現(xiàn)滾動(dòng)過(guò)程中,各部分子 View 隨著滾動(dòng)程度進(jìn)行相應(yīng)變化:主要是在 onNestedPreScroll 方法中,根據(jù)滾動(dòng)距離對(duì)內(nèi)部 View 設(shè)置屬性(透明度、偏移量、縮放等),實(shí)現(xiàn)嵌套滾動(dòng)交互效果,配合工具類(lèi) ViewState(點(diǎn)擊查看源碼) 實(shí)現(xiàn)(記錄 View 的起始狀態(tài)和目標(biāo)狀態(tài)及對(duì)應(yīng)狀態(tài)下的屬性,再根據(jù)滾動(dòng)進(jìn)度動(dòng)態(tài)設(shè)置目標(biāo) View 的相關(guān)屬性,達(dá)到指定 View 樣式隨滾動(dòng)程度變化的目的)
/**
* 嵌套滑動(dòng)進(jìn)行中,要監(jiān)聽(tīng)的子 View 將要滑動(dòng),滑動(dòng)事件即將被消費(fèi)(但最終被誰(shuí)消費(fèi),可以通過(guò)代碼控制)
* @param type = ViewCompat.TYPE_TOUCH 表示是觸摸引起的滾動(dòng) = ViewCompat.TYPE_NON_TOUCH 表示是觸摸后的慣性引起的滾動(dòng)
*/
override fun onNestedPreScroll(
coordinatorLayout: CoordinatorLayout,
child: ShopContentLayout,
target: View,
dx: Int,
dy: Int,
consumed: IntArray,
type: Int
) {
if (mIsScrollToHideFood) {
consumed[1] = dy
return // scroller 滑動(dòng)中.. do nothing
}
mVerticalPagingTouch += dy
if (mVpMain.isScrollable && abs(mVerticalPagingTouch) > mPagingTouchSlop) {
mVpMain.isScrollable = false // 屏蔽 pager橫向滑動(dòng)干擾
}
if (type == ViewCompat.TYPE_NON_TOUCH && mIsFlingAndDown) {
//當(dāng)處于慣性滑動(dòng)時(shí),有觸摸動(dòng)作進(jìn)入,屏蔽慣性滑動(dòng),以防止?jié)L動(dòng)錯(cuò)亂
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 {
//折疊狀態(tài)
if (child.getRootScrollView() != null
//這個(gè)判斷是防止按著bannerView滾動(dòng)時(shí)導(dǎo)致scrollView滾動(dòng)速度翻倍
&& (child.getScrollableView() is RecyclerView)
) {
if (dy > 0) {
child.getRootScrollView()!!.scrollY += dy
}
}
}
}
實(shí)現(xiàn)點(diǎn)擊指定 View 展開(kāi)/收縮布局:同樣是通過(guò)工具類(lèi) ViewState(點(diǎn)擊查看源碼)內(nèi)的拓展函數(shù) Any?.statesChangeByAnimation () 借由屬性動(dòng)畫(huà)去更新指定 View 的屬性
// ViewState 內(nèi)提供的拓展函數(shù)
/**
* 通過(guò)屬性動(dòng)畫(huà)更新指定 View 狀態(tài)
*/
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 中點(diǎn)擊展開(kāi)和收縮時(shí)的調(diào)用示例
/**
* 展開(kāi)/收縮當(dāng)前布局
*/
fun switch(
expanded: Boolean,
byScrollerSlide: Boolean = false
) {
if (mIsExpanded == expanded) {
return
}
sv_main.scrollTo(0, 0)
mIsExpanded = expanded // 目標(biāo)
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
通過(guò)自定義 RecyclerView.ItemDecoration 實(shí)現(xiàn)列表 Item 吸頂過(guò)渡替換效果(點(diǎn)擊跳轉(zhuǎn)查看源碼)
3.最后
要通過(guò) CoordinatorLayout + 自定義 Behavior 實(shí)現(xiàn)多重嵌套滾動(dòng)交互效果,主要還是要了解自定義 Behavior 中嵌套滾動(dòng)時(shí)觸發(fā)的相關(guān)方法的具體調(diào)用時(shí)機(jī)和作用,然后通過(guò)為子 View 去設(shè)置相關(guān) View 屬性,從而實(shí)現(xiàn)滾動(dòng)交互效果。該 Demo 都是業(yè)務(wù)代碼,也沒(méi)什么需要細(xì)講的地方,具體實(shí)現(xiàn)可參考查閱源碼。
- 源碼及 Demo 地址:https://github.com/ziwenL/MeituanDetailDemo
- 實(shí)現(xiàn)過(guò)程中借鑒參考的博客:https://blog.csdn.net/bfbx5173/article/details/80624322
- 如有更好的見(jiàn)解或建議,歡迎留言