前前语

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

全篇已完成,分3期。

中下两期:

Jetpack Compose 完成iOS的回弹作用到底有多简略?跟着我,不难!(中)

Compose 用 nestedScroll 完成iOS的回弹作用,还要帮谷歌修bug?(完结)

前语

说是回弹作用,其实根本点在于怎样完成滑动越界。 之前写过一篇文章:

《# Jetpack Compose 开源实践 – 70行完成与iOS完全一致的滑动越界作用,封装成到处可用的Modifier!》

  • 今日终于腾出空处理了 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的回弹效果到底有多简单?跟着我,不难!(上)

  • 咱们能够经过 outline 裁剪处理
    • 见我的文章:小白也能懂的RecyclerView界面动效定制,让ExpandableListView完全退出舞台!(下)
  • 咱们能够经过自定义view ,然后 canvasclipRect / clipPath 处理

  • 第一种明显不高雅,还增加嵌套层级——谁不知道 在 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() 的运用。

  • 估计明天搞定。
  • 已经搞定:/post/726855…

下期预览

假如 中期顺利的话,或许就一同写了,没有下期。 这一期会讲讲 为什么 NestedScroll咱们按照提示写完了,却仍旧不能正常运转。

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

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

issuetracker.google.com/issues/2766…

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

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