OSDN Git Service

Refactoring fast scroller.
authorWinson Chung <winsonc@google.com>
Tue, 16 Jun 2015 20:35:04 +0000 (13:35 -0700)
committerWinson Chung <winsonc@google.com>
Tue, 23 Jun 2015 21:24:15 +0000 (14:24 -0700)
- Fixing issue with fast scroller not fitting name width.
- Refactoring fast scrolling/scroll bar code out of base recycler view
- Adding animations to fast scroller to match design
- Smooth scrolling when jumping between app rows
- Fixing issue with fast scroller jumping when you first pick it up
- Fixing issue with wrong background paddings being used

Bug: 21874346
Bug: 22031923
Change-Id: I9f011b1f375751f437604b900e95a2942d3f4601

23 files changed:
proguard.flags
res/drawable-ldrtl/all_apps_fastscroll_bg.xml
res/drawable/all_apps_scrollbar_thumb.xml [deleted file]
res/drawable/container_fastscroll_popup_bg.xml [moved from res/drawable/all_apps_fastscroll_bg.xml with 92% similarity]
res/layout/all_apps_container.xml
res/layout/all_apps_search_bar.xml
res/values-sw720dp/dimens.xml
res/values/colors.xml
res/values/dimens.xml
src/com/android/launcher3/BaseRecyclerView.java
src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java [new file with mode: 0644]
src/com/android/launcher3/BaseRecyclerViewFastScrollPopup.java [new file with mode: 0644]
src/com/android/launcher3/BubbleTextView.java
src/com/android/launcher3/Launcher.java
src/com/android/launcher3/LauncherAppState.java
src/com/android/launcher3/LauncherCallbacks.java
src/com/android/launcher3/Utilities.java
src/com/android/launcher3/allapps/AllAppsContainerView.java
src/com/android/launcher3/allapps/AllAppsGridAdapter.java
src/com/android/launcher3/allapps/AllAppsRecyclerView.java
src/com/android/launcher3/allapps/AlphabeticalAppsList.java
src/com/android/launcher3/widget/WidgetsContainerView.java
src/com/android/launcher3/widget/WidgetsRecyclerView.java

index 7ec488b..6a9d6f3 100644 (file)
@@ -1,9 +1,30 @@
+-keep class com.android.launcher3.BaseRecyclerViewFastScrollBar {
+  public void setWidth(int);
+  public int getWidth();
+  public void setTrackAlpha(int);
+  public int getTrackAlpha();
+}
+
+-keep class com.android.launcher3.BaseRecyclerViewFastScrollPopup {
+  public void setAlpha(float);
+  public float getAlpha();
+}
+
+-keep class com.android.launcher3.BubbleTextView {
+  public void setFastScrollFocus(float);
+  public float getFastScrollFocus();
+}
+
+-keep class com.android.launcher3.ButtonDropTarget {
+  public int getTextColor();
+}
+
 -keep class com.android.launcher3.CellLayout {
   public float getBackgroundAlpha();
   public void setBackgroundAlpha(float);
 }
 
--keep class com.android.launcher3.DragLayer$LayoutParams {
+-keep class com.android.launcher3.CellLayout$LayoutParams {
   public void setWidth(int);
   public int getWidth();
   public void setHeight(int);
@@ -14,7 +35,7 @@
   public int getY();
 }
 
--keep class com.android.launcher3.CellLayout$LayoutParams {
+-keep class com.android.launcher3.DragLayer$LayoutParams {
   public void setWidth(int);
   public int getWidth();
   public void setHeight(int);
@@ -25,9 +46,9 @@
   public int getY();
 }
 
--keep class com.android.launcher3.Workspace {
-  public float getBackgroundAlpha();
-  public void setBackgroundAlpha(float);
+-keep class com.android.launcher3.FastBitmapDrawable {
+  public int getBrightness();
+  public void setBrightness(int);
 }
 
 -keep class com.android.launcher3.MemoryDumpActivity {
   public void setAnimationProgress(float);
 }
 
--keep class com.android.launcher3.FastBitmapDrawable {
-  public int getBrightness();
-  public void setBrightness(int);
-}
-
--keep class com.android.launcher3.BaseRecyclerView {
-  public void setFastScrollerAlpha(float);
-  public float getFastScrollerAlpha();
-}
-
--keep class com.android.launcher3.ButtonDropTarget {
-  public int getTextColor();
-}
+-keep class com.android.launcher3.Workspace {
+  public float getBackgroundAlpha();
+  public void setBackgroundAlpha(float);
+}
\ No newline at end of file
index 4777f70..d790968 100644 (file)
@@ -16,7 +16,7 @@
 -->
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle">
-    <solid android:color="@color/all_apps_scrollbar_thumb_color" />
+    <solid android:color="@color/container_fastscroll_thumb_active_color" />
     <size
         android:width="64dp"
         android:height="64dp" />
diff --git a/res/drawable/all_apps_scrollbar_thumb.xml b/res/drawable/all_apps_scrollbar_thumb.xml
deleted file mode 100644 (file)
index 649a963..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2015 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.
--->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-       android:shape="rectangle">
-    <solid android:color="@color/all_apps_scrollbar_thumb_color" />
-    <size android:width="@dimen/all_apps_fast_scroll_bar_width" />
-</shape>
\ No newline at end of file
similarity index 92%
rename from res/drawable/all_apps_fastscroll_bg.xml
rename to res/drawable/container_fastscroll_popup_bg.xml
index 6b74484..2ef07ab 100644 (file)
@@ -16,7 +16,7 @@
 -->
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle">
-    <solid android:color="@color/all_apps_scrollbar_thumb_color" />
+    <solid android:color="@color/container_fastscroll_thumb_active_color" />
     <size
         android:width="64dp"
         android:height="64dp" />
index 1de8201..0b624e6 100644 (file)
@@ -38,8 +38,8 @@
         android:id="@+id/prediction_bar"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:paddingTop="@dimen/all_apps_prediction_bar_top_bottom_padding"
-        android:paddingBottom="@dimen/all_apps_prediction_bar_top_bottom_padding"
+        android:paddingTop="@dimen/all_apps_prediction_bar_top_padding"
+        android:paddingBottom="@dimen/all_apps_prediction_bar_bottom_padding"
         android:orientation="horizontal"
         android:focusable="true"
         android:descendantFocusability="afterDescendants"
index 8d75b15..cf30eac 100644 (file)
@@ -63,8 +63,8 @@
         android:layout_width="wrap_content"
         android:layout_height="@dimen/all_apps_search_bar_height"
         android:layout_gravity="end|center_vertical"
-        android:layout_marginEnd="6dp"
-        android:layout_marginRight="6dp"
+        android:layout_marginEnd="4dp"
+        android:layout_marginRight="4dp"
         android:contentDescription="@string/all_apps_search_bar_hint"
         android:paddingBottom="13dp"
         android:paddingTop="13dp"
index 9d1e352..d48f9ee 100644 (file)
@@ -17,7 +17,7 @@
 <resources>
 <!-- All Apps -->
     <dimen name="all_apps_search_bar_height">54dp</dimen>
-    <dimen name="all_apps_icon_top_bottom_padding">16dp</dimen>
+    <dimen name="all_apps_icon_top_bottom_padding">14dp</dimen>
 
 <!-- QSB -->
     <dimen name="toolbar_button_vertical_padding">8dip</dimen>
index 7d36101..5afc5b9 100644 (file)
     <color name="outline_color">#FFFFFFFF</color>
     <color name="widget_text_panel">#FF374248</color>
 
+    <!-- Containers -->
+    <color name="container_fastscroll_thumb_inactive_color">#42000000</color>
+    <color name="container_fastscroll_thumb_active_color">#009688</color>
+
     <!-- All Apps -->
-    <color name="all_apps_scrollbar_thumb_color">#009688</color>
     <color name="all_apps_grid_section_text_color">#009688</color>
 
     <!-- Widgets view -->
+    <color name="widgets_view_fastscroll_thumb_inactive_color">#42FFFFFF</color>
     <color name="widgets_view_section_text_color">#FFFFFF</color>
     <color name="widgets_view_item_text_color">#C4C4C4</color>
     <color name="widgets_cell_color">#263238</color>
index da56d90..122b831 100644 (file)
     <dimen name="toolbar_button_vertical_padding">4dip</dimen>
     <dimen name="toolbar_button_horizontal_padding">12dip</dimen>
 
-<!-- All Apps -->
+<!-- Container -->
     <!-- Note: This needs to match the fixed insets for the search box. -->
     <dimen name="container_bounds_inset">8dp</dimen>
     <!-- Notes: container_bounds_inset - quantum_panel_outer_padding -->
     <dimen name="container_bounds_minus_quantum_panel_padding_inset">4dp</dimen>
 
+    <dimen name="container_fastscroll_thumb_min_width">4dp</dimen>
+    <dimen name="container_fastscroll_thumb_max_width">8dp</dimen>
+    <dimen name="container_fastscroll_thumb_height">64dp</dimen>
+    <dimen name="container_fastscroll_thumb_touch_inset">-24dp</dimen>
+    <dimen name="container_fastscroll_popup_size">72dp</dimen>
+    <dimen name="container_fastscroll_popup_text_size">48dp</dimen>
+
+<!-- All Apps -->
     <dimen name="all_apps_grid_view_start_margin">0dp</dimen>
     <dimen name="all_apps_grid_section_y_offset">8dp</dimen>
     <dimen name="all_apps_grid_section_text_size">24sp</dimen>
     <dimen name="all_apps_search_bar_prediction_bar_padding">8dp</dimen>
     <dimen name="all_apps_icon_top_bottom_padding">8dp</dimen>
     <dimen name="all_apps_icon_width_gap">24dp</dimen>
-    <dimen name="all_apps_prediction_bar_top_bottom_padding">16dp</dimen>
-
-    <dimen name="all_apps_fast_scroll_bar_width">4dp</dimen>
-    <dimen name="all_apps_fast_scroll_scrubber_touch_inset">-24dp</dimen>
-    <dimen name="all_apps_fast_scroll_popup_size">72dp</dimen>
-    <dimen name="all_apps_fast_scroll_text_size">48dp</dimen>
-
-    <dimen name="all_apps_header_max_elevation">4dp</dimen>
-    <dimen name="all_apps_header_scroll_to_elevation">16dp</dimen>
-    <dimen name="all_apps_header_shadow_height">6dp</dimen>
+    <!-- The top padding should account for the general all_apps_list_top_bottom_padding -->
+    <dimen name="all_apps_prediction_bar_top_padding">0dp</dimen>
+    <dimen name="all_apps_prediction_bar_bottom_padding">16dp</dimen>
+    <dimen name="all_apps_list_top_bottom_padding">8dp</dimen>
 
 <!-- Widget tray -->
     <dimen name="widget_container_inset">8dp</dimen>
index 140c28c..0fae427 100644 (file)
 
 package com.android.launcher3;
 
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ObjectAnimator;
 import android.content.Context;
-import android.content.res.Resources;
 import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
 import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
 import android.support.v7.widget.RecyclerView;
 import android.util.AttributeSet;
 import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewConfiguration;
-
 import com.android.launcher3.util.Thunk;
 
+
 /**
  * A base {@link RecyclerView}, which does the following:
  * <ul>
@@ -41,7 +32,7 @@ import com.android.launcher3.util.Thunk;
  *   <li> Enable fast scroller.
  * </ul>
  */
-public class BaseRecyclerView extends RecyclerView
+public abstract class BaseRecyclerView extends RecyclerView
         implements RecyclerView.OnItemTouchListener {
 
     private static final int SCROLL_DELTA_THRESHOLD_DP = 4;
@@ -50,14 +41,8 @@ public class BaseRecyclerView extends RecyclerView
     @Thunk int mDy = 0;
     private float mDeltaThreshold;
 
-    //
-    // Keeps track of variables required for the second function of this class: fast scroller.
-    //
-
-    private static final float FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR = 1.5f;
-
     /**
-     * The current scroll state of the recycler view.  We use this in updateVerticalScrollbarBounds()
+     * The current scroll state of the recycler view.  We use this in onUpdateScrollbar()
      * and scrollToPositionAtProgress() to determine the scroll position of the recycler view so
      * that we can calculate what the scroll bar looks like, and where to jump to from the fast
      * scroller.
@@ -70,27 +55,12 @@ public class BaseRecyclerView extends RecyclerView
         // The height of a given row (they are currently all the same height)
         public int rowHeight;
     }
-    // Should be maintained inside overriden method #updateVerticalScrollbarBounds
-    public ScrollPositionState scrollPosState = new ScrollPositionState();
-    public Rect verticalScrollbarBounds = new Rect();
 
-    private boolean mDraggingFastScroller;
-
-    private Drawable mScrollbar;
-    private Drawable mFastScrollerBg;
-    private Rect mTmpFastScrollerInvalidateRect = new Rect();
-    private Rect mFastScrollerBounds = new Rect();
-
-    private String mFastScrollSectionName;
-    private Paint mFastScrollTextPaint;
-    private Rect mFastScrollTextBounds = new Rect();
-    private float mFastScrollAlpha;
+    protected BaseRecyclerViewFastScrollBar mScrollbar;
 
     private int mDownX;
     private int mDownY;
     private int mLastY;
-    private int mScrollbarWidth;
-    private int mScrollbarInset;
     protected Rect mBackgroundPadding = new Rect();
 
     public BaseRecyclerView(Context context) {
@@ -104,25 +74,10 @@ public class BaseRecyclerView extends RecyclerView
     public BaseRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
         mDeltaThreshold = getResources().getDisplayMetrics().density * SCROLL_DELTA_THRESHOLD_DP;
+        mScrollbar = new BaseRecyclerViewFastScrollBar(this, getResources());
 
         ScrollListener listener = new ScrollListener();
         setOnScrollListener(listener);
-
-        Resources res = context.getResources();
-        int fastScrollerSize = res.getDimensionPixelSize(R.dimen.all_apps_fast_scroll_popup_size);
-        mScrollbar = res.getDrawable(R.drawable.all_apps_scrollbar_thumb);
-        mFastScrollerBg = res.getDrawable(R.drawable.all_apps_fastscroll_bg);
-        mFastScrollerBg.setBounds(0, 0, fastScrollerSize, fastScrollerSize);
-        mFastScrollTextPaint = new Paint();
-        mFastScrollTextPaint.setColor(Color.WHITE);
-        mFastScrollTextPaint.setAntiAlias(true);
-        mFastScrollTextPaint.setTextSize(res.getDimensionPixelSize(
-                R.dimen.all_apps_fast_scroll_text_size));
-        mScrollbarWidth = res.getDimensionPixelSize(R.dimen.all_apps_fast_scroll_bar_width);
-        mScrollbarInset =
-                res.getDimensionPixelSize(R.dimen.all_apps_fast_scroll_scrubber_touch_inset);
-        setFastScrollerAlpha(mFastScrollAlpha);
-        setOverScrollMode(View.OVER_SCROLL_NEVER);
     }
 
     private class ScrollListener extends OnScrollListener {
@@ -133,6 +88,10 @@ public class BaseRecyclerView extends RecyclerView
         @Override
         public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
             mDy = dy;
+
+            // TODO(winsonc): If we want to animate the section heads while scrolling, we can
+            //                initiate that here if the recycler view scroll state is not
+            //                RecyclerView.SCROLL_STATE_IDLE.
         }
     }
 
@@ -161,8 +120,6 @@ public class BaseRecyclerView extends RecyclerView
      * it is already showing).
      */
     private boolean handleTouchEvent(MotionEvent ev) {
-        ViewConfiguration config = ViewConfiguration.get(getContext());
-
         int action = ev.getAction();
         int x = (int) ev.getX();
         int y = (int) ev.getY();
@@ -174,41 +131,19 @@ public class BaseRecyclerView extends RecyclerView
                 if (shouldStopScroll(ev)) {
                     stopScroll();
                 }
+                mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY);
                 break;
             case MotionEvent.ACTION_MOVE:
-                // Check if we are scrolling
-                if (!mDraggingFastScroller && isPointNearScrollbar(mDownX, mDownY) &&
-                        Math.abs(y - mDownY) > config.getScaledTouchSlop()) {
-                    getParent().requestDisallowInterceptTouchEvent(true);
-                    mDraggingFastScroller = true;
-                    animateFastScrollerVisibility(true);
-                }
-                if (mDraggingFastScroller) {
-                    mLastY = y;
-
-                    // Scroll to the right position, and update the section name
-                    int top = getPaddingTop() + (mFastScrollerBg.getBounds().height() / 2);
-                    int bottom = getHeight() - getPaddingBottom() -
-                            (mFastScrollerBg.getBounds().height() / 2);
-                    float boundedY = (float) Math.max(top, Math.min(bottom, y));
-                    mFastScrollSectionName = scrollToPositionAtProgress((boundedY - top) /
-                            (bottom - top));
-
-                    // Combine the old and new fast scroller bounds to create the full invalidate
-                    // rect
-                    mTmpFastScrollerInvalidateRect.set(mFastScrollerBounds);
-                    updateFastScrollerBounds();
-                    mTmpFastScrollerInvalidateRect.union(mFastScrollerBounds);
-                    invalidateFastScroller(mTmpFastScrollerInvalidateRect);
-                }
+                mLastY = y;
+                mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY);
                 break;
             case MotionEvent.ACTION_UP:
             case MotionEvent.ACTION_CANCEL:
-                mDraggingFastScroller = false;
-                animateFastScrollerVisibility(false);
+                onFastScrollCompleted();
+                mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY);
                 break;
         }
-        return mDraggingFastScroller;
+        return mScrollbar.isDragging();
     }
 
     public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
@@ -234,159 +169,117 @@ public class BaseRecyclerView extends RecyclerView
         mBackgroundPadding.set(padding);
     }
 
-    @Override
-    protected void dispatchDraw(Canvas canvas) {
-        super.dispatchDraw(canvas);
-        drawVerticalScrubber(canvas);
-        drawFastScrollerPopup(canvas);
+    public Rect getBackgroundPadding() {
+        return mBackgroundPadding;
     }
 
     /**
-     * Draws the vertical scrollbar.
+     * Returns the scroll bar width when the user is scrolling.
      */
-    private void drawVerticalScrubber(Canvas canvas) {
-        updateVerticalScrollbarBounds();
-
-        // Draw the scroll bar
-        int restoreCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
-        canvas.translate(verticalScrollbarBounds.left, verticalScrollbarBounds.top);
-        mScrollbar.setBounds(0, 0, mScrollbarWidth, verticalScrollbarBounds.height());
-        mScrollbar.draw(canvas);
-        canvas.restoreToCount(restoreCount);
+    public int getMaxScrollbarWidth() {
+        return mScrollbar.getThumbMaxWidth();
     }
 
     /**
-     * Draws the fast scroller popup.
+     * Returns the available scroll height:
+     *   AvailableScrollHeight = Total height of the all items - last page height
+     *
+     * This assumes that all rows are the same height.
+     *
+     * @param yOffset the offset from the top of the recycler view to start tracking.
      */
-    private void drawFastScrollerPopup(Canvas canvas) {
-        if (mFastScrollAlpha > 0f && mFastScrollSectionName != null && !mFastScrollSectionName.isEmpty()) {
-            // Draw the fast scroller popup
-            int restoreCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
-            canvas.translate(mFastScrollerBounds.left, mFastScrollerBounds.top);
-            mFastScrollerBg.setAlpha((int) (mFastScrollAlpha * 255));
-            mFastScrollerBg.draw(canvas);
-            mFastScrollTextPaint.setAlpha((int) (mFastScrollAlpha * 255));
-            mFastScrollTextPaint.getTextBounds(mFastScrollSectionName, 0,
-                    mFastScrollSectionName.length(), mFastScrollTextBounds);
-            float textWidth = mFastScrollTextPaint.measureText(mFastScrollSectionName);
-            canvas.drawText(mFastScrollSectionName,
-                    (mFastScrollerBounds.width() - textWidth) / 2,
-                    mFastScrollerBounds.height() -
-                            (mFastScrollerBounds.height() - mFastScrollTextBounds.height()) / 2,
-                    mFastScrollTextPaint);
-            canvas.restoreToCount(restoreCount);
-        }
+    protected int getAvailableScrollHeight(int rowCount, int rowHeight, int yOffset) {
+        int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom;
+        int scrollHeight = getPaddingTop() + yOffset + rowCount * rowHeight + getPaddingBottom();
+        int availableScrollHeight = scrollHeight - visibleHeight;
+        return availableScrollHeight;
     }
 
     /**
-     * Returns the scroll bar width.
+     * Returns the available scroll bar height:
+     *   AvailableScrollBarHeight = Total height of the visible view - thumb height
      */
-    public int getScrollbarWidth() {
-        return mScrollbarWidth;
+    protected int getAvailableScrollBarHeight() {
+        int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom;
+        int availableScrollBarHeight = visibleHeight - mScrollbar.getThumbHeight();
+        return availableScrollBarHeight;
     }
 
     /**
-     * Sets the fast scroller alpha.
+     * Returns the track color (ignoring alpha), can be overridden by each subclass.
      */
-    public void setFastScrollerAlpha(float alpha) {
-        mFastScrollAlpha = alpha;
-        invalidateFastScroller(mFastScrollerBounds);
+    public int getFastScrollerTrackColor(int defaultTrackColor) {
+        return defaultTrackColor;
     }
 
     /**
-     * Returns the fast scroller alpha.
+     * Returns the inactive thumb color, can be overridden by each subclass.
      */
-    public float getFastScrollerAlpha() {
-        return mFastScrollAlpha;
+    public int getFastScrollerThumbInactiveColor(int defaultInactiveThumbColor) {
+        return defaultInactiveThumbColor;
     }
 
-    /**
-     * Maps the touch (from 0..1) to the adapter position that should be visible.
-     * <p>Override in each subclass of this base class.
-     */
-    public String scrollToPositionAtProgress(float touchFraction) {
-        return null;
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        super.dispatchDraw(canvas);
+        onUpdateScrollbar();
+        mScrollbar.draw(canvas);
     }
 
     /**
-     * Updates the bounds for the scrollbar.
-     * <p>Override in each subclass of this base class.
-     */
-    public void updateVerticalScrollbarBounds() {};
-
-    /**
-     * Animates the visibility of the fast scroller popup.
+     * Updates the scrollbar thumb offset to match the visible scroll of the recycler view.  It does
+     * this by mapping the available scroll area of the recycler view to the available space for the
+     * scroll bar.
+     *
+     * @param scrollPosState the current scroll position
+     * @param rowCount the number of rows, used to calculate the total scroll height (assumes that
+     *                 all rows are the same height)
+     * @param yOffset the offset to start tracking in the recycler view (only used for all apps)
      */
-    private void animateFastScrollerVisibility(final boolean visible) {
-        ObjectAnimator anim = ObjectAnimator.ofFloat(this, "fastScrollerAlpha", visible ? 1f : 0f);
-        anim.setDuration(visible ? 200 : 150);
-        anim.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationStart(Animator animation) {
-                if (visible) {
-                    onFastScrollingStart();
-                }
-            }
+    protected void synchronizeScrollBarThumbOffsetToViewScroll(ScrollPositionState scrollPosState,
+            int rowCount, int yOffset) {
+        int availableScrollHeight = getAvailableScrollHeight(rowCount, scrollPosState.rowHeight,
+                yOffset);
+        int availableScrollBarHeight = getAvailableScrollBarHeight();
+
+        // Only show the scrollbar if there is height to be scrolled
+        if (availableScrollHeight <= 0) {
+            mScrollbar.setScrollbarThumbOffset(-1, -1);
+            return;
+        }
 
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                if (!visible) {
-                    onFastScrollingEnd();
-                }
-            }
-        });
-        anim.start();
+        // Calculate the current scroll position, the scrollY of the recycler view accounts for the
+        // view padding, while the scrollBarY is drawn right up to the background padding (ignoring
+        // padding)
+        int scrollY = getPaddingTop() + yOffset +
+                (scrollPosState.rowIndex * scrollPosState.rowHeight) - scrollPosState.rowTopOffset;
+        int scrollBarY = mBackgroundPadding.top +
+                (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight);
+
+        // Calculate the position and size of the scroll bar
+        int scrollBarX;
+        if (Utilities.isRtl(getResources())) {
+            scrollBarX = mBackgroundPadding.left;
+        } else {
+            scrollBarX = getWidth() - mBackgroundPadding.right - mScrollbar.getWidth();
+        }
+        mScrollbar.setScrollbarThumbOffset(scrollBarX, scrollBarY);
     }
 
     /**
-     * To be overridden by subclasses.
-     */
-    protected void onFastScrollingStart() {}
-
-    /**
-     * To be overridden by subclasses.
-     */
-    protected void onFastScrollingEnd() {}
-
-    /**
-     * Invalidates the fast scroller popup.
+     * Maps the touch (from 0..1) to the adapter position that should be visible.
+     * <p>Override in each subclass of this base class.
      */
-    protected void invalidateFastScroller(Rect bounds) {
-        invalidate(bounds.left, bounds.top, bounds.right, bounds.bottom);
-    }
+    public abstract String scrollToPositionAtProgress(float touchFraction);
 
     /**
-     * Returns whether a given point is near the scrollbar.
+     * Updates the bounds for the scrollbar.
+     * <p>Override in each subclass of this base class.
      */
-    private boolean isPointNearScrollbar(int x, int y) {
-        // Check if we are scrolling
-        updateVerticalScrollbarBounds();
-        verticalScrollbarBounds.inset(mScrollbarInset, mScrollbarInset);
-        return verticalScrollbarBounds.contains(x, y);
-    }
+    public abstract void onUpdateScrollbar();
 
     /**
-     * Updates the bounds for the fast scroller.
+     * <p>Override in each subclass of this base class.
      */
-    private void updateFastScrollerBounds() {
-        if (mFastScrollAlpha > 0f && !mFastScrollSectionName.isEmpty()) {
-            int x;
-            int y;
-
-            // Calculate the position for the fast scroller popup
-            Rect bgBounds = mFastScrollerBg.getBounds();
-            if (Utilities.isRtl(getResources())) {
-                x = mBackgroundPadding.left + (2 * getScrollbarWidth());
-            } else {
-                x = getWidth() - mBackgroundPadding.right - (2 * getScrollbarWidth()) -
-                        bgBounds.width();
-            }
-            y = mLastY - (int) (FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR * bgBounds.height());
-            y = Math.max(getPaddingTop(), Math.min(y, getHeight() - getPaddingBottom() -
-                    bgBounds.height()));
-            mFastScrollerBounds.set(x, y, x + bgBounds.width(), y + bgBounds.height());
-        } else {
-            mFastScrollerBounds.setEmpty();
-        }
-    }
+    public void onFastScrollCompleted() {}
 }
\ No newline at end of file
diff --git a/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java b/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java
new file mode 100644 (file)
index 0000000..96e994b
--- /dev/null
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2015 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;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ArgbEvaluator;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+/**
+ * The track and scrollbar that shows when you scroll the list.
+ */
+public class BaseRecyclerViewFastScrollBar {
+
+    public interface FastScrollFocusableView {
+        void setFastScrollFocused(boolean focused, boolean animated);
+    }
+
+    private final static int MAX_TRACK_ALPHA = 30;
+    private final static int SCROLL_BAR_VIS_DURATION = 150;
+
+    private BaseRecyclerView mRv;
+    private BaseRecyclerViewFastScrollPopup mPopup;
+
+    private AnimatorSet mScrollbarAnimator;
+
+    private int mThumbInactiveColor;
+    private int mThumbActiveColor;
+    private Point mThumbOffset = new Point(-1, -1);
+    private Paint mThumbPaint;
+    private Paint mTrackPaint;
+    private int mThumbMinWidth;
+    private int mThumbMaxWidth;
+    private int mThumbWidth;
+    private int mThumbHeight;
+    // The inset is the buffer around which a point will still register as a click on the scrollbar
+    private int mTouchInset;
+    private boolean mIsDragging;
+
+    // This is the offset from the top of the scrollbar when the user first starts touching.  To
+    // prevent jumping, this offset is applied as the user scrolls.
+    private int mTouchOffset;
+
+    private Rect mInvalidateRect = new Rect();
+    private Rect mTmpRect = new Rect();
+
+    public BaseRecyclerViewFastScrollBar(BaseRecyclerView rv, Resources res) {
+        mRv = rv;
+        mPopup = new BaseRecyclerViewFastScrollPopup(rv, res);
+        mTrackPaint = new Paint();
+        mTrackPaint.setColor(rv.getFastScrollerTrackColor(Color.BLACK));
+        mTrackPaint.setAlpha(0);
+        mThumbInactiveColor = rv.getFastScrollerThumbInactiveColor(
+                res.getColor(R.color.container_fastscroll_thumb_inactive_color));
+        mThumbActiveColor = res.getColor(R.color.container_fastscroll_thumb_active_color);
+        mThumbPaint = new Paint();
+        mThumbPaint.setColor(mThumbInactiveColor);
+        mThumbWidth = mThumbMinWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_min_width);
+        mThumbMaxWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_max_width);
+        mThumbHeight = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_height);
+        mTouchInset = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_touch_inset);
+    }
+
+    public void setScrollbarThumbOffset(int x, int y) {
+        if (mThumbOffset.x == x && mThumbOffset.y == y) {
+            return;
+        }
+        mInvalidateRect.set(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth, mRv.getHeight());
+        mThumbOffset.set(x, y);
+        mInvalidateRect.union(new Rect(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth,
+                mRv.getHeight()));
+        mRv.invalidate(mInvalidateRect);
+    }
+
+    // Setter/getter for the search bar width for animations
+    public void setWidth(int width) {
+        mInvalidateRect.set(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth, mRv.getHeight());
+        mThumbWidth = width;
+        mInvalidateRect.union(new Rect(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth,
+                mRv.getHeight()));
+        mRv.invalidate(mInvalidateRect);
+    }
+
+    public int getWidth() {
+        return mThumbWidth;
+    }
+
+    // Setter/getter for the track background alpha for animations
+    public void setTrackAlpha(int alpha) {
+        mTrackPaint.setAlpha(alpha);
+        mInvalidateRect.set(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth, mRv.getHeight());
+        mRv.invalidate(mInvalidateRect);
+    }
+
+    public int getTrackAlpha() {
+        return mTrackPaint.getAlpha();
+    }
+
+    public int getThumbHeight() {
+        return mThumbHeight;
+    }
+
+    public int getThumbMaxWidth() {
+        return mThumbMaxWidth;
+    }
+
+    public boolean isDragging() {
+        return mIsDragging;
+    }
+
+    /**
+     * Handles the touch event and determines whether to show the fast scroller (or updates it if
+     * it is already showing).
+     */
+    public void handleTouchEvent(MotionEvent ev, int downX, int downY, int lastY) {
+        ViewConfiguration config = ViewConfiguration.get(mRv.getContext());
+
+        int action = ev.getAction();
+        int y = (int) ev.getY();
+        switch (action) {
+            case MotionEvent.ACTION_DOWN:
+                if (isNearPoint(downX, downY)) {
+                    mTouchOffset = downY - mThumbOffset.y;
+                }
+                break;
+            case MotionEvent.ACTION_MOVE:
+                // Check if we should start scrolling
+                if (!mIsDragging && isNearPoint(downX, downY) &&
+                        Math.abs(y - downY) > config.getScaledTouchSlop()) {
+                    mRv.getParent().requestDisallowInterceptTouchEvent(true);
+                    mIsDragging = true;
+                    mTouchOffset += (lastY - downY);
+                    mPopup.animateVisibility(true);
+                    animateScrollbar(true);
+                }
+                if (mIsDragging) {
+                    // Update the fastscroller section name at this touch position
+                    int top = mRv.getBackgroundPadding().top;
+                    int bottom = mRv.getHeight() - mRv.getBackgroundPadding().bottom - mThumbHeight;
+                    float boundedY = (float) Math.max(top, Math.min(bottom, y - mTouchOffset));
+                    String sectionName = mRv.scrollToPositionAtProgress((boundedY - top) /
+                            (bottom - top));
+                    mPopup.setSectionName(sectionName);
+                    mPopup.animateVisibility(!sectionName.isEmpty());
+                    mRv.invalidate(mPopup.updateFastScrollerBounds(mRv, lastY));
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                mIsDragging = false;
+                mTouchOffset = 0;
+                mPopup.animateVisibility(false);
+                animateScrollbar(false);
+                break;
+        }
+    }
+
+    public void draw(Canvas canvas) {
+        if (mThumbOffset.x < 0 || mThumbOffset.y < 0) {
+            return;
+        }
+
+        // Draw the scroll bar track and thumb
+        if (mTrackPaint.getAlpha() > 0) {
+            canvas.drawRect(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth, mRv.getHeight(), mTrackPaint);
+        }
+        canvas.drawRect(mThumbOffset.x, mThumbOffset.y, mThumbOffset.x + mThumbWidth,
+                mThumbOffset.y + mThumbHeight, mThumbPaint);
+
+        // Draw the popup
+        mPopup.draw(canvas);
+    }
+
+    /**
+     * Animates the width and color of the scrollbar.
+     */
+    private void animateScrollbar(boolean isScrolling) {
+        if (mScrollbarAnimator != null) {
+            mScrollbarAnimator.cancel();
+        }
+        ObjectAnimator trackAlphaAnim = ObjectAnimator.ofInt(this, "trackAlpha",
+                isScrolling ? MAX_TRACK_ALPHA : 0);
+        ObjectAnimator thumbWidthAnim = ObjectAnimator.ofInt(this, "width",
+                isScrolling ? mThumbMaxWidth : mThumbMinWidth);
+        ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(),
+                mThumbPaint.getColor(), isScrolling ? mThumbActiveColor : mThumbInactiveColor);
+        colorAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animator) {
+                mThumbPaint.setColor((Integer) animator.getAnimatedValue());
+                mRv.invalidate(mThumbOffset.x, mThumbOffset.y, mThumbOffset.x + mThumbWidth,
+                        mThumbOffset.y + mThumbHeight);
+            }
+        });
+        mScrollbarAnimator = new AnimatorSet();
+        mScrollbarAnimator.playTogether(trackAlphaAnim, thumbWidthAnim, colorAnimation);
+        mScrollbarAnimator.setDuration(SCROLL_BAR_VIS_DURATION);
+        mScrollbarAnimator.start();
+    }
+
+    /**
+     * Returns whether the specified points are near the scroll bar bounds.
+     */
+    private boolean isNearPoint(int x, int y) {
+        mTmpRect.set(mThumbOffset.x, mThumbOffset.y, mThumbOffset.x + mThumbWidth,
+                mThumbOffset.y + mThumbHeight);
+        mTmpRect.inset(mTouchInset, mTouchInset);
+        return mTmpRect.contains(x, y);
+    }
+}
diff --git a/src/com/android/launcher3/BaseRecyclerViewFastScrollPopup.java b/src/com/android/launcher3/BaseRecyclerViewFastScrollPopup.java
new file mode 100644 (file)
index 0000000..aeeb515
--- /dev/null
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2015 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;
+
+import android.animation.Animator;
+import android.animation.ObjectAnimator;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+
+/**
+ * The fast scroller popup that shows the section name the list will jump to.
+ */
+public class BaseRecyclerViewFastScrollPopup {
+
+    private static final float FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR = 1.5f;
+
+    private Resources mRes;
+    private BaseRecyclerView mRv;
+
+    private Drawable mBg;
+    // The absolute bounds of the fast scroller bg
+    private Rect mBgBounds = new Rect();
+    private int mBgOriginalSize;
+    private Rect mInvalidateRect = new Rect();
+    private Rect mTmpRect = new Rect();
+
+    private String mSectionName;
+    private Paint mTextPaint;
+    private Rect mTextBounds = new Rect();
+    private float mAlpha;
+
+    private Animator mAlphaAnimator;
+    private boolean mVisible;
+
+    public BaseRecyclerViewFastScrollPopup(BaseRecyclerView rv, Resources res) {
+        mRes = res;
+        mRv = rv;
+        mBgOriginalSize = res.getDimensionPixelSize(R.dimen.container_fastscroll_popup_size);
+        mBg = res.getDrawable(R.drawable.container_fastscroll_popup_bg);
+        mBg.setBounds(0, 0, mBgOriginalSize, mBgOriginalSize);
+        mTextPaint = new Paint();
+        mTextPaint.setColor(Color.WHITE);
+        mTextPaint.setAntiAlias(true);
+        mTextPaint.setTextSize(res.getDimensionPixelSize(R.dimen.container_fastscroll_popup_text_size));
+    }
+
+    /**
+     * Sets the section name.
+     */
+    public void setSectionName(String sectionName) {
+        if (!sectionName.equals(mSectionName)) {
+            mSectionName = sectionName;
+            mTextPaint.getTextBounds(sectionName, 0, sectionName.length(), mTextBounds);
+            // Update the width to use measureText since that is more accurate
+            mTextBounds.right = (int) (mTextBounds.left + mTextPaint.measureText(sectionName));
+        }
+    }
+
+    /**
+     * Updates the bounds for the fast scroller.
+     * @return the invalidation rect for this update.
+     */
+    public Rect updateFastScrollerBounds(BaseRecyclerView rv, int lastTouchY) {
+        mInvalidateRect.set(mBgBounds);
+
+        if (isVisible()) {
+            // Calculate the dimensions and position of the fast scroller popup
+            int edgePadding = rv.getMaxScrollbarWidth();
+            int bgPadding = (mBgOriginalSize - mTextBounds.height()) / 2;
+            int bgHeight = mBgOriginalSize;
+            int bgWidth = Math.max(mBgOriginalSize, mTextBounds.width() + (2 * bgPadding));
+            if (Utilities.isRtl(mRes)) {
+                mBgBounds.left = rv.getBackgroundPadding().left + (2 * rv.getMaxScrollbarWidth());
+                mBgBounds.right = mBgBounds.left + bgWidth;
+            } else {
+                mBgBounds.right = rv.getWidth() - rv.getBackgroundPadding().right -
+                        (2 * rv.getMaxScrollbarWidth());
+                mBgBounds.left = mBgBounds.right - bgWidth;
+            }
+            mBgBounds.top = lastTouchY - (int) (FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR * bgHeight);
+            mBgBounds.top = Math.max(edgePadding,
+                    Math.min(mBgBounds.top, rv.getHeight() - edgePadding - bgHeight));
+            mBgBounds.bottom = mBgBounds.top + bgHeight;
+        } else {
+            mBgBounds.setEmpty();
+        }
+
+        // Combine the old and new fast scroller bounds to create the full invalidate rect
+        mInvalidateRect.union(mBgBounds);
+        return mInvalidateRect;
+    }
+
+    /**
+     * Animates the visibility of the fast scroller popup.
+     */
+    public void animateVisibility(boolean visible) {
+        if (mVisible != visible) {
+            mVisible = visible;
+            if (mAlphaAnimator != null) {
+                mAlphaAnimator.cancel();
+            }
+            mAlphaAnimator = ObjectAnimator.ofFloat(this, "alpha", visible ? 1f : 0f);
+            mAlphaAnimator.setDuration(visible ? 200 : 150);
+            mAlphaAnimator.start();
+        }
+    }
+
+    // Setter/getter for the popup alpha for animations
+    public void setAlpha(float alpha) {
+        mAlpha = alpha;
+        mRv.invalidate(mBgBounds);
+    }
+
+    public float getAlpha() {
+        return mAlpha;
+    }
+
+    public int getHeight() {
+        return mBgOriginalSize;
+    }
+
+    public void draw(Canvas c) {
+        if (isVisible()) {
+            // Draw the fast scroller popup
+            int restoreCount = c.save(Canvas.MATRIX_SAVE_FLAG);
+            c.translate(mBgBounds.left, mBgBounds.top);
+            mTmpRect.set(mBgBounds);
+            mTmpRect.offsetTo(0, 0);
+            mBg.setBounds(mTmpRect);
+            mBg.setAlpha((int) (mAlpha * 255));
+            mBg.draw(c);
+            mTextPaint.setAlpha((int) (mAlpha * 255));
+            c.drawText(mSectionName, (mBgBounds.width() - mTextBounds.width()) / 2,
+                    mBgBounds.height() - (mBgBounds.height() - mTextBounds.height()) / 2,
+                    mTextPaint);
+            c.restoreToCount(restoreCount);
+        }
+    }
+
+    public boolean isVisible() {
+        return (mAlpha > 0f) && (mSectionName != null);
+    }
+}
index 6c13b4a..a0be8ea 100644 (file)
@@ -16,6 +16,7 @@
 
 package com.android.launcher3;
 
+import android.animation.ObjectAnimator;
 import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.res.ColorStateList;
@@ -24,6 +25,7 @@ import android.content.res.Resources.Theme;
 import android.content.res.TypedArray;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
+import android.graphics.Paint;
 import android.graphics.Region;
 import android.graphics.drawable.Drawable;
 import android.os.Build;
@@ -34,6 +36,8 @@ import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
 import android.view.ViewParent;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
 import android.widget.TextView;
 import com.android.launcher3.IconCache.IconLoadRequest;
 import com.android.launcher3.model.PackageItemInfo;
@@ -43,7 +47,8 @@ import com.android.launcher3.model.PackageItemInfo;
  * because we want to make the bubble taller than the text and TextView's clip is
  * too aggressive.
  */
-public class BubbleTextView extends TextView {
+public class BubbleTextView extends TextView
+        implements BaseRecyclerViewFastScrollBar.FastScrollFocusableView {
 
     private static SparseArray<Theme> sPreloaderThemes = new SparseArray<Theme>(2);
 
@@ -56,6 +61,13 @@ public class BubbleTextView extends TextView {
     private static final int DISPLAY_WORKSPACE = 0;
     private static final int DISPLAY_ALL_APPS = 1;
 
+    private static final float FAST_SCROLL_FOCUS_MAX_SCALE = 1.15f;
+    private static final int FAST_SCROLL_FOCUS_MODE_NONE = 0;
+    private static final int FAST_SCROLL_FOCUS_MODE_SCALE_ICON = 1;
+    private static final int FAST_SCROLL_FOCUS_MODE_DRAW_CIRCLE_BG = 2;
+    private static final int FAST_SCROLL_FOCUS_FADE_IN_DURATION = 175;
+    private static final int FAST_SCROLL_FOCUS_FADE_OUT_DURATION = 125;
+
     private final Launcher mLauncher;
     private Drawable mIcon;
     private final Drawable mBackground;
@@ -79,6 +91,12 @@ public class BubbleTextView extends TextView {
     private boolean mIgnorePressedStateChange;
     private boolean mDisableRelayout = false;
 
+    private ObjectAnimator mFastScrollFocusAnimator;
+    private Paint mFastScrollFocusBgPaint;
+    private float mFastScrollFocusFraction;
+    private boolean mFastScrollFocused;
+    private final int mFastScrollMode = FAST_SCROLL_FOCUS_MODE_SCALE_ICON;
+
     private IconLoadRequest mIconLoadRequest;
 
     public BubbleTextView(Context context) {
@@ -131,6 +149,13 @@ public class BubbleTextView extends TextView {
             setShadowLayer(SHADOW_LARGE_RADIUS, 0.0f, SHADOW_Y_OFFSET, SHADOW_LARGE_COLOUR);
         }
 
+        if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_DRAW_CIRCLE_BG) {
+            mFastScrollFocusBgPaint = new Paint();
+            mFastScrollFocusBgPaint.setAntiAlias(true);
+            mFastScrollFocusBgPaint.setColor(
+                    getResources().getColor(R.color.container_fastscroll_thumb_active_color));
+        }
+
         setAccessibilityDelegate(LauncherAppState.getInstance().getAccessibilityDelegate());
     }
 
@@ -335,7 +360,18 @@ public class BubbleTextView extends TextView {
     @Override
     public void draw(Canvas canvas) {
         if (!mCustomShadowsEnabled) {
+            // Draw the fast scroll focus bg if we have one
+            if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_DRAW_CIRCLE_BG &&
+                    mFastScrollFocusFraction > 0f) {
+                DeviceProfile grid = mLauncher.getDeviceProfile();
+                int iconCenterX = getScrollX() + (getWidth() / 2);
+                int iconCenterY = getScrollY() + getPaddingTop() + (grid.iconSizePx / 2);
+                canvas.drawCircle(iconCenterX, iconCenterY,
+                        mFastScrollFocusFraction * (getWidth() / 2), mFastScrollFocusBgPaint);
+            }
+
             super.draw(canvas);
+
             return;
         }
 
@@ -538,6 +574,51 @@ public class BubbleTextView extends TextView {
         }
     }
 
+    // Setters & getters for the animation
+    public void setFastScrollFocus(float fraction) {
+        mFastScrollFocusFraction = fraction;
+        if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_SCALE_ICON) {
+            setScaleX(1f + fraction * (FAST_SCROLL_FOCUS_MAX_SCALE - 1f));
+            setScaleY(1f + fraction * (FAST_SCROLL_FOCUS_MAX_SCALE - 1f));
+        } else {
+            invalidate();
+        }
+    }
+
+    public float getFastScrollFocus() {
+        return mFastScrollFocusFraction;
+    }
+
+    @Override
+    public void setFastScrollFocused(final boolean focused, boolean animated) {
+        if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_NONE) {
+            return;
+        }
+
+        if (mFastScrollFocused != focused) {
+            mFastScrollFocused = focused;
+
+            if (animated) {
+                // Clean up the previous focus animator
+                if (mFastScrollFocusAnimator != null) {
+                    mFastScrollFocusAnimator.cancel();
+                }
+                mFastScrollFocusAnimator = ObjectAnimator.ofFloat(this, "fastScrollFocus",
+                        focused ? 1f : 0f);
+                if (focused) {
+                    mFastScrollFocusAnimator.setInterpolator(new DecelerateInterpolator());
+                } else {
+                    mFastScrollFocusAnimator.setInterpolator(new AccelerateInterpolator());
+                }
+                mFastScrollFocusAnimator.setDuration(focused ?
+                        FAST_SCROLL_FOCUS_FADE_IN_DURATION : FAST_SCROLL_FOCUS_FADE_OUT_DURATION);
+                mFastScrollFocusAnimator.start();
+            } else {
+                mFastScrollFocusFraction = focused ? 1f : 0f;
+            }
+        }
+    }
+
     /**
      * Interface to be implemented by the grand parent to allow click shadow effect.
      */
index a75a09a..77c4540 100644 (file)
@@ -1125,27 +1125,6 @@ public class Launcher extends Activity
         public void forceExitFullImmersion();
     }
 
-    public interface LauncherAppsCallbacks {
-        /**
-         * Updates launcher to the available space that AllApps can take so as not to overlap with
-         * any other views.
-         */
-        @Deprecated
-        public void onAllAppsBoundsChanged(Rect bounds);
-
-        /**
-         * Called to dismiss all apps if it is showing.
-         */
-        @Deprecated
-        public void dismissAllApps();
-
-        /**
-         * Sets the search manager to be used for app search.
-         */
-        @Deprecated
-        public void setSearchManager(Object manager);
-    }
-
     public interface LauncherSearchCallbacks {
         /**
          * Called when the search overlay is shown.
index 83d90da..0b7b1fd 100644 (file)
@@ -143,13 +143,6 @@ public class LauncherAppState {
         return mModel;
     }
 
-    /**
-     * TODO(winsonc, hyunyoungs): We need to respect this
-     */
-    boolean shouldShowAppOrWidgetProvider(ComponentName componentName) {
-        return mAppFilter == null || mAppFilter.shouldShowApp(componentName);
-    }
-
     static void setLauncherProvider(LauncherProvider provider) {
         sLauncherProvider = new WeakReference<LauncherProvider>(provider);
     }
index 56db774..49d6d68 100644 (file)
@@ -70,10 +70,12 @@ public interface LauncherCallbacks {
     /*
      * Extension points for replacing the search experience
      */
+    @Deprecated
     public boolean forceDisableVoiceButtonProxy();
     public boolean providesSearch();
     public boolean startSearch(String initialQuery, boolean selectInitialQuery,
             Bundle appSearchData, Rect sourceBounds);
+    @Deprecated
     public void startVoice();
     public boolean hasCustomContentToLeft();
     public void populateCustomContentContainer();
index 3b06c20..af118d7 100644 (file)
@@ -637,6 +637,25 @@ public final class Utilities {
         return -fm.top + fm.bottom;
     }
 
+    /**
+     * Convenience println with multiple args.
+     */
+    public static void println(String key, Object... args) {
+        StringBuilder b = new StringBuilder();
+        b.append(key);
+        b.append(": ");
+        boolean isFirstArgument = true;
+        for (Object arg : args) {
+            if (isFirstArgument) {
+                isFirstArgument = false;
+            } else {
+                b.append(", ");
+            }
+            b.append(arg);
+        }
+        System.out.println(b.toString());
+    }
+
     @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
     public static boolean isRtl(Resources res) {
         return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) &&
index d56e9fc..32b7be8 100644 (file)
@@ -17,7 +17,6 @@ package com.android.launcher3.allapps;
 
 import android.annotation.SuppressLint;
 import android.annotation.TargetApi;
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Point;
@@ -155,6 +154,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
     private int mSectionNamesMargin;
     private int mNumAppsPerRow;
     private int mNumPredictedAppsPerRow;
+    private int mRecyclerViewTopBottomPadding;
     // This coordinate is relative to this container view
     private final Point mBoundsCheckLastTouchDownPos = new Point(-1, -1);
     // This coordinate is relative to its parent
@@ -189,7 +189,8 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
         mPredictionBarHeight = (int) (grid.allAppsIconSizePx + grid.iconDrawablePaddingOriginalPx +
                 Utilities.calculateTextHeight(grid.allAppsIconTextSizePx) +
                 2 * res.getDimensionPixelSize(R.dimen.all_apps_icon_top_bottom_padding) +
-                2 * res.getDimensionPixelSize(R.dimen.all_apps_prediction_bar_top_bottom_padding));
+                res.getDimensionPixelSize(R.dimen.all_apps_prediction_bar_top_padding) +
+                res.getDimensionPixelSize(R.dimen.all_apps_prediction_bar_bottom_padding));
         mSectionNamesMargin = res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin);
         mApps = new AlphabeticalAppsList(context);
         mApps.setAdapterChangedCallback(this);
@@ -199,6 +200,8 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
         mApps.setAdapter(mAdapter);
         mLayoutManager = mAdapter.getLayoutManager();
         mItemDecoration = mAdapter.getItemDecoration();
+        mRecyclerViewTopBottomPadding =
+                res.getDimensionPixelSize(R.dimen.all_apps_list_top_bottom_padding);
 
         mSearchQueryBuilder = new SpannableStringBuilder();
         Selection.setSelection(mSearchQueryBuilder, 0);
@@ -414,7 +417,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
                     new SimpleSectionMergeAlgorithm((int) Math.ceil(mNumAppsPerRow / 2f),
                             MIN_ROWS_IN_MERGED_SECTION_PHONE, MAX_NUM_MERGES_PHONE);
 
-            mAppsRecyclerView.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow);
+            mAppsRecyclerView.setNumAppsPerRow(mNumAppsPerRow);
             mAdapter.setNumAppsPerRow(mNumAppsPerRow);
             mApps.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow, mergeAlgorithm);
         }
@@ -431,14 +434,16 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
     protected void onUpdateBackgroundAndPaddings(Rect searchBarBounds, Rect padding) {
         boolean isRtl = Utilities.isRtl(getResources());
 
-        // TODO: Use quantum_panel instead of quantum_panel_shape.
+        // TODO: Use quantum_panel instead of quantum_panel_shape
         InsetDrawable background = new InsetDrawable(
                 getResources().getDrawable(R.drawable.quantum_panel_shape), padding.left, 0,
                 padding.right, 0);
+        Rect bgPadding = new Rect();
+        background.getPadding(bgPadding);
         mContainerView.setBackground(background);
         mRevealView.setBackground(background.getConstantState().newDrawable());
-        mAppsRecyclerView.updateBackgroundPadding(padding);
-        mAdapter.updateBackgroundPadding(padding);
+        mAppsRecyclerView.updateBackgroundPadding(bgPadding);
+        mAdapter.updateBackgroundPadding(bgPadding);
 
         // Hack: We are going to let the recycler view take the full width, so reset the padding on
         // the container to zero after setting the background and apply the top-bottom padding to
@@ -448,13 +453,14 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
 
         // Pad the recycler view by the background padding plus the start margin (for the section
         // names)
-        int startInset = Math.max(mSectionNamesMargin, mAppsRecyclerView.getScrollbarWidth());
+        int startInset = Math.max(mSectionNamesMargin, mAppsRecyclerView.getMaxScrollbarWidth());
+        int topBottomPadding = mRecyclerViewTopBottomPadding;
         if (isRtl) {
-            mAppsRecyclerView.setPadding(padding.left + mAppsRecyclerView.getScrollbarWidth(), 0,
-                    padding.right + startInset, 0);
+            mAppsRecyclerView.setPadding(padding.left + mAppsRecyclerView.getMaxScrollbarWidth(),
+                    topBottomPadding, padding.right + startInset, topBottomPadding);
         } else {
-            mAppsRecyclerView.setPadding(padding.left + startInset, 0,
-                    padding.right + mAppsRecyclerView.getScrollbarWidth(), 0);
+            mAppsRecyclerView.setPadding(padding.left + startInset, topBottomPadding,
+                    padding.right + mAppsRecyclerView.getMaxScrollbarWidth(), topBottomPadding);
         }
 
         // Inset the search bar to fit its bounds above the container
@@ -474,8 +480,8 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
         // Update the prediction bar insets as well
         mPredictionBarView = (ViewGroup) findViewById(R.id.prediction_bar);
         FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mPredictionBarView.getLayoutParams();
-        lp.leftMargin = padding.left + mAppsRecyclerView.getScrollbarWidth();
-        lp.rightMargin = padding.right + mAppsRecyclerView.getScrollbarWidth();
+        lp.leftMargin = padding.left + mAppsRecyclerView.getMaxScrollbarWidth();
+        lp.rightMargin = padding.right + mAppsRecyclerView.getMaxScrollbarWidth();
         mPredictionBarView.requestLayout();
     }
 
index 68407bd..19e2757 100644 (file)
@@ -337,7 +337,7 @@ class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHol
         mPredictedAppsDividerPaint.setColor(0x1E000000);
         mPredictedAppsDividerPaint.setAntiAlias(true);
         mPredictionBarBottomPadding =
-                res.getDimensionPixelSize(R.dimen.all_apps_prediction_bar_top_bottom_padding);
+                res.getDimensionPixelSize(R.dimen.all_apps_prediction_bar_bottom_padding);
     }
 
     /**
index ff327da..a17f0e3 100644 (file)
@@ -22,10 +22,10 @@ import android.support.v7.widget.RecyclerView;
 import android.util.AttributeSet;
 import android.view.View;
 import com.android.launcher3.BaseRecyclerView;
+import com.android.launcher3.BaseRecyclerViewFastScrollBar;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.Stats;
-import com.android.launcher3.Utilities;
 
 import java.util.List;
 
@@ -35,13 +35,25 @@ import java.util.List;
 public class AllAppsRecyclerView extends BaseRecyclerView
         implements Stats.LaunchSourceProvider {
 
+    private static final int FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON = 0;
+    private static final int FAST_SCROLL_MODE_FREE_SCROLL = 1;
+
+    private static final int FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW = 0;
+    private static final int FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_SECTIONS = 1;
+
     private AlphabeticalAppsList mApps;
     private int mNumAppsPerRow;
-    private int mNumPredictedAppsPerRow;
     private int mPredictionBarHeight;
-    private int mLastFastscrollPosition = -1;
+
+    private BaseRecyclerViewFastScrollBar.FastScrollFocusableView mLastFastScrollFocusedView;
+    private int mPrevFastScrollFocusedPosition;
+    private int mFastScrollFrameIndex;
+    private int[] mFastScrollFrames = new int[10];
+    private final int mFastScrollMode = FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON;
+    private final int mScrollBarMode = FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW;
 
     private Launcher mLauncher;
+    private ScrollPositionState mScrollPosState = new ScrollPositionState();
 
     public AllAppsRecyclerView(Context context) {
         this(context, null);
@@ -59,6 +71,7 @@ public class AllAppsRecyclerView extends BaseRecyclerView
             int defStyleRes) {
         super(context, attrs, defStyleAttr);
         mLauncher = (Launcher) context;
+        setOverScrollMode(View.OVER_SCROLL_NEVER);
     }
 
     /**
@@ -71,9 +84,8 @@ public class AllAppsRecyclerView extends BaseRecyclerView
     /**
      * Sets the number of apps per row in this recycler view.
      */
-    public void setNumAppsPerRow(int numAppsPerRow, int numPredictedAppsPerRow) {
+    public void setNumAppsPerRow(int numAppsPerRow) {
         mNumAppsPerRow = numAppsPerRow;
-        mNumPredictedAppsPerRow = numPredictedAppsPerRow;
 
         DeviceProfile grid = mLauncher.getDeviceProfile();
         RecyclerView.RecycledViewPool pool = getRecycledViewPool();
@@ -103,11 +115,12 @@ public class AllAppsRecyclerView extends BaseRecyclerView
      */
     public int getScrollPosition() {
         List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
-        getCurScrollState(scrollPosState, items);
-        if (scrollPosState.rowIndex != -1) {
+        getCurScrollState(mScrollPosState, items);
+        if (mScrollPosState.rowIndex != -1) {
             int predictionBarHeight = mApps.getPredictedApps().isEmpty() ? 0 : mPredictionBarHeight;
-            return getPaddingTop() + (scrollPosState.rowIndex * scrollPosState.rowHeight) +
-                    predictionBarHeight - scrollPosState.rowTopOffset;
+            return getPaddingTop() + predictionBarHeight +
+                    (mScrollPosState.rowIndex * mScrollPosState.rowHeight) -
+                            mScrollPosState.rowTopOffset;
         }
         return 0;
     }
@@ -132,143 +145,159 @@ public class AllAppsRecyclerView extends BaseRecyclerView
         }
     }
 
-    @Override
-    protected void onFastScrollingEnd() {
-        mLastFastscrollPosition = -1;
-    }
-
     /**
      * Maps the touch (from 0..1) to the adapter position that should be visible.
      */
     @Override
     public String scrollToPositionAtProgress(float touchFraction) {
-        // Ensure that we have any sections
-        List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections =
-                mApps.getFastScrollerSections();
-        if (fastScrollSections.isEmpty()) {
+        int rowCount = mApps.getNumAppRows();
+        if (rowCount == 0) {
             return "";
         }
 
         // Stop the scroller if it is scrolling
-        LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager();
         stopScroll();
 
-        // If there is a prediction bar, then capture the appropriate area for the prediction bar
-        float predictionBarFraction = 0f;
-        if (!mApps.getPredictedApps().isEmpty()) {
-            predictionBarFraction = (float) mNumPredictedAppsPerRow / mApps.getSize();
-            if (touchFraction <= predictionBarFraction) {
-                // Scroll to the top of the view, where the prediction bar is
-                layoutManager.scrollToPositionWithOffset(0, 0);
-                return "";
+        // Find the fastscroll section that maps to this touch fraction
+        List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections =
+                mApps.getFastScrollerSections();
+        AlphabeticalAppsList.FastScrollSectionInfo lastInfo = fastScrollSections.get(0);
+        if (mScrollBarMode == FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW) {
+            for (int i = 1; i < fastScrollSections.size(); i++) {
+                AlphabeticalAppsList.FastScrollSectionInfo info = fastScrollSections.get(i);
+                if (info.touchFraction > touchFraction) {
+                    break;
+                }
+                lastInfo = info;
             }
+        } else if (mScrollBarMode == FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_SECTIONS){
+            lastInfo = fastScrollSections.get((int) (touchFraction * (fastScrollSections.size() - 1)));
+        } else {
+            throw new RuntimeException("Unexpected scroll bar mode");
         }
 
-        // Since the app ranges are from 0..1, we need to map the touch fraction back to 0..1 from
-        // predictionBarFraction..1
-        touchFraction = (touchFraction - predictionBarFraction) *
-                (1f / (1f - predictionBarFraction));
-        AlphabeticalAppsList.FastScrollSectionInfo lastScrollSection = fastScrollSections.get(0);
-        for (int i = 1; i < fastScrollSections.size(); i++) {
-            AlphabeticalAppsList.FastScrollSectionInfo scrollSection = fastScrollSections.get(i);
-            if (lastScrollSection.appRangeFraction <= touchFraction &&
-                    touchFraction < scrollSection.appRangeFraction) {
-                break;
-            }
-            lastScrollSection = scrollSection;
+        // Map the touch position back to the scroll of the recycler view
+        getCurScrollState(mScrollPosState, mApps.getAdapterItems());
+        int predictionBarHeight = mApps.getPredictedApps().isEmpty() ? 0 : mPredictionBarHeight;
+        int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight,
+                predictionBarHeight);
+        LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager();
+        if (mFastScrollMode == FAST_SCROLL_MODE_FREE_SCROLL) {
+            layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction));
         }
 
-        // Scroll to the view at the position, anchored at the top of the screen. We call the scroll
-        // method on the LayoutManager directly since it is not exposed by RecyclerView.
-        if (mLastFastscrollPosition != lastScrollSection.appItem.position) {
-            mLastFastscrollPosition = lastScrollSection.appItem.position;
-            layoutManager.scrollToPositionWithOffset(lastScrollSection.appItem.position, 0);
-        }
+        if (mPrevFastScrollFocusedPosition != lastInfo.fastScrollToItem.position) {
+            mPrevFastScrollFocusedPosition = lastInfo.fastScrollToItem.position;
 
-        return lastScrollSection.sectionName;
-    }
+            // Reset the last focused view
+            if (mLastFastScrollFocusedView != null) {
+                mLastFastScrollFocusedView.setFastScrollFocused(false, true);
+                mLastFastScrollFocusedView = null;
+            }
 
-    /**
-     * Returns the row index for a app index in the list.
-     */
-    private int findRowForAppIndex(int index) {
-        List<AlphabeticalAppsList.SectionInfo> sections = mApps.getSections();
-        int appIndex = 0;
-        int rowCount = 0;
-        for (AlphabeticalAppsList.SectionInfo info : sections) {
-            int numRowsInSection = (int) Math.ceil((float) info.numApps / mNumAppsPerRow);
-            if (appIndex + info.numApps > index) {
-                return rowCount + ((index - appIndex) / mNumAppsPerRow);
+            if (mFastScrollMode == FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON) {
+                smoothSnapToPosition(mPrevFastScrollFocusedPosition, mScrollPosState);
+            } else if (mFastScrollMode == FAST_SCROLL_MODE_FREE_SCROLL) {
+                final ViewHolder vh = findViewHolderForPosition(mPrevFastScrollFocusedPosition);
+                if (vh != null &&
+                        vh.itemView instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView) {
+                    mLastFastScrollFocusedView =
+                            (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView;
+                    mLastFastScrollFocusedView.setFastScrollFocused(true, true);
+                }
+            } else {
+                throw new RuntimeException("Unexpected fast scroll mode");
             }
-            appIndex += info.numApps;
-            rowCount += numRowsInSection;
         }
-        return appIndex;
+        return lastInfo.sectionName;
     }
 
-    /**
-     * Returns the total number of rows in the list.
-     */
-    private int getNumRows() {
-        List<AlphabeticalAppsList.SectionInfo> sections = mApps.getSections();
-        int rowCount = 0;
-        for (AlphabeticalAppsList.SectionInfo info : sections) {
-            int numRowsInSection = (int) Math.ceil((float) info.numApps / mNumAppsPerRow);
-            rowCount += numRowsInSection;
+    @Override
+    public void onFastScrollCompleted() {
+        super.onFastScrollCompleted();
+        // Reset and clean up the last focused view
+        if (mLastFastScrollFocusedView != null) {
+            mLastFastScrollFocusedView.setFastScrollFocused(false, true);
+            mLastFastScrollFocusedView = null;
         }
-        return rowCount;
+        mPrevFastScrollFocusedPosition = -1;
     }
 
-
     /**
      * Updates the bounds for the scrollbar.
      */
     @Override
-    public void updateVerticalScrollbarBounds() {
+    public void onUpdateScrollbar() {
         List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
 
         // Skip early if there are no items or we haven't been measured
         if (items.isEmpty() || mNumAppsPerRow == 0) {
-            verticalScrollbarBounds.setEmpty();
+            mScrollbar.setScrollbarThumbOffset(-1, -1);
             return;
         }
 
         // Find the index and height of the first visible row (all rows have the same height)
-        int x, y;
+        int rowCount = mApps.getNumAppRows();
+        getCurScrollState(mScrollPosState, items);
+        if (mScrollPosState.rowIndex < 0) {
+            mScrollbar.setScrollbarThumbOffset(-1, -1);
+            return;
+        }
+
         int predictionBarHeight = mApps.getPredictedApps().isEmpty() ? 0 : mPredictionBarHeight;
-        int rowCount = getNumRows();
-        getCurScrollState(scrollPosState, items);
-        if (scrollPosState.rowIndex != -1) {
-            int height = getHeight() - getPaddingTop() - getPaddingBottom();
-            int totalScrollHeight = rowCount * scrollPosState.rowHeight + predictionBarHeight;
-            if (totalScrollHeight > height) {
-                int scrollbarHeight = (int) (height / ((float) totalScrollHeight / height));
-
-                // Calculate the position and size of the scroll bar
-                if (Utilities.isRtl(getResources())) {
-                    x = mBackgroundPadding.left;
-                } else {
-                    x = getWidth() - mBackgroundPadding.right - getScrollbarWidth();
-                }
+        synchronizeScrollBarThumbOffsetToViewScroll(mScrollPosState, rowCount, predictionBarHeight);
+    }
 
-                // To calculate the offset, we compute the percentage of the total scrollable height
-                // that the user has already scrolled and then map that to the scroll bar bounds
-                int availableY = totalScrollHeight - height;
-                int availableScrollY = height - scrollbarHeight;
-                y = (scrollPosState.rowIndex * scrollPosState.rowHeight) + predictionBarHeight
-                        - scrollPosState.rowTopOffset;
-                y = getPaddingTop() +
-                        (int) (((float) (getPaddingTop() + y) / availableY) * availableScrollY);
-
-                verticalScrollbarBounds.set(x, y, x + getScrollbarWidth(), y + scrollbarHeight);
-                return;
+    /**
+     * This runnable runs a single frame of the smooth scroll animation and posts the next frame
+     * if necessary.
+     */
+    private Runnable mSmoothSnapNextFrameRunnable = new Runnable() {
+        @Override
+        public void run() {
+            if (mFastScrollFrameIndex < mFastScrollFrames.length) {
+                scrollBy(0, mFastScrollFrames[mFastScrollFrameIndex]);
+                mFastScrollFrameIndex++;
+                postOnAnimation(mSmoothSnapNextFrameRunnable);
+            } else {
+                // Animation completed, set the fast scroll state on the target view
+                final ViewHolder vh = findViewHolderForPosition(mPrevFastScrollFocusedPosition);
+                if (vh != null &&
+                        vh.itemView instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView &&
+                        mLastFastScrollFocusedView != vh.itemView) {
+                    mLastFastScrollFocusedView =
+                            (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView;
+                    mLastFastScrollFocusedView.setFastScrollFocused(true, true);
+                }
             }
         }
-        verticalScrollbarBounds.setEmpty();
+    };
+
+    /**
+     * Smoothly snaps to a given position.  We do this manually by calculating the keyframes
+     * ourselves and animating the scroll on the recycler view.
+     */
+    private void smoothSnapToPosition(final int position, ScrollPositionState scrollPosState) {
+        removeCallbacks(mSmoothSnapNextFrameRunnable);
+
+        // Calculate the full animation from the current scroll position to the final scroll
+        // position, and then run the animation for the duration.
+        int predictionBarHeight = mApps.getPredictedApps().isEmpty() ? 0 : mPredictionBarHeight;
+        int curScrollY = getPaddingTop() + predictionBarHeight +
+                (scrollPosState.rowIndex * scrollPosState.rowHeight) - scrollPosState.rowTopOffset;
+        int newScrollY = getScrollAtPosition(position, scrollPosState.rowHeight);
+        int numFrames = mFastScrollFrames.length;
+        for (int i = 0; i < numFrames; i++) {
+            // TODO(winsonc): We can interpolate this as well.
+            mFastScrollFrames[i] = (newScrollY - curScrollY) / numFrames;
+        }
+        mFastScrollFrameIndex = 0;
+        postOnAnimation(mSmoothSnapNextFrameRunnable);
     }
 
     /**
-     * Returns the current scroll state.
+     * Returns the current scroll state of the apps rows, not including the prediction
+     * bar.
      */
     private void getCurScrollState(ScrollPositionState stateOut,
             List<AlphabeticalAppsList.AdapterItem> items) {
@@ -288,7 +317,7 @@ public class AllAppsRecyclerView extends BaseRecyclerView
             if (position != NO_POSITION) {
                 AlphabeticalAppsList.AdapterItem item = items.get(position);
                 if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE) {
-                    stateOut.rowIndex = findRowForAppIndex(item.appIndex);
+                    stateOut.rowIndex = item.rowIndex;
                     stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child);
                     stateOut.rowHeight = child.getHeight();
                     break;
@@ -296,4 +325,17 @@ public class AllAppsRecyclerView extends BaseRecyclerView
             }
         }
     }
+
+    /**
+     * Returns the scrollY for the given position in the adapter.
+     */
+    private int getScrollAtPosition(int position, int rowHeight) {
+        AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position);
+        if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE) {
+            int predictionBarHeight = mApps.getPredictedApps().isEmpty() ? 0 : mPredictionBarHeight;
+            return getPaddingTop() + predictionBarHeight + item.rowIndex * rowHeight;
+        } else {
+            return 0;
+        }
+    }
 }
index aa73c74..ea99872 100644 (file)
@@ -62,15 +62,13 @@ public class AlphabeticalAppsList {
     public static class FastScrollSectionInfo {
         // The section name
         public String sectionName;
-        // To map the touch (from 0..1) to the index in the app list to jump to in the fast
-        // scroller, we use the fraction in range (0..1) of the app index / total app count.
-        public float appRangeFraction;
         // The AdapterItem to scroll to for this section
-        public AdapterItem appItem;
+        public AdapterItem fastScrollToItem;
+        // The touch fraction that should map to this fast scroll section info
+        public float touchFraction;
 
-        public FastScrollSectionInfo(String sectionName, float appRangeFraction) {
+        public FastScrollSectionInfo(String sectionName) {
             this.sectionName = sectionName;
-            this.appRangeFraction = appRangeFraction;
         }
     }
 
@@ -83,6 +81,8 @@ public class AlphabeticalAppsList {
         public int position;
         // The type of this item
         public int viewType;
+        // The row that this item shows up on
+        public int rowIndex;
 
         /** Section & App properties */
         // The section for this item
@@ -94,6 +94,8 @@ public class AlphabeticalAppsList {
         public String sectionName = null;
         // The index of this app in the section
         public int sectionAppIndex = -1;
+        // The index of this app in the row
+        public int rowAppIndex;
         // The associated AppInfo for the app
         public AppInfo appInfo = null;
         // The index of this app not including sections
@@ -172,6 +174,7 @@ public class AlphabeticalAppsList {
     private AdapterChangedCallback mAdapterChangedCallback;
     private int mNumAppsPerRow;
     private int mNumPredictedAppsPerRow;
+    private int mNumAppRowsInAdapter;
 
     public AlphabeticalAppsList(Context context) {
         mLauncher = (Launcher) context;
@@ -241,6 +244,13 @@ public class AlphabeticalAppsList {
     }
 
     /**
+     * Returns the number of rows of applications (not including predictions)
+     */
+    public int getNumAppRows() {
+        return mNumAppRowsInAdapter;
+    }
+
+    /**
      * Returns whether there are is a filter set.
      */
     public boolean hasFilter() {
@@ -419,23 +429,23 @@ public class AlphabeticalAppsList {
                 // Create a new spacer for the prediction bar
                 AdapterItem sectionItem = AdapterItem.asPredictionBarSpacer(position++);
                 mAdapterItems.add(sectionItem);
+                // Add a fastscroller section for the prediction bar
+                lastFastScrollerSectionInfo = new FastScrollSectionInfo("");
+                lastFastScrollerSectionInfo.fastScrollToItem = sectionItem;
+                mFastScrollerSections.add(lastFastScrollerSectionInfo);
             }
         }
 
         // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the
         // ordered set of sections
-        List<AppInfo> apps = getFiltersAppInfos();
-        int numApps = apps.size();
-        for (int i = 0; i < numApps; i++) {
-            AppInfo info = apps.get(i);
+        for (AppInfo info : getFiltersAppInfos()) {
             String sectionName = getAndUpdateCachedSectionName(info.title);
 
             // Create a new section if the section names do not match
             if (lastSectionInfo == null || !sectionName.equals(lastSectionName)) {
                 lastSectionName = sectionName;
                 lastSectionInfo = new SectionInfo();
-                lastFastScrollerSectionInfo = new FastScrollSectionInfo(sectionName,
-                        (float) appIndex / numApps);
+                lastFastScrollerSectionInfo = new FastScrollSectionInfo(sectionName);
                 mSections.add(lastSectionInfo);
                 mFastScrollerSections.add(lastFastScrollerSectionInfo);
 
@@ -451,7 +461,7 @@ public class AlphabeticalAppsList {
                     lastSectionInfo.numApps++, info, appIndex++);
             if (lastSectionInfo.firstAppItem == null) {
                 lastSectionInfo.firstAppItem = appItem;
-                lastFastScrollerSectionInfo.appItem = appItem;
+                lastFastScrollerSectionInfo.fastScrollToItem = appItem;
             }
             mAdapterItems.add(appItem);
             mFilteredApps.add(info);
@@ -460,6 +470,45 @@ public class AlphabeticalAppsList {
         // Merge multiple sections together as requested by the merge strategy for this device
         mergeSections();
 
+        if (mNumAppsPerRow != 0) {
+            // Update the number of rows in the adapter after we do all the merging (otherwise, we
+            // would have to shift the values again)
+            int numAppsInSection = 0;
+            int numAppsInRow = 0;
+            int rowIndex = -1;
+            for (AdapterItem item : mAdapterItems) {
+                item.rowIndex = 0;
+                if (item.viewType == AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE) {
+                    numAppsInSection = 0;
+                } else if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE) {
+                    if (numAppsInSection % mNumAppsPerRow == 0) {
+                        numAppsInRow = 0;
+                        rowIndex++;
+                    }
+                    item.rowIndex = rowIndex;
+                    item.rowAppIndex = numAppsInRow;
+                    numAppsInSection++;
+                    numAppsInRow++;
+                }
+            }
+            mNumAppRowsInAdapter = rowIndex + 1;
+
+            // Pre-calculate all the fast scroller fractions based on the number of rows, if we have
+            // predicted apps, then we should account for that as a row in the touchFraction
+            float rowFraction = 1f / (mNumAppRowsInAdapter + (mPredictedApps.isEmpty() ? 0 : 1));
+            float initialOffset = mPredictedApps.isEmpty() ? 0 : rowFraction;
+            for (FastScrollSectionInfo info : mFastScrollerSections) {
+                AdapterItem item = info.fastScrollToItem;
+                if (item.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE) {
+                    info.touchFraction = 0f;
+                    continue;
+                }
+
+                float subRowFraction = item.rowAppIndex * (rowFraction / mNumAppsPerRow);
+                info.touchFraction = initialOffset + item.rowIndex * rowFraction + subRowFraction;
+            }
+        }
+
         // Refresh the recycler view
         if (mAdapter != null) {
             mAdapter.notifyDataSetChanged();
@@ -511,6 +560,7 @@ public class AlphabeticalAppsList {
                     // Remove the next section break
                     mAdapterItems.remove(nextSection.sectionBreakItem);
                     int pos = mAdapterItems.indexOf(section.firstAppItem);
+
                     // Point the section for these new apps to the merged section
                     int nextPos = pos + section.numApps;
                     for (int j = nextPos; j < (nextPos + nextSection.numApps); j++) {
index 500311a..5afd7c4 100644 (file)
@@ -345,9 +345,11 @@ public class WidgetsContainerView extends BaseContainerView
         InsetDrawable background = new InsetDrawable(
                 getResources().getDrawable(R.drawable.quantum_panel_shape_dark), padding.left, 0,
                 padding.right, 0);
+        Rect bgPadding = new Rect();
+        background.getPadding(bgPadding);
         mView.setBackground(background);
         getRevealView().setBackground(background.getConstantState().newDrawable());
-        mView.updateBackgroundPadding(padding);
+        mView.updateBackgroundPadding(bgPadding);
     }
 
     /**
index fa7e2f0..3101f33 100644 (file)
 package com.android.launcher3.widget;
 
 import android.content.Context;
-import android.graphics.Rect;
+import android.graphics.Color;
 import android.support.v7.widget.LinearLayoutManager;
 import android.util.AttributeSet;
 import android.view.View;
 import com.android.launcher3.BaseRecyclerView;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.model.WidgetsModel;
+import com.android.launcher3.R;
 import com.android.launcher3.model.PackageItemInfo;
+import com.android.launcher3.model.WidgetsModel;
 
 /**
  * The widgets recycler view.
@@ -33,6 +33,7 @@ public class WidgetsRecyclerView extends BaseRecyclerView {
 
     private static final String TAG = "WidgetsRecyclerView";
     private WidgetsModel mWidgets;
+    private ScrollPositionState mScrollPosState = new ScrollPositionState();
 
     public WidgetsRecyclerView(Context context) {
         this(context, null);
@@ -58,6 +59,14 @@ public class WidgetsRecyclerView extends BaseRecyclerView {
         addOnItemTouchListener(this);
     }
 
+    public int getFastScrollerTrackColor(int defaultTrackColor) {
+        return Color.WHITE;
+    }
+
+    public int getFastScrollerThumbInactiveColor(int defaultInactiveThumbColor) {
+        return getResources().getColor(R.color.widgets_view_fastscroll_thumb_inactive_color);
+    }
+
     /**
      * Sets the widget model in this view, used to determine the fast scroll position.
      */
@@ -70,15 +79,21 @@ public class WidgetsRecyclerView extends BaseRecyclerView {
      */
     @Override
     public String scrollToPositionAtProgress(float touchFraction) {
-        float pos = mWidgets.getPackageSize() * touchFraction;
+        int rowCount = mWidgets.getPackageSize();
+        if (rowCount == 0) {
+            return "";
+        }
 
-        int posInt = (int) pos;
+        // Stop the scroller if it is scrolling
+        stopScroll();
+
+        getCurScrollState(mScrollPosState);
+        float pos = rowCount * touchFraction;
+        int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight, 0);
         LinearLayoutManager layoutManager = ((LinearLayoutManager) getLayoutManager());
-        getCurScrollState(scrollPosState);
-        layoutManager.scrollToPositionWithOffset((int) pos,
-                (int) (scrollPosState.rowHeight * ((float) posInt - pos)));
+        layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction));
 
-        posInt = (int) ((touchFraction == 1)? pos -1 : pos);
+        int posInt = (int) ((touchFraction == 1)? pos -1 : pos);
         PackageItemInfo p = mWidgets.getPackageItemInfo(posInt);
         return p.titleSectionName;
     }
@@ -87,43 +102,23 @@ public class WidgetsRecyclerView extends BaseRecyclerView {
      * Updates the bounds for the scrollbar.
      */
     @Override
-    public void updateVerticalScrollbarBounds() {
+    public void onUpdateScrollbar() {
         int rowCount = mWidgets.getPackageSize();
-        verticalScrollbarBounds.setEmpty();
 
         // Skip early if, there are no items.
         if (rowCount == 0) {
+            mScrollbar.setScrollbarThumbOffset(-1, -1);
             return;
         }
 
         // Skip early if, there no child laid out in the container.
-        getCurScrollState(scrollPosState);
-        if (scrollPosState.rowIndex < 0) {
+        getCurScrollState(mScrollPosState);
+        if (mScrollPosState.rowIndex < 0) {
+            mScrollbar.setScrollbarThumbOffset(-1, -1);
             return;
         }
 
-        int actualHeight = getHeight() - getPaddingTop() - getPaddingBottom();
-        int totalScrollHeight = rowCount * scrollPosState.rowHeight;
-        // Skip early if the height of all the rows are actually less than the container height.
-        if (totalScrollHeight < actualHeight) {
-            verticalScrollbarBounds.setEmpty();
-            return;
-        }
-
-        int scrollbarHeight = (int) (actualHeight / ((float) totalScrollHeight / actualHeight));
-        int availableY = totalScrollHeight - actualHeight;
-        int availableScrollY = actualHeight - scrollbarHeight;
-        int y = (scrollPosState.rowIndex * scrollPosState.rowHeight)
-                - scrollPosState.rowTopOffset;
-        y = getPaddingTop() +
-                (int) (((float) (getPaddingTop() + y) / availableY) * availableScrollY);
-
-        // Calculate the position and size of the scroll bar.
-        int x = getWidth() - getScrollbarWidth() - mBackgroundPadding.right;
-        if (Utilities.isRtl(getResources())) {
-            x = mBackgroundPadding.left;
-        }
-        verticalScrollbarBounds.set(x, y, x + getScrollbarWidth(), y + scrollbarHeight);
+        synchronizeScrollBarThumbOffsetToViewScroll(mScrollPosState, rowCount, 0);
     }
 
     /**