目录
- 缘起
- 初试
- 探索
- 学习
- FreeScrollState
- freeScroll
- 总结
缘起
不久前刷到 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对角线滚动内容请搜索自由互联以前的文章或继续浏览下面的相关文章希望大家以后多多支持自由互联!