1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.launcher3.widget.picker; 17 18 import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions; 19 import static com.android.launcher3.Flags.enableUnfoldedTwoPanePicker; 20 import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.SEARCH; 21 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGETSTRAY_SEARCHED; 22 import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL; 23 24 import android.animation.Animator; 25 import android.content.Context; 26 import android.content.res.Resources; 27 import android.graphics.Rect; 28 import android.os.Bundle; 29 import android.os.Parcelable; 30 import android.os.Process; 31 import android.os.UserHandle; 32 import android.os.UserManager; 33 import android.util.AttributeSet; 34 import android.util.Pair; 35 import android.util.SparseArray; 36 import android.view.LayoutInflater; 37 import android.view.MotionEvent; 38 import android.view.View; 39 import android.view.ViewGroup; 40 import android.view.ViewParent; 41 import android.view.WindowInsets; 42 import android.view.WindowInsetsController; 43 import android.view.animation.AnimationUtils; 44 import android.view.animation.Interpolator; 45 import android.widget.Button; 46 import android.widget.LinearLayout; 47 import android.widget.TextView; 48 49 import androidx.annotation.NonNull; 50 import androidx.annotation.Nullable; 51 import androidx.annotation.Px; 52 import androidx.annotation.VisibleForTesting; 53 import androidx.recyclerview.widget.DefaultItemAnimator; 54 import androidx.recyclerview.widget.RecyclerView; 55 56 import com.android.launcher3.BaseActivity; 57 import com.android.launcher3.DeviceProfile; 58 import com.android.launcher3.LauncherAppState; 59 import com.android.launcher3.R; 60 import com.android.launcher3.anim.PendingAnimation; 61 import com.android.launcher3.compat.AccessibilityManagerCompat; 62 import com.android.launcher3.model.UserManagerState; 63 import com.android.launcher3.model.WidgetItem; 64 import com.android.launcher3.pm.UserCache; 65 import com.android.launcher3.views.RecyclerViewFastScroller; 66 import com.android.launcher3.views.SpringRelativeLayout; 67 import com.android.launcher3.views.StickyHeaderLayout; 68 import com.android.launcher3.widget.BaseWidgetSheet; 69 import com.android.launcher3.widget.WidgetCell; 70 import com.android.launcher3.widget.model.WidgetsListBaseEntry; 71 import com.android.launcher3.widget.picker.search.SearchModeListener; 72 import com.android.launcher3.widget.picker.search.WidgetsSearchBar; 73 import com.android.launcher3.workprofile.PersonalWorkPagedView; 74 import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener; 75 76 import java.util.ArrayList; 77 import java.util.HashMap; 78 import java.util.List; 79 import java.util.Map; 80 import java.util.function.Predicate; 81 import java.util.stream.IntStream; 82 83 /** 84 * Popup for showing the full list of available widgets 85 */ 86 public class WidgetsFullSheet extends BaseWidgetSheet 87 implements OnActivePageChangedListener, 88 WidgetsRecyclerView.HeaderViewDimensionsProvider, SearchModeListener { 89 90 private static final long FADE_IN_DURATION = 150; 91 92 // The widget recommendation table can easily take over the entire screen on devices with small 93 // resolution or landscape on phone. This ratio defines the max percentage of content area that 94 // the table can display with respect to bottom sheet's height. 95 private static final float RECOMMENDATION_TABLE_HEIGHT_RATIO = 0.45f; 96 private static final String RECOMMENDATIONS_SAVED_STATE_KEY = 97 "widgetsFullSheet:mRecommendationsCurrentPage"; 98 private static final String SUPER_SAVED_STATE_KEY = "widgetsFullSheet:superHierarchyState"; 99 private final UserCache mUserCache; 100 private final UserManagerState mUserManagerState = new UserManagerState(); 101 private final UserHandle mCurrentUser = Process.myUserHandle(); 102 private final Predicate<WidgetsListBaseEntry> mPrimaryWidgetsFilter = 103 entry -> mCurrentUser.equals(entry.mPkgItem.user); 104 private final Predicate<WidgetsListBaseEntry> mWorkWidgetsFilter; 105 protected final boolean mHasWorkProfile; 106 // Number of recommendations displayed 107 protected int mRecommendedWidgetsCount; 108 private List<WidgetItem> mRecommendedWidgets = new ArrayList<>(); 109 private Map<WidgetRecommendationCategory, List<WidgetItem>> mRecommendedWidgetsMap = 110 new HashMap<>(); 111 protected int mRecommendationsCurrentPage = 0; 112 protected final SparseArray<AdapterHolder> mAdapters = new SparseArray(); 113 114 private final OnAttachStateChangeListener mBindScrollbarInSearchMode = 115 new OnAttachStateChangeListener() { 116 @Override 117 public void onViewAttachedToWindow(View view) { 118 WidgetsRecyclerView searchRecyclerView = 119 mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView; 120 if (mIsInSearchMode && searchRecyclerView != null) { 121 searchRecyclerView.bindFastScrollbar(mFastScroller); 122 } 123 } 124 125 @Override 126 public void onViewDetachedFromWindow(View view) { 127 } 128 }; 129 130 @Px 131 private final int mTabsHeight; 132 133 @Nullable 134 private WidgetsRecyclerView mCurrentWidgetsRecyclerView; 135 @Nullable 136 private WidgetsRecyclerView mCurrentTouchEventRecyclerView; 137 @Nullable 138 PersonalWorkPagedView mViewPager; 139 private boolean mIsInSearchMode; 140 private boolean mIsNoWidgetsViewNeeded; 141 @Px 142 protected int mMaxSpanPerRow; 143 protected DeviceProfile mDeviceProfile; 144 145 protected TextView mNoWidgetsView; 146 protected StickyHeaderLayout mSearchScrollView; 147 protected WidgetRecommendationsView mWidgetRecommendationsView; 148 protected LinearLayout mWidgetRecommendationsContainer; 149 protected View mTabBar; 150 protected View mSearchBarContainer; 151 protected WidgetsSearchBar mSearchBar; 152 protected TextView mHeaderTitle; 153 protected RecyclerViewFastScroller mFastScroller; 154 protected int mBottomPadding; 155 WidgetsFullSheet(Context context, AttributeSet attrs, int defStyleAttr)156 public WidgetsFullSheet(Context context, AttributeSet attrs, int defStyleAttr) { 157 super(context, attrs, defStyleAttr); 158 mDeviceProfile = mActivityContext.getDeviceProfile(); 159 mUserCache = UserCache.INSTANCE.get(context); 160 mHasWorkProfile = mUserCache.getUserProfiles() 161 .stream() 162 .anyMatch(user -> mUserCache.getUserInfo(user).isWork()); 163 mWorkWidgetsFilter = entry -> mHasWorkProfile 164 && mUserCache.getUserInfo(entry.mPkgItem.user).isWork() 165 && !mUserManagerState.isUserQuiet(entry.mPkgItem.user); 166 mAdapters.put(AdapterHolder.PRIMARY, new AdapterHolder(AdapterHolder.PRIMARY)); 167 mAdapters.put(AdapterHolder.WORK, new AdapterHolder(AdapterHolder.WORK)); 168 mAdapters.put(AdapterHolder.SEARCH, new AdapterHolder(AdapterHolder.SEARCH)); 169 170 Resources resources = getResources(); 171 mUserManagerState.init(UserCache.INSTANCE.get(context), 172 context.getSystemService(UserManager.class)); 173 mTabsHeight = mHasWorkProfile 174 ? resources.getDimensionPixelSize(R.dimen.all_apps_header_pill_height) 175 : 0; 176 } 177 WidgetsFullSheet(Context context, AttributeSet attrs)178 public WidgetsFullSheet(Context context, AttributeSet attrs) { 179 this(context, attrs, 0); 180 } 181 182 @Override onFinishInflate()183 protected void onFinishInflate() { 184 super.onFinishInflate(); 185 186 mContent = findViewById(R.id.container); 187 setContentBackgroundWithParent(getContext().getDrawable(R.drawable.bg_widgets_full_sheet), 188 mContent); 189 mContent.setOutlineProvider(mViewOutlineProvider); 190 mContent.setClipToOutline(true); 191 setupSheet(); 192 } 193 setupSheet()194 protected void setupSheet() { 195 LayoutInflater layoutInflater = LayoutInflater.from(getContext()); 196 int contentLayoutRes = mHasWorkProfile ? R.layout.widgets_full_sheet_paged_view 197 : R.layout.widgets_full_sheet_recyclerview; 198 layoutInflater.inflate(contentLayoutRes, mContent, true); 199 200 setupViews(); 201 202 mWidgetRecommendationsContainer = mSearchScrollView.findViewById( 203 R.id.widget_recommendations_container); 204 mWidgetRecommendationsView = mSearchScrollView.findViewById( 205 R.id.widget_recommendations_view); 206 // To save the currently displayed page, so that, it can be requested when rebinding 207 // recommendations with different size constraints. 208 mWidgetRecommendationsView.addPageSwitchListener( 209 newPage -> mRecommendationsCurrentPage = newPage); 210 mWidgetRecommendationsView.initParentViews(mWidgetRecommendationsContainer); 211 mWidgetRecommendationsView.setWidgetCellLongClickListener(this); 212 mWidgetRecommendationsView.setWidgetCellOnClickListener(this); 213 214 mHeaderTitle = mSearchScrollView.findViewById(R.id.title); 215 216 onWidgetsBound(); 217 } 218 setupViews()219 protected void setupViews() { 220 mSearchScrollView = findViewById(R.id.search_and_recommendations_container); 221 mSearchScrollView.setCurrentRecyclerView(findViewById(R.id.primary_widgets_list_view)); 222 mNoWidgetsView = findViewById(R.id.no_widgets_text); 223 mFastScroller = findViewById(R.id.fast_scroller); 224 mFastScroller.setPopupView(findViewById(R.id.fast_scroller_popup)); 225 mAdapters.get(AdapterHolder.PRIMARY).setup(findViewById(R.id.primary_widgets_list_view)); 226 mAdapters.get(AdapterHolder.SEARCH).setup(findViewById(R.id.search_widgets_list_view)); 227 if (mHasWorkProfile) { 228 mViewPager = findViewById(R.id.widgets_view_pager); 229 mViewPager.setOutlineProvider(mViewOutlineProvider); 230 mViewPager.setClipToOutline(true); 231 mViewPager.setClipChildren(false); 232 mViewPager.initParentViews(this); 233 mViewPager.getPageIndicator().setOnActivePageChangedListener(this); 234 mViewPager.getPageIndicator().setActiveMarker(AdapterHolder.PRIMARY); 235 findViewById(R.id.tab_personal) 236 .setOnClickListener((View view) -> mViewPager.snapToPage(0)); 237 findViewById(R.id.tab_work) 238 .setOnClickListener((View view) -> mViewPager.snapToPage(1)); 239 mAdapters.get(AdapterHolder.WORK).setup(findViewById(R.id.work_widgets_list_view)); 240 setDeviceManagementResources(); 241 } else { 242 mViewPager = null; 243 } 244 245 mTabBar = mSearchScrollView.findViewById(R.id.tabs); 246 mSearchBarContainer = mSearchScrollView.findViewById(R.id.search_bar_container); 247 mSearchBar = mSearchScrollView.findViewById(R.id.widgets_search_bar); 248 249 mSearchBar.initialize( 250 mActivityContext.getPopupDataProvider(), /* searchModeListener= */ this); 251 } 252 setDeviceManagementResources()253 private void setDeviceManagementResources() { 254 if (mActivityContext.getStringCache() != null) { 255 Button personalTab = findViewById(R.id.tab_personal); 256 personalTab.setText(mActivityContext.getStringCache().widgetsPersonalTab); 257 258 Button workTab = findViewById(R.id.tab_work); 259 workTab.setText(mActivityContext.getStringCache().widgetsWorkTab); 260 } 261 } 262 263 @Override onActivePageChanged(int currentActivePage)264 public void onActivePageChanged(int currentActivePage) { 265 AdapterHolder currentAdapterHolder = mAdapters.get(currentActivePage); 266 WidgetsRecyclerView currentRecyclerView = 267 mAdapters.get(currentActivePage).mWidgetsRecyclerView; 268 269 updateRecyclerViewVisibility(currentAdapterHolder); 270 attachScrollbarToRecyclerView(currentRecyclerView); 271 } 272 attachScrollbarToRecyclerView(WidgetsRecyclerView recyclerView)273 private void attachScrollbarToRecyclerView(WidgetsRecyclerView recyclerView) { 274 recyclerView.bindFastScrollbar(mFastScroller); 275 if (mCurrentWidgetsRecyclerView != recyclerView) { 276 // Only reset the scroll position & expanded apps if the currently shown recycler view 277 // has been updated. 278 reset(); 279 resetExpandedHeaders(); 280 mCurrentWidgetsRecyclerView = recyclerView; 281 mSearchScrollView.setCurrentRecyclerView(recyclerView); 282 } 283 } 284 updateRecyclerViewVisibility(AdapterHolder adapterHolder)285 protected void updateRecyclerViewVisibility(AdapterHolder adapterHolder) { 286 // The first item is always an empty space entry. Look for any more items. 287 boolean isWidgetAvailable = adapterHolder.mWidgetsListAdapter.hasVisibleEntries(); 288 adapterHolder.mWidgetsRecyclerView.setVisibility(isWidgetAvailable ? VISIBLE : GONE); 289 290 if (adapterHolder.mAdapterType == AdapterHolder.SEARCH) { 291 mNoWidgetsView.setText(R.string.no_search_results); 292 } else if (adapterHolder.mAdapterType == AdapterHolder.WORK 293 && mUserCache.getUserProfiles().stream() 294 .filter(userHandle -> mUserCache.getUserInfo(userHandle).isWork()) 295 .anyMatch(mUserManagerState::isUserQuiet) 296 && mActivityContext.getStringCache() != null) { 297 mNoWidgetsView.setText(mActivityContext.getStringCache().workProfilePausedTitle); 298 } else { 299 mNoWidgetsView.setText(R.string.no_widgets_available); 300 } 301 mNoWidgetsView.setVisibility(isWidgetAvailable ? GONE : VISIBLE); 302 } 303 reset()304 private void reset() { 305 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView.scrollToTop(); 306 if (mHasWorkProfile) { 307 mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView.scrollToTop(); 308 } 309 mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.scrollToTop(); 310 mSearchScrollView.reset(/* animate= */ true); 311 } 312 313 @VisibleForTesting getRecyclerView()314 public WidgetsRecyclerView getRecyclerView() { 315 if (mIsInSearchMode) { 316 return mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView; 317 } 318 if (!mHasWorkProfile || mViewPager.getCurrentPage() == AdapterHolder.PRIMARY) { 319 return mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView; 320 } 321 return mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView; 322 } 323 324 @Override getAccessibilityTarget()325 protected Pair<View, String> getAccessibilityTarget() { 326 return Pair.create(getRecyclerView(), getContext().getString( 327 mIsOpen ? R.string.widgets_list : R.string.widgets_list_closed)); 328 } 329 330 @Override onAttachedToWindow()331 protected void onAttachedToWindow() { 332 super.onAttachedToWindow(); 333 onWidgetsBound(); 334 } 335 336 @Override onDetachedFromWindow()337 protected void onDetachedFromWindow() { 338 super.onDetachedFromWindow(); 339 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView 340 .removeOnAttachStateChangeListener(mBindScrollbarInSearchMode); 341 if (mHasWorkProfile) { 342 mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView 343 .removeOnAttachStateChangeListener(mBindScrollbarInSearchMode); 344 } 345 } 346 347 @Override setInsets(Rect insets)348 public void setInsets(Rect insets) { 349 super.setInsets(insets); 350 mBottomPadding = Math.max(insets.bottom, mNavBarScrimHeight); 351 setBottomPadding(mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView, mBottomPadding); 352 setBottomPadding(mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView, mBottomPadding); 353 if (mHasWorkProfile) { 354 setBottomPadding(mAdapters.get(AdapterHolder.WORK) 355 .mWidgetsRecyclerView, mBottomPadding); 356 } 357 ((MarginLayoutParams) mNoWidgetsView.getLayoutParams()).bottomMargin = mBottomPadding; 358 359 if (mBottomPadding > 0) { 360 setupNavBarColor(); 361 } else { 362 clearNavBarColor(); 363 } 364 365 requestLayout(); 366 } 367 368 @Override onApplyWindowInsets(WindowInsets insets)369 public WindowInsets onApplyWindowInsets(WindowInsets insets) { 370 WindowInsets w = super.onApplyWindowInsets(insets); 371 if (mInsets.bottom != mNavBarScrimHeight) { 372 setInsets(mInsets); 373 } 374 return w; 375 } 376 setBottomPadding(RecyclerView recyclerView, int bottomPadding)377 private void setBottomPadding(RecyclerView recyclerView, int bottomPadding) { 378 recyclerView.setPadding( 379 recyclerView.getPaddingLeft(), 380 recyclerView.getPaddingTop(), 381 recyclerView.getPaddingRight(), 382 bottomPadding); 383 } 384 385 @Override onContentHorizontalMarginChanged(int contentHorizontalMarginInPx)386 protected void onContentHorizontalMarginChanged(int contentHorizontalMarginInPx) { 387 setContentViewChildHorizontalMargin(mSearchScrollView, contentHorizontalMarginInPx); 388 if (mViewPager == null) { 389 setContentViewChildHorizontalPadding( 390 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView, 391 contentHorizontalMarginInPx); 392 } else { 393 setContentViewChildHorizontalPadding( 394 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView, 395 contentHorizontalMarginInPx); 396 setContentViewChildHorizontalPadding( 397 mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView, 398 contentHorizontalMarginInPx); 399 } 400 setContentViewChildHorizontalPadding( 401 mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView, 402 contentHorizontalMarginInPx); 403 } 404 setContentViewChildHorizontalMargin(View view, int horizontalMarginInPx)405 private static void setContentViewChildHorizontalMargin(View view, int horizontalMarginInPx) { 406 ViewGroup.MarginLayoutParams layoutParams = 407 (ViewGroup.MarginLayoutParams) view.getLayoutParams(); 408 layoutParams.setMarginStart(horizontalMarginInPx); 409 layoutParams.setMarginEnd(horizontalMarginInPx); 410 } 411 setContentViewChildHorizontalPadding(View view, int horizontalPaddingInPx)412 private static void setContentViewChildHorizontalPadding(View view, int horizontalPaddingInPx) { 413 view.setPadding(horizontalPaddingInPx, view.getPaddingTop(), horizontalPaddingInPx, 414 view.getPaddingBottom()); 415 } 416 417 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)418 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 419 doMeasure(widthMeasureSpec, heightMeasureSpec); 420 421 if (updateMaxSpansPerRow()) { 422 doMeasure(widthMeasureSpec, heightMeasureSpec); 423 } 424 } 425 426 /** Returns {@code true} if the max spans have been updated. */ updateMaxSpansPerRow()427 private boolean updateMaxSpansPerRow() { 428 if (getMeasuredWidth() == 0) return false; 429 430 @Px int maxHorizontalSpan = getContentView().getMeasuredWidth() 431 - (2 * mContentHorizontalMargin); 432 if (mMaxSpanPerRow != maxHorizontalSpan) { 433 mMaxSpanPerRow = maxHorizontalSpan; 434 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.setMaxHorizontalSpansPxPerRow( 435 maxHorizontalSpan); 436 mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.setMaxHorizontalSpansPxPerRow( 437 maxHorizontalSpan); 438 if (mHasWorkProfile) { 439 mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.setMaxHorizontalSpansPxPerRow( 440 maxHorizontalSpan); 441 } 442 onRecommendedWidgetsBound(); 443 return true; 444 } 445 return false; 446 } 447 getContentView()448 protected View getContentView() { 449 return mHasWorkProfile 450 ? mViewPager 451 : mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView; 452 } 453 454 @Override onLayout(boolean changed, int l, int t, int r, int b)455 protected void onLayout(boolean changed, int l, int t, int r, int b) { 456 int width = r - l; 457 int height = b - t; 458 459 // Content is laid out as center bottom aligned 460 int contentWidth = mContent.getMeasuredWidth(); 461 int contentLeft = (width - contentWidth - mInsets.left - mInsets.right) / 2 + mInsets.left; 462 mContent.layout(contentLeft, height - mContent.getMeasuredHeight(), 463 contentLeft + contentWidth, height); 464 465 setTranslationShift(mTranslationShift); 466 } 467 468 @Override onWidgetsBound()469 public void onWidgetsBound() { 470 if (mIsInSearchMode) { 471 return; 472 } 473 List<WidgetsListBaseEntry> allWidgets = 474 mActivityContext.getPopupDataProvider().getAllWidgets(); 475 476 AdapterHolder primaryUserAdapterHolder = mAdapters.get(AdapterHolder.PRIMARY); 477 primaryUserAdapterHolder.mWidgetsListAdapter.setWidgets(allWidgets); 478 479 if (mHasWorkProfile) { 480 mViewPager.setVisibility(VISIBLE); 481 mTabBar.setVisibility(VISIBLE); 482 AdapterHolder workUserAdapterHolder = mAdapters.get(AdapterHolder.WORK); 483 workUserAdapterHolder.mWidgetsListAdapter.setWidgets(allWidgets); 484 onActivePageChanged(mViewPager.getCurrentPage()); 485 } else { 486 onActivePageChanged(0); 487 } 488 // Update recommended widgets section so that it occupies appropriate space on screen to 489 // leave enough space for presence/absence of mNoWidgetsView. 490 boolean isNoWidgetsViewNeeded = 491 !mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.hasVisibleEntries() 492 || (mHasWorkProfile && mAdapters.get(AdapterHolder.WORK) 493 .mWidgetsListAdapter.hasVisibleEntries()); 494 if (mIsNoWidgetsViewNeeded != isNoWidgetsViewNeeded) { 495 mIsNoWidgetsViewNeeded = isNoWidgetsViewNeeded; 496 onRecommendedWidgetsBound(); 497 } 498 } 499 500 @Override enterSearchMode(boolean shouldLog)501 public void enterSearchMode(boolean shouldLog) { 502 if (mIsInSearchMode) return; 503 setViewVisibilityBasedOnSearch(/*isInSearchMode= */ true); 504 attachScrollbarToRecyclerView(mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView); 505 if (shouldLog) { 506 mActivityContext.getStatsLogManager().logger().log(LAUNCHER_WIDGETSTRAY_SEARCHED); 507 } 508 } 509 510 @Override exitSearchMode()511 public void exitSearchMode() { 512 if (!mIsInSearchMode) return; 513 onSearchResults(new ArrayList<>()); 514 WidgetsRecyclerView searchRecyclerView = mAdapters.get( 515 AdapterHolder.SEARCH).mWidgetsRecyclerView; 516 // Remove all views when exiting the search mode; this prevents animating from stale results 517 // to new ones the next time we enter search mode. By the time recycler view is hidden, 518 // layout may not have happened to clear up existing results. So, instead of waiting for it 519 // to happen, we clear the views here. 520 searchRecyclerView.swapAdapter( 521 searchRecyclerView.getAdapter(), /*removeAndRecycleExistingViews=*/ true); 522 setViewVisibilityBasedOnSearch(/*isInSearchMode=*/ false); 523 if (mHasWorkProfile) { 524 mViewPager.snapToPage(AdapterHolder.PRIMARY); 525 } 526 attachScrollbarToRecyclerView(mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView); 527 } 528 529 @Override onSearchResults(List<WidgetsListBaseEntry> entries)530 public void onSearchResults(List<WidgetsListBaseEntry> entries) { 531 mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.setWidgetsOnSearch(entries); 532 updateRecyclerViewVisibility(mAdapters.get(AdapterHolder.SEARCH)); 533 } 534 setViewVisibilityBasedOnSearch(boolean isInSearchMode)535 protected void setViewVisibilityBasedOnSearch(boolean isInSearchMode) { 536 mIsInSearchMode = isInSearchMode; 537 if (isInSearchMode) { 538 mWidgetRecommendationsContainer.setVisibility(GONE); 539 if (mHasWorkProfile) { 540 mViewPager.setVisibility(GONE); 541 mTabBar.setVisibility(GONE); 542 } else { 543 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView.setVisibility(GONE); 544 } 545 updateRecyclerViewVisibility(mAdapters.get(AdapterHolder.SEARCH)); 546 // Hide no search results view to prevent it from flashing on enter search. 547 mNoWidgetsView.setVisibility(GONE); 548 } else { 549 mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.setVisibility(GONE); 550 // Visibility of recommended widgets, recycler views and headers are handled in methods 551 // below. 552 onRecommendedWidgetsBound(); 553 onWidgetsBound(); 554 } 555 } 556 resetExpandedHeaders()557 protected void resetExpandedHeaders() { 558 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.resetExpandedHeader(); 559 mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.resetExpandedHeader(); 560 } 561 562 @Override onRecommendedWidgetsBound()563 public void onRecommendedWidgetsBound() { 564 if (mIsInSearchMode) { 565 return; 566 } 567 568 if (enableCategorizedWidgetSuggestions()) { 569 // We avoid applying new recommendations when some are already displayed. 570 if (mRecommendedWidgetsMap.isEmpty()) { 571 mRecommendedWidgetsMap = 572 mActivityContext.getPopupDataProvider().getCategorizedRecommendedWidgets(); 573 } 574 mRecommendedWidgetsCount = mWidgetRecommendationsView.setRecommendations( 575 mRecommendedWidgetsMap, 576 mDeviceProfile, 577 /* availableHeight= */ getMaxAvailableHeightForRecommendations(), 578 /* availableWidth= */ mMaxSpanPerRow, 579 /* cellPadding= */ mWidgetCellHorizontalPadding, 580 /* requestedPage= */ mRecommendationsCurrentPage 581 ); 582 } else { 583 if (mRecommendedWidgets.isEmpty()) { 584 mRecommendedWidgets = 585 mActivityContext.getPopupDataProvider().getRecommendedWidgets(); 586 } 587 mRecommendedWidgetsCount = mWidgetRecommendationsView.setRecommendations( 588 mRecommendedWidgets, 589 mDeviceProfile, 590 /* availableHeight= */ getMaxAvailableHeightForRecommendations(), 591 /* availableWidth= */ mMaxSpanPerRow, 592 /* cellPadding= */ mWidgetCellHorizontalPadding 593 ); 594 } 595 mWidgetRecommendationsContainer.setVisibility( 596 mRecommendedWidgetsCount > 0 ? VISIBLE : GONE); 597 } 598 599 @Px getMaxAvailableHeightForRecommendations()600 protected float getMaxAvailableHeightForRecommendations() { 601 // There isn't enough space to show recommendations in landscape orientation on phones with 602 // a full sheet design. Tablets use a two pane picker. 603 if (mDeviceProfile.isLandscape) { 604 return 0f; 605 } 606 607 return (mDeviceProfile.heightPx - mDeviceProfile.bottomSheetTopPadding) 608 * RECOMMENDATION_TABLE_HEIGHT_RATIO; 609 } 610 611 /** b/209579563: "Widgets" header should be focused first. */ 612 @Override getAccessibilityInitialFocusView()613 protected View getAccessibilityInitialFocusView() { 614 return mHeaderTitle; 615 } 616 open(boolean animate)617 private void open(boolean animate) { 618 if (animate) { 619 if (getPopupContainer().getInsets().bottom > 0) { 620 mContent.setAlpha(0); 621 } 622 setUpOpenAnimation(mActivityContext.getDeviceProfile().bottomSheetOpenDuration); 623 Animator animator = mOpenCloseAnimation.getAnimationPlayer(); 624 animator.setInterpolator(AnimationUtils.loadInterpolator( 625 getContext(), android.R.interpolator.linear_out_slow_in)); 626 post(() -> { 627 animator.setDuration(mActivityContext.getDeviceProfile().bottomSheetOpenDuration) 628 .start(); 629 mContent.animate().alpha(1).setDuration(FADE_IN_DURATION); 630 }); 631 } else { 632 setTranslationShift(TRANSLATION_SHIFT_OPENED); 633 post(this::announceAccessibilityChanges); 634 } 635 } 636 637 @Override handleClose(boolean animate)638 protected void handleClose(boolean animate) { 639 handleClose(animate, mActivityContext.getDeviceProfile().bottomSheetCloseDuration); 640 } 641 642 @Override isOfType(int type)643 protected boolean isOfType(int type) { 644 return (type & TYPE_WIDGETS_FULL_SHEET) != 0; 645 } 646 647 @Override onControllerInterceptTouchEvent(MotionEvent ev)648 public boolean onControllerInterceptTouchEvent(MotionEvent ev) { 649 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 650 mNoIntercept = shouldScroll(ev); 651 if (mSearchBar.isSearchBarFocused() 652 && !getPopupContainer().isEventOverView(mSearchBarContainer, ev)) { 653 mSearchBar.clearSearchBarFocus(); 654 } 655 } 656 657 return super.onControllerInterceptTouchEvent(ev); 658 } 659 shouldScroll(MotionEvent ev)660 protected boolean shouldScroll(MotionEvent ev) { 661 boolean intercept = false; 662 WidgetsRecyclerView recyclerView = getRecyclerView(); 663 RecyclerViewFastScroller scroller = recyclerView.getScrollbar(); 664 // Disable swipe down when recycler view is scrolling 665 if (scroller.getThumbOffsetY() >= 0 && getPopupContainer().isEventOverView(scroller, ev)) { 666 intercept = true; 667 } else if (getPopupContainer().isEventOverView(recyclerView, ev)) { 668 intercept = !recyclerView.shouldContainerScroll(ev, getPopupContainer()); 669 } 670 return intercept; 671 } 672 673 /** Shows the {@link WidgetsFullSheet} on the launcher. */ show(BaseActivity activity, boolean animate)674 public static WidgetsFullSheet show(BaseActivity activity, boolean animate) { 675 WidgetsFullSheet sheet = (WidgetsFullSheet) activity.getLayoutInflater().inflate( 676 getWidgetSheetId(activity), 677 activity.getDragLayer(), 678 false); 679 sheet.attachToContainer(); 680 sheet.mIsOpen = true; 681 sheet.open(animate); 682 return sheet; 683 } 684 685 @Override saveHierarchyState(SparseArray<Parcelable> sparseArray)686 public void saveHierarchyState(SparseArray<Parcelable> sparseArray) { 687 Bundle bundle = new Bundle(); 688 // With widget picker open, when we open shade to switch theme, Launcher re-creates the 689 // picker and calls save/restore hierarchy state. We save the state of recommendations 690 // across those updates. 691 bundle.putInt(RECOMMENDATIONS_SAVED_STATE_KEY, mRecommendationsCurrentPage); 692 mWidgetRecommendationsView.saveState(bundle); 693 SparseArray<Parcelable> superState = new SparseArray<>(); 694 super.saveHierarchyState(superState); 695 bundle.putSparseParcelableArray(SUPER_SAVED_STATE_KEY, superState); 696 sparseArray.put(0, bundle); 697 } 698 699 @Override restoreHierarchyState(SparseArray<Parcelable> sparseArray)700 public void restoreHierarchyState(SparseArray<Parcelable> sparseArray) { 701 Bundle state = (Bundle) sparseArray.get(0); 702 mRecommendationsCurrentPage = state.getInt( 703 RECOMMENDATIONS_SAVED_STATE_KEY, /*defaultValue=*/0); 704 mWidgetRecommendationsView.restoreState(state); 705 super.restoreHierarchyState(state.getSparseParcelableArray(SUPER_SAVED_STATE_KEY)); 706 } 707 getWidgetSheetId(BaseActivity activity)708 private static int getWidgetSheetId(BaseActivity activity) { 709 boolean isTwoPane = (activity.getDeviceProfile().isTablet 710 // Enables two pane picker for tablets in all orientations when the 711 // enableCategorizedWidgetSuggestions flag is on. 712 && (activity.getDeviceProfile().isLandscape || enableCategorizedWidgetSuggestions()) 713 && !activity.getDeviceProfile().isTwoPanels) 714 // Enables two pane picker for unfolded foldables if the flag is on. 715 || (activity.getDeviceProfile().isTwoPanels && enableUnfoldedTwoPanePicker()); 716 717 return isTwoPane ? R.layout.widgets_two_pane_sheet : R.layout.widgets_full_sheet; 718 } 719 720 @Override onInterceptTouchEvent(MotionEvent ev)721 public boolean onInterceptTouchEvent(MotionEvent ev) { 722 return isTouchOnScrollbar(ev) || super.onInterceptTouchEvent(ev); 723 } 724 725 @Override onTouchEvent(MotionEvent ev)726 public boolean onTouchEvent(MotionEvent ev) { 727 return maybeHandleTouchEvent(ev) || super.onTouchEvent(ev); 728 } 729 maybeHandleTouchEvent(MotionEvent ev)730 private boolean maybeHandleTouchEvent(MotionEvent ev) { 731 boolean isEventHandled = false; 732 733 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 734 mCurrentTouchEventRecyclerView = isTouchOnScrollbar(ev) ? getRecyclerView() : null; 735 } 736 737 if (mCurrentTouchEventRecyclerView != null) { 738 final float offsetX = mContent.getX(); 739 final float offsetY = mContent.getY(); 740 ev.offsetLocation(-offsetX, -offsetY); 741 isEventHandled = mCurrentTouchEventRecyclerView.dispatchTouchEvent(ev); 742 ev.offsetLocation(offsetX, offsetY); 743 } 744 745 if (ev.getAction() == MotionEvent.ACTION_UP 746 || ev.getAction() == MotionEvent.ACTION_CANCEL) { 747 mCurrentTouchEventRecyclerView = null; 748 } 749 750 return isEventHandled; 751 } 752 isTouchOnScrollbar(MotionEvent ev)753 private boolean isTouchOnScrollbar(MotionEvent ev) { 754 final float offsetX = mContent.getX(); 755 final float offsetY = mContent.getY(); 756 WidgetsRecyclerView rv = getRecyclerView(); 757 758 ev.offsetLocation(-offsetX, -offsetY); 759 boolean isOnScrollBar = rv != null && rv.getScrollbar() != null && rv.isHitOnScrollBar(ev); 760 ev.offsetLocation(offsetX, offsetY); 761 762 return isOnScrollBar; 763 } 764 765 /** Gets the {@link WidgetsRecyclerView} which shows all widgets in {@link WidgetsFullSheet}. */ 766 @VisibleForTesting getWidgetsView(BaseActivity launcher)767 public static WidgetsRecyclerView getWidgetsView(BaseActivity launcher) { 768 return launcher.findViewById(R.id.primary_widgets_list_view); 769 } 770 771 @Override addHintCloseAnim( float distanceToMove, Interpolator interpolator, PendingAnimation target)772 public void addHintCloseAnim( 773 float distanceToMove, Interpolator interpolator, PendingAnimation target) { 774 target.addAnimatedFloat(mSwipeToDismissProgress, 0f, 1f, interpolator); 775 } 776 777 @Override onCloseComplete()778 protected void onCloseComplete() { 779 super.onCloseComplete(); 780 AccessibilityManagerCompat.sendStateEventToTest(getContext(), NORMAL_STATE_ORDINAL); 781 } 782 783 @Override getHeaderViewHeight()784 public int getHeaderViewHeight() { 785 return measureHeightWithVerticalMargins(mHeaderTitle) 786 + measureHeightWithVerticalMargins(mSearchBarContainer); 787 } 788 789 /** private the height, in pixel, + the vertical margins of a given view. */ measureHeightWithVerticalMargins(View view)790 protected static int measureHeightWithVerticalMargins(View view) { 791 if (view == null || view.getVisibility() != VISIBLE) { 792 return 0; 793 } 794 MarginLayoutParams marginLayoutParams = (MarginLayoutParams) view.getLayoutParams(); 795 return view.getMeasuredHeight() + marginLayoutParams.bottomMargin 796 + marginLayoutParams.topMargin; 797 } 798 getCurrentAdapterHolderType()799 private int getCurrentAdapterHolderType() { 800 if (mIsInSearchMode) { 801 return SEARCH; 802 } else if (mViewPager != null) { 803 return mViewPager.getCurrentPage(); 804 } else { 805 return AdapterHolder.PRIMARY; 806 } 807 } 808 restorePreviousAdapterHolderType(int previousAdapterHolderType)809 private void restorePreviousAdapterHolderType(int previousAdapterHolderType) { 810 if (previousAdapterHolderType == AdapterHolder.WORK && mViewPager != null) { 811 mViewPager.setCurrentPage(previousAdapterHolderType); 812 } else if (previousAdapterHolderType == AdapterHolder.SEARCH) { 813 enterSearchMode(false); 814 } 815 } 816 817 @Override onDeviceProfileChanged(DeviceProfile dp)818 public void onDeviceProfileChanged(DeviceProfile dp) { 819 super.onDeviceProfileChanged(dp); 820 821 if (shouldRecreateLayout(/*oldDp=*/ mDeviceProfile, /*newDp=*/ dp)) { 822 SparseArray<Parcelable> widgetsState = new SparseArray<>(); 823 saveHierarchyState(widgetsState); 824 handleClose(false); 825 WidgetsFullSheet sheet = show(BaseActivity.fromContext(getContext()), false); 826 sheet.restoreRecommendations(mRecommendedWidgets, mRecommendedWidgetsMap); 827 sheet.restoreHierarchyState(widgetsState); 828 sheet.restorePreviousAdapterHolderType(getCurrentAdapterHolderType()); 829 } else if (!isTwoPane()) { 830 reset(); 831 resetExpandedHeaders(); 832 } 833 834 mDeviceProfile = dp; 835 } 836 restoreRecommendations(List<WidgetItem> recommendedWidgets, Map<WidgetRecommendationCategory, List<WidgetItem>> recommendedWidgetsMap)837 private void restoreRecommendations(List<WidgetItem> recommendedWidgets, 838 Map<WidgetRecommendationCategory, List<WidgetItem>> recommendedWidgetsMap) { 839 mRecommendedWidgets = recommendedWidgets; 840 mRecommendedWidgetsMap = recommendedWidgetsMap; 841 } 842 843 /** 844 * Indicates if layout should be re-created on device profile change - so that a different 845 * layout can be displayed. 846 */ shouldRecreateLayout(DeviceProfile oldDp, DeviceProfile newDp)847 private static boolean shouldRecreateLayout(DeviceProfile oldDp, DeviceProfile newDp) { 848 // When folding/unfolding the foldables, we need to switch between the regular widget picker 849 // and the two pane picker, so we rebuild the picker with the correct layout. 850 boolean isFoldUnFold = 851 oldDp.isTwoPanels != newDp.isTwoPanels && enableUnfoldedTwoPanePicker(); 852 // In tablets, on orientation change we switch between single and two pane picker unless the 853 // categorized suggestions flag was on. With the categorized suggestions feature, we use a 854 // two pane picker across all orientations. 855 boolean useDifferentLayoutOnOrientationChange = 856 (!enableCategorizedWidgetSuggestions() && (newDp.isTablet && !newDp.isTwoPanels 857 && oldDp.isLandscape != newDp.isLandscape)); 858 859 return isFoldUnFold || useDifferentLayoutOnOrientationChange; 860 } 861 862 /** 863 * In widget search mode, we should scale down content inside widget bottom sheet, rather 864 * than the whole bottom sheet, to indicate we will navigate back within the widget 865 * bottom sheet. 866 */ 867 @Override shouldAnimateContentViewInBackSwipe()868 public boolean shouldAnimateContentViewInBackSwipe() { 869 return mIsInSearchMode; 870 } 871 872 @Override onBackInvoked()873 public void onBackInvoked() { 874 if (mIsInSearchMode) { 875 mSearchBar.reset(); 876 // Posting animation to next frame will let widget sheet finish updating UI first, and 877 // make animation smoother. 878 post(this::animateSwipeToDismissProgressToStart); 879 } else { 880 super.onBackInvoked(); 881 } 882 } 883 884 @Override onDragStart(boolean start, float startDisplacement)885 public void onDragStart(boolean start, float startDisplacement) { 886 super.onDragStart(start, startDisplacement); 887 WindowInsetsController insetsController = getWindowInsetsController(); 888 if (insetsController != null) { 889 insetsController.hide(WindowInsets.Type.ime()); 890 } 891 } 892 893 @Nullable getViewToShowEducationTip()894 private View getViewToShowEducationTip() { 895 if (mWidgetRecommendationsContainer.getVisibility() == VISIBLE) { 896 return mWidgetRecommendationsView.getViewForEducationTip(); 897 } 898 899 AdapterHolder adapterHolder = mAdapters.get(mIsInSearchMode 900 ? AdapterHolder.SEARCH 901 : mViewPager == null 902 ? AdapterHolder.PRIMARY 903 : mViewPager.getCurrentPage()); 904 WidgetsRowViewHolder viewHolderForTip = 905 (WidgetsRowViewHolder) IntStream.range( 906 0, adapterHolder.mWidgetsListAdapter.getItemCount()) 907 .mapToObj(adapterHolder.mWidgetsRecyclerView:: 908 findViewHolderForAdapterPosition) 909 .filter(viewHolder -> viewHolder instanceof WidgetsRowViewHolder) 910 .findFirst() 911 .orElse(null); 912 if (viewHolderForTip != null) { 913 return ((ViewGroup) viewHolderForTip.tableContainer.getChildAt(0)).getChildAt(0); 914 } 915 916 return null; 917 } 918 isTwoPane()919 protected boolean isTwoPane() { 920 return false; 921 } 922 923 /** Gets the sheet for widget picker, which is used for testing. */ 924 @VisibleForTesting getSheet()925 public View getSheet() { 926 return mContent; 927 } 928 929 /** Opens the first header in widget picker and scrolls to the top of the RecyclerView. */ 930 @VisibleForTesting openFirstHeader()931 public void openFirstHeader() { 932 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.selectFirstHeaderEntry(); 933 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView.scrollToTop(); 934 } 935 936 @Override getHeaderTopClip(@onNull WidgetCell cell)937 protected int getHeaderTopClip(@NonNull WidgetCell cell) { 938 StickyHeaderLayout header = findViewById(R.id.search_and_recommendations_container); 939 if (header == null) { 940 return 0; 941 } 942 Rect cellRect = new Rect(); 943 boolean cellIsPartiallyVisible = cell.getGlobalVisibleRect(cellRect); 944 if (cellIsPartiallyVisible) { 945 Rect occludingRect = new Rect(); 946 for (View headerChild : header.getStickyChildren()) { 947 Rect childRect = new Rect(); 948 boolean childVisible = headerChild.getGlobalVisibleRect(childRect); 949 if (childVisible && childRect.intersect(cellRect)) { 950 occludingRect.union(childRect); 951 } 952 } 953 if (!occludingRect.isEmpty() && cellRect.top < occludingRect.bottom) { 954 return occludingRect.bottom - cellRect.top; 955 } 956 } 957 return 0; 958 } 959 960 @Override scrollCellContainerByY(WidgetCell wc, int scrollByY)961 protected void scrollCellContainerByY(WidgetCell wc, int scrollByY) { 962 for (ViewParent parent = wc.getParent(); parent != null; parent = parent.getParent()) { 963 if (parent instanceof WidgetsRecyclerView recyclerView) { 964 // Scrollable container for main widget list. 965 recyclerView.smoothScrollBy(0, scrollByY); 966 return; 967 } else if (parent instanceof StickyHeaderLayout header) { 968 // Scrollable container for recommendations. We still scroll on the recycler (even 969 // though the recommendations are not in the recycler view) because the 970 // StickyHeaderLayout scroll is connected to the currently visible recycler view. 971 WidgetsRecyclerView recyclerView = findVisibleRecyclerView(); 972 if (recyclerView != null) { 973 recyclerView.smoothScrollBy(0, scrollByY); 974 } 975 return; 976 } else if (parent == this) { 977 return; 978 } 979 } 980 } 981 982 @Nullable findVisibleRecyclerView()983 private WidgetsRecyclerView findVisibleRecyclerView() { 984 if (mViewPager != null) { 985 return (WidgetsRecyclerView) mViewPager.getPageAt(mViewPager.getCurrentPage()); 986 } 987 return findViewById(R.id.primary_widgets_list_view); 988 } 989 990 /** A holder class for holding adapters & their corresponding recycler view. */ 991 final class AdapterHolder { 992 static final int PRIMARY = 0; 993 static final int WORK = 1; 994 static final int SEARCH = 2; 995 996 private final int mAdapterType; 997 final WidgetsListAdapter mWidgetsListAdapter; 998 private final DefaultItemAnimator mWidgetsListItemAnimator; 999 1000 WidgetsRecyclerView mWidgetsRecyclerView; 1001 AdapterHolder(int adapterType)1002 AdapterHolder(int adapterType) { 1003 mAdapterType = adapterType; 1004 Context context = getContext(); 1005 1006 mWidgetsListAdapter = new WidgetsListAdapter( 1007 context, 1008 LayoutInflater.from(context), 1009 this::getEmptySpaceHeight, 1010 /* iconClickListener= */ WidgetsFullSheet.this, 1011 /* iconLongClickListener= */ WidgetsFullSheet.this, 1012 isTwoPane()); 1013 mWidgetsListAdapter.setHasStableIds(true); 1014 switch (mAdapterType) { 1015 case PRIMARY: 1016 mWidgetsListAdapter.setFilter(mPrimaryWidgetsFilter); 1017 break; 1018 case WORK: 1019 mWidgetsListAdapter.setFilter(mWorkWidgetsFilter); 1020 break; 1021 default: 1022 break; 1023 } 1024 mWidgetsListItemAnimator = new WidgetsListItemAnimator(); 1025 } 1026 getEmptySpaceHeight()1027 private int getEmptySpaceHeight() { 1028 return mSearchScrollView.getHeaderHeight(); 1029 } 1030 setup(WidgetsRecyclerView recyclerView)1031 void setup(WidgetsRecyclerView recyclerView) { 1032 mWidgetsRecyclerView = recyclerView; 1033 mWidgetsRecyclerView.setOutlineProvider(mViewOutlineProvider); 1034 mWidgetsRecyclerView.setClipToOutline(true); 1035 mWidgetsRecyclerView.setClipChildren(false); 1036 mWidgetsRecyclerView.setAdapter(mWidgetsListAdapter); 1037 mWidgetsRecyclerView.bindFastScrollbar(mFastScroller); 1038 mWidgetsRecyclerView.setItemAnimator(isTwoPane() ? null : mWidgetsListItemAnimator); 1039 mWidgetsRecyclerView.setHeaderViewDimensionsProvider(WidgetsFullSheet.this); 1040 if (!isTwoPane()) { 1041 mWidgetsRecyclerView.setEdgeEffectFactory( 1042 ((SpringRelativeLayout) mContent).createEdgeEffectFactory()); 1043 } 1044 // Recycler view binds to fast scroller when it is attached to screen. Make sure 1045 // search recycler view is bound to fast scroller if user is in search mode at the time 1046 // of attachment. 1047 if (mAdapterType == PRIMARY || mAdapterType == WORK) { 1048 mWidgetsRecyclerView.addOnAttachStateChangeListener(mBindScrollbarInSearchMode); 1049 } 1050 mWidgetsListAdapter.setMaxHorizontalSpansPxPerRow(mMaxSpanPerRow); 1051 } 1052 } 1053 } 1054