1 /*
2  * Copyright 2021 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 androidx.compose.foundation
18 
19 import android.content.Context
20 import android.os.Build
21 import android.util.AttributeSet
22 import android.view.ViewConfiguration
23 import android.widget.EdgeEffect
24 import androidx.annotation.RequiresApi
25 import androidx.compose.ui.unit.Density
26 import androidx.compose.ui.unit.dp
27 import kotlin.math.abs
28 import kotlin.math.exp
29 import kotlin.math.ln
30 import kotlin.math.roundToInt
31 
32 internal object EdgeEffectCompat {
33 
createnull34     fun create(context: Context): EdgeEffect {
35         return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
36             Api31Impl.create(context, null)
37         } else {
38             GlowEdgeEffectCompat(context)
39         }
40     }
41 
EdgeEffectnull42     fun EdgeEffect.onPullDistanceCompat(deltaDistance: Float, displacement: Float): Float {
43         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
44             return Api31Impl.onPullDistance(this, deltaDistance, displacement)
45         }
46         this.onPull(deltaDistance, displacement)
47         return deltaDistance
48     }
49 
onAbsorbCompatnull50     fun EdgeEffect.onAbsorbCompat(velocity: Int) {
51         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
52             return this.onAbsorb(velocity)
53         } else if (this.isFinished) {
54             // Only absorb the glow effect if it is not active (finished) - dragging to start a glow
55             // and then releasing shouldn't add to the existing glow, it should just decay.
56             this.onAbsorb(velocity)
57         }
58     }
59 
60     /**
61      * When relaxing a stretch with velocity (fling), if the velocity would fully relax the stretch
62      * with some remaining velocity, instead of absorbing we want to propagate the velocity and
63      * relax the overscroll as part of scroll, called within the fling - so for this case we do
64      * nothing and consume no velocity.
65      *
66      * If the velocity is not enough to fully relax the stretch, then we absorb it and consume all
67      * the velocity.
68      *
69      * @param velocity to absorb if needed
70      * @param edgeEffectLength the main axis bounds for this edge effect, needed to transform the
71      *   relative distance from [distanceCompat] into the absolute distance
72      * @return how much velocity was consumed
73      */
EdgeEffectnull74     fun EdgeEffect.absorbToRelaxIfNeeded(
75         velocity: Float,
76         edgeEffectLength: Float,
77         density: Density
78     ): Float {
79         val flingDistance = flingDistance(density, velocity)
80         val actualDistance = distanceCompat * edgeEffectLength
81         return if (flingDistance <= actualDistance) {
82             onAbsorbCompat(velocity.roundToInt())
83             // Consume all velocity when absorbing
84             velocity
85         } else {
86             // Consume nothing, we will relax the stretch in applyToScroll
87             0f
88         }
89     }
90 
91     /**
92      * Used for calls to [EdgeEffect.onRelease] that happen because of scroll delta in the opposite
93      * direction to the overscroll. See [GlowEdgeEffectCompat].
94      */
onReleaseWithOppositeDeltanull95     fun EdgeEffect.onReleaseWithOppositeDelta(delta: Float) {
96         if (this is GlowEdgeEffectCompat) {
97             releaseWithOppositeDelta(delta)
98         } else {
99             onRelease()
100         }
101     }
102 
103     val EdgeEffect.distanceCompat: Float
104         get() {
105             return if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)) {
106                 Api31Impl.getDistance(this)
107             } else 0f
108         }
109 }
110 
111 /**
112  * Compat class to work around a framework issue (b/242864658) - small negative deltas that release
113  * an overscroll followed by positive deltas cause the glow overscroll effect to instantly
114  * disappear. This can happen when you pull the overscroll, and keep it there - small fluctuations
115  * in the pointer position can cause these small negative deltas, even though on average it is not
116  * really moving. To workaround this we only release the overscroll if the cumulative negative
117  * deltas are larger than a minimum value - this should catch the majority of cases.
118  */
119 private class GlowEdgeEffectCompat(context: Context) : EdgeEffect(context) {
120     // Minimum distance in the opposite scroll direction to trigger a release
<lambda>null121     private val oppositeReleaseDeltaThreshold = with(Density(context)) { 1.dp.toPx() }
122     private var oppositeReleaseDelta = 0f
123 
onPullnull124     override fun onPull(deltaDistance: Float, displacement: Float) {
125         oppositeReleaseDelta = 0f
126         super.onPull(deltaDistance, displacement)
127     }
128 
onPullnull129     override fun onPull(deltaDistance: Float) {
130         oppositeReleaseDelta = 0f
131         super.onPull(deltaDistance)
132     }
133 
onReleasenull134     override fun onRelease() {
135         oppositeReleaseDelta = 0f
136         super.onRelease()
137     }
138 
onAbsorbnull139     override fun onAbsorb(velocity: Int) {
140         oppositeReleaseDelta = 0f
141         super.onAbsorb(velocity)
142     }
143 
144     /**
145      * Increments the current cumulative delta, and calls [onRelease] if it is greater than
146      * [oppositeReleaseDeltaThreshold].
147      */
releaseWithOppositeDeltanull148     fun releaseWithOppositeDelta(delta: Float) {
149         oppositeReleaseDelta += delta
150         if (abs(oppositeReleaseDelta) > oppositeReleaseDeltaThreshold) {
151             onRelease()
152         }
153     }
154 }
155 
156 @RequiresApi(Build.VERSION_CODES.S)
157 private object Api31Impl {
createnull158     fun create(context: Context, attrs: AttributeSet?): EdgeEffect {
159         return try {
160             EdgeEffect(context, attrs)
161         } catch (t: Throwable) {
162             EdgeEffect(context) // Old preview release
163         }
164     }
165 
onPullDistancenull166     fun onPullDistance(edgeEffect: EdgeEffect, deltaDistance: Float, displacement: Float): Float {
167         return try {
168             edgeEffect.onPullDistance(deltaDistance, displacement)
169         } catch (t: Throwable) {
170             edgeEffect.onPull(deltaDistance, displacement) // Old preview release
171             0f
172         }
173     }
174 
getDistancenull175     fun getDistance(edgeEffect: EdgeEffect): Float {
176         return try {
177             edgeEffect.getDistance()
178         } catch (t: Throwable) {
179             0f // Old preview release
180         }
181     }
182 }
183 
184 // These constants are copied from the Android spline decay rate
185 private const val Inflection = 0.35f // Tension lines cross at (Inflection, 1)
186 private val PlatformFlingScrollFriction = ViewConfiguration.getScrollFriction()
187 private const val GravityEarth = 9.80665f
188 private const val InchesPerMeter = 39.37f
189 private val DecelerationRate = ln(0.78) / ln(0.9)
190 private val DecelMinusOne = DecelerationRate - 1.0
191 
192 /**
193  * Copied from OverScroller, this returns the distance that a fling with the given velocity will go.
194  *
195  * @return The absolute distance that will be traveled by a fling of the given velocity
196  */
flingDistancenull197 private fun flingDistance(density: Density, velocity: Float): Float {
198     val magicPhysicalCoefficient =
199         (GravityEarth * InchesPerMeter * density.density * 160f * 0.84f).toDouble()
200     val l =
201         ln(Inflection * abs(velocity) / (PlatformFlingScrollFriction * magicPhysicalCoefficient))
202     val distance =
203         PlatformFlingScrollFriction *
204             magicPhysicalCoefficient *
205             exp(DecelerationRate / DecelMinusOne * l)
206     return distance.toFloat()
207 }
208