AndroidFlingPhysics.cpp (7754B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "AndroidFlingPhysics.h" 8 9 #include <cmath> 10 11 #include "mozilla/ClearOnShutdown.h" 12 #include "mozilla/StaticPrefs_apz.h" 13 #include "mozilla/StaticPtr.h" 14 15 namespace mozilla { 16 namespace layers { 17 18 // The fling physics calculations implemented here are adapted from 19 // Chrome's implementation of fling physics on Android: 20 // https://cs.chromium.org/chromium/src/ui/events/android/scroller.cc?rcl=3ae3aaff927038a5c644926842cb0c31dea60c79 21 22 static double ComputeDeceleration(float aDPI) { 23 const float kFriction = 0.84f; 24 const float kGravityEarth = 9.80665f; 25 return kGravityEarth // g (m/s^2) 26 * 39.37f // inch/meter 27 * aDPI // pixels/inch 28 * kFriction; 29 } 30 31 // == std::log(0.78f) / std::log(0.9f) 32 const float kDecelerationRate = 2.3582018f; 33 34 // Default friction constant in android.view.ViewConfiguration. 35 static float GetFlingFriction() { 36 return StaticPrefs::apz_android_chrome_fling_physics_friction(); 37 } 38 39 // Tension lines cross at (GetInflexion(), 1). 40 static float GetInflexion() { 41 // Clamp the inflexion to the range [0,1]. Values outside of this range 42 // do not make sense in the physics model, and for negative values the 43 // approximation used to compute the spline curve does not converge. 44 const float inflexion = 45 StaticPrefs::apz_android_chrome_fling_physics_inflexion(); 46 return std::clamp(inflexion, 0.f, 1.f); 47 } 48 49 // Fling scroll is stopped when the scroll position is |kThresholdForFlingEnd| 50 // pixels or closer from the end. 51 static float GetThresholdForFlingEnd() { 52 return StaticPrefs::apz_android_chrome_fling_physics_stop_threshold(); 53 } 54 55 static double ComputeSplineDeceleration(ParentLayerCoord aVelocity, 56 double aTuningCoeff) { 57 float velocityPerSec = aVelocity * 1000.0f; 58 return std::log(GetInflexion() * velocityPerSec / 59 (GetFlingFriction() * aTuningCoeff)); 60 } 61 62 static TimeDuration ComputeFlingDuration(ParentLayerCoord aVelocity, 63 double aTuningCoeff) { 64 const double splineDecel = ComputeSplineDeceleration(aVelocity, aTuningCoeff); 65 const double timeSeconds = std::exp(splineDecel / (kDecelerationRate - 1.0)); 66 return TimeDuration::FromSeconds(timeSeconds); 67 } 68 69 static ParentLayerCoord ComputeFlingDistance(ParentLayerCoord aVelocity, 70 double aTuningCoeff) { 71 const double splineDecel = ComputeSplineDeceleration(aVelocity, aTuningCoeff); 72 return GetFlingFriction() * aTuningCoeff * 73 std::exp(kDecelerationRate / (kDecelerationRate - 1.0) * splineDecel); 74 } 75 76 struct SplineConstants { 77 public: 78 SplineConstants() { 79 const float kStartTension = 0.5f; 80 const float kEndTension = 1.0f; 81 const float kP1 = kStartTension * GetInflexion(); 82 const float kP2 = 1.0f - kEndTension * (1.0f - GetInflexion()); 83 84 float xMin = 0.0f; 85 for (int i = 0; i < kNumSamples; i++) { 86 const float alpha = static_cast<float>(i) / kNumSamples; 87 88 float xMax = 1.0f; 89 float x, tx, coef; 90 // While the inflexion can be overridden by the user, it's clamped to 91 // [0,1]. For values in this range, the approximation algorithm below 92 // should converge in < 20 iterations. For good measure, we impose an 93 // iteration limit as well. 94 static const int sIterationLimit = 100; 95 int iterations = 0; 96 while (iterations++ < sIterationLimit) { 97 x = xMin + (xMax - xMin) / 2.0f; 98 coef = 3.0f * x * (1.0f - x); 99 tx = coef * ((1.0f - x) * kP1 + x * kP2) + x * x * x; 100 if (FuzzyEqualsAdditive(tx, alpha)) { 101 break; 102 } 103 if (tx > alpha) { 104 xMax = x; 105 } else { 106 xMin = x; 107 } 108 } 109 mSplinePositions[i] = coef * ((1.0f - x) * kStartTension + x) + x * x * x; 110 } 111 mSplinePositions[kNumSamples] = 1.0f; 112 } 113 114 void CalculateCoefficients(float aTime, float* aOutDistanceCoef, 115 float* aOutVelocityCoef) { 116 *aOutDistanceCoef = 1.0f; 117 *aOutVelocityCoef = 0.0f; 118 const int index = static_cast<int>(kNumSamples * aTime); 119 if (index < kNumSamples) { 120 const float tInf = static_cast<float>(index) / kNumSamples; 121 const float dInf = mSplinePositions[index]; 122 const float tSup = static_cast<float>(index + 1) / kNumSamples; 123 const float dSup = mSplinePositions[index + 1]; 124 *aOutVelocityCoef = (dSup - dInf) / (tSup - tInf); 125 *aOutDistanceCoef = dInf + (aTime - tInf) * *aOutVelocityCoef; 126 } 127 } 128 129 private: 130 static const int kNumSamples = 100; 131 float mSplinePositions[kNumSamples + 1]; 132 }; 133 134 StaticAutoPtr<SplineConstants> gSplineConstants; 135 136 /* static */ 137 void AndroidFlingPhysics::InitializeGlobalState() { 138 gSplineConstants = new SplineConstants(); 139 ClearOnShutdown(&gSplineConstants); 140 } 141 142 void AndroidFlingPhysics::Init(const ParentLayerPoint& aStartingVelocity, 143 float aPLPPI) { 144 mVelocity = aStartingVelocity.Length(); 145 // We should not have created a fling animation if there is no velocity. 146 MOZ_ASSERT(mVelocity != 0.0f); 147 const double tuningCoeff = ComputeDeceleration(aPLPPI); 148 mTargetDuration = ComputeFlingDuration(mVelocity, tuningCoeff); 149 MOZ_ASSERT(!mTargetDuration.IsZero()); 150 mDurationSoFar = TimeDuration(); 151 mLastPos = ParentLayerPoint(); 152 mCurrentPos = ParentLayerPoint(); 153 float coeffX = 154 mVelocity == 0 ? 1.0f : aStartingVelocity.x.value / mVelocity.value; 155 float coeffY = 156 mVelocity == 0 ? 1.0f : aStartingVelocity.y.value / mVelocity.value; 157 mTargetDistance = ComputeFlingDistance(mVelocity, tuningCoeff); 158 mTargetPos = 159 ParentLayerPoint(mTargetDistance * coeffX, mTargetDistance * coeffY); 160 const float hyp = mTargetPos.Length(); 161 if (FuzzyEqualsAdditive(hyp, 0.0f)) { 162 mDeltaNorm = ParentLayerPoint(1, 1); 163 } else { 164 mDeltaNorm = ParentLayerPoint(mTargetPos.x / hyp, mTargetPos.y / hyp); 165 } 166 } 167 void AndroidFlingPhysics::Sample(const TimeDuration& aDelta, 168 ParentLayerPoint* aOutVelocity, 169 ParentLayerPoint* aOutOffset) { 170 float newVelocity; 171 if (SampleImpl(aDelta, &newVelocity)) { 172 *aOutOffset = (mCurrentPos - mLastPos); 173 *aOutVelocity = ParentLayerPoint(mDeltaNorm.x * newVelocity, 174 mDeltaNorm.y * newVelocity); 175 mLastPos = mCurrentPos; 176 } else { 177 *aOutOffset = (mTargetPos - mLastPos); 178 *aOutVelocity = ParentLayerPoint(); 179 } 180 } 181 182 bool AndroidFlingPhysics::SampleImpl(const TimeDuration& aDelta, 183 float* aOutVelocity) { 184 mDurationSoFar += aDelta; 185 if (mDurationSoFar >= mTargetDuration) { 186 return false; 187 } 188 189 const float timeRatio = 190 mDurationSoFar.ToSeconds() / mTargetDuration.ToSeconds(); 191 float distanceCoef = 1.0f; 192 float velocityCoef = 0.0f; 193 gSplineConstants->CalculateCoefficients(timeRatio, &distanceCoef, 194 &velocityCoef); 195 196 // The caller expects the velocity in pixels per _millisecond_. 197 *aOutVelocity = 198 velocityCoef * mTargetDistance / mTargetDuration.ToMilliseconds(); 199 200 mCurrentPos = mTargetPos * distanceCoef; 201 202 ParentLayerPoint remainder = mTargetPos - mCurrentPos; 203 const float threshold = GetThresholdForFlingEnd(); 204 if (fabsf(remainder.x) < threshold && fabsf(remainder.y) < threshold) { 205 return false; 206 } 207 208 return true; 209 } 210 211 } // namespace layers 212 } // namespace mozilla