敞开生长之旅!这是我参加「日新计划 2 月更文挑战」的第 1 天,点击检查活动详情。
缘起
不久前刷到 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
原生开发者不断削减,以及官方文档相对粗陋,社区资料也比较匮乏,在出现不能掩盖需求的问题时,比较消耗时刻去找到问题的答案,好在官方现在更新速度还是十分的快,现在也现已是到达可用乃至是易用的程度了,信任间隔好用也不悠远。
参考资料
Android Compose 动画运用详解(九)Animatable之衰减动画
chihsuanwu/compose-free-scroll
Android自定义ViewGroup嵌套与交互实战,幕布全屏翻滚作用