1 /* 2 * Copyright (C) 2022 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 static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.SEARCH; 19 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_COUNT; 20 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB; 21 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB; 22 import static com.android.launcher3.testing.shared.TestProtocol.WORK_TAB_MISSING; 23 import static com.android.launcher3.util.ScrollableLayoutManager.PREDICTIVE_BACK_MIN_SCALE; 24 25 import android.animation.Animator; 26 import android.animation.AnimatorListenerAdapter; 27 import android.animation.ValueAnimator; 28 import android.content.Context; 29 import android.graphics.Canvas; 30 import android.graphics.Color; 31 import android.graphics.Outline; 32 import android.graphics.Paint; 33 import android.graphics.Path; 34 import android.graphics.Path.Direction; 35 import android.graphics.Point; 36 import android.graphics.Rect; 37 import android.graphics.RectF; 38 import android.os.Bundle; 39 import android.os.Parcelable; 40 import android.os.Process; 41 import android.os.UserManager; 42 import android.util.AttributeSet; 43 import android.util.Log; 44 import android.util.SparseArray; 45 import android.util.TypedValue; 46 import android.view.KeyEvent; 47 import android.view.LayoutInflater; 48 import android.view.MotionEvent; 49 import android.view.View; 50 import android.view.ViewOutlineProvider; 51 import android.view.WindowInsets; 52 import android.widget.Button; 53 import android.widget.RelativeLayout; 54 55 import androidx.annotation.NonNull; 56 import androidx.annotation.Nullable; 57 import androidx.annotation.Px; 58 import androidx.annotation.VisibleForTesting; 59 import androidx.core.graphics.ColorUtils; 60 import androidx.recyclerview.widget.RecyclerView; 61 62 import com.android.launcher3.DeviceProfile; 63 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener; 64 import com.android.launcher3.DragSource; 65 import com.android.launcher3.DropTarget.DragObject; 66 import com.android.launcher3.Insettable; 67 import com.android.launcher3.InsettableFrameLayout; 68 import com.android.launcher3.R; 69 import com.android.launcher3.Utilities; 70 import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem; 71 import com.android.launcher3.allapps.search.DefaultSearchAdapterProvider; 72 import com.android.launcher3.allapps.search.SearchAdapterProvider; 73 import com.android.launcher3.config.FeatureFlags; 74 import com.android.launcher3.keyboard.FocusedItemDecorator; 75 import com.android.launcher3.model.StringCache; 76 import com.android.launcher3.model.data.ItemInfo; 77 import com.android.launcher3.testing.shared.TestProtocol; 78 import com.android.launcher3.util.ItemInfoMatcher; 79 import com.android.launcher3.util.Themes; 80 import com.android.launcher3.views.ActivityContext; 81 import com.android.launcher3.views.BaseDragLayer; 82 import com.android.launcher3.views.RecyclerViewFastScroller; 83 import com.android.launcher3.views.ScrimView; 84 import com.android.launcher3.views.SpringRelativeLayout; 85 import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip; 86 87 import java.util.ArrayList; 88 import java.util.Arrays; 89 import java.util.List; 90 import java.util.function.Predicate; 91 import java.util.stream.Stream; 92 93 /** 94 * All apps container view with search support for use in a dragging activity. 95 * 96 * @param <T> Type of context inflating all apps. 97 */ 98 public class ActivityAllAppsContainerView<T extends Context & ActivityContext> 99 extends SpringRelativeLayout implements DragSource, Insettable, 100 OnDeviceProfileChangeListener, PersonalWorkSlidingTabStrip.OnActivePageChangedListener, 101 ScrimView.ScrimDrawingController { 102 103 public static final float PULL_MULTIPLIER = .02f; 104 public static final float FLING_VELOCITY_MULTIPLIER = 1200f; 105 protected static final String BUNDLE_KEY_CURRENT_PAGE = "launcher.allapps.current_page"; 106 private static final long DEFAULT_SEARCH_TRANSITION_DURATION_MS = 300; 107 // Render the header protection at all times to debug clipping issues. 108 private static final boolean DEBUG_HEADER_PROTECTION = false; 109 /** Context of an activity or window that is inflating this container. */ 110 111 protected final T mActivityContext; 112 protected final List<AdapterHolder> mAH; 113 protected final Predicate<ItemInfo> mPersonalMatcher = ItemInfoMatcher.ofUser( 114 Process.myUserHandle()); 115 protected final WorkProfileManager mWorkManager; 116 protected final Point mFastScrollerOffset = new Point(); 117 protected final int mScrimColor; 118 protected final float mHeaderThreshold; 119 120 // Used to animate Search results out and A-Z apps in, or vice-versa. 121 private final SearchTransitionController mSearchTransitionController; 122 private final Paint mHeaderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 123 private final Rect mInsets = new Rect(); 124 private final AllAppsStore mAllAppsStore = new AllAppsStore(); 125 private final RecyclerView.OnScrollListener mScrollListener = 126 new RecyclerView.OnScrollListener() { 127 @Override 128 public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { 129 updateHeaderScroll(recyclerView.computeVerticalScrollOffset()); 130 } 131 }; 132 private final Paint mNavBarScrimPaint; 133 private final int mHeaderProtectionColor; 134 private final Path mTmpPath = new Path(); 135 private final RectF mTmpRectF = new RectF(); 136 protected AllAppsPagedView mViewPager; 137 protected FloatingHeaderView mHeader; 138 protected View mBottomSheetBackground; 139 protected RecyclerViewFastScroller mFastScroller; 140 141 /** 142 * View that defines the search box. Result is rendered inside {@link #mSearchRecyclerView}. 143 */ 144 protected View mSearchContainer; 145 protected SearchUiManager mSearchUiManager; 146 protected boolean mUsingTabs; 147 protected RecyclerViewFastScroller mTouchHandler; 148 149 /** {@code true} when rendered view is in search state instead of the scroll state. */ 150 private boolean mIsSearching; 151 private boolean mRebindAdaptersAfterSearchAnimation; 152 private int mNavBarScrimHeight = 0; 153 private SearchRecyclerView mSearchRecyclerView; 154 protected SearchAdapterProvider<?> mMainAdapterProvider; 155 private View mBottomSheetHandleArea; 156 private boolean mHasWorkApps; 157 private float[] mBottomSheetCornerRadii; 158 private ScrimView mScrimView; 159 private int mHeaderColor; 160 private int mBottomSheetBackgroundColor; 161 private int mTabsProtectionAlpha; 162 @Nullable private AllAppsTransitionController mAllAppsTransitionController; 163 ActivityAllAppsContainerView(Context context)164 public ActivityAllAppsContainerView(Context context) { 165 this(context, null); 166 } 167 ActivityAllAppsContainerView(Context context, AttributeSet attrs)168 public ActivityAllAppsContainerView(Context context, AttributeSet attrs) { 169 this(context, attrs, 0); 170 } 171 ActivityAllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr)172 public ActivityAllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) { 173 super(context, attrs, defStyleAttr); 174 mActivityContext = ActivityContext.lookupContext(context); 175 176 mScrimColor = Themes.getAttrColor(context, R.attr.allAppsScrimColor); 177 mHeaderThreshold = getResources().getDimensionPixelSize( 178 R.dimen.dynamic_grid_cell_border_spacing); 179 mHeaderProtectionColor = Themes.getAttrColor(context, R.attr.allappsHeaderProtectionColor); 180 181 mWorkManager = new WorkProfileManager( 182 mActivityContext.getSystemService(UserManager.class), 183 this, mActivityContext.getStatsLogManager()); 184 mAH = Arrays.asList(null, null, null); 185 mNavBarScrimPaint = new Paint(); 186 mNavBarScrimPaint.setColor(Themes.getAttrColor(context, R.attr.allAppsNavBarScrimColor)); 187 188 AllAppsStore.OnUpdateListener onAppsUpdated = this::onAppsUpdated; 189 if (TestProtocol.sDebugTracing) { 190 Log.d(WORK_TAB_MISSING, "ActivityAllAppsContainer#init registeringListener: " + 191 onAppsUpdated); 192 } 193 mAllAppsStore.addUpdateListener(onAppsUpdated); 194 195 // This is a focus listener that proxies focus from a view into the list view. This is to 196 // work around the search box from getting first focus and showing the cursor. 197 setOnFocusChangeListener((v, hasFocus) -> { 198 if (hasFocus && getActiveRecyclerView() != null) { 199 getActiveRecyclerView().requestFocus(); 200 } 201 }); 202 initContent(); 203 204 mSearchTransitionController = new SearchTransitionController(this); 205 } 206 207 /** 208 * Initializes the view hierarchy and internal variables. Any initialization which actually uses 209 * these members should be done in {@link #onFinishInflate()}. 210 * In terms of subclass initialization, the following would be parallel order for activity: 211 * initContent -> onPreCreate 212 * constructor/init -> onCreate 213 * onFinishInflate -> onPostCreate 214 */ initContent()215 protected void initContent() { 216 mMainAdapterProvider = createMainAdapterProvider(); 217 218 mAH.set(AdapterHolder.MAIN, new AdapterHolder(AdapterHolder.MAIN, 219 new AlphabeticalAppsList<>(mActivityContext, mAllAppsStore, null))); 220 mAH.set(AdapterHolder.WORK, new AdapterHolder(AdapterHolder.WORK, 221 new AlphabeticalAppsList<>(mActivityContext, mAllAppsStore, mWorkManager))); 222 mAH.set(SEARCH, new AdapterHolder(SEARCH, 223 new AlphabeticalAppsList<>(mActivityContext, null, null))); 224 225 getLayoutInflater().inflate(R.layout.all_apps_content, this); 226 mHeader = findViewById(R.id.all_apps_header); 227 mBottomSheetBackground = findViewById(R.id.bottom_sheet_background); 228 mBottomSheetHandleArea = findViewById(R.id.bottom_sheet_handle_area); 229 mSearchRecyclerView = findViewById(R.id.search_results_list_view); 230 mFastScroller = findViewById(R.id.fast_scroller); 231 mFastScroller.setPopupView(findViewById(R.id.fast_scroller_popup)); 232 233 // Add the search box above everything else. 234 mSearchContainer = inflateSearchBox(); 235 addView(mSearchContainer); 236 mSearchUiManager = (SearchUiManager) mSearchContainer; 237 } 238 239 @Override onFinishInflate()240 protected void onFinishInflate() { 241 super.onFinishInflate(); 242 243 mAH.get(SEARCH).setup(mSearchRecyclerView, 244 /* Filter out A-Z apps */ itemInfo -> false); 245 rebindAdapters(true /* force */); 246 float cornerRadius = Themes.getDialogCornerRadius(getContext()); 247 mBottomSheetCornerRadii = new float[]{ 248 cornerRadius, 249 cornerRadius, // Top left radius in px 250 cornerRadius, 251 cornerRadius, // Top right radius in px 252 0, 253 0, // Bottom right 254 0, 255 0 // Bottom left 256 }; 257 final TypedValue value = new TypedValue(); 258 getContext().getTheme().resolveAttribute(android.R.attr.colorBackground, value, true); 259 mBottomSheetBackgroundColor = value.data; 260 updateBackground(mActivityContext.getDeviceProfile()); 261 mSearchUiManager.initializeSearch(this); 262 } 263 264 @Override onAttachedToWindow()265 protected void onAttachedToWindow() { 266 super.onAttachedToWindow(); 267 mActivityContext.addOnDeviceProfileChangeListener(this); 268 } 269 270 @Override onDetachedFromWindow()271 protected void onDetachedFromWindow() { 272 super.onDetachedFromWindow(); 273 mActivityContext.removeOnDeviceProfileChangeListener(this); 274 } 275 getSearchUiManager()276 public SearchUiManager getSearchUiManager() { 277 return mSearchUiManager; 278 } 279 getSearchView()280 public View getSearchView() { 281 return mSearchContainer; 282 } 283 284 /** Invoke when the current search session is finished. */ onClearSearchResult()285 public void onClearSearchResult() { 286 getMainAdapterProvider().clearHighlightedItem(); 287 animateToSearchState(false); 288 rebindAdapters(); 289 } 290 291 /** 292 * Sets results list for search 293 */ setSearchResults(ArrayList<AdapterItem> results)294 public void setSearchResults(ArrayList<AdapterItem> results) { 295 getMainAdapterProvider().clearHighlightedItem(); 296 if (getSearchResultList().setSearchResults(results)) { 297 getSearchRecyclerView().onSearchResultsChanged(); 298 } 299 if (results != null) { 300 animateToSearchState(true); 301 } 302 } 303 304 /** 305 * Sets results list for search. 306 * 307 * @param searchResultCode indicates if the result is final or intermediate for a given query 308 * since we can get search results from multiple sources. 309 */ setSearchResults(ArrayList<AdapterItem> results, int searchResultCode)310 public void setSearchResults(ArrayList<AdapterItem> results, int searchResultCode) { 311 setSearchResults(results); 312 } 313 animateToSearchState(boolean goingToSearch)314 private void animateToSearchState(boolean goingToSearch) { 315 animateToSearchState(goingToSearch, DEFAULT_SEARCH_TRANSITION_DURATION_MS); 316 } 317 setAllAppsTransitionController( AllAppsTransitionController allAppsTransitionController)318 public void setAllAppsTransitionController( 319 AllAppsTransitionController allAppsTransitionController) { 320 mAllAppsTransitionController = allAppsTransitionController; 321 } 322 animateToSearchState(boolean goingToSearch, long durationMs)323 private void animateToSearchState(boolean goingToSearch, long durationMs) { 324 if (!mSearchTransitionController.isRunning() && goingToSearch == isSearching()) { 325 return; 326 } 327 mFastScroller.setVisibility(goingToSearch ? INVISIBLE : VISIBLE); 328 if (goingToSearch) { 329 // Fade out the button to pause work apps. 330 mWorkManager.onActivePageChanged(SEARCH); 331 } else if (mAllAppsTransitionController != null) { 332 // If exiting search, revert predictive back scale on all apps 333 mAllAppsTransitionController.animateAllAppsToNoScale(); 334 } 335 mSearchTransitionController.animateToSearchState(goingToSearch, durationMs, 336 /* onEndRunnable = */ () -> { 337 mIsSearching = goingToSearch; 338 updateSearchResultsVisibility(); 339 int previousPage = getCurrentPage(); 340 if (mRebindAdaptersAfterSearchAnimation) { 341 rebindAdapters(false); 342 mRebindAdaptersAfterSearchAnimation = false; 343 } 344 if (!goingToSearch) { 345 setSearchResults(null); 346 if (mViewPager != null) { 347 mViewPager.setCurrentPage(previousPage); 348 } 349 onActivePageChanged(previousPage); 350 } 351 }); 352 } 353 shouldContainerScroll(MotionEvent ev)354 public boolean shouldContainerScroll(MotionEvent ev) { 355 BaseDragLayer dragLayer = mActivityContext.getDragLayer(); 356 // IF the MotionEvent is inside the search box or handle area, and the container keeps on 357 // receiving touch input, container should move down. 358 if (dragLayer.isEventOverView(mSearchContainer, ev) 359 || dragLayer.isEventOverView(mBottomSheetHandleArea, ev)) { 360 return true; 361 } 362 AllAppsRecyclerView rv = getActiveRecyclerView(); 363 if (rv == null) { 364 return true; 365 } 366 if (rv.getScrollbar() != null 367 && rv.getScrollbar().getThumbOffsetY() >= 0 368 && dragLayer.isEventOverView(rv.getScrollbar(), ev)) { 369 return false; 370 } 371 // Scroll if not within the container view (e.g. over large-screen scrim). 372 if (!dragLayer.isEventOverView(getVisibleContainerView(), ev)) { 373 return true; 374 } 375 return rv.shouldContainerScroll(ev, dragLayer); 376 } 377 reset(boolean animate)378 public void reset(boolean animate) { 379 for (int i = 0; i < mAH.size(); i++) { 380 if (mAH.get(i).mRecyclerView != null) { 381 mAH.get(i).mRecyclerView.scrollToTop(); 382 } 383 } 384 if (mTouchHandler != null) { 385 mTouchHandler.endFastScrolling(); 386 } 387 if (mHeader != null && mHeader.getVisibility() == VISIBLE) { 388 mHeader.reset(animate); 389 } 390 // Reset the base recycler view after transitioning home. 391 updateHeaderScroll(0); 392 // Reset the search bar after transitioning home. 393 mSearchUiManager.resetSearch(); 394 // Animate to A-Z with 0 time to reset the animation with proper state management. 395 animateToSearchState(false, 0); 396 } 397 398 @Override dispatchKeyEvent(KeyEvent event)399 public boolean dispatchKeyEvent(KeyEvent event) { 400 mSearchUiManager.preDispatchKeyEvent(event); 401 return super.dispatchKeyEvent(event); 402 } 403 getDescription()404 public String getDescription() { 405 if (!mUsingTabs && isSearching()) { 406 return getContext().getString(R.string.all_apps_search_results); 407 } else { 408 StringCache cache = mActivityContext.getStringCache(); 409 if (mUsingTabs) { 410 if (cache != null) { 411 return isPersonalTab() 412 ? cache.allAppsPersonalTabAccessibility 413 : cache.allAppsWorkTabAccessibility; 414 } else { 415 return isPersonalTab() 416 ? getContext().getString(R.string.all_apps_button_personal_label) 417 : getContext().getString(R.string.all_apps_button_work_label); 418 } 419 } 420 return getContext().getString(R.string.all_apps_button_label); 421 } 422 } 423 isSearching()424 public boolean isSearching() { 425 return mIsSearching; 426 } 427 428 @Override onActivePageChanged(int currentActivePage)429 public void onActivePageChanged(int currentActivePage) { 430 if (mSearchTransitionController.isRunning()) { 431 // Will be called at the end of the animation. 432 return; 433 } 434 if (mAH.get(currentActivePage).mRecyclerView != null) { 435 mAH.get(currentActivePage).mRecyclerView.bindFastScrollbar(mFastScroller); 436 } 437 // Header keeps track of active recycler view to properly render header protection. 438 mHeader.setActiveRV(currentActivePage); 439 reset(true /* animate */); 440 441 mWorkManager.onActivePageChanged(currentActivePage); 442 } 443 rebindAdapters()444 protected void rebindAdapters() { 445 rebindAdapters(false /* force */); 446 } 447 rebindAdapters(boolean force)448 protected void rebindAdapters(boolean force) { 449 if (mSearchTransitionController.isRunning()) { 450 mRebindAdaptersAfterSearchAnimation = true; 451 return; 452 } 453 updateSearchResultsVisibility(); 454 455 boolean showTabs = shouldShowTabs(); 456 if (showTabs == mUsingTabs && !force) { 457 return; 458 } 459 460 if (isSearching()) { 461 mUsingTabs = showTabs; 462 mWorkManager.detachWorkModeSwitch(); 463 return; 464 } 465 466 if (!FeatureFlags.ENABLE_SEARCH_RESULT_BACKGROUND_DRAWABLES.get()) { 467 RecyclerView.ItemDecoration decoration = getMainAdapterProvider().getDecorator(); 468 getSearchRecyclerView().removeItemDecoration(decoration); 469 getSearchRecyclerView().addItemDecoration(decoration); 470 } 471 472 // replaceAppsRVcontainer() needs to use both mUsingTabs value to remove the old view AND 473 // showTabs value to create new view. Hence the mUsingTabs new value assignment MUST happen 474 // after this call. 475 replaceAppsRVContainer(showTabs); 476 mUsingTabs = showTabs; 477 478 mAllAppsStore.unregisterIconContainer(mAH.get(AdapterHolder.MAIN).mRecyclerView); 479 mAllAppsStore.unregisterIconContainer(mAH.get(AdapterHolder.WORK).mRecyclerView); 480 mAllAppsStore.unregisterIconContainer(mAH.get(AdapterHolder.SEARCH).mRecyclerView); 481 482 if (mUsingTabs) { 483 mAH.get(AdapterHolder.MAIN).setup(mViewPager.getChildAt(0), mPersonalMatcher); 484 mAH.get(AdapterHolder.WORK).setup(mViewPager.getChildAt(1), mWorkManager.getMatcher()); 485 mAH.get(AdapterHolder.WORK).mRecyclerView.setId(R.id.apps_list_view_work); 486 if (FeatureFlags.ENABLE_EXPANDING_PAUSE_WORK_BUTTON.get()) { 487 mAH.get(AdapterHolder.WORK).mRecyclerView.addOnScrollListener( 488 mWorkManager.newScrollListener()); 489 } 490 mViewPager.getPageIndicator().setActiveMarker(AdapterHolder.MAIN); 491 findViewById(R.id.tab_personal) 492 .setOnClickListener((View view) -> { 493 if (mViewPager.snapToPage(AdapterHolder.MAIN)) { 494 mActivityContext.getStatsLogManager().logger() 495 .log(LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB); 496 } 497 mActivityContext.hideKeyboard(); 498 }); 499 findViewById(R.id.tab_work) 500 .setOnClickListener((View view) -> { 501 if (mViewPager.snapToPage(AdapterHolder.WORK)) { 502 mActivityContext.getStatsLogManager().logger() 503 .log(LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB); 504 } 505 mActivityContext.hideKeyboard(); 506 }); 507 setDeviceManagementResources(); 508 onActivePageChanged(mViewPager.getNextPage()); 509 } else { 510 mAH.get(AdapterHolder.MAIN).setup(findViewById(R.id.apps_list_view), null); 511 mAH.get(AdapterHolder.WORK).mRecyclerView = null; 512 } 513 setupHeader(); 514 515 if (isSearchBarOnBottom()) { 516 // Keep the scroller above the search bar. 517 RelativeLayout.LayoutParams scrollerLayoutParams = 518 (LayoutParams) mFastScroller.getLayoutParams(); 519 scrollerLayoutParams.addRule(RelativeLayout.ABOVE, R.id.search_container_all_apps); 520 scrollerLayoutParams.removeRule(RelativeLayout.ALIGN_PARENT_BOTTOM); 521 scrollerLayoutParams.bottomMargin = getResources().getDimensionPixelSize( 522 R.dimen.fastscroll_bottom_margin_floating_search); 523 } 524 525 mAllAppsStore.registerIconContainer(mAH.get(AdapterHolder.MAIN).mRecyclerView); 526 mAllAppsStore.registerIconContainer(mAH.get(AdapterHolder.WORK).mRecyclerView); 527 mAllAppsStore.registerIconContainer(mAH.get(AdapterHolder.SEARCH).mRecyclerView); 528 } 529 replaceAppsRVContainer(boolean showTabs)530 protected View replaceAppsRVContainer(boolean showTabs) { 531 for (int i = AdapterHolder.MAIN; i <= AdapterHolder.WORK; i++) { 532 AdapterHolder adapterHolder = mAH.get(i); 533 if (adapterHolder.mRecyclerView != null) { 534 adapterHolder.mRecyclerView.setLayoutManager(null); 535 adapterHolder.mRecyclerView.setAdapter(null); 536 } 537 } 538 View oldView = getAppsRecyclerViewContainer(); 539 int index = indexOfChild(oldView); 540 removeView(oldView); 541 int layout = showTabs ? R.layout.all_apps_tabs : R.layout.all_apps_rv_layout; 542 final View rvContainer = getLayoutInflater().inflate(layout, this, false); 543 addView(rvContainer, index); 544 if (showTabs) { 545 mViewPager = (AllAppsPagedView) rvContainer; 546 mViewPager.initParentViews(this); 547 mViewPager.getPageIndicator().setOnActivePageChangedListener(this); 548 mViewPager.setOutlineProvider(new ViewOutlineProvider() { 549 @Override 550 public void getOutline(View view, Outline outline) { 551 @Px final int bottomOffsetPx = 552 (int) (ActivityAllAppsContainerView.this.getMeasuredHeight() 553 * PREDICTIVE_BACK_MIN_SCALE); 554 outline.setRect( 555 0, 556 0, 557 view.getMeasuredWidth(), 558 view.getMeasuredHeight() + bottomOffsetPx); 559 } 560 }); 561 562 mWorkManager.reset(); 563 post(() -> mAH.get(AdapterHolder.WORK).applyPadding()); 564 565 } else { 566 mWorkManager.detachWorkModeSwitch(); 567 mViewPager = null; 568 } 569 570 removeCustomRules(rvContainer); 571 removeCustomRules(getSearchRecyclerView()); 572 if (!isSearchSupported()) { 573 layoutWithoutSearchContainer(rvContainer, showTabs); 574 } else if (isSearchBarOnBottom()) { 575 alignParentTop(rvContainer, showTabs); 576 alignParentTop(getSearchRecyclerView(), /* tabs= */ false); 577 layoutAboveSearchContainer(rvContainer); 578 layoutAboveSearchContainer(getSearchRecyclerView()); 579 } else { 580 layoutBelowSearchContainer(rvContainer, showTabs); 581 layoutBelowSearchContainer(getSearchRecyclerView(), /* tabs= */ false); 582 } 583 584 return rvContainer; 585 } 586 setupHeader()587 void setupHeader() { 588 mHeader.setVisibility(View.VISIBLE); 589 boolean tabsHidden = !mUsingTabs; 590 mHeader.setup( 591 mAH.get(AdapterHolder.MAIN).mRecyclerView, 592 mAH.get(AdapterHolder.WORK).mRecyclerView, 593 (SearchRecyclerView) mAH.get(SEARCH).mRecyclerView, 594 getCurrentPage(), 595 tabsHidden); 596 597 int padding = mHeader.getMaxTranslation(); 598 mAH.forEach(adapterHolder -> { 599 adapterHolder.mPadding.top = padding; 600 adapterHolder.applyPadding(); 601 if (adapterHolder.mRecyclerView != null) { 602 adapterHolder.mRecyclerView.scrollToTop(); 603 } 604 }); 605 606 removeCustomRules(mHeader); 607 if (!isSearchSupported()) { 608 layoutWithoutSearchContainer(mHeader, false /* includeTabsMargin */); 609 } else if (isSearchBarOnBottom()) { 610 alignParentTop(mHeader, false /* includeTabsMargin */); 611 } else { 612 layoutBelowSearchContainer(mHeader, false /* includeTabsMargin */); 613 } 614 } 615 updateHeaderScroll(int scrolledOffset)616 protected void updateHeaderScroll(int scrolledOffset) { 617 float prog1 = Utilities.boundToRange((float) scrolledOffset / mHeaderThreshold, 0f, 1f); 618 int headerColor = getHeaderColor(prog1); 619 int tabsAlpha = mHeader.getPeripheralProtectionHeight() == 0 ? 0 620 : (int) (Utilities.boundToRange( 621 (scrolledOffset + mHeader.mSnappedScrolledY) / mHeaderThreshold, 0f, 1f) 622 * 255); 623 if (headerColor != mHeaderColor || mTabsProtectionAlpha != tabsAlpha) { 624 mHeaderColor = headerColor; 625 mTabsProtectionAlpha = tabsAlpha; 626 invalidateHeader(); 627 } 628 if (mSearchUiManager.getEditText() == null) { 629 return; 630 } 631 632 float prog = Utilities.boundToRange((float) scrolledOffset / mHeaderThreshold, 0f, 1f); 633 boolean bgVisible = mSearchUiManager.getBackgroundVisibility(); 634 if (scrolledOffset == 0 && !isSearching()) { 635 bgVisible = true; 636 } else if (scrolledOffset > mHeaderThreshold) { 637 bgVisible = false; 638 } 639 mSearchUiManager.setBackgroundVisibility(bgVisible, 1 - prog); 640 } 641 getHeaderColor(float blendRatio)642 protected int getHeaderColor(float blendRatio) { 643 return ColorUtils.setAlphaComponent( 644 ColorUtils.blendARGB(mScrimColor, mHeaderProtectionColor, blendRatio), 645 (int) (mSearchContainer.getAlpha() * 255)); 646 } 647 648 /** 649 * It is up to the search container view created by {@link #inflateSearchBox()} to use the 650 * floating search bar flag to move itself to the bottom of this container. This method checks 651 * if that had been done; otherwise the flag will be ignored. 652 * 653 * @return true if the search bar is at the bottom of the container (as opposed to the top). 654 **/ isSearchBarOnBottom()655 private boolean isSearchBarOnBottom() { 656 return FeatureFlags.ENABLE_FLOATING_SEARCH_BAR.get() 657 && ((RelativeLayout.LayoutParams) mSearchContainer.getLayoutParams()).getRule( 658 ALIGN_PARENT_BOTTOM) == RelativeLayout.TRUE; 659 } 660 layoutBelowSearchContainer(View v, boolean includeTabsMargin)661 private void layoutBelowSearchContainer(View v, boolean includeTabsMargin) { 662 if (!(v.getLayoutParams() instanceof RelativeLayout.LayoutParams)) { 663 return; 664 } 665 666 RelativeLayout.LayoutParams layoutParams = (LayoutParams) v.getLayoutParams(); 667 layoutParams.addRule(RelativeLayout.ALIGN_TOP, R.id.search_container_all_apps); 668 669 int topMargin = getContext().getResources().getDimensionPixelSize( 670 R.dimen.all_apps_header_top_margin); 671 if (includeTabsMargin) { 672 topMargin += getContext().getResources().getDimensionPixelSize( 673 R.dimen.all_apps_header_pill_height); 674 } 675 layoutParams.topMargin = topMargin; 676 } 677 layoutAboveSearchContainer(View v)678 private void layoutAboveSearchContainer(View v) { 679 if (!(v.getLayoutParams() instanceof RelativeLayout.LayoutParams)) { 680 return; 681 } 682 683 RelativeLayout.LayoutParams layoutParams = (LayoutParams) v.getLayoutParams(); 684 layoutParams.addRule(RelativeLayout.ABOVE, R.id.search_container_all_apps); 685 } 686 alignParentTop(View v, boolean includeTabsMargin)687 private void alignParentTop(View v, boolean includeTabsMargin) { 688 if (!(v.getLayoutParams() instanceof RelativeLayout.LayoutParams)) { 689 return; 690 } 691 692 RelativeLayout.LayoutParams layoutParams = (LayoutParams) v.getLayoutParams(); 693 layoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP); 694 layoutParams.topMargin = 695 includeTabsMargin 696 ? getContext().getResources().getDimensionPixelSize( 697 R.dimen.all_apps_header_pill_height) 698 : 0; 699 } 700 removeCustomRules(View v)701 private void removeCustomRules(View v) { 702 if (!(v.getLayoutParams() instanceof RelativeLayout.LayoutParams)) { 703 return; 704 } 705 706 RelativeLayout.LayoutParams layoutParams = (LayoutParams) v.getLayoutParams(); 707 layoutParams.removeRule(RelativeLayout.ABOVE); 708 layoutParams.removeRule(RelativeLayout.ALIGN_TOP); 709 layoutParams.removeRule(RelativeLayout.ALIGN_PARENT_TOP); 710 } 711 createAdapter(AlphabeticalAppsList<T> appsList)712 protected BaseAllAppsAdapter<T> createAdapter(AlphabeticalAppsList<T> appsList) { 713 return new AllAppsGridAdapter<>(mActivityContext, getLayoutInflater(), appsList, 714 mMainAdapterProvider); 715 } 716 717 // TODO(b/216683257): Remove when Taskbar All Apps supports search. isSearchSupported()718 protected boolean isSearchSupported() { 719 return true; 720 } 721 layoutWithoutSearchContainer(View v, boolean includeTabsMargin)722 private void layoutWithoutSearchContainer(View v, boolean includeTabsMargin) { 723 if (!(v.getLayoutParams() instanceof RelativeLayout.LayoutParams)) { 724 return; 725 } 726 727 RelativeLayout.LayoutParams layoutParams = (LayoutParams) v.getLayoutParams(); 728 layoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP); 729 layoutParams.topMargin = getContext().getResources().getDimensionPixelSize(includeTabsMargin 730 ? R.dimen.all_apps_header_pill_height 731 : R.dimen.all_apps_header_top_margin); 732 } 733 isInAllApps()734 public boolean isInAllApps() { 735 // TODO: Make this abstract 736 return true; 737 } 738 739 /** 740 * Inflates the search box 741 */ inflateSearchBox()742 protected View inflateSearchBox() { 743 return getLayoutInflater().inflate(R.layout.search_container_all_apps, this, false); 744 } 745 746 /** Creates the adapter provider for the main section. */ createMainAdapterProvider()747 protected SearchAdapterProvider<?> createMainAdapterProvider() { 748 return new DefaultSearchAdapterProvider(mActivityContext); 749 } 750 751 /** The adapter provider for the main section. */ getMainAdapterProvider()752 public final SearchAdapterProvider<?> getMainAdapterProvider() { 753 return mMainAdapterProvider; 754 } 755 756 @Override dispatchRestoreInstanceState(SparseArray<Parcelable> sparseArray)757 protected void dispatchRestoreInstanceState(SparseArray<Parcelable> sparseArray) { 758 try { 759 // Many slice view id is not properly assigned, and hence throws null 760 // pointer exception in the underneath method. Catching the exception 761 // simply doesn't restore these slice views. This doesn't have any 762 // user visible effect because because we query them again. 763 super.dispatchRestoreInstanceState(sparseArray); 764 } catch (Exception e) { 765 Log.e("AllAppsContainerView", "restoreInstanceState viewId = 0", e); 766 } 767 768 Bundle state = (Bundle) sparseArray.get(R.id.work_tab_state_id, null); 769 if (state != null) { 770 int currentPage = state.getInt(BUNDLE_KEY_CURRENT_PAGE, 0); 771 if (currentPage == AdapterHolder.WORK && mViewPager != null) { 772 mViewPager.setCurrentPage(currentPage); 773 rebindAdapters(); 774 } else { 775 reset(true); 776 } 777 } 778 } 779 780 @Override dispatchSaveInstanceState(SparseArray<Parcelable> container)781 protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) { 782 super.dispatchSaveInstanceState(container); 783 Bundle state = new Bundle(); 784 state.putInt(BUNDLE_KEY_CURRENT_PAGE, getCurrentPage()); 785 container.put(R.id.work_tab_state_id, state); 786 } 787 788 /** 789 * Sets the long click listener for icons 790 */ setOnIconLongClickListener(OnLongClickListener listener)791 public void setOnIconLongClickListener(OnLongClickListener listener) { 792 for (AdapterHolder holder : mAH) { 793 holder.mAdapter.setOnIconLongClickListener(listener); 794 } 795 } 796 getAppsStore()797 public AllAppsStore getAppsStore() { 798 return mAllAppsStore; 799 } 800 getWorkManager()801 public WorkProfileManager getWorkManager() { 802 return mWorkManager; 803 } 804 805 @Override onDeviceProfileChanged(DeviceProfile dp)806 public void onDeviceProfileChanged(DeviceProfile dp) { 807 for (AdapterHolder holder : mAH) { 808 holder.mAdapter.setAppsPerRow(dp.numShownAllAppsColumns); 809 if (holder.mRecyclerView != null) { 810 // Remove all views and clear the pool, while keeping the data same. After this 811 // call, all the viewHolders will be recreated. 812 holder.mRecyclerView.swapAdapter(holder.mRecyclerView.getAdapter(), true); 813 holder.mRecyclerView.getRecycledViewPool().clear(); 814 } 815 } 816 updateBackground(dp); 817 } 818 updateBackground(DeviceProfile deviceProfile)819 protected void updateBackground(DeviceProfile deviceProfile) { 820 mBottomSheetBackground.setVisibility(deviceProfile.isTablet ? View.VISIBLE : View.GONE); 821 // Note: For tablets, the opaque background and header protection are added in drawOnScrim. 822 // For the taskbar entrypoint, the scrim is drawn differently, so a static background is 823 // added in TaskbarAllAppsContainerView and header protection is not yet supported. 824 } 825 onAppsUpdated()826 private void onAppsUpdated() { 827 mHasWorkApps = Stream.of(mAllAppsStore.getApps()).anyMatch(mWorkManager.getMatcher()); 828 if (TestProtocol.sDebugTracing) { 829 Log.d(WORK_TAB_MISSING, "ActivityAllAppsContainerView#onAppsUpdated hasWorkApps: " + 830 mHasWorkApps + " allApps: " + mAllAppsStore.getApps().length); 831 } 832 if (!isSearching()) { 833 rebindAdapters(); 834 if (mHasWorkApps) { 835 mWorkManager.reset(); 836 } 837 } 838 839 mActivityContext.getStatsLogManager().logger() 840 .withCardinality(mAllAppsStore.getApps().length) 841 .log(LAUNCHER_ALLAPPS_COUNT); 842 } 843 844 @Override onInterceptTouchEvent(MotionEvent ev)845 public boolean onInterceptTouchEvent(MotionEvent ev) { 846 // The AllAppsContainerView houses the QSB and is hence visible from the Workspace 847 // Overview states. We shouldn't intercept for the scrubber in these cases. 848 if (!isInAllApps()) { 849 mTouchHandler = null; 850 return false; 851 } 852 853 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 854 AllAppsRecyclerView rv = getActiveRecyclerView(); 855 if (rv != null && rv.getScrollbar() != null 856 && rv.getScrollbar().isHitInParent(ev.getX(), ev.getY(), mFastScrollerOffset)) { 857 mTouchHandler = rv.getScrollbar(); 858 } else { 859 mTouchHandler = null; 860 } 861 } 862 if (mTouchHandler != null) { 863 return mTouchHandler.handleTouchEvent(ev, mFastScrollerOffset); 864 } 865 return false; 866 } 867 868 @Override onTouchEvent(MotionEvent ev)869 public boolean onTouchEvent(MotionEvent ev) { 870 if (!isInAllApps()) { 871 return false; 872 } 873 874 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 875 AllAppsRecyclerView rv = getActiveRecyclerView(); 876 if (rv != null && rv.getScrollbar() != null 877 && rv.getScrollbar().isHitInParent(ev.getX(), ev.getY(), mFastScrollerOffset)) { 878 mTouchHandler = rv.getScrollbar(); 879 } else { 880 mTouchHandler = null; 881 882 } 883 } 884 if (mTouchHandler != null) { 885 mTouchHandler.handleTouchEvent(ev, mFastScrollerOffset); 886 return true; 887 } 888 if (isSearching() 889 && mActivityContext.getDragLayer().isEventOverView(getVisibleContainerView(), ev)) { 890 // if in search state, consume touch event. 891 return true; 892 } 893 return false; 894 } 895 896 /** The current active recycler view (A-Z list from one of the profiles, or search results). */ getActiveRecyclerView()897 public AllAppsRecyclerView getActiveRecyclerView() { 898 if (isSearching()) { 899 return getSearchRecyclerView(); 900 } 901 return getActiveAppsRecyclerView(); 902 } 903 904 /** The current apps recycler view in the container. */ getActiveAppsRecyclerView()905 private AllAppsRecyclerView getActiveAppsRecyclerView() { 906 if (!mUsingTabs || isPersonalTab()) { 907 return mAH.get(AdapterHolder.MAIN).mRecyclerView; 908 } else { 909 return mAH.get(AdapterHolder.WORK).mRecyclerView; 910 } 911 } 912 913 /** 914 * The container for A-Z apps (the ViewPager for main+work tabs, or main RV). This is currently 915 * hidden while searching. 916 **/ getAppsRecyclerViewContainer()917 protected View getAppsRecyclerViewContainer() { 918 return mViewPager != null ? mViewPager : findViewById(R.id.apps_list_view); 919 } 920 921 /** The RV for search results, which is hidden while A-Z apps are visible. */ getSearchRecyclerView()922 public SearchRecyclerView getSearchRecyclerView() { 923 return mSearchRecyclerView; 924 } 925 isPersonalTab()926 protected boolean isPersonalTab() { 927 return mViewPager == null || mViewPager.getNextPage() == 0; 928 } 929 930 /** 931 * Switches the current page to the provided {@code tab} if tabs are supported, otherwise does 932 * nothing. 933 */ switchToTab(int tab)934 public void switchToTab(int tab) { 935 if (mUsingTabs) { 936 mViewPager.setCurrentPage(tab); 937 } 938 } 939 getLayoutInflater()940 public LayoutInflater getLayoutInflater() { 941 return LayoutInflater.from(getContext()); 942 } 943 944 @Override onDropCompleted(View target, DragObject d, boolean success)945 public void onDropCompleted(View target, DragObject d, boolean success) {} 946 947 @Override setInsets(Rect insets)948 public void setInsets(Rect insets) { 949 mInsets.set(insets); 950 DeviceProfile grid = mActivityContext.getDeviceProfile(); 951 952 applyAdapterSideAndBottomPaddings(grid); 953 954 MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams(); 955 mlp.leftMargin = insets.left; 956 mlp.rightMargin = insets.right; 957 setLayoutParams(mlp); 958 959 if (grid.isVerticalBarLayout()) { 960 setPadding(grid.workspacePadding.left, 0, grid.workspacePadding.right, 0); 961 } else { 962 int topPadding = grid.allAppsTopPadding; 963 if (isSearchBarOnBottom() && !grid.isTablet) { 964 topPadding += getResources().getDimensionPixelSize( 965 R.dimen.all_apps_additional_top_padding_floating_search); 966 } 967 setPadding(grid.allAppsLeftRightMargin, topPadding, grid.allAppsLeftRightMargin, 0); 968 } 969 970 InsettableFrameLayout.dispatchInsets(this, insets); 971 } 972 973 /** 974 * Returns a padding in case a scrim is shown on the bottom of the view and a padding is needed. 975 */ computeNavBarScrimHeight(WindowInsets insets)976 protected int computeNavBarScrimHeight(WindowInsets insets) { 977 return 0; 978 } 979 980 /** 981 * Returns the current height of nav bar scrim 982 */ getNavBarScrimHeight()983 public int getNavBarScrimHeight() { 984 return mNavBarScrimHeight; 985 } 986 987 @Override dispatchApplyWindowInsets(WindowInsets insets)988 public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) { 989 mNavBarScrimHeight = computeNavBarScrimHeight(insets); 990 applyAdapterSideAndBottomPaddings(mActivityContext.getDeviceProfile()); 991 return super.dispatchApplyWindowInsets(insets); 992 } 993 994 @Override dispatchDraw(Canvas canvas)995 protected void dispatchDraw(Canvas canvas) { 996 super.dispatchDraw(canvas); 997 998 if (mNavBarScrimHeight > 0) { 999 canvas.drawRect(0, getHeight() - mNavBarScrimHeight, getWidth(), getHeight(), 1000 mNavBarScrimPaint); 1001 } 1002 } 1003 updateSearchResultsVisibility()1004 protected void updateSearchResultsVisibility() { 1005 if (isSearching()) { 1006 getSearchRecyclerView().setVisibility(VISIBLE); 1007 getAppsRecyclerViewContainer().setVisibility(GONE); 1008 mHeader.setVisibility(GONE); 1009 } else { 1010 getSearchRecyclerView().setVisibility(GONE); 1011 getAppsRecyclerViewContainer().setVisibility(VISIBLE); 1012 mHeader.setVisibility(VISIBLE); 1013 } 1014 if (mHeader.isSetUp()) { 1015 mHeader.setActiveRV(getCurrentPage()); 1016 } 1017 } 1018 applyAdapterSideAndBottomPaddings(DeviceProfile grid)1019 private void applyAdapterSideAndBottomPaddings(DeviceProfile grid) { 1020 int bottomPadding = Math.max(mInsets.bottom, mNavBarScrimHeight); 1021 mAH.forEach(adapterHolder -> { 1022 adapterHolder.mPadding.bottom = bottomPadding; 1023 adapterHolder.mPadding.left = 1024 adapterHolder.mPadding.right = grid.allAppsLeftRightPadding; 1025 adapterHolder.applyPadding(); 1026 }); 1027 } 1028 setDeviceManagementResources()1029 private void setDeviceManagementResources() { 1030 if (mActivityContext.getStringCache() != null) { 1031 Button personalTab = findViewById(R.id.tab_personal); 1032 personalTab.setText(mActivityContext.getStringCache().allAppsPersonalTab); 1033 1034 Button workTab = findViewById(R.id.tab_work); 1035 workTab.setText(mActivityContext.getStringCache().allAppsWorkTab); 1036 } 1037 } 1038 shouldShowTabs()1039 protected boolean shouldShowTabs() { 1040 return mHasWorkApps; 1041 } 1042 1043 // Used by tests only isDescendantViewVisible(int viewId)1044 private boolean isDescendantViewVisible(int viewId) { 1045 final View view = findViewById(viewId); 1046 if (view == null) return false; 1047 1048 if (!view.isShown()) return false; 1049 1050 return view.getGlobalVisibleRect(new Rect()); 1051 } 1052 1053 @VisibleForTesting isPersonalTabVisible()1054 public boolean isPersonalTabVisible() { 1055 return isDescendantViewVisible(R.id.tab_personal); 1056 } 1057 1058 @VisibleForTesting isWorkTabVisible()1059 public boolean isWorkTabVisible() { 1060 return isDescendantViewVisible(R.id.tab_work); 1061 } 1062 getSearchResultList()1063 public AlphabeticalAppsList<T> getSearchResultList() { 1064 return mAH.get(SEARCH).mAppsList; 1065 } 1066 getFloatingHeaderView()1067 public FloatingHeaderView getFloatingHeaderView() { 1068 return mHeader; 1069 } 1070 1071 @VisibleForTesting getContentView()1072 public View getContentView() { 1073 return isSearching() ? getSearchRecyclerView() : getAppsRecyclerViewContainer(); 1074 } 1075 1076 /** The current page visible in all apps. */ getCurrentPage()1077 public int getCurrentPage() { 1078 return isSearching() 1079 ? SEARCH 1080 : mViewPager == null ? AdapterHolder.MAIN : mViewPager.getNextPage(); 1081 } 1082 1083 /** 1084 * Adds an update listener to animator that adds springs to the animation. 1085 */ addSpringFromFlingUpdateListener(ValueAnimator animator, float velocity , float progress )1086 public void addSpringFromFlingUpdateListener(ValueAnimator animator, 1087 float velocity /* release velocity */, 1088 float progress /* portion of the distance to travel*/) { 1089 animator.addListener(new AnimatorListenerAdapter() { 1090 @Override 1091 public void onAnimationStart(Animator animator) { 1092 float distance = (1 - progress) * getHeight(); // px 1093 float settleVelocity = Math.min(0, distance 1094 / (AllAppsTransitionController.INTERP_COEFF * animator.getDuration()) 1095 + velocity); 1096 absorbSwipeUpVelocity(Math.max(1000, Math.abs( 1097 Math.round(settleVelocity * FLING_VELOCITY_MULTIPLIER)))); 1098 } 1099 }); 1100 } 1101 1102 /** Invoked when the container is pulled. */ onPull(float deltaDistance, float displacement)1103 public void onPull(float deltaDistance, float displacement) { 1104 absorbPullDeltaDistance(PULL_MULTIPLIER * deltaDistance, PULL_MULTIPLIER * displacement); 1105 // Current motion spec is to actually push and not pull 1106 // on this surface. However, until EdgeEffect.onPush (b/190612804) is 1107 // implemented at view level, we will simply pull 1108 } 1109 1110 @Override getDrawingRect(Rect outRect)1111 public void getDrawingRect(Rect outRect) { 1112 super.getDrawingRect(outRect); 1113 outRect.offset(0, (int) getTranslationY()); 1114 } 1115 1116 @Override setTranslationY(float translationY)1117 public void setTranslationY(float translationY) { 1118 super.setTranslationY(translationY); 1119 invalidateHeader(); 1120 } 1121 setScrimView(ScrimView scrimView)1122 public void setScrimView(ScrimView scrimView) { 1123 mScrimView = scrimView; 1124 } 1125 1126 @Override drawOnScrimWithScale(Canvas canvas, float scale)1127 public void drawOnScrimWithScale(Canvas canvas, float scale) { 1128 final boolean isTablet = mActivityContext.getDeviceProfile().isTablet; 1129 final View panel = mBottomSheetBackground; 1130 final float translationY = ((View) panel.getParent()).getTranslationY(); 1131 1132 final float horizontalScaleOffset = (1 - scale) * panel.getWidth() / 2; 1133 final float verticalScaleOffset = (1 - scale) * (panel.getHeight() - getHeight() / 2); 1134 1135 final float topNoScale = panel.getTop() + translationY; 1136 final float topWithScale = topNoScale + verticalScaleOffset; 1137 final float leftWithScale = panel.getLeft() + horizontalScaleOffset; 1138 final float rightWithScale = panel.getRight() - horizontalScaleOffset; 1139 // Draw full background panel for tablets. 1140 if (isTablet) { 1141 mHeaderPaint.setColor(mBottomSheetBackgroundColor); 1142 1143 mTmpRectF.set( 1144 leftWithScale, 1145 topWithScale, 1146 rightWithScale, 1147 panel.getBottom()); 1148 mTmpPath.reset(); 1149 mTmpPath.addRoundRect(mTmpRectF, mBottomSheetCornerRadii, Direction.CW); 1150 canvas.drawPath(mTmpPath, mHeaderPaint); 1151 } 1152 1153 if (DEBUG_HEADER_PROTECTION) { 1154 mHeaderPaint.setColor(Color.MAGENTA); 1155 mHeaderPaint.setAlpha(255); 1156 } else { 1157 mHeaderPaint.setColor(mHeaderColor); 1158 mHeaderPaint.setAlpha((int) (getAlpha() * Color.alpha(mHeaderColor))); 1159 } 1160 if (mHeaderPaint.getColor() == mScrimColor || mHeaderPaint.getColor() == 0) { 1161 return; 1162 } 1163 1164 // Draw header on background panel 1165 final float headerBottomNoScale = 1166 getHeaderBottom() + getVisibleContainerView().getPaddingTop(); 1167 final float headerHeightNoScale = headerBottomNoScale - topNoScale; 1168 final float headerBottomWithScaleOnTablet = topWithScale + headerHeightNoScale * scale; 1169 final float headerBottomOffset = (getVisibleContainerView().getHeight() * (1 - scale) / 2); 1170 final float headerBottomWithScaleOnPhone = headerBottomNoScale * scale + headerBottomOffset; 1171 final FloatingHeaderView headerView = getFloatingHeaderView(); 1172 if (isTablet) { 1173 // Start adding header protection if search bar or tabs will attach to the top. 1174 if (!FeatureFlags.ENABLE_FLOATING_SEARCH_BAR.get() || mUsingTabs) { 1175 mTmpRectF.set( 1176 leftWithScale, 1177 topWithScale, 1178 rightWithScale, 1179 headerBottomWithScaleOnTablet); 1180 mTmpPath.reset(); 1181 mTmpPath.addRoundRect(mTmpRectF, mBottomSheetCornerRadii, Direction.CW); 1182 canvas.drawPath(mTmpPath, mHeaderPaint); 1183 } 1184 } else { 1185 canvas.drawRect(0, 0, canvas.getWidth(), headerBottomWithScaleOnPhone, mHeaderPaint); 1186 } 1187 1188 // If tab exist (such as work profile), extend header with tab height 1189 final int tabsHeight = headerView.getPeripheralProtectionHeight(); 1190 if (mTabsProtectionAlpha > 0 && tabsHeight != 0) { 1191 if (DEBUG_HEADER_PROTECTION) { 1192 mHeaderPaint.setColor(Color.BLUE); 1193 mHeaderPaint.setAlpha(255); 1194 } else { 1195 mHeaderPaint.setAlpha((int) (getAlpha() * mTabsProtectionAlpha)); 1196 } 1197 float left = 0f; 1198 float right = canvas.getWidth(); 1199 if (isTablet) { 1200 left = mBottomSheetBackground.getLeft() + horizontalScaleOffset; 1201 right = mBottomSheetBackground.getRight() - horizontalScaleOffset; 1202 } 1203 1204 final float tabTopWithScale = isTablet 1205 ? headerBottomWithScaleOnTablet 1206 : headerBottomWithScaleOnPhone; 1207 final float tabBottomWithScale = tabTopWithScale + tabsHeight * scale; 1208 1209 canvas.drawRect( 1210 left, 1211 tabTopWithScale, 1212 right, 1213 tabBottomWithScale, 1214 mHeaderPaint); 1215 } 1216 } 1217 1218 /** 1219 * redraws header protection 1220 */ invalidateHeader()1221 public void invalidateHeader() { 1222 if (mScrimView != null) { 1223 mScrimView.invalidate(); 1224 } 1225 } 1226 1227 /** Returns the position of the bottom edge of the header */ getHeaderBottom()1228 public int getHeaderBottom() { 1229 int bottom = (int) getTranslationY() + mHeader.getClipTop(); 1230 if (isSearchBarOnBottom()) { 1231 if (mActivityContext.getDeviceProfile().isTablet) { 1232 return bottom + mBottomSheetBackground.getTop(); 1233 } 1234 return bottom; 1235 } 1236 return bottom + mHeader.getTop(); 1237 } 1238 1239 /** 1240 * Returns a view that denotes the visible part of all apps container view. 1241 */ getVisibleContainerView()1242 public View getVisibleContainerView() { 1243 return mActivityContext.getDeviceProfile().isTablet ? mBottomSheetBackground : this; 1244 } 1245 onInitializeRecyclerView(RecyclerView rv)1246 protected void onInitializeRecyclerView(RecyclerView rv) { 1247 rv.addOnScrollListener(mScrollListener); 1248 } 1249 1250 /** Holds a {@link BaseAllAppsAdapter} and related fields. */ 1251 public class AdapterHolder { 1252 public static final int MAIN = 0; 1253 public static final int WORK = 1; 1254 public static final int SEARCH = 2; 1255 1256 private final int mType; 1257 public final BaseAllAppsAdapter<T> mAdapter; 1258 final RecyclerView.LayoutManager mLayoutManager; 1259 final AlphabeticalAppsList<T> mAppsList; 1260 final Rect mPadding = new Rect(); 1261 AllAppsRecyclerView mRecyclerView; 1262 AdapterHolder(int type, AlphabeticalAppsList<T> appsList)1263 AdapterHolder(int type, AlphabeticalAppsList<T> appsList) { 1264 mType = type; 1265 mAppsList = appsList; 1266 mAdapter = createAdapter(mAppsList); 1267 mAppsList.setAdapter(mAdapter); 1268 mLayoutManager = mAdapter.getLayoutManager(); 1269 } 1270 setup(@onNull View rv, @Nullable Predicate<ItemInfo> matcher)1271 void setup(@NonNull View rv, @Nullable Predicate<ItemInfo> matcher) { 1272 mAppsList.updateItemFilter(matcher); 1273 mRecyclerView = (AllAppsRecyclerView) rv; 1274 mRecyclerView.bindFastScrollbar(mFastScroller); 1275 mRecyclerView.setEdgeEffectFactory(createEdgeEffectFactory()); 1276 mRecyclerView.setApps(mAppsList); 1277 mRecyclerView.setLayoutManager(mLayoutManager); 1278 mRecyclerView.setAdapter(mAdapter); 1279 mRecyclerView.setHasFixedSize(true); 1280 // No animations will occur when changes occur to the items in this RecyclerView. 1281 mRecyclerView.setItemAnimator(null); 1282 onInitializeRecyclerView(mRecyclerView); 1283 FocusedItemDecorator focusedItemDecorator = new FocusedItemDecorator(mRecyclerView); 1284 mRecyclerView.addItemDecoration(focusedItemDecorator); 1285 mAdapter.setIconFocusListener(focusedItemDecorator.getFocusListener()); 1286 applyPadding(); 1287 } 1288 applyPadding()1289 void applyPadding() { 1290 if (mRecyclerView != null) { 1291 int bottomOffset = 0; 1292 if (isWork() && mWorkManager.getWorkModeSwitch() != null) { 1293 bottomOffset = mInsets.bottom + mWorkManager.getWorkModeSwitch().getHeight(); 1294 } 1295 mRecyclerView.setPadding(mPadding.left, mPadding.top, mPadding.right, 1296 mPadding.bottom + bottomOffset); 1297 } 1298 } 1299 isWork()1300 private boolean isWork() { 1301 return mType == WORK; 1302 } 1303 isSearch()1304 private boolean isSearch() { 1305 return mType == SEARCH; 1306 } 1307 } 1308 } 1309