Add PageIndicator interface and custom PageIndicatorLine view.

- The current PageIndicator has been renamed to PageIndicatorDots
  and PageIndicatorMarker has been renamed to PageIndicatorDot.
- PageIndicatorDots and PageIndicatorLine implement PageIndicator.
- PageIndicatorLine uses scroll progress and number of pages to
  draw a line of the correct size and position.
- All of these page indicator files are now in a pageindicators package.

Bug: 27227498

Change-Id: I9230d2e0600ce583989bd31d0b0e252b148d15c2
diff --git a/res/layout-port/launcher.xml b/res/layout-port/launcher.xml
index f711274..4576e4d 100644
--- a/res/layout-port/launcher.xml
+++ b/res/layout-port/launcher.xml
@@ -57,12 +57,10 @@
 
         <!-- Keep these behind the workspace so that they are not visible when
              we go into AllApps -->
-        <include
+        <com.android.launcher3.pageindicators.PageIndicatorLine
             android:id="@+id/page_indicator"
-            layout="@layout/page_indicator"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="center_horizontal" />
+            android:layout_width="match_parent"
+            android:layout_height="1dp" />
 
         <include
             android:id="@+id/app_info_drop_target_bar"
diff --git a/res/layout-sw720dp/launcher.xml b/res/layout-sw720dp/launcher.xml
index 780d645..0f755d8 100644
--- a/res/layout-sw720dp/launcher.xml
+++ b/res/layout-sw720dp/launcher.xml
@@ -64,11 +64,10 @@
 
         <!-- Keep these behind the workspace so that they are not visible when
              we go into AllApps -->
-        <include android:id="@+id/page_indicator"
-            layout="@layout/page_indicator"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="center_horizontal" />
+        <com.android.launcher3.pageindicators.PageIndicatorLine
+            android:id="@+id/page_indicator"
+            android:layout_width="match_parent"
+            android:layout_height="1dp" />
 
         <include layout="@layout/widgets_view"
             android:id="@+id/widgets_view"
diff --git a/res/layout/page_indicator.xml b/res/layout/page_indicator.xml
index 68fe3dd..5655159 100644
--- a/res/layout/page_indicator.xml
+++ b/res/layout/page_indicator.xml
@@ -13,9 +13,9 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<com.android.launcher3.PageIndicator
+<com.android.launcher3.pageindicators.PageIndicatorDots
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:launcher="http://schemas.android.com/apk/res-auto"
     android:animateLayoutChanges="true"
     launcher:windowSize="@integer/config_maxNumberOfPageIndicatorsToShow">
-</com.android.launcher3.PageIndicator>
+</com.android.launcher3.pageindicators.PageIndicatorDots>
diff --git a/res/layout/page_indicator_marker.xml b/res/layout/page_indicator_marker.xml
index 564a958..357a761 100644
--- a/res/layout/page_indicator_marker.xml
+++ b/res/layout/page_indicator_marker.xml
@@ -13,7 +13,7 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<com.android.launcher3.PageIndicatorMarker
+<com.android.launcher3.pageindicators.PageIndicatorDot
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:launcher="http://schemas.android.com/apk/res-auto"
     android:layout_width="12dp"
@@ -36,4 +36,4 @@
         android:scaleX="0.5"
         android:scaleY="0.5"
         />
-</com.android.launcher3.PageIndicatorMarker>
+</com.android.launcher3.pageindicators.PageIndicatorDot>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 6d8efaa..2eb9b91 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -32,7 +32,7 @@
     </declare-styleable>
 
     <!-- Page Indicator specific attributes. -->
-    <declare-styleable name="PageIndicator">
+    <declare-styleable name="PageIndicatorDots">
         <attr name="windowSize" format="integer"  />
     </declare-styleable>
 
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 9ab5611..8d11aaa 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -537,7 +537,6 @@
                 lp = (FrameLayout.LayoutParams) pageIndicator.getLayoutParams();
                 lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
                 lp.width = LayoutParams.WRAP_CONTENT;
-                lp.height = LayoutParams.WRAP_CONTENT;
                 lp.bottomMargin = hotseatBarHeightPx;
                 pageIndicator.setLayoutParams(lp);
             }
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 3ce07e3..03b921b 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -112,6 +112,8 @@
 import com.android.launcher3.logging.LoggerUtils;
 import com.android.launcher3.logging.UserEventDispatcher;
 import com.android.launcher3.model.WidgetsModel;
+import com.android.launcher3.pageindicators.PageIndicator;
+import com.android.launcher3.pageindicators.PageIndicatorLine;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.logging.FileLog;
@@ -224,7 +226,7 @@
 
     @Thunk Workspace mWorkspace;
     private View mLauncherView;
-    private View mPageIndicators;
+    private PageIndicatorLine mPageIndicator;
     @Thunk DragLayer mDragLayer;
     private DragController mDragController;
 
@@ -501,6 +503,7 @@
         if (mExtractedColors != null && Utilities.isNycOrAbove()) {
             mExtractedColors.load(this);
             mHotseat.updateColor(mExtractedColors, !mPaused);
+            mPageIndicator.updateColor(mExtractedColors);
         }
     }
 
@@ -1327,7 +1330,7 @@
         mFocusHandler = (FocusIndicatorView) findViewById(R.id.focus_indicator);
         mDragLayer = (DragLayer) findViewById(R.id.drag_layer);
         mWorkspace = (Workspace) mDragLayer.findViewById(R.id.workspace);
-        mPageIndicators = mDragLayer.findViewById(R.id.page_indicator);
+        mPageIndicator = (PageIndicatorLine) mDragLayer.findViewById(R.id.page_indicator);
 
         mLauncherView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
@@ -4526,7 +4529,7 @@
     void showWorkspaceSearchAndHotseat() {
         if (mWorkspace != null) mWorkspace.setAlpha(1f);
         if (mHotseat != null) mHotseat.setAlpha(1f);
-        if (mPageIndicators != null) mPageIndicators.setAlpha(1f);
+        if (mPageIndicator != null) mPageIndicator.setAlpha(1f);
         if (mSearchDropTargetBar != null) mSearchDropTargetBar.animateToState(
                 SearchDropTargetBar.State.SEARCH_BAR, 0);
     }
@@ -4534,7 +4537,7 @@
     void hideWorkspaceSearchAndHotseat() {
         if (mWorkspace != null) mWorkspace.setAlpha(0f);
         if (mHotseat != null) mHotseat.setAlpha(0f);
-        if (mPageIndicators != null) mPageIndicators.setAlpha(0f);
+        if (mPageIndicator != null) mPageIndicator.setAlpha(0f);
         if (mSearchDropTargetBar != null) mSearchDropTargetBar.animateToState(
                 SearchDropTargetBar.State.INVISIBLE, 0);
     }
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index e1cb082..02e894b 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -49,8 +49,11 @@
 import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.view.animation.Interpolator;
+
+import com.android.launcher3.pageindicators.PageIndicator;
 import com.android.launcher3.util.LauncherEdgeEffect;
 import com.android.launcher3.util.Thunk;
+
 import java.util.ArrayList;
 
 /**
@@ -254,8 +257,7 @@
             mPageIndicator = (PageIndicator) grandParent.findViewById(mPageIndicatorViewId);
             mPageIndicator.removeAllMarkers(true);
 
-            ArrayList<PageIndicator.PageMarkerResources> markers =
-                    new ArrayList<PageIndicator.PageMarkerResources>();
+            ArrayList<PageIndicator.PageMarkerResources> markers = new ArrayList<>();
             for (int i = 0; i < getChildCount(); ++i) {
                 markers.add(getPageIndicatorMarker(i));
             }
@@ -264,9 +266,9 @@
 
             OnClickListener listener = getPageIndicatorClickListener();
             if (listener != null) {
-                mPageIndicator.setOnClickListener(listener);
+                mPageIndicator.getView().setOnClickListener(listener);
             }
-            mPageIndicator.setContentDescription(getPageIndicatorDescription());
+            mPageIndicator.getView().setContentDescription(getPageIndicatorDescription());
         }
     }
 
@@ -355,7 +357,8 @@
         return mPageIndicator;
     }
     protected PageIndicator.PageMarkerResources getPageIndicatorMarker(int pageIndex) {
-        return new PageIndicator.PageMarkerResources();
+        return new PageIndicator.PageMarkerResources(R.drawable.ic_pageindicator_current,
+                R.drawable.ic_pageindicator_default);
     }
 
     /**
@@ -430,7 +433,7 @@
                     Math.min(newPage, mTempVisiblePagesRange[1]));
         }
         // Ensure that it is clamped by the actual set of children in all cases
-        validatedPage = Utilities.boundInRange(validatedPage, 0, getPageCount() - 1);
+        validatedPage = Utilities.boundToRange(validatedPage, 0, getPageCount() - 1);
         return validatedPage;
     }
 
@@ -475,7 +478,7 @@
     private void updatePageIndicator() {
         // Update the page indicator (when we aren't reordering)
         if (mPageIndicator != null) {
-            mPageIndicator.setContentDescription(getPageIndicatorDescription());
+            mPageIndicator.getView().setContentDescription(getPageIndicatorDescription());
             if (!isReordering(false)) {
                 mPageIndicator.setActiveMarker(getNextPage());
             }
@@ -931,12 +934,16 @@
     }
 
     @Thunk void updateMaxScrollX() {
+        mMaxScrollX = computeMaxScrollX();
+    }
+
+    protected int computeMaxScrollX() {
         int childCount = getChildCount();
         if (childCount > 0) {
             final int index = mIsRtl ? 0 : childCount - 1;
-            mMaxScrollX = getScrollForPage(index);
+            return getScrollForPage(index);
         } else {
-            mMaxScrollX = 0;
+            return 0;
         }
     }
 
diff --git a/src/com/android/launcher3/PinchAnimationManager.java b/src/com/android/launcher3/PinchAnimationManager.java
index c8c8fa4..477b92c 100644
--- a/src/com/android/launcher3/PinchAnimationManager.java
+++ b/src/com/android/launcher3/PinchAnimationManager.java
@@ -194,7 +194,7 @@
         animateShowHideView(INDEX_HOTSEAT, mLauncher.getHotseat(), show);
         if (mWorkspace.getPageIndicator() != null) {
             // There aren't page indicators in landscape mode on phones, hence the null check.
-            animateShowHideView(INDEX_PAGE_INDICATOR, mWorkspace.getPageIndicator(), show);
+            animateShowHideView(INDEX_PAGE_INDICATOR, mWorkspace.getPageIndicator().getView(), show);
         }
     }
 
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 53522fb..e3b959b 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -798,7 +798,14 @@
      * If value is less than lowerBound, return lowerBound; else if value is greater than upperBound,
      * return upperBound; else return value unchanged.
      */
-    public static int boundInRange(int value, int lowerBound, int upperBound) {
+    public static int boundToRange(int value, int lowerBound, int upperBound) {
+        return Math.max(lowerBound, Math.min(value, upperBound));
+    }
+
+    /**
+     * @see #boundToRange(int, int, int).
+     */
+    public static float boundToRange(float value, float lowerBound, float upperBound) {
         return Math.max(lowerBound, Math.min(value, upperBound));
     }
 
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index e1f0faf..56b83bb 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -73,6 +73,7 @@
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.logging.UserEventDispatcher;
+import com.android.launcher3.pageindicators.PageIndicator;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
 import com.android.launcher3.util.LongArrayMap;
@@ -1273,6 +1274,22 @@
     }
 
     @Override
+    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
+        super.onScrollChanged(l, t, oldl, oldt);
+
+        // Update the page indicator progress.
+        boolean isTransitioning = mIsSwitchingState
+                || (getLayoutTransition() != null && getLayoutTransition().isRunning());
+        if (mPageIndicator != null && !isTransitioning) {
+            showPageIndicatorAtCurrentScroll();
+        }
+    }
+
+    private void showPageIndicatorAtCurrentScroll() {
+        mPageIndicator.setProgress((float) getScrollX() / computeMaxScrollX());
+    }
+
+    @Override
     protected void overScroll(float amount) {
         boolean shouldOverScroll = (amount <= 0 && (!hasCustomContent() || mIsRtl)) ||
                 (amount >= 0 && (!hasCustomContent() || !mIsRtl));
@@ -1325,7 +1342,7 @@
         // different effects based on device performance. On at least one relatively high-end
         // device I've tried, translating the launcher causes things to get quite laggy.
         setTranslationAndAlpha(mLauncher.getSearchDropTargetBar(), transX, alpha);
-        setTranslationAndAlpha(getPageIndicator(), transX, alpha);
+        setTranslationAndAlpha(getPageIndicator().getView(), transX, alpha);
         setTranslationAndAlpha(getChildAt(getCurrentPage()), transX, alpha);
         setTranslationAndAlpha(mLauncher.getHotseat(), transX, alpha);
 
@@ -1538,7 +1555,7 @@
         }
 
         if (getPageIndicator() != null) {
-            getPageIndicator().setTranslationX(translationX);
+            getPageIndicator().getView().setTranslationX(translationX);
         }
 
         if (mCustomContentCallbacks != null) {
@@ -1587,8 +1604,10 @@
             // attach to window
             OnClickListener listener = getPageIndicatorClickListener();
             if (listener != null) {
-                getPageIndicator().setOnClickListener(listener);
+                getPageIndicator().getView().setOnClickListener(listener);
             }
+
+            showPageIndicatorAtCurrentScroll();
         }
 
         // Update wallpaper dimensions if they were changed since last onResume
@@ -1739,8 +1758,8 @@
         super.getVisiblePages(range);
         if (mForceDrawAdjacentPages) {
             // In overview mode, make sure that the two side pages are visible.
-            range[0] = Utilities.boundInRange(getCurrentPage() - 1, numCustomPages(), range[1]);
-            range[1] = Utilities.boundInRange(getCurrentPage() + 1, range[0], getPageCount() - 1);
+            range[0] = Utilities.boundToRange(getCurrentPage() - 1, numCustomPages(), range[1]);
+            range[1] = Utilities.boundToRange(getCurrentPage() + 1, range[0], getPageCount() - 1);
         }
     }
 
@@ -2006,6 +2025,9 @@
         updateChildrenLayersEnabled(false);
         showCustomContentIfNecessary();
         mForceDrawAdjacentPages = false;
+        if (mState == State.NORMAL || mState == State.SPRING_LOADED) {
+            showPageIndicatorAtCurrentScroll();
+        }
     }
 
     void updateCustomContentVisibility() {
diff --git a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
index c0eb7ed..60070a8 100644
--- a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
+++ b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
@@ -288,7 +288,7 @@
         float finalBackgroundAlpha = (states.stateIsSpringLoaded || states.stateIsOverview) ?
                 1.0f : 0f;
         float finalHotseatAlpha = (states.stateIsNormal || states.stateIsSpringLoaded) ? 1f : 0f;
-        float finalPageIndicatorAlpha = states.stateIsNormal ? 1f : 0f;
+        float finalPageIndicatorAlpha = finalHotseatAlpha;
         float finalOverviewPanelAlpha = states.stateIsOverview ? 1f : 0f;
 
         float finalWorkspaceTranslationY = 0;
@@ -357,7 +357,7 @@
 
         final ViewGroup overviewPanel = mLauncher.getOverviewPanel();
         final View hotseat = mLauncher.getHotseat();
-        final View pageIndicator = mWorkspace.getPageIndicator();
+        final View pageIndicator = mWorkspace.getPageIndicator().getView();
         if (animated) {
             LauncherViewPropertyAnimator scale = new LauncherViewPropertyAnimator(mWorkspace);
             scale.scaleX(mNewScale)
diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java
index 1af1485..e1a1431 100644
--- a/src/com/android/launcher3/folder/FolderPagedView.java
+++ b/src/com/android/launcher3/folder/FolderPagedView.java
@@ -39,8 +39,8 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
-import com.android.launcher3.PageIndicator;
-import com.android.launcher3.PageIndicator.PageMarkerResources;
+import com.android.launcher3.pageindicators.PageIndicatorDots;
+import com.android.launcher3.pageindicators.PageIndicator.PageMarkerResources;
 import com.android.launcher3.PagedView;
 import com.android.launcher3.R;
 import com.android.launcher3.ShortcutAndWidgetContainer;
@@ -103,7 +103,7 @@
     private FocusIndicatorView mFocusIndicatorView;
     private PagedFolderKeyEventListener mKeyListener;
 
-    private PageIndicator mPageIndicator;
+    private PageIndicatorDots mPageIndicator;
 
     public FolderPagedView(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -128,7 +128,7 @@
         mFolder = folder;
         mFocusIndicatorView = (FocusIndicatorView) folder.findViewById(R.id.focus_indicator);
         mKeyListener = new PagedFolderKeyEventListener(folder);
-        mPageIndicator = (PageIndicator) folder.findViewById(R.id.folder_page_indicator);
+        mPageIndicator = (PageIndicatorDots) folder.findViewById(R.id.folder_page_indicator);
     }
 
     /**
diff --git a/src/com/android/launcher3/pageindicators/PageIndicator.java b/src/com/android/launcher3/pageindicators/PageIndicator.java
new file mode 100644
index 0000000..6348b12
--- /dev/null
+++ b/src/com/android/launcher3/pageindicators/PageIndicator.java
@@ -0,0 +1,31 @@
+package com.android.launcher3.pageindicators;
+
+import android.view.View;
+
+import java.util.ArrayList;
+
+public interface PageIndicator {
+    View getView();
+    void setProgress(float progress);
+
+    void removeAllMarkers(boolean allowAnimations);
+    void addMarkers(ArrayList<PageMarkerResources> markers, boolean allowAnimations);
+    void setActiveMarker(int activePage);
+    void addMarker(int pageIndex, PageMarkerResources pageIndicatorMarker, boolean allowAnimations);
+    void removeMarker(int pageIndex, boolean allowAnimations);
+    void updateMarker(int pageIndex, PageMarkerResources pageIndicatorMarker);
+
+    /**
+     * Contains two resource ids for each page indicator marker (e.g. dots):
+     * one for when the page is active and one for when the page is inactive.
+     */
+    class PageMarkerResources {
+        int activeId;
+        int inactiveId;
+
+        public PageMarkerResources(int aId, int iaId) {
+            activeId = aId;
+            inactiveId = iaId;
+        }
+    }
+}
diff --git a/src/com/android/launcher3/PageIndicatorMarker.java b/src/com/android/launcher3/pageindicators/PageIndicatorDot.java
similarity index 89%
rename from src/com/android/launcher3/PageIndicatorMarker.java
rename to src/com/android/launcher3/pageindicators/PageIndicatorDot.java
index 7bf21dd..5ed3426 100644
--- a/src/com/android/launcher3/PageIndicatorMarker.java
+++ b/src/com/android/launcher3/pageindicators/PageIndicatorDot.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.launcher3;
+package com.android.launcher3.pageindicators;
 
 import android.content.Context;
 import android.content.res.Resources;
@@ -22,7 +22,9 @@
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 
-public class PageIndicatorMarker extends FrameLayout {
+import com.android.launcher3.R;
+
+public class PageIndicatorDot extends FrameLayout {
     @SuppressWarnings("unused")
     private static final String TAG = "PageIndicator";
 
@@ -32,15 +34,15 @@
     private ImageView mInactiveMarker;
     private boolean mIsActive = false;
 
-    public PageIndicatorMarker(Context context) {
+    public PageIndicatorDot(Context context) {
         this(context, null);
     }
 
-    public PageIndicatorMarker(Context context, AttributeSet attrs) {
+    public PageIndicatorDot(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
     }
 
-    public PageIndicatorMarker(Context context, AttributeSet attrs, int defStyle) {
+    public PageIndicatorDot(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
     }
 
diff --git a/src/com/android/launcher3/PageIndicator.java b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
similarity index 77%
rename from src/com/android/launcher3/PageIndicator.java
rename to src/com/android/launcher3/pageindicators/PageIndicatorDots.java
index 8adbf8d..a488f02 100644
--- a/src/com/android/launcher3/PageIndicator.java
+++ b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
@@ -14,19 +14,22 @@
  * limitations under the License.
  */
 
-package com.android.launcher3;
+package com.android.launcher3.pageindicators;
 
 import android.animation.LayoutTransition;
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
+import android.view.View;
 import android.view.ViewDebug;
 import android.widget.LinearLayout;
 
+import com.android.launcher3.R;
+
 import java.util.ArrayList;
 
-public class PageIndicator extends LinearLayout {
+public class PageIndicatorDots extends LinearLayout implements PageIndicator {
     @SuppressWarnings("unused")
     private static final String TAG = "PageIndicator";
     // Want this to look good? Keep it odd
@@ -36,38 +39,23 @@
     private int[] mWindowRange = new int[2];
     private int mMaxWindowSize;
 
-    private ArrayList<PageIndicatorMarker> mMarkers =
-            new ArrayList<PageIndicatorMarker>();
+    private ArrayList<PageIndicatorDot> mMarkers = new ArrayList<>();
     @ViewDebug.ExportedProperty(category = "launcher")
     private int mActiveMarkerIndex;
 
-    public static class PageMarkerResources {
-        int activeId;
-        int inactiveId;
-
-        public PageMarkerResources() {
-            activeId = R.drawable.ic_pageindicator_current;
-            inactiveId = R.drawable.ic_pageindicator_default;
-        }
-        public PageMarkerResources(int aId, int iaId) {
-            activeId = aId;
-            inactiveId = iaId;
-        }
-    }
-
-    public PageIndicator(Context context) {
+    public PageIndicatorDots(Context context) {
         this(context, null);
     }
 
-    public PageIndicator(Context context, AttributeSet attrs) {
+    public PageIndicatorDots(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
     }
 
-    public PageIndicator(Context context, AttributeSet attrs, int defStyle) {
+    public PageIndicatorDots(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
         TypedArray a = context.obtainStyledAttributes(attrs,
-                R.styleable.PageIndicator, defStyle, 0);
-        mMaxWindowSize = a.getInteger(R.styleable.PageIndicator_windowSize, 15);
+                R.styleable.PageIndicatorDots, defStyle, 0);
+        mMaxWindowSize = a.getInteger(R.styleable.PageIndicatorDots_windowSize, 15);
         mWindowRange[0] = 0;
         mWindowRange[1] = 0;
         mLayoutInflater = LayoutInflater.from(context);
@@ -94,7 +82,7 @@
         transition.disableTransitionType(LayoutTransition.CHANGE_DISAPPEARING);
     }
 
-    void offsetWindowCenterTo(int activeIndex, boolean allowAnimations) {
+    public void offsetWindowCenterTo(int activeIndex, boolean allowAnimations) {
         if (activeIndex < 0) {
             new Throwable().printStackTrace();
         }
@@ -116,7 +104,7 @@
 
         // Remove all the previous children that are no longer in the window
         for (int i = getChildCount() - 1; i >= 0; --i) {
-            PageIndicatorMarker marker = (PageIndicatorMarker) getChildAt(i);
+            PageIndicatorDot marker = (PageIndicatorDot) getChildAt(i);
             int markerIndex = mMarkers.indexOf(marker);
             if (markerIndex < windowStart || markerIndex >= windowEnd) {
                 removeView(marker);
@@ -125,7 +113,7 @@
 
         // Add all the new children that belong in the window
         for (int i = 0; i < mMarkers.size(); ++i) {
-            PageIndicatorMarker marker = (PageIndicatorMarker) mMarkers.get(i);
+            PageIndicatorDot marker = (PageIndicatorDot) mMarkers.get(i);
             if (windowStart <= i && i < windowEnd) {
                 if (indexOfChild(marker) < 0) {
                     addView(marker, i - windowStart);
@@ -161,58 +149,75 @@
         mWindowRange[1] = windowEnd;
     }
 
-    void addMarker(int index, PageMarkerResources marker, boolean allowAnimations) {
+    @Override
+    public void addMarker(int index, PageMarkerResources marker, boolean allowAnimations) {
         index = Math.max(0, Math.min(index, mMarkers.size()));
 
-        PageIndicatorMarker m =
-            (PageIndicatorMarker) mLayoutInflater.inflate(R.layout.page_indicator_marker,
+        PageIndicatorDot m =
+            (PageIndicatorDot) mLayoutInflater.inflate(R.layout.page_indicator_marker,
                     this, false);
         m.setMarkerDrawables(marker.activeId, marker.inactiveId);
 
         mMarkers.add(index, m);
         offsetWindowCenterTo(mActiveMarkerIndex, allowAnimations);
     }
-    void addMarkers(ArrayList<PageMarkerResources> markers, boolean allowAnimations) {
+
+    @Override
+    public void addMarkers(ArrayList<PageMarkerResources> markers, boolean allowAnimations) {
         for (int i = 0; i < markers.size(); ++i) {
             addMarker(Integer.MAX_VALUE, markers.get(i), allowAnimations);
         }
     }
 
-    void updateMarker(int index, PageMarkerResources marker) {
-        PageIndicatorMarker m = mMarkers.get(index);
+    @Override
+    public void updateMarker(int index, PageMarkerResources marker) {
+        PageIndicatorDot m = mMarkers.get(index);
         m.setMarkerDrawables(marker.activeId, marker.inactiveId);
     }
 
-    void removeMarker(int index, boolean allowAnimations) {
+    @Override
+    public void removeMarker(int index, boolean allowAnimations) {
         if (mMarkers.size() > 0) {
             index = Math.max(0, Math.min(mMarkers.size() - 1, index));
             mMarkers.remove(index);
             offsetWindowCenterTo(mActiveMarkerIndex, allowAnimations);
         }
     }
-    void removeAllMarkers(boolean allowAnimations) {
+
+    @Override
+    public View getView() {
+        return this;
+    }
+
+    @Override
+    public void setProgress(float progress) {
+    }
+
+    @Override
+    public void removeAllMarkers(boolean allowAnimations) {
         while (mMarkers.size() > 0) {
             removeMarker(Integer.MAX_VALUE, allowAnimations);
         }
     }
 
-    void setActiveMarker(int index) {
+    @Override
+    public void setActiveMarker(int index) {
         // Center the active marker
         mActiveMarkerIndex = index;
         offsetWindowCenterTo(index, false);
     }
 
-    void dumpState(String txt) {
+    private void dumpState(String txt) {
         System.out.println(txt);
         System.out.println("\tmMarkers: " + mMarkers.size());
         for (int i = 0; i < mMarkers.size(); ++i) {
-            PageIndicatorMarker m = mMarkers.get(i);
+            PageIndicatorDot m = mMarkers.get(i);
             System.out.println("\t\t(" + i + ") " + m);
         }
         System.out.println("\twindow: [" + mWindowRange[0] + ", " + mWindowRange[1] + "]");
         System.out.println("\tchildren: " + getChildCount());
         for (int i = 0; i < getChildCount(); ++i) {
-            PageIndicatorMarker m = (PageIndicatorMarker) getChildAt(i);
+            PageIndicatorDot m = (PageIndicatorDot) getChildAt(i);
             System.out.println("\t\t(" + i + ") " + m);
         }
         System.out.println("\tactive: " + mActiveMarkerIndex);
diff --git a/src/com/android/launcher3/pageindicators/PageIndicatorLine.java b/src/com/android/launcher3/pageindicators/PageIndicatorLine.java
new file mode 100644
index 0000000..449bf06
--- /dev/null
+++ b/src/com/android/launcher3/pageindicators/PageIndicatorLine.java
@@ -0,0 +1,187 @@
+package com.android.launcher3.pageindicators;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.v4.graphics.ColorUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Property;
+import android.view.View;
+import android.view.ViewConfiguration;
+
+import com.android.launcher3.Utilities;
+import com.android.launcher3.dynamicui.ExtractedColors;
+
+import java.util.ArrayList;
+
+/**
+ * A PageIndicator that briefly shows a fraction of a line when moving between pages.
+ *
+ * The fraction is 1 / number of pages and the position is based on the progress of the page scroll.
+ */
+public class PageIndicatorLine extends View implements PageIndicator {
+    private static final String TAG = "PageIndicatorLine";
+
+    private static final int LINE_FADE_DURATION = ViewConfiguration.getScrollBarFadeDuration();
+    private static final int LINE_FADE_DELAY = ViewConfiguration.getScrollDefaultDelay();
+    public static final int WHITE_ALPHA = (int) (0.70f * 255);
+    public static final int BLACK_ALPHA = (int) (0.65f * 255);
+
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
+
+    private ValueAnimator mLineAlphaAnimator;
+    private int mAlpha = 0;
+    private float mProgress = 0f;
+    private int mNumPages = 1;
+    private Paint mLinePaint;
+
+    private Property<Paint, Integer> mPaintAlphaProperty
+            = new Property<Paint, Integer>(Integer.class, "paint_alpha") {
+        @Override
+        public Integer get(Paint paint) {
+            return paint.getAlpha();
+        }
+
+        @Override
+        public void set(Paint paint, Integer alpha) {
+            paint.setAlpha(alpha);
+            invalidate();
+        }
+    };
+
+    private Runnable mHideLineRunnable = new Runnable() {
+        @Override
+        public void run() {
+            animateLineToAlpha(0);
+        }
+    };
+
+    public PageIndicatorLine(Context context) {
+        this(context, null);
+    }
+
+    public PageIndicatorLine(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public PageIndicatorLine(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        mLinePaint = new Paint();
+        mLinePaint.setAlpha(0);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+        if (mNumPages == 0) {
+            return;
+        }
+
+        int availableWidth = canvas.getWidth();
+        int lineWidth = availableWidth / mNumPages;
+        int lineLeft = (int) (mProgress * (availableWidth - lineWidth));
+        int lineRight = lineLeft + lineWidth;
+        canvas.drawRect(lineLeft, 0, lineRight, canvas.getHeight(), mLinePaint);
+    }
+
+    @Override
+    public View getView() {
+        return this;
+    }
+
+    @Override
+    public void setProgress(float progress) {
+        if (getAlpha() == 0) {
+            return;
+        }
+        progress = Utilities.boundToRange(progress, 0f, 1f);
+        animateLineToAlpha(mAlpha);
+        mProgress = progress;
+        invalidate();
+
+        // Hide after a brief period.
+        mHandler.removeCallbacksAndMessages(null);
+        mHandler.postDelayed(mHideLineRunnable, LINE_FADE_DELAY);
+    }
+
+    @Override
+    public void removeAllMarkers(boolean allowAnimations) {
+        mNumPages = 0;
+    }
+
+    @Override
+    public void addMarkers(ArrayList<PageMarkerResources> markers, boolean allowAnimations) {
+        mNumPages += markers.size();
+    }
+
+    @Override
+    public void setActiveMarker(int activePage) {
+    }
+
+    @Override
+    public void addMarker(int pageIndex, PageMarkerResources pageIndicatorMarker,
+            boolean allowAnimations) {
+        mNumPages++;
+    }
+
+    @Override
+    public void removeMarker(int pageIndex, boolean allowAnimations) {
+        mNumPages--;
+    }
+
+    @Override
+    public void updateMarker(int pageIndex, PageMarkerResources pageIndicatorMarker) {
+    }
+
+    /**
+     * The line's color will be:
+     * - mostly opaque white if the hotseat is white (ignoring alpha)
+     * - mostly opaque black if the hotseat is black (ignoring alpha)
+     */
+    public void updateColor(ExtractedColors extractedColors) {
+        int originalLineAlpha = mLinePaint.getAlpha();
+        int color = extractedColors.getColor(ExtractedColors.HOTSEAT_INDEX, Color.TRANSPARENT);
+        if (color != Color.TRANSPARENT) {
+            color = ColorUtils.setAlphaComponent(color, 255);
+            if (color == Color.BLACK) {
+                mAlpha = BLACK_ALPHA;
+            } else if (color == Color.WHITE) {
+                mAlpha = WHITE_ALPHA;
+            } else {
+                Log.e(TAG, "Setting workspace page indicators to an unsupported color: #"
+                        + Integer.toHexString(color));
+            }
+            mLinePaint.setColor(color);
+            mLinePaint.setAlpha(originalLineAlpha);
+        }
+    }
+
+    private void animateLineToAlpha(int alpha) {
+        if (mLineAlphaAnimator != null) {
+            // An animation is already running, so ignore the new animation request unless we are
+            // trying to hide the line, in which case we always allow the animation.
+            if (alpha != 0) {
+                return;
+            }
+            mLineAlphaAnimator.cancel();
+        }
+        mLineAlphaAnimator = ObjectAnimator.ofInt(mLinePaint, mPaintAlphaProperty, alpha);
+        mLineAlphaAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mLineAlphaAnimator = null;
+            }
+        });
+        mLineAlphaAnimator.setDuration(LINE_FADE_DURATION);
+        mLineAlphaAnimator.start();
+    }
+}