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 ,并且外国同行已经提了 issuegoogle 质问他们为何没有对角线滚动。但截止到今天 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对角线滚动内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Kotlin Jetpack组件ViewModel使用详解

    目录 1.ViewModel的出现 2.ViewModel的使用 基本步骤 ViewModel的作用 1.ViewModel的出现 ViewModel应该是Jetpack中最重要的组件之一了.在以前,Activity要负责逻辑处理,又要控制UI展示,还要处理网络回调,导致大型项目难以维护.于是,ViewModel来帮助Activity分担一部分工作,ViewModel就专门用于存放和界面相关的工作. 2.ViewModel的使用 基本步骤 在app/build.gradel文件添加依赖 depe

  • 官网项目Jetpack Startup库学习

    目录 简单认识一下Startup 项目代码 简单认识一下Startup nowinandroid项目作为目前google官方来演示MAD(现代Android开发技术)的示例项目,里面大量依赖运用了jetpack包下的各种库.通过分析学习这些库在项中的实际使用可以帮助我们比直接阅读库的文档来更好的理解和学习.希望通过学习后可以帮助到我们能熟练地在我们自己的项目中正确高效的使用到jetpack里面的各种强大库.不废话了,下面进入我们今天的正题——Startup App Startup  |  And

  • JetpackCompose Scaffold组件使用教程

    目录 搭设基本Scaffold页面 准备工作 主体编写 搭设基本Scaffold页面 scaffold 组件遵循 Material Design,可以协助开发者迅速构建对应框架页面 准备工作 首先在 drawable 文件夹内,添加几张 vector images,用作我们的底部导航栏图标 在主页面中声明数据类,表示单个图标以及其解释文本 data class Item( val name: String, val icon: Int ) 新增组件 mainBody,逐一添加三个底部按钮的图标

  • JetpackCompose Navigation导航实现流程

    目录 Navigation 快速上手 编写欲跳转的两个页面 路由表配置 总路由 主页面调用总路由 跳转逻辑完善 Navigation 快速上手 下面案例简要展示使用 Compose 版本的 Navigation 库来实现两个页面之间的跳转 这是完整的结构(忽略掉红线划过的那个包) 编写欲跳转的两个页面 编写 Demo1 页面 子页面使用多个 composable 组件相组合的方法一一装配起来 Demo1main 渲染整个页面,他接受一个 NavController 对象,用于操纵导航相关的方法

  • Android Jetpack组件ViewModel基本用法详解

    目录 引言 一.概述与作用 二.基本用法 小结 引言 天道好轮回,终于星期五,但是还是忙碌了一天.在项目中,我遇到了一个问题,起因则是无法实时去获取信息来更新UI界面,因为我需要知道我是否获取到了实时信息,我想到的办法有三,利用Handler收发消息在子线程与主线程切换从而更新信息,其二则是利用在页面重绘的时候(一般是页面变动如跳转下个页面和将应用切至后台),其三就是利用Jetpack中最重要的组件之一ViewModel,最后我还是选择了ViewModel,因为感觉更方便. 其实想到的前面两个方

  • 移动端开发之Jetpack Hilt技术实现解耦

    目录 Hilt是什么 Hilt使用地方 依赖注入(DI)概念 Hilt使用 导入 Hilt是什么 Hilt 是基于 Dagger2 的针对 Android场景定制化 的框架. 这有点像什么? RxAndroid 是 RxJava 的Android平台定制化扩展.Andorid虽然由Java.Kotlin构成,但是它有很多平台的特性,比如它有 Java开发 所不知道的 Context 等. Dagger框架虽然很出名,在国外也很流行,但是在国内使用其的App少之又少,列举一些缺点: 上手难,众多A

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

    目录 缘起 初试 探索 学习 FreeScrollState freeScroll 总结 缘起 不久前刷到 newki 前辈的文章,用自定义 viewGroup的方式实现了如图效果: Android自定义ViewGroup嵌套与交互实战,幕布全屏滚动效果 我当时的反应: new bee ! new bee ! 这效果不错 初试 大佬用 Android View 出来了,那能否用 Google 新一代 UI Compose 来整一个呢? 正好手上有本 fun 神写得书 <Jetpack Compo

  • 利用Jetpack Compose实现绘制五角星效果

    目录 说明 自定义星行Modifier 原理 实现 代码 最终实现效果 说明 compose中我们的所有ui操作,包括一些行为,例如:点击.手势等都需要使用Modifier来进行操作.因此对Modifier的理解可以帮助我们解决很多问题的 自定义星行Modifier 本文我们打算自定义一个Modifier,通过这个modifier我们可以实现用一个操作符就画出五角星的效果 原理 我们实现绘制五角星的原理如下图,首先我们会虚构两个圆,将内圆和外圆角度平分五份,然后依次连接内圆和外圆的切点的坐标,然

  • Android Jetpack Compose实现列表吸顶效果

    目录 stickyHeader 实体类 加载假数据 吸顶标题 二级条目 完整代码 效果图 安卓传统的 Recyclerview 打造悬浮头部StickyHeader的吸顶效果,十分麻烦,而在Compose中就简单多了 stickyHeader Compose设计的时候考虑得很周到,他们提供了stickyHeader 作用就是添加一个粘性标题项,即使在它后面滚动时也会保持固定.标头将保持固定,直到下一个标头取而代之. 参数key - 表示唯一的密钥键. 它不允许对列表出现使用相同的键.密钥的类型应

  • 通过Jetpack Compose实现双击点赞动画效果

    目录 实现步骤 先红色画个爱心 点击事件加动画 完整代码 效果图 实现步骤 先红色画个爱心 Icon( Icons.Filled.Favorite, "爱心", Modifier .align(Alignment.Center) tint = Color.Red ) 点击事件加动画 双击监听 .pointerInput(Unit) { detectTapGestures( onDoubleTap = { ... } ) } #### **API 介绍** | API名称 | 作用 |

  • Jetpack Compose实现动画效果的方法详解

    目录 概述 低级别动画API animate*AsState 使用Animatable实现颜色变化效果 使用updateTransition实现颜色和圆角动画 rememberInfiniteTransition TargetBasedAnimation 自定义动画 AnimationSpec Easing AnimationVector 高级动画 概述 compose 为支持动画提供了大量的 api,通过这些 api 我们可以轻松实现动画效果 ps:这些 api 的原理与 Flutter 很接

  • Jetpack Compose实现列表和动画效果详解

    目录 创建一个列表消息卡片 可交互的动画效果 创建一个列表消息卡片 到目前为止,我们只有一个消息的卡片,看上去有点单调,所以让我们来改善它,让它拥有多条信息.我们需要创建一个能够显示多条消息的函数.对于这种情况,我们可以使用 Compose 的 LazyColumn 和 LazyRow.这些 Composable 只渲染屏幕上可见的元素,所以它们的设计对于长列表来说很有效果.同时,它们避免了 RecyclerView 与 XML 布局的复杂性. import androidx.compose.f

  • Jetpack Compose布局的使用详细介绍

    目录 一.标准布局组件 二.修饰符 三.滑动组件 1.ScrollableRow和ScrollableColumn 2.LazyRowFor和LazyColumnFor 一.标准布局组件 Compose中可以将多个控件元素组合使用,例如下面这样, @Composable fun WidgetGroup() { Text(text = "不为往事扰") Text(text = "余生只愿笑") } 但是我们会发现,如果仅仅是这样,两个文本控件会重叠在一起,类似于下面这

  • Jetpack Compose重写TopAppBar实现标题多行折叠详解

    目录 前言 MediumTopAppBar 阅读源码 核心 解决方法 重写TopAppBarLayout 完整代码 前言 想用composes实现类似掘金的文章详细页面的标题栏 上滑隐藏标题后标题栏显示标题 compose.material3下的TopAppBar不能嵌套滚动 MediumTopAppBar 便使用了MediumTopAppBar一开始用着没什么问题,但是标题字数多了,MediumTopAppBar就不支持了,最多就两行,进入源码一看就明白了 @ExperimentalMater

  • Jetpack Compose Canvas绘制超详细介绍

    目录 1. Canvas 2. 绘制方法 1. drawLine 2. drawRect 3. drawRoundRect 4. drawImage 5. drawCircle 6. drawArc 7. drawPath 8. drawPoints 3. DrawScope拓展方法 1. inset 2. translate 3. rotate与rotateRad 4. scale 5. clipRect 6. drawIntoCanvas 7. withTransform 4.参考 1. C

  • Jetpack Compose常用组件详细介绍

    目录 1. Text 2. Image 3. LazyColumn 1. Text 日常最常用的应该就是显示文字,所以有必要说一下Text控件.首先源码如下: @Composable fun Text( text: String, modifier: Modifier = Modifier, color: Color = Color.Unspecified, fontSize: TextUnit = TextUnit.Unspecified, fontStyle: FontStyle? = nu

随机推荐