Jetpack Compose 实现iOS的回弹效果到底有多简单?跟着我,不难!(上)
前前言
已经开源,等不及的可以直接去看,去star,去fork! github.com/Cormor/Comp…
前言
说是回弹效果,其实根本点在于如何实现滑动越界。 之前写过一篇文章:
- 今天终于腾出空解决了 compose 新增 api 导致的 nestedScroll 速度错误。
- ……的一部分,另外的一点点小错误
影响不大,谷歌也一直不修复,我也懒得管了,敲!
- ……的一部分,另外的一点点小错误
- 今天实现了横向越界效果的支持,使用和纵向没什么不同。
- 甚至横向 + 纵向一起用 ——也是可以的
来,一起看看怎么实现。
如何实现越界效果?
预期效果
假设有界面如下——
首先只考虑画面效果的简单偏移,我们使用
Modifier.offset
即可让它偏移——
这和 View 中使用 X/Y 轴移动 ,或使用 translationX/Y 是一样的。
效果破绽
但是会有这样一种可怕的情况:
好么,这才是真正的【越界回弹】效果不是吗?!
但你说服不了自己的良心 (说服不了 产品/UI/动效)。
解决方案
- 在 View 中,我们可以外界套一个 FrameLayout 解决,因为
ViewGroup
默认 clipChildren = true
-
第一种显然不优雅,还增加嵌套层级——谁不知道 在 View 中布局要尽量避免嵌套 啊?
-
这后两者就得结合 translationX/Y 做计算,才能确定 clip 范围了。
淦,幸亏我们是 compose ! ——自带一个 Modifier.clipToBounds()
因为 Modifier
是顺序敏感的,所以我们 在偏移之前设置 clipBounds 就可以了.
Modifier
.clipToBounds()
.absoluteOffset { ... }
需要使用到的 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