Jetpack Compose(8)——嵌套滾動

来源:https://www.cnblogs.com/joy99/p/18272173
-Advertisement-
Play Games

目錄前言一、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

先上效果圖:

簡單分析一下效果:

  1. 佈局分為兩部分,上面是一張圖片,下麵是一個滑動列表
  2. 滑動過程中,上滑時,首先頭部響應滑動,收縮到最小高度之後,列表再開始向上滑動。下滑時,也是頭部先影響滑動,頭部圖片展開到最大高度之後,列表再開始向下滑動。即:不論上上滑還是下滑,都是頭部圖片先響應。
  3. 我們希望是按住列表能滑動,按住頭部圖片是不能滑動的,也就是說頭部圖片不會檢測滑動事件,只有下麵列表會檢測滑動事件。

下麵開始編碼:

  1. 手寫整體佈局應該是 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())
                    }
                }
            }
    }
}
  1. 給 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())
                }
            }
        }
    }
}
  1. 最後就是編寫滑動處理邏輯了。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 的方法。

看下麵這個示例

同樣簡單分析一下效果:

  1. 整個父組件是一個 LazyColumn, 自身可以滾動
  2. 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 的例子中,頭部圖片是不檢測滑動事件的,手指按住圖片滑動是不會響應的,現在需要修改為按住上面圖片也是可以滑動,將頭部收縮和展開。

下麵開始改造:

  1. 給 Column 加上 Modifier.draggable 修飾
Column(
        modifier = Modifier
            .fillMaxSize()
            .draggable(
                state = rememberDraggableState { onDelta ->

                },
                orientation = Orientation.Vertical
            )
            .nestedScroll(connection = connection)
) {
    ...
}
  1. 聲明 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
        )
)

這樣就可以了。

小結

  1. 本文介紹了 Jetpack Compose 中嵌套滾動的相關知識。
    Compose 中嵌套滾動事件的分發思想是,滾動事件會預先交給父組件預先處理,父組件處理消費之後,自己處理剩餘滾動距離,自己處理消費完之後,還有剩餘,會再交給父組件處理。
  2. 一般來說,當子組件檢測滾動事件,則需要實現 NestedScrollConnection 中的 onPreScrollonPostScroll 方法。當自己檢測滾動事件,則需要使用 NestedScrollDispatcher 的相關方法對滾動事件進行分發。
  3. 另外還有 Fling 事件,慣性滾動,其分發思想與滾動一致,不同的它的值表示速度。另外慣性滾動過程實現比較複雜,Compose 提供了預設實現,ScrollableDefaults.flingBehavior(),感興趣的朋友可以繼續研究。
作者:SharpCJ     作者博客:http://joy99.cnblogs.com/     本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接。
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 分享是最有效的學習方式。 博客:https://blog.ktdaddy.com/ 故事 梅雨季,悶熱的夜,令人窒息,窗外一道道閃電劃破漆黑的夜幕,小貓塞著耳機聽著恐怖小說,輾轉反側,終於睡意來了,然而挨千刀的手機早不振晚不振,偏偏這個時候振動了一下,一個激靈,沒有按捺住對內容的好奇,點開了簡訊,卧 ...
  • 華為雲記憶體加速的“全自動主動緩存方案”,可通過界面可視化配置,支持用戶將MySQL表數據映射為GeminiDB中的Hash等結構,同時還支持數據過濾及過期等功能,配置完成後即可實現自動同步,不僅簡單易用還能提升效率。 ...
  • Percona Toolkit 神器全攻略(配置類) Percona Toolkit 神器全攻略系列共八篇,前文回顧: 前文回顧 Percona Toolkit 神器全攻略 Percona Toolkit 神器全攻略(實用類) 全文約定:$為命令提示符、greatsql>為GreatSQL資料庫提示 ...
  • 使用Percona Toolkit的pt-duplicate-key-checker工具時,偶爾會遇到"Error checking xxx: Wide character in print at /usr/bin/pt-duplicate-key-checker line 5248."這類錯誤。如 ...
  • 1.前言 因為小程式是由js代碼編寫的,我js學得不是特別的好,所以,剛開始以為js跟java一行,一行一行的執行,後面才發現,完全不是,所以有時候,我們在獲取用戶信息和openId的時候,要向後臺發送請求,所以有時有可能請求還沒有返回數據,小程式這邊已經賦值了,只能得到一個undifine,很桑心 ...
  • UINavigationController 是 iOS 中用於管理視圖控制器層次結構的一個重要組件,通常用於實現基於堆棧的導航。它提供了一種用戶界面,允許用戶在視圖控制器之間進行層次化的導航,例如從列表視圖到詳細視圖。 UINavigationController 的主要功能 管理視圖控制器堆棧: ...
  • UITabBarController 是 iOS 中用於管理和顯示選項卡界面的一個視圖控制器。它允許用戶在多個視圖控制器之間進行切換,每個視圖控制器對應一個選項卡。 主要功能 管理多個視圖控制器: UITabBarController 管理一個視圖控制器數組,每個視圖控制器對應一個選項卡。 顯示選項 ...
  • 在MVC模型中,V指view,負責用戶界面的顯示、處理用戶輸入,並將輸入傳遞給控制器。C是指ViewController,充當模型和視圖之間的中介。控制器接收用戶輸入,處理用戶請求,並將結果傳遞給視圖以更新顯示。本文詳細介紹在iOS開發中UIView與UIViewController的生命周期。 U ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...