目錄前言一、Jetpack Compose 中處理嵌套滾動的思想二、Modifier.nestedScroll2.1 NestedScrollConnection2.2 NestedScrollDispatcher三、實操講解3.1 父組件消費子組件給過來的事件——NestedScrollConne ...
目錄
前言
所謂嵌套滾動,就是兩個組件之間出現滾動事件衝突了,要給與特定的處理邏輯。在傳統 View 系統中稱之為滑動衝突,一般有兩種解決方案,外部攔截法和內部攔截法。在 Jetpack Compose 中,提供了 Modifier.nestedScroll
修飾符用來處理嵌套滾動的場景。
一、Jetpack Compose 中處理嵌套滾動的思想
在介紹 Modifier.nestedScroll
之前,需要先瞭解 Compose 中嵌套滾動的處理思想。當組件獲得滾動事件後,先交給它的父組件消費,父組件消費之後,將剩餘可用的滾動事件在給到子組件,子組件再消費,子組件消費之後,再將剩餘的滾動事件再給到父組件。
第一趟 ... -> 孫 ——> 子 ——> 父 -> ...
第二趟 ... <- 孫 <—— 子 <—— 父 <- ...
第三趟 ... -> 孫 ——> 子 ——> 父 -> ...
二、Modifier.nestedScroll
有了整體思路之後,再來看 Modifier.nestedScroll 這個修飾符。
fun Modifier.nestedScroll(
connection: NestedScrollConnection,
dispatcher: NestedScrollDispatcher? = null
): Modifier
使用 nestedScroll 參數列表中有一個必選參數 connection
和一個可選參數 dispatcher
-
connection: 嵌套滑動手勢處理的核心邏輯,內部回調可以在子佈局獲得滑動事件前預先消費掉部分或全部手勢偏移量,也可以獲取子佈局消費後剩下的手勢偏移量。
-
dispatcher:調度器,內部包含用於父佈局的 NestedScrollConnection , 可以調用 dispatch* 方法來通知父佈局發生滑動
2.1 NestedScrollConnection
NestedScrollConnection
提供了四個回調方法。
interface NestedScrollConnection {
/**
* 預先劫持滑動事件,消費後再交由子佈局。
* available:當前可用的滑動事件偏移量
* source:滑動事件的類型
* 返回值:當前組件消費的滑動事件偏移量,如果不想消費可返回Offset.Zero
*/
fun onPreScroll(available: Offset, source: NestedScrollSource): Offset = Offset.Zero
/**
* 獲取子佈局處理後的滑動事件
* consumed:之前消費的所有滑動事件偏移量
* available:當前剩下還可用的滑動事件偏移量
* source:滑動事件的類型
* 返回值:當前組件消費的滑動事件偏移量,如果不想消費可返回 Offset.Zero ,則剩下偏移量會繼續交由當前佈局的父佈局進行處理
*/
fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset = Offset.Zero
/**
* 獲取 Fling 開始時的速度
* available:Fling 開始時的速度
* 返回值:當前組件消費的速度,如果不想消費可返回 Velocity.Zero
*/
suspend fun onPreFling(available: Velocity): Velocity = Velocity.Zero
/**
* 獲取 Fling 結束時的速度信息
* consumed:之前消費的所有速度
* available:當前剩下還可用的速度
* 返回值:當前組件消費的速度,如果不想消費可返回Velocity.Zero,剩下速度會繼續交由當前佈局的父佈局進行處理
*/
suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
return Velocity.Zero
}
}
關於各個方法的含義已經在方法註釋中標註了。
註意 Fling 的含義: 當我們手指在滑動列表時,如果是快速滑動並抬起,則列表會根據慣性繼續飄一段距離後停下,這個行為就是 Fling ,onPreFling 在你手指剛抬起時便會回調,而 onPostFling 會在飄一段距離停下後回調。
2.2 NestedScrollDispatcher
NestedScrollDispatcher 的主要方法:
fun dispatchPreScroll(available: Offset, source: NestedScrollSource): Offset {
return parent?.onPreScroll(available, source) ?: Offset.Zero
}
fun dispatchPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
return parent?.onPostScroll(consumed, available, source) ?: Offset.Zero
}
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
}
其實方法實現就能清楚,實際上讓其父組件調用 NestedScrollConnection
中的預消費與後消費方法。
三、實操講解
3.1 父組件消費子組件給過來的事件——NestedScrollConnection
先上效果圖:
簡單分析一下效果:
- 佈局分為兩部分,上面是一張圖片,下麵是一個滑動列表
- 滑動過程中,上滑時,首先頭部響應滑動,收縮到最小高度之後,列表再開始向上滑動。下滑時,也是頭部先影響滑動,頭部圖片展開到最大高度之後,列表再開始向下滑動。即:不論上上滑還是下滑,都是頭部圖片先響應。
- 我們希望是按住列表能滑動,按住頭部圖片是不能滑動的,也就是說頭部圖片不會檢測滑動事件,只有下麵列表會檢測滑動事件。
下麵開始編碼:
- 手寫整體佈局應該是 Column 實現,頭部使用一個 Image ,下麵使用 LazyColumn
@Composable
fun NestedScrollDemo() {
Column(
modifier = Modifier.fillMaxSize()) {
Image(
painter = painterResource(id = R.mipmap.rc_1),
contentDescription = null,
contentScale = ContentScale.FillBounds,
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
)
LazyColumn {
repeat(50) {
item {
Text(text = "item --> $it", modifier = Modifier.fillMaxWidth())
}
}
}
}
}
- 給 Column 組件使用 Modifier.nestedScroll。
這裡簡單做一些定義:頭部圖片最小高度為 80.dp, 最大高度為 200.dp。註意 dp 和 px 之間的轉換。
@Composable
fun NestedScrollDemo() {
val minHeight = 80.dp
val maxHeight = 200.dp
val density = LocalDensity.current
val minHeightPx = with(density) {
minHeight.toPx()
}
val maxHeightPx = with(density) {
maxHeight.toPx()
}
var topHeightPx by remember {
mutableStateOf(maxHeightPx)
}
val connection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
return super.onPreScroll(available, source)
}
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
return super.onPostScroll(consumed, available, source)
}
override suspend fun onPreFling(available: Velocity): Velocity {
return super.onPreFling(available)
}
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
return super.onPostFling(consumed, available)
}
}
}
Column(
modifier = Modifier
.fillMaxSize()
.nestedScroll(connection = connection)
) {
Image(
painter = painterResource(id = R.mipmap.rc_1),
contentDescription = null,
contentScale = ContentScale.FillBounds,
modifier = Modifier
.fillMaxWidth()
.height(with(density) {
topHeightPx.toDp()
})
)
LazyColumn {
repeat(50) {
item {
Text(text = "item --> $it", modifier = Modifier.fillMaxWidth())
}
}
}
}
}
- 最後就是編寫滑動處理邏輯了。LazyColumn 列表檢測到滑動事件,把這個滑動距離先給到父組件Column 消費,Column 消費之後,把剩餘的再給到 LazyColumn 消費,LazyColumn 消費之後,還有剩餘,再給回 Column 消費。其中 LazyColumn 消費事件,不用我們處理,我們的 Modifier.nestedScroll 作用在 Column 上,我們需要預先消費 LazyColumn 給過來的滑動距離——在
onPreScroll
中實現,然後把剩餘的給到 LazyColumn,最後 LazyColumn 消費後還有剩餘的滑動距離,Column 處理 —— 在onPostScroll
中處理。
val connection = remember {
object : NestedScrollConnection {
/**
* 預先劫持滑動事件,消費後再交由子佈局。
* available:當前可用的滑動事件偏移量
* source:滑動事件的類型
* 返回值:當前組件消費的滑動事件偏移量,如果不想消費可返回Offset.Zero
*/
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
if (source == NestedScrollSource.Drag) { // 判斷是滑動事件
if (available.y < 0) { // 向上滑動
val dH = minHeightPx - topHeightPx // 向上滑動過程中,還差多少達到最小高度
if (available.y > dH) { // 如果當前可用的滑動距離全部消費都不足以達到最小高度,就將當前可用距離全部消費掉
topHeightPx += available.y
return Offset(x = 0f, y = available.y)
} else { // 如果當前可用的滑動距離足夠達到最小高度,就只消費掉需要的距離。剩餘的給到子組件。
topHeightPx += dH
return Offset(x = 0f, y = dH)
}
} else { // 下滑
val dH = maxHeightPx - topHeightPx // 向下滑動過程中,還差多少達到最大高度
if (available.y < dH) { // 如果當前可用的滑動距離全部消費都不足以達到最大高度,就將當前可用距離全部消費掉
topHeightPx += available.y
return Offset(x = 0f, y = available.y)
} else { // 如果當前可用的滑動距離足夠達到最大高度,就只消費掉需要的距離。剩餘的給到子組件。
topHeightPx += dH
return Offset(x = 0f, y = dH)
}
}
} else { // 如果不是滑動事件,就不消費。
return Offset.Zero
}
}
/**
* 獲取子佈局處理後的滑動事件
* consumed:之前消費的所有滑動事件偏移量
* available:當前剩下還可用的滑動事件偏移量
* source:滑動事件的類型
* 返回值:當前組件消費的滑動事件偏移量,如果不想消費可返回 Offset.Zero ,則剩下偏移量會繼續交由當前佈局的父佈局進行處理
*/
override fun onPostScroll(
consumed: Offset, available: Offset, source: NestedScrollSource
): Offset {
// 子組件處理後的剩餘的滑動距離,此處不需要消費了,直接不消費。
return Offset.Zero
}
override suspend fun onPreFling(available: Velocity): Velocity {
return super.onPreFling(available)
}
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
return super.onPostFling(consumed, available)
}
}
}
所有的代碼通過註釋已經寫的很詳細了。這樣就實現了上圖的效果。
3.2 子組件對事件進行分發——NestedScrollDispatcher
滑動距離消費不一定要體現在位置、大小之類的變化上。當使用 Modifier.nestedScroll 修飾符處理嵌套滾動時,絕大多數場景使用外部攔截法就能輕鬆實現,給父容器修飾,實現 NestedScollConnection 方法。
使用內部攔截法,一般用於父組件也可以消費事件,需要子容器使用 Modifier.nestedScroll ,併合理使用 NestedScrollDispatcher 的方法。
看下麵這個示例
同樣簡單分析一下效果:
- 整個父組件是一個 LazyColumn, 自身可以滾動
- LazyColumn 中的一個元素是一張圖片,使用 Image 組件,當按住圖片滾動時,優先處理圖片的收縮與展開。
實現如下:
@Composable
fun NestedScrollDemo4() {
val minHeight = 80.dp
val maxHeight = 200.dp
val density = LocalDensity.current
val minHeightPx = with(density) {
minHeight.toPx()
}
val maxHeightPx = with(density) {
maxHeight.toPx()
}
var topHeightPx by remember {
mutableStateOf(maxHeightPx)
}
val connection = remember {
object : NestedScrollConnection {}
}
val dispatcher = NestedScrollDispatcher()
LazyColumn(
modifier = Modifier
.background(Color.LightGray)
.fillMaxSize()
) {
for (i in 0..10) {
item {
Text(text = "item --> $i", modifier = Modifier.fillMaxWidth())
}
}
item {
Image(
painter = painterResource(id = R.mipmap.rc_1),
contentDescription = null,
contentScale = ContentScale.FillBounds,
modifier = Modifier
.fillMaxWidth()
.height(with(density) {
topHeightPx.toDp()
})
.draggable(
state = rememberDraggableState { onDelta ->
// 1. 滑動距離,給到父組件先消費
// 調用父組件劫持滑動事件,讓父組件先消費,返回值是父組件消費掉的滑動距離
// 這裡並不想讓父組件先消費,就給父組件傳了 Offset.Zero。 返回值也就是 Offset.Zero。
val consumed = dispatcher.dispatchPreScroll(
available = Offset(x = 0f, y = 0f), source = NestedScrollSource.Drag
)
// 2. 父組件消費完之後,剩餘的滑動距離,自己按需消費
// 計算父組件消費後剩餘的可使用的滑動距離
val availableY = onDelta - consumed.y
// canConsumeY 是當前需要消費掉的距離
val canConsumeY = if (availableY < 0) { // 向上滑動
val dH = minHeightPx - topHeightPx // 向上滑動過程中,還差多少達到最小高度
if (availableY > dH) { // 如果當前可用的滑動距離全部消費都不足以達到最小高度,就將當前可用距離全部消費掉
availableY
} else { // 如果當前可用的滑動距離足夠達到最小高度,就只消費掉需要的距離
dH
}
} else { // 下滑
val dH = maxHeightPx - topHeightPx // 向下滑動過程中,還差多少達到最大高度
if (availableY < dH) { // 如果當前可用的滑動距離全部消費都不足以達到最大高度,就將當前可用距離全部消費掉
availableY
} else { // 如果當前可用的滑動距離足夠達到最大高度,就只消費掉需要的距離
dH
}
}
// 把當前消費掉的距離給到圖片高度
topHeightPx += canConsumeY
// 父組件消費後,以及本次消費後,最後剩餘的滑動距離
val remain = onDelta - consumed.y - canConsumeY
// 3. 自己消費完之後,還有剩餘的滑動距離,再給到父組件
dispatcher.dispatchPostScroll(
consumed = Offset(x = 0f, y = consumed.y + canConsumeY), // 這裡是總共消費的滑動距離,包括父組件消費的和本次自己消費的
available = Offset(0f, remain), // 剩餘可用的滑動距離
source = NestedScrollSource.Drag
)
}, orientation = Orientation.Vertical
)
.nestedScroll(connection, dispatcher)
)
}
for (j in 11..40) {
item {
Text(text = "item --> $j", modifier = Modifier.fillMaxWidth())
}
}
}
}
關鍵代碼都已經加上了註釋。看起來應該是非常清晰的。
這裡,主要是內部使用 dispatcher 進行事件攔截。
3.2 按照分發順序依次消費
在 3.1 的例子中,頭部圖片是不檢測滑動事件的,手指按住圖片滑動是不會響應的,現在需要修改為按住上面圖片也是可以滑動,將頭部收縮和展開。
下麵開始改造:
- 給 Column 加上 Modifier.draggable 修飾
Column(
modifier = Modifier
.fillMaxSize()
.draggable(
state = rememberDraggableState { onDelta ->
},
orientation = Orientation.Vertical
)
.nestedScroll(connection = connection)
) {
...
}
- 聲明 dispatcher,使用 dispatcher 處理嵌套滑動事件
val dispatcher = remember { NestedScrollDispatcher() }
Column(
modifier = Modifier
.fillMaxSize()
.draggable(
state = rememberDraggableState { onDelta ->
// 1. 滑動距離,給到父組件先消費
// 調用父組件劫持滑動事件,讓父組件先消費,返回值是父組件消費掉的滑動距離
val consumed = dispatcher.dispatchPreScroll(
available = Offset(x = 0f, y = onDelta), source = NestedScrollSource.Drag
)
// 2. 父組件消費完之後,剩餘的滑動距離,自己按需消費
// 計算父組件消費後剩餘的可使用的滑動距離
val availableY = (onDelta - consumed.y)
// consume 是當前需要消費掉的距離
val consumeY = if (availableY < 0) { // 向上滑動
val dH = minHeightPx - topHeightPx // 向上滑動過程中,還差多少達到最小高度
if (availableY > dH) { // 如果當前可用的滑動距離全部消費都不足以達到最小高度,就將當前可用距離全部消費掉
availableY
} else { // 如果當前可用的滑動距離足夠達到最小高度,就只消費掉需要的距離
dH
}
} else { // 下滑
val dH = maxHeightPx - topHeightPx // 向下滑動過程中,還差多少達到最大高度
if (availableY < dH) { // 如果當前可用的滑動距離全部消費都不足以達到最大高度,就將當前可用距離全部消費掉
availableY
} else { // 如果當前可用的滑動距離足夠達到最大高度,就只消費掉需要的距離
dH
}
}
// 把當前消費掉的距離給到圖片高度
topHeightPx += consumeY
// 父組件消費後,以及本次消費後,最後剩餘的滑動距離
val remain = onDelta - consumed.y - consumeY
// 3. 自己消費完之後,還有剩餘的滑動距離,再給到父組件
dispatcher.dispatchPostScroll(
consumed = Offset(x = 0f, y = consumed.y + consumeY), // 這裡是總共消費的滑動距離,包括父組件消費的和本次自己消費的
available = Offset(0f, remain), // 剩餘可用的滑動距離
source = NestedScrollSource.Drag
)
},
orientation = Orientation.Vertical
)
.nestedScroll(
connection = connection,
dispatcher = dispatcher
)
) {
...
}
同樣代碼註釋已經寫得非常清晰了。
完整代碼如下:
@Composable
fun NestedScrollDemo2() {
val minHeight = 80.dp
val maxHeight = 200.dp
val density = LocalDensity.current
val minHeightPx = with(density) {
minHeight.toPx()
}
val maxHeightPx = with(density) {
maxHeight.toPx()
}
var topHeightPx by remember {
mutableStateOf(maxHeightPx)
}
val dispatcher = remember { NestedScrollDispatcher() }
val connection = remember {
object : NestedScrollConnection {
/**
* 預先劫持滑動事件,消費後再交由子佈局。
* available:當前可用的滑動事件偏移量
* source:滑動事件的類型
* 返回值:當前組件消費的滑動事件偏移量,如果不想消費可返回Offset.Zero
*/
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
if (source == NestedScrollSource.Drag) { // 判斷是滑動事件
if (available.y < 0) { // 向上滑動
val dH = minHeightPx - topHeightPx // 向上滑動過程中,還差多少達到最小高度
if (available.y > dH) { // 如果當前可用的滑動距離全部消費都不足以達到最小高度,就將當前可用距離全部消費掉
topHeightPx += available.y
return Offset(x = 0f, y = available.y)
} else { // 如果當前可用的滑動距離足夠達到最小高度,就只消費掉需要的距離。剩餘的給到子組件。
topHeightPx += dH
return Offset(x = 0f, y = dH)
}
} else { // 下滑
val dH = maxHeightPx - topHeightPx // 向下滑動過程中,還差多少達到最大高度
if (available.y < dH) { // 如果當前可用的滑動距離全部消費都不足以達到最大高度,就將當前可用距離全部消費掉
topHeightPx += available.y
return Offset(x = 0f, y = available.y)
} else { // 如果當前可用的滑動距離足夠達到最大高度,就只消費掉需要的距離。剩餘的給到子組件。
topHeightPx += dH
return Offset(x = 0f, y = dH)
}
}
} else { // 如果不是滑動事件,就不消費。
return Offset.Zero
}
}
/**
* 獲取子佈局處理後的滑動事件
* consumed:之前消費的所有滑動事件偏移量
* available:當前剩下還可用的滑動事件偏移量
* source:滑動事件的類型
* 返回值:當前組件消費的滑動事件偏移量,如果不想消費可返回 Offset.Zero ,則剩下偏移量會繼續交由當前佈局的父佈局進行處理
*/
override fun onPostScroll(
consumed: Offset, available: Offset, source: NestedScrollSource
): Offset {
// 子組件處理後的剩餘的滑動距離,此處不需要消費了,直接不消費。
return Offset.Zero
}
override suspend fun onPreFling(available: Velocity): Velocity {
return super.onPreFling(available)
}
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
return super.onPostFling(consumed, available)
}
}
}
Column(
modifier = Modifier
.fillMaxSize()
.draggable(
state = rememberDraggableState { onDelta ->
// 1. 滑動距離,給到父組件先消費
// 調用父組件劫持滑動事件,讓父組件先消費,返回值是父組件消費掉的滑動距離
val consumed = dispatcher.dispatchPreScroll(
available = Offset(x = 0f, y = onDelta), source = NestedScrollSource.Drag
)
// 2. 父組件消費完之後,剩餘的滑動距離,自己按需消費
// 計算父組件消費後剩餘的可使用的滑動距離
val availableY = (onDelta - consumed.y)
// consume 是當前需要消費掉的距離
val consumeY = if (availableY < 0) { // 向上滑動
val dH = minHeightPx - topHeightPx // 向上滑動過程中,還差多少達到最小高度
if (availableY > dH) { // 如果當前可用的滑動距離全部消費都不足以達到最小高度,就將當前可用距離全部消費掉
availableY
} else { // 如果當前可用的滑動距離足夠達到最小高度,就只消費掉需要的距離
dH
}
} else { // 下滑
val dH = maxHeightPx - topHeightPx // 向下滑動過程中,還差多少達到最大高度
if (availableY < dH) { // 如果當前可用的滑動距離全部消費都不足以達到最大高度,就將當前可用距離全部消費掉
availableY
} else { // 如果當前可用的滑動距離足夠達到最大高度,就只消費掉需要的距離
dH
}
}
// 把當前消費掉的距離給到圖片高度
topHeightPx += consumeY
// 父組件消費後,以及本次消費後,最後剩餘的滑動距離
val remain = onDelta - consumed.y - consumeY
// 3. 自己消費完之後,還有剩餘的滑動距離,再給到父組件
dispatcher.dispatchPostScroll(
consumed = Offset(x = 0f, y = consumed.y + consumeY), // 這裡是總共消費的滑動距離,包括父組件消費的和本次自己消費的
available = Offset(0f, remain), // 剩餘可用的滑動距離
source = NestedScrollSource.Drag
)
}, orientation = Orientation.Vertical
)
.nestedScroll(
connection = connection, dispatcher = dispatcher
)
) {
Image(
painter = painterResource(id = R.mipmap.rc_1),
contentDescription = null,
contentScale = ContentScale.FillBounds,
modifier = Modifier
.fillMaxWidth()
.height(with(density) {
topHeightPx.toDp()
})
)
LazyColumn {
repeat(50) {
item {
Text(text = "item --> $it", modifier = Modifier.fillMaxWidth())
}
}
}
}
}
運行效果:
看效果圖不明顯,實際上就是按住圖片位置拖動是可以收縮和展開頂部圖片的。
當然,其實要實現這個效果,也不用整這麼複雜,完全可以給 Image 設置 draggable 修飾符來實現:
Image(
painter = painterResource(id = R.mipmap.rc_1),
contentDescription = null,
contentScale = ContentScale.FillBounds,
modifier = Modifier
.fillMaxWidth()
.height(with(density) {
topHeightPx.toDp()
})
.draggable(
state = rememberDraggableState { onDelta ->
val consumeY = if (onDelta < 0) { // 向上滑動
val dH = minHeightPx - topHeightPx // 向上滑動過程中,還差多少達到最小高度
if (onDelta > dH) { // 如果當前可用的滑動距離全部消費都不足以達到最小高度,就將當前可用距離全部消費掉
onDelta
} else { // 如果當前可用的滑動距離足夠達到最小高度,就只消費掉需要的距離
dH
}
} else { // 下滑
val dH = maxHeightPx - topHeightPx // 向下滑動過程中,還差多少達到最大高度
if (onDelta < dH) { // 如果當前可用的滑動距離全部消費都不足以達到最大高度,就將當前可用距離全部消費掉
onDelta
} else { // 如果當前可用的滑動距離足夠達到最大高度,就只消費掉需要的距離
dH
}
}
topHeightPx += consumeY
},
orientation = Orientation.Vertical
)
)
這樣就可以了。
小結
- 本文介紹了 Jetpack Compose 中嵌套滾動的相關知識。
Compose 中嵌套滾動事件的分發思想是,滾動事件會預先交給父組件預先處理,父組件處理消費之後,自己處理剩餘滾動距離,自己處理消費完之後,還有剩餘,會再交給父組件處理。 - 一般來說,當子組件檢測滾動事件,則需要實現
NestedScrollConnection
中的onPreScroll
和onPostScroll
方法。當自己檢測滾動事件,則需要使用NestedScrollDispatcher
的相關方法對滾動事件進行分發。 - 另外還有 Fling 事件,慣性滾動,其分發思想與滾動一致,不同的它的值表示速度。另外慣性滾動過程實現比較複雜,Compose 提供了預設實現,
ScrollableDefaults.flingBehavior()
,感興趣的朋友可以繼續研究。