Pulling out predictions into another row view.

Change-Id: Iba0d74457a1314cf0c00a88f9b07df049334e542
This commit is contained in:
Winson Chung 2015-05-12 19:05:30 -07:00
parent 11509ad61a
commit 208ed75cfd
14 changed files with 386 additions and 165 deletions

View File

@ -15,5 +15,5 @@
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/WorkspaceIcon"
style="@style/Icon"
android:focusable="true" />

View File

@ -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" />

View File

@ -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"

View File

@ -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>
</FrameLayout>

View File

@ -0,0 +1,29 @@
<?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.
-->
<com.android.launcher3.BubbleTextView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:launcher="http://schemas.android.com/apk/res-auto"
style="@style/Icon.AllApps"
android:id="@+id/icon"
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"
launcher:iconDisplay="all_apps" />

View File

@ -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" />

View File

@ -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"

View File

@ -60,6 +60,7 @@
<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>

View File

@ -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>

View File

@ -151,11 +151,13 @@ public class AlphabeticalAppsList {
* 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 class AlphabeticalAppsList {
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;
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.isSectionHeader = false;
item.sectionInfo = section;
item.sectionName = sectionName;
item.sectionAppIndex = sectionAppIndex;
item.appInfo = appInfo;
item.appIndex = appIndex;
item.isPredictedApp = isPredictedApp;
return item;
}
}
@ -204,6 +209,13 @@ public class AlphabeticalAppsList {
boolean retainApp(AppInfo info, String sectionName);
}
/**
* A callback to notify of changes to the filter.
*/
public interface FilterChangedCallback {
void onFilterChanged();
}
/**
* Common interface for different merging strategies.
*/
@ -260,28 +272,31 @@ public class AlphabeticalAppsList {
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 @@ public class AlphabeticalAppsList {
mMergeAlgorithm = new TabletMergeAlgorithm();
}
mNumAppsPerRow = numAppsPerRow;
mNumPredictedAppsPerRow = numPredictedAppsPerRow;
onAppsUpdated();
}
@ -351,6 +369,9 @@ public class AlphabeticalAppsList {
mFilter = f;
onAppsUpdated();
mAdapter.notifyDataSetChanged();
if (mFilterChangedCallback != null){
mFilterChangedCallback.onFilterChanged();
}
}
}
@ -359,12 +380,19 @@ public class AlphabeticalAppsList {
* 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.
*/
@ -450,6 +478,42 @@ public class AlphabeticalAppsList {
// 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 @@ public class AlphabeticalAppsList {
}
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 @@ public class AlphabeticalAppsList {
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 @@ public class AlphabeticalAppsList {
// 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 @@ public class AlphabeticalAppsList {
}
// 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 @@ public class AlphabeticalAppsList {
// 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 @@ public class AlphabeticalAppsList {
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;

View File

@ -45,8 +45,6 @@ public class AppsContainerRecyclerView extends BaseContainerRecyclerView {
* scroller.
*/
private static class ScrollPositionState {
// The index of the first app in the row (Note that is this not the position)
int rowFirstAppIndex;
// The index of the first visible row
int rowIndex;
// The offset of the first visible row
@ -59,6 +57,7 @@ public class AppsContainerRecyclerView extends BaseContainerRecyclerView {
private AlphabeticalAppsList mApps;
private int mNumAppsPerRow;
private int mNumPredictedAppsPerRow;
private Drawable mScrollbar;
private Drawable mFastScrollerBg;
@ -68,6 +67,7 @@ public class AppsContainerRecyclerView extends BaseContainerRecyclerView {
private Paint mFastScrollTextPaint;
private Rect mFastScrollTextBounds = new Rect();
private float mFastScrollAlpha;
private int mPredictionBarHeight;
private int mDownX;
private int mDownY;
private int mLastX;
@ -123,8 +123,9 @@ public class AppsContainerRecyclerView extends BaseContainerRecyclerView {
/**
* 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
@ -133,6 +134,13 @@ public class AppsContainerRecyclerView extends BaseContainerRecyclerView {
background.getPadding(mBackgroundPadding);
}
/**
* Sets the prediction bar height.
*/
public void setPredictionBarHeight(int height) {
mPredictionBarHeight = height;
}
/**
* Sets the fast scroller alpha.
*/
@ -330,6 +338,26 @@ public class AppsContainerRecyclerView extends BaseContainerRecyclerView {
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);
@ -340,21 +368,19 @@ public class AppsContainerRecyclerView extends BaseContainerRecyclerView {
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();
layoutManager.scrollToPositionWithOffset(lastScrollSection.appItem.position, 0);
// We need to workaround the RecyclerView to get the right scroll position after scrolling
// 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 rowIndex = findRowForAppIndex(mScrollPosState.rowFirstAppIndex);
int y = (rowIndex * mScrollPosState.rowHeight) - mScrollPosState.rowTopOffset;
updateScrollY(y);
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;
}
@ -377,10 +403,9 @@ public class AppsContainerRecyclerView extends BaseContainerRecyclerView {
LAYOUT_DIRECTION_RTL);
int rowCount = getNumRows();
getCurScrollState(mScrollPosState, items);
if (mScrollPosState.rowIndex != -1) {
int height = getHeight() - getPaddingTop() - getPaddingBottom();
int totalScrollHeight = rowCount * mScrollPosState.rowHeight;
int totalScrollHeight = rowCount * mScrollPosState.rowHeight + mPredictionBarHeight;
if (totalScrollHeight > height) {
int scrollbarHeight = Math.max(mScrollbarMinHeight,
(int) (height / ((float) totalScrollHeight / height)));
@ -396,8 +421,8 @@ public class AppsContainerRecyclerView extends BaseContainerRecyclerView {
// that the user has already scrolled and then map that to the scroll bar bounds
int availableY = totalScrollHeight - height;
int availableScrollY = height - scrollbarHeight;
y = (mScrollPosState.rowIndex * mScrollPosState.rowHeight) -
mScrollPosState.rowTopOffset;
y = (mScrollPosState.rowIndex * mScrollPosState.rowHeight) + mPredictionBarHeight
- mScrollPosState.rowTopOffset;
y = getPaddingTop() +
(int) (((float) (getPaddingTop() + y) / availableY) * availableScrollY);
@ -444,7 +469,6 @@ public class AppsContainerRecyclerView extends BaseContainerRecyclerView {
*/
private void getCurScrollState(ScrollPositionState stateOut,
List<AlphabeticalAppsList.AdapterItem> items) {
stateOut.rowFirstAppIndex = -1;
stateOut.rowIndex = -1;
stateOut.rowTopOffset = -1;
stateOut.rowHeight = -1;
@ -454,8 +478,7 @@ public class AppsContainerRecyclerView extends BaseContainerRecyclerView {
int position = getChildPosition(child);
if (position != NO_POSITION) {
AlphabeticalAppsList.AdapterItem item = items.get(position);
if (!item.isSectionHeader) {
stateOut.rowFirstAppIndex = item.appIndex;
if (item.viewType == AppsGridAdapter.ICON_VIEW_TYPE) {
stateOut.rowIndex = findRowForAppIndex(item.appIndex);
stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child);
stateOut.rowHeight = child.getHeight();

View File

@ -26,12 +26,14 @@ import android.text.Editable;
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,11 +46,11 @@ import java.util.regex.Pattern;
* 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 = true;
@ -64,12 +66,14 @@ public class AppsContainerView extends BaseContainerView implements DragSource,
@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;
@ -77,11 +81,13 @@ public class AppsContainerView extends BaseContainerView implements DragSource,
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;
@ -101,12 +107,17 @@ public class AppsContainerView extends BaseContainerView implements DragSource,
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();
@ -186,7 +197,7 @@ public class AppsContainerView extends BaseContainerView implements DragSource,
// 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) {
@ -195,12 +206,20 @@ public class AppsContainerView extends BaseContainerView implements DragSource,
}
}
});
// 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);
@ -227,7 +246,8 @@ public class AppsContainerView extends BaseContainerView implements DragSource,
}
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);
@ -246,6 +266,42 @@ public class AppsContainerView extends BaseContainerView implements DragSource,
onUpdatePaddings();
}
@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
@ -253,9 +309,10 @@ public class AppsContainerView extends BaseContainerView implements DragSource,
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);
}
}
@ -297,10 +354,16 @@ public class AppsContainerView extends BaseContainerView implements DragSource,
// 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();
}
/**
@ -499,7 +562,7 @@ public class AppsContainerView extends BaseContainerView implements DragSource,
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;
@ -509,6 +572,11 @@ public class AppsContainerView extends BaseContainerView implements DragSource,
return false;
}
@Override
public void onFilterChanged() {
updatePredictionBarVisibility();
}
@Override
public View getContent() {
return null;
@ -554,6 +622,12 @@ public class AppsContainerView extends BaseContainerView implements DragSource,
mHeaderView.setElevation(newElevation);
}
}
// XXX: Optimize this, stop once we are out of bounds
if (mRecyclerViewScrollY < 0) {
new Throwable().printStackTrace();
}
mPredictionBarView.setTranslationY(-mRecyclerViewScrollY + mAppsRecyclerView.getPaddingTop());
}
/**
@ -682,6 +756,21 @@ public class AppsContainerView extends BaseContainerView implements DragSource,
getInputMethodManager().hideSoftInputFromWindow(getWindowToken(), 0);
}
/**
* 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.
*/

View File

@ -6,6 +6,7 @@ import android.graphics.Canvas;
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 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> {
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 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> {
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 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> {
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,13 +110,13 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> {
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();
c.drawLine(mTmpBounds.left, top, parent.getWidth() - mTmpBounds.right, top,
mPredictedAppsDividerPaint);
hasDrawnPredictedAppDivider = true;
hasDrawnPredictedAppsDivider = true;
} else if (grid.isPhone() && shouldDrawItemSection(holder, i, items)) {
// At this point, we only draw sections for each section break;
@ -220,9 +231,10 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> {
/**
* 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;
}
/**
@ -233,31 +245,27 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> {
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;
@ -271,11 +279,13 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> {
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);
@ -309,6 +319,13 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> {
mGridLayoutMgr.setSpanCount(appsPerRow);
}
/**
* Sets the prediction row height.
*/
public void setPredictionRowHeight(int height) {
mPredictionBarHeight = height;
}
/**
* Sets whether we are in RTL mode.
*/
@ -350,17 +367,24 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> {
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");
}
@ -374,6 +398,16 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> {
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);
@ -394,9 +428,9 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> {
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;
}
}

View File

@ -127,6 +127,7 @@ public class DeviceProfile {
int allAppsNumRows;
int allAppsNumCols;
int appsViewNumCols;
int appsViewNumPredictiveCols;
int searchBarSpaceWidthPx;
int searchBarSpaceHeightPx;
int pageIndicatorHeightPx;
@ -411,7 +412,7 @@ public class DeviceProfile {
// 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 @@ public class DeviceProfile {
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;