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
  • 通過WPF的按鈕、文本輸入框實現了一個簡單的SpinBox數字輸入用戶組件並可以通過數據綁定數值和步長。本文中介紹了通過Xaml代碼實現自定義組件的佈局,依賴屬性的定義和使用等知識點。 ...
  • 以前,我看到一個朋友在對一個系統做初始化的時候,通過一組魔幻般的按鍵,調出來一個隱藏的系統設置界面,這個界面在常規的菜單或者工具欄是看不到的,因為它是一個後臺設置的關鍵界面,不公開,同時避免常規用戶的誤操作,它是作為一個超級管理員的入口功能,這個是很不錯的思路。其實Winform做這樣的處理也是很容... ...
  • 一:背景 1. 講故事 前些天有位朋友找到我,說他的程式每次關閉時就會自動崩潰,一直找不到原因讓我幫忙看一下怎麼回事,這位朋友應該是第二次找我了,分析了下 dump 還是挺經典的,拿出來給大家分享一下吧。 二:WinDbg 分析 1. 為什麼會崩潰 找崩潰原因比較簡單,用 !analyze -v 命 ...
  • 在一些報表模塊中,需要我們根據用戶操作的名稱,來動態根據人員姓名,更新報表的簽名圖片,也就是電子手寫簽名效果,本篇隨筆介紹一下使用FastReport報表動態更新人員簽名圖片。 ...
  • 最新內容優先發佈於個人博客:小虎技術分享站,隨後逐步搬運到博客園。 創作不易,如果覺得有用請在Github上為博主點亮一顆小星星吧! 博主開始學習編程於11年前,年少時還只會使用cin 和cout ,給單片機點點燈。那時候,類似async/await 和future/promise 模型的認知還不是 ...
  • 之前在阿裡雲ECS 99元/年的活動實例上搭建了一個測試用的MINIO服務,以前都是直接當基礎設施來使用的,這次準備自己學一下S3相容API相關的對象存儲開發,因此有了這個小工具。目前僅包含上傳功能,後續計劃開發一個類似圖床的對象存儲應用。 ...
  • 目錄簡介快速入門安裝 NuGet 包實體類User資料庫類DbFactory增刪改查InsertSelectUpdateDelete總結 簡介 NPoco 是 PetaPoco 的一個分支,具有一些額外的功能,截至現在 github 星數 839。NPoco 中文資料沒多少,我是被博客園群友推薦的, ...
  • 前言 前面使用 Admin.Core 的代碼生成器生成了通用代碼生成器的基礎模塊 分組,模板,項目,項目模型,項目欄位的基礎功能,本篇繼續完善,實現最核心的模板生成功能,並提供生成預覽及代碼文件壓縮下載 準備 首先清楚幾個模塊的關係,如何使用,簡單畫一個流程圖 前面完成了基礎的模板組,模板管理,項目 ...
  • 假設需要實現一個圖標和文本結合的按鈕 ,普通做法是 直接重寫該按鈕的模板; 如果想作為通用的呢? 兩種做法: 附加屬性 自定義控制項 推薦使用附加屬性的形式 第一種:附加屬性 創建Button的附加屬性 ButtonExtensions 1 public static class ButtonExte ...
  • 在C#中,委托是一種引用類型的數據類型,允許我們封裝方法的引用。通過使用委托,我們可以將方法作為參數傳遞給其他方法,或者將多個方法組合在一起,從而實現更靈活的編程模式。委托類似於函數指針,但提供了類型安全和垃圾回收等現代語言特性。 基本概念 定義委托 定義委托需要指定它所代表的方法的原型,包括返回類 ...