mirror of
https://github.com/LawnchairLauncher/lawnchair.git
synced 2026-02-27 15:26:58 +00:00
Add spring effect to preference ui
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
package app.lawnchair.ui.preferences.components
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.dynamicanimation.animation.FloatPropertyCompat
|
||||
import androidx.dynamicanimation.animation.SpringAnimation
|
||||
import androidx.dynamicanimation.animation.SpringForce
|
||||
import kotlin.math.abs
|
||||
|
||||
@Composable
|
||||
fun NestedScrollSpring(content: @Composable () -> Unit) {
|
||||
val dampedScrollShift = remember { mutableStateOf(0f) }
|
||||
val nestedScrollConnection = remember { NestedScrollSpringConnection(dampedScrollShift) }
|
||||
Layout(
|
||||
content,
|
||||
modifier = Modifier
|
||||
.nestedScroll(nestedScrollConnection)
|
||||
) { measurables, constraints ->
|
||||
layout(constraints.maxWidth, constraints.maxHeight) {
|
||||
require(measurables.size == 1)
|
||||
measurables.first().measure(constraints).place(0, dampedScrollShift.value.toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val STIFFNESS = (SpringForce.STIFFNESS_MEDIUM + SpringForce.STIFFNESS_LOW) / 2
|
||||
private const val DAMPING_RATIO = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY
|
||||
private const val VELOCITY_MULTIPLIER = 0.3f
|
||||
|
||||
class NestedScrollSpringConnection(
|
||||
dampedScrollShiftState: MutableState<Float>
|
||||
) : NestedScrollConnection {
|
||||
|
||||
private val springAnim = SpringAnimation(this, DAMPED_SCROLL, 0f).apply {
|
||||
spring = SpringForce(0f).apply {
|
||||
stiffness = STIFFNESS
|
||||
dampingRatio = DAMPING_RATIO
|
||||
}
|
||||
}
|
||||
private var dampedScrollShift by dampedScrollShiftState
|
||||
private var isFlinging = false
|
||||
|
||||
private fun finishScrollWithVelocity(velocity: Float) {
|
||||
springAnim.setStartVelocity(velocity)
|
||||
springAnim.setStartValue(dampedScrollShift)
|
||||
springAnim.start()
|
||||
}
|
||||
|
||||
private fun onAbsorb(velocity: Float) {
|
||||
finishScrollWithVelocity(velocity * VELOCITY_MULTIPLIER)
|
||||
}
|
||||
|
||||
private fun onPull(deltaDistance: Float) {
|
||||
dampedScrollShift += deltaDistance
|
||||
springAnim.cancel()
|
||||
}
|
||||
|
||||
private fun onRelease() {
|
||||
finishScrollWithVelocity(0f)
|
||||
}
|
||||
|
||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||
val scrollOffset = available.y
|
||||
if (isFlinging || dampedScrollShift == 0f || dampedScrollShift > 0f == scrollOffset > 0f) {
|
||||
return Offset.Zero
|
||||
}
|
||||
val shiftAmount = abs(dampedScrollShift)
|
||||
val scrollAmount = abs(scrollOffset)
|
||||
return when {
|
||||
shiftAmount > scrollAmount -> {
|
||||
onPull(scrollOffset)
|
||||
Offset(0f, scrollOffset)
|
||||
}
|
||||
shiftAmount < scrollAmount -> {
|
||||
onPull(-dampedScrollShift)
|
||||
Offset(0f, dampedScrollShift)
|
||||
}
|
||||
else -> Offset.Zero
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPostScroll(
|
||||
consumed: Offset,
|
||||
available: Offset,
|
||||
source: NestedScrollSource
|
||||
): Offset {
|
||||
if (isFlinging) return Offset.Zero
|
||||
onPull(available.y * (VELOCITY_MULTIPLIER / 3f))
|
||||
return available
|
||||
}
|
||||
|
||||
override suspend fun onPreFling(available: Velocity): Velocity {
|
||||
onRelease()
|
||||
isFlinging = true
|
||||
return Velocity.Zero
|
||||
}
|
||||
|
||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||
isFlinging = false
|
||||
onAbsorb(available.y)
|
||||
return Velocity(0f, available.y)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DAMPED_SCROLL = object : FloatPropertyCompat<NestedScrollSpringConnection>("value") {
|
||||
override fun getValue(obj: NestedScrollSpringConnection): Float {
|
||||
return obj.dampedScrollShift
|
||||
}
|
||||
|
||||
override fun setValue(obj: NestedScrollSpringConnection, value: Float) {
|
||||
obj.dampedScrollShift = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,15 +27,17 @@ fun PreferenceLayout(
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
ProvideTopBarFloatingState(scrollState.value > 0)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.verticalScroll(scrollState)
|
||||
.padding(preferenceLayoutPadding()),
|
||||
verticalArrangement = verticalArrangement,
|
||||
horizontalAlignment = horizontalAlignment
|
||||
) {
|
||||
content()
|
||||
NestedScrollSpring {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.verticalScroll(scrollState)
|
||||
.padding(preferenceLayoutPadding()),
|
||||
verticalArrangement = verticalArrangement,
|
||||
horizontalAlignment = horizontalAlignment
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,12 +45,14 @@ fun PreferenceLayout(
|
||||
fun PreferenceLayoutLazyColumn(modifier: Modifier = Modifier, content: LazyListScope.() -> Unit) {
|
||||
val scrollState = rememberLazyListState()
|
||||
ProvideTopBarFloatingState(scrollState.firstVisibleItemIndex > 0 || scrollState.firstVisibleItemScrollOffset > 0)
|
||||
LazyColumn(
|
||||
modifier = modifier.fillMaxHeight(),
|
||||
contentPadding = preferenceLayoutPadding(),
|
||||
state = scrollState
|
||||
) {
|
||||
content()
|
||||
NestedScrollSpring {
|
||||
LazyColumn(
|
||||
modifier = modifier.fillMaxHeight(),
|
||||
contentPadding = preferenceLayoutPadding(),
|
||||
state = scrollState
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user