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.graphics.Color; 20 import android.graphics.Rect; 21 import android.graphics.drawable.ColorDrawable; 22 import android.graphics.drawable.InsetDrawable; 23 import android.support.annotation.NonNull; 24 import android.support.annotation.Nullable; 25 import android.support.v7.widget.RecyclerView; 26 import android.text.Selection; 27 import android.text.Spannable; 28 import android.text.SpannableString; 29 import android.text.SpannableStringBuilder; 30 import android.text.TextUtils; 31 import android.text.method.TextKeyListener; 32 import android.util.AttributeSet; 33 import android.view.KeyEvent; 34 import android.view.MotionEvent; 35 import android.view.View; 36 import android.view.ViewGroup; 37 38 import com.android.launcher3.AppInfo; 39 import com.android.launcher3.BaseContainerView; 40 import com.android.launcher3.BubbleTextView; 41 import com.android.launcher3.DeleteDropTarget; 42 import com.android.launcher3.DeviceProfile; 43 import com.android.launcher3.DragSource; 44 import com.android.launcher3.DropTarget; 45 import com.android.launcher3.ExtendedEditText; 46 import com.android.launcher3.Insettable; 47 import com.android.launcher3.ItemInfo; 48 import com.android.launcher3.Launcher; 49 import com.android.launcher3.R; 50 import com.android.launcher3.Utilities; 51 import com.android.launcher3.config.FeatureFlags; 52 import com.android.launcher3.discovery.AppDiscoveryItem; 53 import com.android.launcher3.discovery.AppDiscoveryUpdateState; 54 import com.android.launcher3.dragndrop.DragController; 55 import com.android.launcher3.dragndrop.DragOptions; 56 import com.android.launcher3.folder.Folder; 57 import com.android.launcher3.graphics.TintedDrawableSpan; 58 import com.android.launcher3.keyboard.FocusedItemDecorator; 59 import com.android.launcher3.userevent.nano.LauncherLogProto.Target; 60 import com.android.launcher3.util.ComponentKey; 61 import com.android.launcher3.util.PackageUserKey; 62 63 import java.util.ArrayList; 64 import java.util.List; 65 import java.util.Set; 66 67 /** 68 * The all apps view container. 69 */ 70 public class AllAppsContainerView extends BaseContainerView implements DragSource, 71 View.OnLongClickListener, AllAppsSearchBarController.Callbacks, Insettable { 72 73 private final Launcher mLauncher; 74 private final AlphabeticalAppsList mApps; 75 private final AllAppsGridAdapter mAdapter; 76 private final RecyclerView.LayoutManager mLayoutManager; 77 78 private AllAppsRecyclerView mAppsRecyclerView; 79 private AllAppsSearchBarController mSearchBarController; 80 81 private View mSearchContainer; 82 private int mSearchContainerMinHeight; 83 private ExtendedEditText mSearchInput; 84 private HeaderElevationController mElevationController; 85 86 private SpannableStringBuilder mSearchQueryBuilder = null; 87 88 private int mNumAppsPerRow; 89 private int mNumPredictedAppsPerRow; 90 91 public AllAppsContainerView(Context context) { 92 this(context, null); 93 } 94 95 public AllAppsContainerView(Context context, AttributeSet attrs) { 96 this(context, attrs, 0); 97 } 98 99 public AllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) { 100 super(context, attrs, defStyleAttr); 101 102 mLauncher = Launcher.getLauncher(context); 103 mApps = new AlphabeticalAppsList(context); 104 mAdapter = new AllAppsGridAdapter(mLauncher, mApps, mLauncher, this); 105 mApps.setAdapter(mAdapter); 106 mLayoutManager = mAdapter.getLayoutManager(); 107 mSearchQueryBuilder = new SpannableStringBuilder(); 108 mSearchContainerMinHeight 109 = getResources().getDimensionPixelSize(R.dimen.all_apps_search_bar_height); 110 111 Selection.setSelection(mSearchQueryBuilder, 0); 112 } 113 114 @Override 115 protected void updateBackground( 116 int paddingLeft, int paddingTop, int paddingRight, int paddingBottom) { 117 if (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP) { 118 if (mLauncher.getDeviceProfile().isVerticalBarLayout()) { 119 getRevealView().setBackground(new InsetDrawable(mBaseDrawable, 120 paddingLeft, paddingTop, paddingRight, paddingBottom)); 121 getContentView().setBackground( 122 new InsetDrawable(new ColorDrawable(Color.TRANSPARENT), 123 paddingLeft, paddingTop, paddingRight, paddingBottom)); 124 } else { 125 getRevealView().setBackground(mBaseDrawable); 126 } 127 } else { 128 super.updateBackground(paddingLeft, paddingTop, paddingRight, paddingBottom); 129 } 130 } 131 132 /** 133 * Sets the current set of predicted apps. 134 */ 135 public void setPredictedApps(List<ComponentKey> apps) { 136 mApps.setPredictedApps(apps); 137 } 138 139 /** 140 * Sets the current set of apps. 141 */ 142 public void setApps(List<AppInfo> apps) { 143 mApps.setApps(apps); 144 } 145 146 /** 147 * Adds new apps to the list. 148 */ 149 public void addApps(List<AppInfo> apps) { 150 mApps.addApps(apps); 151 mSearchBarController.refreshSearchResult(); 152 } 153 154 /** 155 * Updates existing apps in the list 156 */ 157 public void updateApps(List<AppInfo> apps) { 158 mApps.updateApps(apps); 159 mSearchBarController.refreshSearchResult(); 160 } 161 162 /** 163 * Removes some apps from the list. 164 */ 165 public void removeApps(List<AppInfo> apps) { 166 mApps.removeApps(apps); 167 mSearchBarController.refreshSearchResult(); 168 } 169 170 public void setSearchBarVisible(boolean visible) { 171 if (visible) { 172 mSearchBarController.setVisibility(View.VISIBLE); 173 } else { 174 mSearchBarController.setVisibility(View.INVISIBLE); 175 } 176 } 177 178 /** 179 * Sets the search bar that shows above the a-z list. 180 */ 181 public void setSearchBarController(AllAppsSearchBarController searchController) { 182 if (mSearchBarController != null) { 183 throw new RuntimeException("Expected search bar controller to only be set once"); 184 } 185 mSearchBarController = searchController; 186 mSearchBarController.initialize(mApps, mSearchInput, mLauncher, this); 187 mAdapter.setSearchController(mSearchBarController); 188 } 189 190 /** 191 * Scrolls this list view to the top. 192 */ 193 public void scrollToTop() { 194 mAppsRecyclerView.scrollToTop(); 195 } 196 197 /** 198 * Returns whether the view itself will handle the touch event or not. 199 */ 200 public boolean shouldContainerScroll(MotionEvent ev) { 201 int[] point = new int[2]; 202 point[0] = (int) ev.getX(); 203 point[1] = (int) ev.getY(); 204 Utilities.mapCoordInSelfToDescendant(mAppsRecyclerView, this, point); 205 206 // IF the MotionEvent is inside the search box, and the container keeps on receiving 207 // touch input, container should move down. 208 if (mLauncher.getDragLayer().isEventOverView(mSearchContainer, ev)) { 209 return true; 210 } 211 212 // IF the MotionEvent is inside the thumb, container should not be pulled down. 213 if (mAppsRecyclerView.getScrollBar().isNearThumb(point[0], point[1])) { 214 return false; 215 } 216 217 // IF scroller is at the very top OR there is no scroll bar because there is probably not 218 // enough items to scroll, THEN it's okay for the container to be pulled down. 219 if (mAppsRecyclerView.getCurrentScrollY() == 0) { 220 return true; 221 } 222 return false; 223 } 224 225 /** 226 * Focuses the search field and begins an app search. 227 */ 228 public void startAppsSearch() { 229 if (mSearchBarController != null) { 230 mSearchBarController.focusSearchField(); 231 } 232 } 233 234 /** 235 * Resets the state of AllApps. 236 */ 237 public void reset() { 238 // Reset the search bar and base recycler view after transitioning home 239 scrollToTop(); 240 mSearchBarController.reset(); 241 mAppsRecyclerView.reset(); 242 } 243 244 @Override 245 protected void onFinishInflate() { 246 super.onFinishInflate(); 247 248 // This is a focus listener that proxies focus from a view into the list view. This is to 249 // work around the search box from getting first focus and showing the cursor. 250 getContentView().setOnFocusChangeListener(new View.OnFocusChangeListener() { 251 @Override 252 public void onFocusChange(View v, boolean hasFocus) { 253 if (hasFocus) { 254 mAppsRecyclerView.requestFocus(); 255 } 256 } 257 }); 258 259 mSearchContainer = findViewById(R.id.search_container); 260 mSearchInput = (ExtendedEditText) findViewById(R.id.search_box_input); 261 262 // Update the hint to contain the icon. 263 // Prefix the original hint with two spaces. The first space gets replaced by the icon 264 // using span. The second space is used for a singe space character between the hint 265 // and the icon. 266 SpannableString spanned = new SpannableString(" " + mSearchInput.getHint()); 267 spanned.setSpan(new TintedDrawableSpan(getContext(), R.drawable.ic_allapps_search), 268 0, 1, Spannable.SPAN_EXCLUSIVE_INCLUSIVE); 269 mSearchInput.setHint(spanned); 270 271 mElevationController = new HeaderElevationController(mSearchContainer); 272 273 // Load the all apps recycler view 274 mAppsRecyclerView = (AllAppsRecyclerView) findViewById(R.id.apps_list_view); 275 mAppsRecyclerView.setApps(mApps); 276 mAppsRecyclerView.setLayoutManager(mLayoutManager); 277 mAppsRecyclerView.setAdapter(mAdapter); 278 mAppsRecyclerView.setHasFixedSize(true); 279 mAppsRecyclerView.addOnScrollListener(mElevationController); 280 mAppsRecyclerView.setElevationController(mElevationController); 281 282 FocusedItemDecorator focusedItemDecorator = new FocusedItemDecorator(mAppsRecyclerView); 283 mAppsRecyclerView.addItemDecoration(focusedItemDecorator); 284 mAppsRecyclerView.preMeasureViews(mAdapter); 285 mAdapter.setIconFocusListener(focusedItemDecorator.getFocusListener()); 286 287 if (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP) { 288 getRevealView().setVisibility(View.VISIBLE); 289 getContentView().setVisibility(View.VISIBLE); 290 getContentView().setBackground(null); 291 } 292 } 293 294 @Override 295 public View getTouchDelegateTargetView() { 296 return mAppsRecyclerView; 297 } 298 299 @Override 300 public void onBoundsChanged(Rect newBounds) { } 301 302 @Override 303 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 304 DeviceProfile grid = mLauncher.getDeviceProfile(); 305 grid.updateAppsViewNumCols(); 306 if (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP) { 307 if (mNumAppsPerRow != grid.inv.numColumns || 308 mNumPredictedAppsPerRow != grid.inv.numColumns) { 309 mNumAppsPerRow = grid.inv.numColumns; 310 mNumPredictedAppsPerRow = grid.inv.numColumns; 311 312 mAppsRecyclerView.setNumAppsPerRow(grid, mNumAppsPerRow); 313 mAdapter.setNumAppsPerRow(mNumAppsPerRow); 314 mApps.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow); 315 } 316 if (!grid.isVerticalBarLayout()) { 317 MarginLayoutParams searchContainerLp = 318 (MarginLayoutParams) mSearchContainer.getLayoutParams(); 319 320 searchContainerLp.height = mLauncher.getDragLayer().getInsets().top 321 + mSearchContainerMinHeight; 322 mSearchContainer.setLayoutParams(searchContainerLp); 323 } 324 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 325 return; 326 } 327 328 // --- remove START when {@code FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP} is enabled. --- 329 330 // Update the number of items in the grid before we measure the view 331 grid.updateAppsViewNumCols(); 332 if (mNumAppsPerRow != grid.allAppsNumCols || 333 mNumPredictedAppsPerRow != grid.allAppsNumPredictiveCols) { 334 mNumAppsPerRow = grid.allAppsNumCols; 335 mNumPredictedAppsPerRow = grid.allAppsNumPredictiveCols; 336 337 mAppsRecyclerView.setNumAppsPerRow(grid, mNumAppsPerRow); 338 mAdapter.setNumAppsPerRow(mNumAppsPerRow); 339 mApps.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow); 340 } 341 342 // --- remove END when {@code FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP} is enabled. --- 343 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 344 } 345 346 @Override 347 public boolean dispatchKeyEvent(KeyEvent event) { 348 // Determine if the key event was actual text, if so, focus the search bar and then dispatch 349 // the key normally so that it can process this key event 350 if (!mSearchBarController.isSearchFieldFocused() && 351 event.getAction() == KeyEvent.ACTION_DOWN) { 352 final int unicodeChar = event.getUnicodeChar(); 353 final boolean isKeyNotWhitespace = unicodeChar > 0 && 354 !Character.isWhitespace(unicodeChar) && !Character.isSpaceChar(unicodeChar); 355 if (isKeyNotWhitespace) { 356 boolean gotKey = TextKeyListener.getInstance().onKeyDown(this, mSearchQueryBuilder, 357 event.getKeyCode(), event); 358 if (gotKey && mSearchQueryBuilder.length() > 0) { 359 mSearchBarController.focusSearchField(); 360 } 361 } 362 } 363 364 return super.dispatchKeyEvent(event); 365 } 366 367 @Override 368 public boolean onLongClick(final View v) { 369 // Return early if this is not initiated from a touch 370 if (!v.isInTouchMode()) return false; 371 // When we have exited all apps or are in transition, disregard long clicks 372 373 if (!mLauncher.isAppsViewVisible() || 374 mLauncher.getWorkspace().isSwitchingState()) return false; 375 // Return if global dragging is not enabled or we are already dragging 376 if (!mLauncher.isDraggingEnabled()) return false; 377 if (mLauncher.getDragController().isDragging()) return false; 378 379 // Start the drag 380 final DragController dragController = mLauncher.getDragController(); 381 dragController.addDragListener(new DragController.DragListener() { 382 @Override 383 public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) { 384 v.setVisibility(INVISIBLE); 385 } 386 387 @Override 388 public void onDragEnd() { 389 v.setVisibility(VISIBLE); 390 dragController.removeDragListener(this); 391 } 392 }); 393 mLauncher.getWorkspace().beginDragShared(v, this, new DragOptions()); 394 return false; 395 } 396 397 @Override 398 public boolean supportsAppInfoDropTarget() { 399 return true; 400 } 401 402 @Override 403 public boolean supportsDeleteDropTarget() { 404 return false; 405 } 406 407 @Override 408 public float getIntrinsicIconScaleFactor() { 409 DeviceProfile grid = mLauncher.getDeviceProfile(); 410 return (float) grid.allAppsIconSizePx / grid.iconSizePx; 411 } 412 413 @Override 414 public void onDropCompleted(View target, DropTarget.DragObject d, boolean isFlingToDelete, 415 boolean success) { 416 if (isFlingToDelete || !success || (target != mLauncher.getWorkspace() && 417 !(target instanceof DeleteDropTarget) && !(target instanceof Folder))) { 418 // Exit spring loaded mode if we have not successfully dropped or have not handled the 419 // drop in Workspace 420 mLauncher.exitSpringLoadedDragModeDelayed(true, 421 Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null); 422 } 423 mLauncher.unlockScreenOrientation(false); 424 425 if (!success) { 426 d.deferDragViewCleanupPostAnimation = false; 427 } 428 } 429 430 @Override 431 public void onSearchResult(String query, ArrayList<ComponentKey> apps) { 432 if (apps != null) { 433 mApps.setOrderedFilter(apps); 434 mAppsRecyclerView.onSearchResultsChanged(); 435 mAdapter.setLastSearchQuery(query); 436 } 437 } 438 439 @Override 440 public void onAppDiscoverySearchUpdate(@Nullable AppDiscoveryItem app, 441 @NonNull AppDiscoveryUpdateState state) { 442 if (!mLauncher.isDestroyed()) { 443 mApps.onAppDiscoverySearchUpdate(app, state); 444 mAppsRecyclerView.onSearchResultsChanged(); 445 } 446 } 447 448 @Override 449 public void clearSearchResult() { 450 if (mApps.setOrderedFilter(null)) { 451 mAppsRecyclerView.onSearchResultsChanged(); 452 } 453 454 // Clear the search query 455 mSearchQueryBuilder.clear(); 456 mSearchQueryBuilder.clearSpans(); 457 Selection.setSelection(mSearchQueryBuilder, 0); 458 } 459 460 @Override 461 public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) { 462 targetParent.containerType = mAppsRecyclerView.getContainerType(v); 463 } 464 465 public boolean shouldRestoreImeState() { 466 return !TextUtils.isEmpty(mSearchInput.getText()); 467 } 468 469 @Override 470 public void setInsets(Rect insets) { 471 DeviceProfile grid = mLauncher.getDeviceProfile(); 472 if (grid.isVerticalBarLayout()) { 473 ViewGroup.MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams(); 474 mlp.leftMargin = insets.left; 475 mlp.topMargin = insets.top; 476 mlp.rightMargin = insets.right; 477 setLayoutParams(mlp); 478 } else { 479 View navBarBg = findViewById(R.id.nav_bar_bg); 480 ViewGroup.LayoutParams navBarBgLp = navBarBg.getLayoutParams(); 481 navBarBgLp.height = insets.bottom; 482 navBarBg.setLayoutParams(navBarBgLp); 483 navBarBg.setVisibility(View.VISIBLE); 484 } 485 } 486 487 public void updateIconBadges(Set<PackageUserKey> updatedBadges) { 488 final PackageUserKey packageUserKey = new PackageUserKey(null, null); 489 final int n = mAppsRecyclerView.getChildCount(); 490 for (int i = 0; i < n; i++) { 491 View child = mAppsRecyclerView.getChildAt(i); 492 if (!(child instanceof BubbleTextView) || !(child.getTag() instanceof ItemInfo)) { 493 continue; 494 } 495 ItemInfo info = (ItemInfo) child.getTag(); 496 if (packageUserKey.updateFromItemInfo(info) && updatedBadges.contains(packageUserKey)) { 497 ((BubbleTextView) child).applyBadgeState(info, true /* animate */); 498 } 499 } 500 } 501 } 502