1 /* 2 * Copyright (C) 2017 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.search; 17 18 import static android.view.View.MeasureSpec.EXACTLY; 19 import static android.view.View.MeasureSpec.getSize; 20 import static android.view.View.MeasureSpec.makeMeasureSpec; 21 22 import static com.android.launcher3.graphics.IconNormalizer.ICON_VISIBLE_AREA_FACTOR; 23 24 import android.content.Context; 25 import android.graphics.Rect; 26 import android.text.Selection; 27 import android.text.Spannable; 28 import android.text.SpannableString; 29 import android.text.SpannableStringBuilder; 30 import android.text.method.TextKeyListener; 31 import android.util.AttributeSet; 32 import android.view.KeyEvent; 33 import android.view.View; 34 import android.view.ViewGroup.MarginLayoutParams; 35 36 import com.android.launcher3.DeviceProfile; 37 import com.android.launcher3.ExtendedEditText; 38 import com.android.launcher3.Insettable; 39 import com.android.launcher3.Launcher; 40 import com.android.launcher3.R; 41 import com.android.launcher3.allapps.AllAppsContainerView; 42 import com.android.launcher3.allapps.AllAppsStore; 43 import com.android.launcher3.allapps.AlphabeticalAppsList; 44 import com.android.launcher3.allapps.SearchUiManager; 45 import com.android.launcher3.graphics.TintedDrawableSpan; 46 import com.android.launcher3.util.ComponentKey; 47 48 import java.util.ArrayList; 49 50 /** 51 * Layout to contain the All-apps search UI. 52 */ 53 public class AppsSearchContainerLayout extends ExtendedEditText 54 implements SearchUiManager, AllAppsSearchBarController.Callbacks, 55 AllAppsStore.OnUpdateListener, Insettable { 56 57 58 private final Launcher mLauncher; 59 private final AllAppsSearchBarController mSearchBarController; 60 private final SpannableStringBuilder mSearchQueryBuilder; 61 62 private AlphabeticalAppsList mApps; 63 private AllAppsContainerView mAppsView; 64 65 // This value was used to position the QSB. We store it here for translationY animations. 66 private final float mFixedTranslationY; 67 private final float mMarginTopAdjusting; 68 69 public AppsSearchContainerLayout(Context context) { 70 this(context, null); 71 } 72 73 public AppsSearchContainerLayout(Context context, AttributeSet attrs) { 74 this(context, attrs, 0); 75 } 76 77 public AppsSearchContainerLayout(Context context, AttributeSet attrs, int defStyleAttr) { 78 super(context, attrs, defStyleAttr); 79 80 mLauncher = Launcher.getLauncher(context); 81 mSearchBarController = new AllAppsSearchBarController(); 82 83 mSearchQueryBuilder = new SpannableStringBuilder(); 84 Selection.setSelection(mSearchQueryBuilder, 0); 85 86 mFixedTranslationY = getTranslationY(); 87 mMarginTopAdjusting = mFixedTranslationY - getPaddingTop(); 88 89 // Update the hint to contain the icon. 90 // Prefix the original hint with two spaces. The first space gets replaced by the icon 91 // using span. The second space is used for a singe space character between the hint 92 // and the icon. 93 SpannableString spanned = new SpannableString(" " + getHint()); 94 spanned.setSpan(new TintedDrawableSpan(getContext(), R.drawable.ic_allapps_search), 95 0, 1, Spannable.SPAN_EXCLUSIVE_INCLUSIVE); 96 setHint(spanned); 97 } 98 99 @Override 100 protected void onAttachedToWindow() { 101 super.onAttachedToWindow(); 102 mLauncher.getAppsView().getAppsStore().addUpdateListener(this); 103 } 104 105 @Override 106 protected void onDetachedFromWindow() { 107 super.onDetachedFromWindow(); 108 mLauncher.getAppsView().getAppsStore().removeUpdateListener(this); 109 } 110 111 @Override 112 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 113 // Update the width to match the grid padding 114 DeviceProfile dp = mLauncher.getDeviceProfile(); 115 int myRequestedWidth = getSize(widthMeasureSpec); 116 int rowWidth = myRequestedWidth - mAppsView.getActiveRecyclerView().getPaddingLeft() 117 - mAppsView.getActiveRecyclerView().getPaddingRight(); 118 119 int cellWidth = DeviceProfile.calculateCellWidth(rowWidth, dp.inv.numHotseatIcons); 120 int iconVisibleSize = Math.round(ICON_VISIBLE_AREA_FACTOR * dp.iconSizePx); 121 int iconPadding = cellWidth - iconVisibleSize; 122 123 int myWidth = rowWidth - iconPadding + getPaddingLeft() + getPaddingRight(); 124 super.onMeasure(makeMeasureSpec(myWidth, EXACTLY), heightMeasureSpec); 125 } 126 127 @Override 128 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 129 super.onLayout(changed, left, top, right, bottom); 130 131 // Shift the widget horizontally so that its centered in the parent (b/63428078) 132 View parent = (View) getParent(); 133 int availableWidth = parent.getWidth() - parent.getPaddingLeft() - parent.getPaddingRight(); 134 int myWidth = right - left; 135 int expectedLeft = parent.getPaddingLeft() + (availableWidth - myWidth) / 2; 136 int shift = expectedLeft - left; 137 setTranslationX(shift); 138 } 139 140 @Override 141 public void initialize(AllAppsContainerView appsView) { 142 mApps = appsView.getApps(); 143 mAppsView = appsView; 144 mSearchBarController.initialize( 145 new DefaultAppSearchAlgorithm(mApps.getApps()), this, mLauncher, this); 146 } 147 148 @Override 149 public void onAppsUpdated() { 150 mSearchBarController.refreshSearchResult(); 151 } 152 153 @Override 154 public void resetSearch() { 155 mSearchBarController.reset(); 156 } 157 158 @Override 159 public void preDispatchKeyEvent(KeyEvent event) { 160 // Determine if the key event was actual text, if so, focus the search bar and then dispatch 161 // the key normally so that it can process this key event 162 if (!mSearchBarController.isSearchFieldFocused() && 163 event.getAction() == KeyEvent.ACTION_DOWN) { 164 final int unicodeChar = event.getUnicodeChar(); 165 final boolean isKeyNotWhitespace = unicodeChar > 0 && 166 !Character.isWhitespace(unicodeChar) && !Character.isSpaceChar(unicodeChar); 167 if (isKeyNotWhitespace) { 168 boolean gotKey = TextKeyListener.getInstance().onKeyDown(this, mSearchQueryBuilder, 169 event.getKeyCode(), event); 170 if (gotKey && mSearchQueryBuilder.length() > 0) { 171 mSearchBarController.focusSearchField(); 172 } 173 } 174 } 175 } 176 177 @Override 178 public void onSearchResult(String query, ArrayList<ComponentKey> apps) { 179 if (apps != null) { 180 mApps.setOrderedFilter(apps); 181 notifyResultChanged(); 182 mAppsView.setLastSearchQuery(query); 183 } 184 } 185 186 @Override 187 public void clearSearchResult() { 188 if (mApps.setOrderedFilter(null)) { 189 notifyResultChanged(); 190 } 191 192 // Clear the search query 193 mSearchQueryBuilder.clear(); 194 mSearchQueryBuilder.clearSpans(); 195 Selection.setSelection(mSearchQueryBuilder, 0); 196 mAppsView.onClearSearchResult(); 197 } 198 199 private void notifyResultChanged() { 200 mAppsView.onSearchResultsChanged(); 201 } 202 203 @Override 204 public void setInsets(Rect insets) { 205 MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams(); 206 mlp.topMargin = Math.round(Math.max(-mFixedTranslationY, insets.top - mMarginTopAdjusting)); 207 requestLayout(); 208 209 DeviceProfile dp = mLauncher.getDeviceProfile(); 210 if (dp.isVerticalBarLayout()) { 211 mLauncher.getAllAppsController().setScrollRangeDelta(0); 212 } else { 213 mLauncher.getAllAppsController().setScrollRangeDelta( 214 insets.bottom + mlp.topMargin + mFixedTranslationY); 215 } 216 } 217 } 218