From 133602fb9dfc4f626a41f56bcc2d3e30677a839d Mon Sep 17 00:00:00 2001 From: Suphon Thanakornpakapong Date: Wed, 4 Aug 2021 16:48:51 +0700 Subject: [PATCH] Implement stretch overscroll effect --- .../src/app/lawnchair/ui/StretchEdgeEffect.kt | 86 ++++++++++++ .../components/NestedScrollSpring.kt | 122 ------------------ .../components/NestedScrollStretch.kt | 74 +++++++++++ .../components/PreferenceLayout.kt | 4 +- 4 files changed, 162 insertions(+), 124 deletions(-) create mode 100644 lawnchair/src/app/lawnchair/ui/StretchEdgeEffect.kt delete mode 100644 lawnchair/src/app/lawnchair/ui/preferences/components/NestedScrollSpring.kt create mode 100644 lawnchair/src/app/lawnchair/ui/preferences/components/NestedScrollStretch.kt diff --git a/lawnchair/src/app/lawnchair/ui/StretchEdgeEffect.kt b/lawnchair/src/app/lawnchair/ui/StretchEdgeEffect.kt new file mode 100644 index 0000000000..9abcd78544 --- /dev/null +++ b/lawnchair/src/app/lawnchair/ui/StretchEdgeEffect.kt @@ -0,0 +1,86 @@ +package app.lawnchair.ui + +import androidx.core.util.Consumer +import androidx.dynamicanimation.animation.DynamicAnimation +import androidx.dynamicanimation.animation.FloatPropertyCompat +import androidx.dynamicanimation.animation.SpringAnimation +import androidx.dynamicanimation.animation.SpringForce +import com.android.launcher3.anim.Interpolators +import kotlin.math.abs + +class StretchEdgeEffect( + private val setShift: Consumer +) { + private val springAnim = SpringAnimation(this, DAMPED_SCROLL, 0f).apply { + spring = SpringForce(0f).apply { + stiffness = STIFFNESS + dampingRatio = DAMPING_RATIO + } + } + private var dampedScrollShift = 0f + set(value) { + if (field != value) { + field = value + setShift.accept(value) + } + } + + private fun finishScrollWithVelocity(velocity: Float) { + springAnim.setStartVelocity(velocity) + springAnim.setStartValue(dampedScrollShift) + springAnim.start() + } + + fun onAbsorb(velocity: Float) { + finishScrollWithVelocity(velocity * VELOCITY_MULTIPLIER) + } + + fun onPull(deltaDistance: Float) { + dampedScrollShift += deltaDistance * (VELOCITY_MULTIPLIER / 6f) + springAnim.cancel() + } + + fun onRelease() { + if (dampedScrollShift != 0f && !springAnim.isRunning) { + springAnim.setStartValue(dampedScrollShift) + springAnim.start() + } + } + + fun addEndListener(listener: DynamicAnimation.OnAnimationEndListener) { + springAnim.addEndListener(listener) + } + + companion object { + private const val STIFFNESS = SpringForce.STIFFNESS_LOW * 2 + private const val DAMPING_RATIO = SpringForce.DAMPING_RATIO_NO_BOUNCY + private const val VELOCITY_MULTIPLIER = 0.1f + + private val SCALE_INTERPOLATOR = Interpolators.DEACCEL_2 + private const val MAX_DISTANCE = 0.05f + + private val DAMPED_SCROLL = object : FloatPropertyCompat("value") { + override fun getValue(obj: StretchEdgeEffect): Float { + return obj.dampedScrollShift + } + + override fun setValue(obj: StretchEdgeEffect, value: Float) { + obj.dampedScrollShift = value + } + } + + fun getScale(shift: Float, size: Float): Float { + if (shift == 0f) { + return 1f + } + val distance = abs(shift / size).coerceAtMost(MAX_DISTANCE) + val progress = distance / MAX_DISTANCE + val interpolatedProgress = SCALE_INTERPOLATOR.getInterpolation(progress) + return 1f + interpolatedProgress * MAX_DISTANCE + } + + fun getPivot(shift: Float, size: Float): Float { + return if (shift < 0f) size else 0f + } + } +} diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/NestedScrollSpring.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/NestedScrollSpring.kt deleted file mode 100644 index 27affac8cb..0000000000 --- a/lawnchair/src/app/lawnchair/ui/preferences/components/NestedScrollSpring.kt +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright 2021, Lawnchair - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package app.lawnchair.ui.preferences.components - -import androidx.compose.foundation.layout.Box -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.graphicsLayer -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.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) } - Box( - modifier = Modifier - .nestedScroll(nestedScrollConnection) - .graphicsLayer { - translationY = dampedScrollShift.value - }, - ) { - content() - } -} - -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 onPostScroll( - consumed: Offset, - available: Offset, - source: NestedScrollSource - ): Offset { - if (isFlinging) return Offset.Zero - if (available.y != 0f) { - onPull(available.y * (VELOCITY_MULTIPLIER / 3f)) - } else if (dampedScrollShift != 0f && !springAnim.isRunning) { - springAnim.setStartValue(dampedScrollShift) - springAnim.start() - } - 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/NestedScrollStretch.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/NestedScrollStretch.kt new file mode 100644 index 0000000000..6171308a5c --- /dev/null +++ b/lawnchair/src/app/lawnchair/ui/preferences/components/NestedScrollStretch.kt @@ -0,0 +1,74 @@ +package app.lawnchair.ui.preferences.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.drawscope.scale +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.unit.Velocity +import app.lawnchair.ui.StretchEdgeEffect + +@Composable +fun NestedScrollStretch(content: @Composable () -> Unit) { + val shift = remember { mutableStateOf(0f) } + val connection = remember { NestedScrollStretchConnection(shift) } + Box( + modifier = Modifier + .nestedScroll(connection) + .drawWithContent { + val value = shift.value + val height = size.height + val scaleY = StretchEdgeEffect.getScale(value, height) + if (scaleY != 1f) { + val pivotY = StretchEdgeEffect.getPivot(value, height) + scale(1f, scaleY, pivot = Offset(0f, pivotY)) { + this@drawWithContent.drawContent() + } + } else { + drawContent() + } + } + ) { + content() + } +} + +private class NestedScrollStretchConnection( + shiftState: MutableState +) : NestedScrollConnection { + private val effect = StretchEdgeEffect { shift -> shiftState.value = shift } + private var isFlinging = false + + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource + ): Offset { + if (isFlinging) return Offset.Zero + if (available.y != 0f) { + effect.onPull(available.y) + } else { + effect.onRelease() + } + return available + } + + override suspend fun onPreFling(available: Velocity): Velocity { + effect.onRelease() + isFlinging = true + return Velocity.Zero + } + + override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { + isFlinging = false + effect.onAbsorb(available.y) + return Velocity(0f, available.y) + } +} diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/PreferenceLayout.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/PreferenceLayout.kt index 27c0236ad9..5378060b6c 100644 --- a/lawnchair/src/app/lawnchair/ui/preferences/components/PreferenceLayout.kt +++ b/lawnchair/src/app/lawnchair/ui/preferences/components/PreferenceLayout.kt @@ -44,7 +44,7 @@ fun PreferenceLayout( val scrollState = rememberScrollState() ProvideTopBarFloatingState(scrolled = scrollState.value > 0) - NestedScrollSpring { + NestedScrollStretch { Column( verticalArrangement = verticalArrangement, horizontalAlignment = horizontalAlignment, @@ -63,7 +63,7 @@ fun PreferenceLayoutLazyColumn(modifier: Modifier = Modifier, content: LazyListS val scrollState = rememberLazyListState() ProvideTopBarFloatingState(scrolled = scrollState.firstVisibleItemIndex > 0 || scrollState.firstVisibleItemScrollOffset > 0) - NestedScrollSpring { + NestedScrollStretch { LazyColumn( modifier = modifier.fillMaxHeight(), contentPadding = preferenceLayoutPadding(),