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.ExtendedEditText; 29 import com.android.launcher3.R; 30 import com.android.launcher3.Utilities; 31 import com.android.launcher3.util.Thunk; 32 33 import java.util.List; 34 35 36 /** 37 * The default search controller. 38 */ 39 final class DefaultAppSearchController extends AllAppsSearchBarController 40 implements TextWatcher, TextView.OnEditorActionListener, View.OnClickListener { 41 42 private static final boolean ALLOW_SINGLE_APP_LAUNCH = true; 43 44 private static final int FADE_IN_DURATION = 175; 45 private static final int FADE_OUT_DURATION = 100; 46 private static final int SEARCH_TRANSLATION_X_DP = 18; 47 48 private final Context mContext; 49 @Thunk final InputMethodManager mInputMethodManager; 50 51 private DefaultAppSearchAlgorithm mSearchManager; 52 53 private ViewGroup mContainerView; 54 private View mSearchView; 55 @Thunk View mSearchBarContainerView; 56 private View mSearchButtonView; 57 private View mDismissSearchButtonView; 58 @Thunk 59 ExtendedEditText mSearchBarEditView; 60 @Thunk AllAppsRecyclerView mAppsRecyclerView; 61 @Thunk Runnable mFocusRecyclerViewRunnable = new Runnable() { 62 @Override 63 public void run() { 64 mAppsRecyclerView.requestFocus(); 65 } 66 }; 67 DefaultAppSearchController(Context context, ViewGroup containerView, AllAppsRecyclerView appsRecyclerView)68 public DefaultAppSearchController(Context context, ViewGroup containerView, 69 AllAppsRecyclerView appsRecyclerView) { 70 mContext = context; 71 mInputMethodManager = (InputMethodManager) 72 mContext.getSystemService(Context.INPUT_METHOD_SERVICE); 73 mContainerView = containerView; 74 mAppsRecyclerView = appsRecyclerView; 75 } 76 77 @Override getView(ViewGroup parent)78 public View getView(ViewGroup parent) { 79 LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 80 mSearchView = inflater.inflate(R.layout.all_apps_search_bar, parent, false); 81 mSearchView.setOnClickListener(this); 82 83 mSearchButtonView = mSearchView.findViewById(R.id.search_button); 84 mSearchBarContainerView = mSearchView.findViewById(R.id.search_container); 85 mDismissSearchButtonView = mSearchBarContainerView.findViewById(R.id.dismiss_search_button); 86 mDismissSearchButtonView.setOnClickListener(this); 87 mSearchBarEditView = (ExtendedEditText) 88 mSearchBarContainerView.findViewById(R.id.search_box_input); 89 mSearchBarEditView.addTextChangedListener(this); 90 mSearchBarEditView.setOnEditorActionListener(this); 91 mSearchBarEditView.setOnBackKeyListener( 92 new ExtendedEditText.OnBackKeyListener() { 93 @Override 94 public boolean onBackKey() { 95 // Only hide the search field if there is no query, or if there 96 // are no filtered results 97 String query = Utilities.trim( 98 mSearchBarEditView.getEditableText().toString()); 99 if (query.isEmpty() || mApps.hasNoFilteredResults()) { 100 hideSearchField(true, mFocusRecyclerViewRunnable); 101 return true; 102 } 103 return false; 104 } 105 }); 106 return mSearchView; 107 } 108 109 @Override focusSearchField()110 public void focusSearchField() { 111 mSearchBarEditView.requestFocus(); 112 showSearchField(); 113 } 114 115 @Override isSearchFieldFocused()116 public boolean isSearchFieldFocused() { 117 return mSearchBarEditView.isFocused(); 118 } 119 120 @Override onInitialize()121 protected void onInitialize() { 122 mSearchManager = new DefaultAppSearchAlgorithm(mApps.getApps()); 123 } 124 125 @Override reset()126 public void reset() { 127 hideSearchField(false, null); 128 } 129 130 @Override shouldShowPredictionBar()131 public boolean shouldShowPredictionBar() { 132 return false; 133 } 134 135 @Override onClick(View v)136 public void onClick(View v) { 137 if (v == mSearchView) { 138 showSearchField(); 139 } else if (v == mDismissSearchButtonView) { 140 hideSearchField(true, mFocusRecyclerViewRunnable); 141 } 142 } 143 144 @Override beforeTextChanged(CharSequence s, int start, int count, int after)145 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 146 // Do nothing 147 } 148 149 @Override onTextChanged(CharSequence s, int start, int before, int count)150 public void onTextChanged(CharSequence s, int start, int before, int count) { 151 // Do nothing 152 } 153 154 @Override afterTextChanged(final Editable s)155 public void afterTextChanged(final Editable s) { 156 String query = s.toString(); 157 if (query.isEmpty()) { 158 mSearchManager.cancel(true); 159 mCb.clearSearchResult(); 160 } else { 161 mSearchManager.cancel(false); 162 mSearchManager.doSearch(query, mCb); 163 } 164 } 165 166 @Override onEditorAction(TextView v, int actionId, KeyEvent event)167 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 168 // Skip if we disallow app-launch-on-enter 169 if (!ALLOW_SINGLE_APP_LAUNCH) { 170 return false; 171 } 172 // Skip if it's not the right action 173 if (actionId != EditorInfo.IME_ACTION_SEARCH) { 174 return false; 175 } 176 // Skip if there are more than one icon 177 if (mApps.getNumFilteredApps() > 1) { 178 return false; 179 } 180 // Otherwise, find the first icon, or fallback to the search-market-view and launch it 181 List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); 182 for (int i = 0; i < items.size(); i++) { 183 AlphabeticalAppsList.AdapterItem item = items.get(i); 184 switch (item.viewType) { 185 case AllAppsGridAdapter.ICON_VIEW_TYPE: 186 case AllAppsGridAdapter.SEARCH_MARKET_VIEW_TYPE: 187 mAppsRecyclerView.getChildAt(i).performClick(); 188 mInputMethodManager.hideSoftInputFromWindow( 189 mContainerView.getWindowToken(), 0); 190 return true; 191 } 192 } 193 return false; 194 } 195 196 /** 197 * Focuses the search field. 198 */ showSearchField()199 private void showSearchField() { 200 // Show the search bar and focus the search 201 final int translationX = Utilities.pxFromDp(SEARCH_TRANSLATION_X_DP, 202 mContext.getResources().getDisplayMetrics()); 203 mSearchBarContainerView.setVisibility(View.VISIBLE); 204 mSearchBarContainerView.setAlpha(0f); 205 mSearchBarContainerView.setTranslationX(translationX); 206 mSearchBarContainerView.animate() 207 .alpha(1f) 208 .translationX(0) 209 .setDuration(FADE_IN_DURATION) 210 .withLayer() 211 .withEndAction(new Runnable() { 212 @Override 213 public void run() { 214 mSearchBarEditView.requestFocus(); 215 mInputMethodManager.showSoftInput(mSearchBarEditView, 216 InputMethodManager.SHOW_IMPLICIT); 217 } 218 }); 219 mSearchButtonView.animate() 220 .alpha(0f) 221 .translationX(-translationX) 222 .setDuration(FADE_OUT_DURATION) 223 .withLayer(); 224 } 225 226 /** 227 * Unfocuses the search field. 228 */ hideSearchField(boolean animated, final Runnable postAnimationRunnable)229 @Thunk void hideSearchField(boolean animated, final Runnable postAnimationRunnable) { 230 mSearchManager.cancel(true); 231 232 final boolean resetTextField = mSearchBarEditView.getText().toString().length() > 0; 233 final int translationX = Utilities.pxFromDp(SEARCH_TRANSLATION_X_DP, 234 mContext.getResources().getDisplayMetrics()); 235 if (animated) { 236 // Hide the search bar and focus the recycler view 237 mSearchBarContainerView.animate() 238 .alpha(0f) 239 .translationX(0) 240 .setDuration(FADE_IN_DURATION) 241 .withLayer() 242 .withEndAction(new Runnable() { 243 @Override 244 public void run() { 245 mSearchBarContainerView.setVisibility(View.INVISIBLE); 246 if (resetTextField) { 247 mSearchBarEditView.setText(""); 248 } 249 mCb.clearSearchResult(); 250 if (postAnimationRunnable != null) { 251 postAnimationRunnable.run(); 252 } 253 } 254 }); 255 mSearchButtonView.setTranslationX(-translationX); 256 mSearchButtonView.animate() 257 .alpha(1f) 258 .translationX(0) 259 .setDuration(FADE_OUT_DURATION) 260 .withLayer(); 261 } else { 262 mSearchBarContainerView.setVisibility(View.INVISIBLE); 263 if (resetTextField) { 264 mSearchBarEditView.setText(""); 265 } 266 mCb.clearSearchResult(); 267 mSearchButtonView.setAlpha(1f); 268 mSearchButtonView.setTranslationX(0f); 269 if (postAnimationRunnable != null) { 270 postAnimationRunnable.run(); 271 } 272 } 273 mInputMethodManager.hideSoftInputFromWindow(mContainerView.getWindowToken(), 0); 274 } 275 } 276