敞开生长之旅!这是我参加「日新计划 2 月更文挑战」的第 1 天,点击检查活动详情。

缘起

不久前刷到 newki 长辈的文章,用自定义 viewGroup的办法完成了如图作用: Android自定义ViewGroup嵌套与交互实战,幕布全屏翻滚作用

我当时的反响: new bee ! new bee ! 这作用不错

初试

大佬用 Android View 出来了,那能否用 Google 新一代 UI Compose 来整一个呢?

正好手上有本 fun 神写得书 《Jetpack Compose 从入门到实战》。这不就好办了么!

合理我 啪的一下,很快啊,吭! 开始举动之后,

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 官方摆了一道。

Jetpack Compose 版幕布效果,学习如何实现一个对角线滚动

探究

已然官方供给的开箱即用的 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嵌套与交互实战,幕布全屏翻滚作用