First pass of the Launcher Overlay interface / impl

-> Added simple reference launcher extension
-> Make launcher able to handle a null qsb

Change-Id: Ib1575243cac800a335e95bbf00cdc394bb4741c3
diff --git a/src/com/android/launcher3/DragLayer.java b/src/com/android/launcher3/DragLayer.java
index a8a61ea..ff9072a 100644
--- a/src/com/android/launcher3/DragLayer.java
+++ b/src/com/android/launcher3/DragLayer.java
@@ -89,6 +89,8 @@
     private Drawable mLeftHoverDrawableActive;
     private Drawable mRightHoverDrawableActive;
 
+    private boolean mBlockTouches = false;
+
     /**
      * Used to create a new DragLayer from XML.
      *
@@ -185,11 +187,19 @@
         return false;
     }
 
+    public void setBlockTouch(boolean block) {
+        mBlockTouches = block;
+    }
+
     private boolean handleTouchDown(MotionEvent ev, boolean intercept) {
         Rect hitRect = new Rect();
         int x = (int) ev.getX();
         int y = (int) ev.getY();
 
+        if (mBlockTouches) {
+            return true;
+        }
+
         for (AppWidgetResizeFrame child: mResizeFrames) {
             child.getHitRect(hitRect);
             if (hitRect.contains(x, y)) {
@@ -332,6 +342,10 @@
         int x = (int) ev.getX();
         int y = (int) ev.getY();
 
+        if (mBlockTouches) {
+            return true;
+        }
+
         if (action == MotionEvent.ACTION_DOWN) {
             if (handleTouchDown(ev, false)) {
                 return true;
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index be84540..f012968 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -86,6 +86,7 @@
 import android.view.View.OnLongClickListener;
 import android.view.ViewAnimationUtils;
 import android.view.ViewGroup;
+import android.view.ViewStub;
 import android.view.ViewTreeObserver;
 import android.view.Window;
 import android.view.WindowManager;
@@ -227,6 +228,10 @@
 
     private boolean mIsSafeModeEnabled;
 
+    LauncherOverlayCallbacks mLauncherOverlayCallbacks = new LauncherOverlayCallbacksImpl();
+    LauncherOverlay mLauncherOverlay;
+    ViewGroup mLauncherOverlayView;
+
     static final int APPWIDGET_HOST_ID = 1024;
     public static final int EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT = 300;
     private static final int ON_ACTIVITY_RESULT_ANIMATION_DELAY = 500;
@@ -506,6 +511,13 @@
 
         if (mLauncherCallbacks != null) {
             mLauncherCallbacks.onCreate(savedInstanceState);
+            if (mLauncherCallbacks.hasLauncherOverlay()) {
+                ViewStub stub = (ViewStub) findViewById(R.id.launcher_overlay_stub);
+                mLauncherOverlayView = (ViewGroup) stub.inflate();
+                mLauncherOverlay = mLauncherCallbacks.setLauncherOverlayView(mLauncherOverlayView,
+                        mLauncherOverlayCallbacks);
+                mWorkspace.setLauncherOverlay(mLauncherOverlay);
+            }
         }
     }
 
@@ -1144,6 +1156,84 @@
         boolean isScrollingAllowed();
     }
 
+    public interface LauncherOverlay {
+
+        /**
+         * Touch interaction leading to overscroll has begun
+         */
+        public void onScrollInteractionBegin();
+
+        /**
+         * Touch interaction related to overscroll has ended
+         */
+        public void onScrollInteractionEnd();
+
+        /**
+         * Scroll progress, between 0 and 100, when the user scrolls beyond the leftmost
+         * screen (or in the case of RTL, the rightmost screen).
+         */
+        public void onScrollChange(int progress, boolean rtl);
+
+        /**
+         * Screen has stopped scrolling
+         */
+        public void onScrollSettled();
+
+        /**
+         * This method can be called by the Launcher in order to force the LauncherOverlay
+         * to exit fully immersive mode.
+         */
+        public void forceExitFullImmersion();
+    }
+
+    public interface LauncherOverlayCallbacks {
+        /**
+         * This method indicates whether a call to {@link #enterFullImmersion()} will succeed,
+         * however it doesn't modify any state within the launcher.
+         */
+        public boolean canEnterFullImmersion();
+
+        /**
+         * Should be called to tell Launcher that the LauncherOverlay will take over interaction,
+         * eg. by occupying the full screen and handling all touch events.
+         *
+         * @return true if Launcher allows the LauncherOverlay to become fully immersive. In this
+         *          case, Launcher will modify any necessary state and assumes the overlay is
+         *          handling all interaction. If false, the LauncherOverlay should cancel any
+         *
+         */
+        public boolean enterFullImmersion();
+
+        /**
+         * Must be called when exiting fully immersive mode. Indicates to Launcher that it has
+         * full control over UI and state.
+         */
+        public void exitFullImmersion();
+    }
+
+    class LauncherOverlayCallbacksImpl implements LauncherOverlayCallbacks {
+
+        @Override
+        public boolean canEnterFullImmersion() {
+            return mState == State.WORKSPACE;
+        }
+
+        @Override
+        public boolean enterFullImmersion() {
+            if (mState == State.WORKSPACE) {
+                // When fully immersed, disregard any touches which fall through.
+                mDragLayer.setBlockTouch(true);
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public void exitFullImmersion() {
+            mDragLayer.setBlockTouch(false);
+        }
+    }
+
     protected boolean hasSettings() {
         if (mLauncherCallbacks != null) {
             return mLauncherCallbacks.hasSettings();
@@ -4084,11 +4174,8 @@
     }
 
     public View getQsbBar() {
-        if (mLauncherCallbacks != null) {
-            View qsb = mLauncherCallbacks.getQsbBar();
-            if (qsb != null) {
-                return qsb;
-            }
+        if (mLauncherCallbacks != null && mLauncherCallbacks.providesSearch()) {
+            return mLauncherCallbacks.getQsbBar();
         }
 
         if (mQsb == null) {
diff --git a/src/com/android/launcher3/LauncherCallbacks.java b/src/com/android/launcher3/LauncherCallbacks.java
index aef2adc..e0cfa27 100644
--- a/src/com/android/launcher3/LauncherCallbacks.java
+++ b/src/com/android/launcher3/LauncherCallbacks.java
@@ -6,6 +6,7 @@
 import android.os.Bundle;
 import android.view.Menu;
 import android.view.View;
+import android.view.ViewGroup;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -86,4 +87,22 @@
     public boolean overrideWallpaperDimensions();
     public boolean isLauncherPreinstalled();
 
+    /**
+     * Returning true will immediately result in a call to {@link #setLauncherOverlayView(ViewGroup,
+     * com.android.launcher3.Launcher.LauncherOverlayCallbacks)}.
+     *
+     * @return true if this launcher extension will provide an overlay
+     */
+    public boolean hasLauncherOverlay();
+
+    /**
+     * Handshake to establish an overlay relationship
+     *
+     * @param overlayView Full screen overlay ViewGroup into which custom views can be placed.
+     * @param callbacks A set of callbacks provided by Launcher in relation to the overlay
+     * @return an interface used to make requests and notify the Launcher in relation to the overlay
+     */
+    public Launcher.LauncherOverlay setLauncherOverlayView(ViewGroup overlayView,
+            Launcher.LauncherOverlayCallbacks callbacks);
+
 }
diff --git a/src/com/android/launcher3/LauncherExtension.java b/src/com/android/launcher3/LauncherExtension.java
new file mode 100644
index 0000000..10bbd35
--- /dev/null
+++ b/src/com/android/launcher3/LauncherExtension.java
@@ -0,0 +1,354 @@
+package com.android.launcher3;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+/**
+ * This class represents a very trivial LauncherExtension. It primarily serves as a simple
+ * class to exercise the LauncherOverlay interface.
+ */
+public class LauncherExtension extends Launcher {
+
+    //------ Activity methods -------//
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        setLauncherCallbacks(new LauncherExtensionCallbacks());
+        super.onCreate(savedInstanceState);
+    }
+
+    public class LauncherExtensionCallbacks implements LauncherCallbacks {
+
+        LauncherExtensionOverlay mLauncherOverlay = new LauncherExtensionOverlay();
+
+        @Override
+        public void preOnCreate() {
+        }
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+        }
+
+        @Override
+        public void preOnResume() {
+        }
+
+        @Override
+        public void onResume() {
+        }
+
+        @Override
+        public void onStart() {
+        }
+
+        @Override
+        public void onStop() {
+        }
+
+        @Override
+        public void onPause() {
+        }
+
+        @Override
+        public void onDestroy() {
+        }
+
+        @Override
+        public void onSaveInstanceState(Bundle outState) {
+        }
+
+        @Override
+        public void onPostCreate(Bundle savedInstanceState) {
+        }
+
+        @Override
+        public void onNewIntent(Intent intent) {
+        }
+
+        @Override
+        public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        }
+
+        @Override
+        public void onWindowFocusChanged(boolean hasFocus) {
+        }
+
+        @Override
+        public boolean onPrepareOptionsMenu(Menu menu) {
+            return false;
+        }
+
+        @Override
+        public void dump(String prefix, FileDescriptor fd, PrintWriter w, String[] args) {
+        }
+
+        @Override
+        public void onHomeIntent() {
+        }
+
+        @Override
+        public boolean handleBackPressed() {
+            if (mLauncherOverlay.isOverlayPanelShowing()) {
+                mLauncherOverlay.hideOverlayPanel();
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public void onLauncherProviderChange() {
+        }
+
+        @Override
+        public void finishBindingItems(boolean upgradePath) {
+        }
+
+        @Override
+        public void onClickAllAppsButton(View v) {
+        }
+
+        @Override
+        public void bindAllApplications(ArrayList<AppInfo> apps) {
+        }
+
+        @Override
+        public void onClickFolderIcon(View v) {
+        }
+
+        @Override
+        public void onClickAppShortcut(View v) {
+        }
+
+        @Override
+        public void onClickPagedViewIcon(View v) {
+        }
+
+        @Override
+        public void onClickWallpaperPicker(View v) {
+        }
+
+        @Override
+        public void onClickSettingsButton(View v) {
+        }
+
+        @Override
+        public void onClickAddWidgetButton(View v) {
+        }
+
+        @Override
+        public void onPageSwitch(View newPage, int newPageIndex) {
+        }
+
+        @Override
+        public void onWorkspaceLockedChanged() {
+        }
+
+        @Override
+        public void onDragStarted(View view) {
+        }
+
+        @Override
+        public void onInteractionBegin() {
+        }
+
+        @Override
+        public void onInteractionEnd() {
+        }
+
+        @Override
+        public boolean forceDisableVoiceButtonProxy() {
+            return false;
+        }
+
+        @Override
+        public boolean providesSearch() {
+            return true;
+        }
+
+        @Override
+        public boolean startSearch(String initialQuery, boolean selectInitialQuery,
+                Bundle appSearchData, Rect sourceBounds) {
+            return false;
+        }
+
+        @Override
+        public void startVoice() {
+        }
+
+        @Override
+        public boolean hasCustomContentToLeft() {
+            return false;
+        }
+
+        @Override
+        public void populateCustomContentContainer() {
+        }
+
+        @Override
+        public View getQsbBar() {
+            return mLauncherOverlay.getSearchBox();
+        }
+
+        @Override
+        public Intent getFirstRunActivity() {
+            return null;
+        }
+
+        @Override
+        public boolean hasFirstRunActivity() {
+            return false;
+        }
+
+        @Override
+        public boolean hasDismissableIntroScreen() {
+            return false;
+        }
+
+        @Override
+        public View getIntroScreen() {
+            return null;
+        }
+
+        @Override
+        public boolean shouldMoveToDefaultScreenOnHomeIntent() {
+            return true;
+        }
+
+        @Override
+        public boolean hasSettings() {
+            return false;
+        }
+
+        @Override
+        public ComponentName getWallpaperPickerComponent() {
+            return null;
+        }
+
+        @Override
+        public boolean overrideWallpaperDimensions() {
+            return false;
+        }
+
+        @Override
+        public boolean isLauncherPreinstalled() {
+            return false;
+        }
+
+        @Override
+        public boolean hasLauncherOverlay() {
+            return true;
+        }
+
+        @Override
+        public LauncherOverlay setLauncherOverlayView(ViewGroup overlayView,
+                LauncherOverlayCallbacks callbacks) {
+
+            mLauncherOverlay.setOverlayCallbacks(callbacks);
+            mLauncherOverlay.setOverlayView(overlayView);
+
+            return mLauncherOverlay;
+        }
+
+        class LauncherExtensionOverlay implements LauncherOverlay {
+            LauncherOverlayCallbacks mLauncherOverlayCallbacks;
+            ViewGroup mOverlayView;
+            View mSearchBox;
+            View mSearchOverlay;
+            boolean mShowOverlayFeedback;
+            int mProgress;
+            boolean mOverlayPanelShowing;
+
+            @Override
+            public void onScrollInteractionBegin() {
+                if (mLauncherOverlayCallbacks.canEnterFullImmersion()) {
+                    mShowOverlayFeedback = true;
+                    updatePanelOffset(0);
+                    mSearchOverlay.setVisibility(View.VISIBLE);
+                    mSearchOverlay.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+                }
+            }
+
+            @Override
+            public void onScrollChange(int progress, boolean rtl) {
+                mProgress = progress;
+                if (mShowOverlayFeedback) {
+                    updatePanelOffset(progress);
+                }
+            }
+
+            private void updatePanelOffset(int progress) {
+                int panelWidth = mSearchOverlay.getMeasuredWidth();
+                int offset = (int) ((progress / 100f) * panelWidth);
+                mSearchOverlay.setTranslationX(- panelWidth + offset);
+            }
+
+            @Override
+            public void onScrollInteractionEnd() {
+                if (mProgress > 25 && mLauncherOverlayCallbacks.enterFullImmersion()) {
+                    ObjectAnimator oa = LauncherAnimUtils.ofFloat(mSearchOverlay, "translationX", 0);
+                    oa.addListener(new AnimatorListenerAdapter() {
+                        @Override
+                        public void onAnimationEnd(Animator arg0) {
+                            mSearchOverlay.setLayerType(View.LAYER_TYPE_NONE, null);
+                        }
+                    });
+                    oa.start();
+                    mOverlayPanelShowing = true;
+                    mShowOverlayFeedback = false;
+                }
+            }
+
+            @Override
+            public void onScrollSettled() {
+                if (mShowOverlayFeedback) {
+                    mSearchOverlay.setVisibility(View.INVISIBLE);
+                    mSearchOverlay.setLayerType(View.LAYER_TYPE_NONE, null);
+                }
+                mShowOverlayFeedback = false;
+                mProgress = 0;
+            }
+
+            public void hideOverlayPanel() {
+                mLauncherOverlayCallbacks.exitFullImmersion();
+                mSearchOverlay.setVisibility(View.INVISIBLE);
+                mOverlayPanelShowing = false;
+            }
+
+            public boolean isOverlayPanelShowing() {
+                return mOverlayPanelShowing;
+            }
+
+            @Override
+            public void forceExitFullImmersion() {
+                hideOverlayPanel();
+            }
+
+            public void setOverlayView(ViewGroup overlayView) {
+                mOverlayView = (ViewGroup) getLayoutInflater().inflate(
+                        R.layout.launcher_overlay_example, overlayView);
+                mSearchOverlay = mOverlayView.findViewById(R.id.search_overlay);
+                mSearchBox = mOverlayView.findViewById(R.id.search_box);
+            }
+
+            public View getSearchBox() {
+                return mSearchBox;
+            }
+
+            public void setOverlayCallbacks(LauncherOverlayCallbacks callbacks) {
+                mLauncherOverlayCallbacks = callbacks;
+            }
+        };
+    }
+}
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index 48fc0c9..7d65f46 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -204,6 +204,8 @@
 
     protected boolean mAllowLongPress = true;
 
+    private boolean mWasInOverscroll = false;
+
     // Page Indicator
     private int mPageIndicatorViewId;
     private PageIndicator mPageIndicator;
@@ -625,6 +627,7 @@
 
     // a method that subclasses can override to add behavior
     protected void onPageEndMoving() {
+        mWasInOverscroll = false;
     }
 
     /**
@@ -663,6 +666,7 @@
         if (isXBeforeFirstPage) {
             super.scrollTo(0, y);
             if (mAllowOverScroll) {
+                mWasInOverscroll = true;
                 if (isRtl) {
                     overScroll(x - mMaxScrollX);
                 } else {
@@ -672,6 +676,7 @@
         } else if (isXAfterLastPage) {
             super.scrollTo(mMaxScrollX, y);
             if (mAllowOverScroll) {
+                mWasInOverscroll = true;
                 if (isRtl) {
                     overScroll(x);
                 } else {
@@ -679,6 +684,10 @@
                 }
             }
         } else {
+            if (mWasInOverscroll) {
+                overScroll(0);
+                mWasInOverscroll = false;
+            }
             mOverScrollX = x;
             super.scrollTo(x, y);
         }
@@ -1513,6 +1522,7 @@
                 mLastMotionXRemainder = 0;
                 mTouchX = getViewportOffsetX() + getScrollX();
                 mSmoothingTime = System.nanoTime() / NANOTIME_DIV;
+                onScrollInteractionBegin();
                 pageBeginMoving();
             }
         }
@@ -1752,6 +1762,7 @@
             mActivePointerId = ev.getPointerId(0);
 
             if (mTouchState == TOUCH_STATE_SCROLLING) {
+                onScrollInteractionBegin();
                 pageBeginMoving();
             }
             break;
@@ -1940,6 +1951,7 @@
                             getScrollY(), vX, 0, Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
                     invalidate();
                 }
+                onScrollInteractionEnd();
             } else if (mTouchState == TOUCH_STATE_PREV_PAGE) {
                 // at this point we have not moved beyond the touch slop
                 // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so
@@ -2025,6 +2037,15 @@
         mActivePointerId = INVALID_POINTER;
     }
 
+    /**
+     * Triggered by scrolling via touch
+     */
+    protected void onScrollInteractionBegin() {
+    }
+
+    protected void onScrollInteractionEnd() {
+    }
+
     protected void onUnhandledTap(MotionEvent ev) {
         ((Launcher) getContext()).onClick(this);
     }
diff --git a/src/com/android/launcher3/SearchDropTargetBar.java b/src/com/android/launcher3/SearchDropTargetBar.java
index 435dbda..6205088 100644
--- a/src/com/android/launcher3/SearchDropTargetBar.java
+++ b/src/com/android/launcher3/SearchDropTargetBar.java
@@ -19,6 +19,7 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
 import android.content.Context;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
@@ -37,7 +38,7 @@
     private static final int sTransitionOutDuration = 175;
 
     private ObjectAnimator mDropTargetBarAnim;
-    private ObjectAnimator mQSBSearchBarAnim;
+    private ValueAnimator mQSBSearchBarAnim;
     private static final AccelerateInterpolator sAccelerateInterpolator =
             new AccelerateInterpolator();
 
@@ -70,28 +71,38 @@
         mInfoDropTarget.setLauncher(launcher);
         mDeleteDropTarget.setLauncher(launcher);
         mQSBSearchBar = launcher.getQsbBar();
-        if (mEnableDropDownDropTargets) {
-            mQSBSearchBarAnim = LauncherAnimUtils.ofFloat(mQSBSearchBar, "translationY", 0,
-                    -mBarHeight);
+        if (mQSBSearchBar != null) {
+            if (mEnableDropDownDropTargets) {
+                mQSBSearchBarAnim = LauncherAnimUtils.ofFloat(mQSBSearchBar, "translationY", 0,
+                        -mBarHeight);
+            } else {
+                mQSBSearchBarAnim = LauncherAnimUtils.ofFloat(mQSBSearchBar, "alpha", 1f, 0f);
+            }
+            setupAnimation(mQSBSearchBarAnim, mQSBSearchBar);
         } else {
-            mQSBSearchBarAnim = LauncherAnimUtils.ofFloat(mQSBSearchBar, "alpha", 1f, 0f);
+            // Create a no-op animation of the search bar is null
+            mQSBSearchBarAnim = ValueAnimator.ofFloat(0, 0);
+            mQSBSearchBarAnim.setDuration(sTransitionInDuration);
         }
-        setupAnimation(mQSBSearchBarAnim, mQSBSearchBar);
     }
 
     private void prepareStartAnimation(View v) {
         // Enable the hw layers before the animation starts (will be disabled in the onAnimationEnd
         // callback below)
-        v.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+        if (v != null) {
+            v.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+        }
     }
 
-    private void setupAnimation(ObjectAnimator anim, final View v) {
+    private void setupAnimation(ValueAnimator anim, final View v) {
         anim.setInterpolator(sAccelerateInterpolator);
         anim.setDuration(sTransitionInDuration);
         anim.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
-                v.setLayerType(View.LAYER_TYPE_NONE, null);
+                if (v != null) {
+                    v.setLayerType(View.LAYER_TYPE_NONE, null);
+                }
             }
         });
     }
@@ -145,9 +156,9 @@
             mQSBSearchBarAnim.reverse();
         } else {
             mQSBSearchBarAnim.cancel();
-            if (mEnableDropDownDropTargets) {
+            if (mQSBSearchBar != null && mEnableDropDownDropTargets) {
                 mQSBSearchBar.setTranslationY(0);
-            } else {
+            } else if (mQSBSearchBar != null) {
                 mQSBSearchBar.setAlpha(1f);
             }
         }
@@ -161,9 +172,9 @@
             mQSBSearchBarAnim.start();
         } else {
             mQSBSearchBarAnim.cancel();
-            if (mEnableDropDownDropTargets) {
+            if (mQSBSearchBar != null && mEnableDropDownDropTargets) {
                 mQSBSearchBar.setTranslationY(-mBarHeight);
-            } else {
+            } else if (mQSBSearchBar != null) {
                 mQSBSearchBar.setAlpha(0f);
             }
         }
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 2683bec..8ddb837 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -67,6 +67,7 @@
 
 import com.android.launcher3.FolderIcon.FolderRingAnimator;
 import com.android.launcher3.Launcher.CustomContentCallbacks;
+import com.android.launcher3.Launcher.LauncherOverlay;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.compat.PackageInstallerCompat;
 import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo;
@@ -289,6 +290,12 @@
     private boolean mDeferDropAfterUninstall;
     private boolean mUninstallSuccessful;
 
+    // State related to Launcher Overlay
+    LauncherOverlay mLauncherOverlay;
+    boolean mScrollInteractionBegan;
+    boolean mStartedSendingScrollEvents;
+    boolean mShouldSendPageSettled;
+
     private final Runnable mBindPages = new Runnable() {
         @Override
         public void run() {
@@ -1249,6 +1256,58 @@
             stripEmptyScreens();
             mStripScreensOnPageStopMoving = false;
         }
+
+        if (mShouldSendPageSettled) {
+            mLauncherOverlay.onScrollSettled();
+            mShouldSendPageSettled = false;
+        }
+    }
+
+    protected void onScrollInteractionBegin() {
+        super.onScrollInteractionEnd();
+        mScrollInteractionBegan = true;
+    }
+
+    protected void onScrollInteractionEnd() {
+        super.onScrollInteractionEnd();
+        mScrollInteractionBegan = false;
+        if (mStartedSendingScrollEvents) {
+            mStartedSendingScrollEvents = false;
+            mLauncherOverlay.onScrollInteractionEnd();
+        }
+    }
+
+    public void setLauncherOverlay(LauncherOverlay overlay) {
+        mLauncherOverlay = overlay;
+    }
+
+    @Override
+    protected void overScroll(float amount) {
+        boolean isRtl = isLayoutRtl();
+        boolean shouldOverScroll = (amount <= 0 && (!hasCustomContent() || isRtl)) ||
+                (amount >= 0 && (!hasCustomContent() || !isRtl));
+
+        boolean shouldScrollOverlay = (amount <= 0 && mLauncherOverlay != null && !isRtl) ||
+                (amount >= 0 && mLauncherOverlay != null && isRtl);
+
+        if (shouldScrollOverlay) {
+            if (!mStartedSendingScrollEvents && mScrollInteractionBegan) {
+                mStartedSendingScrollEvents = true;
+                mLauncherOverlay.onScrollInteractionBegin();
+                mShouldSendPageSettled = true;
+            }
+            int screenSize = getViewportWidth();
+            float f = (amount / screenSize);
+
+            int progress = (int) Math.abs((f * 100));
+
+            mLauncherOverlay.onScrollChange(progress, isRtl);
+        } else if (shouldOverScroll) {
+            dampedOverScroll(amount);
+            mOverScrollEffect = acceleratedOverFactor(amount);
+        } else {
+            mOverScrollEffect = 0;
+        }
     }
 
     @Override
@@ -1710,18 +1769,6 @@
         }
     }
 
-    @Override
-    protected void overScroll(float amount) {
-        boolean shouldOverScroll = (amount < 0 && (!hasCustomContent() || isLayoutRtl())) ||
-                (amount > 0 && (!hasCustomContent() || !isLayoutRtl()));
-        if (shouldOverScroll) {
-            dampedOverScroll(amount);
-            mOverScrollEffect = acceleratedOverFactor(amount);
-        } else {
-            mOverScrollEffect = 0;
-        }
-    }
-
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
         mWindowToken = getWindowToken();
@@ -2353,10 +2400,6 @@
                 .alpha(finalHotseatAndPageIndicatorAlpha).withLayer();
             hotseatAlpha.addListener(new AlphaUpdateListener(hotseat));
 
-            Animator searchBarAlpha = new LauncherViewPropertyAnimator(searchBar)
-                .alpha(finalSearchBarAlpha).withLayer();
-            searchBarAlpha.addListener(new AlphaUpdateListener(searchBar));
-
             Animator overviewPanelAlpha = new LauncherViewPropertyAnimator(overviewPanel)
                 .alpha(finalOverviewPanelAlpha).withLayer();
             overviewPanelAlpha.addListener(new AlphaUpdateListener(overviewPanel));
@@ -2364,11 +2407,9 @@
             // For animation optimations, we may need to provide the Launcher transition
             // with a set of views on which to force build layers in certain scenarios.
             hotseat.setLayerType(View.LAYER_TYPE_HARDWARE, null);
-            searchBar.setLayerType(View.LAYER_TYPE_HARDWARE, null);
             overviewPanel.setLayerType(View.LAYER_TYPE_HARDWARE, null);
             if (layerViews != null) {
                 layerViews.add(hotseat);
-                layerViews.add(searchBar);
                 layerViews.add(overviewPanel);
             }
 
@@ -2385,11 +2426,21 @@
             overviewPanelAlpha.setDuration(duration);
             pageIndicatorAlpha.setDuration(duration);
             hotseatAlpha.setDuration(duration);
-            searchBarAlpha.setDuration(duration);
+
+            if (searchBar != null) {
+                Animator searchBarAlpha = new LauncherViewPropertyAnimator(searchBar)
+                    .alpha(finalSearchBarAlpha).withLayer();
+                searchBarAlpha.addListener(new AlphaUpdateListener(searchBar));
+                searchBar.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+                if (layerViews != null) {
+                    layerViews.add(searchBar);
+                }
+                searchBarAlpha.setDuration(duration);
+                anim.play(searchBarAlpha);
+            }
 
             anim.play(overviewPanelAlpha);
             anim.play(hotseatAlpha);
-            anim.play(searchBarAlpha);
             anim.play(pageIndicatorAlpha);
             anim.setStartDelay(delay);
         } else {