• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.compose.gesture.effect
18 
19 import androidx.compose.animation.core.AnimationSpec
20 import androidx.compose.foundation.OverscrollEffect
21 import androidx.compose.foundation.OverscrollFactory
22 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
23 import androidx.compose.material3.MaterialTheme
24 import androidx.compose.runtime.Composable
25 import androidx.compose.runtime.remember
26 import androidx.compose.runtime.rememberCoroutineScope
27 import androidx.compose.ui.Modifier
28 import androidx.compose.ui.layout.Measurable
29 import androidx.compose.ui.layout.MeasureResult
30 import androidx.compose.ui.layout.MeasureScope
31 import androidx.compose.ui.node.DelegatableNode
32 import androidx.compose.ui.node.LayoutModifierNode
33 import androidx.compose.ui.unit.Constraints
34 import androidx.compose.ui.unit.Density
35 import androidx.compose.ui.unit.dp
36 import kotlin.math.roundToInt
37 import kotlinx.coroutines.CoroutineScope
38 
39 /** Returns a [remember]ed [OffsetOverscrollEffect]. */
40 @Composable
41 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
rememberOffsetOverscrollEffectnull42 fun rememberOffsetOverscrollEffect(
43     animationSpec: AnimationSpec<Float> = MaterialTheme.motionScheme.slowSpatialSpec()
44 ): OffsetOverscrollEffect {
45     val animationScope = rememberCoroutineScope()
46     return remember(animationScope, animationSpec) {
47         OffsetOverscrollEffect(animationScope, animationSpec)
48     }
49 }
50 
51 @Composable
52 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
rememberOffsetOverscrollEffectFactorynull53 fun rememberOffsetOverscrollEffectFactory(
54     animationSpec: AnimationSpec<Float> = MaterialTheme.motionScheme.slowSpatialSpec()
55 ): OverscrollFactory {
56     val animationScope = rememberCoroutineScope()
57     return remember(animationScope, animationSpec) {
58         OffsetOverscrollEffectFactory(animationScope, animationSpec)
59     }
60 }
61 
62 data class OffsetOverscrollEffectFactory(
63     private val animationScope: CoroutineScope,
64     private val animationSpec: AnimationSpec<Float>,
65 ) : OverscrollFactory {
createOverscrollEffectnull66     override fun createOverscrollEffect(): OverscrollEffect {
67         return OffsetOverscrollEffect(
68             animationScope = animationScope,
69             animationSpec = animationSpec,
70         )
71     }
72 }
73 
74 /** An [OverscrollEffect] that offsets the content by the overscroll value. */
75 class OffsetOverscrollEffect(animationScope: CoroutineScope, animationSpec: AnimationSpec<Float>) :
76     BaseContentOverscrollEffect(animationScope, animationSpec) {
77     override val node: DelegatableNode =
78         object : Modifier.Node(), LayoutModifierNode {
measurenull79             override fun MeasureScope.measure(
80                 measurable: Measurable,
81                 constraints: Constraints,
82             ): MeasureResult {
83                 val placeable = measurable.measure(constraints)
84                 return layout(placeable.width, placeable.height) {
85                     val offsetPx = computeOffset(density = this@measure, overscrollDistance)
86                     if (offsetPx != 0) {
87                         placeable.placeWithLayer(
88                             with(requireConverter()) { offsetPx.toIntOffset() }
89                         )
90                     } else {
91                         placeable.place(0, 0)
92                     }
93                 }
94             }
95         }
96 
97     companion object {
98         private val MaxDistance = 400.dp
99 
computeOffsetnull100         fun computeOffset(density: Density, overscrollDistance: Float): Int {
101             val maxDistancePx = with(density) { MaxDistance.toPx() }
102             val progress = ProgressConverter.Default.convert(overscrollDistance / maxDistancePx)
103             return (progress * maxDistancePx).roundToInt()
104         }
105     }
106 }
107 
108 /** This converter lets you change a linear progress into a function of your choice. */
interfacenull109 fun interface ProgressConverter {
110     fun convert(progress: Float): Float
111 
112     companion object {
113         /** Starts linearly with some resistance and slowly approaches to 0.2f */
114         val Default = tanh(maxProgress = 0.2f, tilt = 3f)
115 
116         /**
117          * The scroll stays linear, with [factor] you can control how much resistance there is.
118          *
119          * @param factor If you choose a value between 0f and 1f, the progress will grow more
120          *   slowly, like there's resistance. A value of 1f means there's no resistance.
121          */
122         fun linear(factor: Float = 1f) = ProgressConverter { it * factor }
123 
124         /**
125          * This function starts linear and slowly approaches [maxProgress].
126          *
127          * See a [visual representation](https://www.desmos.com/calculator/usgvvf0z1u) of this
128          * function.
129          *
130          * @param maxProgress is the maximum progress value.
131          * @param tilt behaves similarly to the factor in the [linear] function, and allows you to
132          *   control how quickly you get to the [maxProgress].
133          */
134         fun tanh(maxProgress: Float, tilt: Float = 1f) = ProgressConverter {
135             maxProgress * kotlin.math.tanh(x = it / (maxProgress * tilt))
136         }
137     }
138 }
139