/* * 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; 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.InsetDrawable; 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.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.FrameLayout; import android.widget.TextView; import com.android.launcher3.util.Thunk; import java.util.List; import java.util.regex.Pattern; /** * The all apps view container. */ public class AppsContainerView extends BaseContainerView implements DragSource, Insettable, TextWatcher, TextView.OnEditorActionListener, LauncherTransitionable, AlphabeticalAppsList.FilterChangedCallback, AppsGridAdapter.PredictionBarSpacerCallbacks, View.OnTouchListener, View.OnClickListener, View.OnLongClickListener { 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 float HEADER_ELEVATION_DP = 4; // How far the user has to scroll in order to reach the full elevation private static final float HEADER_SCROLL_TO_ELEVATION_DP = 16; private static final int FADE_IN_DURATION = 175; private static final int FADE_OUT_DURATION = 100; private static final int SEARCH_TRANSLATION_X_DP = 18; private static final Pattern SPLIT_PATTERN = Pattern.compile("[\\s|\\p{javaSpaceChar}]+"); @Thunk Launcher mLauncher; @Thunk AlphabeticalAppsList mApps; private LayoutInflater mLayoutInflater; private AppsGridAdapter mAdapter; private RecyclerView.LayoutManager mLayoutManager; private RecyclerView.ItemDecoration mItemDecoration; private FrameLayout mContentView; @Thunk AppsContainerRecyclerView mAppsRecyclerView; private ViewGroup mPredictionBarView; private View mHeaderView; private View mSearchBarContainerView; private View mSearchButtonView; private View mDismissSearchButtonView; private AppsContainerSearchEditTextView mSearchBarEditView; 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; // RecyclerView scroll position @Thunk int mRecyclerViewScrollY; private CheckLongPressHelper mPredictionIconCheckForLongPress; private View mPredictionIconUnderTouch; public AppsContainerView(Context context) { this(context, null); } public AppsContainerView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public AppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); LauncherAppState app = LauncherAppState.getInstance(); DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); Resources res = context.getResources(); mContainerInset = context.getResources().getDimensionPixelSize( R.dimen.apps_container_inset); mPredictionBarHeight = grid.allAppsCellHeightPx + 2 * res.getDimensionPixelSize(R.dimen.apps_prediction_icon_top_bottom_padding); mLauncher = (Launcher) context; mLayoutInflater = LayoutInflater.from(context); mNumAppsPerRow = grid.appsViewNumCols; mNumPredictedAppsPerRow = grid.appsViewNumPredictiveCols; mApps = new AlphabeticalAppsList(context, this, mNumAppsPerRow, mNumPredictedAppsPerRow); mAdapter = new AppsGridAdapter(context, mApps, mNumAppsPerRow, this, this, mLauncher, this); mAdapter.setEmptySearchText(res.getString(R.string.loading_apps_message)); mAdapter.setNumAppsPerRow(mNumAppsPerRow); mAdapter.setPredictionRowHeight(mPredictionBarHeight); mLayoutManager = mAdapter.getLayoutManager(); mItemDecoration = mAdapter.getItemDecoration(); mContentMarginStart = mAdapter.getContentMarginStart(); mApps.setAdapter(mAdapter); } /** * Sets the current set of predicted apps. */ public void setPredictedApps(List apps) { mApps.setPredictedApps(apps); } /** * Sets the current set of apps. */ public void setApps(List apps) { mApps.setApps(apps); } /** * Adds new apps to the list. */ public void addApps(List apps) { mApps.addApps(apps); } /** * Updates existing apps in the list */ public void updateApps(List apps) { mApps.updateApps(apps); } /** * Removes some apps from the list. */ public void removeApps(List apps) { mApps.removeApps(apps); } /** * Hides the header bar */ public void hideHeaderBar() { mHeaderView.setVisibility(View.GONE); 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 = (getResources().getConfiguration().getLayoutDirection() == LAYOUT_DIRECTION_RTL); 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) { mAppsRecyclerView.requestFocus(); } } }); // Fix the header view elevation if not dynamically calculating it mHeaderView = findViewById(R.id.header); mHeaderView.setOnClickListener(this); if (Utilities.isLmpOrAbove() && !DYNAMIC_HEADER_ELEVATION) { mHeaderView.setElevation(DynamicGrid.pxFromDp(HEADER_ELEVATION_DP, getContext().getResources().getDisplayMetrics())); } // Fix the prediction bar size mPredictionBarView = (ViewGroup) findViewById(R.id.prediction_bar); FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mPredictionBarView.getLayoutParams(); lp.height = mPredictionBarHeight; mSearchButtonView = mHeaderView.findViewById(R.id.search_button); mSearchBarContainerView = findViewById(R.id.app_search_container); mDismissSearchButtonView = mSearchBarContainerView.findViewById(R.id.dismiss_search_button); mDismissSearchButtonView.setOnClickListener(this); mSearchBarEditView = (AppsContainerSearchEditTextView) findViewById(R.id.app_search_box); if (mSearchBarEditView != null) { mSearchBarEditView.addTextChangedListener(this); mSearchBarEditView.setOnEditorActionListener(this); if (DISMISS_SEARCH_ON_BACK) { mSearchBarEditView.setOnBackKeyListener( new AppsContainerSearchEditTextView.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 = (AppsContainerRecyclerView) 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); mAppsRecyclerView.setOnScrollListenerProxy( new BaseContainerRecyclerView.OnScrollToListener() { @Override public void onScrolledTo(int x, int y) { mRecyclerViewScrollY = y; onRecyclerViewScrolled(); } }); if (mItemDecoration != null) { mAppsRecyclerView.addItemDecoration(mItemDecoration); } onUpdateBackgrounds(); onUpdatePaddings(); } @Override public void onBindPredictionBar() { if (!updatePredictionBarVisibility()) { return; } List predictedApps = mApps.getPredictedApps(); int childCount = mPredictionBarView.getChildCount(); for (int i = 0; i < mNumPredictedAppsPerRow; i++) { BubbleTextView icon; if (i < childCount) { // If a child at that index exists, then get that child icon = (BubbleTextView) mPredictionBarView.getChildAt(i); } else { // Otherwise, inflate a new icon icon = (BubbleTextView) mLayoutInflater.inflate( R.layout.apps_prediction_bar_icon_view, mPredictionBarView, false); icon.setFocusable(true); mPredictionBarView.addView(icon); } // Either apply the app info to the child, or hide the view if (i < predictedApps.size()) { if (icon.getVisibility() != View.VISIBLE) { icon.setVisibility(View.VISIBLE); } icon.applyFromApplicationInfo(predictedApps.get(i)); } else { icon.setVisibility(View.INVISIBLE); } } } @Override protected void onFixedBoundsUpdated() { // Update the number of items in the grid LauncherAppState app = LauncherAppState.getInstance(); DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); if (grid.updateAppsViewNumCols(getContext().getResources(), mFixedBounds.width())) { mNumAppsPerRow = grid.appsViewNumCols; mNumPredictedAppsPerRow = grid.appsViewNumPredictiveCols; 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 = (getResources().getConfiguration().getLayoutDirection() == LAYOUT_DIRECTION_RTL); boolean hasSearchBar = (mSearchBarEditView != null) && (mSearchBarEditView.getVisibility() == View.VISIBLE); 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 = LauncherAppState.getInstance().getDynamicGrid().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; boolean hasSearchBar = (mSearchBarEditView != null) && (mSearchBarEditView.getVisibility() == View.VISIBLE); // 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. mAppsRecyclerView.setBackground(new InsetDrawable( getContext().getResources().getDrawable( hasSearchBar ? R.drawable.apps_list_search_bg : R.drawable.quantum_panel_shape), inset, 0, inset, 0)); getRevealView().setBackground(new InsetDrawable( getContext().getResources().getDrawable(R.drawable.quantum_panel_shape), inset, 0, inset, 0)); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return handleTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent ev) { return handleTouchEvent(ev); } @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() { LauncherAppState app = LauncherAppState.getInstance(); DeviceProfile grid = app.getDynamicGrid().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.apps_view_no_search_results); mAdapter.setEmptySearchText(String.format(formatStr, queryText)); // Do an intersection of the words in the query and each title, and filter out all the // apps that don't match all of the words in the query. final String queryTextLower = queryText.toLowerCase(); final String[] queryWords = SPLIT_PATTERN.split(queryTextLower); mApps.setFilter(new AlphabeticalAppsList.Filter() { @Override public boolean retainApp(AppInfo info, String sectionName) { 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 items = mApps.getAdapterItems(); for (int i = 0; i < items.size(); i++) { AlphabeticalAppsList.AdapterItem item = items.get(i); if (item.viewType == AppsGridAdapter.ICON_VIEW_TYPE) { mAppsRecyclerView.getChildAt(i).performClick(); getInputMethodManager().hideSoftInputFromWindow(getWindowToken(), 0); return true; } } } return false; } @Override public void onFilterChanged() { updatePredictionBarVisibility(); } @Override public View getContent() { return null; } @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 (mSearchBarEditView != null) { if (toWorkspace) { hideSearchField(false, false); } } } /** * Updates the container when the recycler view is scrolled. */ private void onRecyclerViewScrolled() { if (DYNAMIC_HEADER_ELEVATION && Utilities.isLmpOrAbove()) { int elevation = DynamicGrid.pxFromDp(HEADER_ELEVATION_DP, getContext().getResources().getDisplayMetrics()); int scrollToElevation = DynamicGrid.pxFromDp(HEADER_SCROLL_TO_ELEVATION_DP, getContext().getResources().getDisplayMetrics()); float elevationPct = (float) Math.min(mRecyclerViewScrollY, scrollToElevation) / scrollToElevation; float newElevation = elevation * elevationPct; if (Float.compare(mHeaderView.getElevation(), newElevation) != 0) { mHeaderView.setElevation(newElevation); } } mPredictionBarView.setTranslationY(-mRecyclerViewScrollY + mAppsRecyclerView.getPaddingTop()); } @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 = app.getDynamicGrid().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. 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_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) { int[] coord = {x, y}; Rect hitRect = new Rect(); Utilities.mapCoordInSelfToDescendent(mPredictionBarView, this, coord); for (int i = 0; i < mPredictionBarView.getChildCount(); i++) { View child = mPredictionBarView.getChildAt(i); 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 = DynamicGrid.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 = DynamicGrid.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); } }