likes
comments
collection
share

Jetpack Compose 实现iOS的回弹效果到底有多简单?跟着我,不难!(上)

作者站长头像
站长
· 阅读数 68
前前言

已经开源,等不及的可以直接去看,去star,去fork! github.com/Cormor/Comp…

前言

说是回弹效果,其实根本点在于如何实现滑动越界。 之前写过一篇文章:

  • 今天终于腾出空解决了 compose 新增 api 导致的 nestedScroll 速度错误。
    • ……的一部分,另外的一点点小错误影响不大谷歌也一直不修复,我也懒得管了,敲!
  • 今天实现了横向越界效果的支持,使用和纵向没什么不同。
    • 甚至横向 + 纵向一起用 ——也是可以的

来,一起看看怎么实现。

如何实现越界效果?

预期效果

假设有界面如下——

Jetpack Compose 实现iOS的回弹效果到底有多简单?跟着我,不难!(上) 首先只考虑画面效果的简单偏移,我们使用 Modifier.offset 即可让它偏移——

Jetpack Compose 实现iOS的回弹效果到底有多简单?跟着我,不难!(上)

这和 View 中使用 X/Y 轴移动 ,或使用 translationX/Y 是一样的。

效果破绽

但是会有这样一种可怕的情况:

Jetpack Compose 实现iOS的回弹效果到底有多简单?跟着我,不难!(上)

好么,这才是真正的【越界回弹】效果不是吗?!

但你说服不了自己的良心 (说服不了 产品/UI/动效)

解决方案

  • View 中,我们可以外界套一个 FrameLayout 解决,因为 ViewGroup 默认 clipChildren = true

Jetpack Compose 实现iOS的回弹效果到底有多简单?跟着我,不难!(上)


  • 第一种显然不优雅,还增加嵌套层级——谁不知道 在 View 中布局要尽量避免嵌套 啊?

  • 这后两者就得结合 translationX/Y 做计算,才能确定 clip 范围了。

淦,幸亏我们是 compose ! ——自带一个 Modifier.clipToBounds()

因为 Modifier 是顺序敏感的,所以我们 在偏移之前设置 clipBounds 就可以了.

Modifier
    .clipToBounds()
    .absoluteOffset { ... }

Jetpack Compose 实现iOS的回弹效果到底有多简单?跟着我,不难!(上)

需要使用到的 Modifier

根据上文——

  • 我们有一个必要clipToBounds 以确保效果正确。
  • 我们需要有一个 absoluteOffset 语义的 Modifier ,以实现内容偏移。
    • 它不能是 offset ,它会在 RTL 布局中自动反向。
    • absoluteOffset 需要在 lambda 中传入 IntOffset 对象,但我们在当前场景中的移动要么是垂直方向,要么是水平方向
    • 越界滑动的过程,肯定是一堆 Float 变量计算
    • ——所以我们该使用 graphicsLayer,直接传入单个的 float 值给 translationX/Y ,会舒服很多
  • 我们不能只考虑越界效果
    • 我们得支持和可滚动组件一同使用
    • 我们甚至得考虑嵌套滚动 —— NestedScroll 场景,想想就头大。
    • 所以我们需要赶紧攻读 Modifier.nestedScroll() 的使用说明。

整合

所以根据上文,我们可以先写出这么一个基本的 Modifier 出来,再填充内容。

fun Modifier.overScrollOutOfBound(
    // 是否是垂直方向
    isVertical: Boolean = true,
    // 当我需要越界时,要不要考虑parent的意见呢?
    nestedScrollToParent: Boolean = true,
    // 越界发生时,越远越拉不动的阻尼效果怎么实现?预留一个函数在这里,后面实现
    scrollEasing: (currentOffset: Float, newOffset: Float) -> Float,
    // 回弹时肯定是弹簧嘛,留俩弹簧参数以供自定义
    springStiff: Float = OutBoundSpringStiff,
    springDamp: Float = OutBoundSpringDamp,
): Modifier = composed { // composed {} 属于 Modifier 标准写法
   // ...
}

Modifier 内部脉络

上面 Modifier 写出来后,我们得往内部写个大概内容了。

// 可变参数变化时需要重组,重组需要按需重新生成对象。
// 所以我们把这些变化统一成一个参数
// 后面创建的对象统一观察这一个参数即可
val hasChangedParams = remember(nestedScrollToParent, springStiff, springDamp, isVertical) { 
    // 用Android提供的纳秒吧
    SystemClock.elapsedRealtimeNanos()
}

// 偏移值,根据 isVertical 决定自己是x还是y
var offset by remember(hasChangedParams) {
    mutableFloatStateOf(0f) 
}

// dispatcher 和 nestedScrollConnection
// Modifier.nestedScroll() 的俩必备参数
val dispatcher = remember(hasChangedParams) {
    NestedScrollDispatcher()
}
val nestedConnection = remember(hasChangedParams) {
    object : NestedScrollConnection {
        override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset
        override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset
        override suspend fun onPreFling(available: Velocity): Velocity

        override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity
    }
}

// this 是 this Modifier,Modifier 标准写法
// 放到最后面作为 return
this
    .clipToBounds()
    .nestedScroll(nestedConnection, dispatcher)
    .graphicsLayer {
        // 很好理解吧?
        if (isVertical) {
            translationY = offset
        } else {
            translationX = offset
        }
    }

中期预览

要写的还挺多的,这期讲梗概,下期解析 Modifier.nestedScroll() 的使用。

下期预览

如果 中期顺利的话,可能就一起写了,没有下期。 这一期会讲讲 为什么 NestedScroll我们按照提示写完了,却依旧不能正常运行。

——原来是谷歌从 1.4.0-alpha02 起引入了 BUG !

(issue提了那么久了还不修复,快去帮我+1)

issuetracker.google.com/issues/2766…

——什么?bug还不止一个?真正导致不能正常运行的是另一个bug?

(这个还没来得及提issue,所以赶紧通过点赞收藏催我更新我更新完文章了才有精力去提bug

转载自:https://juejin.cn/post/7268122742402564107
评论
请登录