summaryrefslogtreecommitdiffstats
path: root/src/com/android/launcher3/allapps
diff options
context:
space:
mode:
authorWinson Chung <winsonc@google.com>2015-05-22 11:12:27 -0700
committerWinson Chung <winsonc@google.com>2015-05-22 12:21:40 -0700
commit5f4e0fdd2e4edeb9211e2dcd1c99497f175731f8 (patch)
tree3abefdc96cf11c695db912016598157f94a6cca4 /src/com/android/launcher3/allapps
parentc6205603efe1f2987caf96504c87d720a25b5a94 (diff)
downloadandroid_packages_apps_Trebuchet-5f4e0fdd2e4edeb9211e2dcd1c99497f175731f8.tar.gz
android_packages_apps_Trebuchet-5f4e0fdd2e4edeb9211e2dcd1c99497f175731f8.tar.bz2
android_packages_apps_Trebuchet-5f4e0fdd2e4edeb9211e2dcd1c99497f175731f8.zip
Moving all apps code into sub package.
- Renaming resources, dimens, etc to be more consistent - Removing old AppsCustomize resources and other unused code Change-Id: I15ce35e7cb7a9b9344fc7103963e4e4c9e45d89a
Diffstat (limited to 'src/com/android/launcher3/allapps')
-rw-r--r--src/com/android/launcher3/allapps/AllAppsContainerView.java1018
-rw-r--r--src/com/android/launcher3/allapps/AllAppsGridAdapter.java471
-rw-r--r--src/com/android/launcher3/allapps/AllAppsRecyclerView.java528
-rw-r--r--src/com/android/launcher3/allapps/AllAppsRecyclerViewContainerView.java69
-rw-r--r--src/com/android/launcher3/allapps/AllAppsSearchEditView.java65
-rw-r--r--src/com/android/launcher3/allapps/AlphabeticalAppsList.java577
6 files changed, 2728 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..60f9ab347
--- /dev/null
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -0,0 +1,1018 @@
+/*
+ * 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.ComponentName;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.InsetDrawable;
+import android.os.Build;
+import android.support.v7.widget.RecyclerView;
+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.ViewTreeObserver;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+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.Insettable;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherTransitionable;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.Workspace;
+import com.android.launcher3.util.Thunk;
+
+import java.util.List;
+import java.util.regex.Pattern;
+
+
+/**
+ * Interface for controlling the header elevation in response to RecyclerView scroll.
+ */
+interface HeaderElevationController {
+ void onScroll(int scrollY);
+ void updateBackgroundPadding(Drawable bg);
+ void disable();
+}
+
+/**
+ * Implementation of the header elevation mechanism for pre-L devices. It simulates elevation
+ * by drawing a gradient under the header bar.
+ */
+final class HeaderElevationControllerV16 implements HeaderElevationController {
+
+ private final View mShadow;
+ private final float mScrollToElevation;
+ private final Rect mTmpRect = new Rect();
+
+ public HeaderElevationControllerV16(View header) {
+ Resources res = header.getContext().getResources();
+ mScrollToElevation = res.getDimension(R.dimen.all_apps_header_scroll_to_elevation);
+
+ mShadow = new View(header.getContext());
+ mShadow.setBackground(new GradientDrawable(
+ GradientDrawable.Orientation.TOP_BOTTOM, new int[] {0x44000000, 0x00000000}));
+ mShadow.setAlpha(0);
+
+ FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ res.getDimensionPixelSize(R.dimen.all_apps_header_shadow_height));
+ lp.topMargin = ((FrameLayout.LayoutParams) header.getLayoutParams()).height;
+
+ ((ViewGroup) header.getParent()).addView(mShadow, lp);
+ }
+
+ @Override
+ public void onScroll(int scrollY) {
+ float elevationPct = (float) Math.min(scrollY, mScrollToElevation) /
+ mScrollToElevation;
+ mShadow.setAlpha(elevationPct);
+ }
+
+ @Override
+ public void updateBackgroundPadding(Drawable bg) {
+ bg.getPadding(mTmpRect);
+ FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mShadow.getLayoutParams();
+ lp.leftMargin = mTmpRect.left;
+ lp.rightMargin = mTmpRect.right;
+ mShadow.requestLayout();
+ }
+
+ @Override
+ public void disable() {
+ ViewGroup parent = (ViewGroup) mShadow.getParent();
+ if (parent != null) {
+ parent.removeView(mShadow);
+ }
+ }
+}
+
+/**
+ * Implementation of the header elevation mechanism for L+ devices, which makes use of the native
+ * view elevation.
+ */
+@TargetApi(Build.VERSION_CODES.LOLLIPOP)
+final class HeaderElevationControllerVL implements HeaderElevationController {
+
+ private final View mHeader;
+ private final float mMaxElevation;
+ private final float mScrollToElevation;
+
+ public HeaderElevationControllerVL(View header) {
+ mHeader = header;
+
+ Resources res = header.getContext().getResources();
+ mMaxElevation = res.getDimension(R.dimen.all_apps_header_max_elevation);
+ mScrollToElevation = res.getDimension(R.dimen.all_apps_header_scroll_to_elevation);
+ }
+
+ @Override
+ public void onScroll(int scrollY) {
+ float elevationPct = (float) Math.min(scrollY, mScrollToElevation) /
+ mScrollToElevation;
+ float newElevation = mMaxElevation * elevationPct;
+ if (Float.compare(mHeader.getElevation(), newElevation) != 0) {
+ mHeader.setElevation(newElevation);
+ }
+ }
+
+ @Override
+ public void updateBackgroundPadding(Drawable bg) {
+ // Do nothing, the background padding on the header view is already applied
+ }
+
+ @Override
+ public void disable() { }
+}
+
+/**
+ * The all apps view container.
+ */
+public class AllAppsContainerView extends BaseContainerView implements DragSource, Insettable,
+ TextWatcher, TextView.OnEditorActionListener, LauncherTransitionable,
+ AlphabeticalAppsList.AdapterChangedCallback, AllAppsGridAdapter.PredictionBarSpacerCallbacks,
+ View.OnTouchListener, View.OnClickListener, View.OnLongClickListener,
+ ViewTreeObserver.OnPreDrawListener {
+
+ public static final boolean GRID_MERGE_SECTIONS = true;
+
+ private static final boolean ALLOW_SINGLE_APP_LAUNCH = true;
+ private static final boolean DYNAMIC_HEADER_ELEVATION = true;
+ private static final boolean DISMISS_SEARCH_ON_BACK = 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 static final Pattern SPLIT_PATTERN = Pattern.compile("[\\s|\\p{javaSpaceChar}]+");
+
+ @Thunk Launcher mLauncher;
+ @Thunk AlphabeticalAppsList mApps;
+ private LayoutInflater mLayoutInflater;
+ private AllAppsGridAdapter mAdapter;
+ private RecyclerView.LayoutManager mLayoutManager;
+ private RecyclerView.ItemDecoration mItemDecoration;
+
+ private FrameLayout mContentView;
+ @Thunk AllAppsRecyclerView mAppsRecyclerView;
+ private ViewGroup mPredictionBarView;
+ private View mHeaderView;
+ private View mSearchBarContainerView;
+ private View mSearchButtonView;
+ private View mDismissSearchButtonView;
+ private AllAppsSearchEditView mSearchBarEditView;
+
+ private HeaderElevationController mElevationController;
+
+ private int mNumAppsPerRow;
+ private int mNumPredictedAppsPerRow;
+ // 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();
+ // This coordinate is used to proxy click and long-click events to the prediction bar icons
+ private final Point mPredictionIconTouchDownPos = new Point();
+ private int mContentMarginStart;
+ // Normal container insets
+ private int mContainerInset;
+ private int mPredictionBarHeight;
+ private int mLastRecyclerViewScrollPos = -1;
+ private boolean mFocusPredictionBarOnFirstBind;
+
+ private CheckLongPressHelper mPredictionIconCheckForLongPress;
+ private View mPredictionIconUnderTouch;
+
+ 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);
+ LauncherAppState app = LauncherAppState.getInstance();
+ Resources res = context.getResources();
+
+ mLauncher = (Launcher) context;
+ DeviceProfile grid = mLauncher.getDeviceProfile();
+
+ mContainerInset = context.getResources().getDimensionPixelSize(
+ R.dimen.all_apps_container_inset);
+ mPredictionBarHeight = grid.allAppsIconSizePx + grid.iconDrawablePaddingOriginalPx +
+ grid.allAppsIconTextSizePx +
+ 2 * res.getDimensionPixelSize(R.dimen.all_apps_prediction_icon_top_bottom_padding);
+
+ mLayoutInflater = LayoutInflater.from(context);
+
+ mNumAppsPerRow = grid.allAppsNumCols;
+ mNumPredictedAppsPerRow = grid.allAppsNumPredictiveCols;
+ mApps = new AlphabeticalAppsList(context, mNumAppsPerRow, mNumPredictedAppsPerRow);
+ mApps.setAdapterChangedCallback(this);
+ mAdapter = new AllAppsGridAdapter(context, mApps, mNumAppsPerRow, this, this, mLauncher, this);
+ mAdapter.setEmptySearchText(res.getString(R.string.all_apps_loading_message));
+ mAdapter.setNumAppsPerRow(mNumAppsPerRow);
+ mAdapter.setPredictionRowHeight(mPredictionBarHeight);
+ mLayoutManager = mAdapter.getLayoutManager();
+ mItemDecoration = mAdapter.getItemDecoration();
+ mContentMarginStart = mAdapter.getContentMarginStart();
+
+ mApps.setAdapter(mAdapter);
+ }
+
+ /**
+ * Sets the current set of predicted apps.
+ */
+ public void setPredictedApps(List<ComponentName> 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);
+ }
+
+ /**
+ * Hides the header bar
+ */
+ public void hideHeaderBar() {
+ mHeaderView.setVisibility(View.GONE);
+ mElevationController.disable();
+ onUpdateBackgrounds();
+ onUpdatePaddings();
+ }
+
+ /**
+ * 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 mContentView;
+ }
+
+ /**
+ * Returns the reveal view used for the launcher transitions.
+ */
+ public View getRevealView() {
+ return findViewById(R.id.apps_view_transition_overlay);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ boolean isRtl = Utilities.isRtl(getResources());
+ mAdapter.setRtl(isRtl);
+
+ // 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 = (FrameLayout) findViewById(R.id.apps_list);
+ mContentView.setOnFocusChangeListener(new View.OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (v == mContentView && hasFocus) {
+ if (!mApps.getPredictedApps().isEmpty()) {
+ // If the prediction bar is going to be bound, then defer focusing until
+ // it is first bound
+ if (mPredictionBarView.getChildCount() == 0) {
+ mFocusPredictionBarOnFirstBind = true;
+ } else {
+ mPredictionBarView.requestFocus();
+ }
+ } else {
+ mAppsRecyclerView.requestFocus();
+ }
+ }
+ }
+ });
+
+ // Fix the header view elevation if not dynamically calculating it
+ mHeaderView = findViewById(R.id.header);
+ mHeaderView.setOnClickListener(this);
+
+ mElevationController = Utilities.isLmpOrAbove() ?
+ new HeaderElevationControllerVL(mHeaderView) :
+ new HeaderElevationControllerV16(mHeaderView);
+ if (!DYNAMIC_HEADER_ELEVATION) {
+ mElevationController.onScroll(getResources()
+ .getDimensionPixelSize(R.dimen.all_apps_header_scroll_to_elevation));
+ }
+
+ // 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);
+ mDismissSearchButtonView.setOnClickListener(this);
+ mSearchBarEditView = (AllAppsSearchEditView) findViewById(R.id.apps_search_box);
+ if (mSearchBarEditView != null) {
+ mSearchBarEditView.addTextChangedListener(this);
+ mSearchBarEditView.setOnEditorActionListener(this);
+ if (DISMISS_SEARCH_ON_BACK) {
+ 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, true);
+ }
+ }
+ });
+ }
+ }
+ mAppsRecyclerView = (AllAppsRecyclerView) findViewById(R.id.apps_list_view);
+ mAppsRecyclerView.setApps(mApps);
+ mAppsRecyclerView.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow);
+ mAppsRecyclerView.setPredictionBarHeight(mPredictionBarHeight);
+ mAppsRecyclerView.setLayoutManager(mLayoutManager);
+ mAppsRecyclerView.setAdapter(mAdapter);
+ mAppsRecyclerView.setHasFixedSize(true);
+ if (mItemDecoration != null) {
+ mAppsRecyclerView.addItemDecoration(mItemDecoration);
+ }
+ onUpdateBackgrounds();
+ onUpdatePaddings();
+ }
+
+ @Override
+ public void onBindPredictionBar() {
+ updatePredictionBarVisibility();
+
+ 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.all_apps_prediction_bar_icon, mPredictionBarView, false);
+ 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);
+ }
+ }
+
+ if (mFocusPredictionBarOnFirstBind) {
+ mFocusPredictionBarOnFirstBind = false;
+ mPredictionBarView.requestFocus();
+ }
+ }
+
+ @Override
+ protected void onFixedBoundsUpdated() {
+ // Update the number of items in the grid
+ LauncherAppState app = LauncherAppState.getInstance();
+ DeviceProfile grid = mLauncher.getDeviceProfile();
+ if (grid.updateAppsViewNumCols(getContext().getResources(), mFixedBounds.width())) {
+ mNumAppsPerRow = grid.allAppsNumCols;
+ mNumPredictedAppsPerRow = grid.allAppsNumPredictiveCols;
+ mAppsRecyclerView.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow);
+ mAdapter.setNumAppsPerRow(mNumAppsPerRow);
+ mApps.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow);
+ }
+ }
+
+ /**
+ * Update the padding of the Apps view and children. To ensure that the RecyclerView has the
+ * full width to handle touches right to the edge of the screen, we only apply the top and
+ * bottom padding to the AppsContainerView and then the left/right padding on the RecyclerView
+ * itself. In particular, the left/right padding is applied to the background of the view,
+ * and then additionally inset by the start margin.
+ */
+ @Override
+ protected void onUpdatePaddings() {
+ boolean isRtl = Utilities.isRtl(getResources());
+ boolean hasSearchBar = (mSearchBarEditView != null) &&
+ (mSearchBarEditView.getVisibility() == View.VISIBLE);
+
+ // Set the background on the container, but let the recyclerView extend the full screen,
+ // so that the fast-scroller works on the edge as well.
+ mContentView.setPadding(0, 0, 0, 0);
+
+ if (mFixedBounds.isEmpty()) {
+ // If there are no fixed bounds, then use the default padding and insets
+ setPadding(mInsets.left, mContainerInset + mInsets.top, mInsets.right,
+ mContainerInset + mInsets.bottom);
+ } else {
+ // If there are fixed bounds, then we update the padding to reflect the fixed bounds.
+ setPadding(mFixedBounds.left, mFixedBounds.top, getMeasuredWidth() - mFixedBounds.right,
+ mFixedBounds.bottom);
+ }
+
+ // Update the apps recycler view, inset it by the container inset as well
+ DeviceProfile grid = mLauncher.getDeviceProfile();
+ int startMargin = grid.isPhone ? mContentMarginStart : 0;
+ int inset = mFixedBounds.isEmpty() ? mContainerInset : mFixedBoundsContainerInset;
+ if (isRtl) {
+ mAppsRecyclerView.setPadding(inset + mAppsRecyclerView.getScrollbarWidth(), inset,
+ inset + startMargin, inset);
+ } else {
+ mAppsRecyclerView.setPadding(inset + startMargin, inset,
+ inset + mAppsRecyclerView.getScrollbarWidth(), inset);
+ }
+
+ // Update the header bar
+ if (hasSearchBar) {
+ 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();
+ }
+
+ /**
+ * Update the background of the Apps view and children.
+ */
+ @Override
+ protected void onUpdateBackgrounds() {
+ int inset = mFixedBounds.isEmpty() ? mContainerInset : mFixedBoundsContainerInset;
+
+ // Update the background of the reveal view and list to be inset with the fixed bound
+ // insets instead of the default insets
+ // TODO: Use quantum_panel instead of quantum_panel_shape.
+ InsetDrawable background = new InsetDrawable(
+ getContext().getResources().getDrawable(R.drawable.quantum_panel_shape),
+ inset, 0, inset, 0);
+ mContentView.setBackground(background);
+ mAppsRecyclerView.updateBackgroundPadding(background);
+ mAdapter.updateBackgroundPadding(background);
+ mElevationController.updateBackgroundPadding(background);
+ getRevealView().setBackground(background.getConstantState().newDrawable());
+ }
+
+ @Override
+ public boolean onPreDraw() {
+ synchronizeToRecyclerViewScrollPosition(mAppsRecyclerView.getScrollPosition());
+ return true;
+ }
+
+ @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 void onClick(View v) {
+ if (v == mHeaderView) {
+ showSearchField();
+ } else if (v == mDismissSearchButtonView) {
+ hideSearchField(true, true);
+ }
+ }
+
+ @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 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 queryText = s.toString();
+ if (queryText.isEmpty()) {
+ mApps.setFilter(null);
+ } else {
+ String formatStr = getResources().getString(R.string.all_apps_no_search_results);
+ mAdapter.setEmptySearchText(String.format(formatStr, queryText));
+
+ // Do an intersection of the words in the query and each title, and filter out all the
+ // apps that don't match all of the words in the query.
+ final String queryTextLower = queryText.toLowerCase();
+ final String[] queryWords = SPLIT_PATTERN.split(queryTextLower);
+ mApps.setFilter(new AlphabeticalAppsList.Filter() {
+ @Override
+ public boolean retainApp(AppInfo info, String sectionName) {
+ if (sectionName.toLowerCase().contains(queryTextLower)) {
+ return true;
+ }
+ 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;
+ }
+ });
+ }
+ scrollToTop();
+ }
+
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (ALLOW_SINGLE_APP_LAUNCH && actionId == EditorInfo.IME_ACTION_DONE) {
+ // Skip the quick-launch if there isn't exactly one item
+ if (mApps.getSize() != 1) {
+ return false;
+ }
+
+ 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();
+ getInputMethodManager().hideSoftInputFromWindow(getWindowToken(), 0);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void onAdapterItemsChanged() {
+ updatePredictionBarVisibility();
+ }
+
+ @Override
+ public View getContent() {
+ return null;
+ }
+
+ @Override
+ public void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace) {
+ // Register for a pre-draw listener to synchronize the recycler view scroll to other views
+ // in this container
+ if (!toWorkspace) {
+ getViewTreeObserver().addOnPreDrawListener(this);
+ }
+ }
+
+ @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 (mSearchBarEditView != null) {
+ if (toWorkspace) {
+ hideSearchField(false, false);
+ }
+ }
+ if (toWorkspace) {
+ getViewTreeObserver().removeOnPreDrawListener(this);
+ mLastRecyclerViewScrollPos = -1;
+ }
+ }
+
+ /**
+ * Updates the container when the recycler view is scrolled.
+ */
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ private void synchronizeToRecyclerViewScrollPosition(int scrollY) {
+ if (mLastRecyclerViewScrollPos != scrollY) {
+ mLastRecyclerViewScrollPos = scrollY;
+ if (DYNAMIC_HEADER_ELEVATION) {
+ mElevationController.onScroll(scrollY);
+ }
+
+ // Scroll the prediction bar with the contents of the recycler view
+ mPredictionBarView.setTranslationY(-scrollY + mAppsRecyclerView.getPaddingTop());
+ }
+ }
+
+ @Override
+ public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+ // If we were waiting for long-click, cancel the request once a child has started handling
+ // the scrolling
+ if (mPredictionIconCheckForLongPress != null) {
+ mPredictionIconCheckForLongPress.cancelLongPress();
+ }
+ super.requestDisallowInterceptTouchEvent(disallowIntercept);
+ }
+
+ /**
+ * Handles the touch events to dismiss all apps when clicking outside the bounds of the
+ * recycler view.
+ */
+ private boolean handleTouchEvent(MotionEvent ev) {
+ LauncherAppState app = LauncherAppState.getInstance();
+ DeviceProfile grid = mLauncher.getDeviceProfile();
+ int x = (int) ev.getX();
+ int y = (int) ev.getY();
+
+ switch (ev.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ // We workaround the fact that the recycler view needs the touches for the scroll
+ // and we want to intercept it for clicks in the prediction bar by handling clicks
+ // and long clicks in the prediction bar ourselves.
+ if (mPredictionBarView != null && mPredictionBarView.getVisibility() == View.VISIBLE) {
+ mPredictionIconTouchDownPos.set(x, y);
+ mPredictionIconUnderTouch = findPredictedAppAtCoordinate(x, y);
+ if (mPredictionIconUnderTouch != null) {
+ mPredictionIconCheckForLongPress =
+ new CheckLongPressHelper(mPredictionIconUnderTouch, this);
+ mPredictionIconCheckForLongPress.postCheckForLongPress();
+ }
+ }
+
+ if (!mFixedBounds.isEmpty()) {
+ // Outset the fixed bounds and check if the touch is outside all apps
+ Rect tmpRect = new Rect(mFixedBounds);
+ 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_MOVE:
+ if (mPredictionIconUnderTouch != null) {
+ float dist = (float) Math.hypot(x - mPredictionIconTouchDownPos.x,
+ y - mPredictionIconTouchDownPos.y);
+ if (dist > ViewConfiguration.get(getContext()).getScaledTouchSlop()) {
+ if (mPredictionIconCheckForLongPress != null) {
+ mPredictionIconCheckForLongPress.cancelLongPress();
+ }
+ mPredictionIconCheckForLongPress = null;
+ mPredictionIconUnderTouch = null;
+ }
+ }
+ 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;
+ }
+ }
+
+ // Trigger the click on the prediction bar icon if that's where we touched
+ if (mPredictionIconUnderTouch != null &&
+ !mPredictionIconCheckForLongPress.hasPerformedLongPress()) {
+ mLauncher.onClick(mPredictionIconUnderTouch);
+ }
+
+ // Fall through
+ case MotionEvent.ACTION_CANCEL:
+ mBoundsCheckLastTouchDownPos.set(-1, -1);
+ mPredictionIconTouchDownPos.set(-1, -1);
+
+ // On touch up/cancel, cancel the long press on the prediction bar icon if it has
+ // not yet been performed
+ if (mPredictionIconCheckForLongPress != null) {
+ mPredictionIconCheckForLongPress.cancelLongPress();
+ mPredictionIconCheckForLongPress = null;
+ }
+ mPredictionIconUnderTouch = null;
+
+ break;
+ }
+ return false;
+ }
+
+ /**
+ * Returns the predicted app in the prediction bar given a set of local coordinates.
+ */
+ private View findPredictedAppAtCoordinate(int x, int y) {
+ Rect hitRect = new Rect();
+
+ // Ensure we aren't hitting the search bar
+ int[] coord = {x, y};
+ Utilities.mapCoordInSelfToDescendent(mHeaderView, this, coord);
+ mHeaderView.getHitRect(hitRect);
+ if (hitRect.contains(coord[0], coord[1])) {
+ return null;
+ }
+
+ // Check against the children of the prediction bar
+ coord[0] = x;
+ coord[1] = y;
+ Utilities.mapCoordInSelfToDescendent(mPredictionBarView, this, coord);
+ for (int i = 0; i < mPredictionBarView.getChildCount(); i++) {
+ View child = mPredictionBarView.getChildAt(i);
+ if (child.getVisibility() != View.VISIBLE) {
+ continue;
+ }
+ child.getHitRect(hitRect);
+ if (hitRect.contains(coord[0], coord[1])) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Shows the search field.
+ */
+ private void showSearchField() {
+ // Show the search bar and focus the search
+ final int translationX = Utilities.pxFromDp(SEARCH_TRANSLATION_X_DP,
+ getContext().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();
+ getInputMethodManager().showSoftInput(mSearchBarEditView,
+ InputMethodManager.SHOW_IMPLICIT);
+ }
+ });
+ mSearchButtonView.animate()
+ .alpha(0f)
+ .translationX(-translationX)
+ .setDuration(FADE_OUT_DURATION)
+ .withLayer();
+ }
+
+ /**
+ * Hides the search field.
+ */
+ private void hideSearchField(boolean animated, final boolean returnFocusToRecyclerView) {
+ final boolean resetTextField = mSearchBarEditView.getText().toString().length() > 0;
+ final int translationX = Utilities.pxFromDp(SEARCH_TRANSLATION_X_DP,
+ getContext().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("");
+ }
+ mApps.setFilter(null);
+ if (returnFocusToRecyclerView) {
+ mAppsRecyclerView.requestFocus();
+ }
+ }
+ });
+ mSearchButtonView.setTranslationX(-translationX);
+ mSearchButtonView.animate()
+ .alpha(1f)
+ .translationX(0)
+ .setDuration(FADE_OUT_DURATION)
+ .withLayer();
+ } else {
+ mSearchBarContainerView.setVisibility(View.INVISIBLE);
+ if (resetTextField) {
+ mSearchBarEditView.setText("");
+ }
+ mApps.setFilter(null);
+ mSearchButtonView.setAlpha(1f);
+ mSearchButtonView.setTranslationX(0f);
+ if (returnFocusToRecyclerView) {
+ mAppsRecyclerView.requestFocus();
+ }
+ }
+ 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.
+ */
+ private InputMethodManager getInputMethodManager() {
+ return (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+ }
+}
diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
new file mode 100644
index 000000000..e010270ce
--- /dev/null
+++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
@@ -0,0 +1,471 @@
+/*
+ * 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.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
+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;
+ // The message shown when there are no filtered results
+ public static final int EMPTY_SEARCH_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 ViewHolder(View v) {
+ super(v);
+ mContent = v;
+ }
+ }
+
+ /**
+ * 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;
+ }
+
+ if (mApps.getAdapterItems().get(position).viewType != AllAppsGridAdapter.ICON_VIEW_TYPE) {
+ // Both the section breaks and predictive bar span the full width
+ return mAppsPerRow;
+ } else {
+ return 1;
+ }
+ }
+ }
+
+ /**
+ * Helper class to draw the section headers
+ */
+ public class GridItemDecoration extends RecyclerView.ItemDecoration {
+
+ private static final boolean FADE_OUT_SECTIONS = false;
+
+ private HashMap<String, PointF> mCachedSectionBounds = new HashMap<>();
+ private Rect mTmpBounds = new Rect();
+ private Launcher mLauncher;
+
+ public GridItemDecoration(Context context) {
+ mLauncher = (Launcher) context;
+ }
+
+ @Override
+ public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
+ if (mApps.hasFilter()) {
+ return;
+ }
+
+ DeviceProfile grid = mLauncher.getDeviceProfile();
+ List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
+ boolean hasDrawnPredictedAppsDivider = false;
+ 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();
+ c.drawLine(mBackgroundPadding.left, top,
+ parent.getWidth() - mBackgroundPadding.right, top,
+ mPredictedAppsDividerPaint);
+ hasDrawnPredictedAppsDivider = true;
+
+ } else if (grid.isPhone && 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() - mPaddingStart - mStartMargin :
+ mPaddingStart;
+ x += (int) ((mStartMargin - 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_BAR_SPACER_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 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 final Rect mBackgroundPadding = new Rect();
+ @Thunk int mPredictionBarHeight;
+ @Thunk int mAppsPerRow;
+ @Thunk boolean mIsRtl;
+ private String mEmptySearchText;
+
+ // Section drawing
+ @Thunk int mPaddingStart;
+ @Thunk int mStartMargin;
+ @Thunk int mSectionHeaderOffset;
+ @Thunk Paint mSectionTextPaint;
+ @Thunk Paint mPredictedAppsDividerPaint;
+
+ public AllAppsGridAdapter(Context context, AlphabeticalAppsList apps, int appsPerRow,
+ 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);
+ mGridLayoutMgr.setSpanSizeLookup(mGridSizer);
+ mItemDecoration = new GridItemDecoration(context);
+ mLayoutInflater = LayoutInflater.from(context);
+ mTouchListener = touchListener;
+ mIconClickListener = iconClickListener;
+ mIconLongClickListener = iconLongClickListener;
+ mStartMargin = res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin);
+ mSectionHeaderOffset = res.getDimensionPixelSize(R.dimen.all_apps_grid_section_y_offset);
+ mPaddingStart = res.getDimensionPixelSize(R.dimen.all_apps_container_inset);
+
+ 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);
+ }
+
+ /**
+ * Sets the number of apps per row.
+ */
+ public void setNumAppsPerRow(int appsPerRow) {
+ mAppsPerRow = appsPerRow;
+ mGridLayoutMgr.setSpanCount(appsPerRow);
+ }
+
+ /**
+ * Sets the prediction row height.
+ */
+ public void setPredictionRowHeight(int height) {
+ mPredictionBarHeight = height;
+ }
+
+ /**
+ * 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(Drawable background) {
+ background.getPadding(mBackgroundPadding);
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Returns the left padding for the recycler view.
+ */
+ public int getContentMarginStart() {
+ return mStartMargin;
+ }
+
+ @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 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.all_apps_icon, parent, false);
+ icon.setOnTouchListener(mTouchListener);
+ icon.setOnClickListener(mIconClickListener);
+ icon.setOnLongClickListener(mIconLongClickListener);
+ 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_BAR_SPACER_TYPE:
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (mPredictionBarCb != null) {
+ mPredictionBarCb.onBindPredictionBar();
+ }
+ }
+ });
+ 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..e95fa325a
--- /dev/null
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -0,0 +1,528 @@
+/*
+ * 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.animation.ObjectAnimator;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import com.android.launcher3.BaseRecyclerView;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+
+import java.util.List;
+
+/**
+ * A RecyclerView with custom fastscroll support. This is the main container for the all apps
+ * icons.
+ */
+public class AllAppsRecyclerView extends BaseRecyclerView {
+
+ /**
+ * The current scroll state of the recycler view. We use this in updateVerticalScrollbarBounds()
+ * and scrollToPositionAtProgress() to determine the scroll position of the recycler view so
+ * that we can calculate what the scroll bar looks like, and where to jump to from the fast
+ * scroller.
+ */
+ private static class ScrollPositionState {
+ // The index of the first visible row
+ int rowIndex;
+ // The offset of the first visible row
+ int rowTopOffset;
+ // The height of a given row (they are currently all the same height)
+ int rowHeight;
+ }
+
+ private static final float FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR = 1.5f;
+
+ private AlphabeticalAppsList mApps;
+ private int mNumAppsPerRow;
+ private int mNumPredictedAppsPerRow;
+
+ private Drawable mScrollbar;
+ private Drawable mFastScrollerBg;
+ private Rect mTmpFastScrollerInvalidateRect = new Rect();
+ private Rect mFastScrollerBounds = new Rect();
+ private Rect mVerticalScrollbarBounds = new Rect();
+ private boolean mDraggingFastScroller;
+ private String mFastScrollSectionName;
+ private Paint mFastScrollTextPaint;
+ private Rect mFastScrollTextBounds = new Rect();
+ private float mFastScrollAlpha;
+ private int mPredictionBarHeight;
+ private int mDownX;
+ private int mDownY;
+ private int mLastX;
+ private int mLastY;
+ private int mScrollbarWidth;
+ private int mScrollbarMinHeight;
+ private int mScrollbarInset;
+ private Rect mBackgroundPadding = new Rect();
+ private ScrollPositionState mScrollPosState = new ScrollPositionState();
+
+ private Launcher mLauncher;
+
+ 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);
+
+ mLauncher = (Launcher) context;
+ Resources res = context.getResources();
+ int fastScrollerSize = res.getDimensionPixelSize(R.dimen.all_apps_fast_scroll_popup_size);
+ mScrollbar = res.getDrawable(R.drawable.all_apps_scrollbar_thumb);
+ mFastScrollerBg = res.getDrawable(R.drawable.all_apps_fastscroll_bg);
+ mFastScrollerBg.setBounds(0, 0, fastScrollerSize, fastScrollerSize);
+ mFastScrollTextPaint = new Paint();
+ mFastScrollTextPaint.setColor(Color.WHITE);
+ mFastScrollTextPaint.setAntiAlias(true);
+ mFastScrollTextPaint.setTextSize(res.getDimensionPixelSize(
+ R.dimen.all_apps_fast_scroll_text_size));
+ mScrollbarWidth = res.getDimensionPixelSize(R.dimen.all_apps_fast_scroll_bar_width);
+ mScrollbarMinHeight =
+ res.getDimensionPixelSize(R.dimen.all_apps_fast_scroll_bar_min_height);
+ mScrollbarInset =
+ res.getDimensionPixelSize(R.dimen.all_apps_fast_scroll_scrubber_touch_inset);
+ setFastScrollerAlpha(getFastScrollerAlpha());
+ setOverScrollMode(View.OVER_SCROLL_NEVER);
+ }
+
+ /**
+ * Sets the list of apps in this view, used to determine the fastscroll position.
+ */
+ public void setApps(AlphabeticalAppsList apps) {
+ mApps = apps;
+ }
+
+ /**
+ * Sets the number of apps per row in this recycler view.
+ */
+ public void setNumAppsPerRow(int numAppsPerRow, int numPredictedAppsPerRow) {
+ mNumAppsPerRow = numAppsPerRow;
+ mNumPredictedAppsPerRow = numPredictedAppsPerRow;
+
+ DeviceProfile grid = mLauncher.getDeviceProfile();
+ RecyclerView.RecycledViewPool pool = getRecycledViewPool();
+ int approxRows = (int) Math.ceil(grid.availableHeightPx / grid.allAppsIconSizePx);
+ pool.setMaxRecycledViews(AllAppsGridAdapter.PREDICTION_BAR_SPACER_TYPE, 1);
+ pool.setMaxRecycledViews(AllAppsGridAdapter.EMPTY_SEARCH_VIEW_TYPE, 1);
+ pool.setMaxRecycledViews(AllAppsGridAdapter.ICON_VIEW_TYPE, approxRows * mNumAppsPerRow);
+ pool.setMaxRecycledViews(AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE, approxRows);
+ }
+
+ public void updateBackgroundPadding(Drawable background) {
+ background.getPadding(mBackgroundPadding);
+ }
+
+ /**
+ * Sets the prediction bar height.
+ */
+ public void setPredictionBarHeight(int height) {
+ mPredictionBarHeight = height;
+ }
+
+ /**
+ * Sets the fast scroller alpha.
+ */
+ public void setFastScrollerAlpha(float alpha) {
+ mFastScrollAlpha = alpha;
+ invalidateFastScroller(mFastScrollerBounds);
+ }
+
+ /**
+ * Gets the fast scroller alpha.
+ */
+ public float getFastScrollerAlpha() {
+ return mFastScrollAlpha;
+ }
+
+ /**
+ * Returns the scroll bar width.
+ */
+ public int getScrollbarWidth() {
+ return mScrollbarWidth;
+ }
+
+ /**
+ * Scrolls this recycler view to the top.
+ */
+ public void scrollToTop() {
+ scrollToPosition(0);
+ }
+
+ /**
+ * Returns the current scroll position.
+ */
+ public int getScrollPosition() {
+ List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
+ getCurScrollState(mScrollPosState, items);
+ if (mScrollPosState.rowIndex != -1) {
+ int predictionBarHeight = mApps.getPredictedApps().isEmpty() ? 0 : mPredictionBarHeight;
+ return getPaddingTop() + (mScrollPosState.rowIndex * mScrollPosState.rowHeight) +
+ predictionBarHeight - mScrollPosState.rowTopOffset;
+ }
+ return 0;
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ addOnItemTouchListener(this);
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ super.dispatchDraw(canvas);
+ drawVerticalScrubber(canvas);
+ drawFastScrollerPopup(canvas);
+ }
+
+ /**
+ * We intercept the touch handling only to support fast scrolling when initiated from the
+ * scroll bar. Otherwise, we fall back to the default RecyclerView touch handling.
+ */
+ @Override
+ public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent ev) {
+ return handleTouchEvent(ev);
+ }
+
+ @Override
+ public void onTouchEvent(RecyclerView rv, MotionEvent ev) {
+ handleTouchEvent(ev);
+ }
+
+ /**
+ * Handles the touch event and determines whether to show the fast scroller (or updates it if
+ * it is already showing).
+ */
+ private boolean handleTouchEvent(MotionEvent ev) {
+ ViewConfiguration config = ViewConfiguration.get(getContext());
+
+ int action = ev.getAction();
+ int x = (int) ev.getX();
+ int y = (int) ev.getY();
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ // Keep track of the down positions
+ mDownX = mLastX = x;
+ mDownY = mLastY = y;
+ if (shouldStopScroll(ev)) {
+ stopScroll();
+ }
+ break;
+ case MotionEvent.ACTION_MOVE:
+ // Check if we are scrolling
+ if (!mDraggingFastScroller && isPointNearScrollbar(mDownX, mDownY) &&
+ Math.abs(y - mDownY) > config.getScaledTouchSlop()) {
+ getParent().requestDisallowInterceptTouchEvent(true);
+ mDraggingFastScroller = true;
+ animateFastScrollerVisibility(true);
+ }
+ if (mDraggingFastScroller) {
+ mLastX = x;
+ mLastY = y;
+
+ // Scroll to the right position, and update the section name
+ int top = getPaddingTop() + (mFastScrollerBg.getBounds().height() / 2);
+ int bottom = getHeight() - getPaddingBottom() -
+ (mFastScrollerBg.getBounds().height() / 2);
+ float boundedY = (float) Math.max(top, Math.min(bottom, y));
+ mFastScrollSectionName = scrollToPositionAtProgress((boundedY - top) /
+ (bottom - top));
+
+ // Combine the old and new fast scroller bounds to create the full invalidate
+ // rect
+ mTmpFastScrollerInvalidateRect.set(mFastScrollerBounds);
+ updateFastScrollerBounds();
+ mTmpFastScrollerInvalidateRect.union(mFastScrollerBounds);
+ invalidateFastScroller(mTmpFastScrollerInvalidateRect);
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ mDraggingFastScroller = false;
+ animateFastScrollerVisibility(false);
+ break;
+ }
+ return mDraggingFastScroller;
+ }
+
+ /**
+ * Animates the visibility of the fast scroller popup.
+ */
+ private void animateFastScrollerVisibility(boolean visible) {
+ ObjectAnimator anim = ObjectAnimator.ofFloat(this, "fastScrollerAlpha", visible ? 1f : 0f);
+ anim.setDuration(visible ? 200 : 150);
+ anim.start();
+ }
+
+ /**
+ * Returns whether a given point is near the scrollbar.
+ */
+ private boolean isPointNearScrollbar(int x, int y) {
+ // Check if we are scrolling
+ updateVerticalScrollbarBounds();
+ mVerticalScrollbarBounds.inset(mScrollbarInset, mScrollbarInset);
+ return mVerticalScrollbarBounds.contains(x, y);
+ }
+
+ /**
+ * Draws the fast scroller popup.
+ */
+ private void drawFastScrollerPopup(Canvas canvas) {
+ if (mFastScrollAlpha > 0f && !mFastScrollSectionName.isEmpty()) {
+ // Draw the fast scroller popup
+ int restoreCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
+ canvas.translate(mFastScrollerBounds.left, mFastScrollerBounds.top);
+ mFastScrollerBg.setAlpha((int) (mFastScrollAlpha * 255));
+ mFastScrollerBg.draw(canvas);
+ mFastScrollTextPaint.setAlpha((int) (mFastScrollAlpha * 255));
+ mFastScrollTextPaint.getTextBounds(mFastScrollSectionName, 0,
+ mFastScrollSectionName.length(), mFastScrollTextBounds);
+ float textWidth = mFastScrollTextPaint.measureText(mFastScrollSectionName);
+ canvas.drawText(mFastScrollSectionName,
+ (mFastScrollerBounds.width() - textWidth) / 2,
+ mFastScrollerBounds.height() -
+ (mFastScrollerBounds.height() - mFastScrollTextBounds.height()) / 2,
+ mFastScrollTextPaint);
+ canvas.restoreToCount(restoreCount);
+ }
+ }
+
+ /**
+ * Draws the vertical scrollbar.
+ */
+ private void drawVerticalScrubber(Canvas canvas) {
+ updateVerticalScrollbarBounds();
+
+ // Draw the scroll bar
+ int restoreCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
+ canvas.translate(mVerticalScrollbarBounds.left, mVerticalScrollbarBounds.top);
+ mScrollbar.setBounds(0, 0, mScrollbarWidth, mVerticalScrollbarBounds.height());
+ mScrollbar.draw(canvas);
+ canvas.restoreToCount(restoreCount);
+ }
+
+ /**
+ * Invalidates the fast scroller popup.
+ */
+ private void invalidateFastScroller(Rect bounds) {
+ invalidate(bounds.left, bounds.top, bounds.right, bounds.bottom);
+ }
+
+ /**
+ * Maps the touch (from 0..1) to the adapter position that should be visible.
+ */
+ private String scrollToPositionAtProgress(float touchFraction) {
+ // Ensure that we have any sections
+ List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections =
+ mApps.getFastScrollerSections();
+ if (fastScrollSections.isEmpty()) {
+ 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 (!mApps.getPredictedApps().isEmpty()) {
+ predictionBarFraction = (float) mNumPredictedAppsPerRow / mApps.getSize();
+ if (touchFraction <= predictionBarFraction) {
+ // Scroll to the top of the view, where the prediction bar is
+ layoutManager.scrollToPositionWithOffset(0, 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);
+ if (lastScrollSection.appRangeFraction <= touchFraction &&
+ touchFraction < scrollSection.appRangeFraction) {
+ break;
+ }
+ lastScrollSection = scrollSection;
+ }
+
+ // 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;
+ }
+
+ /**
+ * Updates the bounds for the scrollbar.
+ */
+ private void updateVerticalScrollbarBounds() {
+ List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
+
+ // Skip early if there are no items
+ if (items.isEmpty()) {
+ mVerticalScrollbarBounds.setEmpty();
+ return;
+ }
+
+ // Find the index and height of the first visible row (all rows have the same height)
+ int x;
+ int y;
+ int predictionBarHeight = mApps.getPredictedApps().isEmpty() ? 0 : mPredictionBarHeight;
+ int rowCount = getNumRows();
+ getCurScrollState(mScrollPosState, items);
+ if (mScrollPosState.rowIndex != -1) {
+ int height = getHeight() - getPaddingTop() - getPaddingBottom();
+ int totalScrollHeight = rowCount * mScrollPosState.rowHeight + predictionBarHeight;
+ if (totalScrollHeight > height) {
+ int scrollbarHeight = Math.max(mScrollbarMinHeight,
+ (int) (height / ((float) totalScrollHeight / height)));
+
+ // Calculate the position and size of the scroll bar
+ if (Utilities.isRtl(getResources())) {
+ x = mBackgroundPadding.left;
+ } else {
+ x = getWidth() - mBackgroundPadding.right - mScrollbarWidth;
+ }
+
+ // To calculate the offset, we compute the percentage of the total scrollable height
+ // that the user has already scrolled and then map that to the scroll bar bounds
+ int availableY = totalScrollHeight - height;
+ int availableScrollY = height - scrollbarHeight;
+ y = (mScrollPosState.rowIndex * mScrollPosState.rowHeight) + predictionBarHeight
+ - mScrollPosState.rowTopOffset;
+ y = getPaddingTop() +
+ (int) (((float) (getPaddingTop() + y) / availableY) * availableScrollY);
+
+ mVerticalScrollbarBounds.set(x, y, x + mScrollbarWidth, y + scrollbarHeight);
+ return;
+ }
+ }
+ mVerticalScrollbarBounds.setEmpty();
+ }
+
+ /**
+ * Updates the bounds for the fast scroller.
+ */
+ private void updateFastScrollerBounds() {
+ if (mFastScrollAlpha > 0f && !mFastScrollSectionName.isEmpty()) {
+ int x;
+ int y;
+
+ // Calculate the position for the fast scroller popup
+ Rect bgBounds = mFastScrollerBg.getBounds();
+ if (Utilities.isRtl(getResources())) {
+ x = mBackgroundPadding.left + getScrollBarSize();
+ } else {
+ x = getWidth() - getPaddingRight() - getScrollBarSize() - bgBounds.width();
+ }
+ y = mLastY - (int) (FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR * bgBounds.height());
+ y = Math.max(getPaddingTop(), Math.min(y, getHeight() - getPaddingBottom() -
+ bgBounds.height()));
+ mFastScrollerBounds.set(x, y, x + bgBounds.width(), y + bgBounds.height());
+ } else {
+ mFastScrollerBounds.setEmpty();
+ }
+ }
+
+ /**
+ * Returns the row index for a app index in the list.
+ */
+ private int findRowForAppIndex(int index) {
+ List<AlphabeticalAppsList.SectionInfo> sections = mApps.getSections();
+ int appIndex = 0;
+ int rowCount = 0;
+ for (AlphabeticalAppsList.SectionInfo info : sections) {
+ int numRowsInSection = (int) Math.ceil((float) info.numApps / mNumAppsPerRow);
+ if (appIndex + info.numApps > index) {
+ return rowCount + ((index - appIndex) / mNumAppsPerRow);
+ }
+ appIndex += info.numApps;
+ rowCount += numRowsInSection;
+ }
+ return appIndex;
+ }
+
+ /**
+ * Returns the total number of rows in the list.
+ */
+ private int getNumRows() {
+ List<AlphabeticalAppsList.SectionInfo> sections = mApps.getSections();
+ int rowCount = 0;
+ for (AlphabeticalAppsList.SectionInfo info : sections) {
+ int numRowsInSection = (int) Math.ceil((float) info.numApps / mNumAppsPerRow);
+ rowCount += numRowsInSection;
+ }
+ return rowCount;
+ }
+
+ /**
+ * Returns the current scroll state.
+ */
+ private void getCurScrollState(ScrollPositionState stateOut,
+ List<AlphabeticalAppsList.AdapterItem> items) {
+ stateOut.rowIndex = -1;
+ stateOut.rowTopOffset = -1;
+ stateOut.rowHeight = -1;
+
+ // Return early if there are no items
+ if (items.isEmpty()) {
+ 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) {
+ stateOut.rowIndex = findRowForAppIndex(item.appIndex);
+ stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child);
+ stateOut.rowHeight = child.getHeight();
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerViewContainerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerViewContainerView.java
new file mode 100644
index 000000000..8a8afde51
--- /dev/null
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerViewContainerView.java
@@ -0,0 +1,69 @@
+/*
+ * 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.util.AttributeSet;
+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;
+
+/**
+ * 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/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..3d1503d46
--- /dev/null
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -0,0 +1,577 @@
+package com.android.launcher3.allapps;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.compat.AlphabeticIndexCompat;
+import com.android.launcher3.model.AppNameComparator;
+
+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;
+
+ /**
+ * 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;
+ // To map the touch (from 0..1) to the index in the app list to jump to in the fast
+ // scroller, we use the fraction in range (0..1) of the app index / total app count.
+ public float appRangeFraction;
+ // The AdapterItem to scroll to for this section
+ public AdapterItem appItem;
+
+ public FastScrollSectionInfo(String sectionName, float appRangeFraction) {
+ this.sectionName = sectionName;
+ this.appRangeFraction = appRangeFraction;
+ }
+ }
+
+ /**
+ * 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;
+
+ /** 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 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 asPredictionBarSpacer(int pos) {
+ AdapterItem item = new AdapterItem();
+ item.viewType = AllAppsGridAdapter.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 = AllAppsGridAdapter.ICON_VIEW_TYPE;
+ item.position = pos;
+ item.sectionInfo = section;
+ item.sectionName = sectionName;
+ item.sectionAppIndex = sectionAppIndex;
+ item.appInfo = appInfo;
+ item.appIndex = appIndex;
+ return item;
+ }
+ }
+
+ /**
+ * A filter interface to limit the set of applications in the apps list.
+ */
+ public interface Filter {
+ boolean retainApp(AppInfo info, String sectionName);
+ }
+
+ /**
+ * Callback to notify when the set of adapter items have changed.
+ */
+ public interface AdapterChangedCallback {
+ void onAdapterItemsChanged();
+ }
+
+ /**
+ * Common interface for different merging strategies.
+ */
+ private interface MergeAlgorithm {
+ boolean continueMerging(int sectionAppCount, int numAppsPerRow, int mergeCount);
+ }
+
+ /**
+ * The logic we use to merge sections on tablets.
+ */
+ private static class TabletMergeAlgorithm implements MergeAlgorithm {
+
+ @Override
+ public boolean continueMerging(int sectionAppCount, int numAppsPerRow, int mergeCount) {
+ // Merge EVERYTHING
+ return true;
+ }
+ }
+
+ /**
+ * The logic we use to merge sections on phones.
+ */
+ private static class PhoneMergeAlgorithm implements MergeAlgorithm {
+
+ private int mMinAppsPerRow;
+ private int mMinRowsInMergedSection;
+ private int mMaxAllowableMerges;
+
+ public PhoneMergeAlgorithm(int minAppsPerRow, int minRowsInMergedSection, int maxNumMerges) {
+ mMinAppsPerRow = minAppsPerRow;
+ mMinRowsInMergedSection = minRowsInMergedSection;
+ mMaxAllowableMerges = maxNumMerges;
+ }
+
+ @Override
+ public boolean continueMerging(int sectionAppCount, int numAppsPerRow, int mergeCount) {
+ // 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;
+ return (0 < cols && cols < mMinAppsPerRow) &&
+ rows < mMinRowsInMergedSection &&
+ mergeCount < mMaxAllowableMerges;
+ }
+ }
+
+ private static final int MIN_ROWS_IN_MERGED_SECTION_PHONE = 3;
+ private static final int MAX_NUM_MERGES_PHONE = 2;
+
+ private Launcher mLauncher;
+
+ // The set of apps from the system not including predictions
+ private List<AppInfo> mApps = new ArrayList<>();
+ // 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<ComponentName> 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<>();
+ private HashMap<CharSequence, String> mCachedSectionNames = new HashMap<>();
+ private RecyclerView.Adapter mAdapter;
+ private Filter mFilter;
+ private AlphabeticIndexCompat mIndexer;
+ private AppNameComparator mAppNameComparator;
+ private MergeAlgorithm mMergeAlgorithm;
+ private AdapterChangedCallback mAdapterChangedCallback;
+ private int mNumAppsPerRow;
+ private int mNumPredictedAppsPerRow;
+
+ public AlphabeticalAppsList(Context context, int numAppsPerRow, int numPredictedAppsPerRow) {
+ mLauncher = (Launcher) context;
+ mIndexer = new AlphabeticIndexCompat(context);
+ mAppNameComparator = new AppNameComparator(context);
+ setNumAppsPerRow(numAppsPerRow, numPredictedAppsPerRow);
+ }
+
+ /**
+ * Sets the apps updated callback.
+ */
+ public void setAdapterChangedCallback(AdapterChangedCallback cb) {
+ mAdapterChangedCallback = cb;
+ }
+
+ /**
+ * Sets the number of apps per row. Used only for AppsContainerView.SECTIONED_GRID_COALESCED.
+ */
+ public void setNumAppsPerRow(int numAppsPerRow, int numPredictedAppsPerRow) {
+ // Update the merge algorithm
+ DeviceProfile grid = mLauncher.getDeviceProfile();
+ if (grid.isPhone) {
+ mMergeAlgorithm = new PhoneMergeAlgorithm((int) Math.ceil(numAppsPerRow / 2f),
+ MIN_ROWS_IN_MERGED_SECTION_PHONE, MAX_NUM_MERGES_PHONE);
+ } else {
+ mMergeAlgorithm = new TabletMergeAlgorithm();
+ }
+
+ mNumAppsPerRow = numAppsPerRow;
+ mNumPredictedAppsPerRow = numPredictedAppsPerRow;
+
+ onAppsUpdated();
+ }
+
+ /**
+ * Sets the adapter to notify when this dataset changes.
+ */
+ public void setAdapter(RecyclerView.Adapter adapter) {
+ mAdapter = adapter;
+ }
+
+ /**
+ * Returns sections of all the current filtered applications.
+ */
+ public List<SectionInfo> getSections() {
+ return mSections;
+ }
+
+ /**
+ * Returns 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 whether there are is a filter set.
+ */
+ public boolean hasFilter() {
+ return (mFilter != null);
+ }
+
+ /**
+ * Returns whether there are no filtered results.
+ */
+ public boolean hasNoFilteredResults() {
+ return (mFilter != null) && mFilteredApps.isEmpty();
+ }
+
+ /**
+ * Sets the current filter for this list of apps.
+ */
+ public void setFilter(Filter f) {
+ if (mFilter != f) {
+ mFilter = 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<ComponentName> apps) {
+ mPredictedAppComponents.clear();
+ mPredictedAppComponents.addAll(apps);
+ onAppsUpdated();
+ }
+
+ /**
+ * Returns the current set of predicted apps.
+ */
+ public List<AppInfo> getPredictedApps() {
+ return mPredictedApps;
+ }
+
+ /**
+ * Sets the current set of apps.
+ */
+ public void setApps(List<AppInfo> apps) {
+ mApps.clear();
+ mApps.addAll(apps);
+ onAppsUpdated();
+ }
+
+ /**
+ * Adds new apps to the list.
+ */
+ public void addApps(List<AppInfo> apps) {
+ // We add it in place, in alphabetical order
+ for (AppInfo info : apps) {
+ mApps.add(info);
+ }
+ onAppsUpdated();
+ }
+
+ /**
+ * Updates existing apps in the list
+ */
+ public void updateApps(List<AppInfo> apps) {
+ for (AppInfo info : apps) {
+ int index = mApps.indexOf(info);
+ if (index != -1) {
+ mApps.set(index, info);
+ } else {
+ mApps.add(info);
+ }
+ }
+ onAppsUpdated();
+ }
+
+ /**
+ * Removes some apps from the list.
+ */
+ public void removeApps(List<AppInfo> apps) {
+ for (AppInfo info : apps) {
+ int removeIndex = findAppByComponent(mApps, info);
+ if (removeIndex != -1) {
+ mApps.remove(removeIndex);
+ }
+ }
+ onAppsUpdated();
+ }
+
+ /**
+ * Finds the index of an app given a target AppInfo.
+ */
+ private int findAppByComponent(List<AppInfo> apps, AppInfo targetInfo) {
+ ComponentName targetComponent = targetInfo.intent.getComponent();
+ int length = apps.size();
+ for (int i = 0; i < length; ++i) {
+ AppInfo info = apps.get(i);
+ if (info.user.equals(targetInfo.user)
+ && info.intent.getComponent().equals(targetComponent)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Updates internals when the set of apps are updated.
+ */
+ private void onAppsUpdated() {
+ // Sort the list of apps
+ 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 = 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();
+
+ // 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++);
+ mAdapterItems.add(sectionItem);
+ }
+ }
+
+ // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the
+ // ordered set of sections
+ int numApps = mApps.size();
+ for (int i = 0; i < numApps; i++) {
+ AppInfo info = mApps.get(i);
+ String sectionName = getAndUpdateCachedSectionName(info.title);
+
+ // Check if we want to retain this app
+ if (mFilter != null && !mFilter.retainApp(info, sectionName)) {
+ continue;
+ }
+
+ // 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,
+ (float) appIndex / numApps);
+ 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.appItem = appItem;
+ }
+ mAdapterItems.add(appItem);
+ mFilteredApps.add(info);
+ }
+
+ // Merge multiple sections together as requested by the merge strategy for this device
+ mergeSections();
+
+ // Refresh the recycler view
+ if (mAdapter != null) {
+ mAdapter.notifyDataSetChanged();
+ }
+
+ if (mAdapterChangedCallback != null) {
+ mAdapterChangedCallback.onAdapterItemsChanged();
+ }
+ }
+
+ /**
+ * Merges multiple sections to reduce visual raggedness.
+ */
+ private void mergeSections() {
+ // Go through each section and try and merge some of the sections
+ if (AllAppsContainerView.GRID_MERGE_SECTIONS && !hasFilter()) {
+ int sectionAppCount = 0;
+ for (int i = 0; i < mSections.size(); i++) {
+ SectionInfo section = mSections.get(i);
+ sectionAppCount = section.numApps;
+ int mergeCount = 1;
+
+ // Merge rows based on the current strategy
+ while (mMergeAlgorithm.continueMerging(sectionAppCount, mNumAppsPerRow, mergeCount) &&
+ (i + 1) < mSections.size()) {
+ 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;
+ }
+}