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!
推薦閱讀:
- Android VelocityTracker使用案例詳解
- Android Jetpack Compose實現列表吸頂效果
- Jetpack Compose佈局的使用詳細介紹
- Jetpack Compose實現動畫效果的方法詳解
- 利用Jetpack Compose繪制可愛的天氣動畫