1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.launcher3.allapps; 17 18 import android.content.Context; 19 import android.text.Editable; 20 import android.text.TextWatcher; 21 import android.view.KeyEvent; 22 import android.view.LayoutInflater; 23 import android.view.View; 24 import android.view.ViewGroup; 25 import android.view.inputmethod.EditorInfo; 26 import android.view.inputmethod.InputMethodManager; 27 import android.widget.TextView; 28 import com.android.launcher3.R; 29 import com.android.launcher3.Utilities; 30 import com.android.launcher3.util.Thunk; 31 32 import java.util.List; 33 34 35 /** 36 * The default search controller. 37 */ 38 final class DefaultAppSearchController extends AllAppsSearchBarController 39 implements TextWatcher, TextView.OnEditorActionListener, View.OnClickListener { 40 41 private static final boolean ALLOW_SINGLE_APP_LAUNCH = true; 42 43 private static final int FADE_IN_DURATION = 175; 44 private static final int FADE_OUT_DURATION = 100; 45 private static final int SEARCH_TRANSLATION_X_DP = 18; 46 47 private final Context mContext; 48 @Thunk final InputMethodManager mInputMethodManager; 49 50 private DefaultAppSearchAlgorithm mSearchManager; 51 52 private ViewGroup mContainerView; 53 private View mSearchView; 54 @Thunk View mSearchBarContainerView; 55 private View mSearchButtonView; 56 private View mDismissSearchButtonView; 57 @Thunk AllAppsSearchEditView mSearchBarEditView; 58 @Thunk AllAppsRecyclerView mAppsRecyclerView; 59 @Thunk Runnable mFocusRecyclerViewRunnable = new Runnable() { 60 @Override 61 public void run() { 62 mAppsRecyclerView.requestFocus(); 63 } 64 }; 65 66 public DefaultAppSearchController(Context context, ViewGroup containerView, 67 AllAppsRecyclerView appsRecyclerView) { 68 mContext = context; 69 mInputMethodManager = (InputMethodManager) 70 mContext.getSystemService(Context.INPUT_METHOD_SERVICE); 71 mContainerView = containerView; 72 mAppsRecyclerView = appsRecyclerView; 73 } 74 75 @Override 76 public View getView(ViewGroup parent) { 77 LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 78 mSearchView = inflater.inflate(R.layout.all_apps_search_bar, parent, false); 79 mSearchView.setOnClickListener(this); 80 81 mSearchButtonView = mSearchView.findViewById(R.id.search_button); 82 mSearchBarContainerView = mSearchView.findViewById(R.id.search_container); 83 mDismissSearchButtonView = mSearchBarContainerView.findViewById(R.id.dismiss_search_button); 84 mDismissSearchButtonView.setOnClickListener(this); 85 mSearchBarEditView = (AllAppsSearchEditView) 86 mSearchBarContainerView.findViewById(R.id.search_box_input); 87 mSearchBarEditView.addTextChangedListener(this); 88 mSearchBarEditView.setOnEditorActionListener(this); 89 mSearchBarEditView.setOnBackKeyListener( 90 new AllAppsSearchEditView.OnBackKeyListener() { 91 @Override 92 public void onBackKey() { 93 // Only hide the search field if there is no query, or if there 94 // are no filtered results 95 String query = Utilities.trim( 96 mSearchBarEditView.getEditableText().toString()); 97 if (query.isEmpty() || mApps.hasNoFilteredResults()) { 98 hideSearchField(true, mFocusRecyclerViewRunnable); 99 } 100 } 101 }); 102 return mSearchView; 103 } 104 105 @Override 106 public void focusSearchField() { 107 mSearchBarEditView.requestFocus(); 108 showSearchField(); 109 } 110 111 @Override 112 public boolean isSearchFieldFocused() { 113 return mSearchBarEditView.isFocused(); 114 } 115 116 @Override 117 protected void onInitialize() { 118 mSearchManager = new DefaultAppSearchAlgorithm(mApps.getApps()); 119 } 120 121 @Override 122 public void reset() { 123 hideSearchField(false, null); 124 } 125 126 @Override 127 public boolean shouldShowPredictionBar() { 128 return false; 129 } 130 131 @Override 132 public void onClick(View v) { 133 if (v == mSearchView) { 134 showSearchField(); 135 } else if (v == mDismissSearchButtonView) { 136 hideSearchField(true, mFocusRecyclerViewRunnable); 137 } 138 } 139 140 @Override 141 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 142 // Do nothing 143 } 144 145 @Override 146 public void onTextChanged(CharSequence s, int start, int before, int count) { 147 // Do nothing 148 } 149 150 @Override 151 public void afterTextChanged(final Editable s) { 152 String query = s.toString(); 153 if (query.isEmpty()) { 154 mSearchManager.cancel(true); 155 mCb.clearSearchResult(); 156 } else { 157 mSearchManager.cancel(false); 158 mSearchManager.doSearch(query, mCb); 159 } 160 } 161 162 @Override 163 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 164 // Skip if we disallow app-launch-on-enter 165 if (!ALLOW_SINGLE_APP_LAUNCH) { 166 return false; 167 } 168 // Skip if it's not the right action 169 if (actionId != EditorInfo.IME_ACTION_DONE) { 170 return false; 171 } 172 // Skip if there isn't exactly one item 173 if (mApps.getSize() != 1) { 174 return false; 175 } 176 // If there is exactly one icon, then quick-launch it 177 List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); 178 for (int i = 0; i < items.size(); i++) { 179 AlphabeticalAppsList.AdapterItem item = items.get(i); 180 if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE) { 181 mAppsRecyclerView.getChildAt(i).performClick(); 182 mInputMethodManager.hideSoftInputFromWindow( 183 mContainerView.getWindowToken(), 0); 184 return true; 185 } 186 } 187 return false; 188 } 189 190 /** 191 * Focuses the search field. 192 */ 193 private void showSearchField() { 194 // Show the search bar and focus the search 195 final int translationX = Utilities.pxFromDp(SEARCH_TRANSLATION_X_DP, 196 mContext.getResources().getDisplayMetrics()); 197 mSearchBarContainerView.setVisibility(View.VISIBLE); 198 mSearchBarContainerView.setAlpha(0f); 199 mSearchBarContainerView.setTranslationX(translationX); 200 mSearchBarContainerView.animate() 201 .alpha(1f) 202 .translationX(0) 203 .setDuration(FADE_IN_DURATION) 204 .withLayer() 205 .withEndAction(new Runnable() { 206 @Override 207 public void run() { 208 mSearchBarEditView.requestFocus(); 209 mInputMethodManager.showSoftInput(mSearchBarEditView, 210 InputMethodManager.SHOW_IMPLICIT); 211 } 212 }); 213 mSearchButtonView.animate() 214 .alpha(0f) 215 .translationX(-translationX) 216 .setDuration(FADE_OUT_DURATION) 217 .withLayer(); 218 } 219 220 /** 221 * Unfocuses the search field. 222 */ 223 @Thunk void hideSearchField(boolean animated, final Runnable postAnimationRunnable) { 224 mSearchManager.cancel(true); 225 226 final boolean resetTextField = mSearchBarEditView.getText().toString().length() > 0; 227 final int translationX = Utilities.pxFromDp(SEARCH_TRANSLATION_X_DP, 228 mContext.getResources().getDisplayMetrics()); 229 if (animated) { 230 // Hide the search bar and focus the recycler view 231 mSearchBarContainerView.animate() 232 .alpha(0f) 233 .translationX(0) 234 .setDuration(FADE_IN_DURATION) 235 .withLayer() 236 .withEndAction(new Runnable() { 237 @Override 238 public void run() { 239 mSearchBarContainerView.setVisibility(View.INVISIBLE); 240 if (resetTextField) { 241 mSearchBarEditView.setText(""); 242 } 243 mCb.clearSearchResult(); 244 if (postAnimationRunnable != null) { 245 postAnimationRunnable.run(); 246 } 247 } 248 }); 249 mSearchButtonView.setTranslationX(-translationX); 250 mSearchButtonView.animate() 251 .alpha(1f) 252 .translationX(0) 253 .setDuration(FADE_OUT_DURATION) 254 .withLayer(); 255 } else { 256 mSearchBarContainerView.setVisibility(View.INVISIBLE); 257 if (resetTextField) { 258 mSearchBarEditView.setText(""); 259 } 260 mCb.clearSearchResult(); 261 mSearchButtonView.setAlpha(1f); 262 mSearchButtonView.setTranslationX(0f); 263 if (postAnimationRunnable != null) { 264 postAnimationRunnable.run(); 265 } 266 } 267 mInputMethodManager.hideSoftInputFromWindow(mContainerView.getWindowToken(), 0); 268 } 269 } 270