Jetpack Compose实现对角线滚动效果
ALuoBo 人气:0缘起
不久前刷到 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
原生开发者不断减少,以及官方文档相对简陋,社区资料也比较匮乏,在出现不能覆盖需求的问题时,比较耗费时间去找到问题的答案,好在官方目前更新速度还是非常的快,目前也已经是达到可用甚至是易用的程度了,相信距离好用也不遥远。
加载全部内容