From 33572543b9ace2c6c6b0133d2db31c92f351bfa2 Mon Sep 17 00:00:00 2001 From: Suphon Thanakornpakapong Date: Tue, 18 May 2021 13:35:25 +0700 Subject: [PATCH] Add spring effect to preference ui --- .../components/NestedScrollSpring.kt | 121 ++++++++++++++++++ .../components/PreferenceLayout.kt | 34 ++--- 2 files changed, 140 insertions(+), 15 deletions(-) create mode 100644 lawnchair/src/app/lawnchair/ui/preferences/components/NestedScrollSpring.kt diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/NestedScrollSpring.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/NestedScrollSpring.kt new file mode 100644 index 0000000000..575fbc261a --- /dev/null +++ b/lawnchair/src/app/lawnchair/ui/preferences/components/NestedScrollSpring.kt @@ -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 +) : 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("value") { + override fun getValue(obj: NestedScrollSpringConnection): Float { + return obj.dampedScrollShift + } + + override fun setValue(obj: NestedScrollSpringConnection, value: Float) { + obj.dampedScrollShift = value + } + } + } +} diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/PreferenceLayout.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/PreferenceLayout.kt index 7612a93390..e1349fb726 100644 --- a/lawnchair/src/app/lawnchair/ui/preferences/components/PreferenceLayout.kt +++ b/lawnchair/src/app/lawnchair/ui/preferences/components/PreferenceLayout.kt @@ -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() + } } }