Bug 1448439 - Add an AndroidFlingPhysics class containing an implementation of Chrome's fling physics on Android. r=kats draft
authorBotond Ballo <botond@mozilla.com>
Fri, 20 Apr 2018 18:42:08 -0400
changeset 788682 11a3ebd03430e0981be156638762987c7c07a458
parent 788681 e53f1d7bbe3e0a588f5d5ba530c59e03cdc50d69
child 788683 c454a6769ccd0ca9951c41ef2ea9990c24208373
push id108063
push userbballo@mozilla.com
push dateThu, 26 Apr 2018 20:42:29 +0000
reviewerskats
bugs1448439
milestone61.0a1
Bug 1448439 - Add an AndroidFlingPhysics class containing an implementation of Chrome's fling physics on Android. r=kats MozReview-Commit-ID: 509Cl04rozm
gfx/layers/apz/src/AndroidFlingPhysics.cpp
gfx/layers/apz/src/AndroidFlingPhysics.h
gfx/layers/apz/src/AsyncPanZoomController.h
gfx/layers/moz.build
new file mode 100644
--- /dev/null
+++ b/gfx/layers/apz/src/AndroidFlingPhysics.cpp
@@ -0,0 +1,190 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AndroidFlingPhysics.h"
+
+#include <cmath>
+
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/StaticPtr.h"
+
+namespace mozilla {
+namespace layers {
+
+// The fling physics calculations implemented here are adapted from
+// Chrome's implementation of fling physics on Android:
+// https://cs.chromium.org/chromium/src/ui/events/android/scroller.cc?rcl=3ae3aaff927038a5c644926842cb0c31dea60c79
+
+static double ComputeDeceleration(float aFriction)
+{
+  const float kGravityEarth = 9.80665f;
+  return kGravityEarth  // g (m/s^2)
+         * 39.37f       // inch/meter
+         * 160.f        // pixels/inch
+         * aFriction;
+}
+
+// == std::log(0.78f) / std::log(0.9f)
+const float kDecelerationRate = 2.3582018f;
+
+// Default friction constant in android.view.ViewConfiguration.
+const float kFlingFriction = 0.015f;
+
+// Tension lines cross at (kInflexion, 1).
+const float kInflexion = 0.35f;
+
+// Fling scroll is stopped when the scroll position is |kThresholdForFlingEnd|
+// pixels or closer from the end.
+const float kThresholdForFlingEnd = 0.1;
+
+const float kTuningCoeff = ComputeDeceleration(0.84f);
+
+static double ComputeSplineDeceleration(ParentLayerCoord aVelocity)
+{
+  float velocityPerSec = aVelocity * 1000.0f;
+  return std::log(kInflexion * velocityPerSec / (kFlingFriction * kTuningCoeff));
+}
+
+static TimeDuration ComputeFlingDuration(ParentLayerCoord aVelocity)
+{
+  const double splineDecel = ComputeSplineDeceleration(aVelocity);
+  const double timeSeconds = std::exp(splineDecel / (kDecelerationRate - 1.0));
+  return TimeDuration::FromSeconds(timeSeconds);
+}
+
+static ParentLayerCoord ComputeFlingDistance(ParentLayerCoord aVelocity)
+{
+  const double splineDecel = ComputeSplineDeceleration(aVelocity);
+  return kFlingFriction * kTuningCoeff *
+      std::exp(kDecelerationRate / (kDecelerationRate - 1.0) * splineDecel);
+}
+
+struct SplineConstants {
+public:
+  SplineConstants() {
+    const float kStartTension = 0.5f;
+    const float kEndTension = 1.0f;
+    const float kP1 = kStartTension * kInflexion;
+    const float kP2 = 1.0f - kEndTension * (1.0f - kInflexion);
+
+    float xMin = 0.0f;
+    for (int i = 0; i < kNumSamples; i++) {
+      const float alpha = static_cast<float>(i) / kNumSamples;
+
+      float xMax = 1.0f;
+      float x, tx, coef;
+      while (true) {
+        x = xMin + (xMax - xMin) / 2.0f;
+        coef = 3.0f * x * (1.0f - x);
+        tx = coef * ((1.0f - x) * kP1 + x * kP2) + x * x * x;
+        if (FuzzyEqualsAdditive(tx, alpha)) {
+          break;
+        }
+        if (tx > alpha) {
+          xMax = x;
+        } else {
+          xMin = x;
+        }
+      }
+      mSplinePositions[i] = coef * ((1.0f - x) * kStartTension + x) + x * x * x;
+    }
+    mSplinePositions[kNumSamples] = 1.0f;
+  }
+
+  void CalculateCoefficients(float aTime,
+                             float* aOutDistanceCoef,
+                             float* aOutVelocityCoef)
+  {
+    *aOutDistanceCoef = 1.0f;
+    *aOutVelocityCoef = 0.0f;
+    const int index = static_cast<int>(kNumSamples * aTime);
+    if (index < kNumSamples) {
+      const float tInf = static_cast<float>(index) / kNumSamples;
+      const float dInf = mSplinePositions[index];
+      const float tSup = static_cast<float>(index + 1) / kNumSamples;
+      const float dSup = mSplinePositions[index + 1];
+      *aOutVelocityCoef = (dSup - dInf) / (tSup - tInf);
+      *aOutDistanceCoef = dInf + (aTime - tInf) * *aOutVelocityCoef;
+    }
+  }
+private:
+  static const int kNumSamples = 100;
+  float mSplinePositions[kNumSamples + 1];
+};
+
+StaticAutoPtr<SplineConstants> gSplineConstants;
+
+/* static */ void AndroidFlingPhysics::InitializeGlobalState()
+{
+  gSplineConstants = new SplineConstants();
+  ClearOnShutdown(&gSplineConstants);
+}
+
+void AndroidFlingPhysics::Init(const ParentLayerPoint& aStartingVelocity)
+{
+  mVelocity = aStartingVelocity.Length();
+  mTargetDuration = ComputeFlingDuration(mVelocity);
+  MOZ_ASSERT(!mTargetDuration.IsZero());
+  mDurationSoFar = TimeDuration();
+  mLastPos = ParentLayerPoint();
+  mCurrentPos = ParentLayerPoint();
+  float coeffX = mVelocity == 0 ? 1.0f : aStartingVelocity.x / mVelocity;
+  float coeffY = mVelocity == 0 ? 1.0f : aStartingVelocity.y / mVelocity;
+  mTargetDistance = ComputeFlingDistance(mVelocity);
+  mTargetPos = ParentLayerPoint(mTargetDistance * coeffX,
+                                mTargetDistance * coeffY);
+  const float hyp = mTargetPos.Length();
+  if (FuzzyEqualsAdditive(hyp, 0.0f)) {
+    mDeltaNorm = ParentLayerPoint(1, 1);
+  } else {
+    mDeltaNorm = ParentLayerPoint(mTargetPos.x / hyp, mTargetPos.y / hyp);
+  }
+}
+void AndroidFlingPhysics::Sample(const TimeDuration& aDelta,
+                                 ParentLayerPoint* aOutVelocity,
+                                 ParentLayerPoint* aOutOffset)
+{
+  float newVelocity;
+  if (SampleImpl(aDelta, &newVelocity)) {
+    *aOutOffset = (mCurrentPos - mLastPos);
+    *aOutVelocity = ParentLayerPoint(mDeltaNorm.x * newVelocity,
+                                     mDeltaNorm.y * newVelocity);
+    mLastPos = mCurrentPos;
+  } else {
+    *aOutOffset = (mTargetPos - mLastPos);
+    *aOutVelocity = ParentLayerPoint();
+  }
+}
+
+bool AndroidFlingPhysics::SampleImpl(const TimeDuration& aDelta,
+                                     float* aOutVelocity)
+{
+  mDurationSoFar += aDelta;
+  if (mDurationSoFar >= mTargetDuration) {
+    return false;
+  }
+
+  const float timeRatio = mDurationSoFar.ToSeconds() / mTargetDuration.ToSeconds();
+  float distanceCoef = 1.0f;
+  float velocityCoef = 0.0f;
+  gSplineConstants->CalculateCoefficients(timeRatio, &distanceCoef, &velocityCoef);
+
+  // The caller expects the velocity in pixels per _millisecond_.
+  *aOutVelocity = velocityCoef * mTargetDistance / mTargetDuration.ToMilliseconds();
+
+  mCurrentPos = mTargetPos * distanceCoef;
+
+  ParentLayerPoint remainder = mTargetPos - mCurrentPos;
+  if (fabsf(remainder.x) < kThresholdForFlingEnd && fabsf(remainder.y) < kThresholdForFlingEnd) {
+    return false;
+  }
+
+  return true;
+}
+
+
+} // namespace layers
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/gfx/layers/apz/src/AndroidFlingPhysics.h
@@ -0,0 +1,46 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_AndroidFlingPhysics_h_
+#define mozilla_layers_AndroidFlingPhysics_h_
+
+#include "AsyncPanZoomController.h"
+#include "Units.h"
+#include "gfxPrefs.h"
+#include "mozilla/Assertions.h"
+
+namespace mozilla {
+namespace layers {
+
+class AndroidFlingPhysics {
+public:
+  void Init(const ParentLayerPoint& aVelocity);
+  void Sample(const TimeDuration& aDelta,
+              ParentLayerPoint* aOutVelocity,
+              ParentLayerPoint* aOutOffset);
+
+  static void InitializeGlobalState();
+private:
+  // Returns false if the animation should end.
+  bool SampleImpl(const TimeDuration& aDelta, float* aOutVelocity);
+
+  // Information pertaining to the current fling.
+  // This is initialized on each call to Init().
+  ParentLayerCoord mVelocity;  // diagonal velocity (length of velocity vector)
+  TimeDuration mTargetDuration;
+  TimeDuration mDurationSoFar;
+  ParentLayerPoint mLastPos;
+  ParentLayerPoint mCurrentPos;
+  ParentLayerCoord mTargetDistance;  // diagonal distance
+  ParentLayerPoint mTargetPos;  // really a target *offset* relative to the
+                                // start position, which we don't track
+  ParentLayerPoint mDeltaNorm;  // mTargetPos with length normalized to 1
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_AndroidFlingPhysics_h_
--- a/gfx/layers/apz/src/AsyncPanZoomController.h
+++ b/gfx/layers/apz/src/AsyncPanZoomController.h
@@ -45,16 +45,17 @@ class APZCTreeManager;
 struct ScrollableLayerGuid;
 class CompositorController;
 class MetricsSharingController;
 class GestureEventListener;
 struct AsyncTransform;
 class AsyncPanZoomAnimation;
 class StackScrollerFlingAnimation;
 template <typename FlingPhysics> class GenericFlingAnimation;
+class AndroidFlingPhysics;
 class DesktopFlingPhysics;
 class InputBlockState;
 struct FlingHandoffState;
 class TouchBlockState;
 class PanGestureBlockState;
 class OverscrollHandoffChain;
 struct OverscrollHandoffState;
 class StateChangeNotificationBlocker;
@@ -1159,16 +1160,17 @@ public:
   ParentLayerPoint AttemptFling(const FlingHandoffState& aHandoffState);
 
   ParentLayerPoint AdjustHandoffVelocityForOverscrollBehavior(ParentLayerPoint& aHandoffVelocity) const;
 
 private:
   friend class StackScrollerFlingAnimation;
   friend class AutoscrollAnimation;
   template <typename FlingPhysics> friend class GenericFlingAnimation;
+  friend class AndroidFlingPhysics;
   friend class DesktopFlingPhysics;
   friend class OverscrollAnimation;
   friend class SmoothScrollAnimation;
   friend class GenericScrollAnimation;
   friend class WheelScrollAnimation;
   friend class KeyboardScrollAnimation;
 
   friend class GenericOverscrollEffect;
--- a/gfx/layers/moz.build
+++ b/gfx/layers/moz.build
@@ -284,16 +284,17 @@ if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'coco
         'MacIOSurfaceHelpers.cpp',
         'MacIOSurfaceImage.cpp',
     ]
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'android':
     UNIFIED_SOURCES += [
         'apz/src/AndroidAPZ.cpp',
         'apz/src/AndroidDynamicToolbarAnimator.cpp',
+        'apz/src/AndroidFlingPhysics.cpp',
     ]
     EXPORTS.mozilla.layers += [
         'apz/src/AndroidDynamicToolbarAnimator.h',
     ]
 
 UNIFIED_SOURCES += [
     'AnimationHelper.cpp',
     'AnimationInfo.cpp',