Jetpack Compose實現對角線滾動效果

緣起

不久前刷到 newki 前輩的文章,用自定義 viewGroup的方式實現瞭如圖效果: Android自定義ViewGroup嵌套與交互實戰,幕佈全屏滾動效果

我當時的反應: new bee ! new bee ! 這效果不錯

初試

大佬用 Android View 出來瞭,那能否用 Google 新一代 UI Compose 來整一個呢?

正好手上有本 fun 神寫得書 《Jetpack Compose 從入門到實戰》。這不就好辦瞭麼!

正當我 啪的一下,很快啊,吭! 開始行動之後,

拿著書翻到瞭手勢處理這一章節,找到瞭這個:

Scrollable,當視圖組件的寬度或長度超出屏幕邊界時,我們希望能滑動查看更多的內容… 這不就完事瞭麼,隨便寫個 composable 加一個 Modifier.scrollable即可實現滑動效果

但是,緊接著一句話 “Orientation 僅有 Horizontal 與 Vertical 可供選擇,這說明我們隻能監聽水平或垂直方向的滾動。”

那我們如果給一個組合同時添加兩個方向的scrollable呢? 比如這樣:

private fun TwoOrientaionScrollView(modifier: Modifier = Modifier) {
    val horizontalScrollState = rememberScrollState()
    val verticalScrollState = rememberScrollState()
    Column(modifier = modifier
        .horizontalScroll(horizontalScrollState)
        .verticalScroll(verticalScrollState)
    ) {
        ...
    }
}

經過測試,這種方法隻能實現在兩個方向滑動(垂直,水平)且每次手勢隻有一個方向在滑動,我們要達到目標效果,那必須是要支持斜著滑動的。

大意瞭,沒有閃,被 Android 官方擺瞭一道。

探索

既然官方提供的開箱即用的 API 無法滿足我們的要求,那我們就需要動手去定制一個特殊的手勢處理規則去實現。

那萬能的互聯網中有沒有大佬已經用compose自定義手勢實現瞭呢?

可是找遍瞭 google 百度 chatGPT 也沒有找到什麼有價值的文章值得去參考,倒是在Stack Overflow上一番翻箱倒櫃之後,找到瞭一個線索————這種需求叫做 對角線滾動 / diagonal scroll ,並且外國同行已經提瞭 issue 給 google 質問他們為何沒有對角線滾動。但截止到今天 2023/2/7 仍舊google沒有提供新的api也沒有關閉這個問題。

插一句,不知道為何隔壁鴻蒙原本是支持自由方向滾動的,鴻蒙稱之為 Orientation.free , 但是在 api v9 時卻把這個方向給廢棄瞭

當我愈發苦惱時,我把 diagonal scroll鍵入交友網站github時,一道閃光出現瞭

chihsuanwu/compose-free-scroll:提供可讓組合自由滾動的 modifier

這是來自臺灣省的開發者的開源項目,作者也已經發佈到遠程倉,可以讓大傢一鍵導入並極速使用

測試效果:

完美!

學習

接下來一起學習一下大佬的代碼吧 ,核心代碼:

  • FreeScrollState.kt 用來表示滑動狀態,並提供瞭滑動到指定位置的方法
  • FreeScroll.kt實現允許對角線滾動的 modifier

FreeScrollState

內部使用兩個 ScrollState 分別控制水平和垂直滾動的 state

class FreeScrollState(
    val horizontalScrollState: ScrollState,
    val verticalScrollState: ScrollState,
) { 
        ...
}
// 用rememberScrollState 分別創建兩個方向的 scrollState
@Composable
fun rememberFreeScrollState(initialX: Int = 0, initialY: Int = 0): FreeScrollState {
    val horizontalScrollState = rememberScrollState(initialX)
    val verticalScrollState = rememberScrollState(initialY)
    return FreeScrollState(
        horizontalScrollState = horizontalScrollState,
        verticalScrollState = verticalScrollState,
    )
}

值得一提的是,可以學習到作者使用協程來處理 scrollBy, scrollTo 以及 animateScrollBy animateScrollTo , 例如:

suspend fun scrollTo(
    x: Int,
    y: Int,
): Offset = coroutineScope {
    val xOffset = async {
        horizontalScrollState.scrollTo(x)
    }
    val yOffset = async {
        verticalScrollState.scrollTo(y)
    }
    // 使用 async.awawit() 來同時獲取兩個結果
    Offset(xOffset.await(), yOffset.await()) 
}

freeScroll

這是一個Modifier的拓展方法,在這個方法中,實現瞭自定義手勢邏輯。

fun Modifier.freeScroll(
    state: FreeScrollState,
    enabled: Boolean = true
): Modifier = composed {
    val velocityTracker = remember { VelocityTracker() }
    val flingSpec = rememberSplineBasedDecay<Float>()
    this.verticalScroll(state = state.verticalScrollState, enabled = false)
        .horizontalScroll(state = state.horizontalScrollState, enabled = false)
        .pointerInput(enabled) {
            if (!enabled) return@pointerInput
            coroutineScope {
                detectDragGestures(
                    onDragStart = { },
                    onDrag = { change, dragAmount ->
                        change.consume()
                        //1 拖拽中
                        onDrag(change, dragAmount, state, velocityTracker, this) 
                      
                    },
                    onDragEnd = {
                        //2 拖拽結束時
                        onEnd(velocityTracker, state, flingSpec, this)
                        
                    }
                )
            }
        }
}

可以看到,核心就是PointerInput中采用detectDraGestures 拖拽監聽,並聲明瞭一個速度追蹤 器velocityTracker,和一個衰減動畫 rememberSplineBasedDecay 來使拖拽結束有一段慣性運動也就是fling

@OptIn(ExperimentalComposeUiApi::class)
private fun onDrag(
    change: PointerInputChange,
    dragAmount: Offset,
    state: FreeScrollState,
    velocityTracker: VelocityTracker,
    coroutineScope: CoroutineScope
) {
    // Add historical position to velocity tracker to increase accuracy
    val changeList = change.historical.map {
        it.uptimeMillis to it.position
    } + (change.uptimeMillis to change.position)

    changeList.forEach { (time, pos) ->
        val position = Offset(
            pos.x - state.horizontalScrollState.value,
            pos.y - state.verticalScrollState.value
        )
        velocityTracker.addPosition(time, position)
    }

    coroutineScope.launch {
        state.horizontalScrollState.scrollBy(-dragAmount.x)
        state.verticalScrollState.scrollBy(-dragAmount.y)
    }
}

onDrag抽出一個方法,方法中,我們將拖拽的過程中的手勢點位添加到速度追蹤 器velocityTracker中不斷精確我們得滾動速度。並將位置點位更新到兩個scrollState

private fun onEnd(
    velocityTracker: VelocityTracker,
    state: FreeScrollState,
    flingSpec: DecayAnimationSpec<Float>,
    coroutineScope: CoroutineScope
) {
    val velocity = velocityTracker.calculateVelocity()
    velocityTracker.resetTracking()

    // Launch two animation separately to make sure they work simultaneously.
    coroutineScope.launch {
        state.horizontalScrollState.fling(-velocity.x, flingSpec)
    }
    coroutineScope.launch {
        state.verticalScrollState.fling(-velocity.y, flingSpec)
    }
}
private suspend fun ScrollState.fling(initialVelocity: Float, flingDecay: DecayAnimationSpec<Float>) {
    if (abs(initialVelocity) < 0.1f) return // Ignore flings with very low velocity

    scroll {
        var lastValue = 0f
        AnimationState(
            initialValue = 0f,
            initialVelocity = initialVelocity,
        ).animateDecay(flingDecay) {
            val delta = value - lastValue
            val consumed = scrollBy(delta)
            lastValue = value
            // avoid rounding errors and stop if anything is unconsumed
            if (abs(delta - consumed) > 0.5f) this.cancelAnimation()
        }
    }
}

在拖拽結束後,從velocityTracker拿出估算的速度值,用來給設置fling的衰減滾動動畫。 也就是說實際上滾動效果== 拖拽移動 + fling。

總結

JetPack Compose 是一個很強大很現代的 UI 工具,與使用自定義 View 來實現復雜手勢以及動畫效果時,代碼量大大減少,更加靈活。但是現在由於一方面 Android 原生開發者不斷減少,以及官方文檔相對簡陋,社區資料也比較匱乏,在出現不能覆蓋需求的問題時,比較耗費時間去找到問題的答案,好在官方目前更新速度還是非常的快,目前也已經是達到可用甚至是易用的程度瞭,相信距離好用也不遙遠。

到此這篇關於Jetpack Compose實現對角線滾動效果的文章就介紹到這瞭,更多相關Jetpack Compose對角線滾動內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: