OSDN Git Service

Adding app grid layout with fastscroller.
authorWinson Chung <winsonc@google.com>
Tue, 10 Mar 2015 23:28:47 +0000 (16:28 -0700)
committerWinson Chung <winsonc@google.com>
Fri, 13 Mar 2015 01:57:02 +0000 (18:57 -0700)
- Adding filtering and using alphabetic index for app grouping.

Change-Id: I745b644fa8f90f5ff24a8642ac377ef1c65d8aff

25 files changed:
Android.mk
res/drawable/apps_list_fastscroll_bg.xml [new file with mode: 0644]
res/drawable/apps_list_scrollbar_thumb.xml [new file with mode: 0644]
res/drawable/apps_list_search_bg.xml [new file with mode: 0644]
res/layout-sw600dp/apps_view.xml
res/layout/apps_grid_row_icon_view.xml
res/layout/apps_grid_row_view.xml [deleted file]
res/layout/apps_list_reveal_view.xml
res/layout/apps_list_row_icon_view.xml
res/layout/apps_list_row_view.xml
res/layout/apps_list_view.xml
res/layout/apps_view.xml
res/values-sw600dp/dimens.xml
res/values/attrs.xml
res/values/colors.xml
res/values/dimens.xml
res/values/strings.xml
src/com/android/launcher3/AlphabeticalAppsList.java [new file with mode: 0644]
src/com/android/launcher3/AppsContainerRecyclerView.java [new file with mode: 0644]
src/com/android/launcher3/AppsContainerView.java
src/com/android/launcher3/AppsGridAdapter.java [new file with mode: 0644]
src/com/android/launcher3/AppsListAdapter.java [new file with mode: 0644]
src/com/android/launcher3/BubbleTextView.java
src/com/android/launcher3/DeviceProfile.java
src/com/android/launcher3/compat/AlphabeticIndexCompat.java [new file with mode: 0644]

index 5267469..3853808 100644 (file)
@@ -37,6 +37,9 @@ LOCAL_PROTOC_OPTIMIZE_TYPE := nano
 LOCAL_PROTOC_FLAGS := --proto_path=$(LOCAL_PATH)/protos/
 
 LOCAL_SDK_VERSION := 21
+LOCAL_STATIC_JAVA_LIBRARIES := \
+        android-support-v4 \
+        android-support-v7-recyclerview
 
 LOCAL_PACKAGE_NAME := Launcher3
 #LOCAL_CERTIFICATE := shared
diff --git a/res/drawable/apps_list_fastscroll_bg.xml b/res/drawable/apps_list_fastscroll_bg.xml
new file mode 100644 (file)
index 0000000..4ec1848
--- /dev/null
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2015 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+    <solid android:color="@color/apps_view_scrollbar_thumb_color" />
+    <size
+        android:width="48dp"
+        android:height="48dp" />
+    <corners android:radius="4dp" />
+</shape>
\ No newline at end of file
diff --git a/res/drawable/apps_list_scrollbar_thumb.xml b/res/drawable/apps_list_scrollbar_thumb.xml
new file mode 100644 (file)
index 0000000..ddd65b2
--- /dev/null
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2015 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+    <solid android:color="@color/apps_view_scrollbar_thumb_color" />
+    <size android:width="4dp"/>
+</shape>
\ No newline at end of file
diff --git a/res/drawable/apps_list_search_bg.xml b/res/drawable/apps_list_search_bg.xml
new file mode 100644 (file)
index 0000000..eda33a9
--- /dev/null
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2015 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+    <solid android:color="#ffffff" />
+    <corners
+        android:topLeftRadius="3dp"
+        android:topRightRadius="3dp" />
+</shape>
\ No newline at end of file
index 1f773b3..3bb6ec5 100644 (file)
     android:descendantFocusability="afterDescendants">
     <include
         layout="@layout/apps_list_reveal_view"
-        android:layout_width="420dp"
-        android:layout_height="match_parent"
+        android:layout_width="@dimen/apps_container_width"
+        android:layout_height="540dp"
         android:layout_gravity="center" />
     <include
         layout="@layout/apps_list_view"
-        android:layout_width="420dp"
-        android:layout_height="match_parent"
+        android:layout_width="@dimen/apps_container_width"
+        android:layout_height="540dp"
         android:layout_gravity="center" />
 </com.android.launcher3.AppsContainerView>
\ No newline at end of file
index 11c8eeb..81e74b9 100644 (file)
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-
 <com.android.launcher3.BubbleTextView
     xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:launcher="http://schemas.android.com/apk/res-auto"
     style="@style/WorkspaceIcon.AppsCustomize"
-    android:id="@+id/application_icon"
+    android:id="@+id/icon"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_gravity="left|center_vertical"
+    android:paddingTop="8dp"
+    android:paddingBottom="8dp"
     android:focusable="true"
-    android:background="@drawable/focusable_view_bg" />
+    android:background="@drawable/focusable_view_bg"
+    launcher:deferShadowGeneration="true" />
+
diff --git a/res/layout/apps_grid_row_view.xml b/res/layout/apps_grid_row_view.xml
deleted file mode 100644 (file)
index bce43bc..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="@dimen/apps_view_row_height"
-    android:paddingTop="12dp"
-    android:paddingBottom="12dp"
-    android:orientation="horizontal"
-    android:focusable="true"
-    android:background="@drawable/focusable_view_bg"
-    android:descendantFocusability="afterDescendants">
-    <TextView
-        android:id="@+id/section"
-        android:layout_width="48dp"
-        android:layout_height="wrap_content"
-        android:layout_gravity="center_vertical"
-        android:paddingRight="8dp"
-        android:paddingBottom="12dp"
-        android:gravity="right"
-        android:textColor="#1ca195"
-        android:textSize="16sp"
-        android:textAllCaps="true"
-        android:focusable="false" />
-</LinearLayout>
\ No newline at end of file
index 4a26787..19e462b 100644 (file)
@@ -15,7 +15,7 @@
 -->
 <FrameLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/all_apps_transition_overlay"
+    android:id="@+id/apps_view_transition_overlay"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:layout_gravity="center"
index 607af9b..867dbdc 100644 (file)
     style="@style/WorkspaceIcon.AppsCustomize"
     android:id="@+id/application_icon"
     android:layout_width="match_parent"
-    android:layout_height="wrap_content"
+    android:layout_height="match_parent"
     android:focusable="true"
     android:background="@drawable/focusable_view_bg"
     launcher:iconPaddingOverride="24dp"
     launcher:textSizeOverride="16dp"
-    launcher:layoutHorizontal="true" />
+    launcher:layoutHorizontal="true"
+    launcher:deferShadowGeneration="true" />
index c4dcd00..83c175b 100644 (file)
@@ -26,9 +26,8 @@
         android:layout_width="64dp"
         android:layout_height="match_parent"
         android:paddingLeft="16dp"
-        android:gravity="left|center_vertical"
-        android:textColor="#009688"
-        android:textSize="24sp"
-        android:textAllCaps="true"
+        android:gravity="start|center_vertical"
+        android:textColor="@color/apps_view_section_text_color"
+        android:textSize="@dimen/apps_view_section_text_size"
         android:focusable="false" />
 </LinearLayout>
\ No newline at end of file
index b1b0f31..dfb2fc4 100644 (file)
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<ListView
+<LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/apps_list"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:layout_gravity="center"
-    android:paddingTop="12dp"
-    android:paddingBottom="12dp"
-    android:clipToPadding="false"
-    android:scrollbars="vertical"
+    android:orientation="vertical"
     android:elevation="15dp"
     android:background="@drawable/apps_list_bg"
-    android:visibility="gone"
-    android:focusable="true"
-    android:descendantFocusability="afterDescendants" />
\ No newline at end of file
+    android:visibility="gone">
+    <EditText
+        android:id="@+id/app_search_box"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:padding="16dp"
+        android:hint="@string/apps_view_search_bar_hint"
+        android:maxLines="1"
+        android:singleLine="true"
+        android:scrollHorizontally="true"
+        android:gravity="fill_horizontal"
+        android:textSize="16sp"
+        android:textColor="#4c4c4c"
+        android:textColorHint="#9c9c9c"
+        android:imeOptions="flagNoExtractUi"
+        android:background="@drawable/apps_list_search_bg"
+        android:elevation="4dp" />
+    <com.android.launcher3.AppsContainerRecyclerView
+        android:id="@+id/apps_list_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_gravity="center"
+        android:paddingTop="12dp"
+        android:paddingBottom="12dp"
+        android:clipToPadding="false"
+        android:scrollbars="vertical"
+        android:scrollbarThumbVertical="@drawable/apps_list_scrollbar_thumb"
+        android:focusable="true"
+        android:descendantFocusability="afterDescendants" />
+</LinearLayout>
\ No newline at end of file
index 19ad3d2..c1bae63 100644 (file)
@@ -19,7 +19,7 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:padding="8dp"
-    android:background="#22000000"
+    android:background="@drawable/apps_customize_bg"
     android:descendantFocusability="afterDescendants">
     <include
         layout="@layout/apps_list_reveal_view" />
index f7ad0c4..d907587 100644 (file)
@@ -18,6 +18,7 @@
     <dimen name="app_icon_size">64dp</dimen>
 
 <!-- Apps view -->
+    <dimen name="apps_container_width">480dp</dimen>
     <dimen name="apps_view_row_height">76dp</dimen>
 
 <!-- AppsCustomize -->
index 4e7c592..845b182 100644 (file)
@@ -24,6 +24,7 @@
         <attr name="iconSizeOverride" format="dimension" />
         <attr name="iconPaddingOverride" format="dimension" />
         <attr name="textSizeOverride" format="dimension" />
+        <attr name="deferShadowGeneration" format="boolean" />
     </declare-styleable>
 
     <!-- Page Indicator specific attributes. -->
index 2daf9fe..590a887 100644 (file)
@@ -36,4 +36,8 @@
     <color name="outline_color">#FFFFFFFF</color>
     <color name="widget_text_panel">#FF374248</color>
 
+<!-- Apps view -->
+    <color name="apps_view_scrollbar_thumb_color">#009688</color>
+    <color name="apps_view_section_text_color">#009688</color>
+
 </resources>
index 013bd92..9b4c560 100644 (file)
     <dimen name="toolbar_button_horizontal_padding">12dip</dimen>
 
 <!-- Apps view -->
+    <dimen name="apps_container_width">0dp</dimen>
+    <dimen name="apps_grid_view_start_margin">52dp</dimen>
     <dimen name="apps_view_row_height">64dp</dimen>
+    <dimen name="apps_view_section_text_size">24sp</dimen>
+    <dimen name="apps_view_fast_scroll_gutter_size">48dp</dimen>
+    <dimen name="apps_view_fast_scroll_popup_size">64dp</dimen>
+    <dimen name="apps_view_fast_scroll_text_size">48dp</dimen>
 
 <!-- AllApps/Customize/AppsCustomize -->
     <!-- The height of the tab bar - if this changes, we should update the
index 74b8814..0d113db 100644 (file)
          drop if there are multiple choices. [CHAR_LIMIT=35] -->
     <string name="external_drop_widget_pick_title">Choose widget to create</string>
 
+    <!-- Apps view -->
+    <!-- Search bar text in the apps view. [CHAR_LIMIT=50] -->
+    <string name="apps_view_search_bar_hint">Search Apps</string>
+
     <!-- Folders -->
     <skip />
     <!-- Label of Folder name field in Rename folder dialog box -->
diff --git a/src/com/android/launcher3/AlphabeticalAppsList.java b/src/com/android/launcher3/AlphabeticalAppsList.java
new file mode 100644 (file)
index 0000000..2847afc
--- /dev/null
@@ -0,0 +1,242 @@
+package com.android.launcher3;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.support.v7.widget.RecyclerView;
+import com.android.launcher3.compat.AlphabeticIndexCompat;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+
+/**
+ * The alphabetically sorted list of applications.
+ */
+public class AlphabeticalAppsList {
+
+    /**
+     * Info about a section in the alphabetic list
+     */
+    public class SectionInfo {
+        public String sectionName;
+        public int numAppsInSection;
+    }
+
+    /**
+     * A filter interface to limit the set of applications in the apps list.
+     */
+    public interface Filter {
+        public boolean retainApp(AppInfo info);
+    }
+
+    // Hack to force RecyclerView to break sections
+    public static final AppInfo SECTION_BREAK_INFO = null;
+
+    private List<AppInfo> mApps = new ArrayList<>();
+    private List<AppInfo> mFilteredApps = new ArrayList<>();
+    private List<AppInfo> mSectionedFilteredApps = new ArrayList<>();
+    private List<SectionInfo> mSections = new ArrayList<>();
+    private RecyclerView.Adapter mAdapter;
+    private Filter mFilter;
+    private AlphabeticIndexCompat mIndexer;
+
+    public AlphabeticalAppsList(Context context) {
+        mIndexer = new AlphabeticIndexCompat(context);
+    }
+
+    /**
+     * Sets the adapter to notify when this dataset changes.
+     */
+    public void setAdapter(RecyclerView.Adapter adapter) {
+        mAdapter = adapter;
+    }
+
+    /**
+     * Returns sections of all the current filtered applications.
+     */
+    public List<SectionInfo> getSections() {
+        return mSections;
+    }
+
+    /**
+     * Returns the current filtered list of applications broken down into their sections.
+     */
+    public List<AppInfo> getApps() {
+        return mSectionedFilteredApps;
+    }
+
+    /**
+     * Returns the current filtered list of applications.
+     */
+    public List<AppInfo> getAppsWithoutSectionBreaks() {
+        return mFilteredApps;
+    }
+
+    /**
+     * Returns the section name for the application.
+     */
+    public String getSectionNameForApp(AppInfo info) {
+        String title = info.title.toString();
+        String sectionName = mIndexer.getBucketLabel(mIndexer.getBucketIndex(title));
+        return sectionName;
+    }
+
+    /**
+     * Returns the indexer for this locale.
+     */
+    public AlphabeticIndexCompat getIndexer() {
+        return mIndexer;
+    }
+
+    /**
+     * Sets the current filter for this list of apps.
+     */
+    public void setFilter(Filter f) {
+        mFilter = f;
+        onAppsUpdated();
+        mAdapter.notifyDataSetChanged();
+    }
+
+    /**
+     * Sets the current set of apps.
+     */
+    public void setApps(List<AppInfo> apps) {
+        Collections.sort(apps, LauncherModel.getAppNameComparator());
+        mApps.clear();
+        mApps.addAll(apps);
+        onAppsUpdated();
+        mAdapter.notifyDataSetChanged();
+    }
+
+    /**
+     * Adds new apps to the list.
+     */
+    public void addApps(List<AppInfo> apps) {
+        // We add it in place, in alphabetical order
+        for (AppInfo info : apps) {
+            addApp(info);
+        }
+    }
+
+    /**
+     * Updates existing apps in the list
+     */
+    public void updateApps(List<AppInfo> apps) {
+        for (AppInfo info : apps) {
+            int index = mApps.indexOf(info);
+            if (index != -1) {
+                mApps.set(index, info);
+                onAppsUpdated();
+                mAdapter.notifyItemChanged(index);
+            } else {
+                addApp(info);
+            }
+        }
+    }
+
+    /**
+     * Removes some apps from the list.
+     */
+    public void removeApps(List<AppInfo> apps) {
+        for (AppInfo info : apps) {
+            int removeIndex = findAppByComponent(mApps, info);
+            if (removeIndex != -1) {
+                int sectionedIndex = mSectionedFilteredApps.indexOf(info);
+                int numAppsInSection = numAppsInSection(info);
+                mApps.remove(removeIndex);
+                onAppsUpdated();
+                if (numAppsInSection == 1) {
+                    // Remove the section and the icon
+                    mAdapter.notifyItemRemoved(sectionedIndex - 1);
+                    mAdapter.notifyItemRemoved(sectionedIndex - 1);
+                } else {
+                    mAdapter.notifyItemRemoved(sectionedIndex);
+                }
+            }
+        }
+    }
+
+    /**
+     * Finds the index of an app given a target AppInfo.
+     */
+    private int findAppByComponent(List<AppInfo> apps, AppInfo targetInfo) {
+        ComponentName targetComponent = targetInfo.intent.getComponent();
+        int length = apps.size();
+        for (int i = 0; i < length; ++i) {
+            AppInfo info = apps.get(i);
+            if (info.user.equals(info.user)
+                    && info.intent.getComponent().equals(targetComponent)) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * Implementation to actually add an app to the alphabetic list
+     */
+    private void addApp(AppInfo info) {
+        Comparator<AppInfo> appNameComparator = LauncherModel.getAppNameComparator();
+        int index = Collections.binarySearch(mApps, info, appNameComparator);
+        if (index < 0) {
+            mApps.add(-(index + 1), info);
+            onAppsUpdated();
+
+            int sectionedIndex = mSectionedFilteredApps.indexOf(info);
+            int numAppsInSection = numAppsInSection(info);
+            if (numAppsInSection == 1) {
+                // New section added along with icon
+                mAdapter.notifyItemInserted(sectionedIndex - 1);
+                mAdapter.notifyItemInserted(sectionedIndex - 1);
+            } else {
+                mAdapter.notifyItemInserted(sectionedIndex);
+            }
+        }
+    }
+
+    /**
+     * Returns the number of apps in the section that the given info is in.
+     */
+    private int numAppsInSection(AppInfo info) {
+        int appIndex = mFilteredApps.indexOf(info);
+        int appCount = 0;
+        for (SectionInfo section : mSections) {
+            if (appCount + section.numAppsInSection > appIndex) {
+                return section.numAppsInSection;
+            }
+            appCount += section.numAppsInSection;
+        }
+        return 1;
+    }
+
+    /**
+     * Updates internals when the set of apps are updated.
+     */
+    private void onAppsUpdated() {
+        // Recreate the filtered apps
+        mFilteredApps.clear();
+        for (AppInfo info : mApps) {
+            if (mFilter == null || mFilter.retainApp(info)) {
+                mFilteredApps.add(info);
+            }
+        }
+
+        // Section the apps (for convenience for the grid layout)
+        mSections.clear();
+        mSectionedFilteredApps.clear();
+        SectionInfo lastSectionInfo = null;
+        for (AppInfo info : mFilteredApps) {
+            String sectionName = getSectionNameForApp(info);
+            if (lastSectionInfo == null || !lastSectionInfo.sectionName.equals(sectionName)) {
+                lastSectionInfo = new SectionInfo();
+                lastSectionInfo.sectionName = sectionName;
+                mSectionedFilteredApps.add(SECTION_BREAK_INFO);
+                mSections.add(lastSectionInfo);
+            }
+            lastSectionInfo.numAppsInSection++;
+            mSectionedFilteredApps.add(info);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/AppsContainerRecyclerView.java b/src/com/android/launcher3/AppsContainerRecyclerView.java
new file mode 100644 (file)
index 0000000..2280e99
--- /dev/null
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3;
+
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+import java.util.List;
+
+/**
+ * A RecyclerView with custom fastscroll support.  This is the main container for the all apps
+ * icons.
+ */
+public class AppsContainerRecyclerView extends RecyclerView
+        implements RecyclerView.OnItemTouchListener {
+
+    private AlphabeticalAppsList mApps;
+    private int mNumAppsPerRow;
+
+    private Drawable mFastScrollerBg;
+    private boolean mDraggingFastScroller;
+    private String mFastScrollSectionName;
+    private Paint mFastScrollTextPaint;
+    private Rect mFastScrollTextBounds = new Rect();
+    private float mFastScrollAlpha;
+    private int mDownX;
+    private int mDownY;
+    private int mLastX;
+    private int mLastY;
+    private int mGutterSize;
+
+    public AppsContainerRecyclerView(Context context) {
+        this(context, null);
+    }
+
+    public AppsContainerRecyclerView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public AppsContainerRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public AppsContainerRecyclerView(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr);
+
+        Resources res = context.getResources();
+        int fastScrollerSize = res.getDimensionPixelSize(R.dimen.apps_view_fast_scroll_popup_size);
+        mFastScrollerBg = res.getDrawable(R.drawable.apps_list_fastscroll_bg);
+        mFastScrollerBg.setBounds(0, 0, fastScrollerSize, fastScrollerSize);
+        mFastScrollTextPaint = new Paint();
+        mFastScrollTextPaint.setColor(Color.WHITE);
+        mFastScrollTextPaint.setAntiAlias(true);
+        mFastScrollTextPaint.setTextSize(res.getDimensionPixelSize(
+                R.dimen.apps_view_fast_scroll_text_size));
+        mGutterSize = res.getDimensionPixelSize(R.dimen.apps_view_fast_scroll_gutter_size);
+        setFastScrollerAlpha(getFastScrollerAlpha());
+    }
+
+    /**
+     * Sets the list of apps in this view, used to determine the fastscroll position.
+     */
+    public void setApps(AlphabeticalAppsList apps) {
+        mApps = apps;
+    }
+
+    /**
+     * Sets the number of apps per row in this recycler view.
+     */
+    public void setNumAppsPerRow(int rowSize) {
+        mNumAppsPerRow = rowSize;
+    }
+
+    /**
+     * Sets the fast scroller alpha.
+     */
+    public void setFastScrollerAlpha(float alpha) {
+        mFastScrollAlpha = alpha;
+        invalidateFastScroller();
+    }
+
+    /**
+     * Gets the fast scroller alpha.
+     */
+    public float getFastScrollerAlpha() {
+        return mFastScrollAlpha;
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        addOnItemTouchListener(this);
+    }
+
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        super.dispatchDraw(canvas);
+
+        if (mFastScrollAlpha > 0f) {
+            boolean isRtl = (getResources().getConfiguration().getLayoutDirection() ==
+                    LAYOUT_DIRECTION_RTL);
+            Rect bgBounds = mFastScrollerBg.getBounds();
+            int restoreCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
+            int x;
+            if (isRtl) {
+                x = getPaddingLeft() + getScrollBarSize();
+            } else {
+                x = getWidth() - getPaddingRight() - getScrollBarSize() - bgBounds.width();
+            }
+            int y = mLastY - bgBounds.height() / 2;
+            y = Math.max(getPaddingTop(), Math.min(y, getHeight() - getPaddingBottom() -
+                    bgBounds.height()));
+            canvas.translate(x, y);
+            mFastScrollerBg.setAlpha((int) (mFastScrollAlpha * 255));
+            mFastScrollerBg.draw(canvas);
+            mFastScrollTextPaint.setAlpha((int) (mFastScrollAlpha * 255));
+            mFastScrollTextPaint.getTextBounds(mFastScrollSectionName, 0,
+                    mFastScrollSectionName.length(), mFastScrollTextBounds);
+            canvas.drawText(mFastScrollSectionName,
+                    (bgBounds.width() - mFastScrollTextBounds.width()) / 2,
+                    bgBounds.height() -  (bgBounds.height() - mFastScrollTextBounds.height()) / 2,
+                    mFastScrollTextPaint);
+            canvas.restoreToCount(restoreCount);
+        }
+    }
+
+    /**
+     * We intercept the touch handling only to support fast scrolling when initiated from the
+     * gutter.  Otherwise, we fall back to the default RecyclerView touch handling.
+     */
+    @Override
+    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent ev) {
+        return handleTouchEvent(ev);
+    }
+
+    @Override
+    public void onTouchEvent(RecyclerView rv, MotionEvent ev) {
+        handleTouchEvent(ev);
+    }
+
+    /**
+     * Handles the touch event and determines whether to show the fast scroller (or updates it if
+     * it is already showing).
+     */
+    private boolean handleTouchEvent(MotionEvent ev) {
+        ViewConfiguration config = ViewConfiguration.get(getContext());
+
+        int action = ev.getAction();
+        int x = (int) ev.getX();
+        int y = (int) ev.getY();
+        switch (action) {
+            case MotionEvent.ACTION_DOWN:
+                // Keep track of the down positions
+                mDownX = mLastX = x;
+                mDownY = mLastY = y;
+                stopScroll();
+                break;
+            case MotionEvent.ACTION_MOVE:
+                // Check if we are scrolling
+                boolean isRtl = (getResources().getConfiguration().getLayoutDirection() ==
+                        LAYOUT_DIRECTION_RTL);
+                boolean isInGutter;
+                if (isRtl) {
+                    isInGutter = mDownX < mGutterSize;
+                } else {
+                    isInGutter = mDownX >= (getWidth() - mGutterSize);
+                }
+                if (!mDraggingFastScroller && isInGutter &&
+                        Math.abs(y - mDownY) > config.getScaledTouchSlop()) {
+                    getParent().requestDisallowInterceptTouchEvent(true);
+                    mDraggingFastScroller = true;
+                    animateFastScrollerVisibility(true);
+                }
+                if (mDraggingFastScroller) {
+                    mLastX = x;
+                    mLastY = y;
+
+                    // Scroll to the right position, and update the section name
+                    int top = getPaddingTop() + (mFastScrollerBg.getBounds().height() / 2);
+                    int bottom = getHeight() - getPaddingBottom() -
+                            (mFastScrollerBg.getBounds().height() / 2);
+                    float boundedY = (float) Math.max(top, Math.min(bottom, y));
+                    mFastScrollSectionName = scrollToPositionAtProgress((boundedY - top) /
+                            (bottom - top));
+                    invalidateFastScroller();
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                mDraggingFastScroller = false;
+                animateFastScrollerVisibility(false);
+                break;
+        }
+        return mDraggingFastScroller;
+
+    }
+
+    /**
+     * Animates the visibility of the fast scroller popup.
+     */
+    private void animateFastScrollerVisibility(boolean visible) {
+        ObjectAnimator anim = ObjectAnimator.ofFloat(this, "fastScrollerAlpha", visible ? 1f : 0f);
+        anim.setDuration(visible ? 200 : 150);
+        anim.start();
+    }
+
+    /**
+     * Invalidates the fast scroller popup.
+     */
+    private void invalidateFastScroller() {
+        invalidate(getWidth() - getPaddingRight() - getScrollBarSize() -
+                mFastScrollerBg.getIntrinsicWidth(), 0, getWidth(), getHeight());
+    }
+
+    /**
+     * Maps the progress (from 0..1) to the position that should be visible
+     */
+    private String scrollToPositionAtProgress(float progress) {
+        List<AlphabeticalAppsList.SectionInfo> sections = mApps.getSections();
+        // Get the total number of rows
+        int rowCount = 0;
+        for (AlphabeticalAppsList.SectionInfo info : sections) {
+            int numRowsInSection = (int) Math.ceil((float) info.numAppsInSection / mNumAppsPerRow);
+            rowCount += numRowsInSection;
+        }
+
+        // Find the index of the first app in that row and scroll to that position
+        int rowAtProgress = (int) (progress * rowCount);
+        int appIndex = 0;
+        rowCount = 0;
+        for (AlphabeticalAppsList.SectionInfo info : sections) {
+            int numRowsInSection = (int) Math.ceil((float) info.numAppsInSection / mNumAppsPerRow);
+            if (rowCount + numRowsInSection > rowAtProgress) {
+                appIndex += (rowAtProgress - rowCount) * mNumAppsPerRow;
+                break;
+            }
+            rowCount += numRowsInSection;
+            appIndex += info.numAppsInSection;
+        }
+        appIndex = Math.max(0, Math.min(mApps.getAppsWithoutSectionBreaks().size() - 1, appIndex));
+        AppInfo appInfo = mApps.getAppsWithoutSectionBreaks().get(appIndex);
+        int sectionedAppIndex = mApps.getApps().indexOf(appInfo);
+        scrollToPosition(sectionedAppIndex);
+
+        // Returns the section name of the row
+        return mApps.getSectionNameForApp(appInfo);
+    }
+}
index cabacec..cc31e20 100644 (file)
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package com.android.launcher3;
 
-import android.content.ComponentName;
 import android.content.Context;
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.support.v7.widget.RecyclerView;
+import android.text.Editable;
+import android.text.TextWatcher;
 import android.util.AttributeSet;
-import android.view.Gravity;
-import android.view.LayoutInflater;
+import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
-import android.view.ViewGroup;
-import android.widget.BaseAdapter;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
 import android.widget.FrameLayout;
-import android.widget.LinearLayout;
-import android.widget.ListView;
-import android.widget.SectionIndexer;
 import android.widget.TextView;
+import com.android.launcher3.compat.AlphabeticIndexCompat;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.Map;
-
-
-/**
- * Represents a row in the apps list view.
- */
-class AppsRow {
-    int sectionId;
-    String sectionDescription;
-    List<AppInfo> apps;
-
-    public AppsRow(int sId, String sc, List<AppInfo> ai) {
-        sectionId = sId;
-        sectionDescription = sc;
-        apps = ai;
-    }
-
-    public AppsRow(int sId, List<AppInfo> ai) {
-        sectionId = sId;
-        apps = ai;
-    }
-}
-
-/**
- * An interface to an algorithm that generates app rows.
- */
-interface AppRowAlgorithm {
-    public List<AppsRow> computeAppRows(List<AppInfo> sortedApps, int appsPerRow);
-    public int getIconViewLayoutId();
-    public int getRowViewLayoutId();
-    public void bindRowViewIconToInfo(BubbleTextView icon, AppInfo info);
-}
-
-/**
- * Computes the rows in the apps list view.
- */
-class SectionedAppsAlgorithm implements AppRowAlgorithm {
-
-    @Override
-    public List<AppsRow> computeAppRows(List<AppInfo> sortedApps, int appsPerRow) {
-        List<AppsRow> rows = new ArrayList<>();
-        LinkedHashMap<String, List<AppInfo>> sections = computeSectionedApps(sortedApps);
-        int sectionId = 0;
-        for (Map.Entry<String, List<AppInfo>> sectionEntry : sections.entrySet()) {
-            String section = sectionEntry.getKey();
-            List<AppInfo> apps = sectionEntry.getValue();
-            int numRows = (int) Math.ceil((float) apps.size() / appsPerRow);
-            for (int i = 0; i < numRows; i++) {
-                List<AppInfo> appsInRow = new ArrayList<>();
-                int offset = i * appsPerRow;
-                for (int j = 0; j < appsPerRow; j++) {
-                    if (offset + j < apps.size()) {
-                        appsInRow.add(apps.get(offset + j));
-                    }
-                }
-                if (i == 0) {
-                    rows.add(new AppsRow(sectionId, section, appsInRow));
-                } else {
-                    rows.add(new AppsRow(sectionId, appsInRow));
-                }
-            }
-            sectionId++;
-        }
-        return rows;
-    }
-
-    @Override
-    public int getIconViewLayoutId() {
-        return R.layout.apps_grid_row_icon_view;
-    }
-
-    @Override
-    public int getRowViewLayoutId() {
-        return R.layout.apps_grid_row_view;
-    }
-
-    private LinkedHashMap<String, List<AppInfo>> computeSectionedApps(List<AppInfo> sortedApps) {
-        LinkedHashMap<String, List<AppInfo>> sections = new LinkedHashMap<>();
-        for (AppInfo info : sortedApps) {
-            String section = getSection(info);
-            List<AppInfo> sectionApps = sections.get(section);
-            if (sectionApps == null) {
-                sectionApps = new ArrayList<>();
-                sections.put(section, sectionApps);
-            }
-            sectionApps.add(info);
-        }
-        return sections;
-    }
-
-    @Override
-    public void bindRowViewIconToInfo(BubbleTextView icon, AppInfo info) {
-        icon.applyFromApplicationInfo(info);
-    }
-
-    private String getSection(AppInfo app) {
-        return app.title.toString().substring(0, 1).toLowerCase();
-    }
-}
-
-/**
- * Computes the rows in the apps grid view.
- */
-class ListedAppsAlgorithm implements AppRowAlgorithm {
-
-    @Override
-    public List<AppsRow> computeAppRows(List<AppInfo> sortedApps, int appsPerRow) {
-        List<AppsRow> rows = new ArrayList<>();
-        int sectionId = -1;
-        String prevSection = "";
-        for (AppInfo info : sortedApps) {
-            List<AppInfo> appsInRow = new ArrayList<>();
-            appsInRow.add(info);
-            String section = getSection(info);
-            if (!prevSection.equals(section)) {
-                prevSection = section;
-                sectionId++;
-                rows.add(new AppsRow(sectionId, section, appsInRow));
-            } else {
-                rows.add(new AppsRow(sectionId, appsInRow));
-            }
-        }
-        return rows;
-    }
-
-    @Override
-    public int getIconViewLayoutId() {
-        return R.layout.apps_list_row_icon_view;
-    }
-
-    @Override
-    public int getRowViewLayoutId() {
-        return R.layout.apps_list_row_view;
-    }
-
-    @Override
-    public void bindRowViewIconToInfo(BubbleTextView icon, AppInfo info) {
-        icon.applyFromApplicationInfo(info);
-    }
-
-    private String getSection(AppInfo app) {
-        return app.title.toString().substring(0, 1).toLowerCase();
-    }
-}
-
-/**
- * The adapter of all the apps
- */
-class AppsListAdapter extends BaseAdapter implements SectionIndexer {
-
-    private LayoutInflater mLayoutInflater;
-    private List<AppsRow> mAppRows = new ArrayList<>();
-    private View.OnTouchListener mTouchListener;
-    private View.OnClickListener mIconClickListener;
-    private View.OnLongClickListener mIconLongClickListener;
-    private AppRowAlgorithm mRowAlgorithm;
-    private int mAppsPerRow;
-
-    public AppsListAdapter(Context context, View.OnTouchListener touchListener,
-            View.OnClickListener iconClickListener, View.OnLongClickListener iconLongClickListener) {
-        mLayoutInflater = LayoutInflater.from(context);
-        mTouchListener = touchListener;
-        mIconClickListener = iconClickListener;
-        mIconLongClickListener = iconLongClickListener;
-    }
-
-    void setApps(List<AppsRow> apps, int appsPerRow, AppRowAlgorithm algo) {
-        mAppsPerRow = appsPerRow;
-        mRowAlgorithm = algo;
-        mAppRows.clear();
-        mAppRows.addAll(apps);
-        notifyDataSetChanged();
-    }
-
-    @Override
-    public int getCount() {
-        return mAppRows.size();
-    }
-
-    @Override
-    public Object getItem(int position) {
-        return mAppRows.get(position);
-    }
-
-    @Override
-    public long getItemId(int position) {
-        return position;
-    }
-
-    @Override
-    public View getView(int position, View convertView, ViewGroup parent) {
-        AppsRow info = mAppRows.get(position);
-        ViewGroup row = (ViewGroup) convertView;
-        if (row == null) {
-            // Inflate the row and all the icon children necessary
-            row = (ViewGroup) mLayoutInflater.inflate(mRowAlgorithm.getRowViewLayoutId(),
-                    parent, false);
-            for (int i = 0; i < mAppsPerRow; i++) {
-                BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
-                        mRowAlgorithm.getIconViewLayoutId(), row, false);
-                LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(0,
-                        ViewGroup.LayoutParams.WRAP_CONTENT, 1);
-                lp.gravity = Gravity.CENTER_VERTICAL;
-                icon.setLayoutParams(lp);
-                icon.setOnTouchListener(mTouchListener);
-                icon.setOnClickListener(mIconClickListener);
-                icon.setOnLongClickListener(mIconLongClickListener);
-                icon.setFocusable(true);
-                row.addView(icon);
-            }
-        }
-        // Bind the section header
-        TextView tv = (TextView) row.findViewById(R.id.section);
-        if (info.sectionDescription != null) {
-            tv.setText(info.sectionDescription);
-            tv.setVisibility(View.VISIBLE);
-        } else {
-            tv.setVisibility(View.INVISIBLE);
-        }
-        // Bind the icons
-        for (int i = 0; i < mAppsPerRow; i++) {
-            BubbleTextView icon = (BubbleTextView) row.getChildAt(i + 1);
-            if (i < info.apps.size()) {
-                mRowAlgorithm.bindRowViewIconToInfo(icon, info.apps.get(i));
-                icon.setVisibility(View.VISIBLE);
-            } else {
-                icon.setVisibility(View.INVISIBLE);
-            }
-        }
-        return row;
-    }
-
-    @Override
-    public Object[] getSections() {
-        ArrayList<Object> sections = new ArrayList<>();
-        int prevSectionId = -1;
-        for (AppsRow row : mAppRows) {
-            if (row.sectionId != prevSectionId) {
-                sections.add(row.sectionDescription.toUpperCase());
-                prevSectionId = row.sectionId;
-            }
-        }
-        return sections.toArray();
-    }
-
-    @Override
-    public int getPositionForSection(int sectionIndex) {
-        for (int i = 0; i < mAppRows.size(); i++) {
-            AppsRow row = mAppRows.get(i);
-            if (row.sectionId == sectionIndex) {
-                return i;
-            }
-        }
-        return 0;
-    }
-
-    @Override
-    public int getSectionForPosition(int position) {
-        return mAppRows.get(position).sectionId;
-    }
-}
 
-/**
- * The alphabetically sorted list of applications.
- */
-class AlphabeticalAppList {
-
-    /**
-     * Callbacks for when this list is modified.
-     */
-    public interface Callbacks {
-        public void onAppsUpdated();
-    }
-
-    private List<AppInfo> mApps;
-    private Callbacks mCb;
-
-    public AlphabeticalAppList(Callbacks cb) {
-        mCb = cb;
-    }
-
-    /**
-     * Returns the list of applications.
-     */
-    public List<AppInfo> getApps() {
-        return mApps;
-    }
-
-    /**
-     * Sets the current set of apps.
-     */
-    public void setApps(List<AppInfo> apps) {
-        Collections.sort(apps, LauncherModel.getAppNameComparator());
-        mApps = apps;
-        mCb.onAppsUpdated();
-    }
-
-    /**
-     * Adds new apps to the list.
-     */
-    public void addApps(List<AppInfo> apps) {
-        // We add it in place, in alphabetical order
-        Comparator<AppInfo> appNameComparator = LauncherModel.getAppNameComparator();
-        for (AppInfo info : apps) {
-            // This call will return the exact index of where the item is if >= 0, or the index
-            // where it should be inserted if < 0.
-            int index = Collections.binarySearch(mApps, info, appNameComparator);
-            if (index < 0) {
-                mApps.add(-(index + 1), info);
-            }
-        }
-        mCb.onAppsUpdated();
-    }
-
-    /**
-     * Updates existing apps in the list
-     */
-    public void updateApps(List<AppInfo> apps) {
-        Comparator<AppInfo> appNameComparator = LauncherModel.getAppNameComparator();
-        for (AppInfo info : apps) {
-            int index = mApps.indexOf(info);
-            if (index != -1) {
-                mApps.set(index, info);
-            } else {
-                index = Collections.binarySearch(mApps, info, appNameComparator);
-                if (index < 0) {
-                    mApps.add(-(index + 1), info);
-                }
-            }
-        }
-        mCb.onAppsUpdated();
-    }
-
-    /**
-     * Removes some apps from the list.
-     */
-    public void removeApps(List<AppInfo> apps) {
-        for (AppInfo info : apps) {
-            int removeIndex = findAppByComponent(mApps, info);
-            if (removeIndex != -1) {
-                mApps.remove(removeIndex);
-            }
-        }
-        mCb.onAppsUpdated();
-    }
-
-    /**
-     * Finds the index of an app given a target AppInfo.
-     */
-    private int findAppByComponent(List<AppInfo> apps, AppInfo targetInfo) {
-        ComponentName targetComponent = targetInfo.intent.getComponent();
-        int length = apps.size();
-        for (int i = 0; i < length; ++i) {
-            AppInfo info = apps.get(i);
-            if (info.user.equals(info.user)
-                    && info.intent.getComponent().equals(targetComponent)) {
-                return i;
-            }
-        }
-        return -1;
-    }
-
-}
 
 /**
  * The all apps list view container.
  */
 public class AppsContainerView extends FrameLayout implements DragSource, View.OnTouchListener,
-        View.OnLongClickListener, Insettable, AlphabeticalAppList.Callbacks {
+        View.OnLongClickListener, Insettable, TextWatcher, TextView.OnEditorActionListener,
+        LauncherTransitionable {
 
-    static final int GRID_LAYOUT = 0;
-    static final int LIST_LAYOUT = 1;
-    static final int USE_LAYOUT = LIST_LAYOUT;
+    private static final boolean ALLOW_SINGLE_APP_LAUNCH = true;
+
+    private static final int GRID_LAYOUT = 0;
+    private static final int LIST_LAYOUT = 1;
+    private static final int USE_LAYOUT = GRID_LAYOUT;
 
     private Launcher mLauncher;
-    private AppRowAlgorithm mAppRowsAlgorithm;
-    private AppsListAdapter mAdapter;
-    private AlphabeticalAppList mApps;
-    private ListView mList;
-    private int mAppsRowSize;
+    private AlphabeticalAppsList mApps;
+    private RecyclerView.Adapter mAdapter;
+    private RecyclerView.LayoutManager mLayoutManager;
+    private RecyclerView.ItemDecoration mItemDecoration;
+    private AppsContainerRecyclerView mAppsListView;
+    private EditText mSearchBar;
+    private int mNumAppsPerRow;
     private Point mLastTouchDownPos = new Point();
     private Rect mPadding = new Rect();
+    private int mContentMarginStart;
 
     public AppsContainerView(Context context) {
         this(context, null);
@@ -423,15 +78,22 @@ public class AppsContainerView extends FrameLayout implements DragSource, View.O
         DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
 
         mLauncher = (Launcher) context;
+        mApps = new AlphabeticalAppsList(context);
         if (USE_LAYOUT == GRID_LAYOUT) {
-            mAppRowsAlgorithm = new SectionedAppsAlgorithm();
-            mAppsRowSize = grid.allAppsRowsSize;
+            mNumAppsPerRow = grid.appsViewNumCols;
+            AppsGridAdapter adapter = new AppsGridAdapter(context, mApps, mNumAppsPerRow, this,
+                    mLauncher, this);
+            mLayoutManager = adapter.getLayoutManager(context);
+            mItemDecoration = adapter.getItemDecoration();
+            mAdapter = adapter;
+            mContentMarginStart = adapter.getContentMarginStart();
         } else if (USE_LAYOUT == LIST_LAYOUT) {
-            mAppRowsAlgorithm = new ListedAppsAlgorithm();
-            mAppsRowSize = 1;
+            mNumAppsPerRow = 1;
+            AppsListAdapter adapter = new AppsListAdapter(context, mApps, this, mLauncher, this);
+            mLayoutManager = adapter.getLayoutManager(context);
+            mAdapter = adapter;
         }
-        mAdapter = new AppsListAdapter(context, this, mLauncher, this);
-        mApps = new AlphabeticalAppList(this);
+        mApps.setAdapter(mAdapter);
     }
 
     /**
@@ -466,7 +128,7 @@ public class AppsContainerView extends FrameLayout implements DragSource, View.O
      * Scrolls this list view to the top.
      */
     public void scrollToTop() {
-        mList.scrollTo(0, 0);
+        mAppsListView.scrollToPosition(0);
     }
 
     /**
@@ -480,23 +142,37 @@ public class AppsContainerView extends FrameLayout implements DragSource, View.O
      * Returns the reveal view used for the launcher transitions.
      */
     public View getRevealView() {
-        return findViewById(R.id.all_apps_transition_overlay);
-    }
-
-    @Override
-    public void onAppsUpdated() {
-        List<AppsRow> rows = mAppRowsAlgorithm.computeAppRows(mApps.getApps(), mAppsRowSize);
-        mAdapter.setApps(rows, mAppsRowSize, mAppRowsAlgorithm);
+        return findViewById(R.id.apps_view_transition_overlay);
     }
 
     @Override
     protected void onFinishInflate() {
-        mList = (ListView) findViewById(R.id.apps_list);
-        mList.setFastScrollEnabled(true);
-        mList.setFastScrollAlwaysVisible(true);
-        mList.setItemsCanFocus(true);
-        mList.setAdapter(mAdapter);
-        mPadding.set(getPaddingLeft(), getPaddingTop(), getPaddingRight(), getPaddingBottom());
+        boolean isRtl = (getResources().getConfiguration().getLayoutDirection() ==
+                LAYOUT_DIRECTION_RTL);
+        if (USE_LAYOUT == GRID_LAYOUT) {
+            ((AppsGridAdapter) mAdapter).setRtl(isRtl);
+        }
+        mSearchBar = (EditText) findViewById(R.id.app_search_box);
+        mSearchBar.addTextChangedListener(this);
+        mSearchBar.setOnEditorActionListener(this);
+        mAppsListView = (AppsContainerRecyclerView) findViewById(R.id.apps_list_view);
+        mAppsListView.setApps(mApps);
+        mAppsListView.setNumAppsPerRow(mNumAppsPerRow);
+        mAppsListView.setLayoutManager(mLayoutManager);
+        mAppsListView.setAdapter(mAdapter);
+        mAppsListView.setHasFixedSize(true);
+        if (isRtl) {
+            mAppsListView.setPadding(mAppsListView.getPaddingLeft(), mAppsListView.getPaddingTop(),
+                    mAppsListView.getPaddingRight() + mContentMarginStart, mAppsListView.getPaddingBottom());
+        } else {
+            mAppsListView.setPadding(mAppsListView.getPaddingLeft() + mContentMarginStart, mAppsListView.getPaddingTop(),
+                    mAppsListView.getPaddingRight(), mAppsListView.getPaddingBottom());
+        }
+        if (mItemDecoration != null) {
+            mAppsListView.addItemDecoration(mItemDecoration);
+        }
+        mPadding.set(getPaddingLeft(), getPaddingTop(), getPaddingRight(),
+                getPaddingBottom());
     }
 
     @Override
@@ -574,7 +250,8 @@ public class AppsContainerView extends FrameLayout implements DragSource, View.O
     }
 
     @Override
-    public void onDropCompleted(View target, DropTarget.DragObject d, boolean isFlingToDelete, boolean success) {
+    public void onDropCompleted(View target, DropTarget.DragObject d, boolean isFlingToDelete,
+            boolean success) {
         if (isFlingToDelete || !success || (target != mLauncher.getWorkspace() &&
                 !(target instanceof DeleteDropTarget) && !(target instanceof Folder))) {
             // Exit spring loaded mode if we have not successfully dropped or have not handled the
@@ -606,4 +283,83 @@ public class AppsContainerView extends FrameLayout implements DragSource, View.O
             d.deferDragViewCleanupPostAnimation = false;
         }
     }
+
+    @Override
+    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+        // Do nothing
+    }
+
+    @Override
+    public void onTextChanged(CharSequence s, int start, int before, int count) {
+        // Do nothing
+    }
+
+    @Override
+    public void afterTextChanged(final Editable s) {
+        if (s.toString().isEmpty()) {
+            mApps.setFilter(null);
+        } else {
+            final AlphabeticIndexCompat indexer = mApps.getIndexer();
+            final String filterText = s.toString().toLowerCase().replaceAll("\\s+", "");
+            mApps.setFilter(new AlphabeticalAppsList.Filter() {
+                @Override
+                public boolean retainApp(AppInfo info) {
+                    String title = info.title.toString();
+                    String sectionName = mApps.getSectionNameForApp(info);
+                    return sectionName.toLowerCase().contains(filterText) ||
+                            title.toLowerCase().replaceAll("\\s+", "").contains(filterText);
+                }
+            });
+        }
+    }
+
+    @Override
+    public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+        if (ALLOW_SINGLE_APP_LAUNCH && actionId == EditorInfo.IME_ACTION_DONE) {
+            List<AppInfo> appsWithoutSections = mApps.getAppsWithoutSectionBreaks();
+            List<AppInfo> apps = mApps.getApps();
+            if (appsWithoutSections.size() == 1) {
+                mSearchBar.clearFocus();
+                mAppsListView.getChildAt(apps.indexOf(appsWithoutSections.get(0))).performClick();
+                InputMethodManager imm = (InputMethodManager)
+                        getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+                imm.hideSoftInputFromWindow(getWindowToken(), 0);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public View getContent() {
+        return null;
+    }
+
+    @Override
+    public void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace) {
+        if (!toWorkspace) {
+            // Disable the focus so that the search bar doesn't get focus
+            mSearchBar.setFocusableInTouchMode(false);
+        }
+    }
+
+    @Override
+    public void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace) {
+        // Do nothing
+    }
+
+    @Override
+    public void onLauncherTransitionStep(Launcher l, float t) {
+        // Do nothing
+    }
+
+    @Override
+    public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) {
+        if (toWorkspace) {
+            // Clear the search bar
+            mSearchBar.setText("");
+        } else {
+            mSearchBar.setFocusableInTouchMode(true);
+        }
+    }
 }
diff --git a/src/com/android/launcher3/AppsGridAdapter.java b/src/com/android/launcher3/AppsGridAdapter.java
new file mode 100644 (file)
index 0000000..6727e4f
--- /dev/null
@@ -0,0 +1,206 @@
+package com.android.launcher3;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import com.android.launcher3.compat.AlphabeticIndexCompat;
+
+
+/**
+ * The grid view adapter of all the apps.
+ */
+class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> {
+
+    public static final String TAG = "AppsGridAdapter";
+
+    private static final int SECTION_BREAK_VIEW_TYPE = 0;
+    private static final int ICON_VIEW_TYPE = 1;
+
+    /**
+     * ViewHolder for each icon.
+     */
+    public static class ViewHolder extends RecyclerView.ViewHolder {
+        public View mContent;
+        public boolean mIsSectionRow;
+
+        public ViewHolder(View v, boolean isSectionRow) {
+            super(v);
+            mContent = v;
+            mIsSectionRow = isSectionRow;
+        }
+    }
+
+    /**
+     * Helper class to size the grid items.
+     */
+    public class GridSpanSizer extends GridLayoutManager.SpanSizeLookup {
+        @Override
+        public int getSpanSize(int position) {
+            AppInfo info = mApps.getApps().get(position);
+            if (info == AlphabeticalAppsList.SECTION_BREAK_INFO) {
+                return mAppsPerRow;
+            } else {
+                return 1;
+            }
+        }
+    }
+
+    /**
+     * Helper class to draw the section headers
+     */
+    public class GridItemDecoration extends RecyclerView.ItemDecoration {
+
+        @Override
+        public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
+            AlphabeticIndexCompat indexer = mApps.getIndexer();
+            for (int i = 0; i < parent.getChildCount(); i++) {
+                View child = parent.getChildAt(i);
+                ViewHolder holder = (ViewHolder) parent.getChildViewHolder(child);
+                if (holder != null) {
+                    GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams)
+                            child.getLayoutParams();
+                    if (!holder.mIsSectionRow && !lp.isItemRemoved()) {
+                        if (mApps.getApps().get(holder.getPosition() - 1) ==
+                                AlphabeticalAppsList.SECTION_BREAK_INFO) {
+                            // Draw at the parent
+                            AppInfo info = mApps.getApps().get(holder.getPosition());
+                            String section = mApps.getSectionNameForApp(info);
+                            mSectionTextPaint.getTextBounds(section, 0, section.length(),
+                                    mTmpBounds);
+                            if (mIsRtl) {
+                                c.drawText(section, parent.getWidth() - mStartMargin +
+                                                (mStartMargin - mTmpBounds.width()) / 2,
+                                        child.getTop() + (2 * child.getPaddingTop()) +
+                                                mTmpBounds.height(), mSectionTextPaint);
+                            } else {
+                                c.drawText(section, (mStartMargin - mTmpBounds.width()) / 2,
+                                    child.getTop() + (2 * child.getPaddingTop()) +
+                                            mTmpBounds.height(), mSectionTextPaint);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        @Override
+        public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
+                RecyclerView.State state) {
+            // Do nothing
+        }
+    }
+
+    private LayoutInflater mLayoutInflater;
+    private AlphabeticalAppsList mApps;
+    private GridSpanSizer mGridSizer;
+    private GridItemDecoration mItemDecoration;
+    private View.OnTouchListener mTouchListener;
+    private View.OnClickListener mIconClickListener;
+    private View.OnLongClickListener mIconLongClickListener;
+    private int mAppsPerRow;
+    private boolean mIsRtl;
+
+    // Section drawing
+    private int mStartMargin;
+    private Paint mSectionTextPaint;
+    private Rect mTmpBounds = new Rect();
+
+
+    public AppsGridAdapter(Context context, AlphabeticalAppsList apps, int appsPerRow,
+            View.OnTouchListener touchListener, View.OnClickListener iconClickListener,
+            View.OnLongClickListener iconLongClickListener) {
+        Resources res = context.getResources();
+        mApps = apps;
+        mAppsPerRow = appsPerRow;
+        mGridSizer = new GridSpanSizer();
+        mItemDecoration = new GridItemDecoration();
+        mLayoutInflater = LayoutInflater.from(context);
+        mTouchListener = touchListener;
+        mIconClickListener = iconClickListener;
+        mIconLongClickListener = iconLongClickListener;
+        mStartMargin = res.getDimensionPixelSize(R.dimen.apps_grid_view_start_margin);
+        mSectionTextPaint = new Paint();
+        mSectionTextPaint.setTextSize(res.getDimensionPixelSize(
+                R.dimen.apps_view_section_text_size));
+        mSectionTextPaint.setColor(res.getColor(R.color.apps_view_section_text_color));
+        mSectionTextPaint.setAntiAlias(true);
+    }
+
+    /**
+     * Sets whether we are in RTL mode.
+     */
+    public void setRtl(boolean rtl) {
+        mIsRtl = rtl;
+    }
+
+    /**
+     * Returns the grid layout manager.
+     */
+    public GridLayoutManager getLayoutManager(Context context) {
+        GridLayoutManager layoutMgr = new GridLayoutManager(context, mAppsPerRow,
+                GridLayoutManager.VERTICAL, false);
+        layoutMgr.setSpanSizeLookup(mGridSizer);
+        return layoutMgr;
+    }
+
+    /**
+     * Returns the item decoration for the recycler view.
+     */
+    public RecyclerView.ItemDecoration getItemDecoration() {
+        return mItemDecoration;
+    }
+
+    /**
+     * Returns the left padding for the recycler view.
+     */
+    public int getContentMarginStart() {
+        return mStartMargin;
+    }
+
+    @Override
+    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+        switch (viewType) {
+            case SECTION_BREAK_VIEW_TYPE:
+                return new ViewHolder(new View(parent.getContext()), true);
+            case ICON_VIEW_TYPE:
+                BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
+                        R.layout.apps_grid_row_icon_view, parent, false);
+                icon.setOnTouchListener(mTouchListener);
+                icon.setOnClickListener(mIconClickListener);
+                icon.setOnLongClickListener(mIconLongClickListener);
+                icon.setFocusable(true);
+                return new ViewHolder(icon, false);
+            default:
+                throw new RuntimeException("Unexpected view type");
+        }
+    }
+
+    @Override
+    public void onBindViewHolder(ViewHolder holder, int position) {
+        AppInfo info = mApps.getApps().get(position);
+        if (info != AlphabeticalAppsList.SECTION_BREAK_INFO) {
+            BubbleTextView icon = (BubbleTextView) holder.mContent;
+            icon.applyFromApplicationInfo(info);
+        }
+    }
+
+    @Override
+    public int getItemCount() {
+        return mApps.getApps().size();
+    }
+
+    @Override
+    public int getItemViewType(int position) {
+        if (mApps.getApps().get(position) == AlphabeticalAppsList.SECTION_BREAK_INFO) {
+            return SECTION_BREAK_VIEW_TYPE;
+        }
+        return ICON_VIEW_TYPE;
+    }
+}
diff --git a/src/com/android/launcher3/AppsListAdapter.java b/src/com/android/launcher3/AppsListAdapter.java
new file mode 100644 (file)
index 0000000..8ac381e
--- /dev/null
@@ -0,0 +1,119 @@
+package com.android.launcher3;
+
+import android.content.Context;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import com.android.launcher3.compat.AlphabeticIndexCompat;
+
+/**
+ * The linear list view adapter for all the apps.
+ */
+class AppsListAdapter extends RecyclerView.Adapter<AppsListAdapter.ViewHolder> {
+
+    /**
+     * ViewHolder for each row.
+     */
+    public static class ViewHolder extends RecyclerView.ViewHolder {
+        public View mContent;
+
+        public ViewHolder(View v) {
+            super(v);
+            mContent = v;
+        }
+    }
+
+    private static final int SECTION_BREAK_VIEW_TYPE = 0;
+    private static final int ICON_VIEW_TYPE = 1;
+
+    private LayoutInflater mLayoutInflater;
+    private AlphabeticalAppsList mApps;
+    private View.OnTouchListener mTouchListener;
+    private View.OnClickListener mIconClickListener;
+    private View.OnLongClickListener mIconLongClickListener;
+
+    public AppsListAdapter(Context context, AlphabeticalAppsList apps,
+            View.OnTouchListener touchListener, View.OnClickListener iconClickListener,
+            View.OnLongClickListener iconLongClickListener) {
+        mApps = apps;
+        mLayoutInflater = LayoutInflater.from(context);
+        mTouchListener = touchListener;
+        mIconClickListener = iconClickListener;
+        mIconLongClickListener = iconLongClickListener;
+    }
+
+    public RecyclerView.LayoutManager getLayoutManager(Context context) {
+        return new LinearLayoutManager(context);
+    }
+
+    @Override
+    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+        switch (viewType) {
+            case SECTION_BREAK_VIEW_TYPE:
+                return new ViewHolder(new View(parent.getContext()));
+            case ICON_VIEW_TYPE:
+                // Inflate the row and all the icon children necessary
+                ViewGroup row = (ViewGroup) mLayoutInflater.inflate(R.layout.apps_list_row_view,
+                        parent, false);
+                BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
+                        R.layout.apps_list_row_icon_view, row, false);
+                LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(0,
+                        ViewGroup.LayoutParams.WRAP_CONTENT, 1);
+                lp.gravity = Gravity.CENTER_VERTICAL;
+                icon.setLayoutParams(lp);
+                icon.setOnTouchListener(mTouchListener);
+                icon.setOnClickListener(mIconClickListener);
+                icon.setOnLongClickListener(mIconLongClickListener);
+                icon.setFocusable(true);
+                row.addView(icon);
+                return new ViewHolder(row);
+            default:
+                throw new RuntimeException("Unexpected view type");
+        }
+    }
+
+    @Override
+    public void onBindViewHolder(ViewHolder holder, int position) {
+        AppInfo info = mApps.getApps().get(position);
+        if (info != AlphabeticalAppsList.SECTION_BREAK_INFO) {
+            ViewGroup content = (ViewGroup) holder.mContent;
+            String sectionDescription = mApps.getSectionNameForApp(info);
+
+            // Bind the section header
+            boolean showSectionHeader = true;
+            if (position > 0) {
+                AppInfo prevInfo = mApps.getApps().get(position - 1);
+                showSectionHeader = (prevInfo == AlphabeticalAppsList.SECTION_BREAK_INFO);
+            }
+            TextView tv = (TextView) content.findViewById(R.id.section);
+            if (showSectionHeader) {
+                tv.setText(sectionDescription);
+                tv.setVisibility(View.VISIBLE);
+            } else {
+                tv.setVisibility(View.INVISIBLE);
+            }
+
+            // Bind the icon
+            BubbleTextView icon = (BubbleTextView) content.getChildAt(1);
+            icon.applyFromApplicationInfo(info);
+        }
+    }
+
+    @Override
+    public int getItemCount() {
+        return mApps.getApps().size();
+    }
+
+    @Override
+    public int getItemViewType(int position) {
+        if (mApps.getApps().get(position) == AlphabeticalAppsList.SECTION_BREAK_INFO) {
+            return SECTION_BREAK_VIEW_TYPE;
+        }
+        return ICON_VIEW_TYPE;
+    }
+}
index 5ea84ae..fabae57 100644 (file)
@@ -63,6 +63,7 @@ public class BubbleTextView extends TextView {
 
     private float mSlop;
 
+    private final boolean mDeferShadowGenerationOnTouch;
     private final boolean mCustomShadowsEnabled;
     private final boolean mLayoutHorizontal;
     private final int mIconSize;
@@ -96,6 +97,8 @@ public class BubbleTextView extends TextView {
                 grid.iconDrawablePaddingPx);
         mTextSize = a.getDimensionPixelSize(R.styleable.BubbleTextView_textSizeOverride,
                 grid.allAppsIconTextSizePx);
+        mDeferShadowGenerationOnTouch =
+                a.getBoolean(R.styleable.BubbleTextView_deferShadowGeneration, false);
         a.recycle();
 
         if (mCustomShadowsEnabled) {
@@ -218,7 +221,7 @@ public class BubbleTextView extends TextView {
                 // So that the pressed outline is visible immediately on setStayPressed(),
                 // we pre-create it on ACTION_DOWN (it takes a small but perceptible amount of time
                 // to create it)
-                if (mPressedBackground == null) {
+                if (!mDeferShadowGenerationOnTouch && mPressedBackground == null) {
                     mPressedBackground = mOutlineHelper.createMediumDropShadow(this);
                 }
 
@@ -247,6 +250,10 @@ public class BubbleTextView extends TextView {
         mStayPressed = stayPressed;
         if (!stayPressed) {
             mPressedBackground = null;
+        } else {
+            if (mPressedBackground == null) {
+                mPressedBackground = mOutlineHelper.createMediumDropShadow(this);
+            }
         }
 
         // Only show the shadow effect when persistent pressed state is set.
index b5bb55c..ddd3002 100644 (file)
@@ -122,8 +122,7 @@ public class DeviceProfile {
     int hotseatAllAppsRank;
     int allAppsNumRows;
     int allAppsNumCols;
-    // TODO(winsonc): to be used with the grid layout
-    int allAppsRowsSize;
+    int appsViewNumCols;
     int searchBarSpaceWidthPx;
     int searchBarSpaceHeightPx;
     int pageIndicatorHeightPx;
@@ -365,7 +364,7 @@ public class DeviceProfile {
         }
     }
 
-    private void updateIconSize(float scale, int drawablePadding, Resources resources,
+    private void updateIconSize(float scale, int drawablePadding, Resources res,
                                 DisplayMetrics dm) {
         iconSizePx = (int) (DynamicGrid.pxFromDp(iconSize, dm) * scale);
         iconTextSizePx = (int) (DynamicGrid.pxFromSp(iconTextSize, dm) * scale);
@@ -374,9 +373,9 @@ public class DeviceProfile {
 
         // Search Bar
         searchBarSpaceWidthPx = Math.min(widthPx,
-                resources.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_max_width));
+                res.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_max_width));
         searchBarSpaceHeightPx = getSearchBarTopOffset()
-                + resources.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_height);
+                + res.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_height);
 
         // Calculate the actual text height
         Paint textPaint = new Paint();
@@ -384,7 +383,7 @@ public class DeviceProfile {
         FontMetrics fm = textPaint.getFontMetrics();
         cellWidthPx = iconSizePx;
         cellHeightPx = iconSizePx + iconDrawablePaddingPx + (int) Math.ceil(fm.bottom - fm.top);
-        final float scaleDps = resources.getDimensionPixelSize(R.dimen.dragViewScale);
+        final float scaleDps = res.getDimensionPixelSize(R.dimen.dragViewScale);
         dragViewScale = (iconSizePx + scaleDps) / iconSizePx;
 
         // Hotseat
@@ -402,11 +401,11 @@ public class DeviceProfile {
         allAppsCellWidthPx = allAppsIconSizePx;
         allAppsCellHeightPx = allAppsIconSizePx + drawablePadding + iconTextSizePx;
         int maxLongEdgeCellCount =
-                resources.getInteger(R.integer.config_dynamic_grid_max_long_edge_cell_count);
+                res.getInteger(R.integer.config_dynamic_grid_max_long_edge_cell_count);
         int maxShortEdgeCellCount =
-                resources.getInteger(R.integer.config_dynamic_grid_max_short_edge_cell_count);
+                res.getInteger(R.integer.config_dynamic_grid_max_short_edge_cell_count);
         int minEdgeCellCount =
-                resources.getInteger(R.integer.config_dynamic_grid_min_edge_cell_count);
+                res.getInteger(R.integer.config_dynamic_grid_min_edge_cell_count);
         int maxRows = (isLandscape ? maxShortEdgeCellCount : maxLongEdgeCellCount);
         int maxCols = (isLandscape ? maxLongEdgeCellCount : maxShortEdgeCellCount);
 
@@ -417,10 +416,17 @@ public class DeviceProfile {
             allAppsNumRows = (availableHeightPx - pageIndicatorHeightPx) /
                     (allAppsCellHeightPx + allAppsCellPaddingPx);
             allAppsNumRows = Math.max(minEdgeCellCount, Math.min(maxRows, allAppsNumRows));
-            allAppsNumCols = (availableWidthPx) /
-                    (allAppsCellWidthPx + allAppsCellPaddingPx);
+            allAppsNumCols = (availableWidthPx) / (allAppsCellWidthPx + allAppsCellPaddingPx);
             allAppsNumCols = Math.max(minEdgeCellCount, Math.min(maxCols, allAppsNumCols));
         }
+
+        int appsContainerViewPx = res.getDimensionPixelSize(R.dimen.apps_container_width);
+        int appsViewLeftMarginPx =
+                res.getDimensionPixelSize(R.dimen.apps_grid_view_start_margin);
+        int availableAppsWidthPx = (appsContainerViewPx > 0) ? appsContainerViewPx :
+                availableWidthPx;
+        appsViewNumCols = (availableAppsWidthPx - appsViewLeftMarginPx) /
+                (allAppsCellWidthPx + allAppsCellPaddingPx);
     }
 
     void updateFromConfiguration(Context context, Resources resources, int wPx, int hPx,
diff --git a/src/com/android/launcher3/compat/AlphabeticIndexCompat.java b/src/com/android/launcher3/compat/AlphabeticIndexCompat.java
new file mode 100644 (file)
index 0000000..602a845
--- /dev/null
@@ -0,0 +1,131 @@
+package com.android.launcher3.compat;
+
+import android.content.Context;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.util.Locale;
+
+/**
+ * Fallback class to support Alphabetic indexing if not supported by the framework.
+ * TODO(winsonc): disable for non-english locales
+ */
+class BaseAlphabeticIndex {
+
+    private static final String BUCKETS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-";
+    private static final int UNKNOWN_BUCKET_INDEX = BUCKETS.length() - 1;
+
+    public BaseAlphabeticIndex() {}
+
+    /**
+     * Sets the max number of the label buckets in this index.
+     */
+    public void setMaxLabelCount(int count) {
+        // Not currently supported
+    }
+
+    /**
+     * Returns the index of the bucket in which the given string should appear.
+     */
+    public int getBucketIndex(String s) {
+        if (s.isEmpty()) {
+            return UNKNOWN_BUCKET_INDEX;
+        }
+        int index = BUCKETS.indexOf(s.substring(0, 1).toUpperCase());
+        if (index != -1) {
+            return index;
+        }
+        return UNKNOWN_BUCKET_INDEX;
+    }
+
+    /**
+     * Returns the label for the bucket at the given index (as returned by getBucketIndex).
+     */
+    public String getBucketLabel(int index) {
+        return BUCKETS.substring(index, index + 1);
+    }
+}
+
+/**
+ * Reflected libcore.icu.AlphabeticIndex implementation, falls back to the base alphabetic index.
+ */
+public class AlphabeticIndexCompat extends BaseAlphabeticIndex {
+
+    private Object mAlphabeticIndex;
+    private Method mAddLabelsMethod;
+    private Method mSetMaxLabelCountMethod;
+    private Method mGetBucketIndexMethod;
+    private Method mGetBucketLabelMethod;
+    private boolean mHasValidAlphabeticIndex;
+
+    public AlphabeticIndexCompat(Context context) {
+        super();
+        try {
+            Locale curLocale = context.getResources().getConfiguration().locale;
+            Class clazz = Class.forName("libcore.icu.AlphabeticIndex");
+            Constructor ctor = clazz.getConstructor(Locale.class);
+            mAddLabelsMethod = clazz.getDeclaredMethod("addLabels", Locale.class);
+            mSetMaxLabelCountMethod = clazz.getDeclaredMethod("setMaxLabelCount", int.class);
+            mGetBucketIndexMethod = clazz.getDeclaredMethod("getBucketIndex", String.class);
+            mGetBucketLabelMethod = clazz.getDeclaredMethod("getBucketLabel", int.class);
+            mAlphabeticIndex = ctor.newInstance(curLocale);
+            try {
+                // Ensure we always have some base English locale buckets
+                if (!curLocale.getLanguage().equals(new Locale("en").getLanguage())) {
+                    mAddLabelsMethod.invoke(mAlphabeticIndex, Locale.ENGLISH);
+                }
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+            mHasValidAlphabeticIndex = true;
+        } catch (Exception e) {
+            mHasValidAlphabeticIndex = false;
+        }
+    }
+
+    /**
+     * Sets the max number of the label buckets in this index.
+     * (ICU 51 default is 99)
+     */
+    public void setMaxLabelCount(int count) {
+        if (mHasValidAlphabeticIndex) {
+            try {
+                mSetMaxLabelCountMethod.invoke(mAlphabeticIndex, count);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        } else {
+            super.setMaxLabelCount(count);
+        }
+    }
+
+    /**
+     * Returns the index of the bucket in which {@param s} should appear.
+     * Function is synchronized because underlying routine walks an iterator
+     * whose state is maintained inside the index object.
+     */
+    public int getBucketIndex(String s) {
+        if (mHasValidAlphabeticIndex) {
+            try {
+                return (Integer) mGetBucketIndexMethod.invoke(mAlphabeticIndex, s);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+        return super.getBucketIndex(s);
+    }
+
+    /**
+     * Returns the label for the bucket at the given index (as returned by getBucketIndex).
+     */
+    public String getBucketLabel(int index) {
+        if (mHasValidAlphabeticIndex) {
+            try {
+                return (String) mGetBucketLabelMethod.invoke(mAlphabeticIndex, index);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+        return super.getBucketLabel(index);
+    }
+}