AllAppsContainerView.java revision 60331a9be74a14051e6e192db69307ce652da2ae
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 */ 16package com.android.launcher3.allapps; 17 18import android.annotation.SuppressLint; 19import android.annotation.TargetApi; 20import android.content.ComponentName; 21import android.content.Context; 22import android.content.res.Resources; 23import android.graphics.Point; 24import android.graphics.Rect; 25import android.graphics.drawable.InsetDrawable; 26import android.os.Build; 27import android.os.Bundle; 28import android.support.v7.widget.RecyclerView; 29import android.text.Selection; 30import android.text.SpannableStringBuilder; 31import android.text.method.TextKeyListener; 32import android.util.AttributeSet; 33import android.view.KeyEvent; 34import android.view.LayoutInflater; 35import android.view.MotionEvent; 36import android.view.View; 37import android.view.ViewConfiguration; 38import android.view.ViewGroup; 39import android.view.ViewTreeObserver; 40import android.widget.FrameLayout; 41import android.widget.LinearLayout; 42 43import com.android.launcher3.AppInfo; 44import com.android.launcher3.BaseContainerView; 45import com.android.launcher3.BubbleTextView; 46import com.android.launcher3.CellLayout; 47import com.android.launcher3.CheckLongPressHelper; 48import com.android.launcher3.DeleteDropTarget; 49import com.android.launcher3.DeviceProfile; 50import com.android.launcher3.DragSource; 51import com.android.launcher3.DropTarget; 52import com.android.launcher3.Folder; 53import com.android.launcher3.ItemInfo; 54import com.android.launcher3.Launcher; 55import com.android.launcher3.LauncherTransitionable; 56import com.android.launcher3.R; 57import com.android.launcher3.Stats; 58import com.android.launcher3.Utilities; 59import com.android.launcher3.Workspace; 60import com.android.launcher3.util.ComponentKey; 61import com.android.launcher3.util.Thunk; 62 63import java.nio.charset.Charset; 64import java.nio.charset.CharsetEncoder; 65import java.util.ArrayList; 66import java.util.List; 67 68 69 70/** 71 * A merge algorithm that merges every section indiscriminately. 72 */ 73final class FullMergeAlgorithm implements AlphabeticalAppsList.MergeAlgorithm { 74 75 @Override 76 public boolean continueMerging(AlphabeticalAppsList.SectionInfo section, 77 AlphabeticalAppsList.SectionInfo withSection, 78 int sectionAppCount, int numAppsPerRow, int mergeCount) { 79 // Merge EVERYTHING 80 return true; 81 } 82} 83 84/** 85 * The logic we use to merge multiple sections. We only merge sections when their final row 86 * contains less than a certain number of icons, and stop at a specified max number of merges. 87 * In addition, we will try and not merge sections that identify apps from different scripts. 88 */ 89final class SimpleSectionMergeAlgorithm implements AlphabeticalAppsList.MergeAlgorithm { 90 91 private int mMinAppsPerRow; 92 private int mMinRowsInMergedSection; 93 private int mMaxAllowableMerges; 94 private CharsetEncoder mAsciiEncoder; 95 96 public SimpleSectionMergeAlgorithm(int minAppsPerRow, int minRowsInMergedSection, int maxNumMerges) { 97 mMinAppsPerRow = minAppsPerRow; 98 mMinRowsInMergedSection = minRowsInMergedSection; 99 mMaxAllowableMerges = maxNumMerges; 100 mAsciiEncoder = Charset.forName("US-ASCII").newEncoder(); 101 } 102 103 @Override 104 public boolean continueMerging(AlphabeticalAppsList.SectionInfo section, 105 AlphabeticalAppsList.SectionInfo withSection, 106 int sectionAppCount, int numAppsPerRow, int mergeCount) { 107 // Continue merging if the number of hanging apps on the final row is less than some 108 // fixed number (ragged), the merged rows has yet to exceed some minimum row count, 109 // and while the number of merged sections is less than some fixed number of merges 110 int rows = sectionAppCount / numAppsPerRow; 111 int cols = sectionAppCount % numAppsPerRow; 112 113 // Ensure that we do not merge across scripts, currently we only allow for english and 114 // native scripts so we can test if both can just be ascii encoded 115 boolean isCrossScript = false; 116 if (section.firstAppItem != null && withSection.firstAppItem != null) { 117 isCrossScript = mAsciiEncoder.canEncode(section.firstAppItem.sectionName) != 118 mAsciiEncoder.canEncode(withSection.firstAppItem.sectionName); 119 } 120 return (0 < cols && cols < mMinAppsPerRow) && 121 rows < mMinRowsInMergedSection && 122 mergeCount < mMaxAllowableMerges && 123 !isCrossScript; 124 } 125} 126 127/** 128 * The all apps view container. 129 */ 130public class AllAppsContainerView extends BaseContainerView implements DragSource, 131 LauncherTransitionable, AlphabeticalAppsList.AdapterChangedCallback, 132 AllAppsGridAdapter.PredictionBarSpacerCallbacks, View.OnTouchListener, 133 View.OnLongClickListener, ViewTreeObserver.OnPreDrawListener, 134 AllAppsSearchBarController.Callbacks, Stats.LaunchSourceProvider { 135 136 private static final int MIN_ROWS_IN_MERGED_SECTION_PHONE = 3; 137 private static final int MAX_NUM_MERGES_PHONE = 2; 138 139 @Thunk Launcher mLauncher; 140 @Thunk AlphabeticalAppsList mApps; 141 private LayoutInflater mLayoutInflater; 142 private AllAppsGridAdapter mAdapter; 143 private RecyclerView.LayoutManager mLayoutManager; 144 private RecyclerView.ItemDecoration mItemDecoration; 145 146 @Thunk View mContent; 147 @Thunk View mContainerView; 148 @Thunk View mRevealView; 149 @Thunk AllAppsRecyclerView mAppsRecyclerView; 150 @Thunk ViewGroup mPredictionBarView; 151 @Thunk AllAppsSearchBarController mSearchBarController; 152 private ViewGroup mSearchBarContainerView; 153 private View mSearchBarView; 154 155 private int mSectionNamesMargin; 156 private int mNumAppsPerRow; 157 private int mNumPredictedAppsPerRow; 158 // This coordinate is relative to this container view 159 private final Point mBoundsCheckLastTouchDownPos = new Point(-1, -1); 160 // This coordinate is relative to its parent 161 private final Point mIconLastTouchPos = new Point(); 162 // This coordinate is used to proxy click and long-click events to the prediction bar icons 163 private final Point mPredictionIconTouchDownPos = new Point(); 164 // Normal container insets 165 private int mPredictionBarHeight; 166 private int mLastRecyclerViewScrollPos = -1; 167 @Thunk boolean mFocusPredictionBarOnFirstBind; 168 169 private SpannableStringBuilder mSearchQueryBuilder = null; 170 171 private CheckLongPressHelper mPredictionIconCheckForLongPress; 172 private View mPredictionIconUnderTouch; 173 174 public AllAppsContainerView(Context context) { 175 this(context, null); 176 } 177 178 public AllAppsContainerView(Context context, AttributeSet attrs) { 179 this(context, attrs, 0); 180 } 181 182 public AllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) { 183 super(context, attrs, defStyleAttr); 184 Resources res = context.getResources(); 185 186 mLauncher = (Launcher) context; 187 mLayoutInflater = LayoutInflater.from(context); 188 DeviceProfile grid = mLauncher.getDeviceProfile(); 189 mPredictionBarHeight = (int) (grid.allAppsIconSizePx + grid.iconDrawablePaddingOriginalPx + 190 Utilities.calculateTextHeight(grid.allAppsIconTextSizePx) + 191 2 * res.getDimensionPixelSize(R.dimen.all_apps_icon_top_bottom_padding) + 192 2 * res.getDimensionPixelSize(R.dimen.all_apps_prediction_bar_top_bottom_padding)); 193 mSectionNamesMargin = res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin); 194 mApps = new AlphabeticalAppsList(context); 195 mApps.setAdapterChangedCallback(this); 196 mAdapter = new AllAppsGridAdapter(context, mApps, this, this, mLauncher, this); 197 mAdapter.setEmptySearchText(res.getString(R.string.all_apps_loading_message)); 198 mAdapter.setPredictionRowHeight(mPredictionBarHeight); 199 mApps.setAdapter(mAdapter); 200 mLayoutManager = mAdapter.getLayoutManager(); 201 mItemDecoration = mAdapter.getItemDecoration(); 202 203 mSearchQueryBuilder = new SpannableStringBuilder(); 204 Selection.setSelection(mSearchQueryBuilder, 0); 205 } 206 207 /** 208 * Sets the current set of predicted apps. 209 */ 210 public void setPredictedApps(List<ComponentName> apps) { 211 mApps.setPredictedApps(apps); 212 } 213 214 /** 215 * Sets the current set of apps. 216 */ 217 public void setApps(List<AppInfo> apps) { 218 mApps.setApps(apps); 219 } 220 221 /** 222 * Adds new apps to the list. 223 */ 224 public void addApps(List<AppInfo> apps) { 225 mApps.addApps(apps); 226 } 227 228 /** 229 * Updates existing apps in the list 230 */ 231 public void updateApps(List<AppInfo> apps) { 232 mApps.updateApps(apps); 233 } 234 235 /** 236 * Removes some apps from the list. 237 */ 238 public void removeApps(List<AppInfo> apps) { 239 mApps.removeApps(apps); 240 } 241 242 /** 243 * Sets the search bar that shows above the a-z list. 244 */ 245 public void setSearchBarController(AllAppsSearchBarController searchController) { 246 if (mSearchBarController != null) { 247 throw new RuntimeException("Expected search bar controller to only be set once"); 248 } 249 mSearchBarController = searchController; 250 mSearchBarController.initialize(mApps, this); 251 252 // Add the new search view to the layout 253 View searchBarView = searchController.getView(mSearchBarContainerView); 254 mSearchBarContainerView.addView(searchBarView); 255 mSearchBarContainerView.setVisibility(View.VISIBLE); 256 mSearchBarView = searchBarView; 257 setHasSearchBar(); 258 259 updateBackgroundAndPaddings(); 260 } 261 262 /** 263 * Scrolls this list view to the top. 264 */ 265 public void scrollToTop() { 266 mAppsRecyclerView.scrollToTop(); 267 } 268 269 /** 270 * Returns the content view used for the launcher transitions. 271 */ 272 public View getContentView() { 273 return mContainerView; 274 } 275 276 /** 277 * Returns the all apps search view. 278 */ 279 public View getSearchBarView() { 280 return mSearchBarView; 281 } 282 283 /** 284 * Returns the reveal view used for the launcher transitions. 285 */ 286 public View getRevealView() { 287 return mRevealView; 288 } 289 290 /** 291 * Returns an new instance of the default app search controller. 292 */ 293 public AllAppsSearchBarController newDefaultAppSearchController() { 294 return new DefaultAppSearchController(getContext(), this, mAppsRecyclerView); 295 } 296 297 @Override 298 protected void onFinishInflate() { 299 super.onFinishInflate(); 300 boolean isRtl = Utilities.isRtl(getResources()); 301 mAdapter.setRtl(isRtl); 302 mContent = findViewById(R.id.content); 303 304 // This is a focus listener that proxies focus from a view into the list view. This is to 305 // work around the search box from getting first focus and showing the cursor. 306 View.OnFocusChangeListener focusProxyListener = new View.OnFocusChangeListener() { 307 @Override 308 public void onFocusChange(View v, boolean hasFocus) { 309 if (hasFocus) { 310 if (!mApps.getPredictedApps().isEmpty()) { 311 // If the prediction bar is going to be bound, then defer focusing until 312 // it is first bound 313 if (mPredictionBarView.getChildCount() == 0) { 314 mFocusPredictionBarOnFirstBind = true; 315 } else { 316 mPredictionBarView.requestFocus(); 317 } 318 } else { 319 mAppsRecyclerView.requestFocus(); 320 } 321 } 322 } 323 }; 324 mSearchBarContainerView = (ViewGroup) findViewById(R.id.search_box_container); 325 mSearchBarContainerView.setOnFocusChangeListener(focusProxyListener); 326 mContainerView = findViewById(R.id.all_apps_container); 327 mContainerView.setOnFocusChangeListener(focusProxyListener); 328 mRevealView = findViewById(R.id.all_apps_reveal); 329 330 // Load the all apps recycler view 331 mAppsRecyclerView = (AllAppsRecyclerView) findViewById(R.id.apps_list_view); 332 mAppsRecyclerView.setApps(mApps); 333 mAppsRecyclerView.setPredictionBarHeight(mPredictionBarHeight); 334 mAppsRecyclerView.setLayoutManager(mLayoutManager); 335 mAppsRecyclerView.setAdapter(mAdapter); 336 mAppsRecyclerView.setHasFixedSize(true); 337 if (mItemDecoration != null) { 338 mAppsRecyclerView.addItemDecoration(mItemDecoration); 339 } 340 341 // Fix the prediction bar height 342 mPredictionBarView = (ViewGroup) findViewById(R.id.prediction_bar); 343 FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mPredictionBarView.getLayoutParams(); 344 lp.height = mPredictionBarHeight; 345 346 updateBackgroundAndPaddings(); 347 } 348 349 @Override 350 public void onBoundsChanged(Rect newBounds) { 351 mLauncher.updateOverlayBounds(newBounds); 352 } 353 354 @Override 355 public void onBindPredictionBar() { 356 updatePredictionBarVisibility(); 357 358 List<AppInfo> predictedApps = mApps.getPredictedApps(); 359 360 // Remove extra prediction icons 361 while (mPredictionBarView.getChildCount() > mNumPredictedAppsPerRow) { 362 mPredictionBarView.removeViewAt(mPredictionBarView.getChildCount() - 1); 363 } 364 365 int childCount = mPredictionBarView.getChildCount(); 366 for (int i = 0; i < mNumPredictedAppsPerRow; i++) { 367 BubbleTextView icon; 368 if (i < childCount) { 369 // If a child at that index exists, then get that child 370 icon = (BubbleTextView) mPredictionBarView.getChildAt(i); 371 } else { 372 // Otherwise, inflate a new icon 373 icon = (BubbleTextView) mLayoutInflater.inflate( 374 R.layout.all_apps_prediction_bar_icon, mPredictionBarView, false); 375 icon.setFocusable(true); 376 icon.setLongPressTimeout(ViewConfiguration.get(getContext()).getLongPressTimeout()); 377 mPredictionBarView.addView(icon); 378 } 379 380 // Either apply the app info to the child, or hide the view 381 if (i < predictedApps.size()) { 382 if (icon.getVisibility() != View.VISIBLE) { 383 icon.setVisibility(View.VISIBLE); 384 } 385 icon.applyFromApplicationInfo(predictedApps.get(i)); 386 } else { 387 icon.setVisibility(View.INVISIBLE); 388 } 389 } 390 391 if (mFocusPredictionBarOnFirstBind) { 392 mFocusPredictionBarOnFirstBind = false; 393 mPredictionBarView.requestFocus(); 394 } 395 } 396 397 @Override 398 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 399 // Update the number of items in the grid before we measure the view 400 int availableWidth = !mContentBounds.isEmpty() ? mContentBounds.width() : 401 MeasureSpec.getSize(widthMeasureSpec); 402 DeviceProfile grid = mLauncher.getDeviceProfile(); 403 grid.updateAppsViewNumCols(getResources(), availableWidth); 404 if (mNumAppsPerRow != grid.allAppsNumCols || 405 mNumPredictedAppsPerRow != grid.allAppsNumPredictiveCols) { 406 mNumAppsPerRow = grid.allAppsNumCols; 407 mNumPredictedAppsPerRow = grid.allAppsNumPredictiveCols; 408 409 // If there is a start margin to draw section names, determine how we are going to merge 410 // app sections 411 boolean mergeSectionsFully = mSectionNamesMargin == 0 || !grid.isPhone; 412 AlphabeticalAppsList.MergeAlgorithm mergeAlgorithm = mergeSectionsFully ? 413 new FullMergeAlgorithm() : 414 new SimpleSectionMergeAlgorithm((int) Math.ceil(mNumAppsPerRow / 2f), 415 MIN_ROWS_IN_MERGED_SECTION_PHONE, MAX_NUM_MERGES_PHONE); 416 417 mAppsRecyclerView.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow); 418 mAdapter.setNumAppsPerRow(mNumAppsPerRow); 419 mApps.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow, mergeAlgorithm); 420 } 421 422 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 423 } 424 425 /** 426 * Update the background and padding of the Apps view and children. Instead of insetting the 427 * container view, we inset the background and padding of the recycler view to allow for the 428 * recycler view to handle touch events (for fast scrolling) all the way to the edge. 429 */ 430 @Override 431 protected void onUpdateBackgroundAndPaddings(Rect searchBarBounds, Rect padding) { 432 boolean isRtl = Utilities.isRtl(getResources()); 433 434 // TODO: Use quantum_panel instead of quantum_panel_shape. 435 InsetDrawable background = new InsetDrawable( 436 getResources().getDrawable(R.drawable.quantum_panel_shape), padding.left, 0, 437 padding.right, 0); 438 mContainerView.setBackground(background); 439 mRevealView.setBackground(background.getConstantState().newDrawable()); 440 mAppsRecyclerView.updateBackgroundPadding(padding); 441 mAdapter.updateBackgroundPadding(padding); 442 443 // Hack: We are going to let the recycler view take the full width, so reset the padding on 444 // the container to zero after setting the background and apply the top-bottom padding to 445 // the content view instead so that the launcher transition clips correctly. 446 mContent.setPadding(0, padding.top, 0, padding.bottom); 447 mContainerView.setPadding(0, 0, 0, 0); 448 449 // Pad the recycler view by the background padding plus the start margin (for the section 450 // names) 451 int startInset = Math.max(mSectionNamesMargin, mAppsRecyclerView.getScrollbarWidth()); 452 if (isRtl) { 453 mAppsRecyclerView.setPadding(padding.left + mAppsRecyclerView.getScrollbarWidth(), 0, 454 padding.right + startInset, 0); 455 } else { 456 mAppsRecyclerView.setPadding(padding.left + startInset, 0, 457 padding.right + mAppsRecyclerView.getScrollbarWidth(), 0); 458 } 459 460 // Inset the search bar to fit its bounds above the container 461 if (mSearchBarView != null) { 462 Rect backgroundPadding = new Rect(); 463 if (mSearchBarView.getBackground() != null) { 464 mSearchBarView.getBackground().getPadding(backgroundPadding); 465 } 466 LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) 467 mSearchBarContainerView.getLayoutParams(); 468 lp.leftMargin = searchBarBounds.left - backgroundPadding.left; 469 lp.topMargin = searchBarBounds.top - backgroundPadding.top; 470 lp.rightMargin = (getMeasuredWidth() - searchBarBounds.right) - backgroundPadding.right; 471 mSearchBarContainerView.requestLayout(); 472 } 473 474 // Update the prediction bar insets as well 475 mPredictionBarView = (ViewGroup) findViewById(R.id.prediction_bar); 476 FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mPredictionBarView.getLayoutParams(); 477 lp.leftMargin = padding.left + mAppsRecyclerView.getScrollbarWidth(); 478 lp.rightMargin = padding.right + mAppsRecyclerView.getScrollbarWidth(); 479 mPredictionBarView.requestLayout(); 480 } 481 482 @Override 483 public boolean onPreDraw() { 484 if (mNumAppsPerRow > 0) { 485 // Update the position of the prediction bar to match the scroll of the all apps list 486 synchronizeToRecyclerViewScrollPosition(mAppsRecyclerView.getScrollPosition()); 487 } 488 return true; 489 } 490 491 @Override 492 public boolean dispatchKeyEvent(KeyEvent event) { 493 // Determine if the key event was actual text, if so, focus the search bar and then dispatch 494 // the key normally so that it can process this key event 495 if (!mSearchBarController.isSearchFieldFocused() && 496 event.getAction() == KeyEvent.ACTION_DOWN) { 497 final int unicodeChar = event.getUnicodeChar(); 498 final boolean isKeyNotWhitespace = unicodeChar > 0 && 499 !Character.isWhitespace(unicodeChar) && !Character.isSpaceChar(unicodeChar); 500 if (isKeyNotWhitespace) { 501 boolean gotKey = TextKeyListener.getInstance().onKeyDown(this, mSearchQueryBuilder, 502 event.getKeyCode(), event); 503 if (gotKey && mSearchQueryBuilder.length() > 0) { 504 mSearchBarController.focusSearchField(); 505 } 506 } 507 } 508 509 return super.dispatchKeyEvent(event); 510 } 511 512 @Override 513 public boolean onInterceptTouchEvent(MotionEvent ev) { 514 return handleTouchEvent(ev); 515 } 516 517 @SuppressLint("ClickableViewAccessibility") 518 @Override 519 public boolean onTouchEvent(MotionEvent ev) { 520 return handleTouchEvent(ev); 521 } 522 523 @SuppressLint("ClickableViewAccessibility") 524 @Override 525 public boolean onTouch(View v, MotionEvent ev) { 526 switch (ev.getAction()) { 527 case MotionEvent.ACTION_DOWN: 528 case MotionEvent.ACTION_MOVE: 529 mIconLastTouchPos.set((int) ev.getX(), (int) ev.getY()); 530 break; 531 } 532 return false; 533 } 534 535 @Override 536 public boolean onLongClick(View v) { 537 // Return early if this is not initiated from a touch 538 if (!v.isInTouchMode()) return false; 539 // When we have exited all apps or are in transition, disregard long clicks 540 if (!mLauncher.isAppsViewVisible() || 541 mLauncher.getWorkspace().isSwitchingState()) return false; 542 // Return if global dragging is not enabled 543 if (!mLauncher.isDraggingEnabled()) return false; 544 545 // Start the drag 546 mLauncher.getWorkspace().beginDragShared(v, mIconLastTouchPos, this, false); 547 // Enter spring loaded mode 548 mLauncher.enterSpringLoadedDragMode(); 549 550 return false; 551 } 552 553 @Override 554 public boolean supportsFlingToDelete() { 555 return true; 556 } 557 558 @Override 559 public boolean supportsAppInfoDropTarget() { 560 return true; 561 } 562 563 @Override 564 public boolean supportsDeleteDropTarget() { 565 return false; 566 } 567 568 @Override 569 public float getIntrinsicIconScaleFactor() { 570 DeviceProfile grid = mLauncher.getDeviceProfile(); 571 return (float) grid.allAppsIconSizePx / grid.iconSizePx; 572 } 573 574 @Override 575 public void onFlingToDeleteCompleted() { 576 // We just dismiss the drag when we fling, so cleanup here 577 mLauncher.exitSpringLoadedDragModeDelayed(true, 578 Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null); 579 mLauncher.unlockScreenOrientation(false); 580 } 581 582 @Override 583 public void onDropCompleted(View target, DropTarget.DragObject d, boolean isFlingToDelete, 584 boolean success) { 585 if (isFlingToDelete || !success || (target != mLauncher.getWorkspace() && 586 !(target instanceof DeleteDropTarget) && !(target instanceof Folder))) { 587 // Exit spring loaded mode if we have not successfully dropped or have not handled the 588 // drop in Workspace 589 mLauncher.exitSpringLoadedDragModeDelayed(true, 590 Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null); 591 } 592 mLauncher.unlockScreenOrientation(false); 593 594 // Display an error message if the drag failed due to there not being enough space on the 595 // target layout we were dropping on. 596 if (!success) { 597 boolean showOutOfSpaceMessage = false; 598 if (target instanceof Workspace) { 599 int currentScreen = mLauncher.getCurrentWorkspaceScreen(); 600 Workspace workspace = (Workspace) target; 601 CellLayout layout = (CellLayout) workspace.getChildAt(currentScreen); 602 ItemInfo itemInfo = (ItemInfo) d.dragInfo; 603 if (layout != null) { 604 layout.calculateSpans(itemInfo); 605 showOutOfSpaceMessage = 606 !layout.findCellForSpan(null, itemInfo.spanX, itemInfo.spanY); 607 } 608 } 609 if (showOutOfSpaceMessage) { 610 mLauncher.showOutOfSpaceMessage(false); 611 } 612 613 d.deferDragViewCleanupPostAnimation = false; 614 } 615 } 616 617 @Override 618 public void onAdapterItemsChanged() { 619 updatePredictionBarVisibility(); 620 } 621 622 @Override 623 public void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace) { 624 // Register for a pre-draw listener to synchronize the recycler view scroll to other views 625 // in this container 626 if (!toWorkspace) { 627 getViewTreeObserver().addOnPreDrawListener(this); 628 } 629 } 630 631 @Override 632 public void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace) { 633 // Do nothing 634 } 635 636 @Override 637 public void onLauncherTransitionStep(Launcher l, float t) { 638 // Do nothing 639 } 640 641 @Override 642 public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) { 643 if (toWorkspace) { 644 getViewTreeObserver().removeOnPreDrawListener(this); 645 mLastRecyclerViewScrollPos = -1; 646 647 // Reset the search bar after transitioning home 648 mSearchBarController.reset(); 649 } 650 } 651 652 /** 653 * Updates the container when the recycler view is scrolled. 654 */ 655 @TargetApi(Build.VERSION_CODES.LOLLIPOP) 656 private void synchronizeToRecyclerViewScrollPosition(int scrollY) { 657 if (mLastRecyclerViewScrollPos != scrollY) { 658 mLastRecyclerViewScrollPos = scrollY; 659 660 // Scroll the prediction bar with the contents of the recycler view 661 mPredictionBarView.setTranslationY(-scrollY + mAppsRecyclerView.getPaddingTop()); 662 } 663 } 664 665 @Override 666 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { 667 // If we were waiting for long-click, cancel the request once a child has started handling 668 // the scrolling 669 if (mPredictionIconCheckForLongPress != null) { 670 mPredictionIconCheckForLongPress.cancelLongPress(); 671 } 672 super.requestDisallowInterceptTouchEvent(disallowIntercept); 673 } 674 675 /** 676 * Handles the touch events to dismiss all apps when clicking outside the bounds of the 677 * recycler view. 678 */ 679 private boolean handleTouchEvent(MotionEvent ev) { 680 DeviceProfile grid = mLauncher.getDeviceProfile(); 681 int x = (int) ev.getX(); 682 int y = (int) ev.getY(); 683 684 switch (ev.getAction()) { 685 case MotionEvent.ACTION_DOWN: 686 // We workaround the fact that the recycler view needs the touches for the scroll 687 // and we want to intercept it for clicks in the prediction bar by handling clicks 688 // and long clicks in the prediction bar ourselves. 689 if (mPredictionBarView != null && mPredictionBarView.getVisibility() == View.VISIBLE) { 690 mPredictionIconTouchDownPos.set(x, y); 691 mPredictionIconUnderTouch = findPredictedAppAtCoordinate(x, y); 692 if (mPredictionIconUnderTouch != null) { 693 mPredictionIconCheckForLongPress = 694 new CheckLongPressHelper(mPredictionIconUnderTouch, this); 695 mPredictionIconCheckForLongPress.postCheckForLongPress(); 696 } 697 } 698 699 if (!mContentBounds.isEmpty()) { 700 // Outset the fixed bounds and check if the touch is outside all apps 701 Rect tmpRect = new Rect(mContentBounds); 702 tmpRect.inset(-grid.allAppsIconSizePx / 2, 0); 703 if (ev.getX() < tmpRect.left || ev.getX() > tmpRect.right) { 704 mBoundsCheckLastTouchDownPos.set(x, y); 705 return true; 706 } 707 } else { 708 // Check if the touch is outside all apps 709 if (ev.getX() < getPaddingLeft() || 710 ev.getX() > (getWidth() - getPaddingRight())) { 711 mBoundsCheckLastTouchDownPos.set(x, y); 712 return true; 713 } 714 } 715 break; 716 case MotionEvent.ACTION_MOVE: 717 if (mPredictionIconUnderTouch != null) { 718 float dist = (float) Math.hypot(x - mPredictionIconTouchDownPos.x, 719 y - mPredictionIconTouchDownPos.y); 720 if (dist > ViewConfiguration.get(getContext()).getScaledTouchSlop()) { 721 if (mPredictionIconCheckForLongPress != null) { 722 mPredictionIconCheckForLongPress.cancelLongPress(); 723 } 724 mPredictionIconCheckForLongPress = null; 725 mPredictionIconUnderTouch = null; 726 } 727 } 728 break; 729 case MotionEvent.ACTION_UP: 730 if (mBoundsCheckLastTouchDownPos.x > -1) { 731 ViewConfiguration viewConfig = ViewConfiguration.get(getContext()); 732 float dx = ev.getX() - mBoundsCheckLastTouchDownPos.x; 733 float dy = ev.getY() - mBoundsCheckLastTouchDownPos.y; 734 float distance = (float) Math.hypot(dx, dy); 735 if (distance < viewConfig.getScaledTouchSlop()) { 736 // The background was clicked, so just go home 737 Launcher launcher = (Launcher) getContext(); 738 launcher.showWorkspace(true); 739 return true; 740 } 741 } 742 743 // Trigger the click on the prediction bar icon if that's where we touched 744 if (mPredictionIconUnderTouch != null && 745 !mPredictionIconCheckForLongPress.hasPerformedLongPress()) { 746 mLauncher.onClick(mPredictionIconUnderTouch); 747 } 748 749 // Fall through 750 case MotionEvent.ACTION_CANCEL: 751 mBoundsCheckLastTouchDownPos.set(-1, -1); 752 mPredictionIconTouchDownPos.set(-1, -1); 753 754 // On touch up/cancel, cancel the long press on the prediction bar icon if it has 755 // not yet been performed 756 if (mPredictionIconCheckForLongPress != null) { 757 mPredictionIconCheckForLongPress.cancelLongPress(); 758 mPredictionIconCheckForLongPress = null; 759 } 760 mPredictionIconUnderTouch = null; 761 762 break; 763 } 764 return false; 765 } 766 767 @Override 768 public void onSearchResult(String query, ArrayList<ComponentKey> apps) { 769 if (apps != null) { 770 if (apps.isEmpty()) { 771 String formatStr = getResources().getString(R.string.all_apps_no_search_results); 772 mAdapter.setEmptySearchText(String.format(formatStr, query)); 773 } else { 774 mAppsRecyclerView.scrollToTop(); 775 } 776 mApps.setOrderedFilter(apps); 777 } 778 } 779 780 @Override 781 public void clearSearchResult() { 782 mApps.setOrderedFilter(null); 783 784 // Clear the search query 785 mSearchQueryBuilder.clear(); 786 mSearchQueryBuilder.clearSpans(); 787 Selection.setSelection(mSearchQueryBuilder, 0); 788 } 789 790 @Override 791 public void fillInLaunchSourceData(Bundle sourceData) { 792 // Since the other cases are caught by the AllAppsRecyclerView LaunchSourceProvider, we just 793 // handle the prediction bar icons here 794 sourceData.putString(Stats.SOURCE_EXTRA_CONTAINER, Stats.CONTAINER_ALL_APPS); 795 sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER, 796 Stats.SUB_CONTAINER_ALL_APPS_PREDICTION); 797 } 798 799 /** 800 * Returns the predicted app in the prediction bar given a set of local coordinates. 801 */ 802 private View findPredictedAppAtCoordinate(int x, int y) { 803 Rect hitRect = new Rect(); 804 805 // Ensure that are touching in the recycler view 806 int[] coord = {x, y}; 807 Utilities.mapCoordInSelfToDescendent(mAppsRecyclerView, this, coord); 808 mAppsRecyclerView.getHitRect(hitRect); 809 if (!hitRect.contains(coord[0], coord[1])) { 810 return null; 811 } 812 813 // Check against the children of the prediction bar 814 coord[0] = x; 815 coord[1] = y; 816 Utilities.mapCoordInSelfToDescendent(mPredictionBarView, this, coord); 817 for (int i = 0; i < mPredictionBarView.getChildCount(); i++) { 818 View child = mPredictionBarView.getChildAt(i); 819 if (child.getVisibility() != View.VISIBLE) { 820 continue; 821 } 822 child.getHitRect(hitRect); 823 if (hitRect.contains(coord[0], coord[1])) { 824 return child; 825 } 826 } 827 return null; 828 } 829 830 /** 831 * Updates the visibility of the prediction bar. 832 * @return whether the prediction bar is visible 833 */ 834 private boolean updatePredictionBarVisibility() { 835 boolean showPredictionBar = !mApps.getPredictedApps().isEmpty() && 836 (!mApps.hasFilter() || mSearchBarController.shouldShowPredictionBar()); 837 if (showPredictionBar) { 838 mPredictionBarView.setVisibility(View.VISIBLE); 839 } else if (!showPredictionBar) { 840 mPredictionBarView.setVisibility(View.INVISIBLE); 841 } 842 return showPredictionBar; 843 } 844} 845