OSDN Git Service

Reimplement the CM scrubber against the new Launcher
authorcretin45 <cretin45@gmail.com>
Sat, 14 Nov 2015 00:51:43 +0000 (16:51 -0800)
committercretin45 <cretin45@gmail.com>
Mon, 23 Nov 2015 20:10:32 +0000 (12:10 -0800)
PS4: Implement RTL support

Change-Id: I4456d54b5924913d1b36e1cfa9a2269150f6fb3e

28 files changed:
res/drawable-hdpi/letter_indicator.png [new file with mode: 0644]
res/drawable-mdpi/letter_indicator.png [new file with mode: 0644]
res/drawable-xhdpi/letter_indicator.png [new file with mode: 0644]
res/drawable-xxhdpi/letter_indicator.png [new file with mode: 0644]
res/drawable/scrubber_back.xml [new file with mode: 0644]
res/drawable/seek_back.xml [new file with mode: 0644]
res/layout/all_apps_container.xml
res/layout/scrub_layout.xml [new file with mode: 0644]
res/layout/scrubber_container.xml [new file with mode: 0644]
res/layout/widgets_view.xml
res/values/attrs.xml
res/values/colors.xml
res/values/dimens.xml
src/com/android/launcher3/AutoExpandTextView.java [new file with mode: 0644]
src/com/android/launcher3/BaseContainerView.java
src/com/android/launcher3/BaseRecyclerView.java
src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java
src/com/android/launcher3/BaseRecyclerViewScrubber.java [new file with mode: 0644]
src/com/android/launcher3/BaseRecyclerViewScrubberSection.java [new file with mode: 0644]
src/com/android/launcher3/BubbleTextView.java
src/com/android/launcher3/DeviceProfile.java
src/com/android/launcher3/Launcher.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/AllAppsRecyclerViewContainerView.java
src/com/android/launcher3/widget/WidgetsContainerView.java
src/com/android/launcher3/widget/WidgetsRecyclerView.java

diff --git a/res/drawable-hdpi/letter_indicator.png b/res/drawable-hdpi/letter_indicator.png
new file mode 100644 (file)
index 0000000..4770d81
Binary files /dev/null and b/res/drawable-hdpi/letter_indicator.png differ
diff --git a/res/drawable-mdpi/letter_indicator.png b/res/drawable-mdpi/letter_indicator.png
new file mode 100644 (file)
index 0000000..2ecfe7c
Binary files /dev/null and b/res/drawable-mdpi/letter_indicator.png differ
diff --git a/res/drawable-xhdpi/letter_indicator.png b/res/drawable-xhdpi/letter_indicator.png
new file mode 100644 (file)
index 0000000..6f21860
Binary files /dev/null and b/res/drawable-xhdpi/letter_indicator.png differ
diff --git a/res/drawable-xxhdpi/letter_indicator.png b/res/drawable-xxhdpi/letter_indicator.png
new file mode 100644 (file)
index 0000000..acbacb0
Binary files /dev/null and b/res/drawable-xxhdpi/letter_indicator.png differ
diff --git a/res/drawable/scrubber_back.xml b/res/drawable/scrubber_back.xml
new file mode 100644 (file)
index 0000000..c5022de
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+     Copyright (C) 2015 The CyanogenMod 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">
+    <solid android:color="@color/scrubber_background"/>
+</shape>
diff --git a/res/drawable/seek_back.xml b/res/drawable/seek_back.xml
new file mode 100644 (file)
index 0000000..d97a870
--- /dev/null
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2015 The CyanogenMod 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.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="360dp"
+    android:height="48dp"
+    android:viewportWidth="360"
+    android:viewportHeight="48">
+
+    <path
+        android:strokeColor="#FFFFFF"
+        android:strokeWidth="2"
+        android:strokeMiterLimit="10"
+        android:strokeLineCap="round"
+        android:pathData="M16,24h328" />
+</vector>
\ No newline at end of file
index 626edaf..3a2c96c 100644 (file)
         android:focusable="true"
         android:descendantFocusability="afterDescendants" />
 
+    <ViewStub
+        android:id="@+id/scrubber_container_stub"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:inflatedId="@+id/scrubber_container"
+        android:layout="@layout/scrubber_container"
+        android:layout_gravity="bottom"
+        android:clipToPadding="false"/>
+
 </com.android.launcher3.allapps.AllAppsRecyclerViewContainerView>
\ No newline at end of file
diff --git a/res/layout/scrub_layout.xml b/res/layout/scrub_layout.xml
new file mode 100644 (file)
index 0000000..11ee381
--- /dev/null
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2015 The CyanogenMod 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.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/app_drawer_scrubber_container"
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/scrubber_height"
+    android:paddingLeft="@dimen/app_drawer_scrubber_padding"
+    android:paddingRight="@dimen/app_drawer_scrubber_padding"
+    android:layout_alignParentBottom="true"
+    android:background="@drawable/seek_back">
+
+    <com.android.launcher3.AutoExpandTextView
+        android:id="@+id/scrubberText"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:textColor="@android:color/white"
+        android:maxLines="1"
+        android:lines="1"
+        android:textSize="8sp" />
+
+    <SeekBar
+        android:id="@+id/scrubber"
+        android:thumb="@android:color/transparent"
+        android:progressDrawable="@android:color/transparent"
+        android:layout_width="match_parent"
+        android:layout_gravity="center"
+        android:layout_height="match_parent" />
+
+</FrameLayout>
diff --git a/res/layout/scrubber_container.xml b/res/layout/scrubber_container.xml
new file mode 100644 (file)
index 0000000..4fe8475
--- /dev/null
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2015 The CyanogenMod 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.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                android:id="@+id/scrubber_container"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_gravity="bottom"
+                android:clipToPadding="false">
+    <FrameLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="bottom"
+        android:layout_below="@+id/scrubberIndicator"
+        android:background="@drawable/scrubber_back"
+        android:clipToPadding="false">
+
+        <com.android.launcher3.BaseRecyclerViewScrubber
+            android:id="@+id/base_scrubber"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:clickable="true"
+            android:layout_gravity="bottom"/>
+    </FrameLayout>
+
+    <TextView
+        android:id="@+id/scrubberIndicator"
+        android:background="@drawable/letter_indicator"
+        android:layout_width="100dp"
+        android:layout_marginLeft="@dimen/app_drawer_scrubber_padding"
+        android:layout_marginRight="@dimen/app_drawer_scrubber_padding"
+        android:paddingTop="18dp"
+        android:textSize="24sp"
+        android:gravity="center_horizontal|top"
+        android:textColor="@android:color/black"
+        android:clickable="false"
+        android:layout_marginBottom="-40dp"
+        android:visibility="invisible"
+        android:layout_height="100dp" />
+</RelativeLayout>
index 755634f..1f276ad 100644 (file)
             android:layout_gravity="center"
             android:elevation="15dp"
             android:visibility="gone" />
+
+        <ViewStub
+            android:id="@+id/scrubber_container_stub"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:inflatedId="@+id/scrubber_container"
+            android:layout="@layout/scrubber_container"
+            android:layout_gravity="bottom"
+            android:clipToPadding="false"/>
     </FrameLayout>
 
 </com.android.launcher3.widget.WidgetsContainerView>
\ No newline at end of file
index 7ffebce..d7f9ef4 100644 (file)
         <attr name="layout_ignoreTopInsets" />
         <attr name="layout_ignoreBottomInsets" />
     </declare-styleable>
+
+    <declare-styleable name="AutofitTextView">
+        <attr name="minTextSize" format="dimension" />
+        <attr name="precision" format="float" />
+        <attr name="sizeToFit" format="boolean" />
+    </declare-styleable>
 </resources>
index 8a7f627..b70e1f8 100644 (file)
@@ -33,8 +33,9 @@
     <color name="folder_edge_effect_color">#FF757575</color>
 
     <color name="quantum_panel_text_color">#FF666666</color>
+    <color name="quantum_panel_text_color_dark">#FFF</color>
     <color name="quantum_panel_bg_color">#FFF5F5F5</color>
-    <color name="quantum_panel_bg_color_dark">#FF374248</color>
+    <color name="quantum_panel_bg_color_dark">#76000000</color>
 
     <color name="outline_color">#FFFFFFFF</color>
 
 
     <!-- All Apps -->
     <color name="all_apps_grid_section_text_color">#009688</color>
+    <color name="all_apps_grid_section_text_color_dark">#FFF</color>
     <color name="all_apps_search_market_button_focused_bg_color">#DDDDDD</color>
 
     <!-- Widgets view -->
     <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>
+
+    <color name="app_scrubber_highlight_color">@android:color/white</color>
+    <color name="app_scrubber_gray_color">@android:color/darker_gray</color>
+    <color name="scrubber_background">#CC14191E</color>
 </resources>
index e3c8194..799ea98 100644 (file)
 
 <!-- All Apps -->
     <dimen name="all_apps_grid_view_start_margin">0dp</dimen>
+    <dimen name="all_apps_grid_view_start_margin_with_sections">36dp</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_height">48dp</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_icon_width_gap_with_sections">12dp</dimen>
+    <dimen name="all_apps_icon_size_with_sections">48dp</dimen>
     <!-- The top padding should account for the existing all_apps_list_top_bottom_padding -->
     <dimen name="all_apps_prediction_icon_top_padding">8dp</dimen>
     <dimen name="all_apps_prediction_icon_bottom_padding">18dp</dimen>
@@ -78,6 +81,8 @@
     <dimen name="all_apps_empty_search_bg_top_offset">144dp</dimen>
     <dimen name="all_apps_background_canvas_width">700dp</dimen>
     <dimen name="all_apps_background_canvas_height">475dp</dimen>
+    <dimen name="all_apps_icon_size_grid">55dp</dimen>
+    <dimen name="all_apps_icon_size_ragged">48dp</dimen>
 
 <!-- Widget tray -->
     <dimen name="widget_container_inset">8dp</dimen>
     <dimen name="blur_size_click_shadow">4dp</dimen>
     <dimen name="click_shadow_high_shift">2dp</dimen>
 
-<!-- Pending widget -->
+    <dimen name="app_drawer_scrubber_padding">20dp</dimen>
     <dimen name="pending_widget_min_padding">8dp</dimen>
     <dimen name="pending_widget_elevation">2dp</dimen>
 
+    <!-- Scrubber -->
+    <dimen name="scrubber_bottom_padding">30dp</dimen>
+    <dimen name="scrubber_height">48dp</dimen>
+    <dimen name="app_drawer_scrubber_padding">20dp</dimen>
+
 <!-- Folder open animation -->
     <integer name="folder_translate_y_dist">300</integer>
     <integer name="folder_icon_translate_y_dist">100</integer>
diff --git a/src/com/android/launcher3/AutoExpandTextView.java b/src/com/android/launcher3/AutoExpandTextView.java
new file mode 100644 (file)
index 0000000..ea7ac89
--- /dev/null
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2014 Grantland Chew
+ * Copyright (C) 2015 The CyanogenMod 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.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.text.method.TransformationMethod;
+import android.text.style.ForegroundColorSpan;
+import android.util.AttributeSet;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+
+/**
+ * A single-line TextView that resizes it's letter spacing to fit the width of the view
+ *
+ * @author Grantland Chew <grantlandchew@gmail.com>
+ * @author Linus Lee <llee@cyngn.com>
+ */
+public class AutoExpandTextView extends TextView {
+    // How precise we want to be when reaching the target textWidth size
+    private static final float PRECISION = 0.01f;
+
+    // Attributes
+    private float mPrecision;
+    private TextPaint mPaint;
+    private float[] mPositions;
+
+    public static class HighlightedText {
+        public String mText;
+        public boolean mHighlight;
+
+        public HighlightedText(String text, boolean highlight) {
+            mText = text;
+            mHighlight = highlight;
+        }
+    }
+
+    public AutoExpandTextView(Context context) {
+        super(context);
+        init(context, null, 0);
+    }
+
+    public AutoExpandTextView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context, attrs, 0);
+    }
+
+    public AutoExpandTextView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        init(context, attrs, defStyle);
+    }
+
+    private void init(Context context, AttributeSet attrs, int defStyle) {
+        float precision = PRECISION;
+
+        if (attrs != null) {
+            TypedArray ta = context.obtainStyledAttributes(
+                    attrs,
+                    R.styleable.AutofitTextView,
+                    defStyle,
+                    0);
+            precision = ta.getFloat(R.styleable.AutofitTextView_precision, precision);
+        }
+
+        mPaint = new TextPaint();
+        setPrecision(precision);
+    }
+
+    /**
+     * @return the amount of precision used to calculate the correct text size to fit within it's
+     * bounds.
+     */
+    public float getPrecision() {
+        return mPrecision;
+    }
+
+    /**
+     * Set the amount of precision used to calculate the correct text size to fit within it's
+     * bounds. Lower precision is more precise and takes more time.
+     *
+     * @param precision The amount of precision.
+     */
+    public void setPrecision(float precision) {
+        if (precision != mPrecision) {
+            mPrecision = precision;
+            refitText();
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void setLines(int lines) {
+        super.setLines(1);
+        refitText();
+    }
+
+    /**
+     * Only allow max lines of 1
+     */
+    @Override
+    public void setMaxLines(int maxLines) {
+        super.setMaxLines(1);
+        refitText();
+    }
+
+    /**
+     * Re size the font so the specified text fits in the text box assuming the text box is the
+     * specified width.
+     */
+    private void refitText() {
+        CharSequence text = getText();
+
+        if (TextUtils.isEmpty(text)) {
+            return;
+        }
+
+        TransformationMethod method = getTransformationMethod();
+        if (method != null) {
+            text = method.getTransformation(text, this);
+        }
+        int targetWidth = getWidth() - getPaddingLeft() - getPaddingRight();
+        if (targetWidth > 0) {
+            float high = 100;
+            float low = 0;
+
+            mPaint.set(getPaint());
+            mPaint.setTextSize(getTextSize());
+            float letterSpacing = getLetterSpacing(text, mPaint, targetWidth, low, high,
+                    mPrecision);
+            mPaint.setLetterSpacing(letterSpacing);
+            calculateSections(text);
+
+            super.setLetterSpacing(letterSpacing);
+        }
+    }
+
+    public float getPositionOfSection(int position) {
+        if (mPositions == null || position >= mPositions.length) {
+            return 0;
+        }
+        return mPositions[position];
+    }
+
+    /**
+     * This calculates the different horizontal positions of each character
+     */
+    private void calculateSections(CharSequence text) {
+        mPositions = new float[text.length()];
+        for (int i = 0; i < text.length(); i++) {
+            if (i == 0) {
+                mPositions[0] = mPaint.measureText(text, 0, 1) / 2;
+            } else {
+                // try to be lazy and just add the width of the newly added char
+                mPositions[i] = mPaint.measureText(text, i, i + 1) + mPositions[i - 1];
+            }
+        }
+    }
+
+    /**
+     * Sets the list of sections in the text view.  This will take the first character of each
+     * and space it out in the text view using letter spacing
+     */
+    public void setSections(ArrayList<HighlightedText> sections) {
+        mPositions = null;
+        if (sections == null || sections.size() == 0) {
+            setText("");
+            return;
+        }
+
+        Resources r = getContext().getResources();
+        int highlightColor = r.getColor(R.color.app_scrubber_highlight_color);
+        int grayColor = r.getColor(R.color.app_scrubber_gray_color);
+
+        SpannableStringBuilder builder = new SpannableStringBuilder();
+        for (HighlightedText highlightText : sections) {
+            SpannableString spannable = new SpannableString(highlightText.mText.substring(0, 1));
+            spannable.setSpan(
+                    new ForegroundColorSpan(highlightText.mHighlight ? highlightColor : grayColor),
+                    0, spannable.length(), 0);
+            builder.append(spannable);
+        }
+
+        setText(builder);
+    }
+
+    private static float getLetterSpacing(CharSequence text, TextPaint paint, float targetWidth,
+                                          float low, float high, float precision) {
+        float mid = (low + high) / 2.0f;
+        paint.setLetterSpacing(mid);
+
+        float measuredWidth = paint.measureText(text, 0, text.length());
+
+        if (high - low < precision) {
+            if (measuredWidth < targetWidth) {
+                return mid;
+            } else {
+                return low;
+            }
+        } else if (measuredWidth > targetWidth) {
+            return getLetterSpacing(text, paint, targetWidth, low, mid, precision);
+        } else if (measuredWidth < targetWidth) {
+            return getLetterSpacing(text, paint, targetWidth, mid, high, precision);
+        } else {
+            return mid;
+        }
+    }
+
+    @Override
+    protected void onTextChanged(final CharSequence text, final int start,
+                                 final int lengthBefore, final int lengthAfter) {
+        super.onTextChanged(text, start, lengthBefore, lengthAfter);
+        refitText();
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+        if (w != oldw) {
+            refitText();
+        }
+    }
+}
\ No newline at end of file
index c118240..ac2afa9 100644 (file)
@@ -20,7 +20,10 @@ import android.content.Context;
 import android.graphics.Rect;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.view.View;
+import android.view.ViewStub;
 import android.widget.LinearLayout;
+import android.widget.TextView;
 
 /**
  * A base container view, which supports resizing.
@@ -43,6 +46,11 @@ public abstract class BaseContainerView extends LinearLayout implements Insettab
     // The inset to apply to the edges and between the search bar and the container
     private int mContainerBoundsInset;
     private boolean mHasSearchBar;
+    private boolean mUseScrubber;
+
+    protected View mScrubberContainerView;
+    protected BaseRecyclerViewScrubber mScrubber;
+    protected final int mScrubberHeight;
 
     public BaseContainerView(Context context) {
         this(context, null);
@@ -55,6 +63,7 @@ public abstract class BaseContainerView extends LinearLayout implements Insettab
     public BaseContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
         mContainerBoundsInset = getResources().getDimensionPixelSize(R.dimen.container_bounds_inset);
+        mScrubberHeight = getResources().getDimensionPixelSize(R.dimen.scrubber_height);
     }
 
     @Override
@@ -67,6 +76,10 @@ public abstract class BaseContainerView extends LinearLayout implements Insettab
         mHasSearchBar = true;
     }
 
+    protected boolean hasSearchBar() {
+        return mHasSearchBar;
+    }
+
     /**
      * Sets the search bar bounds for this container view to match.
      */
@@ -87,10 +100,46 @@ public abstract class BaseContainerView extends LinearLayout implements Insettab
         });
     }
 
+    public final void setUseScrubber(boolean use) {
+        mUseScrubber = use;
+        if (use) {
+            ViewStub stub = (ViewStub) findViewById(R.id.scrubber_container_stub);
+            mScrubberContainerView = stub.inflate();
+            if (mScrubberContainerView == null) {
+                throw new IllegalStateException(
+                        "Layout must contain an id: R.id.scrubber_container");
+            }
+            mScrubber = (BaseRecyclerViewScrubber)
+                    mScrubberContainerView.findViewById(R.id.base_scrubber);
+            BaseRecyclerView recyclerView = getRecyclerView();
+            if (recyclerView != null) {
+                mScrubber.setRecycler(recyclerView);
+                mScrubber
+                        .setScrubberIndicator((TextView) mScrubberContainerView
+                                .findViewById(R.id.scrubberIndicator));
+                mScrubber.updateSections();
+            }
+        } else {
+            removeView(mScrubberContainerView);
+            BaseRecyclerView recyclerView = getRecyclerView();
+            if (recyclerView != null) {
+                recyclerView.setUseScrollbar(true);
+            }
+        }
+    }
+
+    public final boolean userScrubber() {
+        return mUseScrubber;
+    }
+
+    protected void updateBackgroundAndPaddings() {
+        updateBackgroundAndPaddings(false);
+    }
+
     /**
      * Update the backgrounds and padding in response to a change in the bounds or insets.
      */
-    protected void updateBackgroundAndPaddings() {
+    protected void updateBackgroundAndPaddings(boolean force) {
         Rect padding;
         Rect searchBarBounds = new Rect();
         if (!isValidSearchBarBounds(mFixedSearchBarBounds)) {
@@ -119,7 +168,8 @@ public abstract class BaseContainerView extends LinearLayout implements Insettab
 
         // If either the computed container padding has changed, or the computed search bar bounds
         // has changed, then notify the container
-        if (!padding.equals(mContentPadding) || !searchBarBounds.equals(mSearchBarBounds)) {
+        if (force || !padding.equals(mContentPadding) ||
+                !searchBarBounds.equals(mSearchBarBounds)) {
             mContentPadding.set(padding);
             mContentBounds.set(padding.left, padding.top,
                     getMeasuredWidth() - padding.right,
@@ -135,6 +185,11 @@ public abstract class BaseContainerView extends LinearLayout implements Insettab
     protected abstract void onUpdateBackgroundAndPaddings(Rect searchBarBounds, Rect padding);
 
     /**
+     * This might be null if the container doesn't have a recycler.
+     */
+    protected abstract BaseRecyclerView getRecyclerView();
+
+    /**
      * Returns whether the search bar bounds we got are considered valid.
      */
     private boolean isValidSearchBarBounds(Rect searchBarBounds) {
index f0d8b3b..77925b5 100644 (file)
@@ -57,6 +57,7 @@ public abstract class BaseRecyclerView extends RecyclerView
     }
 
     protected BaseRecyclerViewFastScrollBar mScrollbar;
+    protected boolean mUseScrollbar = false;
 
     private int mDownX;
     private int mDownY;
@@ -74,7 +75,6 @@ public abstract 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);
@@ -93,12 +93,16 @@ public abstract class BaseRecyclerView extends RecyclerView
             //                initiate that here if the recycler view scroll state is not
             //                RecyclerView.SCROLL_STATE_IDLE.
 
-            onUpdateScrollbar(dy);
+            if (mUseScrollbar) {
+                onUpdateScrollbar(dy);
+            }
         }
     }
 
     public void reset() {
-        mScrollbar.reattachThumbToScroll();
+        if (mUseScrollbar) {
+            mScrollbar.reattachThumbToScroll();
+        }
     }
 
     @Override
@@ -137,19 +141,28 @@ public abstract class BaseRecyclerView extends RecyclerView
                 if (shouldStopScroll(ev)) {
                     stopScroll();
                 }
-                mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY);
+                if (mScrollbar != null) {
+                    mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY);
+                }
                 break;
             case MotionEvent.ACTION_MOVE:
                 mLastY = y;
-                mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY);
+                if (mScrollbar != null) {
+                    mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY);
+                }
                 break;
             case MotionEvent.ACTION_UP:
             case MotionEvent.ACTION_CANCEL:
                 onFastScrollCompleted();
-                mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY);
+                if (mScrollbar != null) {
+                    mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY);
+                }
                 break;
         }
-        return mScrollbar.isDraggingThumb();
+        if (mUseScrollbar) {
+            return mScrollbar.isDraggingThumb();
+        }
+        return false;
     }
 
     public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
@@ -183,7 +196,10 @@ public abstract class BaseRecyclerView extends RecyclerView
      * Returns the scroll bar width when the user is scrolling.
      */
     public int getMaxScrollbarWidth() {
-        return mScrollbar.getThumbMaxWidth();
+        if (mUseScrollbar) {
+            return mScrollbar.getThumbMaxWidth();
+        }
+        return 0;
     }
 
     /**
@@ -204,9 +220,12 @@ public abstract class BaseRecyclerView extends RecyclerView
      *   AvailableScrollBarHeight = Total height of the visible view - thumb height
      */
     protected int getAvailableScrollBarHeight() {
-        int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom;
-        int availableScrollBarHeight = visibleHeight - mScrollbar.getThumbHeight();
-        return availableScrollBarHeight;
+        if (mUseScrollbar) {
+            int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom;
+            int availableScrollBarHeight = visibleHeight - mScrollbar.getThumbHeight();
+            return availableScrollBarHeight;
+        }
+        return 0;
     }
 
     /**
@@ -223,11 +242,23 @@ public abstract class BaseRecyclerView extends RecyclerView
         return defaultInactiveThumbColor;
     }
 
+    public void setUseScrollbar(boolean useScrollbar) {
+        mUseScrollbar = useScrollbar;
+        if (useScrollbar) {
+            mScrollbar = new BaseRecyclerViewFastScrollBar(this, getResources());
+        }  else {
+            mScrollbar = null;
+        }
+        invalidate();
+    }
+
     @Override
     protected void dispatchDraw(Canvas canvas) {
         super.dispatchDraw(canvas);
-        onUpdateScrollbar(0);
-        mScrollbar.draw(canvas);
+        if (mUseScrollbar) {
+            onUpdateScrollbar(0);
+            mScrollbar.draw(canvas);
+        }
     }
 
     /**
@@ -241,6 +272,9 @@ public abstract class BaseRecyclerView extends RecyclerView
      */
     protected void synchronizeScrollBarThumbOffsetToViewScroll(ScrollPositionState scrollPosState,
             int rowCount) {
+        if (!mUseScrollbar) {
+            return;
+        }
         // Only show the scrollbar if there is height to be scrolled
         int availableScrollBarHeight = getAvailableScrollBarHeight();
         int availableScrollHeight = getAvailableScrollHeight(rowCount, scrollPosState.rowHeight);
@@ -273,6 +307,12 @@ public abstract class BaseRecyclerView extends RecyclerView
      */
     public abstract String scrollToPositionAtProgress(float touchFraction);
 
+    public abstract String scrollToSection(String sectionName);
+
+    public abstract String[] getSectionNames();
+
+    public void setFastScrollDragging(boolean dragging) {}
+
     /**
      * Updates the bounds for the scrollbar.
      * <p>Override in each subclass of this base class.
index fcee7e8..e7b7992 100644 (file)
@@ -38,6 +38,7 @@ public class BaseRecyclerViewFastScrollBar {
 
     public interface FastScrollFocusableView {
         void setFastScrollFocused(boolean focused, boolean animated);
+        void setFastScrollDimmed(boolean dimmed, boolean animated);
     }
 
     private final static int MAX_TRACK_ALPHA = 30;
@@ -193,6 +194,7 @@ public class BaseRecyclerViewFastScrollBar {
                         Math.abs(y - downY) > config.getScaledTouchSlop()) {
                     mRv.getParent().requestDisallowInterceptTouchEvent(true);
                     mIsDragging = true;
+                    mRv.setFastScrollDragging(mIsDragging);
                     if (mCanThumbDetach) {
                         mIsThumbDetached = true;
                     }
@@ -220,6 +222,7 @@ public class BaseRecyclerViewFastScrollBar {
                 mIgnoreDragGesture = false;
                 if (mIsDragging) {
                     mIsDragging = false;
+                    mRv.setFastScrollDragging(mIsDragging);
                     mPopup.animateVisibility(false);
                     animateScrollbar(false);
                 }
diff --git a/src/com/android/launcher3/BaseRecyclerViewScrubber.java b/src/com/android/launcher3/BaseRecyclerViewScrubber.java
new file mode 100644 (file)
index 0000000..1692548
--- /dev/null
@@ -0,0 +1,424 @@
+/*
+ * Copyright (C) 2013 The CyanogenMod 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.AnimatorListenerAdapter;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.PointF;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.Message;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.LinearSmoothScroller;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+import java.lang.IllegalArgumentException;
+import java.util.ArrayList;
+import java.util.Collections;
+
+/**
+ * BaseRecyclerViewScrubber
+ * <pre>
+ *     This is the scrubber at the bottom of a BaseRecyclerView
+ * </pre>
+ *
+ * @see {@link LinearLayout}
+ */
+public class BaseRecyclerViewScrubber extends LinearLayout {
+    private BaseRecyclerView mBaseRecyclerView;
+    private TextView mScrubberIndicator;
+    private SeekBar mSeekBar;
+    private AutoExpandTextView mScrubberText;
+    private SectionContainer mSectionContainer;
+    private ScrubberAnimationState mScrubberAnimationState;
+    private Drawable mTransparentDrawable;
+    private boolean mIsRtl;
+
+    private static final int MSG_SET_TARGET = 1000;
+    private static final int MSG_ANIMATE_PICK = MSG_SET_TARGET + 1;
+
+    /**
+     * UiHandler
+     * <pre>
+     *     Using a handler for sending signals to perform certain actions.  The reason for
+     *     using this is to be able to remove and replace a signal if signals are being
+     *     sent too fast (e.g. user scrubbing like crazy). This allows the touch loop to
+     *     complete then later run the animations in their own loops.
+     * </pre>
+     */
+    private class UiHandler extends Handler {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_SET_TARGET:
+                    int adapterIndex = msg.arg1;
+                    performSetTarget(adapterIndex);
+
+                    break;
+                case MSG_ANIMATE_PICK:
+                    int index = msg.arg1;
+                    int width = msg.arg2;
+                    int lastIndex = (Integer)msg.obj;
+                    performAnimatePickMessage(index, width, lastIndex);
+                    break;
+                default:
+                    super.handleMessage(msg);
+            }
+        }
+
+        /**
+         * Overidden to remove identical calls if they are called subsequently fast enough.
+         *
+         * This is the final point that is public in the call chain.  Other calls to sendMessageXXX
+         * will eventually call this function which calls "enqueueMessage" which is private.
+         *
+         * @param msg {@link Message}
+         * @param uptimeMillis {@link Long}
+         *
+         * @throws IllegalArgumentException {@link IllegalArgumentException}
+         */
+        @Override
+        public boolean sendMessageAtTime(Message msg, long uptimeMillis) throws
+                IllegalArgumentException {
+            if (msg == null) {
+                throw new IllegalArgumentException("'msg' cannot be null!");
+            }
+            if (hasMessages(msg.what)) {
+                removeMessages(msg.what);
+            }
+            return super.sendMessageAtTime(msg, uptimeMillis);
+        }
+
+    }
+    private Handler mUiHandler = new UiHandler();
+    private void sendSetTargetMessage(int adapterIndex) {
+        Message msg = mUiHandler.obtainMessage(MSG_SET_TARGET);
+        msg.what = MSG_SET_TARGET;
+        msg.arg1 = adapterIndex;
+        mUiHandler.sendMessage(msg);
+    }
+    private void performSetTarget(int adapterIndex) {
+        mBaseRecyclerView.scrollToSection(mSectionContainer.getSectionName(adapterIndex, mIsRtl));
+    }
+    private void sendAnimatePickMessage(int index, int width, int lastIndex) {
+        Message msg = mUiHandler.obtainMessage(MSG_ANIMATE_PICK);
+        msg.what = MSG_ANIMATE_PICK;
+        msg.arg1 = index;
+        msg.arg2 = width;
+        msg.obj = lastIndex;
+        mUiHandler.sendMessage(msg);
+    }
+    private void performAnimatePickMessage(int index, int width, int lastIndex) {
+        if (mScrubberIndicator != null) {
+            // get the index based on the direction the user is scrolling
+            int directionalIndex = mSectionContainer.getDirectionalIndex(lastIndex, index);
+            String sectionText = mSectionContainer.getSectionName(directionalIndex, mIsRtl);
+            float translateX = (index * width) / (float) mSectionContainer.size();
+            // if we are showing letters, grab the position based on the text view
+            if (mSectionContainer.showLetters()) {
+                translateX = mScrubberText.getPositionOfSection(index);
+            }
+            // center the x position
+            translateX -= mScrubberIndicator.getMeasuredWidth() / 2;
+            if (mIsRtl) {
+                translateX = -translateX;
+            }
+            mScrubberIndicator.setTranslationX(translateX);
+            mScrubberIndicator.setText(sectionText);
+        }
+    }
+
+    /**
+     * Constructor
+     *
+     * @param context {@link Context}
+     * @param attrs {@link AttributeSet}
+     */
+    public BaseRecyclerViewScrubber(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context);
+    }
+
+    /**
+     * Constructor
+     *
+     * @param context {@link Context}
+     */
+    public BaseRecyclerViewScrubber(Context context) {
+        super(context);
+        init(context);
+    }
+
+    @Override
+    public void onRtlPropertiesChanged(int layoutDirection) {
+        super.onRtlPropertiesChanged(layoutDirection);
+        mIsRtl = Utilities.isRtl(getResources());
+        updateSections();
+    }
+
+    /**
+     * Simple container class that tries to abstract out the knowledge of complex sections vs
+     * simple string sections
+     */
+    private static class SectionContainer {
+        private BaseRecyclerViewScrubberSection.
+                RtlIndexArrayList<BaseRecyclerViewScrubberSection> mSections;
+        private String[] mSectionNames;
+        private final boolean mIsRtl;
+
+        public SectionContainer(String[] sections, boolean isRtl) {
+            mIsRtl = isRtl;
+            mSections = BaseRecyclerViewScrubberSection.createSections(sections, isRtl);
+            mSectionNames = sections;
+            if (isRtl) {
+                final int N = mSectionNames.length;
+                for(int i = 0; i < N / 2; i++) {
+                    String temp = mSectionNames[i];
+                    mSectionNames[i] = mSectionNames[N - i - 1];
+                    mSectionNames[N - i - 1] = temp;
+                }
+                Collections.reverse(mSections);
+            }
+        }
+
+        public int size() {
+            return showLetters() ? mSections.size() : mSectionNames.length;
+        }
+
+        public String getSectionName(int idx, boolean isRtl) {
+            if (size() == 0) {
+                return null;
+            }
+            return showLetters() ? mSections.get(idx, isRtl).getText() : mSectionNames[idx];
+        }
+
+        /**
+         * Because the list section headers is not necessarily the same size as the scrubber
+         * letters, we need to map from the larger list to the smaller list.
+         * In the case that curIdx is not highlighted, it will use the directional index to
+         * determine the adapter index
+         * @return the mSectionNames index (aka the underlying adapter index).
+         */
+        public int getAdapterIndex(int prevIdx, int curIdx) {
+            if (!showLetters() || size() == 0) {
+                return curIdx;
+            }
+
+            // because we have some unhighlighted letters, we need to first get the directional
+            // index before getting the adapter index
+            return mSections.get(getDirectionalIndex(prevIdx, curIdx), mIsRtl).getAdapterIndex();
+        }
+
+        /**
+         * Given the direction the user is scrolling in, return the closest index which is a
+         * highlighted index
+         */
+        public int getDirectionalIndex(int prevIdx, int curIdx) {
+            if (!showLetters() || size() == 0 || mSections.get(curIdx, mIsRtl).getHighlight()) {
+                return curIdx;
+            }
+
+            if (prevIdx < curIdx) {
+                if (mIsRtl) {
+                    return mSections.get(curIdx).getPreviousIndex();
+                } else {
+                    return mSections.get(curIdx).getNextIndex();
+                }
+            } else {
+                if (mIsRtl) {
+                    return mSections.get(curIdx).getNextIndex();
+                } else {
+                    return mSections.get(curIdx).getPreviousIndex();
+                }
+
+            }
+        }
+
+        /**
+         * @return true if the scrubber is showing characters as opposed to a line
+         */
+        public boolean showLetters() {
+            return mSections != null;
+        }
+
+        /**
+         * Initializes the scrubber text with the proper characters
+         */
+        public void initializeScrubberText(AutoExpandTextView scrubberText) {
+            scrubberText.setSections(BaseRecyclerViewScrubberSection.getHighlightText(mSections));
+        }
+    }
+
+    public void updateSections() {
+        if (mBaseRecyclerView != null) {
+            mSectionContainer = new SectionContainer(mBaseRecyclerView.getSectionNames(), mIsRtl);
+            mSectionContainer.initializeScrubberText(mScrubberText);
+            mSeekBar.setMax(mSectionContainer.size() - 1);
+
+            // show a white line if there are no letters, otherwise show transparent
+            Drawable d = mSectionContainer.showLetters() ? mTransparentDrawable
+                    : getContext().getResources().getDrawable(R.drawable.seek_back);
+            ((ViewGroup) mSeekBar.getParent()).setBackground(d);
+        }
+    }
+
+    public void setRecycler(BaseRecyclerView baseRecyclerView) {
+        mBaseRecyclerView = baseRecyclerView;
+    }
+
+    public void setScrubberIndicator(TextView scrubberIndicator) {
+        mScrubberIndicator = scrubberIndicator;
+    }
+
+    private boolean isReady() {
+        return mBaseRecyclerView != null &&
+                mSectionContainer != null;
+    }
+
+    private void init(Context context) {
+        mIsRtl = Utilities.isRtl(context.getResources());
+        LayoutInflater.from(context).inflate(R.layout.scrub_layout, this);
+        mTransparentDrawable = new ColorDrawable(Color.TRANSPARENT);
+        mScrubberAnimationState = new ScrubberAnimationState();
+        mSeekBar = (SeekBar) findViewById(R.id.scrubber);
+        mScrubberText = (AutoExpandTextView) findViewById(R.id.scrubberText);
+        mSeekBar.setOnSeekBarChangeListener(mScrubberAnimationState);
+    }
+
+    /**
+     * Handles the animations of the scrubber indicator
+     */
+    private class ScrubberAnimationState implements SeekBar.OnSeekBarChangeListener {
+        private static final long SCRUBBER_DISPLAY_DURATION_IN = 60;
+        private static final long SCRUBBER_DISPLAY_DURATION_OUT = 150;
+        private static final long SCRUBBER_DISPLAY_DELAY_IN = 0;
+        private static final long SCRUBBER_DISPLAY_DELAY_OUT = 200;
+        private static final float SCRUBBER_SCALE_START = 0f;
+        private static final float SCRUBBER_SCALE_END = 1f;
+        private static final float SCRUBBER_ALPHA_START = 0f;
+        private static final float SCRUBBER_ALPHA_END = 1f;
+
+        private boolean mTouchingTrack = false;
+        private boolean mAnimatingIn = false;
+        private int mLastIndex = -1;
+
+        private void touchTrack(boolean touching) {
+            mTouchingTrack = touching;
+
+            if (mScrubberIndicator != null) {
+                if (mTouchingTrack) {
+                    animateIn();
+                } else if (!mAnimatingIn) { // finish animating in before animating out
+                    animateOut();
+                }
+
+                mBaseRecyclerView.setFastScrollDragging(mTouchingTrack);
+            }
+        }
+
+        private void animateIn() {
+            if (mScrubberIndicator == null) {
+                return;
+            }
+            // start from a scratch position when animating in
+            mScrubberIndicator.animate().cancel();
+            mScrubberIndicator.setPivotX(mScrubberIndicator.getMeasuredWidth() / 2);
+            mScrubberIndicator.setPivotY(mScrubberIndicator.getMeasuredHeight() * 0.9f);
+            mScrubberIndicator.setAlpha(SCRUBBER_ALPHA_START);
+            mScrubberIndicator.setScaleX(SCRUBBER_SCALE_START);
+            mScrubberIndicator.setScaleY(SCRUBBER_SCALE_START);
+            mScrubberIndicator.setVisibility(View.VISIBLE);
+            mAnimatingIn = true;
+
+            mScrubberIndicator.animate()
+                .alpha(SCRUBBER_ALPHA_END)
+                .scaleX(SCRUBBER_SCALE_END)
+                .scaleY(SCRUBBER_SCALE_END)
+                .setStartDelay(SCRUBBER_DISPLAY_DELAY_IN)
+                .setDuration(SCRUBBER_DISPLAY_DURATION_IN)
+                .setListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        mAnimatingIn = false;
+                        // if the user has stopped touching the seekbar, animate back out
+                        if (!mTouchingTrack) {
+                            animateOut();
+                        }
+                    }
+                }).start();
+        }
+
+        private void animateOut() {
+            if (mScrubberIndicator == null) {
+                return;
+            }
+            mScrubberIndicator.animate()
+                .alpha(SCRUBBER_ALPHA_START)
+                .scaleX(SCRUBBER_SCALE_START)
+                .scaleY(SCRUBBER_SCALE_START)
+                .setStartDelay(SCRUBBER_DISPLAY_DELAY_OUT)
+                .setDuration(SCRUBBER_DISPLAY_DURATION_OUT)
+                .setListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        mScrubberIndicator.setVisibility(View.INVISIBLE);
+                    }
+                });
+        }
+
+        @Override
+        public void onProgressChanged(SeekBar seekBar, int index, boolean fromUser) {
+            if (!isReady()) {
+                return;
+            }
+            progressChanged(seekBar, index, fromUser);
+        }
+
+        private void progressChanged(SeekBar seekBar, int index, boolean fromUser) {
+
+            sendAnimatePickMessage(index, seekBar.getWidth(), mLastIndex);
+
+            // get the index of the underlying list
+            int adapterIndex = mSectionContainer.getDirectionalIndex(mLastIndex, index);
+            // Post set target index on queue to get processed by Looper later
+            sendSetTargetMessage(adapterIndex);
+
+            mLastIndex = index;
+        }
+
+        @Override
+        public void onStartTrackingTouch(SeekBar seekBar) {
+            touchTrack(true);
+        }
+
+        @Override
+        public void onStopTrackingTouch(SeekBar seekBar) {
+            touchTrack(false);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/BaseRecyclerViewScrubberSection.java b/src/com/android/launcher3/BaseRecyclerViewScrubberSection.java
new file mode 100644 (file)
index 0000000..1d17ea8
--- /dev/null
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2015 The CyanogenMod 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.text.TextUtils;
+import android.util.Log;
+import android.util.SparseArray;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+
+public class BaseRecyclerViewScrubberSection {
+    private static final String TAG = "BRVScrubberSections";
+    private static final String ALPHA_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+    private static final int MAX_NUMBER_CUSTOM_SECTIONS = 8;
+    private static final int MAX_SECTIONS = ALPHA_LETTERS.length() + MAX_NUMBER_CUSTOM_SECTIONS;
+    public static final int INVALID_INDEX = -1;
+
+    private AutoExpandTextView.HighlightedText mHighlightedText;
+    private int mPreviousValidIndex;
+    private int mNextValidIndex;
+    private int mAdapterIndex;
+
+    public BaseRecyclerViewScrubberSection(String text, boolean highlight, int idx) {
+        mHighlightedText = new AutoExpandTextView.HighlightedText(text, highlight);
+        mAdapterIndex = idx;
+        mPreviousValidIndex = mNextValidIndex = idx;
+    }
+
+    public boolean getHighlight() {
+        return mHighlightedText.mHighlight;
+    }
+
+    public String getText() {
+        return mHighlightedText.mText;
+    }
+
+    public int getPreviousIndex() {
+        return mPreviousValidIndex;
+    }
+
+    public int getNextIndex() {
+        return mNextValidIndex;
+    }
+
+    public int getAdapterIndex() {
+        return mAdapterIndex;
+    }
+
+    private static int
+        getFirstValidIndex(RtlIndexArrayList<BaseRecyclerViewScrubberSection> sections,
+            boolean isRtl) {
+        for (int i = 0; i < sections.size(); i++) {
+            if (sections.get(i, isRtl).getHighlight()) {
+                return i;
+            }
+        }
+
+        return INVALID_INDEX;
+    }
+
+    private static void createIndices(RtlIndexArrayList<BaseRecyclerViewScrubberSection> sections,
+            boolean isRtl) {
+        if (sections == null || sections.size() == 0) {
+            return;
+        }
+
+        // walk forwards and fill out the previous valid index based on the previous highlight
+        int currentIdx = getFirstValidIndex(sections, isRtl);
+        for (int i = 0; i < sections.size(); i++) {
+            if (sections.get(i, isRtl).getHighlight()) {
+                currentIdx = i;
+            }
+
+            sections.get(i, isRtl).mPreviousValidIndex = currentIdx;
+        }
+
+        // currentIdx should be now on the last valid index so walk back and fill the other way
+        for (int i = sections.size() - 1; i >= 0; i--) {
+            if (sections.get(i, isRtl).getHighlight()) {
+                currentIdx = i;
+            }
+
+            sections.get(i, isRtl).mNextValidIndex = currentIdx;
+        }
+    }
+
+    public static ArrayList<AutoExpandTextView.HighlightedText> getHighlightText(
+            RtlIndexArrayList<BaseRecyclerViewScrubberSection> sections) {
+        if (sections == null) {
+            return null;
+        }
+
+        ArrayList<AutoExpandTextView.HighlightedText> highlights = new ArrayList<>(sections.size());
+        for (BaseRecyclerViewScrubberSection section : sections) {
+            highlights.add(section.mHighlightedText);
+        }
+
+        return highlights;
+    }
+
+    private static void addAlphaLetters(RtlIndexArrayList<BaseRecyclerViewScrubberSection> sections,
+                                        HashMap<Integer, Integer> foundAlphaLetters) {
+        for (int i = 0; i < ALPHA_LETTERS.length(); i++) {
+            boolean highlighted = foundAlphaLetters.containsKey(i);
+            int index = highlighted
+                    ? foundAlphaLetters.get(i) : BaseRecyclerViewScrubberSection.INVALID_INDEX;
+
+            sections.add(new BaseRecyclerViewScrubberSection(ALPHA_LETTERS.substring(i, i + 1),
+                    highlighted, index));
+        }
+    }
+
+    /**
+     * Takes the sections and runs some checks to see if we can create a valid
+     * appDrawerScrubberSection out of it.  This list will contain the original header list plus
+     * fill out the remaining sections based on the ALPHA_LETTERS.  It will then determine which
+     * ones to highlight as well as what letters to highlight when scrolling over the
+     * grayed out sections
+     * @param sectionNames list of sectionName Strings
+     * @return the list of scrubber sections
+     */
+    public static RtlIndexArrayList<BaseRecyclerViewScrubberSection>
+        createSections(String[] sectionNames, boolean isRtl) {
+        // check if we have a valid header section
+        if (!validSectionNameList(sectionNames)) {
+            return null;
+        }
+
+        // this will track the mapping of ALPHA_LETTERS index to the headers index
+        HashMap<Integer, Integer> foundAlphaLetters = new HashMap<>();
+        RtlIndexArrayList<BaseRecyclerViewScrubberSection> sections =
+                new RtlIndexArrayList<>(sectionNames.length);
+        boolean inAlphaLetterSection = false;
+
+        for (int i = 0; i < sectionNames.length; i++) {
+            int alphaLetterIndex = TextUtils.isEmpty(sectionNames[i])
+                    ? -1 : ALPHA_LETTERS.indexOf(sectionNames[i]);
+
+            // if we found an ALPHA_LETTERS store that in foundAlphaLetters and continue
+            if (alphaLetterIndex >= 0) {
+                foundAlphaLetters.put(alphaLetterIndex, i);
+                inAlphaLetterSection = true;
+            } else {
+                // if we are exiting the ALPHA_LETTERS section, add it here
+                if (inAlphaLetterSection) {
+                    addAlphaLetters(sections, foundAlphaLetters);
+                    inAlphaLetterSection = false;
+                }
+
+                // add the custom header
+                sections.add(new BaseRecyclerViewScrubberSection(sectionNames[i], true, i));
+            }
+        }
+
+        // if the last section are the alpha letters, then add it
+        if (inAlphaLetterSection) {
+            addAlphaLetters(sections, foundAlphaLetters);
+        }
+
+        // create the forward and backwards indices for scrolling over the grayed out sections
+        BaseRecyclerViewScrubberSection.createIndices(sections, isRtl);
+
+        return sections;
+    }
+
+    /**
+     * Walk through the sectionNames and check for a few things:
+     * 1) No more than MAX_NUMBER_CUSTOM_SECTIONS sectionNames exist in the sectionNames list or no more
+     * than MAX_SECTIONS sectionNames exist in the list
+     * 2) the headers that fall in the ALPHA_LETTERS category are in the same order as ALPHA_LETTERS
+     * 3) There are no sectionNames that exceed length of 1
+     * 4) The alpha letter sectionName is together and not separated by other things
+     */
+    private static boolean validSectionNameList(String[] sectionNames) {
+        int numCustomSections = 0;
+        int previousAlphaIndex = -1;
+        boolean foundAlphaSections = false;
+
+        for (String s : sectionNames) {
+            if (TextUtils.isEmpty(s)) {
+                numCustomSections++;
+                continue;
+            }
+
+            if (s.length() > 1) {
+                Log.w(TAG, "Found section " + s + " with length: " + s.length());
+                return false;
+            }
+
+            int alphaIndex = ALPHA_LETTERS.indexOf(s);
+            if (alphaIndex >= 0) {
+                if (previousAlphaIndex != -1) {
+                    // if the previous alpha index is >= alphaIndex then it is in the wrong order
+                    if (previousAlphaIndex >= alphaIndex) {
+                        Log.w(TAG, "Found letter index " + previousAlphaIndex
+                                + " which is greater than " + alphaIndex);
+                        return false;
+                    }
+                }
+
+                // if we've found headers previously and the index is -1 that means the alpha
+                // letters are separated out into two sections so return false
+                if (foundAlphaSections && previousAlphaIndex == -1) {
+                    Log.w(TAG, "Found alpha letters twice");
+                    return false;
+                }
+
+                previousAlphaIndex = alphaIndex;
+                foundAlphaSections = true;
+            } else {
+                numCustomSections++;
+                previousAlphaIndex = -1;
+            }
+        }
+
+        final int listSize = foundAlphaSections
+                ? numCustomSections + ALPHA_LETTERS.length()
+                : numCustomSections;
+
+        // if one of these conditions are satisfied, then return true
+        if (numCustomSections <= MAX_NUMBER_CUSTOM_SECTIONS || listSize <= MAX_SECTIONS) {
+            return true;
+        }
+
+        if (numCustomSections > MAX_NUMBER_CUSTOM_SECTIONS) {
+            Log.w(TAG, "Found " + numCustomSections + "# custom sections when " +
+                    MAX_NUMBER_CUSTOM_SECTIONS + " is allowed!");
+        } else if (listSize > MAX_SECTIONS) {
+            Log.w(TAG, "Found " + listSize + " sections when " +
+                    MAX_SECTIONS + " is allowed!");
+        }
+
+        return false;
+    }
+
+    public static class RtlIndexArrayList<T> extends ArrayList<T>  {
+
+        public RtlIndexArrayList(int capacity) {
+            super(capacity);
+        }
+
+        public T get(int index, boolean isRtl) {
+            if (isRtl) {
+                index = size() - 1 - index;
+            }
+            return super.get(index);
+        }
+    }
+}
\ No newline at end of file
index 5070878..205c113 100644 (file)
@@ -86,7 +86,7 @@ public class BubbleTextView extends TextView
     private final boolean mDeferShadowGenerationOnTouch;
     private final boolean mCustomShadowsEnabled;
     private final boolean mLayoutHorizontal;
-    private final int mIconSize;
+    private int mIconSize;
     private int mTextColor;
 
     private boolean mStayPressed;
@@ -94,9 +94,11 @@ public class BubbleTextView extends TextView
     private boolean mDisableRelayout = false;
 
     private ObjectAnimator mFastScrollFocusAnimator;
+    private ObjectAnimator mFastScrollDimAnimator;
     private Paint mFastScrollFocusBgPaint;
     private float mFastScrollFocusFraction;
     private boolean mFastScrollFocused;
+    private boolean mFastScrollDimmed;
     private final int mFastScrollMode = FAST_SCROLL_FOCUS_MODE_SCALE_ICON;
 
     private IconLoadRequest mIconLoadRequest;
@@ -433,6 +435,10 @@ public class BubbleTextView extends TextView
         if (mBackground != null) mBackground.setCallback(null);
     }
 
+    public void setIconSize(int iconSize) {
+        mIconSize = iconSize;
+    }
+
     @Override
     public void setTextColor(int color) {
         mTextColor = color;
@@ -628,6 +634,30 @@ public class BubbleTextView extends TextView
         }
     }
 
+    @Override
+    public void setFastScrollDimmed(boolean dimmed, boolean animated) {
+        if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_NONE) {
+            return;
+        }
+
+        if (!animated) {
+            mFastScrollDimmed = dimmed;
+            setAlpha(dimmed ? 0.4f : 1f);
+        } else  if (mFastScrollDimmed != dimmed) {
+            mFastScrollDimmed = dimmed;
+
+            // Clean up the previous dim animator
+            if (mFastScrollDimAnimator != null) {
+                mFastScrollDimAnimator.cancel();
+            }
+            mFastScrollDimAnimator = ObjectAnimator.ofFloat(this, View.ALPHA,
+                    dimmed ? 0.4f : 1f);
+            mFastScrollDimAnimator.setDuration(dimmed ?
+                    FAST_SCROLL_FOCUS_FADE_IN_DURATION : FAST_SCROLL_FOCUS_FADE_OUT_DURATION);
+            mFastScrollDimAnimator.start();
+        }
+    }
+
     /**
      * Interface to be implemented by the grand parent to allow click shadow effect.
      */
index 774594f..f77b348 100644 (file)
@@ -33,6 +33,8 @@ import android.view.ViewGroup.MarginLayoutParams;
 import android.widget.FrameLayout;
 import android.widget.LinearLayout;
 
+import com.android.launcher3.allapps.AllAppsContainerView;
+
 public class DeviceProfile {
 
     public final InvariantDeviceProfile inv;
@@ -225,11 +227,13 @@ public class DeviceProfile {
     /**
      * @param recyclerViewWidth the available width of the AllAppsRecyclerView
      */
-    public void updateAppsViewNumCols(Resources res, int recyclerViewWidth) {
-        int appsViewLeftMarginPx =
-                res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin);
-        int allAppsCellWidthGap =
-                res.getDimensionPixelSize(R.dimen.all_apps_icon_width_gap);
+    public void updateAppsViewNumCols(Resources res, int recyclerViewWidth, int gridStrategy) {
+        int appsViewLeftMarginPx = gridStrategy == AllAppsContainerView.SECTION_STRATEGY_GRID ?
+                res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin) :
+                res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin_with_sections);
+        int allAppsCellWidthGap = gridStrategy == AllAppsContainerView.SECTION_STRATEGY_GRID ?
+                res.getDimensionPixelSize(R.dimen.all_apps_icon_width_gap) :
+                res.getDimensionPixelSize(R.dimen.all_apps_icon_width_gap_with_sections);
         int availableAppsWidthPx = (recyclerViewWidth > 0) ? recyclerViewWidth : availableWidthPx;
         int numAppsCols = (availableAppsWidthPx - appsViewLeftMarginPx) /
                 (allAppsIconSizePx + allAppsCellWidthGap);
index 6faea20..1976ca9 100644 (file)
@@ -348,6 +348,8 @@ public class Launcher extends Activity
 
     private DeviceProfile mDeviceProfile;
 
+    private boolean mUseScrubber = true;
+
     // This is set to the view that launched the activity that navigated the user away from
     // launcher. Since there is no callback for when the activity has finished launching, enable
     // the press state and keep this reference to reset the press state when we return to launcher.
@@ -1448,6 +1450,11 @@ public class Launcher extends Activity
             mAppsView.setSearchBarController(mAppsView.newDefaultAppSearchController());
         }
 
+        mAppsView.setUseScrubber(mUseScrubber);
+        mAppsView.setSectionStrategy(AllAppsContainerView.SECTION_STRATEGY_RAGGED);
+        mAppsView.setGridTheme(AllAppsContainerView.GRID_THEME_DARK);
+        mWidgetsView.setUseScrubber(false);
+
         // Setup the drag controller (drop targets have to be added in reverse order in priority)
         dragController.setDragScoller(mWorkspace);
         dragController.setScrollView(mDragLayer);
index 88c6aca..bff7752 100644 (file)
@@ -35,6 +35,7 @@ import android.view.ViewGroup;
 import android.widget.LinearLayout;
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.BaseContainerView;
+import com.android.launcher3.BaseRecyclerView;
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.DeleteDropTarget;
 import com.android.launcher3.DeviceProfile;
@@ -130,8 +131,14 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
         LauncherTransitionable, View.OnTouchListener, View.OnLongClickListener,
         AllAppsSearchBarController.Callbacks {
 
+    public static final int SECTION_STRATEGY_GRID = 1;
+    public static final int SECTION_STRATEGY_RAGGED = 2;
+
+    public static final int GRID_THEME_LIGHT = 1;
+    public static final int GRID_THEME_DARK = 2;
+
     private static final int MIN_ROWS_IN_MERGED_SECTION_PHONE = 3;
-    private static final int MAX_NUM_MERGES_PHONE = 2;
+    private static final int MAX_NUM_MERGES_PHONE = 1;
 
     @Thunk Launcher mLauncher;
     @Thunk AlphabeticalAppsList mApps;
@@ -148,6 +155,10 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
     private View mSearchBarView;
     private SpannableStringBuilder mSearchQueryBuilder = null;
 
+    private int mSectionStrategy = SECTION_STRATEGY_RAGGED;
+    private int mGridTheme = GRID_THEME_DARK;
+    private int mLastGridTheme = -1;
+
     private int mSectionNamesMargin;
     private int mNumAppsPerRow;
     private int mNumPredictedAppsPerRow;
@@ -178,9 +189,12 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
         Resources res = context.getResources();
 
         mLauncher = (Launcher) context;
-        mSectionNamesMargin = res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin);
+        mSectionNamesMargin = mSectionStrategy == SECTION_STRATEGY_GRID ?
+                res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin) :
+                res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin_with_sections);
         mApps = new AlphabeticalAppsList(context);
-        mAdapter = new AllAppsGridAdapter(mLauncher, mApps, this, mLauncher, this);
+        mAdapter = new AllAppsGridAdapter(mLauncher, mApps, this, mLauncher,
+                this, mSectionStrategy, mGridTheme);
         mApps.setAdapter(mAdapter);
         mLayoutManager = mAdapter.getLayoutManager();
         mItemDecoration = mAdapter.getItemDecoration();
@@ -196,6 +210,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
      */
     public void setPredictedApps(List<ComponentKey> apps) {
         mApps.setPredictedApps(apps);
+        updateScrubber();
     }
 
     /**
@@ -203,6 +218,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
      */
     public void setApps(List<AppInfo> apps) {
         mApps.setApps(apps);
+        updateScrubber();
     }
 
     /**
@@ -210,6 +226,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
      */
     public void addApps(List<AppInfo> apps) {
         mApps.addApps(apps);
+        updateScrubber();
     }
 
     /**
@@ -217,6 +234,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
      */
     public void updateApps(List<AppInfo> apps) {
         mApps.updateApps(apps);
+        updateScrubber();
     }
 
     /**
@@ -224,6 +242,29 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
      */
     public void removeApps(List<AppInfo> apps) {
         mApps.removeApps(apps);
+        updateScrubber();
+    }
+
+    private void updateScrubber() {
+        if (userScrubber()) {
+            mScrubber.updateSections();
+        }
+    }
+
+    public void setSectionStrategy(int sectionStrategy) {
+        Resources res = getResources();
+        mSectionStrategy = sectionStrategy;
+        mSectionNamesMargin = mSectionStrategy == SECTION_STRATEGY_GRID ?
+                res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin) :
+                res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin_with_sections);
+        mAdapter.setSectionStrategy(mSectionStrategy);
+        mAppsRecyclerView.setSectionStrategy(mSectionStrategy);
+    }
+
+    public void setGridTheme(int gridTheme) {
+        mGridTheme = gridTheme;
+        mAdapter.setGridTheme(mGridTheme);
+        updateBackgroundAndPaddings(true);
     }
 
     /**
@@ -316,6 +357,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
         // Load the all apps recycler view
         mAppsRecyclerView = (AllAppsRecyclerView) findViewById(R.id.apps_list_view);
         mAppsRecyclerView.setApps(mApps);
+        mAppsRecyclerView.setSectionStrategy(mSectionStrategy);
         mAppsRecyclerView.setLayoutManager(mLayoutManager);
         mAppsRecyclerView.setAdapter(mAdapter);
         mAppsRecyclerView.setHasFixedSize(true);
@@ -337,7 +379,8 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
         int availableWidth = !mContentBounds.isEmpty() ? mContentBounds.width() :
                 MeasureSpec.getSize(widthMeasureSpec);
         DeviceProfile grid = mLauncher.getDeviceProfile();
-        grid.updateAppsViewNumCols(getResources(), availableWidth);
+        grid.updateAppsViewNumCols(getResources(), availableWidth,
+                mSectionStrategy);
         if (mNumAppsPerRow != grid.allAppsNumCols ||
                 mNumPredictedAppsPerRow != grid.allAppsNumPredictiveCols) {
             mNumAppsPerRow = grid.allAppsNumCols;
@@ -345,7 +388,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
 
             // If there is a start margin to draw section names, determine how we are going to merge
             // app sections
-            boolean mergeSectionsFully = mSectionNamesMargin == 0 || !grid.isPhone;
+            boolean mergeSectionsFully = mSectionStrategy == SECTION_STRATEGY_GRID;
             AlphabeticalAppsList.MergeAlgorithm mergeAlgorithm = mergeSectionsFully ?
                     new FullMergeAlgorithm() :
                     new SimpleSectionMergeAlgorithm((int) Math.ceil(mNumAppsPerRow / 2f),
@@ -369,8 +412,10 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
         boolean isRtl = Utilities.isRtl(getResources());
 
         // TODO: Use quantum_panel instead of quantum_panel_shape
+        int bgRes = mGridTheme == GRID_THEME_DARK ? R.drawable.quantum_panel_shape_dark :
+                R.drawable.quantum_panel_shape;
         InsetDrawable background = new InsetDrawable(
-                getResources().getDrawable(R.drawable.quantum_panel_shape), padding.left, 0,
+                getResources().getDrawable(bgRes), padding.left, 0,
                 padding.right, 0);
         Rect bgPadding = new Rect();
         background.getPadding(bgPadding);
@@ -389,12 +434,24 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
         // names)
         int startInset = Math.max(mSectionNamesMargin, mAppsRecyclerView.getMaxScrollbarWidth());
         int topBottomPadding = mRecyclerViewTopBottomPadding;
+        final boolean useScubber = userScrubber();
         if (isRtl) {
             mAppsRecyclerView.setPadding(padding.left + mAppsRecyclerView.getMaxScrollbarWidth(),
-                    topBottomPadding, padding.right + startInset, topBottomPadding);
+                    topBottomPadding, padding.right + startInset, useScubber ?
+                            mScrubberHeight + topBottomPadding : topBottomPadding);
+            if (useScubber) {
+                mScrubberContainerView
+                        .setPadding(padding.left + mAppsRecyclerView.getMaxScrollbarWidth(),
+                                0, padding.right, 0);
+            }
         } else {
             mAppsRecyclerView.setPadding(padding.left + startInset, topBottomPadding,
-                    padding.right + mAppsRecyclerView.getMaxScrollbarWidth(), topBottomPadding);
+                    padding.right + mAppsRecyclerView.getMaxScrollbarWidth(),
+                    useScubber ?  mScrubberHeight + topBottomPadding : topBottomPadding);
+            if (useScubber) {
+                mScrubberContainerView.setPadding(padding.left, 0,
+                        padding.right + mAppsRecyclerView.getMaxScrollbarWidth(), 0);
+            }
         }
 
         // Inset the search bar to fit its bounds above the container
@@ -556,7 +613,9 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
     public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) {
         if (toWorkspace) {
             // Reset the search bar and base recycler view after transitioning home
-            mSearchBarController.reset();
+            if (hasSearchBar()) {
+                mSearchBarController.reset();
+            }
             mAppsRecyclerView.reset();
         }
     }
@@ -614,6 +673,12 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
     public void onSearchResult(String query, ArrayList<ComponentKey> apps) {
         if (apps != null) {
             mApps.setOrderedFilter(apps);
+            if (mGridTheme != GRID_THEME_LIGHT) {
+                mLastGridTheme = mGridTheme;
+                mGridTheme = GRID_THEME_LIGHT;
+                updateBackgroundAndPaddings(true);
+                mAdapter.setGridTheme(mGridTheme);
+            }
             mAdapter.setLastSearchQuery(query);
             mAppsRecyclerView.onSearchResultsChanged();
         }
@@ -623,10 +688,20 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
     public void clearSearchResult() {
         mApps.setOrderedFilter(null);
         mAppsRecyclerView.onSearchResultsChanged();
-
+        if (mLastGridTheme != -1 && mLastGridTheme != GRID_THEME_LIGHT) {
+            mGridTheme = mLastGridTheme;
+            updateBackgroundAndPaddings(true);
+            mAdapter.setGridTheme(mGridTheme);
+            mLastGridTheme = -1;
+        }
         // Clear the search query
         mSearchQueryBuilder.clear();
         mSearchQueryBuilder.clearSpans();
         Selection.setSelection(mSearchQueryBuilder, 0);
     }
+
+    @Override
+    protected BaseRecyclerView getRecyclerView() {
+        return mAppsRecyclerView;
+    }
 }
index 1f95133..a483907 100644 (file)
@@ -24,7 +24,6 @@ import android.graphics.Canvas;
 import android.graphics.Paint;
 import android.graphics.PointF;
 import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
 import android.support.v4.view.accessibility.AccessibilityRecordCompat;
 import android.support.v4.view.accessibility.AccessibilityEventCompat;
 import android.net.Uri;
@@ -69,6 +68,10 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.
     // The message to continue to a market search when there are no filtered results
     public static final int SEARCH_MARKET_VIEW_TYPE = 5;
 
+    private boolean mIconsDimmed = false;
+
+    private int mGridTheme;
+
     /**
      * ViewHolder for each icon.
      */
@@ -142,7 +145,7 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.
     public class GridItemDecoration extends RecyclerView.ItemDecoration {
 
         private static final boolean DEBUG_SECTION_MARGIN = false;
-        private static final boolean FADE_OUT_SECTIONS = false;
+        private static final boolean FADE_OUT_SECTIONS = true;
 
         private HashMap<String, PointF> mCachedSectionBounds = new HashMap<>();
         private Rect mTmpBounds = new Rect();
@@ -349,12 +352,16 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.
     // Section drawing
     @Thunk int mSectionNamesMargin;
     @Thunk int mSectionHeaderOffset;
+    @Thunk int mSectionStrategy;
     @Thunk Paint mSectionTextPaint;
     @Thunk Paint mPredictedAppsDividerPaint;
 
+    private int mIconSize;
+    private int mAllAppsTextColor;
+
     public AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps,
             View.OnTouchListener touchListener, View.OnClickListener iconClickListener,
-            View.OnLongClickListener iconLongClickListener) {
+            View.OnLongClickListener iconLongClickListener, int sectionStrategy, int gridTheme) {
         Resources res = launcher.getResources();
         mLauncher = launcher;
         mApps = apps;
@@ -367,13 +374,31 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.
         mTouchListener = touchListener;
         mIconClickListener = iconClickListener;
         mIconLongClickListener = iconLongClickListener;
-        mSectionNamesMargin = res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin);
+        mSectionStrategy = sectionStrategy;
+        mGridTheme = gridTheme;
+        mSectionNamesMargin = mSectionStrategy ==
+                AllAppsContainerView.SECTION_STRATEGY_GRID ?
+                res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin) :
+                res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin_with_sections);
+
+        mIconSize = mSectionStrategy ==
+                AllAppsContainerView.SECTION_STRATEGY_GRID ?
+                res.getDimensionPixelSize(R.dimen.all_apps_icon_size_grid) :
+                res.getDimensionPixelSize(R.dimen.all_apps_icon_size_ragged);
+
+        mAllAppsTextColor = mGridTheme == AllAppsContainerView.GRID_THEME_DARK ?
+                res.getColor(R.color.quantum_panel_text_color_dark) :
+                res.getColor(R.color.quantum_panel_text_color);
+
         mSectionHeaderOffset = res.getDimensionPixelSize(R.dimen.all_apps_grid_section_y_offset);
 
         mSectionTextPaint = new Paint();
         mSectionTextPaint.setTextSize(res.getDimensionPixelSize(
                 R.dimen.all_apps_grid_section_text_size));
-        mSectionTextPaint.setColor(res.getColor(R.color.all_apps_grid_section_text_color));
+        int sectionTextColorId = mGridTheme == AllAppsContainerView.GRID_THEME_DARK ?
+                R.color.all_apps_grid_section_text_color_dark :
+                R.color.all_apps_grid_section_text_color;
+        mSectionTextPaint.setColor(res.getColor(sectionTextColorId));
         mSectionTextPaint.setAntiAlias(true);
 
         mPredictedAppsDividerPaint = new Paint();
@@ -408,6 +433,19 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.
         mIsRtl = rtl;
     }
 
+    public void setSectionStrategy(int sectionStrategy) {
+        Resources res = mLauncher.getResources();
+        mSectionStrategy = sectionStrategy;
+        mSectionNamesMargin = mSectionStrategy ==
+                AllAppsContainerView.SECTION_STRATEGY_GRID ?
+                res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin) :
+                res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin_with_sections);
+        mIconSize = mSectionStrategy ==
+                AllAppsContainerView.SECTION_STRATEGY_GRID ?
+                res.getDimensionPixelSize(R.dimen.all_apps_icon_size_grid) :
+                res.getDimensionPixelSize(R.dimen.all_apps_icon_size_ragged);
+    }
+
     /**
      * Sets the last search query that was made, used to show when there are no results and to also
      * seed the intent for searching the market.
@@ -501,12 +539,17 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.
             case ICON_VIEW_TYPE: {
                 AppInfo info = mApps.getAdapterItems().get(position).appInfo;
                 BubbleTextView icon = (BubbleTextView) holder.mContent;
+                icon.setIconSize(mIconSize);
+                icon.setTextColor(mAllAppsTextColor);
                 icon.applyFromApplicationInfo(info);
+                icon.setFastScrollDimmed(mIconsDimmed, !mIconsDimmed);
                 break;
             }
             case PREDICTION_ICON_VIEW_TYPE: {
                 AppInfo info = mApps.getAdapterItems().get(position).appInfo;
                 BubbleTextView icon = (BubbleTextView) holder.mContent;
+                icon.setIconSize(mIconSize);
+                icon.setTextColor(mAllAppsTextColor);
                 icon.applyFromApplicationInfo(info);
                 break;
             }
@@ -542,6 +585,25 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.
         return item.viewType;
     }
 
+    public void setIconsDimmed(boolean iconsDimmed) {
+        if (mIconsDimmed != iconsDimmed) {
+            mIconsDimmed = iconsDimmed;
+            notifyDataSetChanged();
+        }
+    }
+
+    public void setGridTheme(int gridTheme) {
+        mGridTheme = gridTheme;
+        int sectionTextColorId = mGridTheme == AllAppsContainerView.GRID_THEME_DARK ?
+                R.color.all_apps_grid_section_text_color_dark :
+                R.color.all_apps_grid_section_text_color;
+        mSectionTextPaint.setColor(mLauncher.getResources().getColor(sectionTextColorId));
+        Resources res = mLauncher.getResources();
+        mAllAppsTextColor = mGridTheme == AllAppsContainerView.GRID_THEME_DARK ?
+                res.getColor(R.color.all_apps_grid_section_text_color_dark) :
+                res.getColor(R.color.all_apps_grid_section_text_color);
+    }
+
     /**
      * Creates a new market search intent.
      */
index 2f66e2c..6c853a0 100644 (file)
@@ -15,7 +15,6 @@
  */
 package com.android.launcher3.allapps;
 
-import android.animation.ObjectAnimator;
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Canvas;
@@ -34,6 +33,7 @@ import com.android.launcher3.Stats;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.util.Thunk;
 
+import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -50,9 +50,15 @@ public class AllAppsRecyclerView extends BaseRecyclerView
 
     private AlphabeticalAppsList mApps;
     private int mNumAppsPerRow;
+    private int mSectionStrategy = AllAppsContainerView.SECTION_STRATEGY_RAGGED;
+
+    private boolean mFocusSection = false;
 
     @Thunk BaseRecyclerViewFastScrollBar.FastScrollFocusableView mLastFastScrollFocusedView;
+    @Thunk ArrayList<BaseRecyclerViewFastScrollBar.FastScrollFocusableView>
+            mLastFastScrollFocusedViews = new ArrayList();
     @Thunk int mPrevFastScrollFocusedPosition;
+    @Thunk AlphabeticalAppsList.SectionInfo mPrevFastScrollFocusedSection;
     @Thunk int mFastScrollFrameIndex;
     @Thunk final int[] mFastScrollFrames = new int[10];
 
@@ -81,7 +87,9 @@ public class AllAppsRecyclerView extends BaseRecyclerView
         super(context, attrs, defStyleAttr);
 
         Resources res = getResources();
-        mScrollbar.setDetachThumbOnFastScroll();
+        if (mUseScrollbar) {
+            mScrollbar.setDetachThumbOnFastScroll();
+        }
         mEmptySearchBackgroundTopOffset = res.getDimensionPixelSize(
                 R.dimen.all_apps_empty_search_bg_top_offset);
     }
@@ -109,13 +117,20 @@ public class AllAppsRecyclerView extends BaseRecyclerView
         pool.setMaxRecycledViews(AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE, approxRows);
     }
 
+    public void setSectionStrategy(int sectionStrategy) {
+        mSectionStrategy = sectionStrategy;
+        mFocusSection = mSectionStrategy == AllAppsContainerView.SECTION_STRATEGY_RAGGED;
+    }
+
     /**
      * Scrolls this recycler view to the top.
      */
     public void scrollToTop() {
-        // Ensure we reattach the scrollbar if it was previously detached while fast-scrolling
-        if (mScrollbar.isThumbDetached()) {
-            mScrollbar.reattachThumbToScroll();
+        if (mUseScrollbar) {
+            // Ensure we reattach the scrollbar if it was previously detached while fast-scrolling
+            if (mScrollbar.isThumbDetached()) {
+                mScrollbar.reattachThumbToScroll();
+            }
         }
         scrollToPosition(0);
     }
@@ -235,10 +250,18 @@ public class AllAppsRecyclerView extends BaseRecyclerView
         }
 
         if (mPrevFastScrollFocusedPosition != lastInfo.fastScrollToItem.position) {
+            if (mFocusSection) {
+                setSectionFastScrollDimmed(mPrevFastScrollFocusedPosition, true, true);
+            } else if (mLastFastScrollFocusedView != null){
+                mLastFastScrollFocusedView.setFastScrollDimmed(true, true);
+            }
             mPrevFastScrollFocusedPosition = lastInfo.fastScrollToItem.position;
-
-            // Reset the last focused view
-            if (mLastFastScrollFocusedView != null) {
+            mPrevFastScrollFocusedSection =
+                    getSectionInfoForPosition(lastInfo.fastScrollToItem.position);
+            // Reset the last focused section
+            if (mFocusSection) {
+                clearSectionFocusedItems();
+            } else if (mLastFastScrollFocusedView != null) {
                 mLastFastScrollFocusedView.setFastScrollFocused(false, true);
                 mLastFastScrollFocusedView = null;
             }
@@ -246,12 +269,17 @@ public class AllAppsRecyclerView extends BaseRecyclerView
             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);
+                if (mFocusSection) {
+                    setSectionFastScrollFocused(mPrevFastScrollFocusedPosition);
+                } else {
+                    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");
@@ -264,11 +292,14 @@ public class AllAppsRecyclerView extends BaseRecyclerView
     public void onFastScrollCompleted() {
         super.onFastScrollCompleted();
         // Reset and clean up the last focused view
-        if (mLastFastScrollFocusedView != null) {
+        if (mFocusSection) {
+            clearSectionFocusedItems();
+        } else if (mLastFastScrollFocusedView != null) {
             mLastFastScrollFocusedView.setFastScrollFocused(false, true);
             mLastFastScrollFocusedView = null;
         }
         mPrevFastScrollFocusedPosition = -1;
+        mPrevFastScrollFocusedSection = null;
     }
 
     /**
@@ -276,6 +307,9 @@ public class AllAppsRecyclerView extends BaseRecyclerView
      */
     @Override
     public void onUpdateScrollbar(int dy) {
+        if (!mUseScrollbar) {
+            return;
+        }
         List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
 
         // Skip early if there are no items or we haven't been measured
@@ -294,7 +328,8 @@ public class AllAppsRecyclerView extends BaseRecyclerView
 
         // Only show the scrollbar if there is height to be scrolled
         int availableScrollBarHeight = getAvailableScrollBarHeight();
-        int availableScrollHeight = getAvailableScrollHeight(mApps.getNumAppRows(), mScrollPosState.rowHeight);
+        int availableScrollHeight = getAvailableScrollHeight(mApps.getNumAppRows(),
+                mScrollPosState.rowHeight);
         if (availableScrollHeight <= 0) {
             mScrollbar.setThumbOffset(-1, -1);
             return;
@@ -354,6 +389,97 @@ public class AllAppsRecyclerView extends BaseRecyclerView
         }
     }
 
+    @Override
+    public String scrollToSection(String sectionName) {
+        List<AlphabeticalAppsList.FastScrollSectionInfo> scrollSectionInfos =
+                mApps.getFastScrollerSections();
+        if (scrollSectionInfos != null) {
+            for (int i = 0; i < scrollSectionInfos.size(); i++) {
+                AlphabeticalAppsList.FastScrollSectionInfo info = scrollSectionInfos.get(i);
+                if (info.sectionName.equals(sectionName))  {
+                    scrollToPositionAtProgress(info.touchFraction);
+                    return info.sectionName;
+                }
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public String[] getSectionNames() {
+        List<AlphabeticalAppsList.FastScrollSectionInfo> scrollSectionInfos =
+                mApps.getFastScrollerSections();
+        if (scrollSectionInfos != null) {
+            String[] sectionNames = new String[scrollSectionInfos.size()];
+            for (int i = 0; i < scrollSectionInfos.size(); i++) {
+                AlphabeticalAppsList.FastScrollSectionInfo info = scrollSectionInfos.get(i);
+                sectionNames[i] = info.sectionName;
+            }
+
+        return sectionNames;
+        }
+        return new String[0];
+    }
+
+    private AlphabeticalAppsList.SectionInfo getSectionInfoForPosition(int position) {
+        List<AlphabeticalAppsList.SectionInfo> sections =
+                mApps.getSections();
+        for (AlphabeticalAppsList.SectionInfo section : sections) {
+           if (section.firstAppItem.position == position) {
+               return section;
+           }
+        }
+        return null;
+    }
+
+    private void setSectionFastScrollFocused(int position) {
+        if (mPrevFastScrollFocusedSection != null) {
+            for (int i = 0; i < mPrevFastScrollFocusedSection.numApps; i++) {
+                int sectionPosition = position+i;
+                final ViewHolder vh = findViewHolderForAdapterPosition(sectionPosition);
+                if (vh != null &&
+                        vh.itemView instanceof
+                                BaseRecyclerViewFastScrollBar.FastScrollFocusableView) {
+                    final BaseRecyclerViewFastScrollBar.FastScrollFocusableView view =
+                            (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView;
+                    view.setFastScrollFocused(true, true);
+                    mLastFastScrollFocusedViews.add(view);
+                }
+            }
+        }
+    }
+
+    private void setSectionFastScrollDimmed(int position, boolean dimmed, boolean animate) {
+        if (mPrevFastScrollFocusedSection != null) {
+            for (int i = 0; i < mPrevFastScrollFocusedSection.numApps; i++) {
+                int sectionPosition = position+i;
+                final ViewHolder vh = findViewHolderForAdapterPosition(sectionPosition);
+                if (vh != null &&
+                        vh.itemView instanceof
+                                BaseRecyclerViewFastScrollBar.FastScrollFocusableView) {
+                    final BaseRecyclerViewFastScrollBar.FastScrollFocusableView view =
+                            (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView;
+                    view.setFastScrollDimmed(dimmed, animate);
+                }
+            }
+        }
+    }
+
+    private void clearSectionFocusedItems() {
+        final int N = mLastFastScrollFocusedViews.size();
+        for (int i = 0; i < N; i++) {
+            BaseRecyclerViewFastScrollBar.FastScrollFocusableView view =
+                    mLastFastScrollFocusedViews.get(i);
+            view.setFastScrollFocused(false, true);
+        }
+        mLastFastScrollFocusedViews.clear();
+    }
+
+    @Override
+    public void setFastScrollDragging(boolean dragging) {
+        ((AllAppsGridAdapter) getAdapter()).setIconsDimmed(dragging);
+    }
+
     /**
      * This runnable runs a single frame of the smooth scroll animation and posts the next frame
      * if necessary.
@@ -362,18 +488,27 @@ public class AllAppsRecyclerView extends BaseRecyclerView
         @Override
         public void run() {
             if (mFastScrollFrameIndex < mFastScrollFrames.length) {
+                if (mFocusSection) {
+                    setSectionFastScrollDimmed(mPrevFastScrollFocusedPosition, false, true);
+                }
                 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);
+                if (mFocusSection) {
+                    setSectionFastScrollFocused(mPrevFastScrollFocusedPosition);
+                } 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);
+                        mLastFastScrollFocusedView.setFastScrollDimmed(false, true);
+                    }
                 }
             }
         }
index 14e2a18..d5ebdab 100644 (file)
@@ -17,9 +17,7 @@ package com.android.launcher3.allapps;
 
 import android.content.Context;
 import android.graphics.Bitmap;
-import android.graphics.Canvas;
 import android.util.AttributeSet;
-import android.view.View;
 import android.view.ViewGroup;
 import android.widget.FrameLayout;
 import com.android.launcher3.BubbleTextView;
index 0c6ea31..268e26e 100644 (file)
@@ -29,6 +29,7 @@ import android.view.View;
 import android.widget.Toast;
 
 import com.android.launcher3.BaseContainerView;
+import com.android.launcher3.BaseRecyclerView;
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.DeleteDropTarget;
 import com.android.launcher3.DeviceProfile;
@@ -366,4 +367,9 @@ public class WidgetsContainerView extends BaseContainerView
         }
         return mWidgetPreviewLoader;
     }
+
+    @Override
+    protected BaseRecyclerView getRecyclerView() {
+        return mView;
+    }
 }
\ No newline at end of file
index 884bdc4..ac32f15 100644 (file)
@@ -126,20 +126,34 @@ public class WidgetsRecyclerView extends BaseRecyclerView {
         // Skip early if there are no widgets.
         int rowCount = mWidgets.getPackageSize();
         if (rowCount == 0) {
-            mScrollbar.setThumbOffset(-1, -1);
+            if (mUseScrollbar) {
+                mScrollbar.setThumbOffset(-1, -1);
+            }
             return;
         }
 
         // Skip early if, there no child laid out in the container.
         getCurScrollState(mScrollPosState);
         if (mScrollPosState.rowIndex < 0) {
-            mScrollbar.setThumbOffset(-1, -1);
+            if (mUseScrollbar) {
+                mScrollbar.setThumbOffset(-1, -1);
+            }
             return;
         }
 
         synchronizeScrollBarThumbOffsetToViewScroll(mScrollPosState, rowCount);
     }
 
+    @Override
+    public String scrollToSection(String sectionName) {
+        return null;
+    }
+
+    @Override
+    public String[] getSectionNames() {
+        return new String[0];
+    }
+
     /**
      * Returns the current scroll state.
      */