欢迎访问移动开发之家(rcyd.net),关注移动开发教程。移动开发之家  移动开发问答|  每日更新
页面位置 : > > > 内容正文

Jetpack Compose实现对角线滚动效果,

来源: 开发者 投稿于  被查看 48899 次 评论:98

Jetpack Compose实现对角线滚动效果,


目录
  • 缘起
  • 初试
  • 探索
  • 学习
    • 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对角线滚动内容请搜索3672js教程以前的文章或继续浏览下面的相关文章希望大家以后多多支持3672js教程!

    您可能感兴趣的文章:
    • 官网项目Jetpack Startup库学习
    • Android Jetpack组件ViewModel基本用法详解
    • JetpackCompose Scaffold组件使用教程
    • JetpackCompose Navigation导航实现流程
    • Kotlin Jetpack组件ViewModel使用详解
    • 移动端开发之Jetpack Hilt技术实现解耦

    用户评论