前言
在這篇文章中,我將會(huì)介紹在Compose中如何完成觸摸事件和嵌套滑動(dòng)的處理。
1 Compose中的觸摸事件
在原生的View體系中,常見的觸摸事件有:ACTION_DOWN、ACTION_MOVE、ACTION_UP,當(dāng)手指按下時(shí),會(huì)遍歷View樹型結(jié)構(gòu)拿到mFirstTouchTarget,以此將后續(xù)的MOVE事件和UP事件都交給這個(gè)組件消費(fèi),在View中消費(fèi)事件是通過onTouchEvent方法處理的。
如果我們想要對事件進(jìn)行攔截,通常會(huì)重寫onInterceptTouchEvent,根據(jù)具體的業(yè)務(wù)場景來判斷是否攔截事件,以及在嵌套的滑動(dòng)組件中,對于事件沖突的處理尤為重要,所以本節(jié)我將會(huì)介紹在Compose中如何完成觸摸事件的處理。
1.1 Compose中的點(diǎn)擊事件
在Compose當(dāng)中的Modifier提供了clickable函數(shù)用于處理點(diǎn)擊事件;
Text(text = "點(diǎn)擊我", Modifier.clickable {
Log.d(TAG, "TestTouchEvent: 單擊事件")
})
而對于雙擊,長按,則是另一個(gè)函數(shù)combinedClickable來完成。
fun Modifier.combinedClickable(
enabled: Boolean = true,
onClickLabel: String? = null,
role: Role? = null,
onLongClickLabel: String? = null,
onLongClick: (() -> Unit)? = null,
onDoubleClick: (() -> Unit)? = null,
onClick: () -> Unit
){
// ......
}
其實(shí)從源碼中可以看到(這里我不帶大家看了,可以自行查看,很簡單的源碼),像點(diǎn)擊手勢的處理,是通過Modifier.pointInput來處理的。
Text(text = "點(diǎn)擊我", Modifier.pointerInput(Unit) {
awaitEachGesture {
val event = awaitPointerEvent()
when(event.type){
PointerEventType.Press ->{
Log.d(TAG, "TestTouchEvent: Press")
}
PointerEventType.Move->{
Log.d(TAG, "TestTouchEvent: Move")
}
PointerEventType.Exit->{
Log.d(TAG, "TestTouchEvent: Exit")
}
PointerEventType.Scroll->{
Log.d(TAG, "TestTouchEvent: Scroll")
}
}
}
})
Modifier.pointInput算是Compose對于觸摸反饋?zhàn)畹讓拥奶幚砹?,通過awaitPointerEvent可以獲取用戶輸入的事件,根據(jù)類型判斷是Press(點(diǎn)擊)、Move(移動(dòng))、Scroll(滑動(dòng))等事件類型。
Text(text = "點(diǎn)擊我", Modifier.pointerInput(Unit) {
detectTapGestures {
Log.d(TAG, "TestTouchEvent: 點(diǎn)擊了")
}
})
或者直接在pointInputScope中通過detectTapGestures來監(jiān)測點(diǎn)擊事件。
這是我之前在介紹Compose時(shí),已經(jīng)使用過點(diǎn)擊事件,這里是簡單的對點(diǎn)擊事件的底層實(shí)現(xiàn)做了介紹,接下來我要介紹一下滑動(dòng)事件。
1.2 Compose中的滑動(dòng)事件 - draggable
在Compose中,提供了draggable和scrollable函數(shù),用于處理滑動(dòng)事件,先看下draggable函數(shù)。
fun Modifier.draggable(
state: DraggableState,
orientation: Orientation,
enabled: Boolean = true,
interactionSource: MutableInteractionSource? = null,
startDragImmediately: Boolean = false,
onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit = {},
onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit = {},
reverseDirection: Boolean = false
): Modifier {
// ......
}
在draggable函數(shù)中,有兩個(gè)必填的值,state和orientation,在滑動(dòng)組件中,例如LazyColumn、Pager等,必須要有一個(gè)state對象。
1.2.1 LazyColumn中的state對象
在LazyColumn中,state默認(rèn)是執(zhí)行了rememberLazyListState函數(shù),利用其返回值,也就是得到了LazyListState對象。
@Composable
fun TestScrollableState() {
val list = remember {
mutableStateListOf("A", "B", "C", "D", "E", "F")
}
val scope = rememberCoroutineScope()
val state = rememberLazyListState()
LazyColumn(state = state) {
items(list) {
Text(
text = "當(dāng)前字母:$it",
Modifier
.fillMaxWidth()
.height(200.dp)
)
}
}
Button(onClick = {
scope.launch {
state.scrollToItem(list.size - 1)
}
}) {
Text(text = "定位")
}
}
那么這個(gè)state是干什么用的呢?其實(shí)就是為了用來處理列表的滑動(dòng),或者監(jiān)聽列表滑動(dòng)。 我們常見的一個(gè)需求就是,當(dāng)進(jìn)到某個(gè)頁面時(shí),需要定位到列表中的某個(gè)元素,那么如果使用RecyclerView,那么可以通過scrollToPosition(index)來完成。
但是Compose是聲明式的UI,無法拿到組件的實(shí)例對象,因此就是通過state來完成滑動(dòng)的控制,例如點(diǎn)擊按鈕滑動(dòng)到列表最后一位,那么就調(diào)用state的scrollToItem函數(shù)來完成。
1.2.2 draggable函數(shù)分析
再回到draggable函數(shù),除了state之外,還需要設(shè)置orientation,就是滑動(dòng)的方向。因?yàn)?code>draggable是監(jiān)聽一維方向的滑動(dòng), 因此只能拿到x軸或者y軸方向上滑動(dòng)偏移量。
@Composable
fun TestDraggable() {
Text(
text = "懸浮窗",
Modifier
.size(200.dp)
.background(Color.Blue)
.draggable(rememberDraggableState {
Log.d(TAG, "TestDraggable: $it")
}, Orientation.Horizontal)
)
}
因?yàn)?code>draggable也需要一個(gè)state,一般情況下都是會(huì)使用rememberDraggableState來生成一個(gè)DraggableState,我們看其回調(diào)值其實(shí)就是一個(gè)float類型的參數(shù),意味著draggable就是用來檢測一維方向上的偏移量。
@Composable
fun rememberDraggableState(onDelta: (Float) -> Unit): DraggableState {
val onDeltaState = rememberUpdatedState(onDelta)
return remember { DraggableState { onDeltaState.value.invoke(it) } }
}
還有一個(gè)參數(shù),不是必填項(xiàng),就是interactionSource,能夠反映此時(shí)的用戶與界面的交互狀態(tài),假設(shè)有個(gè)需求,當(dāng)組件拖拽的時(shí)候,需要顯示某個(gè)文案;停止拖拽之后顯示另一個(gè)文案。
@Composable
fun TestDraggable() {
val interaction = remember {
MutableInteractionSource()
}
Column {
Text(
text = "懸浮窗",
Modifier
.size(200.dp)
.background(Color.Blue)
.draggable(rememberDraggableState {
Log.d(TAG, "TestDraggable: $it")
}, Orientation.Horizontal, interactionSource = interaction)
)
val isDragged by interaction.collectIsDraggedAsState()
if (isDragged) {
Text(text = "正在拖拽")
} else {
Text(text = "靜止?fàn)顟B(tài)中")
}
}
}
可以將MutableInteractionSource轉(zhuǎn)換為可監(jiān)聽的State,當(dāng)拖動(dòng)狀態(tài)發(fā)生變化時(shí),可以監(jiān)聽到。
1.2.3 通過draggable實(shí)現(xiàn)拖拽效果
在前面我提到,所有的滑動(dòng)組件都會(huì)使用到state,像draggable中使用到的rememberDraggableState可以拿到一維方向的偏移量,那么肯定能夠在偏移量上做文章,實(shí)現(xiàn)拖拽效果。
@Composable
fun TestDraggable() {
val interaction = remember {
MutableInteractionSource()
}
// x軸的滑動(dòng)距離
var scrollX = remember {
mutableStateOf(0)
}
Column {
Text(
text = "懸浮窗",
Modifier
.draggable(rememberDraggableState {
// 發(fā)起重組
scrollX.value += it.toInt()
}, Orientation.Horizontal, interactionSource = interaction)
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints = constraints)
layout(placeable.width, placeable.height) {
//擺放位置
placeable.placeRelative(IntOffset(scrollX.value, 0))
}
}
.size(200.dp)
.background(Color.Blue)
)
val isDragged by interaction.collectIsDraggedAsState()
if (isDragged) {
Text(text = "正在拖拽")
} else {
Text(text = "靜止?fàn)顟B(tài)中")
}
}
}
例如記錄一個(gè)scrollX,用于記錄水平方向的偏移量,這個(gè)值是累加的,每次拖拽都會(huì)觸發(fā)重組重新測量布局,在Modifier.layout函數(shù)中進(jìn)行布局的重新擺放邏輯。
1.3 Compose中的滑動(dòng)事件 - scrollable
這一節(jié)將會(huì)介紹scrollable的使用,其實(shí)如果看過scrollable的源碼,會(huì)發(fā)現(xiàn)它在底層還是通過draggable實(shí)現(xiàn)的。
@ExperimentalFoundationApi
fun Modifier.scrollable(
state: ScrollableState,
orientation: Orientation,
overscrollEffect: OverscrollEffect?,
enabled: Boolean = true,
reverseDirection: Boolean = false,
flingBehavior: FlingBehavior? = null,
interactionSource: MutableInteractionSource? = null
): Modifier {
// ......
}
那么為什么不統(tǒng)一用draggable,而是要單獨(dú)加了一個(gè)scrollable?原因就是通過scrollable要做一些精細(xì)化的效果處理,例如:慣性滑動(dòng)、嵌套滑動(dòng)nestscroll、邊界回彈等。
其實(shí)很好理解,draggable從字面意思上看就是拖拽,雖然滑動(dòng)也是拖拽的一種,但是并不意味著所有的拖拽場景都需要所謂的慣性滑動(dòng)、嵌套滑動(dòng),例如設(shè)置中的進(jìn)度條。
來看下使用,我下面是用scrollable實(shí)現(xiàn)了組件的橫向移動(dòng)能力,
@Composable
fun TestScrollable() {
val currentX = remember {
mutableStateOf(0)
}
Text(
text = "懸浮窗",
Modifier
.offset {
IntOffset(currentX.value, 0)
}
.scrollable(rememberScrollableState {
currentX.value += it.toInt()
it
}, Orientation.Horizontal)
.size(200.dp)
.background(Color.Blue)
)
}
和draggable一樣,scrollable也需要一個(gè)state,Compose給提供好了就是rememberScrollableState函數(shù)。
@Composable
fun rememberScrollableState(consumeScrollDelta: (Float) -> Float): ScrollableState {
val lambdaState = rememberUpdatedState(consumeScrollDelta)
return remember { ScrollableState { lambdaState.value.invoke(it) } }
}
但是需要注意的是,和rememberDraggableState不同的是,它需要一個(gè)返回值,這個(gè)返回值代表當(dāng)前橫滑的距離被某個(gè)組件消費(fèi)了多少。 以便將剩余的滑動(dòng)距離交給父容器或者子組件消費(fèi),這就是嵌套滑動(dòng)的原理所在。
除此之外,scrollable中還提供了overscrollEffect參數(shù),用于處理觸邊后的回彈效果,Compose中提供了默認(rèn)的實(shí)現(xiàn)ScrollableDefaults.overscrollEffect()。
flingBehavior則是用于處理慣性滑動(dòng),默認(rèn)可以不傳值,在底層使用默認(rèn)值。
// 如果沒有特殊的慣性滑動(dòng)需求,底層使用默認(rèn)值。
val fling = flingBehavior ?: ScrollableDefaults.flingBehavior()
所以scrollable在draggable的基礎(chǔ)之上,增加了幾種滑動(dòng)效果的邏輯處理。
1.4 Compose的二維滑動(dòng)
前面我在介紹draggable和scrollable的時(shí)候說過,他們只支持一維方向的滑動(dòng),所以需要設(shè)置orientation屬性,如果想要監(jiān)聽二維的滑動(dòng),Compose沒有提供直接使用的API,需要Modifier.pointerInput來配合完成。
在1.1 小節(jié)中,我介紹點(diǎn)擊事件的時(shí)候,提到過detectTapGestures可以從底層監(jiān)聽點(diǎn)擊事件,那么如果想要監(jiān)聽二維滑動(dòng),那么可以通過detectDragGestures來完成。
Text(text = "二維滑動(dòng)",
Modifier
.size(200.dp)
.background(Color.Blue)
.pointerInput(Unit){
detectDragGestures { change, dragAmount ->
}
})
在detectDragGestures中,有兩個(gè)參數(shù),第一個(gè)參數(shù):PointerInputChange,代表手指點(diǎn)按的信息,每一個(gè)手指按下都有對應(yīng)的id和位置信息等,以此來處理手勢的抬起和按下;第二個(gè)參數(shù):代表的是手指滑動(dòng)的位置信息,是一個(gè)Offset類型的數(shù)據(jù),記錄x和y軸的偏移量。
@Composable
fun TestMultiScroll() {
val currentX = remember {
mutableStateOf(0)
}
val currentY = remember {
mutableStateOf(0)
}
Text(text = "二維滑動(dòng)",
Modifier
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
placeable.placeRelative(currentX.value, currentY.value)
}
}
.size(200.dp)
.background(Color.Blue)
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
currentX.value += dragAmount.x.roundToInt()
currentY.value += dragAmount.y.roundToInt()
}
})
}
所以要實(shí)現(xiàn)懸浮窗的拖動(dòng),就可以使用detectDragGestures來實(shí)現(xiàn)。
2 Compose中的嵌套滑動(dòng)
在傳統(tǒng)的View體系中,如何通過嵌套滑動(dòng)機(jī)制完成一些需求,它的實(shí)現(xiàn)還是比較復(fù)雜的,需要實(shí)現(xiàn)NestScrollingParent和NestScrollingChild接口。
而在Compose中實(shí)現(xiàn)嵌套滑動(dòng),在1.3小節(jié)中,我介紹過了Modifier.scrollable,其實(shí)就是在其基礎(chǔ)之上實(shí)現(xiàn)。
2.1 Compose自有的嵌套滑動(dòng)組件
在傳統(tǒng)的View體系中,像RecyclerView,NestScrollView等,都具備嵌套滑動(dòng)的能力,例如RecyclerView:
public class RecyclerView extends ViewGroup implements ScrollingView,
NestedScrollingChild2, NestedScrollingChild3 { // ......}
它實(shí)現(xiàn)了NestedScrollingChild2接口,所以它具備嵌套滑動(dòng)的能力。所以在Compose當(dāng)中通過Modifier.scrollable實(shí)現(xiàn)的滑動(dòng)組件,大概率都具備嵌套滑動(dòng)的能力,比如LazyColumn。
兩個(gè)LazyColumn嵌套在一起,當(dāng)滑動(dòng)內(nèi)部的LazyColumn的時(shí)候,外部的LazyColumn不會(huì)滑動(dòng),只有當(dāng)內(nèi)部的LazyColumn到底之后,外部的LazyColumn才可以繼續(xù)滑動(dòng)。
@Composable
fun TestNestScroll() {
LazyColumn {
item {
LazyColumn(
Modifier
.fillMaxWidth()
.height(200.dp)
) {
items(10) {
Text(
text = "child $it",
Modifier
.fillMaxWidth()
.height(30.dp)
.background(Color.Red)
)
}
}
}
items(20) {
Text(
text = "parent $it",
Modifier
.fillMaxWidth()
.height(30.dp)
.background(Color.Blue)
)
}
}
}
當(dāng)然,作為程序員,面對一些定制化的需求,還是需要自己實(shí)現(xiàn)的。
2.2 自定義實(shí)現(xiàn)嵌套滑動(dòng)
在Compose當(dāng)中,提供了Modifier.nestedScroll來實(shí)現(xiàn)嵌套滑動(dòng),既然我要講嵌套滑動(dòng),首先需要明確一下,嵌套滑動(dòng)的原理:
其實(shí)嵌套滑動(dòng)很簡單,在Compose當(dāng)中對于父容器是不會(huì)主動(dòng)處理滑動(dòng)事件,是子組件通過回調(diào)通知父容器是否需要滑動(dòng),通常是在子組件滑動(dòng)之前「詢問」父容器是否要消費(fèi)滑動(dòng)距離,以及在子組件滑動(dòng)完成之后,也要詢問父容器是否需要消費(fèi)剩余的滑動(dòng)距離。
ok,知道原理之后,就知道該做哪些事了!
- 通知父容器是否消費(fèi)事件,分兩次進(jìn)行;
- 父容器接收到回調(diào)之后,選擇是否處理事件消費(fèi)
那么如何通知父容器是否消費(fèi)事件,就是采用NestedScrollDispatcher來進(jìn)行嵌套滑動(dòng)的事件分發(fā),也就是nestedScroll函數(shù)的第二個(gè)參數(shù)。
fun Modifier.nestedScroll(
connection: NestedScrollConnection,
dispatcher: NestedScrollDispatcher? = null
): Modifier = composed(
inspectorInfo = debugInspectorInfo {
name = "nestedScroll"
properties["connection"] = connection
properties["dispatcher"] = dispatcher
}
) {
val scope = rememberCoroutineScope()
// provide noop dispatcher if needed
val resolvedDispatcher = dispatcher ?: remember { NestedScrollDispatcher() }
remember(connection, resolvedDispatcher, scope) {
resolvedDispatcher.originNestedScrollScope = scope
NestedScrollModifierLocal(resolvedDispatcher, connection)
}
}
接下來帶大家實(shí)現(xiàn)一個(gè)嵌套滑動(dòng)組件。
@Composable
fun TestNestScroll2() {
var currentY = remember {
mutableStateOf(0)
}
Column(
Modifier
.fillMaxWidth()
.offset {
IntOffset(0, currentY.value)
}
.draggable(rememberDraggableState {
currentY.value += it.roundToInt()
}, Orientation.Vertical)
) {
for (index in 1..20) {
Text(
text = "第 $index 個(gè)組件",
Modifier
.fillMaxWidth()
.height(20.dp)
)
}
}
}
這個(gè)組件具備了上下滑動(dòng)的能力,接下來會(huì)處理嵌套滑動(dòng)的邏輯。
2.2.1 NestedScrollDispatcher
伙伴們重點(diǎn)看下NestedScrollDispatcher關(guān)于滑動(dòng)事件分發(fā)函數(shù)的注釋:
class NestedScrollDispatcher {
// ......
/**
* 用于子組件處理滑動(dòng)之前,回調(diào)通知父容器是否需要消費(fèi)事件
*
* @param available 一次滑動(dòng)事件的距離
* @param source 滑動(dòng)事件的來源
*
* @return 祖先節(jié)點(diǎn),或者說父容器消費(fèi)的滑動(dòng)距離
*/
fun dispatchPreScroll(available: Offset, source: NestedScrollSource): Offset {
return parent?.onPreScroll(available, source) ?: Offset.Zero
}
/**
* 子組件滑動(dòng)完成之后,再次通知父容器是否需要消費(fèi)事件
*
* @param consumed 當(dāng)前子組件消費(fèi)的距離
* @param available 當(dāng)前父容器可以再次消費(fèi)的剩余距離
* @param source 滑動(dòng)事件的來源
*
* @return the amount of scroll that was consumed by all ancestors
*/
fun dispatchPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
return parent?.onPostScroll(consumed, available, source) ?: Offset.Zero
}
// 下面兩個(gè)慣性函數(shù)其實(shí)是一樣的。
suspend fun dispatchPreFling(available: Velocity): Velocity {
return parent?.onPreFling(available) ?: Velocity.Zero
}
suspend fun dispatchPostFling(consumed: Velocity, available: Velocity): Velocity {
return parent?.onPostFling(consumed, available) ?: Velocity.Zero
}
}
NestedScrollDispatcher就是用來通知父容器是否需要消費(fèi)事件的工具,所以我對draggable的內(nèi)部邏輯進(jìn)行了修改。
Modifier.draggable(rememberDraggableState { duration ->
//滑動(dòng)前,通知父容器,有duration長度的滑動(dòng)距離,要不要消費(fèi)?
val parentConsumed =
dispatch.dispatchPreScroll(Offset(0f, duration), NestedScrollSource.Drag)
//那么子組件能夠消費(fèi)的距離,需要減去父容器消費(fèi)的距離,具體父容器消費(fèi)多少,不需要關(guān)心
val availableDuration = duration.roundToInt() - parentConsumed.y.roundToInt()
currentY.value += availableDuration
// 滑動(dòng)結(jié)束之后,再次通知父容器,要不要消費(fèi)?
dispatch.dispatchPostScroll(
Offset(0f, availableDuration.toFloat()), // 子組件消費(fèi)了全部的剩余距離
Offset.Zero, // 父容器可消費(fèi)的滑動(dòng)距離為0
NestedScrollSource.Drag
)
}, Orientation.Vertical)
2.2.2 NestedScrollConnection
那么子組件通過NestScrollDispatcher發(fā)起的回調(diào),父容器在哪接收到呢?就是通過NestedScrollConnection,它是一個(gè)接口,所以在用的時(shí)候需要自己實(shí)現(xiàn)一個(gè)實(shí)例,或者創(chuàng)建一個(gè)匿名內(nèi)部類都可以。
接口函數(shù)的注釋可以閱讀一下:
@JvmDefaultWithCompatibility
interface NestedScrollConnection {
/**
* Pre scroll event chain. 它會(huì)在子組件允許父容器消費(fèi)滑動(dòng)事件的時(shí)候回調(diào),是在子組件滑動(dòng)之前接收到的回調(diào)。
*
* @param available 父容器可以消費(fèi)的滑動(dòng)距離,即dispatch.dispatchPreScroll傳入的第一個(gè)參數(shù)
* @param source 滑動(dòng)事件來源
*
* @return 當(dāng)前組件消費(fèi)多少的滑動(dòng)事件
*/
fun onPreScroll(available: Offset, source: NestedScrollSource): Offset = Offset.Zero
/**
* Post scroll event pass. 子組件完成滑動(dòng)之后會(huì)回調(diào)
* @param consumed 子組件消費(fèi)的滑動(dòng)距離,即dispatch.dispatchPostScroll傳入的第一個(gè)參數(shù)
* @param available 父容器可以消費(fèi)的滑動(dòng)距離,即dispatch.dispatchPostScroll傳入的第二個(gè)參數(shù)
* @param source 滑動(dòng)事件來源
*
* @return the amount that was consumed by this connection
*/
fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset = Offset.Zero
// 慣性的我先不管了
suspend fun onPreFling(available: Velocity): Velocity = Velocity.Zero
suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
return Velocity.Zero
}
}
所以總體的嵌套滑動(dòng)處理,在理解了其中的原理之后,其實(shí)就大概能寫出來其中的核心邏輯了,當(dāng)然這個(gè)demo不存在嵌套滑動(dòng)的邏輯,我在內(nèi)部再加一個(gè)LazyColumn。
@Composable
fun TestNestScroll2() {
var currentY = remember {
mutableStateOf(0)
}
//分發(fā)給父容器
val dispatch = remember {
NestedScrollDispatcher()
}
val connection = remember {
object : NestedScrollConnection{
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
// 假設(shè)全消費(fèi)了
Log.d(TAG, "onPreScroll: $available ")
return available
}
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
Log.d(TAG, "onPostScroll: consumed $consumed available $available")
//子組件滑動(dòng)完成之后, 如果還有可用的距離,那么父組件就消費(fèi)
return super.onPostScroll(consumed, available, source)
}
}
}
Column(
Modifier
.fillMaxWidth()
.offset {
IntOffset(0, currentY.value)
}
.nestedScroll(connection, dispatch)
.draggable(rememberDraggableState { duration ->
//滑動(dòng)前,通知父容器,有duration長度的滑動(dòng)距離,要不要消費(fèi)?
val parentConsumed =
dispatch.dispatchPreScroll(Offset(0f, duration), NestedScrollSource.Drag)
//那么子組件能夠消費(fèi)的距離,需要減去父容器消費(fèi)的距離,具體父容器消費(fèi)多少,不需要關(guān)心
val availableDuration = duration.roundToInt() - parentConsumed.y.roundToInt()
Log.d(TAG, "TestNestScroll2: availableDuration $availableDuration")
currentY.value += availableDuration
// 滑動(dòng)結(jié)束之后,再次通知父容器,要不要消費(fèi)?
dispatch.dispatchPostScroll(
Offset(0f, availableDuration.toFloat()), // 子組件消費(fèi)了全部的剩余距離
Offset.Zero, // 父容器可消費(fèi)的滑動(dòng)距離為0
NestedScrollSource.Drag
)
}, Orientation.Vertical)
) {
for (index in 1..20) {
Text(
text = "第 $index 個(gè)組件",
Modifier
.fillMaxWidth()
.height(20.dp)
)
}
//內(nèi)部嵌套一個(gè)LazyColumn。
LazyColumn(Modifier.height(80.dp)){
items(10){
Text(
text = "第 $it 個(gè)內(nèi)部組件",
Modifier
.fillMaxWidth()
.height(20.dp)
)
}
}
}
}
先看下這個(gè)布局結(jié)構(gòu)

當(dāng)子組件滑動(dòng)的時(shí)候,自定義的ScrollView(父容器)會(huì)首先收到子組件的回調(diào),在onPreScroll中處理決定是否要消費(fèi)事件:
- 假設(shè)在
onPreScroll中,父容器消費(fèi)了全部的滑動(dòng)距離,那么內(nèi)部的LazyColumn就不能滑動(dòng)了; - 默認(rèn)不處理
onPreScroll,在子組件不能再滑動(dòng)的時(shí)候,就繼續(xù)滑動(dòng)父容器,需要做下面的邏輯。
val connection = remember {
object : NestedScrollConnection{
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
Log.d(TAG, "onPostScroll: consumed $consumed available $available")
//子組件滑動(dòng)完成之后, 如果還有可用的距離,那么父組件就消費(fèi)
currentY.value += available.y.toInt()
return available
}
}
}
因?yàn)樽咏M件已經(jīng)無法滑動(dòng)了,因此所有的事件子組件不再消費(fèi),從而在onPostScroll中會(huì)將事件原封不動(dòng)的回調(diào)給父容器,父容器從而消費(fèi)事件繼續(xù)滑動(dòng)。
如果把定義的ScrollView放在其他的容器中,那么其自身就會(huì)成為子組件,會(huì)執(zhí)行draggable中的嵌套滑動(dòng)邏輯。