Bug 1448439 - Add an AndroidFlingPhysics class containing an implementation of Chrome's fling physics on Android. r=kats
authorBotond Ballo <botond@mozilla.com>
Fri, 20 Apr 2018 18:42:08 -0400
changeset 472036 f6f3318816787873baf0a0c83c90a42bec1f389c
parent 472035 da26dcf12417c29b3d1f11e251e088ffb83632c9
child 472037 9ee2b45eeb3abb0ee922dddfd2adbd47fa4dd261
push id1728
push userjlund@mozilla.com
push dateMon, 18 Jun 2018 21:12:27 +0000
treeherdermozilla-release@c296fde26f5f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskats
bugs1448439
milestone61.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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',