Jetpack?Compose實(shí)現(xiàn)對角線滾動效果
緣起
不久前刷到 newki 前輩的文章,用自定義 viewGroup的方式實(shí)現(xiàn)了如圖效果: Android自定義ViewGroup嵌套與交互實(shí)戰(zhàn),幕布全屏滾動效果

我當(dāng)時的反應(yīng): new bee ! new bee ! 這效果不錯
初試
大佬用 Android View 出來了,那能否用 Google 新一代 UI Compose 來整一個呢?
正好手上有本 fun 神寫得書 《Jetpack Compose 從入門到實(shí)戰(zhàn)》。這不就好辦了么!
正當(dāng)我 啪的一下,很快啊,吭! 開始行動之后,
拿著書翻到了手勢處理這一章節(jié),找到了這個:
Scrollable,當(dāng)視圖組件的寬度或長度超出屏幕邊界時,我們希望能滑動查看更多的內(nèi)容... 這不就完事了么,隨便寫個 composable 加一個 Modifier.scrollable即可實(shí)現(xiàn)滑動效果
但是,緊接著一句話 “Orientation 僅有 Horizontal 與 Vertical 可供選擇,這說明我們只能監(jiān)聽水平或垂直方向的滾動。”
那我們?nèi)绻o一個組合同時添加兩個方向的scrollable呢? 比如這樣:
private fun TwoOrientaionScrollView(modifier: Modifier = Modifier) {
val horizontalScrollState = rememberScrollState()
val verticalScrollState = rememberScrollState()
Column(modifier = modifier
.horizontalScroll(horizontalScrollState)
.verticalScroll(verticalScrollState)
) {
...
}
}經(jīng)過測試,這種方法只能實(shí)現(xiàn)在兩個方向滑動(垂直,水平)且每次手勢只有一個方向在滑動,我們要達(dá)到目標(biāo)效果,那必須是要支持斜著滑動的。
大意了,沒有閃,被 Android 官方擺了一道。
探索
既然官方提供的開箱即用的 API 無法滿足我們的要求,那我們就需要動手去定制一個特殊的手勢處理規(guī)則去實(shí)現(xiàn)。
那萬能的互聯(lián)網(wǎng)中有沒有大佬已經(jīng)用compose自定義手勢實(shí)現(xiàn)了呢?
可是找遍了 google 百度 chatGPT 也沒有找到什么有價值的文章值得去參考,倒是在Stack Overflow上一番翻箱倒柜之后,找到了一個線索————這種需求叫做 對角線滾動 / diagonal scroll ,并且外國同行已經(jīng)提了 issue 給 google 質(zhì)問他們?yōu)楹螞]有對角線滾動。但截止到今天 2023/2/7 仍舊google沒有提供新的api也沒有關(guān)閉這個問題。
插一句,不知道為何隔壁鴻蒙原本是支持自由方向滾動的,鴻蒙稱之為 Orientation.free , 但是在 api v9 時卻把這個方向給廢棄了
當(dāng)我愈發(fā)苦惱時,我把 diagonal scroll鍵入交友網(wǎng)站github時,一道閃光出現(xiàn)了
chihsuanwu/compose-free-scroll:提供可讓組合自由滾動的 modifier
這是來自臺灣省的開發(fā)者的開源項(xiàng)目,作者也已經(jīng)發(fā)布到遠(yuǎn)程倉,可以讓大家一鍵導(dǎo)入并極速使用
測試效果:

完美!
學(xué)習(xí)
接下來一起學(xué)習(xí)一下大佬的代碼吧 ,核心代碼:
FreeScrollState.kt用來表示滑動狀態(tài),并提供了滑動到指定位置的方法FreeScroll.kt實(shí)現(xiàn)允許對角線滾動的modifier
FreeScrollState
內(nèi)部使用兩個 ScrollState 分別控制水平和垂直滾動的 state
class FreeScrollState(
val horizontalScrollState: ScrollState,
val verticalScrollState: ScrollState,
) {
...
}
// 用rememberScrollState 分別創(chuàng)建兩個方向的 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,
)
}值得一提的是,可以學(xué)習(xí)到作者使用協(xié)程來處理 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() 來同時獲取兩個結(jié)果
Offset(xOffset.await(), yOffset.await())
}freeScroll
這是一個Modifier的拓展方法,在這個方法中,實(shí)現(xiàn)了自定義手勢邏輯。
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 拖拽結(jié)束時
onEnd(velocityTracker, state, flingSpec, this)
}
)
}
}
}可以看到,核心就是PointerInput中采用detectDraGestures 拖拽監(jiān)聽,并聲明了一個速度追蹤 器velocityTracker,和一個衰減動畫 rememberSplineBasedDecay 來使拖拽結(jié)束有一段慣性運(yùn)動也就是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抽出一個方法,方法中,我們將拖拽的過程中的手勢點(diǎn)位添加到速度追蹤 器velocityTracker中不斷精確我們得滾動速度。并將位置點(diǎn)位更新到兩個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()
}
}
}
在拖拽結(jié)束后,從velocityTracker拿出估算的速度值,用來給設(shè)置fling的衰減滾動動畫。 也就是說實(shí)際上滾動效果== 拖拽移動 + fling。
總結(jié)
JetPack Compose 是一個很強(qiáng)大很現(xiàn)代的 UI 工具,與使用自定義 View 來實(shí)現(xiàn)復(fù)雜手勢以及動畫效果時,代碼量大大減少,更加靈活。但是現(xiàn)在由于一方面 Android 原生開發(fā)者不斷減少,以及官方文檔相對簡陋,社區(qū)資料也比較匱乏,在出現(xiàn)不能覆蓋需求的問題時,比較耗費(fèi)時間去找到問題的答案,好在官方目前更新速度還是非常的快,目前也已經(jīng)是達(dá)到可用甚至是易用的程度了,相信距離好用也不遙遠(yuǎn)。
到此這篇關(guān)于Jetpack Compose實(shí)現(xiàn)對角線滾動效果的文章就介紹到這了,更多相關(guān)Jetpack Compose對角線滾動內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Jetpack Compose按鈕組件使用實(shí)例詳細(xì)講解
- Jetpack Compose圖片組件使用實(shí)例詳細(xì)講解
- Jetpack Compose之選擇器使用實(shí)例講解
- Jetpack Compose實(shí)現(xiàn)對話框和進(jìn)度條實(shí)例解析
- Android使用Jetpack Compose開發(fā)零基礎(chǔ)起步教程
- Jetpack?Compose基礎(chǔ)組件之文字組件
- 融會貫通Android?Jetpack?Compose中的Snackbar
- 使用Jetpack Compose實(shí)現(xiàn)翻轉(zhuǎn)卡片效果流程詳解
相關(guān)文章
flutter?Bloc?add兩次只響應(yīng)一次問題解析
這篇文章主要為大家介紹了flutter?Bloc?add兩次只響應(yīng)一次問題解析記錄,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11
Android ProgressBar直線進(jìn)度條的實(shí)例代碼
本文通過實(shí)例代碼給大家介紹了android progressbar直線進(jìn)度條的實(shí)現(xiàn)方法,非常不錯,具有參考借鑒價值,需要的朋友參考下吧2017-06-06
自己實(shí)現(xiàn)的android樹控件treeview
在項(xiàng)目中經(jīng)常需要一個需要一個樹狀框架,因?yàn)橐恍┰驔]有使用系統(tǒng)自帶的控件,所以就自己寫了一個,現(xiàn)在分享給大家2014-01-01
Android 仿微博的點(diǎn)贊功能的實(shí)現(xiàn)原理(持續(xù)點(diǎn)贊再取消)
經(jīng)常玩微博的同志都知道,微博的持續(xù)點(diǎn)贊再取消功能,下面小編給大家?guī)砹薃ndroid 仿微博的點(diǎn)贊功能的實(shí)現(xiàn)原理(持續(xù)點(diǎn)贊再取消),感興趣的朋友跟隨腳本之家小編一起看看吧2018-03-03
Android自定義加載控件實(shí)現(xiàn)數(shù)據(jù)加載動畫
這篇文章主要為大家詳細(xì)介紹了Android自定義加載控件實(shí)現(xiàn)數(shù)據(jù)加載動畫的相關(guān)資料,仿美團(tuán)、京東數(shù)據(jù)加載動畫、小人奔跑動畫,感興趣的小伙伴們可以參考一下2016-04-04
Android 根據(jù)手勢頂部View自動展示與隱藏效果
這篇文章主要介紹了Android 根據(jù)手勢頂部View自動展示與隱藏效果,本文給大家介紹非常詳細(xì)包括實(shí)現(xiàn)原理和實(shí)例代碼,需要的朋友參考下吧2017-08-08
Android實(shí)現(xiàn)取消GridView中Item選中時默認(rèn)的背景色
這篇文章主要介紹了Android實(shí)現(xiàn)取消GridView中Item選中時默認(rèn)的背景色,涉及Android GridView中Item屬性設(shè)置的相關(guān)技巧,需要的朋友可以參考下2016-02-02
Android短信驗(yàn)證碼監(jiān)聽解決onChange多次調(diào)用的方法
本篇文章主要介紹了Android短信驗(yàn)證碼監(jiān)聽解決onChange多次調(diào)用的方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-03-03
Android使用DrawerLayout實(shí)現(xiàn)仿QQ雙向側(cè)滑菜單
這篇文章主要介紹了Android使用DrawerLayout實(shí)現(xiàn)仿QQ雙向側(cè)滑菜單的方法和詳細(xì)代碼,有需要的小伙伴可以認(rèn)真參考下。2016-01-01
Android中ListView分頁加載數(shù)據(jù)功能實(shí)現(xiàn)
本篇文章主要介紹了Android中ListView分頁加載數(shù)據(jù)功能實(shí)現(xiàn),具有一定的參考價值,有需要的可以了解一下。2016-11-11

