Skip to content

Commit 07fc5bd

Browse files
committed
1.新增demo:实时调整的springDamp示例
2.优化结构:demo的代码结构稍微优化 3.优化逻辑和性能:overscroll现在改用mutableState对象,使得可以少一些协程上的使用和可能的性能损失 4.优化接口:新增parabolaScrollEasing function,帮助定制scrollEasing 5.优化注释:接口注释都用英文,有需要的老哥自行翻译下
1 parent 9f1ad4d commit 07fc5bd

2 files changed

Lines changed: 114 additions & 63 deletions

File tree

app/src/main/java/com/cormor/composeoverscroll/MainActivity.kt

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.os.Bundle
44
import androidx.activity.ComponentActivity
55
import androidx.activity.compose.setContent
66
import androidx.compose.foundation.background
7+
import androidx.compose.foundation.layout.Arrangement
78
import androidx.compose.foundation.layout.Column
89
import androidx.compose.foundation.layout.fillMaxSize
910
import androidx.compose.foundation.layout.fillMaxWidth
@@ -18,6 +19,7 @@ import androidx.compose.runtime.getValue
1819
import androidx.compose.runtime.mutableStateOf
1920
import androidx.compose.runtime.remember
2021
import androidx.compose.runtime.setValue
22+
import androidx.compose.ui.Alignment
2123
import androidx.compose.ui.Modifier
2224
import androidx.compose.ui.graphics.Color
2325
import androidx.compose.ui.unit.dp
@@ -35,10 +37,18 @@ class MainActivity : ComponentActivity() {
3537
@Composable fun DemoPage() {
3638
// overscrollVertical 需放在scroll相关Modifier前面
3739
// 注意,可滚动的Compose中嵌套可滚动项,需要设置高度/量算规则以帮助量算,否则量算时遇到无限高度的可滚动项目会崩溃
38-
var sliderValue by remember { mutableStateOf(300f) }
40+
var springStiff by remember { mutableStateOf(300f) }
41+
var springDamp by remember { mutableStateOf(1f) }
3942
// 整体可滚动+overscroll
40-
Column(Modifier.fillMaxSize().overScrollVertical(false, springStiff = sliderValue).verticalScroll(rememberScrollState())) {
41-
Slider(sliderValue, { sliderValue = it }, Modifier.fillMaxWidth(), valueRange = 1f..1000f)
43+
Column(Modifier.fillMaxSize().overScrollVertical(false, springStiff = springStiff, springDamp = springDamp).verticalScroll(rememberScrollState())) {
44+
Column(Modifier.height(100.dp), Arrangement.Center, Alignment.CenterHorizontally) {
45+
Text("springStiff=$springStiff")
46+
Slider(springStiff, { springStiff = it }, Modifier.fillMaxWidth(), valueRange = 1f..1000f)
47+
}
48+
Column(Modifier.height(100.dp), Arrangement.Center, Alignment.CenterHorizontally) {
49+
Text("springDamp=$springDamp")
50+
Slider(springDamp, { springDamp = it }, Modifier.fillMaxWidth(), valueRange = Float.MIN_VALUE..1f)
51+
}
4252
// 普通的lazyColumn
4353
LazyColumn(Modifier.fillMaxWidth().weight(1f)) {
4454
items(15, { "${it}_1" }, { 1 }) {
@@ -52,13 +62,23 @@ class MainActivity : ComponentActivity() {
5262
}
5363
item(contentType = "inner nested") {
5464
// 该LazyColumn nestedScrollToParent = false
55-
LazyColumn(Modifier.fillMaxWidth().height(300.dp).background(Color.Yellow).overScrollVertical(false)) {
65+
LazyColumn(Modifier
66+
.fillMaxWidth()
67+
.height(300.dp)
68+
.background(Color.Yellow)
69+
.overScrollVertical(false, springStiff = springStiff, springDamp = springDamp)
70+
) {
5671
items(15, { "${it}_3-" }, { 1 }) {
5772
Content(it)
5873
}
5974
item(contentType = "inner inner nested Item") {
6075
// 多重嵌套
61-
LazyColumn(Modifier.fillMaxWidth().height(100.dp).background(Color.Green).overScrollVertical(false)) {
76+
LazyColumn(Modifier
77+
.fillMaxWidth()
78+
.height(100.dp)
79+
.background(Color.Green)
80+
.overScrollVertical(true, springStiff = springStiff, springDamp = springDamp)
81+
) {
6282
items(25, { "${it}_3" }, { 1 }) {
6383
Content(it)
6484
}

overscroll_core/src/main/java/com/cormor/overscroll/core/OverScroll.kt

Lines changed: 89 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import androidx.compose.animation.core.Spring
66
import androidx.compose.animation.core.spring
77
import androidx.compose.foundation.gestures.scrollable
88
import androidx.compose.foundation.layout.offset
9+
import androidx.compose.runtime.Stable
10+
import androidx.compose.runtime.mutableStateOf
911
import androidx.compose.runtime.remember
1012
import androidx.compose.ui.Modifier
1113
import androidx.compose.ui.composed
@@ -18,111 +20,139 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
1820
import androidx.compose.ui.unit.IntOffset
1921
import androidx.compose.ui.unit.Velocity
2022
import kotlinx.coroutines.async
23+
import kotlinx.coroutines.coroutineScope
2124
import kotlinx.coroutines.launch
22-
import kotlinx.coroutines.withContext
2325
import kotlin.math.abs
2426
import kotlin.math.roundToInt
2527
import kotlin.math.sign
2628
import kotlin.math.sqrt
2729

30+
/**
31+
* A parabolic rolling easing curve.
32+
*
33+
* When rolling in the same direction, the farther away from 0, the greater the "resistance"; the closer to 0, the smaller the "resistance";
34+
*
35+
* No drag effect is applied when the scrolling direction is opposite to the currently existing overscroll offset
36+
*
37+
* Note: when [p]=50f, its performance should be consistent with iOS
38+
* @param currentOffset Offset value currently out of bounds
39+
* @param newOffset The offset of the new scroll
40+
* @param p Key parameters for parabolic curve calculation
41+
*/
42+
@Stable
43+
fun parabolaScrollEasing(currentOffset: Float, newOffset: Float, p: Float = 50f): Float {
44+
val ratio = (p / (sqrt(p * abs(currentOffset + newOffset / 2).coerceAtLeast(Float.MIN_VALUE)))).coerceIn(Float.MIN_VALUE, 1f)
45+
return if (sign(currentOffset) == sign(newOffset)) {
46+
currentOffset + newOffset * ratio
47+
} else {
48+
currentOffset + newOffset
49+
}
50+
}
51+
2852
/**
2953
* OverScroll effect for scrollable Composable .
3054
*
3155
* You should call it before Modifiers with similar semantics such as [Modifier.scrollable], so that nested scrolling can work normally
3256
* @Author: cormor
3357
* @Email: cangtiansuo@gmail.com
34-
* @param nestedScrollToParent 是否将嵌套滚动事件分发给parent
35-
* @param scrollEasing 传入值分别是当前已有的overscrollOffset和新的来自手势的offset,修改它配合[springStiff]以定制滑动阻尼效果。当前默认easing来自iOS,可以不修改!
36-
* @param springStiff overscroll的springStiff,为了更好的用户体验,新值不建议高于[Spring.StiffnessMediumLow]
37-
* @param springDamp overscroll的springDamp,一般不需要设置
58+
* @param nestedScrollToParent Whether to dispatch nested scroll events to parent
59+
* @param scrollEasing The incoming values are the currently existing overscroll Offset
60+
* and the new offset from the gesture.
61+
* modify it to cooperate with [springStiff] to customize the sliding damping effect.
62+
* The current default easing comes from iOS, you don't need to modify it!
63+
* @param springStiff springStiff for overscroll effect,For better user experience, the new value is not recommended to be higher than[Spring.StiffnessMediumLow]
64+
* @param springDamp springDamp for overscroll effect,generally do not need to set
3865
*/
3966
fun Modifier.overScrollVertical(
4067
nestedScrollToParent: Boolean = true,
41-
scrollEasing: (currentOverscrollOffset: Float, newScrollOffset: Float) -> Float =
42-
{ currentOffset, newOffset ->
43-
val p = 50f
44-
val ratio = (p / (sqrt(p * abs(currentOffset + newOffset / 2).coerceAtLeast(Float.MIN_VALUE)))).coerceIn(Float.MIN_VALUE, 1f)
45-
if (sign(currentOffset) == sign(newOffset)) {
46-
currentOffset + newOffset * ratio
47-
} else {
48-
currentOffset + newOffset
49-
}
50-
},
68+
scrollEasing: (currentOffset: Float, newOffset: Float) -> Float = @Stable { currentOffset, newOffset -> parabolaScrollEasing(currentOffset, newOffset) },
5169
springStiff: Float = 300f,
5270
springDamp: Float = Spring.DampingRatioNoBouncy,
5371
): Modifier = composed {
5472
val dispatcher = remember { NestedScrollDispatcher() }
55-
val overscrollOffset = remember { Animatable(0f) }
73+
val overscrollY = remember { mutableStateOf(0f) }
5674

5775
val nestedConnection = remember(nestedScrollToParent, springStiff, springDamp) {
5876
object : NestedScrollConnection {
77+
/**
78+
* If the offset is less than this value, we consider the animation to end.
79+
*/
5980
val visibilityThreshold = 0.5f
6081
lateinit var lastFlingAnimator: Animatable<Float, AnimationVector1D>
6182

83+
var offsetY
84+
get() = overscrollY.value
85+
set(value) {
86+
overscrollY.value = value
87+
}
88+
6289
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
63-
if (this::lastFlingAnimator.isInitialized) {
90+
if (::lastFlingAnimator.isInitialized && lastFlingAnimator.isRunning) {
6491
dispatcher.coroutineScope.launch {
6592
lastFlingAnimator.stop()
6693
}
6794
}
68-
val parentConsume = when {
69-
nestedScrollToParent -> dispatcher.dispatchPreScroll(available, source)
70-
else -> Offset.Zero
95+
val realAvailable = when {
96+
nestedScrollToParent -> available - dispatcher.dispatchPreScroll(available, source)
97+
else -> available
7198
}
72-
val realAvailable = available - parentConsume
7399

74-
val isSameDirection = sign(realAvailable.y) == sign(overscrollOffset.value)
75-
if (abs(overscrollOffset.value) <= visibilityThreshold || isSameDirection) {
76-
return parentConsume
100+
val isSameDirection = sign(realAvailable.y) == sign(offsetY)
101+
if (abs(offsetY) <= visibilityThreshold || isSameDirection) {
102+
return available - realAvailable
77103
}
104+
val offsetAtLast = offsetY + realAvailable.y
78105
// sign changed, coerce to start scrolling and exit
79-
if (sign(overscrollOffset.value) != sign(overscrollOffset.value + realAvailable.y)) dispatcher.coroutineScope.launch {
80-
overscrollOffset.snapTo(0f)
81-
} else dispatcher.coroutineScope.launch {
82-
overscrollOffset.snapTo(scrollEasing(overscrollOffset.value, realAvailable.y))
106+
return if (sign(offsetY) != sign(offsetAtLast)) {
107+
offsetY = 0f
108+
Offset(x = 0f, y = offsetAtLast)
109+
} else {
110+
offsetY = scrollEasing(offsetY, realAvailable.y)
111+
Offset(x = 0f, y = available.y)
83112
}
84-
return Offset(x = 0f, y = available.y)
85113
}
86114

87115
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
88116
val realAvailable = when {
89117
nestedScrollToParent -> available - dispatcher.dispatchPostScroll(consumed, available, source)
90118
else -> available
91119
}
92-
dispatcher.coroutineScope.launch {
93-
when (source) {
94-
NestedScrollSource.Fling -> overscrollOffset.snapTo(overscrollOffset.value + realAvailable.y)
95-
else -> overscrollOffset.snapTo(scrollEasing(overscrollOffset.value, realAvailable.y))
96-
}
120+
when (source) {
121+
NestedScrollSource.Fling -> offsetY += realAvailable.y
122+
else -> offsetY = scrollEasing(offsetY, realAvailable.y)
97123
}
98124
return Offset(x = 0f, y = available.y)
99125
}
100126

101127
override suspend fun onPreFling(available: Velocity): Velocity {
102-
if (this::lastFlingAnimator.isInitialized) {
103-
lastFlingAnimator.snapTo(lastFlingAnimator.value)
128+
if (::lastFlingAnimator.isInitialized && lastFlingAnimator.isRunning) {
129+
lastFlingAnimator.stop()
104130
}
105131
val parentConsumed = when {
106132
nestedScrollToParent -> dispatcher.dispatchPreFling(available)
107133
else -> Velocity.Zero
108134
}
109135
val realAvailable = available - parentConsumed
110136
var leftVelocity = realAvailable.y
111-
if (abs(overscrollOffset.value) >= visibilityThreshold && sign(realAvailable.y) != sign(overscrollOffset.value)) {
137+
if (abs(offsetY) >= visibilityThreshold && sign(realAvailable.y) != sign(offsetY)) {
112138
var lastValue = 0f
113-
lastFlingAnimator = Animatable(overscrollOffset.value)
114-
dispatcher.coroutineScope.async {
115-
lastFlingAnimator.animateTo(0f, spring(springDamp, springStiff, visibilityThreshold), realAvailable.y) {
116-
if (abs(value) < visibilityThreshold || sign(value) != sign(lastValue) && lastValue != 0f) dispatcher.coroutineScope.launch {
117-
this@animateTo.stop()
118-
overscrollOffset.snapTo(0f)
119-
} else dispatcher.coroutineScope.launch {
120-
overscrollOffset.snapTo(scrollEasing(overscrollOffset.value, value - overscrollOffset.value))
139+
lastFlingAnimator = Animatable(offsetY)
140+
coroutineScope {
141+
async {
142+
lastFlingAnimator.animateTo(0f, spring(springDamp, springStiff, visibilityThreshold), realAvailable.y) {
143+
if (abs(value) < visibilityThreshold || sign(value) != sign(lastValue) && lastValue != 0f) {
144+
offsetY = 0f
145+
dispatcher.coroutineScope.launch {
146+
this@animateTo.snapTo(0f)
147+
}
148+
} else {
149+
offsetY = scrollEasing(offsetY, value - offsetY)
150+
}
151+
lastValue = value
152+
leftVelocity = velocity
121153
}
122-
lastValue = value
123-
leftVelocity = velocity
124-
}
125-
}.join()
154+
}.join()
155+
}
126156
}
127157
return Velocity(parentConsumed.x, y = available.y - leftVelocity)
128158
}
@@ -132,20 +162,21 @@ fun Modifier.overScrollVertical(
132162
nestedScrollToParent -> available - dispatcher.dispatchPostFling(consumed, available)
133163
else -> available
134164
}
135-
lastFlingAnimator = Animatable(overscrollOffset.value)
136-
val leftVelocity = withContext(dispatcher.coroutineScope.coroutineContext) {
137-
lastFlingAnimator.animateTo(0f, spring(springDamp, springStiff, visibilityThreshold), realAvailable.y) {
138-
dispatcher.coroutineScope.launch {
139-
overscrollOffset.snapTo(scrollEasing(overscrollOffset.value, value - overscrollOffset.value))
140-
}
141-
}.endState.velocity
165+
var leftVelocity = 0f
166+
coroutineScope {
167+
async {
168+
lastFlingAnimator = Animatable(offsetY)
169+
leftVelocity = lastFlingAnimator.animateTo(0f, spring(springDamp, springStiff, visibilityThreshold), realAvailable.y) {
170+
offsetY = scrollEasing(offsetY, value - offsetY)
171+
}.endState.velocity
172+
}.join()
142173
}
143-
return available.copy(y = available.y - leftVelocity)
174+
return Velocity(x = 0f, y = available.y - leftVelocity)
144175
}
145176
}
146177
}
147178
this
148179
.clipToBounds()
149-
.offset { IntOffset(0, overscrollOffset.value.roundToInt()) }
180+
.offset { IntOffset(0, overscrollY.value.roundToInt()) }
150181
.nestedScroll(nestedConnection, dispatcher)
151182
}

0 commit comments

Comments
 (0)