Add physics motion to items in all apps.

Motion applied to:
- Icons
- Prediction icons
- Prediction divider

Bug: 38349031
Change-Id: I376e6e39080c8c80463a0ce8b104b05e4d576f17
diff --git a/Android.mk b/Android.mk
index 6cb40c5..c8a53d2 100644
--- a/Android.mk
+++ b/Android.mk
@@ -26,7 +26,8 @@
 LOCAL_STATIC_JAVA_LIBRARIES := \
     android-support-v4 \
     android-support-v7-recyclerview \
-    android-support-v7-palette
+    android-support-v7-palette \
+    android-support-dynamic-animation
 
 LOCAL_SRC_FILES := \
     $(call all-java-files-under, src) \
diff --git a/res/layout/all_apps.xml b/res/layout/all_apps.xml
index f3539dc..09b9655 100644
--- a/res/layout/all_apps.xml
+++ b/res/layout/all_apps.xml
@@ -51,6 +51,7 @@
             android:layout_height="match_parent"
             android:layout_gravity="center_horizontal|top"
             android:clipToPadding="false"
+            android:overScrollMode="never"
             android:descendantFocusability="afterDescendants"
             android:focusable="true"
             android:paddingStart="@dimen/container_fastscroll_thumb_max_width"
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index d9ee2c5..a399d74 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -20,6 +20,8 @@
 import android.graphics.Rect;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.InsetDrawable;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.LinearLayoutManager;
 import android.support.v7.widget.RecyclerView;
 import android.text.Selection;
 import android.text.SpannableStringBuilder;
@@ -42,6 +44,7 @@
 import com.android.launcher3.PromiseAppInfo;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.SpringAnimationHandler;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragOptions;
@@ -63,7 +66,7 @@
     private final Launcher mLauncher;
     private final AlphabeticalAppsList mApps;
     private final AllAppsGridAdapter mAdapter;
-    private final RecyclerView.LayoutManager mLayoutManager;
+    private final LinearLayoutManager mLayoutManager;
 
     private AllAppsRecyclerView mAppsRecyclerView;
     private SearchUiManager mSearchUiManager;
@@ -74,6 +77,8 @@
     private int mNumAppsPerRow;
     private int mNumPredictedAppsPerRow;
 
+    private SpringAnimationHandler mSpringAnimationHandler;
+
     public AllAppsContainerView(Context context) {
         this(context, null);
     }
@@ -87,7 +92,9 @@
 
         mLauncher = Launcher.getLauncher(context);
         mApps = new AlphabeticalAppsList(context);
-        mAdapter = new AllAppsGridAdapter(mLauncher, mApps, mLauncher, this);
+        mSpringAnimationHandler = new SpringAnimationHandler(SpringAnimationHandler.Y_DIRECTION);
+        mAdapter = new AllAppsGridAdapter(mLauncher, mApps, mLauncher, this,
+                mSpringAnimationHandler);
         mApps.setAdapter(mAdapter);
         mLayoutManager = mAdapter.getLayoutManager();
         mSearchQueryBuilder = new SpannableStringBuilder();
@@ -227,6 +234,10 @@
         mAppsRecyclerView.setLayoutManager(mLayoutManager);
         mAppsRecyclerView.setAdapter(mAdapter);
         mAppsRecyclerView.setHasFixedSize(true);
+        if (FeatureFlags.LAUNCHER3_PHYSICS) {
+            mAppsRecyclerView.setSpringAnimationHandler(mSpringAnimationHandler);
+            mAppsRecyclerView.addOnScrollListener(new SpringMotionOnScrollListener());
+        }
 
         mSearchContainer = findViewById(R.id.search_container);
         mSearchUiManager = (SearchUiManager) mSearchContainer;
@@ -404,4 +415,36 @@
             }
         }
     }
+
+    public SpringAnimationHandler getSpringAnimationHandler() {
+        return mSpringAnimationHandler;
+    }
+
+    public class SpringMotionOnScrollListener extends RecyclerView.OnScrollListener {
+
+        private int mScrollState = RecyclerView.SCROLL_STATE_IDLE;
+
+        @Override
+        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+            if (mScrollState == RecyclerView.SCROLL_STATE_DRAGGING) {
+                mSpringAnimationHandler.skipToEnd();
+                return;
+            }
+
+            int first = mLayoutManager.findFirstVisibleItemPosition();
+            int last = mLayoutManager.findLastVisibleItemPosition();
+
+            // We only show the spring animation when at the top or bottom, so we wait until the
+            // first or last row is visible to ensure that all animations run in sync.
+            if (first == 0 || last >= mAdapter.getItemCount() - mAdapter.getNumAppsPerRow()) {
+                mSpringAnimationHandler.animateToFinalPosition(0);
+            }
+        }
+
+        @Override
+        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+            super.onScrollStateChanged(recyclerView, newState);
+            mScrollState = newState;
+        }
+    }
 }
diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
index cfd04e2..d3d23ca 100644
--- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
+++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
@@ -19,6 +19,7 @@
 import android.content.Intent;
 import android.content.res.Resources;
 import android.graphics.Point;
+import android.support.animation.SpringAnimation;
 import android.support.v4.view.accessibility.AccessibilityEventCompat;
 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
 import android.support.v4.view.accessibility.AccessibilityRecordCompat;
@@ -38,6 +39,8 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.allapps.AlphabeticalAppsList.AdapterItem;
+import com.android.launcher3.anim.SpringAnimationHandler;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.discovery.AppDiscoveryAppInfo;
 import com.android.launcher3.discovery.AppDiscoveryItemView;
 import com.android.launcher3.util.PackageManagerHelper;
@@ -80,6 +83,8 @@
             | VIEW_TYPE_PREDICTION_ICON;
     public static final int VIEW_TYPE_MASK_CONTENT = VIEW_TYPE_MASK_ICON
             | VIEW_TYPE_DISCOVERY_ITEM;
+    public static final int VIEW_TYPE_MASK_HAS_SPRINGS = VIEW_TYPE_MASK_ICON
+            | VIEW_TYPE_PREDICTION_DIVIDER;
 
 
     public interface BindViewCallback {
@@ -90,6 +95,12 @@
      * ViewHolder for each icon.
      */
     public static class ViewHolder extends RecyclerView.ViewHolder {
+
+        /**
+         * Springs used for items where isViewType(viewType, VIEW_TYPE_MASK_HAS_SPRINGS) is true.
+         */
+        private SpringAnimation spring;
+
         public ViewHolder(View v) {
             super(v);
         }
@@ -202,8 +213,11 @@
     // The intent to send off to the market app, updated each time the search query changes.
     private Intent mMarketSearchIntent;
 
+    private SpringAnimationHandler mSpringAnimationHandler;
+
     public AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps, View.OnClickListener
-            iconClickListener, View.OnLongClickListener iconLongClickListener) {
+            iconClickListener, View.OnLongClickListener iconLongClickListener,
+            SpringAnimationHandler springAnimationHandler) {
         Resources res = launcher.getResources();
         mLauncher = launcher;
         mApps = apps;
@@ -214,6 +228,7 @@
         mLayoutInflater = LayoutInflater.from(launcher);
         mIconClickListener = iconClickListener;
         mIconLongClickListener = iconLongClickListener;
+        mSpringAnimationHandler = springAnimationHandler;
     }
 
     public static boolean isDividerViewType(int viewType) {
@@ -236,6 +251,10 @@
         mGridLayoutMgr.setSpanCount(appsPerRow);
     }
 
+    public int getNumAppsPerRow() {
+        return mAppsPerRow;
+    }
+
     public void setIconFocusListener(OnFocusChangeListener focusListener) {
         mIconFocusListener = focusListener;
     }
@@ -327,7 +346,6 @@
                 AppInfo info = mApps.getAdapterItems().get(position).appInfo;
                 BubbleTextView icon = (BubbleTextView) holder.itemView;
                 icon.applyFromApplicationInfo(info);
-                icon.setAccessibilityDelegate(mLauncher.getAccessibilityDelegate());
                 break;
             case VIEW_TYPE_DISCOVERY_ITEM:
                 AppDiscoveryAppInfo appDiscoveryAppInfo = (AppDiscoveryAppInfo)
@@ -365,6 +383,23 @@
     }
 
     @Override
+    public void onViewAttachedToWindow(ViewHolder holder) {
+        int type = holder.getItemViewType();
+        if (FeatureFlags.LAUNCHER3_PHYSICS && isViewType(type, VIEW_TYPE_MASK_HAS_SPRINGS)) {
+            holder.spring = mSpringAnimationHandler.add(holder.itemView,
+                    holder.getAdapterPosition(), mApps, mAppsPerRow, holder.spring);
+        }
+    }
+
+    @Override
+    public void onViewDetachedFromWindow(ViewHolder holder) {
+        int type = holder.getItemViewType();
+        if (FeatureFlags.LAUNCHER3_PHYSICS && isViewType(type, VIEW_TYPE_MASK_HAS_SPRINGS)) {
+            holder.spring = mSpringAnimationHandler.remove(holder.spring);
+        }
+    }
+
+    @Override
     public boolean onFailedToRecycleView(ViewHolder holder) {
         // Always recycle and we will reset the view when it is bound
         return true;
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
index 16b2bd1..d76abcc 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -28,8 +28,8 @@
 import com.android.launcher3.BaseRecyclerView;
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
+import com.android.launcher3.anim.SpringAnimationHandler;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.graphics.DrawableFactory;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
@@ -53,6 +53,8 @@
     private AllAppsBackgroundDrawable mEmptySearchBackground;
     private int mEmptySearchBackgroundTopOffset;
 
+    private SpringAnimationHandler mSpringAnimationHandler;
+
     public AllAppsRecyclerView(Context context) {
         this(context, null);
     }
@@ -75,6 +77,18 @@
                 R.dimen.all_apps_empty_search_bg_top_offset);
     }
 
+    public void setSpringAnimationHandler(SpringAnimationHandler springAnimationHandler) {
+        mSpringAnimationHandler = springAnimationHandler;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent e) {
+        if (FeatureFlags.LAUNCHER3_PHYSICS && mSpringAnimationHandler != null) {
+            mSpringAnimationHandler.addMovement(e);
+        }
+        return super.onTouchEvent(e);
+    }
+
     /**
      * Sets the list of apps in this view, used to determine the fastscroll position.
      */
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index 7c6ff51..dd0d238 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -22,6 +22,7 @@
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace;
+import com.android.launcher3.anim.SpringAnimationHandler;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dynamicui.ExtractedColors;
 import com.android.launcher3.graphics.GradientView;
@@ -99,6 +100,8 @@
     private GradientView mGradientView;
     private ScrimView mScrimView;
 
+    private SpringAnimationHandler mSpringAnimationHandler;
+
     public AllAppsTransitionController(Launcher l) {
         mLauncher = l;
         mDetector = new VerticalPullDetector(l);
@@ -161,6 +164,9 @@
 
     @Override
     public boolean onControllerTouchEvent(MotionEvent ev) {
+        if (hasSpringAnimationHandler()) {
+            mSpringAnimationHandler.addMovement(ev);
+        }
         return mDetector.onTouchEvent(ev);
     }
 
@@ -179,6 +185,9 @@
         mCurrentAnimation = LauncherAnimUtils.createAnimatorSet();
         mShiftStart = mAppsView.getTranslationY();
         preparePull(start);
+        if (hasSpringAnimationHandler()) {
+            mSpringAnimationHandler.skipToEnd();
+        }
     }
 
     @Override
@@ -214,6 +223,9 @@
                 mLauncher.showAppsView(true /* animated */,
                         false /* updatePredictedApps */,
                         false /* focusSearchBar */);
+                if (hasSpringAnimationHandler()) {
+                    mSpringAnimationHandler.animateToFinalPosition(0);
+                }
             } else {
                 calculateDuration(velocity, Math.abs(mShiftRange - mAppsView.getTranslationY()));
                 mLauncher.showWorkspace(true);
@@ -498,6 +510,9 @@
 
     public void finishPullUp() {
         mHotseat.setVisibility(View.INVISIBLE);
+        if (hasSpringAnimationHandler()) {
+            mSpringAnimationHandler.reset();
+        }
         setProgress(0f);
     }
 
@@ -506,6 +521,9 @@
         mHotseat.setBackgroundTransparent(false /* transparent */);
         mHotseat.setVisibility(View.VISIBLE);
         mAppsView.reset();
+        if (hasSpringAnimationHandler()) {
+            mSpringAnimationHandler.reset();
+        }
         setProgress(1f);
     }
 
@@ -537,6 +555,11 @@
         mCaretController = new AllAppsCaretController(
                 mWorkspace.getPageIndicator().getCaretDrawable(), mLauncher);
         mAppsView.getSearchUiManager().addOnScrollRangeChangeListener(this);
+        mSpringAnimationHandler = mAppsView.getSpringAnimationHandler();
+    }
+
+    private boolean hasSpringAnimationHandler() {
+        return FeatureFlags.LAUNCHER3_PHYSICS && mSpringAnimationHandler != null;
     }
 
     @Override
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index 0474419..b84c627 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -228,6 +228,13 @@
     }
 
     /**
+     * Returns the predicted apps.
+     */
+    public List<AppInfo> getPredictedApps() {
+        return mPredictedApps;
+    }
+
+    /**
      * Returns fast scroller sections of all the current filtered applications.
      */
     public List<FastScrollSectionInfo> getFastScrollerSections() {
diff --git a/src/com/android/launcher3/anim/SpringAnimationHandler.java b/src/com/android/launcher3/anim/SpringAnimationHandler.java
new file mode 100644
index 0000000..5792127
--- /dev/null
+++ b/src/com/android/launcher3/anim/SpringAnimationHandler.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.anim;
+
+import android.support.animation.DynamicAnimation;
+import android.support.animation.SpringAnimation;
+import android.support.animation.SpringForce;
+import android.support.annotation.IntDef;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+
+import com.android.launcher3.Utilities;
+import com.android.launcher3.allapps.AlphabeticalAppsList;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+
+/**
+ * Handler class that manages springs for a set of views that should all move based on the same
+ * {@link MotionEvent}s.
+ *
+ * Supports using physics for X or Y translations.
+ */
+public class SpringAnimationHandler {
+
+    private static final String TAG = "SpringAnimationHandler";
+    private static final boolean DEBUG = false;
+
+    private static final float DEFAULT_MAX_VALUE = 100;
+    private static final float DEFAULT_MIN_VALUE = -DEFAULT_MAX_VALUE;
+
+    private static final float SPRING_DAMPING_RATIO = 0.55f;
+    private static final float MIN_SPRING_STIFFNESS = 580f;
+    private static final float MAX_SPRING_STIFFNESS = 900f;
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({Y_DIRECTION, X_DIRECTION})
+    public @interface Direction {}
+    public static final int Y_DIRECTION = 0;
+    public static final int X_DIRECTION = 1;
+    private int mDirection;
+
+    private VelocityTracker mVelocityTracker;
+    private float mCurrentVelocity = 0;
+    private boolean mShouldComputeVelocity = false;
+
+    private ArrayList<SpringAnimation> mAnimations = new ArrayList<>();
+
+    public SpringAnimationHandler(@Direction int direction) {
+        mDirection = direction;
+        mVelocityTracker = VelocityTracker.obtain();
+    }
+
+    public SpringAnimation add(View view, int position, AlphabeticalAppsList apps, int appsPerRow,
+            SpringAnimation recycle) {
+        int numPredictedApps = Math.min(appsPerRow, apps.getPredictedApps().size());
+        int appPosition = getAppPosition(position, numPredictedApps, appsPerRow);
+
+        int col = appPosition % appsPerRow;
+        int row = appPosition / appsPerRow;
+
+        int numTotalRows = apps.getNumAppRows() - 1; // zero offset
+        if (row > (numTotalRows / 2)) {
+            // Mirror the rows so that the top row acts the same as the bottom row.
+            row = Math.abs(numTotalRows - row);
+        }
+
+        // We manipulate the stiffness, min, and max values based on the items distance to the first
+        // row and the items distance to the center column to create the ^-shaped motion effect.
+        float rowFactor = (1 + row) * 0.5f;
+        float colFactor = getColumnFactor(col, appsPerRow);
+
+        float minValue = DEFAULT_MIN_VALUE * (rowFactor + colFactor);
+        float maxValue = DEFAULT_MAX_VALUE * (rowFactor + colFactor);
+
+        float stiffness = Utilities.boundToRange(MAX_SPRING_STIFFNESS - (row * 50f),
+                MIN_SPRING_STIFFNESS, MAX_SPRING_STIFFNESS);
+
+        SpringAnimation animation = (recycle != null ? recycle : createSpringAnimation(view))
+                .setStartVelocity(mCurrentVelocity)
+                .setMinValue(minValue)
+                .setMaxValue(maxValue);
+        animation.getSpring().setStiffness(stiffness);
+
+        mAnimations.add(animation);
+        return animation;
+    }
+
+    public SpringAnimation remove(SpringAnimation animation) {
+        animation.skipToEnd();
+        mAnimations.remove(animation);
+        return animation;
+    }
+
+    public void addMovement(MotionEvent event) {
+        int action = event.getActionMasked();
+        if (DEBUG) Log.d(TAG, "addMovement#action=" + action);
+        switch (action) {
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_DOWN:
+                reset();
+                break;
+        }
+
+        getVelocityTracker().addMovement(event);
+        mShouldComputeVelocity = true;
+    }
+
+    public void animateToFinalPosition(float position) {
+        if (DEBUG) Log.d(TAG, "animateToFinalPosition#computeVelocity=" + mShouldComputeVelocity);
+
+        if (mShouldComputeVelocity) {
+            computeVelocity();
+            setStartVelocity(mCurrentVelocity);
+        }
+
+        int size = mAnimations.size();
+        for (int i = 0; i < size; ++i) {
+            mAnimations.get(i).animateToFinalPosition(position);
+        }
+
+        reset();
+    }
+
+    public void skipToEnd() {
+        if (DEBUG) Log.d(TAG, "setStartVelocity#skipToEnd");
+        if (DEBUG) Log.v(TAG, "setStartVelocity#skipToEnd", new Exception());
+
+        int size = mAnimations.size();
+        for (int i = 0; i < size; ++i) {
+            mAnimations.get(i).skipToEnd();
+        }
+    }
+
+    public void reset() {
+        if (mVelocityTracker != null) {
+            mVelocityTracker.recycle();
+            mVelocityTracker = null;
+        }
+        mCurrentVelocity = 0;
+    }
+
+    private void setStartVelocity(float velocity) {
+        int size = mAnimations.size();
+        for (int i = 0; i < size; ++i) {
+            mAnimations.get(i).setStartVelocity(velocity);
+        }
+    }
+
+    private void computeVelocity() {
+        getVelocityTracker().computeCurrentVelocity(300);
+
+        mCurrentVelocity = isVerticalDirection()
+                ? getVelocityTracker().getYVelocity()
+                : getVelocityTracker().getXVelocity();
+        mShouldComputeVelocity = false;
+
+        if (DEBUG) Log.d(TAG, "computeVelocity=" + mCurrentVelocity);
+    }
+
+    private boolean isVerticalDirection() {
+        return mDirection == Y_DIRECTION;
+    }
+
+    private SpringAnimation createSpringAnimation(View view) {
+        DynamicAnimation.ViewProperty property = isVerticalDirection()
+                ? DynamicAnimation.TRANSLATION_Y
+                : DynamicAnimation.TRANSLATION_X;
+
+        return new SpringAnimation(view, property, 0)
+                .setStartValue(1f)
+                .setSpring(new SpringForce(0)
+                .setDampingRatio(SPRING_DAMPING_RATIO));
+    }
+
+    /**
+     * @return The app position is the position of the app in the Adapter if we ignored all other
+     * view types.
+     *
+     * ie. The first predicted app is at position 0, and the first app of all apps is
+     *     at {@param appsPerRow}.
+     */
+    private int getAppPosition(int position, int numPredictedApps, int appsPerRow) {
+        int appPosition = position;
+        int numDividerViews = 1 + (numPredictedApps == 0 ? 0 : 1);
+
+        int allAppsStartAt = numDividerViews + numPredictedApps;
+        if (numDividerViews == 1 || position < allAppsStartAt) {
+            appPosition -= 1;
+        } else {
+            // We cannot assume that the predicted row will always be full.
+            int numPredictedAppsOffset = appsPerRow - numPredictedApps;
+            appPosition = position + numPredictedAppsOffset - numDividerViews;
+        }
+
+        return appPosition;
+    }
+
+    /**
+     * Increase the column factor as the distance increases between the column and the center
+     * column(s).
+     */
+    private float getColumnFactor(int col, int numCols) {
+        float centerColumn = numCols / 2;
+        int distanceToCenter = (int) Math.abs(col - centerColumn);
+
+        boolean evenNumberOfColumns = numCols % 2 == 0;
+        if (evenNumberOfColumns && col < centerColumn) {
+            distanceToCenter -= 1;
+        }
+
+        float factor = 0;
+        while (distanceToCenter > 0) {
+            if (distanceToCenter == 1) {
+                factor += 0.2f;
+            } else {
+                factor += 0.1f;
+            }
+            --distanceToCenter;
+        }
+
+        return factor;
+    }
+
+    private VelocityTracker getVelocityTracker() {
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+        return mVelocityTracker;
+    }
+}
diff --git a/src_flags/com/android/launcher3/config/FeatureFlags.java b/src_flags/com/android/launcher3/config/FeatureFlags.java
index c0184fa..2f97a2b 100644
--- a/src_flags/com/android/launcher3/config/FeatureFlags.java
+++ b/src_flags/com/android/launcher3/config/FeatureFlags.java
@@ -40,6 +40,8 @@
     public static boolean LAUNCHER3_PROMISE_APPS_IN_ALL_APPS = true;
     // When enabled uses the AllAppsRadialGradientAndScrimDrawable for all apps
     public static boolean LAUNCHER3_GRADIENT_ALL_APPS = false;
+    // When enabled allows use of physics based motions in the Launcher.
+    public static boolean LAUNCHER3_PHYSICS = false;
 
     // Feature flag to enable moving the QSB on the 0th screen of the workspace.
     public static final boolean QSB_ON_FIRST_SCREEN = true;