diff options
author | Sunny Goyal <sunnygoyal@google.com> | 2017-05-07 11:56:00 -0700 |
---|---|---|
committer | Sunny Goyal <sunnygoyal@google.com> | 2017-05-08 14:43:32 -0700 |
commit | 161f96bc77805ed87f831b68e51fad61a23153bc (patch) | |
tree | a3f74eb9dcac6dd4673a51122ac62e5e92aea676 /src/com/android/launcher3/allapps/search | |
parent | b73fa5d7a46e840df13b2d5fb8ae956344524c3e (diff) | |
download | android_packages_apps_Trebuchet-161f96bc77805ed87f831b68e51fad61a23153bc.tar.gz android_packages_apps_Trebuchet-161f96bc77805ed87f831b68e51fad61a23153bc.tar.bz2 android_packages_apps_Trebuchet-161f96bc77805ed87f831b68e51fad61a23153bc.zip |
Moving apps search related logic into a custom layout file
This will allow derivative projects to easily change the search behavior
by simply overriding the xml file
Bug: 37616877
Change-Id: Ib8d6a2dab06819a52611e9a3d97c70c5a49bbf97
Diffstat (limited to 'src/com/android/launcher3/allapps/search')
4 files changed, 620 insertions, 0 deletions
diff --git a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java new file mode 100644 index 000000000..547d9e185 --- /dev/null +++ b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java @@ -0,0 +1,220 @@ +/* + * 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.search; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.view.KeyEvent; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.TextView; +import android.widget.TextView.OnEditorActionListener; + +import com.android.launcher3.ExtendedEditText; +import com.android.launcher3.Launcher; +import com.android.launcher3.Utilities; +import com.android.launcher3.allapps.AlphabeticalAppsList; +import com.android.launcher3.discovery.AppDiscoveryItem; +import com.android.launcher3.discovery.AppDiscoveryUpdateState; +import com.android.launcher3.util.ComponentKey; +import com.android.launcher3.util.PackageManagerHelper; + +import java.util.ArrayList; + +/** + * An interface to a search box that AllApps can command. + */ +public class AllAppsSearchBarController + implements TextWatcher, OnEditorActionListener, ExtendedEditText.OnBackKeyListener { + + protected Launcher mLauncher; + protected AlphabeticalAppsList mApps; + protected Callbacks mCb; + protected ExtendedEditText mInput; + protected String mQuery; + + protected DefaultAppSearchAlgorithm mSearchAlgorithm; + protected InputMethodManager mInputMethodManager; + + public void setVisibility(int visibility) { + mInput.setVisibility(visibility); + } + /** + * Sets the references to the apps model and the search result callback. + */ + public final void initialize( + AlphabeticalAppsList apps, ExtendedEditText input, + Launcher launcher, Callbacks cb) { + mApps = apps; + mCb = cb; + mLauncher = launcher; + + mInput = input; + mInput.addTextChangedListener(this); + mInput.setOnEditorActionListener(this); + mInput.setOnBackKeyListener(this); + + mInputMethodManager = (InputMethodManager) + mInput.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + + mSearchAlgorithm = onInitializeSearch(); + + onInitialized(); + } + + /** + * You can override this method to perform custom initialization. + */ + protected void onInitialized() { + } + + /** + * This method will get called when the controller is set. + */ + public DefaultAppSearchAlgorithm onInitializeSearch() { + return new DefaultAppSearchAlgorithm(mApps.getApps()); + } + + @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) { + mQuery = s.toString(); + if (mQuery.isEmpty()) { + mSearchAlgorithm.cancel(true); + mCb.clearSearchResult(); + } else { + mSearchAlgorithm.cancel(false); + mSearchAlgorithm.doSearch(mQuery, mCb); + } + } + + public void refreshSearchResult() { + if (TextUtils.isEmpty(mQuery)) { + return; + } + // If play store continues auto updating an app, we want to show partial result. + mSearchAlgorithm.cancel(false); + mSearchAlgorithm.doSearch(mQuery, mCb); + } + + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + // Skip if it's not the right action + if (actionId != EditorInfo.IME_ACTION_SEARCH) { + return false; + } + + // Skip if the query is empty + String query = v.getText().toString(); + if (query.isEmpty()) { + return false; + } + return mLauncher.startActivitySafely(v, + PackageManagerHelper.getMarketSearchIntent(mLauncher, query), null); + } + + @Override + public boolean onBackKey() { + // Only hide the search field if there is no query + String query = Utilities.trim(mInput.getEditableText().toString()); + if (query.isEmpty()) { + reset(); + return true; + } + return false; + } + + /** + * Resets the search bar state. + */ + public void reset() { + unfocusSearchField(); + mCb.clearSearchResult(); + mInput.setText(""); + mQuery = null; + hideKeyboard(); + } + + protected void hideKeyboard() { + mInputMethodManager.hideSoftInputFromWindow(mInput.getWindowToken(), 0); + } + + protected void unfocusSearchField() { + View nextFocus = mInput.focusSearch(View.FOCUS_DOWN); + if (nextFocus != null) { + nextFocus.requestFocus(); + } + } + + /** + * Focuses the search field to handle key events. + */ + public void focusSearchField() { + mInput.showKeyboard(); + } + + /** + * Returns whether the search field is focused. + */ + public boolean isSearchFieldFocused() { + return mInput.isFocused(); + } + + /** + * Callback for getting search results. + */ + public interface Callbacks { + + /** + * Called when the search is complete. + * + * @param apps sorted list of matching components or null if in case of failure. + */ + void onSearchResult(String query, ArrayList<ComponentKey> apps); + + /** + * Called when the search results should be cleared. + */ + void clearSearchResult(); + + /** + * Called when the app discovery is providing an update of search, which can either be + * START for starting a new discovery, + * UPDATE for providing a new search result, can be called multiple times, + * END for indicating the end of results. + * + * @param app result item if UPDATE, else null + * @param app the update state, START, UPDATE or END + */ + void onAppDiscoverySearchUpdate(@Nullable AppDiscoveryItem app, + @NonNull AppDiscoveryUpdateState state); + } + +}
\ No newline at end of file diff --git a/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java b/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java new file mode 100644 index 000000000..116ec8866 --- /dev/null +++ b/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2017 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.search; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.Selection; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.SpannableStringBuilder; +import android.text.TextUtils; +import android.text.method.TextKeyListener; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.widget.FrameLayout; + +import com.android.launcher3.ExtendedEditText; +import com.android.launcher3.Launcher; +import com.android.launcher3.R; +import com.android.launcher3.allapps.AllAppsGridAdapter; +import com.android.launcher3.allapps.AllAppsRecyclerView; +import com.android.launcher3.allapps.AlphabeticalAppsList; +import com.android.launcher3.allapps.SearchUiManager; +import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.discovery.AppDiscoveryItem; +import com.android.launcher3.discovery.AppDiscoveryUpdateState; +import com.android.launcher3.graphics.TintedDrawableSpan; +import com.android.launcher3.util.ComponentKey; + +import java.util.ArrayList; + +/** + * Layout to contain the All-apps search UI. + */ +public class AppsSearchContainerLayout extends FrameLayout + implements SearchUiManager, AllAppsSearchBarController.Callbacks { + + private final Launcher mLauncher; + private final int mMinHeight; + private final AllAppsSearchBarController mSearchBarController; + private final SpannableStringBuilder mSearchQueryBuilder; + private final HeaderElevationController mElevationController; + + private ExtendedEditText mSearchInput; + private AlphabeticalAppsList mApps; + private AllAppsRecyclerView mAppsRecyclerView; + private AllAppsGridAdapter mAdapter; + + public AppsSearchContainerLayout(Context context) { + this(context, null); + } + + public AppsSearchContainerLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public AppsSearchContainerLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + mLauncher = Launcher.getLauncher(context); + mMinHeight = getResources().getDimensionPixelSize(R.dimen.all_apps_search_bar_height); + mSearchBarController = new AllAppsSearchBarController(); + mElevationController = new HeaderElevationController(this); + + mSearchQueryBuilder = new SpannableStringBuilder(); + Selection.setSelection(mSearchQueryBuilder, 0); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mSearchInput = findViewById(R.id.search_box_input); + + // Update the hint to contain the icon. + // Prefix the original hint with two spaces. The first space gets replaced by the icon + // using span. The second space is used for a singe space character between the hint + // and the icon. + SpannableString spanned = new SpannableString(" " + mSearchInput.getHint()); + spanned.setSpan(new TintedDrawableSpan(getContext(), R.drawable.ic_allapps_search), + 0, 1, Spannable.SPAN_EXCLUSIVE_INCLUSIVE); + mSearchInput.setHint(spanned); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP && + !mLauncher.getDeviceProfile().isVerticalBarLayout()) { + getLayoutParams().height = mLauncher.getDragLayer().getInsets().top + mMinHeight; + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + + @Override + public void initialize( + AlphabeticalAppsList appsList, AllAppsRecyclerView recyclerView) { + mApps = appsList; + mAppsRecyclerView = recyclerView; + mAppsRecyclerView.addOnScrollListener(mElevationController); + mAdapter = (AllAppsGridAdapter) mAppsRecyclerView.getAdapter(); + + mSearchBarController.initialize(appsList, mSearchInput, mLauncher, this); + } + + @Override + public void refreshSearchResult() { + mSearchBarController.refreshSearchResult(); + } + + @Override + public void reset() { + mElevationController.reset(); + mSearchBarController.reset(); + } + + @Override + public void preDispatchKeyEvent(KeyEvent event) { + // Determine if the key event was actual text, if so, focus the search bar and then dispatch + // the key normally so that it can process this key event + if (!mSearchBarController.isSearchFieldFocused() && + event.getAction() == KeyEvent.ACTION_DOWN) { + final int unicodeChar = event.getUnicodeChar(); + final boolean isKeyNotWhitespace = unicodeChar > 0 && + !Character.isWhitespace(unicodeChar) && !Character.isSpaceChar(unicodeChar); + if (isKeyNotWhitespace) { + boolean gotKey = TextKeyListener.getInstance().onKeyDown(this, mSearchQueryBuilder, + event.getKeyCode(), event); + if (gotKey && mSearchQueryBuilder.length() > 0) { + mSearchBarController.focusSearchField(); + } + } + } + } + + @Override + public boolean shouldRestoreImeState() { + return !TextUtils.isEmpty(mSearchInput.getText()); + } + + @Override + public void startAppsSearch() { + if (mApps != null) { + mSearchBarController.focusSearchField(); + } + } + + @Override + public void onSearchResult(String query, ArrayList<ComponentKey> apps) { + if (apps != null) { + mApps.setOrderedFilter(apps); + notifyResultChanged(); + mAdapter.setLastSearchQuery(query); + } + } + + @Override + public void clearSearchResult() { + if (mApps.setOrderedFilter(null)) { + notifyResultChanged(); + } + + // Clear the search query + mSearchQueryBuilder.clear(); + mSearchQueryBuilder.clearSpans(); + Selection.setSelection(mSearchQueryBuilder, 0); + } + + @Override + public void onAppDiscoverySearchUpdate( + @Nullable AppDiscoveryItem app, @NonNull AppDiscoveryUpdateState state) { + if (!mLauncher.isDestroyed()) { + mApps.onAppDiscoverySearchUpdate(app, state); + notifyResultChanged(); + } + } + + private void notifyResultChanged() { + mElevationController.reset(); + mAppsRecyclerView.onSearchResultsChanged(); + } +} diff --git a/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java b/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java new file mode 100644 index 000000000..457b454ef --- /dev/null +++ b/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java @@ -0,0 +1,136 @@ +/* + * 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.search; + +import android.os.Handler; + +import com.android.launcher3.AppInfo; +import com.android.launcher3.util.ComponentKey; + +import java.util.ArrayList; +import java.util.List; + +/** + * The default search implementation. + */ +public class DefaultAppSearchAlgorithm { + + private final List<AppInfo> mApps; + protected final Handler mResultHandler; + + public DefaultAppSearchAlgorithm(List<AppInfo> apps) { + mApps = apps; + mResultHandler = new Handler(); + } + + public void cancel(boolean interruptActiveRequests) { + if (interruptActiveRequests) { + mResultHandler.removeCallbacksAndMessages(null); + } + } + + public void doSearch(final String query, + final AllAppsSearchBarController.Callbacks callback) { + final ArrayList<ComponentKey> result = getTitleMatchResult(query); + mResultHandler.post(new Runnable() { + + @Override + public void run() { + callback.onSearchResult(query, result); + } + }); + } + + protected ArrayList<ComponentKey> getTitleMatchResult(String query) { + // Do an intersection of the words in the query and each title, and filter out all the + // apps that don't match all of the words in the query. + final String queryTextLower = query.toLowerCase(); + final ArrayList<ComponentKey> result = new ArrayList<>(); + for (AppInfo info : mApps) { + if (matches(info, queryTextLower)) { + result.add(info.toComponentKey()); + } + } + return result; + } + + protected boolean matches(AppInfo info, String query) { + int queryLength = query.length(); + + String title = info.title.toString(); + int titleLength = title.length(); + + if (titleLength < queryLength || queryLength <= 0) { + return false; + } + + int lastType; + int thisType = Character.UNASSIGNED; + int nextType = Character.getType(title.codePointAt(0)); + + int end = titleLength - queryLength; + for (int i = 0; i <= end; i++) { + lastType = thisType; + thisType = nextType; + nextType = i < (titleLength - 1) ? + Character.getType(title.codePointAt(i + 1)) : Character.UNASSIGNED; + if (isBreak(thisType, lastType, nextType) && + title.substring(i, i + queryLength).equalsIgnoreCase(query)) { + return true; + } + } + return false; + } + + /** + * Returns true if the current point should be a break point. Following cases + * are considered as break points: + * 1) Any non space character after a space character + * 2) Any digit after a non-digit character + * 3) Any capital character after a digit or small character + * 4) Any capital character before a small character + */ + protected boolean isBreak(int thisType, int prevType, int nextType) { + switch (thisType) { + case Character.UPPERCASE_LETTER: + if (nextType == Character.UPPERCASE_LETTER) { + return true; + } + // Follow through + case Character.TITLECASE_LETTER: + // Break point if previous was not a upper case + return prevType != Character.UPPERCASE_LETTER; + case Character.LOWERCASE_LETTER: + // Break point if previous was not a letter. + return prevType > Character.OTHER_LETTER || prevType <= Character.UNASSIGNED; + case Character.DECIMAL_DIGIT_NUMBER: + case Character.LETTER_NUMBER: + case Character.OTHER_NUMBER: + // Break point if previous was not a number + return !(prevType == Character.DECIMAL_DIGIT_NUMBER + || prevType == Character.LETTER_NUMBER + || prevType == Character.OTHER_NUMBER); + case Character.MATH_SYMBOL: + case Character.CURRENCY_SYMBOL: + case Character.OTHER_PUNCTUATION: + case Character.DASH_PUNCTUATION: + // Always a break point for a symbol + return true; + default: + return false; + } + } +} diff --git a/src/com/android/launcher3/allapps/search/HeaderElevationController.java b/src/com/android/launcher3/allapps/search/HeaderElevationController.java new file mode 100644 index 000000000..ab4e88fc8 --- /dev/null +++ b/src/com/android/launcher3/allapps/search/HeaderElevationController.java @@ -0,0 +1,69 @@ +package com.android.launcher3.allapps.search; + +import android.content.res.Resources; +import android.graphics.Outline; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.view.ViewOutlineProvider; + +import com.android.launcher3.BaseRecyclerView; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; + +/** + * Helper class for controlling the header elevation in response to RecyclerView scroll. + */ +public class HeaderElevationController extends RecyclerView.OnScrollListener { + + private final View mHeader; + private final float mMaxElevation; + private final float mScrollToElevation; + + private int mCurrentY = 0; + + public HeaderElevationController(View header) { + mHeader = header; + final Resources res = mHeader.getContext().getResources(); + mMaxElevation = res.getDimension(R.dimen.all_apps_header_max_elevation); + mScrollToElevation = res.getDimension(R.dimen.all_apps_header_scroll_to_elevation); + + // We need to provide a custom outline so the shadow only appears on the bottom edge. + // The top, left and right edges are all extended out, and the shadow is clipped + // by the parent. + final ViewOutlineProvider vop = new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + final View parent = (View) mHeader.getParent(); + + final int left = parent.getLeft(); // Use the parent to account for offsets + final int top = view.getTop(); + final int right = left + view.getWidth(); + final int bottom = view.getBottom(); + + final int offset = Utilities.pxFromDp(mMaxElevation, res.getDisplayMetrics()); + outline.setRect(left - offset, top - offset, right + offset, bottom); + } + }; + mHeader.setOutlineProvider(vop); + } + + public void reset() { + mCurrentY = 0; + onScroll(mCurrentY); + } + + @Override + public final void onScrolled(RecyclerView recyclerView, int dx, int dy) { + mCurrentY = ((BaseRecyclerView) recyclerView).getCurrentScrollY(); + onScroll(mCurrentY); + } + + private void onScroll(int scrollY) { + float elevationPct = Math.min(scrollY, mScrollToElevation) / mScrollToElevation; + float newElevation = mMaxElevation * elevationPct; + if (Float.compare(mHeader.getElevation(), newElevation) != 0) { + mHeader.setElevation(newElevation); + } + } + +} |