summaryrefslogtreecommitdiffstats
path: root/src/com/android/launcher3/allapps
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/launcher3/allapps')
-rw-r--r--src/com/android/launcher3/allapps/AllAppsContainerView.java638
-rw-r--r--src/com/android/launcher3/allapps/AllAppsGridAdapter.java488
-rw-r--r--src/com/android/launcher3/allapps/AllAppsRecyclerView.java327
-rw-r--r--src/com/android/launcher3/allapps/AllAppsRecyclerViewContainerView.java72
-rw-r--r--src/com/android/launcher3/allapps/AllAppsSearchBarController.java100
-rw-r--r--src/com/android/launcher3/allapps/AllAppsSearchEditView.java65
-rw-r--r--src/com/android/launcher3/allapps/AlphabeticalAppsList.java590
-rw-r--r--src/com/android/launcher3/allapps/DefaultAppSearchAlgorithm.java94
-rw-r--r--src/com/android/launcher3/allapps/DefaultAppSearchController.java269
9 files changed, 2643 insertions, 0 deletions
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
new file mode 100644
index 000000000..67d572819
--- /dev/null
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -0,0 +1,638 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.allapps;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.InsetDrawable;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v7.widget.RecyclerView;
+import android.text.Selection;
+import android.text.SpannableStringBuilder;
+import android.text.method.TextKeyListener;
+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.ViewTreeObserver;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.BaseContainerView;
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.CellLayout;
+import com.android.launcher3.CheckLongPressHelper;
+import com.android.launcher3.DeleteDropTarget;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.DragSource;
+import com.android.launcher3.DropTarget;
+import com.android.launcher3.Folder;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherTransitionable;
+import com.android.launcher3.R;
+import com.android.launcher3.Stats;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.Workspace;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.Thunk;
+
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetEncoder;
+import java.util.ArrayList;
+import java.util.List;
+
+
+
+/**
+ * A merge algorithm that merges every section indiscriminately.
+ */
+final class FullMergeAlgorithm implements AlphabeticalAppsList.MergeAlgorithm {
+
+ @Override
+ public boolean continueMerging(AlphabeticalAppsList.SectionInfo section,
+ AlphabeticalAppsList.SectionInfo withSection,
+ int sectionAppCount, int numAppsPerRow, int mergeCount) {
+ // Don't merge the predicted apps
+ if (section.firstAppItem.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE) {
+ return false;
+ }
+ // Otherwise, merge every other section
+ return true;
+ }
+}
+
+/**
+ * The logic we use to merge multiple sections. We only merge sections when their final row
+ * contains less than a certain number of icons, and stop at a specified max number of merges.
+ * In addition, we will try and not merge sections that identify apps from different scripts.
+ */
+final class SimpleSectionMergeAlgorithm implements AlphabeticalAppsList.MergeAlgorithm {
+
+ private int mMinAppsPerRow;
+ private int mMinRowsInMergedSection;
+ private int mMaxAllowableMerges;
+ private CharsetEncoder mAsciiEncoder;
+
+ public SimpleSectionMergeAlgorithm(int minAppsPerRow, int minRowsInMergedSection, int maxNumMerges) {
+ mMinAppsPerRow = minAppsPerRow;
+ mMinRowsInMergedSection = minRowsInMergedSection;
+ mMaxAllowableMerges = maxNumMerges;
+ mAsciiEncoder = Charset.forName("US-ASCII").newEncoder();
+ }
+
+ @Override
+ public boolean continueMerging(AlphabeticalAppsList.SectionInfo section,
+ AlphabeticalAppsList.SectionInfo withSection,
+ int sectionAppCount, int numAppsPerRow, int mergeCount) {
+ // Don't merge the predicted apps
+ if (section.firstAppItem.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE) {
+ return false;
+ }
+
+ // Continue merging if the number of hanging apps on the final row is less than some
+ // fixed number (ragged), the merged rows has yet to exceed some minimum row count,
+ // and while the number of merged sections is less than some fixed number of merges
+ int rows = sectionAppCount / numAppsPerRow;
+ int cols = sectionAppCount % numAppsPerRow;
+
+ // Ensure that we do not merge across scripts, currently we only allow for english and
+ // native scripts so we can test if both can just be ascii encoded
+ boolean isCrossScript = false;
+ if (section.firstAppItem != null && withSection.firstAppItem != null) {
+ isCrossScript = mAsciiEncoder.canEncode(section.firstAppItem.sectionName) !=
+ mAsciiEncoder.canEncode(withSection.firstAppItem.sectionName);
+ }
+ return (0 < cols && cols < mMinAppsPerRow) &&
+ rows < mMinRowsInMergedSection &&
+ mergeCount < mMaxAllowableMerges &&
+ !isCrossScript;
+ }
+}
+
+/**
+ * The all apps view container.
+ */
+public class AllAppsContainerView extends BaseContainerView implements DragSource,
+ LauncherTransitionable, View.OnTouchListener, View.OnLongClickListener,
+ AllAppsSearchBarController.Callbacks {
+
+ private static final int MIN_ROWS_IN_MERGED_SECTION_PHONE = 3;
+ private static final int MAX_NUM_MERGES_PHONE = 2;
+
+ @Thunk Launcher mLauncher;
+ @Thunk AlphabeticalAppsList mApps;
+ private AllAppsGridAdapter mAdapter;
+ private RecyclerView.LayoutManager mLayoutManager;
+ private RecyclerView.ItemDecoration mItemDecoration;
+
+ @Thunk View mContent;
+ @Thunk View mContainerView;
+ @Thunk View mRevealView;
+ @Thunk AllAppsRecyclerView mAppsRecyclerView;
+ @Thunk AllAppsSearchBarController mSearchBarController;
+ private ViewGroup mSearchBarContainerView;
+ private View mSearchBarView;
+
+ private int mSectionNamesMargin;
+ private int mNumAppsPerRow;
+ private int mNumPredictedAppsPerRow;
+ private int mRecyclerViewTopBottomPadding;
+ // This coordinate is relative to this container view
+ private final Point mBoundsCheckLastTouchDownPos = new Point(-1, -1);
+ // This coordinate is relative to its parent
+ private final Point mIconLastTouchPos = new Point();
+
+ private SpannableStringBuilder mSearchQueryBuilder = null;
+
+ public AllAppsContainerView(Context context) {
+ this(context, null);
+ }
+
+ public AllAppsContainerView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public AllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ Resources res = context.getResources();
+
+ mLauncher = (Launcher) context;
+ mSectionNamesMargin = res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin);
+ mApps = new AlphabeticalAppsList(context);
+ mAdapter = new AllAppsGridAdapter(context, mApps, this, mLauncher, this);
+ mAdapter.setEmptySearchText(res.getString(R.string.all_apps_loading_message));
+ mApps.setAdapter(mAdapter);
+ mLayoutManager = mAdapter.getLayoutManager();
+ mItemDecoration = mAdapter.getItemDecoration();
+ mRecyclerViewTopBottomPadding =
+ res.getDimensionPixelSize(R.dimen.all_apps_list_top_bottom_padding);
+
+ mSearchQueryBuilder = new SpannableStringBuilder();
+ Selection.setSelection(mSearchQueryBuilder, 0);
+ }
+
+ /**
+ * Sets the current set of predicted apps.
+ */
+ public void setPredictedApps(List<ComponentKey> apps) {
+ mApps.setPredictedApps(apps);
+ }
+
+ /**
+ * Sets the current set of apps.
+ */
+ public void setApps(List<AppInfo> apps) {
+ mApps.setApps(apps);
+ }
+
+ /**
+ * Adds new apps to the list.
+ */
+ public void addApps(List<AppInfo> apps) {
+ mApps.addApps(apps);
+ }
+
+ /**
+ * Updates existing apps in the list
+ */
+ public void updateApps(List<AppInfo> apps) {
+ mApps.updateApps(apps);
+ }
+
+ /**
+ * Removes some apps from the list.
+ */
+ public void removeApps(List<AppInfo> apps) {
+ mApps.removeApps(apps);
+ }
+
+ /**
+ * Sets the search bar that shows above the a-z list.
+ */
+ public void setSearchBarController(AllAppsSearchBarController searchController) {
+ if (mSearchBarController != null) {
+ throw new RuntimeException("Expected search bar controller to only be set once");
+ }
+ mSearchBarController = searchController;
+ mSearchBarController.initialize(mApps, this);
+
+ // Add the new search view to the layout
+ View searchBarView = searchController.getView(mSearchBarContainerView);
+ mSearchBarContainerView.addView(searchBarView);
+ mSearchBarContainerView.setVisibility(View.VISIBLE);
+ mSearchBarView = searchBarView;
+ setHasSearchBar();
+
+ updateBackgroundAndPaddings();
+ }
+
+ /**
+ * Scrolls this list view to the top.
+ */
+ public void scrollToTop() {
+ mAppsRecyclerView.scrollToTop();
+ }
+
+ /**
+ * Returns the content view used for the launcher transitions.
+ */
+ public View getContentView() {
+ return mContainerView;
+ }
+
+ /**
+ * Returns the all apps search view.
+ */
+ public View getSearchBarView() {
+ return mSearchBarView;
+ }
+
+ /**
+ * Returns the reveal view used for the launcher transitions.
+ */
+ public View getRevealView() {
+ return mRevealView;
+ }
+
+ /**
+ * Returns an new instance of the default app search controller.
+ */
+ public AllAppsSearchBarController newDefaultAppSearchController() {
+ return new DefaultAppSearchController(getContext(), this, mAppsRecyclerView);
+ }
+
+ /**
+ * Focuses the search field and begins an app search.
+ */
+ public void startAppsSearch() {
+ if (mSearchBarController != null) {
+ mSearchBarController.focusSearchField();
+ }
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ boolean isRtl = Utilities.isRtl(getResources());
+ mAdapter.setRtl(isRtl);
+ mContent = findViewById(R.id.content);
+
+ // This is a focus listener that proxies focus from a view into the list view. This is to
+ // work around the search box from getting first focus and showing the cursor.
+ View.OnFocusChangeListener focusProxyListener = new View.OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (hasFocus) {
+ mAppsRecyclerView.requestFocus();
+ }
+ }
+ };
+ mSearchBarContainerView = (ViewGroup) findViewById(R.id.search_box_container);
+ mSearchBarContainerView.setOnFocusChangeListener(focusProxyListener);
+ mContainerView = findViewById(R.id.all_apps_container);
+ mContainerView.setOnFocusChangeListener(focusProxyListener);
+ mRevealView = findViewById(R.id.all_apps_reveal);
+
+ // Load the all apps recycler view
+ mAppsRecyclerView = (AllAppsRecyclerView) findViewById(R.id.apps_list_view);
+ mAppsRecyclerView.setApps(mApps);
+ mAppsRecyclerView.setLayoutManager(mLayoutManager);
+ mAppsRecyclerView.setAdapter(mAdapter);
+ mAppsRecyclerView.setHasFixedSize(true);
+ if (mItemDecoration != null) {
+ mAppsRecyclerView.addItemDecoration(mItemDecoration);
+ }
+
+ updateBackgroundAndPaddings();
+ }
+
+ @Override
+ public void onBoundsChanged(Rect newBounds) {
+ mLauncher.updateOverlayBounds(newBounds);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // Update the number of items in the grid before we measure the view
+ int availableWidth = !mContentBounds.isEmpty() ? mContentBounds.width() :
+ MeasureSpec.getSize(widthMeasureSpec);
+ DeviceProfile grid = mLauncher.getDeviceProfile();
+ grid.updateAppsViewNumCols(getResources(), availableWidth);
+ if (mNumAppsPerRow != grid.allAppsNumCols ||
+ mNumPredictedAppsPerRow != grid.allAppsNumPredictiveCols) {
+ mNumAppsPerRow = grid.allAppsNumCols;
+ mNumPredictedAppsPerRow = grid.allAppsNumPredictiveCols;
+
+ // If there is a start margin to draw section names, determine how we are going to merge
+ // app sections
+ boolean mergeSectionsFully = mSectionNamesMargin == 0 || !grid.isPhone;
+ AlphabeticalAppsList.MergeAlgorithm mergeAlgorithm = mergeSectionsFully ?
+ new FullMergeAlgorithm() :
+ new SimpleSectionMergeAlgorithm((int) Math.ceil(mNumAppsPerRow / 2f),
+ MIN_ROWS_IN_MERGED_SECTION_PHONE, MAX_NUM_MERGES_PHONE);
+
+ mAppsRecyclerView.setNumAppsPerRow(grid, mNumAppsPerRow);
+ mAdapter.setNumAppsPerRow(mNumAppsPerRow);
+ mApps.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow, mergeAlgorithm);
+ }
+
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ /**
+ * Update the background and padding of the Apps view and children. Instead of insetting the
+ * container view, we inset the background and padding of the recycler view to allow for the
+ * recycler view to handle touch events (for fast scrolling) all the way to the edge.
+ */
+ @Override
+ protected void onUpdateBackgroundAndPaddings(Rect searchBarBounds, Rect padding) {
+ boolean isRtl = Utilities.isRtl(getResources());
+
+ // TODO: Use quantum_panel instead of quantum_panel_shape
+ InsetDrawable background = new InsetDrawable(
+ getResources().getDrawable(R.drawable.quantum_panel_shape), padding.left, 0,
+ padding.right, 0);
+ Rect bgPadding = new Rect();
+ background.getPadding(bgPadding);
+ mContainerView.setBackground(background);
+ mRevealView.setBackground(background.getConstantState().newDrawable());
+ mAppsRecyclerView.updateBackgroundPadding(bgPadding);
+ mAdapter.updateBackgroundPadding(bgPadding);
+
+ // Hack: We are going to let the recycler view take the full width, so reset the padding on
+ // the container to zero after setting the background and apply the top-bottom padding to
+ // the content view instead so that the launcher transition clips correctly.
+ mContent.setPadding(0, padding.top, 0, padding.bottom);
+ mContainerView.setPadding(0, 0, 0, 0);
+
+ // Pad the recycler view by the background padding plus the start margin (for the section
+ // names)
+ int startInset = Math.max(mSectionNamesMargin, mAppsRecyclerView.getMaxScrollbarWidth());
+ int topBottomPadding = mRecyclerViewTopBottomPadding;
+ if (isRtl) {
+ mAppsRecyclerView.setPadding(padding.left + mAppsRecyclerView.getMaxScrollbarWidth(),
+ topBottomPadding, padding.right + startInset, topBottomPadding);
+ } else {
+ mAppsRecyclerView.setPadding(padding.left + startInset, topBottomPadding,
+ padding.right + mAppsRecyclerView.getMaxScrollbarWidth(), topBottomPadding);
+ }
+
+ // Inset the search bar to fit its bounds above the container
+ if (mSearchBarView != null) {
+ Rect backgroundPadding = new Rect();
+ if (mSearchBarView.getBackground() != null) {
+ mSearchBarView.getBackground().getPadding(backgroundPadding);
+ }
+ LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
+ mSearchBarContainerView.getLayoutParams();
+ lp.leftMargin = searchBarBounds.left - backgroundPadding.left;
+ lp.topMargin = searchBarBounds.top - backgroundPadding.top;
+ lp.rightMargin = (getMeasuredWidth() - searchBarBounds.right) - backgroundPadding.right;
+ mSearchBarContainerView.requestLayout();
+ }
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ // Determine if the key event was actual text, if so, focus the search bar and then dispatch
+ // the key normally so that it can process this key event
+ if (!mSearchBarController.isSearchFieldFocused() &&
+ event.getAction() == KeyEvent.ACTION_DOWN) {
+ final int unicodeChar = event.getUnicodeChar();
+ final boolean isKeyNotWhitespace = unicodeChar > 0 &&
+ !Character.isWhitespace(unicodeChar) && !Character.isSpaceChar(unicodeChar);
+ if (isKeyNotWhitespace) {
+ boolean gotKey = TextKeyListener.getInstance().onKeyDown(this, mSearchQueryBuilder,
+ event.getKeyCode(), event);
+ if (gotKey && mSearchQueryBuilder.length() > 0) {
+ mSearchBarController.focusSearchField();
+ }
+ }
+ }
+
+ return super.dispatchKeyEvent(event);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ return handleTouchEvent(ev);
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ return handleTouchEvent(ev);
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ @Override
+ public boolean onTouch(View v, MotionEvent ev) {
+ switch (ev.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_MOVE:
+ mIconLastTouchPos.set((int) ev.getX(), (int) ev.getY());
+ break;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onLongClick(View v) {
+ // Return early if this is not initiated from a touch
+ if (!v.isInTouchMode()) return false;
+ // When we have exited all apps or are in transition, disregard long clicks
+ if (!mLauncher.isAppsViewVisible() ||
+ mLauncher.getWorkspace().isSwitchingState()) return false;
+ // Return if global dragging is not enabled
+ if (!mLauncher.isDraggingEnabled()) return false;
+
+ // Start the drag
+ mLauncher.getWorkspace().beginDragShared(v, mIconLastTouchPos, this, false);
+ // Enter spring loaded mode
+ mLauncher.enterSpringLoadedDragMode();
+
+ return false;
+ }
+
+ @Override
+ public boolean supportsFlingToDelete() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsAppInfoDropTarget() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsDeleteDropTarget() {
+ return false;
+ }
+
+ @Override
+ public float getIntrinsicIconScaleFactor() {
+ DeviceProfile grid = mLauncher.getDeviceProfile();
+ return (float) grid.allAppsIconSizePx / grid.iconSizePx;
+ }
+
+ @Override
+ public void onFlingToDeleteCompleted() {
+ // We just dismiss the drag when we fling, so cleanup here
+ mLauncher.exitSpringLoadedDragModeDelayed(true,
+ Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null);
+ mLauncher.unlockScreenOrientation(false);
+ }
+
+ @Override
+ public void onDropCompleted(View target, DropTarget.DragObject d, boolean isFlingToDelete,
+ boolean success) {
+ if (isFlingToDelete || !success || (target != mLauncher.getWorkspace() &&
+ !(target instanceof DeleteDropTarget) && !(target instanceof Folder))) {
+ // Exit spring loaded mode if we have not successfully dropped or have not handled the
+ // drop in Workspace
+ mLauncher.exitSpringLoadedDragModeDelayed(true,
+ Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null);
+ }
+ mLauncher.unlockScreenOrientation(false);
+
+ // Display an error message if the drag failed due to there not being enough space on the
+ // target layout we were dropping on.
+ if (!success) {
+ boolean showOutOfSpaceMessage = false;
+ if (target instanceof Workspace) {
+ int currentScreen = mLauncher.getCurrentWorkspaceScreen();
+ Workspace workspace = (Workspace) target;
+ CellLayout layout = (CellLayout) workspace.getChildAt(currentScreen);
+ ItemInfo itemInfo = (ItemInfo) d.dragInfo;
+ if (layout != null) {
+ layout.calculateSpans(itemInfo);
+ showOutOfSpaceMessage =
+ !layout.findCellForSpan(null, itemInfo.spanX, itemInfo.spanY);
+ }
+ }
+ if (showOutOfSpaceMessage) {
+ mLauncher.showOutOfSpaceMessage(false);
+ }
+
+ d.deferDragViewCleanupPostAnimation = false;
+ }
+ }
+
+ @Override
+ public void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace) {
+ // Do nothing
+ }
+
+ @Override
+ public void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace) {
+ // Do nothing
+ }
+
+ @Override
+ public void onLauncherTransitionStep(Launcher l, float t) {
+ // Do nothing
+ }
+
+ @Override
+ public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) {
+ if (toWorkspace) {
+ // Reset the search bar after transitioning home
+ mSearchBarController.reset();
+ }
+ }
+
+ /**
+ * Handles the touch events to dismiss all apps when clicking outside the bounds of the
+ * recycler view.
+ */
+ private boolean handleTouchEvent(MotionEvent ev) {
+ DeviceProfile grid = mLauncher.getDeviceProfile();
+ int x = (int) ev.getX();
+ int y = (int) ev.getY();
+
+ switch (ev.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ if (!mContentBounds.isEmpty()) {
+ // Outset the fixed bounds and check if the touch is outside all apps
+ Rect tmpRect = new Rect(mContentBounds);
+ tmpRect.inset(-grid.allAppsIconSizePx / 2, 0);
+ if (ev.getX() < tmpRect.left || ev.getX() > tmpRect.right) {
+ mBoundsCheckLastTouchDownPos.set(x, y);
+ return true;
+ }
+ } else {
+ // Check if the touch is outside all apps
+ if (ev.getX() < getPaddingLeft() ||
+ ev.getX() > (getWidth() - getPaddingRight())) {
+ mBoundsCheckLastTouchDownPos.set(x, y);
+ return true;
+ }
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ if (mBoundsCheckLastTouchDownPos.x > -1) {
+ ViewConfiguration viewConfig = ViewConfiguration.get(getContext());
+ float dx = ev.getX() - mBoundsCheckLastTouchDownPos.x;
+ float dy = ev.getY() - mBoundsCheckLastTouchDownPos.y;
+ float distance = (float) Math.hypot(dx, dy);
+ if (distance < viewConfig.getScaledTouchSlop()) {
+ // The background was clicked, so just go home
+ Launcher launcher = (Launcher) getContext();
+ launcher.showWorkspace(true);
+ return true;
+ }
+ }
+ // Fall through
+ case MotionEvent.ACTION_CANCEL:
+ mBoundsCheckLastTouchDownPos.set(-1, -1);
+ break;
+ }
+ return false;
+ }
+
+ @Override
+ public void onSearchResult(String query, ArrayList<ComponentKey> apps) {
+ if (apps != null) {
+ if (apps.isEmpty()) {
+ String formatStr = getResources().getString(R.string.all_apps_no_search_results);
+ mAdapter.setEmptySearchText(String.format(formatStr, query));
+ } else {
+ mAppsRecyclerView.scrollToTop();
+ }
+ mApps.setOrderedFilter(apps);
+ }
+ }
+
+ @Override
+ public void clearSearchResult() {
+ mApps.setOrderedFilter(null);
+
+ // Clear the search query
+ mSearchQueryBuilder.clear();
+ mSearchQueryBuilder.clearSpans();
+ Selection.setSelection(mSearchQueryBuilder, 0);
+ }
+}
diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
new file mode 100644
index 000000000..e96567c41
--- /dev/null
+++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
@@ -0,0 +1,488 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.allapps;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.support.v4.view.accessibility.AccessibilityRecordCompat;
+import android.support.v4.view.accessibility.AccessibilityEventCompat;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.TextView;
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.util.Thunk;
+
+import java.util.HashMap;
+import java.util.List;
+
+
+/**
+ * The grid view adapter of all the apps.
+ */
+class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHolder> {
+
+ public static final String TAG = "AppsGridAdapter";
+ private static final boolean DEBUG = false;
+
+ // 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;
+ // A prediction icon
+ public static final int PREDICTION_ICON_VIEW_TYPE = 2;
+ // The message shown when there are no filtered results
+ public static final int EMPTY_SEARCH_VIEW_TYPE = 3;
+
+ /**
+ * ViewHolder for each icon.
+ */
+ public static class ViewHolder extends RecyclerView.ViewHolder {
+ public View mContent;
+
+ public ViewHolder(View v) {
+ super(v);
+ mContent = v;
+ }
+ }
+
+ /**
+ * A subclass of GridLayoutManager that overrides accessibility values during app search.
+ */
+ public class AppsGridLayoutManager extends GridLayoutManager {
+
+ public AppsGridLayoutManager(Context context) {
+ super(context, 1, GridLayoutManager.VERTICAL, false);
+ }
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ if (mApps.hasNoFilteredResults()) {
+ // Disregard the no-search-results text as a list item for accessibility
+ final AccessibilityRecordCompat record = AccessibilityEventCompat
+ .asRecord(event);
+ record.setItemCount(0);
+ }
+ }
+
+ @Override
+ public int getRowCountForAccessibility(RecyclerView.Recycler recycler,
+ RecyclerView.State state) {
+ if (mApps.hasNoFilteredResults()) {
+ // Disregard the no-search-results text as a list item for accessibility
+ return 0;
+ } else {
+ return super.getRowCountForAccessibility(recycler, state);
+ }
+ }
+ }
+
+ /**
+ * Helper class to size the grid items.
+ */
+ public class GridSpanSizer extends GridLayoutManager.SpanSizeLookup {
+
+ public GridSpanSizer() {
+ super();
+ setSpanIndexCacheEnabled(true);
+ }
+
+ @Override
+ public int getSpanSize(int position) {
+ if (mApps.hasNoFilteredResults()) {
+ // Empty view spans full width
+ return mAppsPerRow;
+ }
+
+ switch (mApps.getAdapterItems().get(position).viewType) {
+ case AllAppsGridAdapter.ICON_VIEW_TYPE:
+ case AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE:
+ return 1;
+ default:
+ // Section breaks span the full width
+ return mAppsPerRow;
+ }
+ }
+ }
+
+ /**
+ * Helper class to draw the section headers
+ */
+ public class GridItemDecoration extends RecyclerView.ItemDecoration {
+
+ private static final boolean DEBUG_SECTION_MARGIN = false;
+ private static final boolean FADE_OUT_SECTIONS = false;
+
+ private HashMap<String, PointF> mCachedSectionBounds = new HashMap<>();
+ private Rect mTmpBounds = new Rect();
+
+ @Override
+ public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
+ if (mApps.hasFilter() || mAppsPerRow == 0) {
+ return;
+ }
+
+ if (DEBUG_SECTION_MARGIN) {
+ Paint p = new Paint();
+ p.setColor(0x33ff0000);
+ c.drawRect(mBackgroundPadding.left, 0, mBackgroundPadding.left + mSectionNamesMargin,
+ parent.getMeasuredHeight(), p);
+ }
+
+ List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
+ boolean hasDrawnPredictedAppsDivider = false;
+ boolean showSectionNames = mSectionNamesMargin > 0;
+ int childCount = parent.getChildCount();
+ int lastSectionTop = 0;
+ int lastSectionHeight = 0;
+ for (int i = 0; i < childCount; i++) {
+ View child = parent.getChildAt(i);
+ ViewHolder holder = (ViewHolder) parent.getChildViewHolder(child);
+ if (!isValidHolderAndChild(holder, child, items)) {
+ continue;
+ }
+
+ if (shouldDrawItemDivider(holder, items) && !hasDrawnPredictedAppsDivider) {
+ // Draw the divider under the predicted apps
+ int top = child.getTop() + child.getHeight() + mPredictionBarDividerOffset;
+ c.drawLine(mBackgroundPadding.left, top,
+ parent.getWidth() - mBackgroundPadding.right, top,
+ mPredictedAppsDividerPaint);
+ hasDrawnPredictedAppsDivider = true;
+
+ } else if (showSectionNames && shouldDrawItemSection(holder, i, items)) {
+ // At this point, we only draw sections for each section break;
+ int viewTopOffset = (2 * child.getPaddingTop());
+ int pos = holder.getPosition();
+ AlphabeticalAppsList.AdapterItem item = items.get(pos);
+ AlphabeticalAppsList.SectionInfo sectionInfo = item.sectionInfo;
+
+ // Draw all the sections for this index
+ String lastSectionName = item.sectionName;
+ for (int j = item.sectionAppIndex; j < sectionInfo.numApps; j++, pos++) {
+ AlphabeticalAppsList.AdapterItem nextItem = items.get(pos);
+ String sectionName = nextItem.sectionName;
+ if (nextItem.sectionInfo != sectionInfo) {
+ break;
+ }
+ if (j > item.sectionAppIndex && sectionName.equals(lastSectionName)) {
+ continue;
+ }
+
+
+ // Find the section name bounds
+ PointF sectionBounds = getAndCacheSectionBounds(sectionName);
+
+ // Calculate where to draw the section
+ int sectionBaseline = (int) (viewTopOffset + sectionBounds.y);
+ int x = mIsRtl ?
+ parent.getWidth() - mBackgroundPadding.left - mSectionNamesMargin :
+ mBackgroundPadding.left;
+ x += (int) ((mSectionNamesMargin - sectionBounds.x) / 2f);
+ int y = child.getTop() + sectionBaseline;
+
+ // Determine whether this is the last row with apps in that section, if
+ // so, then fix the section to the row allowing it to scroll past the
+ // baseline, otherwise, bound it to the baseline so it's in the viewport
+ int appIndexInSection = items.get(pos).sectionAppIndex;
+ int nextRowPos = Math.min(items.size() - 1,
+ pos + mAppsPerRow - (appIndexInSection % mAppsPerRow));
+ AlphabeticalAppsList.AdapterItem nextRowItem = items.get(nextRowPos);
+ boolean fixedToRow = !sectionName.equals(nextRowItem.sectionName);
+ if (!fixedToRow) {
+ y = Math.max(sectionBaseline, y);
+ }
+
+ // In addition, if it overlaps with the last section that was drawn, then
+ // offset it so that it does not overlap
+ if (lastSectionHeight > 0 && y <= (lastSectionTop + lastSectionHeight)) {
+ y += lastSectionTop - y + lastSectionHeight;
+ }
+
+ // Draw the section header
+ if (FADE_OUT_SECTIONS) {
+ int alpha = 255;
+ if (fixedToRow) {
+ alpha = Math.min(255,
+ (int) (255 * (Math.max(0, y) / (float) sectionBaseline)));
+ }
+ mSectionTextPaint.setAlpha(alpha);
+ }
+ c.drawText(sectionName, x, y, mSectionTextPaint);
+
+ lastSectionTop = y;
+ lastSectionHeight = (int) (sectionBounds.y + mSectionHeaderOffset);
+ lastSectionName = sectionName;
+ }
+ i += (sectionInfo.numApps - item.sectionAppIndex);
+ }
+ }
+ }
+
+ @Override
+ public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
+ RecyclerView.State state) {
+ // Do nothing
+ }
+
+ /**
+ * Given a section name, return the bounds of the given section name.
+ */
+ private PointF getAndCacheSectionBounds(String sectionName) {
+ PointF bounds = mCachedSectionBounds.get(sectionName);
+ if (bounds == null) {
+ mSectionTextPaint.getTextBounds(sectionName, 0, sectionName.length(), mTmpBounds);
+ bounds = new PointF(mSectionTextPaint.measureText(sectionName), mTmpBounds.height());
+ mCachedSectionBounds.put(sectionName, bounds);
+ }
+ return bounds;
+ }
+
+ /**
+ * Returns whether we consider this a valid view holder for us to draw a divider or section for.
+ */
+ private boolean isValidHolderAndChild(ViewHolder holder, View child,
+ List<AlphabeticalAppsList.AdapterItem> items) {
+ // Ensure item is not already removed
+ GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams)
+ child.getLayoutParams();
+ if (lp.isItemRemoved()) {
+ return false;
+ }
+ // Ensure we have a valid holder
+ if (holder == null) {
+ return false;
+ }
+ // Ensure we have a holder position
+ int pos = holder.getPosition();
+ if (pos < 0 || pos >= items.size()) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Returns whether to draw the divider for a given child.
+ */
+ private boolean shouldDrawItemDivider(ViewHolder holder,
+ List<AlphabeticalAppsList.AdapterItem> items) {
+ int pos = holder.getPosition();
+ return items.get(pos).viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE;
+ }
+
+ /**
+ * Returns whether to draw the section for the given child.
+ */
+ private boolean shouldDrawItemSection(ViewHolder holder, int childIndex,
+ List<AlphabeticalAppsList.AdapterItem> items) {
+ int pos = holder.getPosition();
+ AlphabeticalAppsList.AdapterItem item = items.get(pos);
+
+ // Ensure it's an icon
+ if (item.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE) {
+ return false;
+ }
+ // Draw the section header for the first item in each section
+ return (childIndex == 0) ||
+ (items.get(pos - 1).viewType == AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE);
+ }
+ }
+
+ private LayoutInflater mLayoutInflater;
+ @Thunk AlphabeticalAppsList mApps;
+ private GridLayoutManager mGridLayoutMgr;
+ private GridSpanSizer mGridSizer;
+ private GridItemDecoration mItemDecoration;
+ private View.OnTouchListener mTouchListener;
+ private View.OnClickListener mIconClickListener;
+ private View.OnLongClickListener mIconLongClickListener;
+ @Thunk final Rect mBackgroundPadding = new Rect();
+ @Thunk int mPredictionBarDividerOffset;
+ @Thunk int mAppsPerRow;
+ @Thunk boolean mIsRtl;
+ private String mEmptySearchText;
+
+ // Section drawing
+ @Thunk int mSectionNamesMargin;
+ @Thunk int mSectionHeaderOffset;
+ @Thunk Paint mSectionTextPaint;
+ @Thunk Paint mPredictedAppsDividerPaint;
+
+ public AllAppsGridAdapter(Context context, AlphabeticalAppsList apps,
+ View.OnTouchListener touchListener, View.OnClickListener iconClickListener,
+ View.OnLongClickListener iconLongClickListener) {
+ Resources res = context.getResources();
+ mApps = apps;
+ mGridSizer = new GridSpanSizer();
+ mGridLayoutMgr = new AppsGridLayoutManager(context);
+ mGridLayoutMgr.setSpanSizeLookup(mGridSizer);
+ mItemDecoration = new GridItemDecoration();
+ mLayoutInflater = LayoutInflater.from(context);
+ mTouchListener = touchListener;
+ mIconClickListener = iconClickListener;
+ mIconLongClickListener = iconLongClickListener;
+ mSectionNamesMargin = res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin);
+ mSectionHeaderOffset = res.getDimensionPixelSize(R.dimen.all_apps_grid_section_y_offset);
+
+ mSectionTextPaint = new Paint();
+ mSectionTextPaint.setTextSize(res.getDimensionPixelSize(
+ R.dimen.all_apps_grid_section_text_size));
+ mSectionTextPaint.setColor(res.getColor(R.color.all_apps_grid_section_text_color));
+ mSectionTextPaint.setAntiAlias(true);
+
+ mPredictedAppsDividerPaint = new Paint();
+ mPredictedAppsDividerPaint.setStrokeWidth(Utilities.pxFromDp(1f, res.getDisplayMetrics()));
+ mPredictedAppsDividerPaint.setColor(0x1E000000);
+ mPredictedAppsDividerPaint.setAntiAlias(true);
+ mPredictionBarDividerOffset =
+ (-res.getDimensionPixelSize(R.dimen.all_apps_prediction_icon_bottom_padding) +
+ res.getDimensionPixelSize(R.dimen.all_apps_icon_top_bottom_padding)) / 2;
+ }
+
+ /**
+ * Sets the number of apps per row.
+ */
+ public void setNumAppsPerRow(int appsPerRow) {
+ mAppsPerRow = appsPerRow;
+ mGridLayoutMgr.setSpanCount(appsPerRow);
+ }
+
+ /**
+ * Sets whether we are in RTL mode.
+ */
+ public void setRtl(boolean rtl) {
+ mIsRtl = rtl;
+ }
+
+ /**
+ * Sets the text to show when there are no apps.
+ */
+ public void setEmptySearchText(String query) {
+ mEmptySearchText = query;
+ }
+
+ /**
+ * Notifies the adapter of the background padding so that it can draw things correctly in the
+ * item decorator.
+ */
+ public void updateBackgroundPadding(Rect padding) {
+ mBackgroundPadding.set(padding);
+ }
+
+ /**
+ * Returns the grid layout manager.
+ */
+ public GridLayoutManager getLayoutManager() {
+ return mGridLayoutMgr;
+ }
+
+ /**
+ * Returns the item decoration for the recycler view.
+ */
+ public RecyclerView.ItemDecoration getItemDecoration() {
+ // We don't draw any headers when we are uncomfortably dense
+ return mItemDecoration;
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ switch (viewType) {
+ case EMPTY_SEARCH_VIEW_TYPE:
+ return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search, parent,
+ false));
+ case SECTION_BREAK_VIEW_TYPE:
+ return new ViewHolder(new View(parent.getContext()));
+ case ICON_VIEW_TYPE: {
+ BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
+ R.layout.all_apps_icon, parent, false);
+ icon.setOnTouchListener(mTouchListener);
+ icon.setOnClickListener(mIconClickListener);
+ icon.setOnLongClickListener(mIconLongClickListener);
+ icon.setLongPressTimeout(ViewConfiguration.get(parent.getContext())
+ .getLongPressTimeout());
+ icon.setFocusable(true);
+ return new ViewHolder(icon);
+ }
+ case PREDICTION_ICON_VIEW_TYPE: {
+ BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
+ R.layout.all_apps_prediction_bar_icon, parent, false);
+ icon.setOnTouchListener(mTouchListener);
+ icon.setOnClickListener(mIconClickListener);
+ icon.setOnLongClickListener(mIconLongClickListener);
+ icon.setLongPressTimeout(ViewConfiguration.get(parent.getContext())
+ .getLongPressTimeout());
+ icon.setFocusable(true);
+ return new ViewHolder(icon);
+ }
+ default:
+ throw new RuntimeException("Unexpected view type");
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ switch (holder.getItemViewType()) {
+ case ICON_VIEW_TYPE: {
+ AppInfo info = mApps.getAdapterItems().get(position).appInfo;
+ BubbleTextView icon = (BubbleTextView) holder.mContent;
+ icon.applyFromApplicationInfo(info);
+ break;
+ }
+ case PREDICTION_ICON_VIEW_TYPE: {
+ AppInfo info = mApps.getAdapterItems().get(position).appInfo;
+ BubbleTextView icon = (BubbleTextView) holder.mContent;
+ icon.applyFromApplicationInfo(info);
+ break;
+ }
+ case EMPTY_SEARCH_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.getAdapterItems().size();
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ if (mApps.hasNoFilteredResults()) {
+ return EMPTY_SEARCH_VIEW_TYPE;
+ }
+
+ AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position);
+ return item.viewType;
+ }
+}
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
new file mode 100644
index 000000000..730c8d15a
--- /dev/null
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.allapps;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.os.Bundle;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.android.launcher3.BaseRecyclerView;
+import com.android.launcher3.BaseRecyclerViewFastScrollBar;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Stats;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.util.Thunk;
+
+import java.util.List;
+
+/**
+ * A RecyclerView with custom fast scroll support for the all apps view.
+ */
+public class AllAppsRecyclerView extends BaseRecyclerView
+ implements Stats.LaunchSourceProvider {
+
+ private static final int FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON = 0;
+ private static final int FAST_SCROLL_MODE_FREE_SCROLL = 1;
+
+ private static final int FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW = 0;
+ private static final int FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_SECTIONS = 1;
+
+ private AlphabeticalAppsList mApps;
+ private int mNumAppsPerRow;
+
+ @Thunk BaseRecyclerViewFastScrollBar.FastScrollFocusableView mLastFastScrollFocusedView;
+ @Thunk int mPrevFastScrollFocusedPosition;
+ @Thunk int mFastScrollFrameIndex;
+ @Thunk final int[] mFastScrollFrames = new int[10];
+
+ private final int mFastScrollMode = FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON;
+ private final int mScrollBarMode = FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW;
+
+ private ScrollPositionState mScrollPosState = new ScrollPositionState();
+
+ public AllAppsRecyclerView(Context context) {
+ this(context, null);
+ }
+
+ public AllAppsRecyclerView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ /**
+ * Sets the list of apps in this view, used to determine the fastscroll position.
+ */
+ public void setApps(AlphabeticalAppsList apps) {
+ mApps = apps;
+ }
+
+ /**
+ * Sets the number of apps per row in this recycler view.
+ */
+ public void setNumAppsPerRow(DeviceProfile grid, int numAppsPerRow) {
+ mNumAppsPerRow = numAppsPerRow;
+
+ RecyclerView.RecycledViewPool pool = getRecycledViewPool();
+ int approxRows = (int) Math.ceil(grid.availableHeightPx / grid.allAppsIconSizePx);
+ pool.setMaxRecycledViews(AllAppsGridAdapter.EMPTY_SEARCH_VIEW_TYPE, 1);
+ pool.setMaxRecycledViews(AllAppsGridAdapter.ICON_VIEW_TYPE, approxRows * mNumAppsPerRow);
+ pool.setMaxRecycledViews(AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE, mNumAppsPerRow);
+ pool.setMaxRecycledViews(AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE, approxRows);
+ }
+
+ /**
+ * Scrolls this recycler view to the top.
+ */
+ public void scrollToTop() {
+ scrollToPosition(0);
+ }
+
+ /**
+ * We need to override the draw to ensure that we don't draw the overscroll effect beyond the
+ * background bounds.
+ */
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ canvas.clipRect(mBackgroundPadding.left, mBackgroundPadding.top,
+ getWidth() - mBackgroundPadding.right,
+ getHeight() - mBackgroundPadding.bottom);
+ super.dispatchDraw(canvas);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ // Bind event handlers
+ addOnItemTouchListener(this);
+ }
+
+ @Override
+ public void fillInLaunchSourceData(Bundle sourceData) {
+ sourceData.putString(Stats.SOURCE_EXTRA_CONTAINER, Stats.CONTAINER_ALL_APPS);
+ if (mApps.hasFilter()) {
+ sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER,
+ Stats.SUB_CONTAINER_ALL_APPS_SEARCH);
+ } else {
+ sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER,
+ Stats.SUB_CONTAINER_ALL_APPS_A_Z);
+ }
+ }
+
+ /**
+ * Maps the touch (from 0..1) to the adapter position that should be visible.
+ */
+ @Override
+ public String scrollToPositionAtProgress(float touchFraction) {
+ int rowCount = mApps.getNumAppRows();
+ if (rowCount == 0) {
+ return "";
+ }
+
+ // Stop the scroller if it is scrolling
+ stopScroll();
+
+ // Find the fastscroll section that maps to this touch fraction
+ List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections =
+ mApps.getFastScrollerSections();
+ AlphabeticalAppsList.FastScrollSectionInfo lastInfo = fastScrollSections.get(0);
+ if (mScrollBarMode == FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW) {
+ for (int i = 1; i < fastScrollSections.size(); i++) {
+ AlphabeticalAppsList.FastScrollSectionInfo info = fastScrollSections.get(i);
+ if (info.touchFraction > touchFraction) {
+ break;
+ }
+ lastInfo = info;
+ }
+ } else if (mScrollBarMode == FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_SECTIONS){
+ lastInfo = fastScrollSections.get((int) (touchFraction * (fastScrollSections.size() - 1)));
+ } else {
+ throw new RuntimeException("Unexpected scroll bar mode");
+ }
+
+ // Map the touch position back to the scroll of the recycler view
+ getCurScrollState(mScrollPosState, mApps.getAdapterItems());
+ int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight, 0);
+ LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager();
+ if (mFastScrollMode == FAST_SCROLL_MODE_FREE_SCROLL) {
+ layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction));
+ }
+
+ if (mPrevFastScrollFocusedPosition != lastInfo.fastScrollToItem.position) {
+ mPrevFastScrollFocusedPosition = lastInfo.fastScrollToItem.position;
+
+ // Reset the last focused view
+ if (mLastFastScrollFocusedView != null) {
+ mLastFastScrollFocusedView.setFastScrollFocused(false, true);
+ mLastFastScrollFocusedView = null;
+ }
+
+ if (mFastScrollMode == FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON) {
+ smoothSnapToPosition(mPrevFastScrollFocusedPosition, mScrollPosState);
+ } else if (mFastScrollMode == FAST_SCROLL_MODE_FREE_SCROLL) {
+ final ViewHolder vh = findViewHolderForPosition(mPrevFastScrollFocusedPosition);
+ if (vh != null &&
+ vh.itemView instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView) {
+ mLastFastScrollFocusedView =
+ (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView;
+ mLastFastScrollFocusedView.setFastScrollFocused(true, true);
+ }
+ } else {
+ throw new RuntimeException("Unexpected fast scroll mode");
+ }
+ }
+ return lastInfo.sectionName;
+ }
+
+ @Override
+ public void onFastScrollCompleted() {
+ super.onFastScrollCompleted();
+ // Reset and clean up the last focused view
+ if (mLastFastScrollFocusedView != null) {
+ mLastFastScrollFocusedView.setFastScrollFocused(false, true);
+ mLastFastScrollFocusedView = null;
+ }
+ mPrevFastScrollFocusedPosition = -1;
+ }
+
+ /**
+ * Updates the bounds for the scrollbar.
+ */
+ @Override
+ public void onUpdateScrollbar() {
+ List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
+
+ // Skip early if there are no items or we haven't been measured
+ if (items.isEmpty() || mNumAppsPerRow == 0) {
+ mScrollbar.setScrollbarThumbOffset(-1, -1);
+ return;
+ }
+
+ // Find the index and height of the first visible row (all rows have the same height)
+ int rowCount = mApps.getNumAppRows();
+ getCurScrollState(mScrollPosState, items);
+ if (mScrollPosState.rowIndex < 0) {
+ mScrollbar.setScrollbarThumbOffset(-1, -1);
+ return;
+ }
+
+ synchronizeScrollBarThumbOffsetToViewScroll(mScrollPosState, rowCount, 0);
+ }
+
+ /**
+ * This runnable runs a single frame of the smooth scroll animation and posts the next frame
+ * if necessary.
+ */
+ @Thunk Runnable mSmoothSnapNextFrameRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (mFastScrollFrameIndex < mFastScrollFrames.length) {
+ scrollBy(0, mFastScrollFrames[mFastScrollFrameIndex]);
+ mFastScrollFrameIndex++;
+ postOnAnimation(mSmoothSnapNextFrameRunnable);
+ } else {
+ // Animation completed, set the fast scroll state on the target view
+ final ViewHolder vh = findViewHolderForPosition(mPrevFastScrollFocusedPosition);
+ if (vh != null &&
+ vh.itemView instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView &&
+ mLastFastScrollFocusedView != vh.itemView) {
+ mLastFastScrollFocusedView =
+ (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView;
+ mLastFastScrollFocusedView.setFastScrollFocused(true, true);
+ }
+ }
+ }
+ };
+
+ /**
+ * Smoothly snaps to a given position. We do this manually by calculating the keyframes
+ * ourselves and animating the scroll on the recycler view.
+ */
+ private void smoothSnapToPosition(final int position, ScrollPositionState scrollPosState) {
+ removeCallbacks(mSmoothSnapNextFrameRunnable);
+
+ // Calculate the full animation from the current scroll position to the final scroll
+ // position, and then run the animation for the duration.
+ int curScrollY = getPaddingTop() +
+ (scrollPosState.rowIndex * scrollPosState.rowHeight) - scrollPosState.rowTopOffset;
+ int newScrollY = getScrollAtPosition(position, scrollPosState.rowHeight);
+ int numFrames = mFastScrollFrames.length;
+ for (int i = 0; i < numFrames; i++) {
+ // TODO(winsonc): We can interpolate this as well.
+ mFastScrollFrames[i] = (newScrollY - curScrollY) / numFrames;
+ }
+ mFastScrollFrameIndex = 0;
+ postOnAnimation(mSmoothSnapNextFrameRunnable);
+ }
+
+ /**
+ * Returns the current scroll state of the apps rows.
+ */
+ private void getCurScrollState(ScrollPositionState stateOut,
+ List<AlphabeticalAppsList.AdapterItem> items) {
+ stateOut.rowIndex = -1;
+ stateOut.rowTopOffset = -1;
+ stateOut.rowHeight = -1;
+
+ // Return early if there are no items or we haven't been measured
+ if (items.isEmpty() || mNumAppsPerRow == 0) {
+ return;
+ }
+
+ 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 == AllAppsGridAdapter.ICON_VIEW_TYPE ||
+ item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) {
+ stateOut.rowIndex = item.rowIndex;
+ stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child);
+ stateOut.rowHeight = child.getHeight();
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the scrollY for the given position in the adapter.
+ */
+ private int getScrollAtPosition(int position, int rowHeight) {
+ AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position);
+ if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE ||
+ item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) {
+ int offset = item.rowIndex > 0 ? getPaddingTop() : 0;
+ return offset + item.rowIndex * rowHeight;
+ } else {
+ return 0;
+ }
+ }
+}
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerViewContainerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerViewContainerView.java
new file mode 100644
index 000000000..14e2a1863
--- /dev/null
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerViewContainerView.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.allapps;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.BubbleTextView.BubbleTextShadowHandler;
+import com.android.launcher3.ClickShadowView;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+
+/**
+ * A container for RecyclerView to allow for the click shadow view to be shown behind an icon that
+ * is launching.
+ */
+public class AllAppsRecyclerViewContainerView extends FrameLayout
+ implements BubbleTextShadowHandler {
+
+ private final ClickShadowView mTouchFeedbackView;
+
+ public AllAppsRecyclerViewContainerView(Context context) {
+ this(context, null);
+ }
+
+ public AllAppsRecyclerViewContainerView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public AllAppsRecyclerViewContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ Launcher launcher = (Launcher) context;
+ DeviceProfile grid = launcher.getDeviceProfile();
+
+ mTouchFeedbackView = new ClickShadowView(context);
+
+ // Make the feedback view large enough to hold the blur bitmap.
+ int size = grid.allAppsIconSizePx + mTouchFeedbackView.getExtraSize();
+ addView(mTouchFeedbackView, size, size);
+ }
+
+ @Override
+ public void setPressedIcon(BubbleTextView icon, Bitmap background) {
+ if (icon == null || background == null) {
+ mTouchFeedbackView.setBitmap(null);
+ mTouchFeedbackView.animate().cancel();
+ } else if (mTouchFeedbackView.setBitmap(background)) {
+ mTouchFeedbackView.alignWithIconView(icon, (ViewGroup) icon.getParent());
+ mTouchFeedbackView.animateShadow();
+ }
+ }
+}
diff --git a/src/com/android/launcher3/allapps/AllAppsSearchBarController.java b/src/com/android/launcher3/allapps/AllAppsSearchBarController.java
new file mode 100644
index 000000000..2b363c0cb
--- /dev/null
+++ b/src/com/android/launcher3/allapps/AllAppsSearchBarController.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.allapps;
+
+import android.content.ComponentName;
+import android.graphics.Rect;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.launcher3.util.ComponentKey;
+
+import java.util.ArrayList;
+
+/**
+ * An interface to a search box that AllApps can command.
+ */
+public abstract class AllAppsSearchBarController {
+
+ protected AlphabeticalAppsList mApps;
+ protected Callbacks mCb;
+
+ /**
+ * Sets the references to the apps model and the search result callback.
+ */
+ public final void initialize(AlphabeticalAppsList apps, Callbacks cb) {
+ mApps = apps;
+ mCb = cb;
+ onInitialize();
+ }
+
+ /**
+ * To be overridden by subclasses. This method will get called when the controller is set,
+ * before getView().
+ */
+ protected abstract void onInitialize();
+
+ /**
+ * Returns the search bar view.
+ * @param parent the parent to attach the search bar view to.
+ */
+ public abstract View getView(ViewGroup parent);
+
+ /**
+ * Focuses the search field to handle key events.
+ */
+ public abstract void focusSearchField();
+
+ /**
+ * Returns whether the search field is focused.
+ */
+ public abstract boolean isSearchFieldFocused();
+
+ /**
+ * Resets the search bar state.
+ */
+ public abstract void reset();
+
+ /**
+ * Returns whether the prediction bar should currently be visible depending on the state of
+ * the search bar.
+ */
+ @Deprecated
+ public abstract boolean shouldShowPredictionBar();
+
+ /**
+ * Callback for getting search results.
+ */
+ public interface Callbacks {
+
+ /**
+ * Called when the bounds of the search bar has changed.
+ */
+ void onBoundsChanged(Rect newBounds);
+
+ /**
+ * Called when the search is complete.
+ *
+ * @param apps sorted list of matching components or null if in case of failure.
+ */
+ void onSearchResult(String query, ArrayList<ComponentKey> apps);
+
+ /**
+ * Called when the search results should be cleared.
+ */
+ void clearSearchResult();
+ }
+} \ No newline at end of file
diff --git a/src/com/android/launcher3/allapps/AllAppsSearchEditView.java b/src/com/android/launcher3/allapps/AllAppsSearchEditView.java
new file mode 100644
index 000000000..b7dcd66ed
--- /dev/null
+++ b/src/com/android/launcher3/allapps/AllAppsSearchEditView.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.allapps;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.widget.EditText;
+
+
+/**
+ * The edit text for the search container
+ */
+public class AllAppsSearchEditView extends EditText {
+
+ /**
+ * Implemented by listeners of the back key.
+ */
+ public interface OnBackKeyListener {
+ public void onBackKey();
+ }
+
+ private OnBackKeyListener mBackKeyListener;
+
+ public AllAppsSearchEditView(Context context) {
+ this(context, null);
+ }
+
+ public AllAppsSearchEditView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public AllAppsSearchEditView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public void setOnBackKeyListener(OnBackKeyListener listener) {
+ mBackKeyListener = listener;
+ }
+
+ @Override
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ // If this is a back key, propagate the key back to the listener
+ if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) {
+ if (mBackKeyListener != null) {
+ mBackKeyListener.onBackKey();
+ }
+ return false;
+ }
+ return super.onKeyPreIme(keyCode, event);
+ }
+}
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
new file mode 100644
index 000000000..47241ce5d
--- /dev/null
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -0,0 +1,590 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.allapps;
+
+import android.content.Context;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.compat.AlphabeticIndexCompat;
+import com.android.launcher3.compat.UserHandleCompat;
+import com.android.launcher3.model.AppNameComparator;
+import com.android.launcher3.util.ComponentKey;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * The alphabetically sorted list of applications.
+ */
+public class AlphabeticalAppsList {
+
+ public static final String TAG = "AlphabeticalAppsList";
+ private static final boolean DEBUG = false;
+ private static final boolean DEBUG_PREDICTIONS = false;
+
+ /**
+ * Info about a section in the alphabetic list
+ */
+ public static class SectionInfo {
+ // The number of applications in this section
+ public int numApps;
+ // The section break AdapterItem for this section
+ public AdapterItem sectionBreakItem;
+ // The first app AdapterItem for this section
+ public AdapterItem firstAppItem;
+ }
+
+ /**
+ * Info about a fast scroller section, depending if sections are merged, the fast scroller
+ * sections will not be the same set as the section headers.
+ */
+ public static class FastScrollSectionInfo {
+ // The section name
+ public String sectionName;
+ // The AdapterItem to scroll to for this section
+ public AdapterItem fastScrollToItem;
+ // The touch fraction that should map to this fast scroll section info
+ public float touchFraction;
+
+ public FastScrollSectionInfo(String sectionName) {
+ this.sectionName = sectionName;
+ }
+ }
+
+ /**
+ * Info about a particular adapter item (can be either section or app)
+ */
+ public static class AdapterItem {
+ /** Common properties */
+ // The index of this adapter item in the list
+ public int position;
+ // The type of this item
+ public int viewType;
+ // The row that this item shows up on
+ public int rowIndex;
+
+ /** Section & App properties */
+ // The section for this item
+ public SectionInfo sectionInfo;
+
+ /** App-only properties */
+ // The section name of this app. Note that there can be multiple items with different
+ // sectionNames in the same section
+ public String sectionName = null;
+ // The index of this app in the section
+ public int sectionAppIndex = -1;
+ // The index of this app in the row
+ public int rowAppIndex;
+ // The associated AppInfo for the app
+ public AppInfo appInfo = null;
+ // The index of this app not including sections
+ public int appIndex = -1;
+
+ public static AdapterItem asSectionBreak(int pos, SectionInfo section) {
+ AdapterItem item = new AdapterItem();
+ item.viewType = AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE;
+ item.position = pos;
+ item.sectionInfo = section;
+ section.sectionBreakItem = item;
+ return item;
+ }
+
+ public static AdapterItem asPredictedApp(int pos, SectionInfo section, String sectionName,
+ int sectionAppIndex, AppInfo appInfo, int appIndex) {
+ AdapterItem item = asApp(pos, section, sectionName, sectionAppIndex, appInfo, appIndex);
+ item.viewType = AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE;
+ return item;
+ }
+
+ public static AdapterItem asApp(int pos, SectionInfo section, String sectionName,
+ int sectionAppIndex, AppInfo appInfo, int appIndex) {
+ AdapterItem item = new AdapterItem();
+ item.viewType = AllAppsGridAdapter.ICON_VIEW_TYPE;
+ item.position = pos;
+ item.sectionInfo = section;
+ item.sectionName = sectionName;
+ item.sectionAppIndex = sectionAppIndex;
+ item.appInfo = appInfo;
+ item.appIndex = appIndex;
+ return item;
+ }
+ }
+
+ /**
+ * Common interface for different merging strategies.
+ */
+ public interface MergeAlgorithm {
+ boolean continueMerging(SectionInfo section, SectionInfo withSection,
+ int sectionAppCount, int numAppsPerRow, int mergeCount);
+ }
+
+ private Launcher mLauncher;
+
+ // The set of apps from the system not including predictions
+ private final List<AppInfo> mApps = new ArrayList<>();
+ private final HashMap<ComponentKey, AppInfo> mComponentToAppMap = new HashMap<>();
+
+ // The set of filtered apps with the current filter
+ private List<AppInfo> mFilteredApps = new ArrayList<>();
+ // The current set of adapter items
+ private List<AdapterItem> mAdapterItems = new ArrayList<>();
+ // The set of sections for the apps with the current filter
+ private List<SectionInfo> mSections = new ArrayList<>();
+ // The set of sections that we allow fast-scrolling to (includes non-merged sections)
+ private List<FastScrollSectionInfo> mFastScrollerSections = new ArrayList<>();
+ // The set of predicted app component names
+ private List<ComponentKey> mPredictedAppComponents = new ArrayList<>();
+ // The set of predicted apps resolved from the component names and the current set of apps
+ private List<AppInfo> mPredictedApps = new ArrayList<>();
+ // The of ordered component names as a result of a search query
+ private ArrayList<ComponentKey> mSearchResults;
+ private HashMap<CharSequence, String> mCachedSectionNames = new HashMap<>();
+ private RecyclerView.Adapter mAdapter;
+ private AlphabeticIndexCompat mIndexer;
+ private AppNameComparator mAppNameComparator;
+ private MergeAlgorithm mMergeAlgorithm;
+ private int mNumAppsPerRow;
+ private int mNumPredictedAppsPerRow;
+ private int mNumAppRowsInAdapter;
+
+ public AlphabeticalAppsList(Context context) {
+ mLauncher = (Launcher) context;
+ mIndexer = new AlphabeticIndexCompat(context);
+ mAppNameComparator = new AppNameComparator(context);
+ }
+
+ /**
+ * Sets the number of apps per row.
+ */
+ public void setNumAppsPerRow(int numAppsPerRow, int numPredictedAppsPerRow,
+ MergeAlgorithm mergeAlgorithm) {
+ mNumAppsPerRow = numAppsPerRow;
+ mNumPredictedAppsPerRow = numPredictedAppsPerRow;
+ mMergeAlgorithm = mergeAlgorithm;
+
+ updateAdapterItems();
+ }
+
+ /**
+ * Sets the adapter to notify when this dataset changes.
+ */
+ public void setAdapter(RecyclerView.Adapter adapter) {
+ mAdapter = adapter;
+ }
+
+ /**
+ * Returns all the apps.
+ */
+ public List<AppInfo> getApps() {
+ return mApps;
+ }
+
+ /**
+ * Returns sections of all the current filtered applications.
+ */
+ public List<SectionInfo> getSections() {
+ return mSections;
+ }
+
+ /**
+ * Returns fast scroller sections of all the current filtered applications.
+ */
+ public List<FastScrollSectionInfo> getFastScrollerSections() {
+ return mFastScrollerSections;
+ }
+
+ /**
+ * Returns the current filtered list of applications broken down into their sections.
+ */
+ public List<AdapterItem> getAdapterItems() {
+ return mAdapterItems;
+ }
+
+ /**
+ * Returns the number of applications in this list.
+ */
+ public int getSize() {
+ return mFilteredApps.size();
+ }
+
+ /**
+ * Returns the number of rows of applications (not including predictions)
+ */
+ public int getNumAppRows() {
+ return mNumAppRowsInAdapter;
+ }
+
+ /**
+ * Returns whether there are is a filter set.
+ */
+ public boolean hasFilter() {
+ return (mSearchResults != null);
+ }
+
+ /**
+ * Returns whether there are no filtered results.
+ */
+ public boolean hasNoFilteredResults() {
+ return (mSearchResults != null) && mFilteredApps.isEmpty();
+ }
+
+ /**
+ * Sets the sorted list of filtered components.
+ */
+ public void setOrderedFilter(ArrayList<ComponentKey> f) {
+ if (mSearchResults != f) {
+ mSearchResults = f;
+ updateAdapterItems();
+ }
+ }
+
+ /**
+ * Sets the current set of predicted apps. Since this can be called before we get the full set
+ * of applications, we should merge the results only in onAppsUpdated() which is idempotent.
+ */
+ public void setPredictedApps(List<ComponentKey> apps) {
+ mPredictedAppComponents.clear();
+ mPredictedAppComponents.addAll(apps);
+ onAppsUpdated();
+ }
+
+ /**
+ * Sets the current set of apps.
+ */
+ public void setApps(List<AppInfo> apps) {
+ mComponentToAppMap.clear();
+ addApps(apps);
+ }
+
+ /**
+ * Adds new apps to the list.
+ */
+ public void addApps(List<AppInfo> apps) {
+ updateApps(apps);
+ }
+
+ /**
+ * Updates existing apps in the list
+ */
+ public void updateApps(List<AppInfo> apps) {
+ for (AppInfo app : apps) {
+ mComponentToAppMap.put(app.toComponentKey(), app);
+ }
+ onAppsUpdated();
+ }
+
+ /**
+ * Removes some apps from the list.
+ */
+ public void removeApps(List<AppInfo> apps) {
+ for (AppInfo app : apps) {
+ mComponentToAppMap.remove(app.toComponentKey());
+ }
+ onAppsUpdated();
+ }
+
+ /**
+ * Updates internals when the set of apps are updated.
+ */
+ private void onAppsUpdated() {
+ // Sort the list of apps
+ mApps.clear();
+ mApps.addAll(mComponentToAppMap.values());
+ Collections.sort(mApps, mAppNameComparator.getAppInfoComparator());
+
+ // As a special case for some languages (currently only Simplified Chinese), we may need to
+ // coalesce sections
+ Locale curLocale = mLauncher.getResources().getConfiguration().locale;
+ TreeMap<String, ArrayList<AppInfo>> sectionMap = null;
+ boolean localeRequiresSectionSorting = curLocale.equals(Locale.SIMPLIFIED_CHINESE);
+ if (localeRequiresSectionSorting) {
+ // Compute the section headers. We use a TreeMap with the section name comparator to
+ // ensure that the sections are ordered when we iterate over it later
+ sectionMap = new TreeMap<>(mAppNameComparator.getSectionNameComparator());
+ for (AppInfo info : mApps) {
+ // Add the section to the cache
+ String sectionName = getAndUpdateCachedSectionName(info.title);
+
+ // Add it to the mapping
+ ArrayList<AppInfo> sectionApps = sectionMap.get(sectionName);
+ if (sectionApps == null) {
+ sectionApps = new ArrayList<>();
+ sectionMap.put(sectionName, sectionApps);
+ }
+ sectionApps.add(info);
+ }
+
+ // Add each of the section apps to the list in order
+ List<AppInfo> allApps = new ArrayList<>(mApps.size());
+ for (Map.Entry<String, ArrayList<AppInfo>> entry : sectionMap.entrySet()) {
+ allApps.addAll(entry.getValue());
+ }
+
+ mApps.clear();
+ mApps.addAll(allApps);
+ } else {
+ // Just compute the section headers for use below
+ for (AppInfo info : mApps) {
+ // Add the section to the cache
+ getAndUpdateCachedSectionName(info.title);
+ }
+ }
+
+ // Recompose the set of adapter items from the current set of apps
+ updateAdapterItems();
+ }
+
+ /**
+ * Updates the set of filtered apps with the current filter. At this point, we expect
+ * mCachedSectionNames to have been calculated for the set of all apps in mApps.
+ */
+ private void updateAdapterItems() {
+ SectionInfo lastSectionInfo = null;
+ String lastSectionName = null;
+ FastScrollSectionInfo lastFastScrollerSectionInfo = null;
+ int position = 0;
+ int appIndex = 0;
+
+ // Prepare to update the list of sections, filtered apps, etc.
+ mFilteredApps.clear();
+ mFastScrollerSections.clear();
+ mAdapterItems.clear();
+ mSections.clear();
+
+ if (DEBUG_PREDICTIONS) {
+ if (mPredictedAppComponents.isEmpty() && !mApps.isEmpty()) {
+ mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName,
+ UserHandleCompat.myUserHandle()));
+ mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName,
+ UserHandleCompat.myUserHandle()));
+ mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName,
+ UserHandleCompat.myUserHandle()));
+ mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName,
+ UserHandleCompat.myUserHandle()));
+ }
+ }
+
+ // Process the predicted app components
+ mPredictedApps.clear();
+ if (mPredictedAppComponents != null && !mPredictedAppComponents.isEmpty() && !hasFilter()) {
+ for (ComponentKey ck : mPredictedAppComponents) {
+ AppInfo info = mComponentToAppMap.get(ck);
+ if (info != null) {
+ mPredictedApps.add(info);
+ } else {
+ if (LauncherAppState.isDogfoodBuild()) {
+ Log.e(TAG, "Predicted app not found: " + ck.flattenToString(mLauncher));
+ }
+ }
+ // Stop at the number of predicted apps
+ if (mPredictedApps.size() == mNumPredictedAppsPerRow) {
+ break;
+ }
+ }
+
+ if (!mPredictedApps.isEmpty()) {
+ // Add a section for the predictions
+ lastSectionInfo = new SectionInfo();
+ lastFastScrollerSectionInfo = new FastScrollSectionInfo("");
+ AdapterItem sectionItem = AdapterItem.asSectionBreak(position++, lastSectionInfo);
+ mSections.add(lastSectionInfo);
+ mFastScrollerSections.add(lastFastScrollerSectionInfo);
+ mAdapterItems.add(sectionItem);
+
+ // Add the predicted app items
+ for (AppInfo info : mPredictedApps) {
+ AdapterItem appItem = AdapterItem.asPredictedApp(position++, lastSectionInfo,
+ "", lastSectionInfo.numApps++, info, appIndex++);
+ if (lastSectionInfo.firstAppItem == null) {
+ lastSectionInfo.firstAppItem = appItem;
+ lastFastScrollerSectionInfo.fastScrollToItem = appItem;
+ }
+ mAdapterItems.add(appItem);
+ mFilteredApps.add(info);
+ }
+ }
+ }
+
+ // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the
+ // ordered set of sections
+ for (AppInfo info : getFiltersAppInfos()) {
+ String sectionName = getAndUpdateCachedSectionName(info.title);
+
+ // Create a new section if the section names do not match
+ if (lastSectionInfo == null || !sectionName.equals(lastSectionName)) {
+ lastSectionName = sectionName;
+ lastSectionInfo = new SectionInfo();
+ lastFastScrollerSectionInfo = new FastScrollSectionInfo(sectionName);
+ mSections.add(lastSectionInfo);
+ mFastScrollerSections.add(lastFastScrollerSectionInfo);
+
+ // Create a new section item to break the flow of items in the list
+ if (!hasFilter()) {
+ AdapterItem sectionItem = AdapterItem.asSectionBreak(position++, lastSectionInfo);
+ mAdapterItems.add(sectionItem);
+ }
+ }
+
+ // Create an app item
+ AdapterItem appItem = AdapterItem.asApp(position++, lastSectionInfo, sectionName,
+ lastSectionInfo.numApps++, info, appIndex++);
+ if (lastSectionInfo.firstAppItem == null) {
+ lastSectionInfo.firstAppItem = appItem;
+ lastFastScrollerSectionInfo.fastScrollToItem = appItem;
+ }
+ mAdapterItems.add(appItem);
+ mFilteredApps.add(info);
+ }
+
+ // Merge multiple sections together as requested by the merge strategy for this device
+ mergeSections();
+
+ if (mNumAppsPerRow != 0) {
+ // Update the number of rows in the adapter after we do all the merging (otherwise, we
+ // would have to shift the values again)
+ int numAppsInSection = 0;
+ int numAppsInRow = 0;
+ int rowIndex = -1;
+ for (AdapterItem item : mAdapterItems) {
+ item.rowIndex = 0;
+ if (item.viewType == AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE) {
+ numAppsInSection = 0;
+ } else if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE ||
+ item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) {
+ if (numAppsInSection % mNumAppsPerRow == 0) {
+ numAppsInRow = 0;
+ rowIndex++;
+ }
+ item.rowIndex = rowIndex;
+ item.rowAppIndex = numAppsInRow;
+ numAppsInSection++;
+ numAppsInRow++;
+ }
+ }
+ mNumAppRowsInAdapter = rowIndex + 1;
+
+ // Pre-calculate all the fast scroller fractions based on the number of rows
+ float rowFraction = 1f / mNumAppRowsInAdapter;
+ for (FastScrollSectionInfo info : mFastScrollerSections) {
+ AdapterItem item = info.fastScrollToItem;
+ if (item.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE &&
+ item.viewType != AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) {
+ info.touchFraction = 0f;
+ continue;
+ }
+
+ float subRowFraction = item.rowAppIndex * (rowFraction / mNumAppsPerRow);
+ info.touchFraction = item.rowIndex * rowFraction + subRowFraction;
+ }
+ }
+
+ // Refresh the recycler view
+ if (mAdapter != null) {
+ mAdapter.notifyDataSetChanged();
+ }
+ }
+
+ private List<AppInfo> getFiltersAppInfos() {
+ if (mSearchResults == null) {
+ return mApps;
+ }
+
+ ArrayList<AppInfo> result = new ArrayList<>();
+ for (ComponentKey key : mSearchResults) {
+ AppInfo match = mComponentToAppMap.get(key);
+ if (match != null) {
+ result.add(match);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Merges multiple sections to reduce visual raggedness.
+ */
+ private void mergeSections() {
+ // Ignore merging until we have an algorithm and a valid row size
+ if (mMergeAlgorithm == null || mNumAppsPerRow == 0) {
+ return;
+ }
+
+ // Go through each section and try and merge some of the sections
+ if (!hasFilter()) {
+ int sectionAppCount = 0;
+ for (int i = 0; i < mSections.size() - 1; i++) {
+ SectionInfo section = mSections.get(i);
+ sectionAppCount = section.numApps;
+ int mergeCount = 1;
+
+ // Merge rows based on the current strategy
+ while (i < (mSections.size() - 1) &&
+ mMergeAlgorithm.continueMerging(section, mSections.get(i + 1),
+ sectionAppCount, mNumAppsPerRow, mergeCount)) {
+ SectionInfo nextSection = mSections.remove(i + 1);
+
+ // Remove the next section break
+ mAdapterItems.remove(nextSection.sectionBreakItem);
+ int pos = mAdapterItems.indexOf(section.firstAppItem);
+
+ // Point the section for these new apps to the merged section
+ int nextPos = pos + section.numApps;
+ for (int j = nextPos; j < (nextPos + nextSection.numApps); j++) {
+ AdapterItem item = mAdapterItems.get(j);
+ item.sectionInfo = section;
+ item.sectionAppIndex += section.numApps;
+ }
+
+ // Update the following adapter items of the removed section item
+ pos = mAdapterItems.indexOf(nextSection.firstAppItem);
+ for (int j = pos; j < mAdapterItems.size(); j++) {
+ AdapterItem item = mAdapterItems.get(j);
+ item.position--;
+ }
+ section.numApps += nextSection.numApps;
+ sectionAppCount += nextSection.numApps;
+
+ if (DEBUG) {
+ Log.d(TAG, "Merging: " + nextSection.firstAppItem.sectionName +
+ " to " + section.firstAppItem.sectionName +
+ " mergedNumRows: " + (sectionAppCount / mNumAppsPerRow));
+ }
+ mergeCount++;
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the cached section name for the given title, recomputing and updating the cache if
+ * the title has no cached section name.
+ */
+ private String getAndUpdateCachedSectionName(CharSequence title) {
+ String sectionName = mCachedSectionNames.get(title);
+ if (sectionName == null) {
+ sectionName = mIndexer.computeSectionName(title);
+ mCachedSectionNames.put(title, sectionName);
+ }
+ return sectionName;
+ }
+}
diff --git a/src/com/android/launcher3/allapps/DefaultAppSearchAlgorithm.java b/src/com/android/launcher3/allapps/DefaultAppSearchAlgorithm.java
new file mode 100644
index 000000000..10740ec77
--- /dev/null
+++ b/src/com/android/launcher3/allapps/DefaultAppSearchAlgorithm.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.allapps;
+
+import android.os.Handler;
+
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.util.ComponentKey;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * The default search implementation.
+ */
+public class DefaultAppSearchAlgorithm {
+
+ private static final Pattern SPLIT_PATTERN = Pattern.compile("[\\s|\\p{javaSpaceChar}]+");
+
+ private final List<AppInfo> mApps;
+ protected final Handler mResultHandler;
+
+ public DefaultAppSearchAlgorithm(List<AppInfo> apps) {
+ mApps = apps;
+ mResultHandler = new Handler();
+ }
+
+ public void cancel(boolean interruptActiveRequests) {
+ if (interruptActiveRequests) {
+ mResultHandler.removeCallbacksAndMessages(null);
+ }
+ }
+
+ public void doSearch(final String query,
+ final AllAppsSearchBarController.Callbacks callback) {
+ final ArrayList<ComponentKey> result = getTitleMatchResult(query);
+ mResultHandler.post(new Runnable() {
+
+ @Override
+ public void run() {
+ callback.onSearchResult(query, result);
+ }
+ });
+ }
+
+ protected ArrayList<ComponentKey> getTitleMatchResult(String query) {
+ // 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 = query.toLowerCase();
+ final String[] queryWords = SPLIT_PATTERN.split(queryTextLower);
+
+ final ArrayList<ComponentKey> result = new ArrayList<>();
+ for (AppInfo info : mApps) {
+ if (matches(info, queryWords)) {
+ result.add(info.toComponentKey());
+ }
+ }
+ return result;
+ }
+
+ protected boolean matches(AppInfo info, String[] queryWords) {
+ String title = info.title.toString();
+ String[] words = SPLIT_PATTERN.split(title.toLowerCase());
+ 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 true;
+ }
+}
diff --git a/src/com/android/launcher3/allapps/DefaultAppSearchController.java b/src/com/android/launcher3/allapps/DefaultAppSearchController.java
new file mode 100644
index 000000000..83b920589
--- /dev/null
+++ b/src/com/android/launcher3/allapps/DefaultAppSearchController.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.allapps;
+
+import android.content.Context;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.TextView;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.util.Thunk;
+
+import java.util.List;
+
+
+/**
+ * The default search controller.
+ */
+final class DefaultAppSearchController extends AllAppsSearchBarController
+ implements TextWatcher, TextView.OnEditorActionListener, View.OnClickListener {
+
+ private static final boolean ALLOW_SINGLE_APP_LAUNCH = true;
+
+ 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;
+
+ private final Context mContext;
+ @Thunk final InputMethodManager mInputMethodManager;
+
+ private DefaultAppSearchAlgorithm mSearchManager;
+
+ private ViewGroup mContainerView;
+ private View mSearchView;
+ @Thunk View mSearchBarContainerView;
+ private View mSearchButtonView;
+ private View mDismissSearchButtonView;
+ @Thunk AllAppsSearchEditView mSearchBarEditView;
+ @Thunk AllAppsRecyclerView mAppsRecyclerView;
+ @Thunk Runnable mFocusRecyclerViewRunnable = new Runnable() {
+ @Override
+ public void run() {
+ mAppsRecyclerView.requestFocus();
+ }
+ };
+
+ public DefaultAppSearchController(Context context, ViewGroup containerView,
+ AllAppsRecyclerView appsRecyclerView) {
+ mContext = context;
+ mInputMethodManager = (InputMethodManager)
+ mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
+ mContainerView = containerView;
+ mAppsRecyclerView = appsRecyclerView;
+ }
+
+ @Override
+ public View getView(ViewGroup parent) {
+ LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+ mSearchView = inflater.inflate(R.layout.all_apps_search_bar, parent, false);
+ mSearchView.setOnClickListener(this);
+
+ mSearchButtonView = mSearchView.findViewById(R.id.search_button);
+ mSearchBarContainerView = mSearchView.findViewById(R.id.search_container);
+ mDismissSearchButtonView = mSearchBarContainerView.findViewById(R.id.dismiss_search_button);
+ mDismissSearchButtonView.setOnClickListener(this);
+ mSearchBarEditView = (AllAppsSearchEditView)
+ mSearchBarContainerView.findViewById(R.id.search_box_input);
+ mSearchBarEditView.addTextChangedListener(this);
+ mSearchBarEditView.setOnEditorActionListener(this);
+ mSearchBarEditView.setOnBackKeyListener(
+ new AllAppsSearchEditView.OnBackKeyListener() {
+ @Override
+ public void onBackKey() {
+ // Only hide the search field if there is no query, or if there
+ // are no filtered results
+ String query = Utilities.trim(
+ mSearchBarEditView.getEditableText().toString());
+ if (query.isEmpty() || mApps.hasNoFilteredResults()) {
+ hideSearchField(true, mFocusRecyclerViewRunnable);
+ }
+ }
+ });
+ return mSearchView;
+ }
+
+ @Override
+ public void focusSearchField() {
+ mSearchBarEditView.requestFocus();
+ showSearchField();
+ }
+
+ @Override
+ public boolean isSearchFieldFocused() {
+ return mSearchBarEditView.isFocused();
+ }
+
+ @Override
+ protected void onInitialize() {
+ mSearchManager = new DefaultAppSearchAlgorithm(mApps.getApps());
+ }
+
+ @Override
+ public void reset() {
+ hideSearchField(false, null);
+ }
+
+ @Override
+ public boolean shouldShowPredictionBar() {
+ return false;
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v == mSearchView) {
+ showSearchField();
+ } else if (v == mDismissSearchButtonView) {
+ hideSearchField(true, mFocusRecyclerViewRunnable);
+ }
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ // Do nothing
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ // Do nothing
+ }
+
+ @Override
+ public void afterTextChanged(final Editable s) {
+ String query = s.toString();
+ if (query.isEmpty()) {
+ mSearchManager.cancel(true);
+ mCb.clearSearchResult();
+ } else {
+ mSearchManager.cancel(false);
+ mSearchManager.doSearch(query, mCb);
+ }
+ }
+
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ // Skip if we disallow app-launch-on-enter
+ if (!ALLOW_SINGLE_APP_LAUNCH) {
+ return false;
+ }
+ // Skip if it's not the right action
+ if (actionId != EditorInfo.IME_ACTION_DONE) {
+ return false;
+ }
+ // Skip if there isn't exactly one item
+ if (mApps.getSize() != 1) {
+ return false;
+ }
+ // If there is exactly one icon, then quick-launch it
+ List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
+ for (int i = 0; i < items.size(); i++) {
+ AlphabeticalAppsList.AdapterItem item = items.get(i);
+ if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE) {
+ mAppsRecyclerView.getChildAt(i).performClick();
+ mInputMethodManager.hideSoftInputFromWindow(
+ mContainerView.getWindowToken(), 0);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Focuses the search field.
+ */
+ private void showSearchField() {
+ // Show the search bar and focus the search
+ final int translationX = Utilities.pxFromDp(SEARCH_TRANSLATION_X_DP,
+ mContext.getResources().getDisplayMetrics());
+ mSearchBarContainerView.setVisibility(View.VISIBLE);
+ mSearchBarContainerView.setAlpha(0f);
+ mSearchBarContainerView.setTranslationX(translationX);
+ mSearchBarContainerView.animate()
+ .alpha(1f)
+ .translationX(0)
+ .setDuration(FADE_IN_DURATION)
+ .withLayer()
+ .withEndAction(new Runnable() {
+ @Override
+ public void run() {
+ mSearchBarEditView.requestFocus();
+ mInputMethodManager.showSoftInput(mSearchBarEditView,
+ InputMethodManager.SHOW_IMPLICIT);
+ }
+ });
+ mSearchButtonView.animate()
+ .alpha(0f)
+ .translationX(-translationX)
+ .setDuration(FADE_OUT_DURATION)
+ .withLayer();
+ }
+
+ /**
+ * Unfocuses the search field.
+ */
+ @Thunk void hideSearchField(boolean animated, final Runnable postAnimationRunnable) {
+ mSearchManager.cancel(true);
+
+ final boolean resetTextField = mSearchBarEditView.getText().toString().length() > 0;
+ final int translationX = Utilities.pxFromDp(SEARCH_TRANSLATION_X_DP,
+ mContext.getResources().getDisplayMetrics());
+ if (animated) {
+ // Hide the search bar and focus the recycler view
+ mSearchBarContainerView.animate()
+ .alpha(0f)
+ .translationX(0)
+ .setDuration(FADE_IN_DURATION)
+ .withLayer()
+ .withEndAction(new Runnable() {
+ @Override
+ public void run() {
+ mSearchBarContainerView.setVisibility(View.INVISIBLE);
+ if (resetTextField) {
+ mSearchBarEditView.setText("");
+ }
+ mCb.clearSearchResult();
+ if (postAnimationRunnable != null) {
+ postAnimationRunnable.run();
+ }
+ }
+ });
+ mSearchButtonView.setTranslationX(-translationX);
+ mSearchButtonView.animate()
+ .alpha(1f)
+ .translationX(0)
+ .setDuration(FADE_OUT_DURATION)
+ .withLayer();
+ } else {
+ mSearchBarContainerView.setVisibility(View.INVISIBLE);
+ if (resetTextField) {
+ mSearchBarEditView.setText("");
+ }
+ mCb.clearSearchResult();
+ mSearchButtonView.setAlpha(1f);
+ mSearchButtonView.setTranslationX(0f);
+ if (postAnimationRunnable != null) {
+ postAnimationRunnable.run();
+ }
+ }
+ mInputMethodManager.hideSoftInputFromWindow(mContainerView.getWindowToken(), 0);
+ }
+}