Minor changes to apps view.

- Ensuring that apps with numbers and in other locals have a section header.
- Adding an empty state when there are no apps with the current filter
- Removing unnecessary call to check AppInfos

Change-Id: I9dc541c680475b98745fa257ad7e4af06e3966c9
This commit is contained in:
Winson Chung 2015-03-13 11:14:16 -07:00
parent 93f98eaf18
commit 888b3a10bf
11 changed files with 173 additions and 46 deletions

View File

@ -22,7 +22,7 @@
android:background="#22000000"
android:descendantFocusability="afterDescendants">
<include
layout="@layout/apps_list_reveal_view"
layout="@layout/apps_reveal_view"
android:layout_width="@dimen/apps_container_width"
android:layout_height="540dp"
android:layout_gravity="center" />

View File

@ -0,0 +1,28 @@
<?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.
-->
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/empty_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingTop="24dp"
android:paddingBottom="24dp"
android:paddingRight="@dimen/apps_grid_view_start_margin"
android:textSize="16sp"
android:textColor="#4c4c4c"
android:focusable="false" />

View File

@ -22,7 +22,7 @@
android:background="@drawable/apps_customize_bg"
android:descendantFocusability="afterDescendants">
<include
layout="@layout/apps_list_reveal_view" />
layout="@layout/apps_reveal_view" />
<include
layout="@layout/apps_list_view" />
</com.android.launcher3.AppsContainerView>

View File

@ -74,6 +74,10 @@
<!-- Apps view -->
<!-- Search bar text in the apps view. [CHAR_LIMIT=50] -->
<string name="apps_view_search_bar_hint">Search Apps</string>
<!-- Loading apps text. [CHAR_LIMIT=50] -->
<string name="loading_apps_message">Loading Apps...</string>
<!-- No-search-results text. [CHAR_LIMIT=50] -->
<string name="apps_view_no_search_results">No Apps found matching \"<xliff:g id="query" example="Android">%1$s</xliff:g>\"</string>
<!-- Folders -->
<skip />

View File

@ -78,9 +78,7 @@ public class AlphabeticalAppsList {
* 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;
return mIndexer.computeSectionName(info.title.toString().trim());
}
/**
@ -90,6 +88,13 @@ public class AlphabeticalAppsList {
return mIndexer;
}
/**
* Returns whether there are no filtered results.
*/
public boolean hasNoFilteredResults() {
return (mFilter != null) && mFilteredApps.isEmpty();
}
/**
* Sets the current filter for this list of apps.
*/

View File

@ -16,10 +16,12 @@
package com.android.launcher3;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Rect;
import android.support.v7.widget.RecyclerView;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.KeyEvent;
@ -30,7 +32,6 @@ import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.TextView;
import com.android.launcher3.compat.AlphabeticIndexCompat;
import java.util.List;
@ -76,6 +77,7 @@ public class AppsContainerView extends FrameLayout implements DragSource, View.O
super(context, attrs, defStyleAttr, defStyleRes);
LauncherAppState app = LauncherAppState.getInstance();
DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
Resources res = context.getResources();
mLauncher = (Launcher) context;
mApps = new AlphabeticalAppsList(context);
@ -83,6 +85,7 @@ public class AppsContainerView extends FrameLayout implements DragSource, View.O
mNumAppsPerRow = grid.appsViewNumCols;
AppsGridAdapter adapter = new AppsGridAdapter(context, mApps, mNumAppsPerRow, this,
mLauncher, this);
adapter.setEmptySearchText(res.getString(R.string.loading_apps_message));
mLayoutManager = adapter.getLayoutManager(context);
mItemDecoration = adapter.getItemDecoration();
mAdapter = adapter;
@ -90,6 +93,7 @@ public class AppsContainerView extends FrameLayout implements DragSource, View.O
} else if (USE_LAYOUT == LIST_LAYOUT) {
mNumAppsPerRow = 1;
AppsListAdapter adapter = new AppsListAdapter(context, mApps, this, mLauncher, this);
adapter.setEmptySearchText(res.getString(R.string.loading_apps_message));
mLayoutManager = adapter.getLayoutManager(context);
mAdapter = adapter;
}
@ -163,10 +167,12 @@ public class AppsContainerView extends FrameLayout implements DragSource, View.O
mAppsListView.setHasFixedSize(true);
if (isRtl) {
mAppsListView.setPadding(mAppsListView.getPaddingLeft(), mAppsListView.getPaddingTop(),
mAppsListView.getPaddingRight() + mContentMarginStart, mAppsListView.getPaddingBottom());
mAppsListView.getPaddingRight() + mContentMarginStart,
mAppsListView.getPaddingBottom());
} else {
mAppsListView.setPadding(mAppsListView.getPaddingLeft() + mContentMarginStart, mAppsListView.getPaddingTop(),
mAppsListView.getPaddingRight(), mAppsListView.getPaddingBottom());
mAppsListView.setPadding(mAppsListView.getPaddingLeft() + mContentMarginStart,
mAppsListView.getPaddingTop(), mAppsListView.getPaddingRight(),
mAppsListView.getPaddingBottom());
}
if (mItemDecoration != null) {
mAppsListView.addItemDecoration(mItemDecoration);
@ -299,7 +305,15 @@ public class AppsContainerView extends FrameLayout implements DragSource, View.O
if (s.toString().isEmpty()) {
mApps.setFilter(null);
} else {
final AlphabeticIndexCompat indexer = mApps.getIndexer();
String formatStr = getResources().getString(R.string.apps_view_no_search_results);
if (USE_LAYOUT == GRID_LAYOUT) {
((AppsGridAdapter) mAdapter).setEmptySearchText(String.format(formatStr,
s.toString()));
} else {
((AppsListAdapter) mAdapter).setEmptySearchText(String.format(formatStr,
s.toString()));
}
final String filterText = s.toString().toLowerCase().replaceAll("\\s+", "");
mApps.setFilter(new AlphabeticalAppsList.Filter() {
@Override

View File

@ -10,6 +10,7 @@ import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.android.launcher3.compat.AlphabeticIndexCompat;
@ -22,6 +23,7 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> {
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;
/**
* ViewHolder for each icon.
@ -29,11 +31,13 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> {
public static class ViewHolder extends RecyclerView.ViewHolder {
public View mContent;
public boolean mIsSectionRow;
public boolean mIsEmptyRow;
public ViewHolder(View v, boolean isSectionRow) {
public ViewHolder(View v, boolean isSectionRow, boolean isEmptyRow) {
super(v);
mContent = v;
mIsSectionRow = isSectionRow;
mIsEmptyRow = isEmptyRow;
}
}
@ -43,8 +47,14 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> {
public class GridSpanSizer extends GridLayoutManager.SpanSizeLookup {
@Override
public int getSpanSize(int position) {
if (mApps.hasNoFilteredResults()) {
// Empty view spans full width
return mAppsPerRow;
}
AppInfo info = mApps.getApps().get(position);
if (info == AlphabeticalAppsList.SECTION_BREAK_INFO) {
// Section break spans full width
return mAppsPerRow;
} else {
return 1;
@ -59,14 +69,13 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> {
@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 (!holder.mIsSectionRow && !holder.mIsEmptyRow && !lp.isItemRemoved()) {
if (mApps.getApps().get(holder.getPosition() - 1) ==
AlphabeticalAppsList.SECTION_BREAK_INFO) {
// Draw at the parent
@ -106,6 +115,7 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> {
private View.OnLongClickListener mIconLongClickListener;
private int mAppsPerRow;
private boolean mIsRtl;
private String mEmptySearchText;
// Section drawing
private int mStartMargin;
@ -140,6 +150,13 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> {
mIsRtl = rtl;
}
/**
* Sets the text to show when there are no apps.
*/
public void setEmptySearchText(String query) {
mEmptySearchText = query;
}
/**
* Returns the grid layout manager.
*/
@ -167,8 +184,12 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> {
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
case EMPTY_VIEW_TYPE:
return new ViewHolder(mLayoutInflater.inflate(R.layout.apps_empty_view, parent,
false), false /* isSectionRow */, true /* isEmptyRow */);
case SECTION_BREAK_VIEW_TYPE:
return new ViewHolder(new View(parent.getContext()), true);
return new ViewHolder(new View(parent.getContext()), true /* isSectionRow */,
false /* isEmptyRow */);
case ICON_VIEW_TYPE:
BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
R.layout.apps_grid_row_icon_view, parent, false);
@ -176,7 +197,7 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> {
icon.setOnClickListener(mIconClickListener);
icon.setOnLongClickListener(mIconLongClickListener);
icon.setFocusable(true);
return new ViewHolder(icon, false);
return new ViewHolder(icon, false /* isSectionRow */, false /* isEmptyRow */);
default:
throw new RuntimeException("Unexpected view type");
}
@ -184,21 +205,33 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> {
@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);
switch (holder.getItemViewType()) {
case ICON_VIEW_TYPE:
AppInfo info = mApps.getApps().get(position);
BubbleTextView icon = (BubbleTextView) holder.mContent;
icon.applyFromApplicationInfo(info);
break;
case EMPTY_VIEW_TYPE:
TextView emptyViewText = (TextView) holder.mContent.findViewById(R.id.empty_text);
emptyViewText.setText(mEmptySearchText);
break;
}
}
@Override
public int getItemCount() {
if (mApps.hasNoFilteredResults()) {
// For the empty view
return 1;
}
return mApps.getApps().size();
}
@Override
public int getItemViewType(int position) {
if (mApps.getApps().get(position) == AlphabeticalAppsList.SECTION_BREAK_INFO) {
if (mApps.hasNoFilteredResults()) {
return EMPTY_VIEW_TYPE;
} else if (mApps.getApps().get(position) == AlphabeticalAppsList.SECTION_BREAK_INFO) {
return SECTION_BREAK_VIEW_TYPE;
}
return ICON_VIEW_TYPE;

View File

@ -30,12 +30,14 @@ class AppsListAdapter extends RecyclerView.Adapter<AppsListAdapter.ViewHolder> {
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;
private LayoutInflater mLayoutInflater;
private AlphabeticalAppsList mApps;
private View.OnTouchListener mTouchListener;
private View.OnClickListener mIconClickListener;
private View.OnLongClickListener mIconLongClickListener;
private String mEmptySearchText;
public AppsListAdapter(Context context, AlphabeticalAppsList apps,
View.OnTouchListener touchListener, View.OnClickListener iconClickListener,
@ -51,9 +53,19 @@ class AppsListAdapter extends RecyclerView.Adapter<AppsListAdapter.ViewHolder> {
return new LinearLayoutManager(context);
}
/**
* Sets the text to show when there are no apps.
*/
public void setEmptySearchText(String query) {
mEmptySearchText = query;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
case EMPTY_VIEW_TYPE:
return new ViewHolder(mLayoutInflater.inflate(R.layout.apps_empty_view, parent,
false));
case SECTION_BREAK_VIEW_TYPE:
return new ViewHolder(new View(parent.getContext()));
case ICON_VIEW_TYPE:
@ -79,39 +91,51 @@ class AppsListAdapter extends RecyclerView.Adapter<AppsListAdapter.ViewHolder> {
@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);
switch (holder.getItemViewType()) {
case ICON_VIEW_TYPE:
AppInfo info = mApps.getApps().get(position);
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 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);
// Bind the icon
BubbleTextView icon = (BubbleTextView) content.getChildAt(1);
icon.applyFromApplicationInfo(info);
break;
case EMPTY_VIEW_TYPE:
TextView emptyViewText = (TextView) holder.mContent.findViewById(R.id.empty_text);
emptyViewText.setText(mEmptySearchText);
break;
}
}
@Override
public int getItemCount() {
if (mApps.hasNoFilteredResults()) {
// For the empty view
return 1;
}
return mApps.getApps().size();
}
@Override
public int getItemViewType(int position) {
if (mApps.getApps().get(position) == AlphabeticalAppsList.SECTION_BREAK_INFO) {
if (mApps.hasNoFilteredResults()) {
return EMPTY_VIEW_TYPE;
} else if (mApps.getApps().get(position) == AlphabeticalAppsList.SECTION_BREAK_INFO) {
return SECTION_BREAK_VIEW_TYPE;
}
return ICON_VIEW_TYPE;

View File

@ -161,7 +161,8 @@ public class BubbleTextView extends TextView {
if (info.contentDescription != null) {
setContentDescription(info.contentDescription);
}
setTag(info);
// We don't need to check the info since it's not a ShortcutInfo
super.setTag(info);
}
@Override

View File

@ -27,7 +27,7 @@ class BaseAlphabeticIndex {
/**
* Returns the index of the bucket in which the given string should appear.
*/
public int getBucketIndex(String s) {
protected int getBucketIndex(String s) {
if (s.isEmpty()) {
return UNKNOWN_BUCKET_INDEX;
}
@ -41,7 +41,7 @@ class BaseAlphabeticIndex {
/**
* Returns the label for the bucket at the given index (as returned by getBucketIndex).
*/
public String getBucketLabel(int index) {
protected String getBucketLabel(int index) {
return BUCKETS.substring(index, index + 1);
}
}
@ -99,12 +99,30 @@ public class AlphabeticIndexCompat extends BaseAlphabeticIndex {
}
}
/**
* Computes the section name for an given string {@param s}.
*/
public String computeSectionName(String s) {
String sectionName = getBucketLabel(getBucketIndex(s));
if (sectionName.trim().isEmpty() && s.length() > 0) {
boolean startsWithDigit = Character.isDigit(s.charAt(0));
if (startsWithDigit) {
// Digit section
return "#";
} else {
// Unknown section
return "\u2022";
}
}
return sectionName;
}
/**
* 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) {
protected int getBucketIndex(String s) {
if (mHasValidAlphabeticIndex) {
try {
return (Integer) mGetBucketIndexMethod.invoke(mAlphabeticIndex, s);
@ -118,7 +136,7 @@ public class AlphabeticIndexCompat extends BaseAlphabeticIndex {
/**
* Returns the label for the bucket at the given index (as returned by getBucketIndex).
*/
public String getBucketLabel(int index) {
protected String getBucketLabel(int index) {
if (mHasValidAlphabeticIndex) {
try {
return (String) mGetBucketLabelMethod.invoke(mAlphabeticIndex, index);