Merge "Set proper height of the widget tray container for preloading." into ub-launcher3-burnaby
diff --git a/res/layout/all_apps_button.xml b/res/layout/all_apps_button.xml
index 9d6d82b..68cc109 100644
--- a/res/layout/all_apps_button.xml
+++ b/res/layout/all_apps_button.xml
@@ -15,5 +15,5 @@
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
- style="@style/WorkspaceIcon"
+ style="@style/Icon"
android:focusable="true" />
diff --git a/res/layout/application.xml b/res/layout/application.xml
index c21dea0..831cee5 100644
--- a/res/layout/application.xml
+++ b/res/layout/application.xml
@@ -15,5 +15,5 @@
-->
<com.android.launcher3.BubbleTextView xmlns:android="http://schemas.android.com/apk/res/android"
- style="@style/WorkspaceIcon"
+ style="@style/Icon"
android:focusable="true" />
diff --git a/res/layout/apps_grid_row_icon_view.xml b/res/layout/apps_grid_icon_view.xml
similarity index 96%
rename from res/layout/apps_grid_row_icon_view.xml
rename to res/layout/apps_grid_icon_view.xml
index acb3da3..67d7d50 100644
--- a/res/layout/apps_grid_row_icon_view.xml
+++ b/res/layout/apps_grid_icon_view.xml
@@ -16,7 +16,7 @@
<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"
+ style="@style/Icon.AllApps"
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
diff --git a/res/layout/apps_list_view.xml b/res/layout/apps_list_view.xml
index ddcb639..ef20323 100644
--- a/res/layout/apps_list_view.xml
+++ b/res/layout/apps_list_view.xml
@@ -13,20 +13,37 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<LinearLayout
+<FrameLayout
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:orientation="vertical"
android:elevation="15dp"
android:visibility="gone"
android:focusableInTouchMode="true">
+ <com.android.launcher3.AppsContainerRecyclerView
+ android:id="@+id/apps_list_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginTop="@dimen/apps_search_bar_height"
+ android:layout_gravity="center_horizontal|top"
+ android:clipToPadding="false"
+ android:focusable="true"
+ android:descendantFocusability="afterDescendants" />
+ <LinearLayout
+ android:id="@+id/prediction_bar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/apps_search_bar_height"
+ android:orientation="horizontal"
+ android:visibility="invisible">
+ </LinearLayout>
+
+ <!-- We always want the search bar on top, so it goes last. -->
<FrameLayout
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="@dimen/apps_search_bar_height"
- android:orientation="horizontal"
android:background="@drawable/apps_search_bg">
<LinearLayout
android:id="@+id/app_search_container"
@@ -40,8 +57,8 @@
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:layout_marginStart="4dp"
- android:paddingTop="12dp"
- android:paddingBottom="12dp"
+ android:paddingTop="13dp"
+ android:paddingBottom="13dp"
android:contentDescription="@string/all_apps_button_label"
android:src="@drawable/ic_arrow_back_grey" />
<com.android.launcher3.AppsContainerSearchEditTextView
@@ -69,19 +86,9 @@
android:layout_height="wrap_content"
android:layout_gravity="end|center_vertical"
android:layout_marginEnd="6dp"
- android:paddingTop="12dp"
- android:paddingBottom="12dp"
+ android:paddingTop="13dp"
+ android:paddingBottom="13dp"
android:contentDescription="@string/apps_view_search_bar_hint"
android:src="@drawable/ic_search_grey" />
</FrameLayout>
- <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:focusable="true"
- android:descendantFocusability="afterDescendants" />
-</LinearLayout>
\ No newline at end of file
+</FrameLayout>
\ No newline at end of file
diff --git a/res/layout/apps_grid_row_icon_view.xml b/res/layout/apps_prediction_bar_icon_view.xml
similarity index 77%
copy from res/layout/apps_grid_row_icon_view.xml
copy to res/layout/apps_prediction_bar_icon_view.xml
index acb3da3..4a6f157 100644
--- a/res/layout/apps_grid_row_icon_view.xml
+++ b/res/layout/apps_prediction_bar_icon_view.xml
@@ -16,13 +16,12 @@
<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"
+ style="@style/Icon.AllApps"
android:id="@+id/icon"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="left|center_vertical"
- android:paddingTop="@dimen/apps_icon_top_bottom_padding"
- android:paddingBottom="@dimen/apps_icon_top_bottom_padding"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:layout_weight="1"
android:focusable="true"
android:background="@drawable/focusable_view_bg"
launcher:deferShadowGeneration="true"
diff --git a/res/layout/folder_application.xml b/res/layout/folder_application.xml
index b48b613..4d00331 100644
--- a/res/layout/folder_application.xml
+++ b/res/layout/folder_application.xml
@@ -15,5 +15,5 @@
-->
<com.android.launcher3.BubbleTextView xmlns:android="http://schemas.android.com/apk/res/android"
- style="@style/WorkspaceIcon.Folder"
+ style="@style/Icon.Folder"
android:focusable="true" />
diff --git a/res/layout/folder_icon.xml b/res/layout/folder_icon.xml
index fd45d76..d9a7671 100644
--- a/res/layout/folder_icon.xml
+++ b/res/layout/folder_icon.xml
@@ -28,7 +28,7 @@
android:antialias="true"
android:src="@drawable/portal_ring_inner_holo"/>
<com.android.launcher3.BubbleTextView
- style="@style/WorkspaceIcon"
+ style="@style/Icon"
android:id="@+id/folder_icon_name"
android:layout_gravity="top"
android:layout_width="match_parent"
diff --git a/res/layout/user_folder.xml b/res/layout/user_folder.xml
index 5bacc96..ab34917 100644
--- a/res/layout/user_folder.xml
+++ b/res/layout/user_folder.xml
@@ -45,20 +45,16 @@
android:id="@+id/folder_footer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:orientation="vertical" >
-
- <include
- android:id="@+id/folder_page_indicator"
- android:layout_width="wrap_content"
- android:layout_height="12dp"
- android:layout_gravity="center_horizontal"
- layout="@layout/page_indicator" />
+ android:orientation="horizontal"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp" >
<com.android.launcher3.FolderEditText
android:id="@+id/folder_name"
- android:layout_width="match_parent"
+ android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
+ android:layout_weight="1"
android:background="#00000000"
android:fontFamily="sans-serif-condensed"
android:gravity="center_horizontal"
@@ -72,6 +68,13 @@
android:textColorHint="#ff808080"
android:textCursorDrawable="@null"
android:textSize="14sp" />
+
+ <include
+ android:id="@+id/folder_page_indicator"
+ android:layout_width="wrap_content"
+ android:layout_height="12dp"
+ android:layout_gravity="center_vertical"
+ layout="@layout/page_indicator" />
</LinearLayout>
</com.android.launcher3.Folder>
\ No newline at end of file
diff --git a/res/values-sw600dp/dimens.xml b/res/values-sw600dp/dimens.xml
index 2bdd4f0..4a869e5 100644
--- a/res/values-sw600dp/dimens.xml
+++ b/res/values-sw600dp/dimens.xml
@@ -21,7 +21,6 @@
<dimen name="apps_container_inset">18dp</dimen>
<dimen name="apps_grid_view_start_margin">0dp</dimen>
<dimen name="apps_view_section_text_size">26sp</dimen>
- <dimen name="apps_view_row_height">72dp</dimen>
<dimen name="apps_icon_top_bottom_padding">12dp</dimen>
<!-- AppsCustomize -->
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index f944d4b..1a92545 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -50,17 +50,17 @@
<dimen name="apps_container_width">0dp</dimen>
<dimen name="apps_container_height">0dp</dimen>
<dimen name="apps_container_inset">8dp</dimen>
- <dimen name="apps_grid_view_start_margin">52dp</dimen>
+ <dimen name="apps_grid_view_start_margin">56dp</dimen>
<dimen name="apps_grid_section_y_offset">8dp</dimen>
- <dimen name="apps_view_row_height">64dp</dimen>
<dimen name="apps_view_section_text_size">24sp</dimen>
- <dimen name="apps_view_fast_scroll_bar_width">6dp</dimen>
+ <dimen name="apps_view_fast_scroll_bar_width">4dp</dimen>
<dimen name="apps_view_fast_scroll_bar_min_height">64dp</dimen>
<dimen name="apps_view_fast_scroll_scrubber_touch_inset">-16dp</dimen>
- <dimen name="apps_view_fast_scroll_popup_size">64dp</dimen>
- <dimen name="apps_view_fast_scroll_text_size">40dp</dimen>
+ <dimen name="apps_view_fast_scroll_popup_size">72dp</dimen>
+ <dimen name="apps_view_fast_scroll_text_size">48dp</dimen>
<dimen name="apps_search_bar_height">52dp</dimen>
<dimen name="apps_icon_top_bottom_padding">8dp</dimen>
+ <dimen name="apps_prediction_icon_top_bottom_padding">12dp</dimen>
<!-- Note: This needs to match the fixed insets for the search box. -->
<dimen name="container_fixed_bounds_inset">8dp</dimen>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 16d4cbe..78cc083 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -19,7 +19,7 @@
<resources>
- <style name="WorkspaceIcon">
+ <style name="Icon">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">match_parent</item>
<item name="android:layout_gravity">center</item>
@@ -32,11 +32,7 @@
<item name="android:fontFamily">sans-serif-condensed</item>
</style>
- <style name="WorkspaceIcon.Portrait"></style>
-
- <style name="WorkspaceIcon.Landscape"></style>
-
- <style name="WorkspaceIcon.AppsCustomize">
+ <style name="Icon.AllApps">
<item name="android:background">@null</item>
<item name="android:textColor">@color/quantum_panel_text_color</item>
<item name="android:drawablePadding">@dimen/dynamic_grid_icon_drawable_padding</item>
@@ -44,7 +40,7 @@
<item name="customShadows">false</item>
</style>
- <style name="WorkspaceIcon.Folder">
+ <style name="Icon.Folder">
<item name="android:background">@null</item>
<item name="android:textColor">@color/quantum_panel_text_color</item>
<item name="android:shadowRadius">0</item>
diff --git a/src/com/android/launcher3/AlphabeticalAppsList.java b/src/com/android/launcher3/AlphabeticalAppsList.java
index dc75637..eff7b06 100644
--- a/src/com/android/launcher3/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/AlphabeticalAppsList.java
@@ -151,11 +151,13 @@
* Info about a particular adapter item (can be either section or app)
*/
public static class AdapterItem {
- /** Section & App properties */
+ /** Common properties */
// The index of this adapter item in the list
public int position;
- // Whether or not the item at this adapter position is a section or not
- public boolean isSectionHeader;
+ // The type of this item
+ public int viewType;
+
+ /** Section & App properties */
// The section for this item
public SectionInfo sectionInfo;
@@ -169,30 +171,33 @@
public AppInfo appInfo = null;
// The index of this app not including sections
public int appIndex = -1;
- // Whether or not this is a predicted app
- public boolean isPredictedApp;
public static AdapterItem asSectionBreak(int pos, SectionInfo section) {
AdapterItem item = new AdapterItem();
+ item.viewType = AppsGridAdapter.SECTION_BREAK_VIEW_TYPE;
item.position = pos;
- item.isSectionHeader = true;
item.sectionInfo = section;
section.sectionBreakItem = item;
return item;
}
- public static AdapterItem asApp(int pos, SectionInfo section, String sectionName,
- int sectionAppIndex, AppInfo appInfo, int appIndex,
- boolean isPredictedApp) {
+ public static AdapterItem asPredictionBarSpacer(int pos) {
AdapterItem item = new AdapterItem();
+ item.viewType = AppsGridAdapter.PREDICTION_BAR_SPACER_TYPE;
item.position = pos;
- item.isSectionHeader = false;
+ return item;
+ }
+
+ public static AdapterItem asApp(int pos, SectionInfo section, String sectionName,
+ int sectionAppIndex, AppInfo appInfo, int appIndex) {
+ AdapterItem item = new AdapterItem();
+ item.viewType = AppsGridAdapter.ICON_VIEW_TYPE;
+ item.position = pos;
item.sectionInfo = section;
item.sectionName = sectionName;
item.sectionAppIndex = sectionAppIndex;
item.appInfo = appInfo;
item.appIndex = appIndex;
- item.isPredictedApp = isPredictedApp;
return item;
}
}
@@ -205,6 +210,13 @@
}
/**
+ * A callback to notify of changes to the filter.
+ */
+ public interface FilterChangedCallback {
+ void onFilterChanged();
+ }
+
+ /**
* Common interface for different merging strategies.
*/
private interface MergeAlgorithm {
@@ -260,28 +272,31 @@
private List<AdapterItem> mSectionedFilteredApps = new ArrayList<>();
private List<SectionInfo> mSections = new ArrayList<>();
private List<FastScrollSectionInfo> mFastScrollerSections = new ArrayList<>();
- private List<ComponentName> mPredictedApps = new ArrayList<>();
+ private List<ComponentName> mPredictedAppComponents = new ArrayList<>();
+ private List<AppInfo> mPredictedApps = new ArrayList<>();
private HashMap<CharSequence, String> mCachedSectionNames = new HashMap<>();
private RecyclerView.Adapter mAdapter;
private Filter mFilter;
private AlphabeticIndexCompat mIndexer;
private AppNameComparator mAppNameComparator;
private MergeAlgorithm mMergeAlgorithm;
+ private FilterChangedCallback mFilterChangedCallback;
private int mNumAppsPerRow;
+ private int mNumPredictedAppsPerRow;
- public AlphabeticalAppsList(Context context, int numAppsPerRow) {
+ public AlphabeticalAppsList(Context context, FilterChangedCallback cb, int numAppsPerRow,
+ int numPredictedAppsPerRow) {
mContext = context;
mIndexer = new AlphabeticIndexCompat(context);
mAppNameComparator = new AppNameComparator(context);
- setNumAppsPerRow(numAppsPerRow);
+ mFilterChangedCallback = cb;
+ setNumAppsPerRow(numAppsPerRow, numPredictedAppsPerRow);
}
/**
* Sets the number of apps per row. Used only for AppsContainerView.SECTIONED_GRID_COALESCED.
*/
- public void setNumAppsPerRow(int numAppsPerRow) {
- mNumAppsPerRow = numAppsPerRow;
-
+ public void setNumAppsPerRow(int numAppsPerRow, int numPredictedAppsPerRow) {
// Update the merge algorithm
DeviceProfile grid = LauncherAppState.getInstance().getDynamicGrid().getDeviceProfile();
if (grid.isPhone()) {
@@ -291,6 +306,9 @@
mMergeAlgorithm = new TabletMergeAlgorithm();
}
+ mNumAppsPerRow = numAppsPerRow;
+ mNumPredictedAppsPerRow = numPredictedAppsPerRow;
+
onAppsUpdated();
}
@@ -351,6 +369,9 @@
mFilter = f;
onAppsUpdated();
mAdapter.notifyDataSetChanged();
+ if (mFilterChangedCallback != null){
+ mFilterChangedCallback.onFilterChanged();
+ }
}
}
@@ -359,13 +380,20 @@
* of applications, we should merge the results only in onAppsUpdated() which is idempotent.
*/
public void setPredictedApps(List<ComponentName> apps) {
- mPredictedApps.clear();
- mPredictedApps.addAll(apps);
+ mPredictedAppComponents.clear();
+ mPredictedAppComponents.addAll(apps);
onAppsUpdated();
mAdapter.notifyDataSetChanged();
}
/**
+ * Returns the current set of predicted apps.
+ */
+ public List<AppInfo> getPredictedApps() {
+ return mPredictedApps;
+ }
+
+ /**
* Sets the current set of apps.
*/
public void setApps(List<AppInfo> apps) {
@@ -450,6 +478,42 @@
// Sort the list of apps
Collections.sort(mApps, mAppNameComparator.getAppInfoComparator());
+ // Prepare to update the list of sections, filtered apps, etc.
+ mFilteredApps.clear();
+ mSections.clear();
+ mSectionedFilteredApps.clear();
+ mFastScrollerSections.clear();
+ SectionInfo lastSectionInfo = null;
+ String lastSectionName = null;
+ FastScrollSectionInfo lastFastScrollerSectionInfo = null;
+ int position = 0;
+ int appIndex = 0;
+ List<AppInfo> allApps = new ArrayList<>();
+
+
+ // Process the predicted app components
+ mPredictedApps.clear();
+ if (mPredictedAppComponents != null && !mPredictedAppComponents.isEmpty() && !hasFilter()) {
+ for (ComponentName cn : mPredictedAppComponents) {
+ for (AppInfo info : mApps) {
+ if (cn.equals(info.componentName)) {
+ mPredictedApps.add(info);
+ break;
+ }
+ }
+ // Stop at the number of predicted apps
+ if (mPredictedApps.size() == mNumPredictedAppsPerRow) {
+ break;
+ }
+ }
+
+ if (!mPredictedApps.isEmpty()) {
+ // Create a new spacer for the prediction bar
+ AdapterItem sectionItem = AdapterItem.asPredictionBarSpacer(position++);
+ mSectionedFilteredApps.add(sectionItem);
+ }
+ }
+
// As a special case for some languages (currently only Simplified Chinese), we may need to
// coalesce sections
Locale curLocale = mContext.getResources().getConfiguration().locale;
@@ -475,6 +539,11 @@
}
sectionApps.add(info);
}
+
+ // Add it to the list
+ for (Map.Entry<String, ArrayList<AppInfo>> entry : sectionMap.entrySet()) {
+ allApps.addAll(entry.getValue());
+ }
} else {
// Just compute the section headers for use below
for (AppInfo info : mApps) {
@@ -485,44 +554,7 @@
mCachedSectionNames.put(info.title, sectionName);
}
}
- }
-
- // Prepare to update the list of sections, filtered apps, etc.
- mFilteredApps.clear();
- mSections.clear();
- mSectionedFilteredApps.clear();
- mFastScrollerSections.clear();
- SectionInfo lastSectionInfo = null;
- String lastSectionName = null;
- FastScrollSectionInfo lastFastScrollerSectionInfo = null;
- int position = 0;
- int appIndex = 0;
- List<AppInfo> allApps = new ArrayList<>();
-
- // Add the predicted apps to the combined list
- int numPredictedApps = 0;
- if (mPredictedApps != null && !mPredictedApps.isEmpty() && !hasFilter()) {
- for (ComponentName cn : mPredictedApps) {
- for (AppInfo info : mApps) {
- if (cn.equals(info.componentName)) {
- allApps.add(info);
- numPredictedApps++;
- break;
- }
- }
- // Stop at the number of predicted apps
- if (numPredictedApps == mNumAppsPerRow) {
- break;
- }
- }
- }
-
- // Add all the other apps to the combined list
- if (localeRequiresSectionSorting) {
- for (Map.Entry<String, ArrayList<AppInfo>> entry : sectionMap.entrySet()) {
- allApps.addAll(entry.getValue());
- }
- } else {
+ // Add it to the list
allApps.addAll(mApps);
}
@@ -530,10 +562,9 @@
// ordered set of sections
int numApps = allApps.size();
for (int i = 0; i < numApps; i++) {
- boolean isPredictedApp = i < numPredictedApps;
AppInfo info = allApps.get(i);
// The section name was computed above so this should be find
- String sectionName = isPredictedApp ? "" : mCachedSectionNames.get(info.title);
+ String sectionName = mCachedSectionNames.get(info.title);
// Check if we want to retain this app
if (mFilter != null && !mFilter.retainApp(info, sectionName)) {
@@ -541,8 +572,7 @@
}
// Create a new section if the section names do not match
- if (lastSectionInfo == null ||
- (!isPredictedApp && !sectionName.equals(lastSectionName))) {
+ if (lastSectionInfo == null || !sectionName.equals(lastSectionName)) {
lastSectionName = sectionName;
lastSectionInfo = new SectionInfo();
lastFastScrollerSectionInfo = new FastScrollSectionInfo(sectionName,
@@ -559,7 +589,7 @@
// Create an app item
AdapterItem appItem = AdapterItem.asApp(position++, lastSectionInfo, sectionName,
- lastSectionInfo.numApps++, info, appIndex++, isPredictedApp);
+ lastSectionInfo.numApps++, info, appIndex++);
if (lastSectionInfo.firstAppItem == null) {
lastSectionInfo.firstAppItem = appItem;
lastFastScrollerSectionInfo.appItem = appItem;
@@ -568,6 +598,14 @@
mFilteredApps.add(info);
}
+ // Merge multiple sections together as requested by the merge strategy for this device
+ mergeSections();
+ }
+
+ /**
+ * Merges multiple sections to reduce visual raggedness.
+ */
+ private void mergeSections() {
// Go through each section and try and merge some of the sections
if (AppsContainerView.GRID_MERGE_SECTIONS && !hasFilter()) {
int sectionAppCount = 0;
diff --git a/src/com/android/launcher3/AppsContainerRecyclerView.java b/src/com/android/launcher3/AppsContainerRecyclerView.java
index edb6f0c..3952923 100644
--- a/src/com/android/launcher3/AppsContainerRecyclerView.java
+++ b/src/com/android/launcher3/AppsContainerRecyclerView.java
@@ -38,10 +38,26 @@
*/
public class AppsContainerRecyclerView extends BaseContainerRecyclerView {
+ /**
+ * The current scroll state of the recycler view. We use this in updateVerticalScrollbarBounds()
+ * and scrollToPositionAtProgress() to determine the scroll position of the recycler view so
+ * that we can calculate what the scroll bar looks like, and where to jump to from the fast
+ * scroller.
+ */
+ private static class ScrollPositionState {
+ // The index of the first visible row
+ int rowIndex;
+ // The offset of the first visible row
+ int rowTopOffset;
+ // The height of a given row (they are currently all the same height)
+ int rowHeight;
+ }
+
private static final float FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR = 1.5f;
private AlphabeticalAppsList mApps;
private int mNumAppsPerRow;
+ private int mNumPredictedAppsPerRow;
private Drawable mScrollbar;
private Drawable mFastScrollerBg;
@@ -51,6 +67,7 @@
private Paint mFastScrollTextPaint;
private Rect mFastScrollTextBounds = new Rect();
private float mFastScrollAlpha;
+ private int mPredictionBarHeight;
private int mDownX;
private int mDownY;
private int mLastX;
@@ -58,6 +75,8 @@
private int mScrollbarWidth;
private int mScrollbarMinHeight;
private int mScrollbarInset;
+ private Rect mBackgroundPadding = new Rect();
+ private ScrollPositionState mScrollPosState = new ScrollPositionState();
public AppsContainerRecyclerView(Context context) {
this(context, null);
@@ -91,6 +110,7 @@
mScrollbarInset =
res.getDimensionPixelSize(R.dimen.apps_view_fast_scroll_scrubber_touch_inset);
setFastScrollerAlpha(getFastScrollerAlpha());
+ setOverScrollMode(View.OVER_SCROLL_NEVER);
}
/**
@@ -103,8 +123,22 @@
/**
* Sets the number of apps per row in this recycler view.
*/
- public void setNumAppsPerRow(int rowSize) {
- mNumAppsPerRow = rowSize;
+ public void setNumAppsPerRow(int numAppsPerRow, int numPredictedAppsPerRow) {
+ mNumAppsPerRow = numAppsPerRow;
+ mNumPredictedAppsPerRow = numPredictedAppsPerRow;
+ }
+
+ @Override
+ public void setBackground(Drawable background) {
+ super.setBackground(background);
+ background.getPadding(mBackgroundPadding);
+ }
+
+ /**
+ * Sets the prediction bar height.
+ */
+ public void setPredictionBarHeight(int height) {
+ mPredictionBarHeight = height;
}
/**
@@ -129,6 +163,14 @@
return mScrollbarWidth;
}
+ /**
+ * Scrolls this recycler view to the top.
+ */
+ public void scrollToTop() {
+ scrollToPosition(0);
+ updateScrollY(0);
+ }
+
@Override
protected void onFinishInflate() {
super.onFinishInflate();
@@ -238,7 +280,7 @@
// Calculate the position for the fast scroller popup
Rect bgBounds = mFastScrollerBg.getBounds();
if (isRtl) {
- x = getPaddingLeft() + getScrollBarSize();
+ x = mBackgroundPadding.left + getScrollBarSize();
} else {
x = getWidth() - getPaddingRight() - getScrollBarSize() - bgBounds.width();
}
@@ -281,7 +323,7 @@
* Invalidates the fast scroller popup.
*/
private void invalidateFastScroller() {
- invalidate(getWidth() - getPaddingRight() - getScrollBarSize() -
+ invalidate(getWidth() - mBackgroundPadding.right - getScrollBarSize() -
mFastScrollerBg.getIntrinsicWidth(), 0, getWidth(), getHeight());
}
@@ -296,6 +338,26 @@
return "";
}
+ // Stop the scroller if it is scrolling
+ LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager();
+ stopScroll();
+
+ // If there is a prediction bar, then capture the appropriate area for the prediction bar
+ float predictionBarFraction = 0f;
+ if (mPredictionBarHeight > 0) {
+ predictionBarFraction = (float) mNumPredictedAppsPerRow / mApps.getSize();
+ if (touchFraction <= predictionBarFraction) {
+ // Scroll to the top of the view, where the prediction bar is
+ layoutManager.scrollToPositionWithOffset(0, 0);
+ updateScrollY(0);
+ return "";
+ }
+ }
+
+ // Since the app ranges are from 0..1, we need to map the touch fraction back to 0..1 from
+ // predictionBarFraction..1
+ touchFraction = (touchFraction - predictionBarFraction) *
+ (1f / (1f - predictionBarFraction));
AlphabeticalAppsList.FastScrollSectionInfo lastScrollSection = fastScrollSections.get(0);
for (int i = 1; i < fastScrollSections.size(); i++) {
AlphabeticalAppsList.FastScrollSectionInfo scrollSection = fastScrollSections.get(i);
@@ -306,10 +368,17 @@
lastScrollSection = scrollSection;
}
- // Scroll the position into view, anchored at the top of the screen if possible. We call the
- // scroll method on the LayoutManager directly since it is not exposed by RecyclerView.
- LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager();
- stopScroll();
+ // We need to workaround the RecyclerView to get the right scroll position
+ List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
+ getCurScrollState(mScrollPosState, items);
+ if (mScrollPosState.rowIndex != -1) {
+ int scrollY = getPaddingTop() + (mScrollPosState.rowIndex * mScrollPosState.rowHeight) +
+ mPredictionBarHeight - mScrollPosState.rowTopOffset;
+ updateScrollY(scrollY);
+ }
+
+ // Scroll to the view at the position, anchored at the top of the screen. We call the scroll
+ // method on the LayoutManager directly since it is not exposed by RecyclerView.
layoutManager.scrollToPositionWithOffset(lastScrollSection.appItem.position, 0);
return lastScrollSection.sectionName;
@@ -332,44 +401,28 @@
int y;
boolean isRtl = (getResources().getConfiguration().getLayoutDirection() ==
LAYOUT_DIRECTION_RTL);
- int rowIndex = -1;
- int rowTopOffset = -1;
- int rowHeight = -1;
int rowCount = getNumRows();
- int childCount = getChildCount();
- for (int i = 0; i < childCount; i++) {
- View child = getChildAt(i);
- int position = getChildPosition(child);
- if (position != NO_POSITION) {
- AlphabeticalAppsList.AdapterItem item = items.get(position);
- if (!item.isSectionHeader) {
- rowIndex = findRowForAppIndex(item.appIndex);
- rowTopOffset = getLayoutManager().getDecoratedTop(child);
- rowHeight = child.getHeight();
- break;
- }
- }
- }
-
- if (rowIndex != -1) {
+ getCurScrollState(mScrollPosState, items);
+ if (mScrollPosState.rowIndex != -1) {
int height = getHeight() - getPaddingTop() - getPaddingBottom();
- int totalScrollHeight = rowCount * rowHeight;
+ int totalScrollHeight = rowCount * mScrollPosState.rowHeight + mPredictionBarHeight;
if (totalScrollHeight > height) {
int scrollbarHeight = Math.max(mScrollbarMinHeight,
(int) (height / ((float) totalScrollHeight / height)));
// Calculate the position and size of the scroll bar
if (isRtl) {
- x = getPaddingLeft();
+ x = mBackgroundPadding.left;
} else {
- x = getWidth() - getPaddingRight() - mScrollbarWidth;
+ x = getWidth() - mBackgroundPadding.right - mScrollbarWidth;
}
// To calculate the offset, we compute the percentage of the total scrollable height
// that the user has already scrolled and then map that to the scroll bar bounds
int availableY = totalScrollHeight - height;
int availableScrollY = height - scrollbarHeight;
- y = (rowIndex * rowHeight) - rowTopOffset;
+ y = (mScrollPosState.rowIndex * mScrollPosState.rowHeight) + mPredictionBarHeight
+ - mScrollPosState.rowTopOffset;
y = getPaddingTop() +
(int) (((float) (getPaddingTop() + y) / availableY) * availableScrollY);
@@ -410,4 +463,28 @@
}
return rowCount;
}
+
+ /**
+ * Returns the current scroll state.
+ */
+ private void getCurScrollState(ScrollPositionState stateOut,
+ List<AlphabeticalAppsList.AdapterItem> items) {
+ stateOut.rowIndex = -1;
+ stateOut.rowTopOffset = -1;
+ stateOut.rowHeight = -1;
+ int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ int position = getChildPosition(child);
+ if (position != NO_POSITION) {
+ AlphabeticalAppsList.AdapterItem item = items.get(position);
+ if (item.viewType == AppsGridAdapter.ICON_VIEW_TYPE) {
+ stateOut.rowIndex = findRowForAppIndex(item.appIndex);
+ stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child);
+ stateOut.rowHeight = child.getHeight();
+ break;
+ }
+ }
+ }
+ }
}
diff --git a/src/com/android/launcher3/AppsContainerView.java b/src/com/android/launcher3/AppsContainerView.java
index 8a5c660..692c23f 100644
--- a/src/com/android/launcher3/AppsContainerView.java
+++ b/src/com/android/launcher3/AppsContainerView.java
@@ -26,12 +26,14 @@
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.KeyEvent;
+import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
+import android.view.ViewGroup;
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.TextView;
import com.android.launcher3.util.Thunk;
@@ -44,16 +46,18 @@
* The all apps view container.
*/
public class AppsContainerView extends BaseContainerView implements DragSource, Insettable,
- TextWatcher, TextView.OnEditorActionListener, LauncherTransitionable, View.OnTouchListener,
- View.OnClickListener, View.OnLongClickListener {
+ TextWatcher, TextView.OnEditorActionListener, LauncherTransitionable,
+ AlphabeticalAppsList.FilterChangedCallback, AppsGridAdapter.PredictionBarSpacerCallbacks,
+ View.OnTouchListener, View.OnClickListener, View.OnLongClickListener {
public static final boolean GRID_MERGE_SECTIONS = true;
- public static final boolean GRID_HIDE_SECTION_HEADERS = false;
private static final boolean ALLOW_SINGLE_APP_LAUNCH = true;
- private static final boolean DYNAMIC_HEADER_ELEVATION = false;
+ private static final boolean DYNAMIC_HEADER_ELEVATION = true;
private static final boolean DISMISS_SEARCH_ON_BACK = true;
private static final float HEADER_ELEVATION_DP = 4;
+ // How far the user has to scroll in order to reach the full elevation
+ private static final float HEADER_SCROLL_TO_ELEVATION_DP = 16;
private static final int FADE_IN_DURATION = 175;
private static final int FADE_OUT_DURATION = 100;
private static final int SEARCH_TRANSLATION_X_DP = 18;
@@ -62,12 +66,14 @@
@Thunk Launcher mLauncher;
@Thunk AlphabeticalAppsList mApps;
+ private LayoutInflater mLayoutInflater;
private AppsGridAdapter mAdapter;
private RecyclerView.LayoutManager mLayoutManager;
private RecyclerView.ItemDecoration mItemDecoration;
- private LinearLayout mContentView;
+ private FrameLayout mContentView;
@Thunk AppsContainerRecyclerView mAppsRecyclerView;
+ private ViewGroup mPredictionBarView;
private View mHeaderView;
private View mSearchBarContainerView;
private View mSearchButtonView;
@@ -75,11 +81,13 @@
private AppsContainerSearchEditTextView mSearchBarEditView;
private int mNumAppsPerRow;
+ private int mNumPredictedAppsPerRow;
private Point mLastTouchDownPos = new Point(-1, -1);
private Point mLastTouchPos = new Point();
private int mContentMarginStart;
// Normal container insets
private int mContainerInset;
+ private int mPredictionBarHeight;
// RecyclerView scroll position
@Thunk int mRecyclerViewScrollY;
@@ -99,12 +107,17 @@
mContainerInset = context.getResources().getDimensionPixelSize(
R.dimen.apps_container_inset);
+ mPredictionBarHeight = grid.allAppsCellHeightPx +
+ 2 * res.getDimensionPixelSize(R.dimen.apps_prediction_icon_top_bottom_padding);
mLauncher = (Launcher) context;
+ mLayoutInflater = LayoutInflater.from(context);
mNumAppsPerRow = grid.appsViewNumCols;
- mApps = new AlphabeticalAppsList(context, mNumAppsPerRow);
- mAdapter = new AppsGridAdapter(context, mApps, mNumAppsPerRow, this, mLauncher, this);
+ mNumPredictedAppsPerRow = grid.appsViewNumPredictiveCols;
+ mApps = new AlphabeticalAppsList(context, this, mNumAppsPerRow, mNumPredictedAppsPerRow);
+ mAdapter = new AppsGridAdapter(context, mApps, mNumAppsPerRow, this, this, mLauncher, this);
mAdapter.setEmptySearchText(res.getString(R.string.loading_apps_message));
mAdapter.setNumAppsPerRow(mNumAppsPerRow);
+ mAdapter.setPredictionRowHeight(mPredictionBarHeight);
mLayoutManager = mAdapter.getLayoutManager();
mItemDecoration = mAdapter.getItemDecoration();
mContentMarginStart = mAdapter.getContentMarginStart();
@@ -159,8 +172,7 @@
* Scrolls this list view to the top.
*/
public void scrollToTop() {
- mAppsRecyclerView.scrollToPosition(0);
- mRecyclerViewScrollY = 0;
+ mAppsRecyclerView.scrollToTop();
}
/**
@@ -185,7 +197,7 @@
// Work around the search box getting first focus and showing the cursor by
// proxying the focus from the content view to the recycler view directly
- mContentView = (LinearLayout) findViewById(R.id.apps_list);
+ mContentView = (FrameLayout) findViewById(R.id.apps_list);
mContentView.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
@@ -194,12 +206,20 @@
}
}
});
+
+ // Fix the header view elevation if not dynamically calculating it
mHeaderView = findViewById(R.id.header);
mHeaderView.setOnClickListener(this);
if (Utilities.isLmpOrAbove() && !DYNAMIC_HEADER_ELEVATION) {
mHeaderView.setElevation(DynamicGrid.pxFromDp(HEADER_ELEVATION_DP,
getContext().getResources().getDisplayMetrics()));
}
+
+ // Fix the prediction bar size
+ mPredictionBarView = (ViewGroup) findViewById(R.id.prediction_bar);
+ FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mPredictionBarView.getLayoutParams();
+ lp.height = mPredictionBarHeight;
+
mSearchButtonView = mHeaderView.findViewById(R.id.search_button);
mSearchBarContainerView = findViewById(R.id.app_search_container);
mDismissSearchButtonView = mSearchBarContainerView.findViewById(R.id.dismiss_search_button);
@@ -226,22 +246,19 @@
}
mAppsRecyclerView = (AppsContainerRecyclerView) findViewById(R.id.apps_list_view);
mAppsRecyclerView.setApps(mApps);
- mAppsRecyclerView.setNumAppsPerRow(mNumAppsPerRow);
+ mAppsRecyclerView.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow);
+ mAppsRecyclerView.setPredictionBarHeight(mPredictionBarHeight);
mAppsRecyclerView.setLayoutManager(mLayoutManager);
mAppsRecyclerView.setAdapter(mAdapter);
mAppsRecyclerView.setHasFixedSize(true);
- mAppsRecyclerView.setOnScrollListenerProxy(new RecyclerView.OnScrollListener() {
- @Override
- public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
- // Do nothing
- }
-
- @Override
- public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
- mRecyclerViewScrollY += dy;
- onRecyclerViewScrolled();
- }
- });
+ mAppsRecyclerView.setOnScrollListenerProxy(
+ new BaseContainerRecyclerView.OnScrollToListener() {
+ @Override
+ public void onScrolledTo(int x, int y) {
+ mRecyclerViewScrollY = y;
+ onRecyclerViewScrolled();
+ }
+ });
if (mItemDecoration != null) {
mAppsRecyclerView.addItemDecoration(mItemDecoration);
}
@@ -250,15 +267,52 @@
}
@Override
+ public void onBindPredictionBar() {
+ if (!updatePredictionBarVisibility()) {
+ return;
+ }
+
+ List<AppInfo> predictedApps = mApps.getPredictedApps();
+ int childCount = mPredictionBarView.getChildCount();
+ for (int i = 0; i < mNumPredictedAppsPerRow; i++) {
+ BubbleTextView icon;
+ if (i < childCount) {
+ // If a child at that index exists, then get that child
+ icon = (BubbleTextView) mPredictionBarView.getChildAt(i);
+ } else {
+ // Otherwise, inflate a new icon
+ icon = (BubbleTextView) mLayoutInflater.inflate(
+ R.layout.apps_prediction_bar_icon_view, mPredictionBarView, false);
+ icon.setOnTouchListener(this);
+ icon.setOnClickListener(mLauncher);
+ icon.setOnLongClickListener(this);
+ icon.setFocusable(true);
+ mPredictionBarView.addView(icon);
+ }
+
+ // Either apply the app info to the child, or hide the view
+ if (i < predictedApps.size()) {
+ if (icon.getVisibility() != View.VISIBLE) {
+ icon.setVisibility(View.VISIBLE);
+ }
+ icon.applyFromApplicationInfo(predictedApps.get(i));
+ } else {
+ icon.setVisibility(View.INVISIBLE);
+ }
+ }
+ }
+
+ @Override
protected void onFixedBoundsUpdated() {
// Update the number of items in the grid
LauncherAppState app = LauncherAppState.getInstance();
DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
if (grid.updateAppsViewNumCols(getContext().getResources(), mFixedBounds.width())) {
mNumAppsPerRow = grid.appsViewNumCols;
- mAppsRecyclerView.setNumAppsPerRow(mNumAppsPerRow);
+ mNumPredictedAppsPerRow = grid.appsViewNumPredictiveCols;
+ mAppsRecyclerView.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow);
mAdapter.setNumAppsPerRow(mNumAppsPerRow);
- mApps.setNumAppsPerRow(mNumAppsPerRow);
+ mApps.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow);
}
}
@@ -291,17 +345,25 @@
int startMargin = grid.isPhone() ? mContentMarginStart : 0;
int inset = mFixedBounds.isEmpty() ? mContainerInset : mFixedBoundsContainerInset;
if (isRtl) {
- mAppsRecyclerView.setPadding(inset, inset, inset + startMargin, inset);
+ mAppsRecyclerView.setPadding(inset + mAppsRecyclerView.getScrollbarWidth(), inset,
+ inset + startMargin, inset);
} else {
- mAppsRecyclerView.setPadding(inset + startMargin, inset, inset, inset);
+ mAppsRecyclerView.setPadding(inset + startMargin, inset,
+ inset + mAppsRecyclerView.getScrollbarWidth(), inset);
}
// Update the header bar
if (hasSearchBar) {
- LinearLayout.LayoutParams lp =
- (LinearLayout.LayoutParams) mHeaderView.getLayoutParams();
+ FrameLayout.LayoutParams lp =
+ (FrameLayout.LayoutParams) mHeaderView.getLayoutParams();
lp.leftMargin = lp.rightMargin = inset;
+ mHeaderView.requestLayout();
}
+
+ FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mPredictionBarView.getLayoutParams();
+ lp.leftMargin = inset + mAppsRecyclerView.getScrollbarWidth();
+ lp.rightMargin = inset + mAppsRecyclerView.getScrollbarWidth();
+ mPredictionBarView.requestLayout();
}
/**
@@ -456,7 +518,10 @@
String formatStr = getResources().getString(R.string.apps_view_no_search_results);
mAdapter.setEmptySearchText(String.format(formatStr, queryText));
+ // Do an intersection of the words in the query and each title, and filter out all the
+ // apps that don't match all of the words in the query.
final String queryTextLower = queryText.toLowerCase();
+ final String[] queryWords = SPLIT_PATTERN.split(queryTextLower);
mApps.setFilter(new AlphabeticalAppsList.Filter() {
@Override
public boolean retainApp(AppInfo info, String sectionName) {
@@ -465,12 +530,21 @@
}
String title = info.title.toString();
String[] words = SPLIT_PATTERN.split(title.toLowerCase());
- for (int i = 0; i < words.length; i++) {
- if (words[i].startsWith(queryTextLower)) {
- return true;
+ for (int qi = 0; qi < queryWords.length; qi++) {
+ boolean foundMatch = false;
+ for (int i = 0; i < words.length; i++) {
+ if (words[i].startsWith(queryWords[qi])) {
+ foundMatch = true;
+ break;
+ }
+ }
+ if (!foundMatch) {
+ // If there is a word in the query that does not match any words in this
+ // title, so skip it.
+ return false;
}
}
- return false;
+ return true;
}
});
}
@@ -488,7 +562,7 @@
List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
for (int i = 0; i < items.size(); i++) {
AlphabeticalAppsList.AdapterItem item = items.get(i);
- if (!item.isSectionHeader) {
+ if (item.viewType == AppsGridAdapter.ICON_VIEW_TYPE) {
mAppsRecyclerView.getChildAt(i).performClick();
getInputMethodManager().hideSoftInputFromWindow(getWindowToken(), 0);
return true;
@@ -499,6 +573,11 @@
}
@Override
+ public void onFilterChanged() {
+ updatePredictionBarVisibility();
+ }
+
+ @Override
public View getContent() {
return null;
}
@@ -531,13 +610,20 @@
* Updates the container when the recycler view is scrolled.
*/
private void onRecyclerViewScrolled() {
- if (DYNAMIC_HEADER_ELEVATION) {
- int elevation = Math.min(mRecyclerViewScrollY, DynamicGrid.pxFromDp(HEADER_ELEVATION_DP,
- getContext().getResources().getDisplayMetrics()));
- if (Float.compare(mHeaderView.getElevation(), elevation) != 0) {
- mHeaderView.setElevation(elevation);
+ if (DYNAMIC_HEADER_ELEVATION && Utilities.isLmpOrAbove()) {
+ int elevation = DynamicGrid.pxFromDp(HEADER_ELEVATION_DP,
+ getContext().getResources().getDisplayMetrics());
+ int scrollToElevation = DynamicGrid.pxFromDp(HEADER_SCROLL_TO_ELEVATION_DP,
+ getContext().getResources().getDisplayMetrics());
+ float elevationPct = (float) Math.min(mRecyclerViewScrollY, scrollToElevation) /
+ scrollToElevation;
+ float newElevation = elevation * elevationPct;
+ if (Float.compare(mHeaderView.getElevation(), newElevation) != 0) {
+ mHeaderView.setElevation(newElevation);
}
}
+
+ mPredictionBarView.setTranslationY(-mRecyclerViewScrollY + mAppsRecyclerView.getPaddingTop());
}
/**
@@ -667,6 +753,21 @@
}
/**
+ * Updates the visibility of the prediction bar.
+ * @return whether the prediction bar is visible
+ */
+ private boolean updatePredictionBarVisibility() {
+ boolean showPredictionBar = !mApps.getPredictedApps().isEmpty() && (!mApps.hasFilter() ||
+ mSearchBarEditView.getEditableText().toString().isEmpty());
+ if (showPredictionBar) {
+ mPredictionBarView.setVisibility(View.VISIBLE);
+ } else if (!showPredictionBar) {
+ mPredictionBarView.setVisibility(View.INVISIBLE);
+ }
+ return showPredictionBar;
+ }
+
+ /**
* Returns an input method manager.
*/
private InputMethodManager getInputMethodManager() {
diff --git a/src/com/android/launcher3/AppsGridAdapter.java b/src/com/android/launcher3/AppsGridAdapter.java
index a6902d5..dfbfa01 100644
--- a/src/com/android/launcher3/AppsGridAdapter.java
+++ b/src/com/android/launcher3/AppsGridAdapter.java
@@ -6,6 +6,7 @@
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.Rect;
+import android.os.Handler;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
@@ -26,21 +27,31 @@
public static final String TAG = "AppsGridAdapter";
private static final boolean DEBUG = false;
- private static final int SECTION_BREAK_VIEW_TYPE = 0;
- private static final int ICON_VIEW_TYPE = 1;
- private static final int EMPTY_VIEW_TYPE = 2;
+ // A section break in the grid
+ public static final int SECTION_BREAK_VIEW_TYPE = 0;
+ // A normal icon
+ public static final int ICON_VIEW_TYPE = 1;
+ // The message shown when there are no filtered results
+ public static final int EMPTY_VIEW_TYPE = 2;
+ // The spacer used for the prediction bar
+ public static final int PREDICTION_BAR_SPACER_TYPE = 3;
+
+ /**
+ * Callback for when the prediction bar spacer is bound.
+ */
+ public interface PredictionBarSpacerCallbacks {
+ void onBindPredictionBar();
+ }
/**
* ViewHolder for each icon.
*/
public static class ViewHolder extends RecyclerView.ViewHolder {
public View mContent;
- public boolean mIsEmptyRow;
- public ViewHolder(View v, boolean isEmptyRow) {
+ public ViewHolder(View v) {
super(v);
mContent = v;
- mIsEmptyRow = isEmptyRow;
}
}
@@ -61,8 +72,8 @@
return mAppsPerRow;
}
- if (mApps.getAdapterItems().get(position).isSectionHeader) {
- // Section break spans full width
+ if (mApps.getAdapterItems().get(position).viewType != AppsGridAdapter.ICON_VIEW_TYPE) {
+ // Both the section breaks and predictive bar span the full width
return mAppsPerRow;
} else {
return 1;
@@ -88,7 +99,7 @@
DeviceProfile grid = LauncherAppState.getInstance().getDynamicGrid().getDeviceProfile();
List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
- boolean hasDrawnPredictedAppDivider = false;
+ boolean hasDrawnPredictedAppsDivider = false;
int childCount = parent.getChildCount();
int lastSectionTop = 0;
int lastSectionHeight = 0;
@@ -99,14 +110,13 @@
continue;
}
- if (shouldDrawItemDivider(holder, items) && !hasDrawnPredictedAppDivider) {
- // Draw the divider under the predicted app
+ if (shouldDrawItemDivider(holder, items) && !hasDrawnPredictedAppsDivider) {
+ // Draw the divider under the predicted apps
+ parent.getBackground().getPadding(mTmpBounds);
int top = child.getTop() + child.getHeight();
- int left = parent.getPaddingLeft();
- int right = parent.getWidth() - parent.getPaddingRight();
- int iconInset = (((right - left) / mAppsPerRow) - grid.allAppsIconSizePx) / 2;
- c.drawLine(left + iconInset, top, right - iconInset, top, mPredictedAppsDividerPaint);
- hasDrawnPredictedAppDivider = true;
+ c.drawLine(mTmpBounds.left, top, parent.getWidth() - mTmpBounds.right, top,
+ mPredictedAppsDividerPaint);
+ hasDrawnPredictedAppsDivider = true;
} else if (grid.isPhone() && shouldDrawItemSection(holder, i, items)) {
// At this point, we only draw sections for each section break;
@@ -221,9 +231,10 @@
/**
* Returns whether to draw the divider for a given child.
*/
- private boolean shouldDrawItemDivider(ViewHolder holder, List<AlphabeticalAppsList.AdapterItem> items) {
+ private boolean shouldDrawItemDivider(ViewHolder holder,
+ List<AlphabeticalAppsList.AdapterItem> items) {
int pos = holder.getPosition();
- return items.get(pos).isPredictedApp;
+ return items.get(pos).viewType == AppsGridAdapter.PREDICTION_BAR_SPACER_TYPE;
}
/**
@@ -234,31 +245,27 @@
int pos = holder.getPosition();
AlphabeticalAppsList.AdapterItem item = items.get(pos);
- // Ensure it's not an empty row
- if (holder.mIsEmptyRow) {
- return false;
- }
- // Ensure this is not a section break
- if (item.isSectionHeader) {
- return false;
- }
- // Ensure this is not a predicted app
- if (item.isPredictedApp) {
+ // Ensure it's an icon
+ if (item.viewType != AppsGridAdapter.ICON_VIEW_TYPE) {
return false;
}
// Draw the section header for the first item in each section
- return (childIndex == 0) || (items.get(pos - 1).isSectionHeader && !item.isSectionHeader);
+ return (childIndex == 0) ||
+ (items.get(pos - 1).viewType == AppsGridAdapter.SECTION_BREAK_VIEW_TYPE);
}
}
+ private Handler mHandler;
private LayoutInflater mLayoutInflater;
@Thunk AlphabeticalAppsList mApps;
private GridLayoutManager mGridLayoutMgr;
private GridSpanSizer mGridSizer;
private GridItemDecoration mItemDecoration;
+ private PredictionBarSpacerCallbacks mPredictionBarCb;
private View.OnTouchListener mTouchListener;
private View.OnClickListener mIconClickListener;
private View.OnLongClickListener mIconLongClickListener;
+ @Thunk int mPredictionBarHeight;
@Thunk int mAppsPerRow;
@Thunk boolean mIsRtl;
private String mEmptySearchText;
@@ -272,11 +279,13 @@
public AppsGridAdapter(Context context, AlphabeticalAppsList apps, int appsPerRow,
- View.OnTouchListener touchListener, View.OnClickListener iconClickListener,
- View.OnLongClickListener iconLongClickListener) {
+ PredictionBarSpacerCallbacks pbCb, View.OnTouchListener touchListener,
+ View.OnClickListener iconClickListener, View.OnLongClickListener iconLongClickListener) {
Resources res = context.getResources();
+ mHandler = new Handler();
mApps = apps;
mAppsPerRow = appsPerRow;
+ mPredictionBarCb = pbCb;
mGridSizer = new GridSpanSizer();
mGridLayoutMgr = new GridLayoutManager(context, appsPerRow, GridLayoutManager.VERTICAL,
false);
@@ -297,8 +306,8 @@
mSectionTextPaint.setAntiAlias(true);
mPredictedAppsDividerPaint = new Paint();
- mPredictedAppsDividerPaint.setStrokeWidth(DynamicGrid.pxFromDp(1.5f, res.getDisplayMetrics()));
- mPredictedAppsDividerPaint.setColor(0x10000000);
+ mPredictedAppsDividerPaint.setStrokeWidth(DynamicGrid.pxFromDp(1f, res.getDisplayMetrics()));
+ mPredictedAppsDividerPaint.setColor(0x1E000000);
mPredictedAppsDividerPaint.setAntiAlias(true);
}
@@ -311,6 +320,13 @@
}
/**
+ * Sets the prediction row height.
+ */
+ public void setPredictionRowHeight(int height) {
+ mPredictionBarHeight = height;
+ }
+
+ /**
* Sets whether we are in RTL mode.
*/
public void setRtl(boolean rtl) {
@@ -351,17 +367,24 @@
switch (viewType) {
case EMPTY_VIEW_TYPE:
return new ViewHolder(mLayoutInflater.inflate(R.layout.apps_empty_view, parent,
- false), true /* isEmptyRow */);
+ false));
case SECTION_BREAK_VIEW_TYPE:
- return new ViewHolder(new View(parent.getContext()), false /* isEmptyRow */);
+ return new ViewHolder(new View(parent.getContext()));
+ case PREDICTION_BAR_SPACER_TYPE:
+ // Create a view of a specific height to match the floating prediction bar
+ View v = new View(parent.getContext());
+ ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, mPredictionBarHeight);
+ v.setLayoutParams(lp);
+ return new ViewHolder(v);
case ICON_VIEW_TYPE:
BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
- R.layout.apps_grid_row_icon_view, parent, false);
+ R.layout.apps_grid_icon_view, parent, false);
icon.setOnTouchListener(mTouchListener);
icon.setOnClickListener(mIconClickListener);
icon.setOnLongClickListener(mIconLongClickListener);
icon.setFocusable(true);
- return new ViewHolder(icon, false /* isEmptyRow */);
+ return new ViewHolder(icon);
default:
throw new RuntimeException("Unexpected view type");
}
@@ -375,6 +398,16 @@
BubbleTextView icon = (BubbleTextView) holder.mContent;
icon.applyFromApplicationInfo(info);
break;
+ case PREDICTION_BAR_SPACER_TYPE:
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (mPredictionBarCb != null) {
+ mPredictionBarCb.onBindPredictionBar();
+ }
+ }
+ });
+ break;
case EMPTY_VIEW_TYPE:
TextView emptyViewText = (TextView) holder.mContent.findViewById(R.id.empty_text);
emptyViewText.setText(mEmptySearchText);
@@ -395,9 +428,9 @@
public int getItemViewType(int position) {
if (mApps.hasNoFilteredResults()) {
return EMPTY_VIEW_TYPE;
- } else if (mApps.getAdapterItems().get(position).isSectionHeader) {
- return SECTION_BREAK_VIEW_TYPE;
}
- return ICON_VIEW_TYPE;
+
+ AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position);
+ return item.viewType;
}
}
diff --git a/src/com/android/launcher3/BaseContainerRecyclerView.java b/src/com/android/launcher3/BaseContainerRecyclerView.java
index 5b30e3d..59e20ca 100644
--- a/src/com/android/launcher3/BaseContainerRecyclerView.java
+++ b/src/com/android/launcher3/BaseContainerRecyclerView.java
@@ -29,12 +29,20 @@
public class BaseContainerRecyclerView extends RecyclerView
implements RecyclerView.OnItemTouchListener {
+ /**
+ * Listener to get notified when the absolute scroll changes.
+ */
+ public interface OnScrollToListener {
+ void onScrolledTo(int x, int y);
+ }
+
private static final int SCROLL_DELTA_THRESHOLD_DP = 4;
/** Keeps the last known scrolling delta/velocity along y-axis. */
@Thunk int mDy = 0;
+ @Thunk int mScrollY;
private float mDeltaThreshold;
- private RecyclerView.OnScrollListener mScrollListenerProxy;
+ private OnScrollToListener mScrollToListener;
public BaseContainerRecyclerView(Context context) {
this(context, null);
@@ -60,8 +68,9 @@
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
mDy = dy;
- if (mScrollListenerProxy != null) {
- mScrollListenerProxy.onScrolled(recyclerView, dx, dy);
+ mScrollY += dy;
+ if (mScrollToListener != null) {
+ mScrollToListener.onScrolledTo(0, mScrollY);
}
}
}
@@ -69,8 +78,8 @@
/**
* Sets an additional scroll listener, only needed for LMR1 version of the support lib.
*/
- public void setOnScrollListenerProxy(RecyclerView.OnScrollListener listener) {
- mScrollListenerProxy = listener;
+ public void setOnScrollListenerProxy(OnScrollToListener listener) {
+ mScrollToListener = listener;
}
@Override
@@ -97,6 +106,17 @@
}
/**
+ * Updates the scroll position, used to workaround a RecyclerView issue with scrolling to
+ * position.
+ */
+ protected void updateScrollY(int scrollY) {
+ mScrollY = scrollY;
+ if (mScrollToListener != null) {
+ mScrollToListener.onScrolledTo(0, mScrollY);
+ }
+ }
+
+ /**
* Returns whether this {@link MotionEvent} should trigger the scroll to be stopped.
*/
protected boolean shouldStopScroll(MotionEvent ev) {
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 3bbf0e7..dc6de07 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -127,6 +127,7 @@
int allAppsNumRows;
int allAppsNumCols;
int appsViewNumCols;
+ int appsViewNumPredictiveCols;
int searchBarSpaceWidthPx;
int searchBarSpaceHeightPx;
int pageIndicatorHeightPx;
@@ -411,7 +412,7 @@
// All Apps
allAppsCellWidthPx = allAppsIconSizePx;
- allAppsCellHeightPx = allAppsIconSizePx + drawablePadding + iconTextSizePx;
+ allAppsCellHeightPx = allAppsIconSizePx + drawablePadding + allAppsIconTextSizePx;
int maxLongEdgeCellCount =
res.getInteger(R.integer.config_dynamic_grid_max_long_edge_cell_count);
int maxShortEdgeCellCount =
@@ -440,10 +441,13 @@
int appsViewLeftMarginPx =
res.getDimensionPixelSize(R.dimen.apps_grid_view_start_margin);
int availableAppsWidthPx = (containerWidth > 0) ? containerWidth : availableWidthPx;
- int numCols = (availableAppsWidthPx - appsViewLeftMarginPx) /
+ int numAppsCols = (availableAppsWidthPx - appsViewLeftMarginPx) /
(allAppsCellWidthPx + 2 * allAppsCellPaddingPx);
- if (numCols != appsViewNumCols) {
- appsViewNumCols = numCols;
+ int numPredictiveAppCols = isPhone() ? numColumns : numAppsCols;
+ if ((numAppsCols != appsViewNumCols) ||
+ (numPredictiveAppCols != appsViewNumPredictiveCols)) {
+ appsViewNumCols = numAppsCols;
+ appsViewNumPredictiveCols = numPredictiveAppCols;
return true;
}
return false;
diff --git a/src/com/android/launcher3/Folder.java b/src/com/android/launcher3/Folder.java
index a955b27..377e8ee 100644
--- a/src/com/android/launcher3/Folder.java
+++ b/src/com/android/launcher3/Folder.java
@@ -90,6 +90,8 @@
*/
private static final float ICON_OVERSCROLL_WIDTH_FACTOR = 0.45f;
+ public static final int FOOTER_ANIMATION_DURATION = 200;
+
private static final int REORDER_DELAY = 250;
private static final int ON_EXIT_CLOSE_DELAY = 400;
private static final Rect sTempRect = new Rect();
@@ -211,10 +213,7 @@
InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_FLAG_CAP_WORDS);
mFooter = findViewById(R.id.folder_footer);
- updateFooterHeight();
- }
- public void updateFooterHeight() {
// We find out how tall footer wants to be (it is set to wrap_content), so that
// we can allocate the appropriate amount of space for it.
int measureSpec = MeasureSpec.UNSPECIFIED;
@@ -547,6 +546,36 @@
mContent.setFocusOnFirstChild();
}
});
+
+ // Footer animation
+ if (mContent.getPageCount() > 1 && !mInfo.hasOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION)) {
+ int footerWidth = mContent.getDesiredWidth()
+ - mFooter.getPaddingLeft() - mFooter.getPaddingRight();
+
+ float textWidth = mFolderName.getPaint().measureText(mFolderName.getText().toString());
+ mFolderName.setTranslationX((footerWidth - textWidth) / 2);
+ mContent.setMarkerScale(0);
+
+ // Do not update the flag if we are in drag mode. The flag will be updated, when we
+ // actually drop the icon.
+ final boolean updateAnimationFlag = !mDragInProgress;
+ openFolderAnim.addListener(new AnimatorListenerAdapter() {
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mFolderName.animate().setDuration(FOOTER_ANIMATION_DURATION).translationX(0);
+ mContent.animateMarkers();
+
+ if (updateAnimationFlag) {
+ mInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, true, mLauncher);
+ }
+ }
+ });
+ } else {
+ mFolderName.setTranslationX(0);
+ mContent.setMarkerScale(1);
+ }
+
openFolderAnim.start();
// Make sure the folder picks up the last drag move even if the finger doesn't move.
@@ -823,6 +852,14 @@
// Reordering may have occured, and we need to save the new item locations. We do this once
// at the end to prevent unnecessary database operations.
updateItemLocationsInDatabaseBatch();
+
+ // Use the item count to check for multi-page as the folder UI may not have
+ // been refreshed yet.
+ if (getItemCount() <= mContent.itemsPerPage()) {
+ // Show the animation, next time something is added to the folder.
+ mInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, false, mLauncher);
+ }
+
}
@Override
@@ -1200,6 +1237,11 @@
// Clear the drag info, as it is no longer being dragged.
mCurrentDragInfo = null;
mDragInProgress = false;
+
+ if (mContent.getPageCount() > 1) {
+ // The animation has already been shown while opening the folder.
+ mInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, true, mLauncher);
+ }
}
// This is used so the item doesn't immediately appear in the folder when added. In one case
@@ -1214,6 +1256,7 @@
v.setVisibility(VISIBLE);
}
+ @Override
public void onAdd(ShortcutInfo item) {
// If the item was dropped onto this open folder, we have done the work associated
// with adding the item to the folder, as indicated by mSuppressOnAdd being set
diff --git a/src/com/android/launcher3/FolderInfo.java b/src/com/android/launcher3/FolderInfo.java
index 9675371..930f911 100644
--- a/src/com/android/launcher3/FolderInfo.java
+++ b/src/com/android/launcher3/FolderInfo.java
@@ -42,6 +42,11 @@
public static final int FLAG_WORK_FOLDER = 0x00000002;
/**
+ * The multi-page animation has run for this folder
+ */
+ public static final int FLAG_MULTI_PAGE_ANIMATION = 0x00000004;
+
+ /**
* Whether this folder has been opened
*/
boolean opened;
diff --git a/src/com/android/launcher3/FolderPagedView.java b/src/com/android/launcher3/FolderPagedView.java
index 05b2bbf..a6494d2 100644
--- a/src/com/android/launcher3/FolderPagedView.java
+++ b/src/com/android/launcher3/FolderPagedView.java
@@ -20,9 +20,11 @@
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
+import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.animation.DecelerateInterpolator;
+import android.view.animation.OvershootInterpolator;
import com.android.launcher3.FocusHelper.PagedFolderKeyEventListener;
import com.android.launcher3.PageIndicator.PageMarkerResources;
@@ -44,6 +46,8 @@
private static final int START_VIEW_REORDER_DELAY = 30;
private static final float VIEW_REORDER_DELAY_FACTOR = 0.9f;
+ private static final int PAGE_INDICATOR_ANIMATION_DELAY = 150;
+
/**
* Fraction of the width to scroll when showing the next page hint.
*/
@@ -70,7 +74,7 @@
private FocusIndicatorView mFocusIndicatorView;
private PagedFolderKeyEventListener mKeyListener;
- private View mPageIndicator;
+ private PageIndicator mPageIndicator;
public FolderPagedView(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -93,7 +97,7 @@
mFolder = folder;
mFocusIndicatorView = (FocusIndicatorView) folder.findViewById(R.id.focus_indicator);
mKeyListener = new PagedFolderKeyEventListener(folder);
- mPageIndicator = folder.findViewById(R.id.folder_page_indicator);
+ mPageIndicator = (PageIndicator) folder.findViewById(R.id.folder_page_indicator);
}
/**
@@ -330,11 +334,8 @@
setEnableOverscroll(getPageCount() > 1);
// Update footer
- int indicatorVisibility = mPageIndicator.getVisibility();
mPageIndicator.setVisibility(getPageCount() > 1 ? View.VISIBLE : View.GONE);
- if (indicatorVisibility != mPageIndicator.getVisibility()) {
- mFolder.updateFooterHeight();
- }
+ mFolder.mFolderName.setGravity(getPageCount() > 1 ? Gravity.START : Gravity.CENTER_HORIZONTAL);
}
public int getDesiredWidth() {
@@ -624,4 +625,29 @@
}
}
}
+
+ public void setMarkerScale(float scale) {
+ int count = mPageIndicator.getChildCount();
+ for (int i = 0; i < count; i++) {
+ View marker = mPageIndicator.getChildAt(i);
+ marker.animate().cancel();
+ marker.setScaleX(scale);
+ marker.setScaleY(scale);
+ }
+ }
+
+ public void animateMarkers() {
+ int count = mPageIndicator.getChildCount();
+ OvershootInterpolator interpolator = new OvershootInterpolator(4);
+ for (int i = 0; i < count; i++) {
+ mPageIndicator.getChildAt(i).animate().scaleX(1).scaleY(1)
+ .setInterpolator(interpolator)
+ .setDuration(Folder.FOOTER_ANIMATION_DURATION)
+ .setStartDelay(PAGE_INDICATOR_ANIMATION_DELAY * i);
+ }
+ }
+
+ public int itemsPerPage() {
+ return mMaxItemsPerPage;
+ }
}
diff --git a/src/com/android/launcher3/LauncherStateTransitionAnimation.java b/src/com/android/launcher3/LauncherStateTransitionAnimation.java
index 51f84bf..73ae51c 100644
--- a/src/com/android/launcher3/LauncherStateTransitionAnimation.java
+++ b/src/com/android/launcher3/LauncherStateTransitionAnimation.java
@@ -378,11 +378,12 @@
dispatchOnLauncherTransitionStart(toView, animated, false);
// Enable all necessary layers
+ boolean isLmpOrAbove = Utilities.isLmpOrAbove();
for (View v : layerViews.keySet()) {
if (layerViews.get(v) == BUILD_AND_SET_LAYER) {
v.setLayerType(View.LAYER_TYPE_HARDWARE, null);
}
- if (Utilities.isViewAttachedToWindow(v)) {
+ if (isLmpOrAbove && Utilities.isViewAttachedToWindow(v)) {
v.buildLayer();
}
}
@@ -697,11 +698,12 @@
dispatchOnLauncherTransitionStart(toView, animated, false);
// Enable all necessary layers
+ boolean isLmpOrAbove = Utilities.isLmpOrAbove();
for (View v : layerViews.keySet()) {
if (layerViews.get(v) == BUILD_AND_SET_LAYER) {
v.setLayerType(View.LAYER_TYPE_HARDWARE, null);
}
- if (Utilities.isLmpOrAbove()) {
+ if (isLmpOrAbove && Utilities.isViewAttachedToWindow(v)) {
v.buildLayer();
}
}
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index aa86567..c8e7d9c 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -636,21 +636,15 @@
AccessibilityManager am =
(AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
if (am.isEnabled()) {
- AccessibilityEvent ev =
- AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_SCROLLED);
- ev.setItemCount(getChildCount());
- ev.setFromIndex(mCurrentPage);
- ev.setToIndex(getNextPage());
+ if (mCurrentPage != getNextPage()) {
+ AccessibilityEvent ev =
+ AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_SCROLLED);
+ ev.setItemCount(getChildCount());
+ ev.setFromIndex(getNextPage());
+ ev.setToIndex(getNextPage());
- final int action;
- if (getNextPage() >= mCurrentPage) {
- action = AccessibilityNodeInfo.ACTION_SCROLL_FORWARD;
- } else {
- action = AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD;
+ sendAccessibilityEventUnchecked(ev);
}
-
- ev.setAction(action);
- sendAccessibilityEventUnchecked(ev);
}
}
@@ -2133,8 +2127,6 @@
focusedChild.clearFocus();
}
- sendScrollAccessibilityEvent();
-
pageBeginMoving();
awakenScrollBars(duration);
if (immediate) {