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 android.view.View.MeasureSpec.makeMeasureSpec; 19 20 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y; 21 import static com.android.launcher3.config.FeatureFlags.LARGE_SCREEN_WIDGET_PICKER; 22 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGETSTRAY_SEARCHED; 23 import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL; 24 25 import android.animation.Animator; 26 import android.animation.AnimatorListenerAdapter; 27 import android.animation.PropertyValuesHolder; 28 import android.content.Context; 29 import android.content.pm.LauncherApps; 30 import android.content.res.Configuration; 31 import android.content.res.Resources; 32 import android.graphics.Outline; 33 import android.graphics.Rect; 34 import android.os.Process; 35 import android.os.UserHandle; 36 import android.os.UserManager; 37 import android.util.AttributeSet; 38 import android.util.Pair; 39 import android.util.SparseArray; 40 import android.view.LayoutInflater; 41 import android.view.MotionEvent; 42 import android.view.View; 43 import android.view.ViewGroup; 44 import android.view.ViewOutlineProvider; 45 import android.view.WindowInsets; 46 import android.view.animation.AnimationUtils; 47 import android.view.animation.Interpolator; 48 import android.widget.Button; 49 import android.widget.FrameLayout; 50 import android.widget.LinearLayout; 51 import android.widget.TextView; 52 53 import androidx.annotation.FloatRange; 54 import androidx.annotation.NonNull; 55 import androidx.annotation.Nullable; 56 import androidx.annotation.Px; 57 import androidx.annotation.VisibleForTesting; 58 import androidx.recyclerview.widget.DefaultItemAnimator; 59 import androidx.recyclerview.widget.RecyclerView; 60 61 import com.android.launcher3.DeviceProfile; 62 import com.android.launcher3.Launcher; 63 import com.android.launcher3.R; 64 import com.android.launcher3.Utilities; 65 import com.android.launcher3.anim.PendingAnimation; 66 import com.android.launcher3.compat.AccessibilityManagerCompat; 67 import com.android.launcher3.model.UserManagerState; 68 import com.android.launcher3.model.WidgetItem; 69 import com.android.launcher3.model.data.PackageItemInfo; 70 import com.android.launcher3.pm.UserCache; 71 import com.android.launcher3.recyclerview.ViewHolderBinder; 72 import com.android.launcher3.util.PackageUserKey; 73 import com.android.launcher3.views.ArrowTipView; 74 import com.android.launcher3.views.RecyclerViewFastScroller; 75 import com.android.launcher3.views.SpringRelativeLayout; 76 import com.android.launcher3.views.StickyHeaderLayout; 77 import com.android.launcher3.views.WidgetsEduView; 78 import com.android.launcher3.widget.BaseWidgetSheet; 79 import com.android.launcher3.widget.LauncherWidgetHolder.ProviderChangedListener; 80 import com.android.launcher3.widget.model.WidgetsListBaseEntry; 81 import com.android.launcher3.widget.model.WidgetsListContentEntry; 82 import com.android.launcher3.widget.model.WidgetsListHeaderEntry; 83 import com.android.launcher3.widget.picker.search.SearchModeListener; 84 import com.android.launcher3.widget.picker.search.WidgetsSearchBar; 85 import com.android.launcher3.widget.util.WidgetsTableUtils; 86 import com.android.launcher3.workprofile.PersonalWorkPagedView; 87 import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener; 88 89 import java.util.ArrayList; 90 import java.util.Collections; 91 import java.util.List; 92 import java.util.function.Predicate; 93 import java.util.stream.IntStream; 94 95 /** 96 * Popup for showing the full list of available widgets 97 */ 98 public class WidgetsFullSheet extends BaseWidgetSheet 99 implements ProviderChangedListener, OnActivePageChangedListener, 100 WidgetsRecyclerView.HeaderViewDimensionsProvider, SearchModeListener { 101 102 private static final long FADE_IN_DURATION = 150; 103 private static final long EDUCATION_TIP_DELAY_MS = 200; 104 private static final long EDUCATION_DIALOG_DELAY_MS = 500; 105 private static final float VERTICAL_START_POSITION = 0.3f; 106 private static final int PERSONAL_TAB = 0; 107 private static final int WORK_TAB = 1; 108 private static final String SUGGESTIONS_PACKAGE_NAME = "widgets_list_suggestions_entry"; 109 // The widget recommendation table can easily take over the entire screen on devices with small 110 // resolution or landscape on phone. This ratio defines the max percentage of content area that 111 // the table can display. 112 private static final float RECOMMENDATION_TABLE_HEIGHT_RATIO = 0.75f; 113 114 private static final String KEY_WIDGETS_EDUCATION_DIALOG_SEEN = 115 "launcher.widgets_education_dialog_seen"; 116 117 private final UserManagerState mUserManagerState = new UserManagerState(); 118 119 private final boolean mHasWorkProfile; 120 private final SparseArray<AdapterHolder> mAdapters = new SparseArray(); 121 private final UserHandle mCurrentUser = Process.myUserHandle(); 122 private final Predicate<WidgetsListBaseEntry> mPrimaryWidgetsFilter = 123 entry -> mCurrentUser.equals(entry.mPkgItem.user); 124 private final Predicate<WidgetsListBaseEntry> mWorkWidgetsFilter = 125 entry -> !mCurrentUser.equals(entry.mPkgItem.user) 126 && !mUserManagerState.isUserQuiet(entry.mPkgItem.user); 127 @Nullable private ArrowTipView mLatestEducationalTip; 128 private final OnLayoutChangeListener mLayoutChangeListenerToShowTips = 129 new OnLayoutChangeListener() { 130 @Override 131 public void onLayoutChange(View v, int left, int top, int right, int bottom, 132 int oldLeft, int oldTop, int oldRight, int oldBottom) { 133 if (hasSeenEducationTip()) { 134 removeOnLayoutChangeListener(this); 135 return; 136 } 137 138 // Widgets are loaded asynchronously, We are adding a delay because we only want 139 // to show the tip when the widget preview has finished loading and rendering in 140 // this view. 141 removeCallbacks(mShowEducationTipTask); 142 postDelayed(mShowEducationTipTask, EDUCATION_TIP_DELAY_MS); 143 } 144 }; 145 146 private final Runnable mShowEducationTipTask = () -> { 147 if (hasSeenEducationTip()) { 148 removeOnLayoutChangeListener(mLayoutChangeListenerToShowTips); 149 return; 150 } 151 mLatestEducationalTip = showEducationTipOnViewIfPossible(getViewToShowEducationTip()); 152 if (mLatestEducationalTip != null) { 153 removeOnLayoutChangeListener(mLayoutChangeListenerToShowTips); 154 } 155 }; 156 157 private final OnAttachStateChangeListener mBindScrollbarInSearchMode = 158 new OnAttachStateChangeListener() { 159 @Override 160 public void onViewAttachedToWindow(View view) { 161 WidgetsRecyclerView searchRecyclerView = 162 mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView; 163 if (mIsInSearchMode && searchRecyclerView != null) { 164 searchRecyclerView.bindFastScrollbar(mFastScroller); 165 } 166 } 167 168 @Override 169 public void onViewDetachedFromWindow(View view) { 170 } 171 }; 172 173 private final ViewOutlineProvider mViewOutlineProvider = new ViewOutlineProvider() { 174 @Override 175 public void getOutline(View view, Outline outline) { 176 outline.setRect( 177 0, 178 0, 179 view.getMeasuredWidth(), 180 view.getMeasuredHeight() + getBottomOffsetPx() 181 ); 182 } 183 }; 184 185 @Px private final int mTabsHeight; 186 187 @Nullable private WidgetsRecyclerView mCurrentWidgetsRecyclerView; 188 @Nullable private PersonalWorkPagedView mViewPager; 189 private boolean mIsInSearchMode; 190 private boolean mIsNoWidgetsViewNeeded; 191 @Px private int mMaxSpanPerRow; 192 private TextView mNoWidgetsView; 193 194 private StickyHeaderLayout mSearchScrollView; 195 private WidgetsRecommendationTableLayout mRecommendedWidgetsTable; 196 private LinearLayout mSuggestedWidgetsContainer; 197 private WidgetsListHeader mSuggestedWidgetsHeader; 198 private View mTabBar; 199 private View mSearchBarContainer; 200 private WidgetsSearchBar mSearchBar; 201 private TextView mHeaderTitle; 202 private FrameLayout mRightPane; 203 private WidgetsListTableViewHolderBinder mWidgetsListTableViewHolderBinder; 204 private DeviceProfile mDeviceProfile; 205 private final boolean mIsTwoPane; 206 207 private int mOrientation; 208 private @Nullable WidgetsRecyclerView mCurrentTouchEventRecyclerView; 209 210 private RecyclerViewFastScroller mFastScroller; 211 WidgetsFullSheet(Context context, AttributeSet attrs, int defStyleAttr)212 public WidgetsFullSheet(Context context, AttributeSet attrs, int defStyleAttr) { 213 super(context, attrs, defStyleAttr); 214 mDeviceProfile = Launcher.getLauncher(context).getDeviceProfile(); 215 mIsTwoPane = mDeviceProfile.isTablet 216 && mDeviceProfile.isLandscape 217 && LARGE_SCREEN_WIDGET_PICKER.get(); 218 mHasWorkProfile = context.getSystemService(LauncherApps.class).getProfiles().size() > 1; 219 mAdapters.put(AdapterHolder.PRIMARY, new AdapterHolder(AdapterHolder.PRIMARY)); 220 mAdapters.put(AdapterHolder.WORK, new AdapterHolder(AdapterHolder.WORK)); 221 mAdapters.put(AdapterHolder.SEARCH, new AdapterHolder(AdapterHolder.SEARCH)); 222 223 Resources resources = getResources(); 224 mTabsHeight = mHasWorkProfile 225 ? resources.getDimensionPixelSize(R.dimen.all_apps_header_pill_height) 226 : 0; 227 228 mUserManagerState.init(UserCache.INSTANCE.get(context), 229 context.getSystemService(UserManager.class)); 230 setContentBackground(getContext().getDrawable(R.drawable.bg_widgets_full_sheet)); 231 } 232 WidgetsFullSheet(Context context, AttributeSet attrs)233 public WidgetsFullSheet(Context context, AttributeSet attrs) { 234 this(context, attrs, 0); 235 } 236 237 @Override onFinishInflate()238 protected void onFinishInflate() { 239 super.onFinishInflate(); 240 mContent = findViewById(R.id.container); 241 242 mContent.setOutlineProvider(mViewOutlineProvider); 243 mContent.setClipToOutline(true); 244 245 LayoutInflater layoutInflater = LayoutInflater.from(getContext()); 246 int contentLayoutRes = mHasWorkProfile ? R.layout.widgets_full_sheet_paged_view 247 : R.layout.widgets_full_sheet_recyclerview; 248 if (mIsTwoPane) { 249 contentLayoutRes = mHasWorkProfile ? R.layout.widgets_full_sheet_paged_view_large_screen 250 : R.layout.widgets_full_sheet_recyclerview_large_screen; 251 } 252 layoutInflater.inflate(contentLayoutRes, mContent, true); 253 254 mFastScroller = findViewById(R.id.fast_scroller); 255 if (mIsTwoPane) { 256 mFastScroller.setVisibility(GONE); 257 } 258 mFastScroller.setPopupView(findViewById(R.id.fast_scroller_popup)); 259 260 mAdapters.get(AdapterHolder.PRIMARY).setup(findViewById(R.id.primary_widgets_list_view)); 261 mAdapters.get(AdapterHolder.SEARCH).setup(findViewById(R.id.search_widgets_list_view)); 262 if (mHasWorkProfile) { 263 mViewPager = findViewById(R.id.widgets_view_pager); 264 mViewPager.setOutlineProvider(mViewOutlineProvider); 265 mViewPager.setClipToOutline(true); 266 mViewPager.setClipChildren(false); 267 mViewPager.initParentViews(this); 268 mViewPager.getPageIndicator().setOnActivePageChangedListener(this); 269 mViewPager.getPageIndicator().setActiveMarker(AdapterHolder.PRIMARY); 270 findViewById(R.id.tab_personal) 271 .setOnClickListener((View view) -> mViewPager.snapToPage(0)); 272 findViewById(R.id.tab_work) 273 .setOnClickListener((View view) -> mViewPager.snapToPage(1)); 274 mAdapters.get(AdapterHolder.WORK).setup(findViewById(R.id.work_widgets_list_view)); 275 setDeviceManagementResources(); 276 } else { 277 mViewPager = null; 278 } 279 280 mNoWidgetsView = findViewById(R.id.no_widgets_text); 281 282 mSearchScrollView = findViewById(R.id.search_and_recommendations_container); 283 mSearchScrollView.setCurrentRecyclerView(findViewById(R.id.primary_widgets_list_view)); 284 285 mRecommendedWidgetsTable = mIsTwoPane 286 ? mContent.findViewById(R.id.recommended_widget_table) 287 : mSearchScrollView.findViewById(R.id.recommended_widget_table); 288 289 mRecommendedWidgetsTable.setWidgetCellLongClickListener(this); 290 mRecommendedWidgetsTable.setWidgetCellOnClickListener(this); 291 292 // Add suggested widgets. 293 if (mIsTwoPane) { 294 mSuggestedWidgetsContainer = mSearchScrollView.findViewById(R.id.suggestions_header); 295 296 // Inflate the suggestions header. 297 mSuggestedWidgetsHeader = (WidgetsListHeader) layoutInflater.inflate( 298 R.layout.widgets_list_row_header_two_pane, 299 mSuggestedWidgetsContainer, 300 false); 301 mSuggestedWidgetsHeader.setExpanded(true); 302 303 PackageItemInfo packageItemInfo = new PackageItemInfo( 304 /* packageName= */ SUGGESTIONS_PACKAGE_NAME, 305 Process.myUserHandle()) { 306 @Override 307 public boolean usingLowResIcon() { 308 return false; 309 } 310 }; 311 packageItemInfo.title = getContext().getString(R.string.suggested_widgets_header_title); 312 WidgetsListHeaderEntry widgetsListHeaderEntry = WidgetsListHeaderEntry.create( 313 packageItemInfo, 314 getContext().getString(R.string.suggested_widgets_header_title), 315 mActivityContext.getPopupDataProvider().getRecommendedWidgets()) 316 .withWidgetListShown(); 317 318 mSuggestedWidgetsHeader.applyFromItemInfoWithIcon(widgetsListHeaderEntry); 319 mSuggestedWidgetsHeader.setIcon( 320 getContext().getDrawable(R.drawable.widget_suggestions_icon)); 321 mSuggestedWidgetsHeader.setOnClickListener(view -> { 322 mSuggestedWidgetsHeader.setExpanded(true); 323 resetExpandedHeaders(); 324 mRightPane.removeAllViews(); 325 mRightPane.addView(mRecommendedWidgetsTable); 326 }); 327 mSuggestedWidgetsContainer.addView(mSuggestedWidgetsHeader); 328 } 329 330 mTabBar = mSearchScrollView.findViewById(R.id.tabs); 331 mSearchBarContainer = mSearchScrollView.findViewById(R.id.search_bar_container); 332 mSearchBar = mSearchScrollView.findViewById(R.id.widgets_search_bar); 333 mHeaderTitle = mIsTwoPane 334 ? mContent.findViewById(R.id.title) 335 : mSearchScrollView.findViewById(R.id.title); 336 mRightPane = mIsTwoPane ? mContent.findViewById(R.id.right_pane) : null; 337 mWidgetsListTableViewHolderBinder = 338 new WidgetsListTableViewHolderBinder(mActivityContext, layoutInflater, this, this); 339 onRecommendedWidgetsBound(); 340 onWidgetsBound(); 341 342 mSearchBar.initialize( 343 mActivityContext.getPopupDataProvider(), /* searchModeListener= */ this); 344 345 setUpEducationViewsIfNeeded(); 346 } 347 setDeviceManagementResources()348 private void setDeviceManagementResources() { 349 if (mActivityContext.getStringCache() != null) { 350 Button personalTab = findViewById(R.id.tab_personal); 351 personalTab.setText(mActivityContext.getStringCache().widgetsPersonalTab); 352 353 Button workTab = findViewById(R.id.tab_work); 354 workTab.setText(mActivityContext.getStringCache().widgetsWorkTab); 355 } 356 } 357 358 @Override onActivePageChanged(int currentActivePage)359 public void onActivePageChanged(int currentActivePage) { 360 361 // if the current active page changes to personal or work we set suggestions 362 // to be the selected widget 363 if (mIsTwoPane && (currentActivePage == PERSONAL_TAB || currentActivePage == WORK_TAB)) { 364 mSuggestedWidgetsHeader.callOnClick(); 365 } 366 367 AdapterHolder currentAdapterHolder = mAdapters.get(currentActivePage); 368 WidgetsRecyclerView currentRecyclerView = 369 mAdapters.get(currentActivePage).mWidgetsRecyclerView; 370 371 updateRecyclerViewVisibility(currentAdapterHolder); 372 attachScrollbarToRecyclerView(currentRecyclerView); 373 } 374 375 @Override onBackProgressed(@loatRangefrom = 0.0, to = 1.0) float progress)376 public void onBackProgressed(@FloatRange(from = 0.0, to = 1.0) float progress) { 377 super.onBackProgressed(progress); 378 mFastScroller.setVisibility(progress > 0 ? View.INVISIBLE : View.VISIBLE); 379 } 380 attachScrollbarToRecyclerView(WidgetsRecyclerView recyclerView)381 private void attachScrollbarToRecyclerView(WidgetsRecyclerView recyclerView) { 382 recyclerView.bindFastScrollbar(mFastScroller); 383 if (mCurrentWidgetsRecyclerView != recyclerView) { 384 // Only reset the scroll position & expanded apps if the currently shown recycler view 385 // has been updated. 386 reset(); 387 resetExpandedHeaders(); 388 mCurrentWidgetsRecyclerView = recyclerView; 389 mSearchScrollView.setCurrentRecyclerView(recyclerView); 390 } 391 } 392 updateRecyclerViewVisibility(AdapterHolder adapterHolder)393 private void updateRecyclerViewVisibility(AdapterHolder adapterHolder) { 394 // The first item is always an empty space entry. Look for any more items. 395 boolean isWidgetAvailable = adapterHolder.mWidgetsListAdapter.hasVisibleEntries(); 396 397 if (mIsTwoPane) { 398 mRightPane.setVisibility(isWidgetAvailable ? VISIBLE : GONE); 399 } 400 401 adapterHolder.mWidgetsRecyclerView.setVisibility(isWidgetAvailable ? VISIBLE : GONE); 402 403 if (adapterHolder.mAdapterType == AdapterHolder.SEARCH) { 404 mNoWidgetsView.setText(R.string.no_search_results); 405 } else if (adapterHolder.mAdapterType == AdapterHolder.WORK 406 && mUserManagerState.isAnyProfileQuietModeEnabled() 407 && mActivityContext.getStringCache() != null) { 408 mNoWidgetsView.setText(mActivityContext.getStringCache().workProfilePausedTitle); 409 } else { 410 mNoWidgetsView.setText(R.string.no_widgets_available); 411 } 412 mNoWidgetsView.setVisibility(isWidgetAvailable ? GONE : VISIBLE); 413 } 414 reset()415 private void reset() { 416 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView.scrollToTop(); 417 if (mHasWorkProfile) { 418 mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView.scrollToTop(); 419 } 420 mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.scrollToTop(); 421 mSearchScrollView.reset(/* animate= */ true); 422 } 423 424 @VisibleForTesting getRecyclerView()425 public WidgetsRecyclerView getRecyclerView() { 426 if (mIsInSearchMode) { 427 return mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView; 428 } 429 if (!mHasWorkProfile || mViewPager.getCurrentPage() == AdapterHolder.PRIMARY) { 430 return mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView; 431 } 432 return mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView; 433 } 434 435 @Override getAccessibilityTarget()436 protected Pair<View, String> getAccessibilityTarget() { 437 return Pair.create(getRecyclerView(), getContext().getString( 438 mIsOpen ? R.string.widgets_list : R.string.widgets_list_closed)); 439 } 440 441 @Override onAttachedToWindow()442 protected void onAttachedToWindow() { 443 super.onAttachedToWindow(); 444 mActivityContext.getAppWidgetHolder().addProviderChangeListener(this); 445 notifyWidgetProvidersChanged(); 446 onRecommendedWidgetsBound(); 447 } 448 449 @Override onDetachedFromWindow()450 protected void onDetachedFromWindow() { 451 super.onDetachedFromWindow(); 452 mActivityContext.getAppWidgetHolder().removeProviderChangeListener(this); 453 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView 454 .removeOnAttachStateChangeListener(mBindScrollbarInSearchMode); 455 if (mHasWorkProfile) { 456 mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView 457 .removeOnAttachStateChangeListener(mBindScrollbarInSearchMode); 458 } 459 } 460 461 @Override setInsets(Rect insets)462 public void setInsets(Rect insets) { 463 super.setInsets(insets); 464 int bottomPadding = Math.max(insets.bottom, mNavBarScrimHeight); 465 setBottomPadding(mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView, bottomPadding); 466 setBottomPadding(mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView, bottomPadding); 467 if (mHasWorkProfile) { 468 setBottomPadding(mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView, bottomPadding); 469 } 470 ((MarginLayoutParams) mNoWidgetsView.getLayoutParams()).bottomMargin = bottomPadding; 471 472 if (bottomPadding > 0) { 473 setupNavBarColor(); 474 } else { 475 clearNavBarColor(); 476 } 477 478 requestLayout(); 479 } 480 setBottomPadding(RecyclerView recyclerView, int bottomPadding)481 private void setBottomPadding(RecyclerView recyclerView, int bottomPadding) { 482 recyclerView.setPadding( 483 recyclerView.getPaddingLeft(), 484 recyclerView.getPaddingTop(), 485 recyclerView.getPaddingRight(), 486 bottomPadding); 487 } 488 489 @Override onContentHorizontalMarginChanged(int contentHorizontalMarginInPx)490 protected void onContentHorizontalMarginChanged(int contentHorizontalMarginInPx) { 491 setContentViewChildHorizontalMargin(mSearchScrollView, contentHorizontalMarginInPx); 492 if (mViewPager == null) { 493 setContentViewChildHorizontalPadding( 494 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView, 495 contentHorizontalMarginInPx); 496 } else { 497 setContentViewChildHorizontalPadding( 498 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView, 499 contentHorizontalMarginInPx); 500 setContentViewChildHorizontalPadding( 501 mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView, 502 contentHorizontalMarginInPx); 503 } 504 setContentViewChildHorizontalPadding( 505 mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView, 506 contentHorizontalMarginInPx); 507 } 508 setContentViewChildHorizontalMargin(View view, int horizontalMarginInPx)509 private static void setContentViewChildHorizontalMargin(View view, int horizontalMarginInPx) { 510 ViewGroup.MarginLayoutParams layoutParams = 511 (ViewGroup.MarginLayoutParams) view.getLayoutParams(); 512 layoutParams.setMarginStart(horizontalMarginInPx); 513 layoutParams.setMarginEnd(horizontalMarginInPx); 514 } 515 setContentViewChildHorizontalPadding(View view, int horizontalPaddingInPx)516 private static void setContentViewChildHorizontalPadding(View view, int horizontalPaddingInPx) { 517 view.setPadding(horizontalPaddingInPx, view.getPaddingTop(), horizontalPaddingInPx, 518 view.getPaddingBottom()); 519 } 520 521 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)522 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 523 doMeasure(widthMeasureSpec, heightMeasureSpec); 524 525 if (updateMaxSpansPerRow()) { 526 doMeasure(widthMeasureSpec, heightMeasureSpec); 527 } 528 } 529 530 /** Returns {@code true} if the max spans have been updated. */ updateMaxSpansPerRow()531 private boolean updateMaxSpansPerRow() { 532 if (getMeasuredWidth() == 0) return false; 533 534 View content = mHasWorkProfile 535 ? mViewPager 536 : mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView; 537 if (mIsTwoPane && mRightPane != null) { 538 content = mRightPane; 539 } 540 541 @Px int maxHorizontalSpan = content.getMeasuredWidth() - (2 * mContentHorizontalMargin); 542 if (mMaxSpanPerRow != maxHorizontalSpan) { 543 mMaxSpanPerRow = maxHorizontalSpan; 544 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.setMaxHorizontalSpansPxPerRow( 545 maxHorizontalSpan); 546 mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.setMaxHorizontalSpansPxPerRow( 547 maxHorizontalSpan); 548 if (mHasWorkProfile) { 549 mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.setMaxHorizontalSpansPxPerRow( 550 maxHorizontalSpan); 551 } 552 onRecommendedWidgetsBound(); 553 return true; 554 } 555 return false; 556 } 557 558 @Override onLayout(boolean changed, int l, int t, int r, int b)559 protected void onLayout(boolean changed, int l, int t, int r, int b) { 560 int width = r - l; 561 int height = b - t; 562 563 // Content is laid out as center bottom aligned 564 int contentWidth = mContent.getMeasuredWidth(); 565 int contentLeft = (width - contentWidth - mInsets.left - mInsets.right) / 2 + mInsets.left; 566 mContent.layout(contentLeft, height - mContent.getMeasuredHeight(), 567 contentLeft + contentWidth, height); 568 569 setTranslationShift(mTranslationShift); 570 } 571 572 @Override notifyWidgetProvidersChanged()573 public void notifyWidgetProvidersChanged() { 574 mActivityContext.refreshAndBindWidgetsForPackageUser(null); 575 } 576 577 @Override onWidgetsBound()578 public void onWidgetsBound() { 579 if (mIsInSearchMode) { 580 return; 581 } 582 List<WidgetsListBaseEntry> allWidgets = 583 mActivityContext.getPopupDataProvider().getAllWidgets(); 584 585 AdapterHolder primaryUserAdapterHolder = mAdapters.get(AdapterHolder.PRIMARY); 586 primaryUserAdapterHolder.mWidgetsListAdapter.setWidgets(allWidgets); 587 588 if (mHasWorkProfile) { 589 mViewPager.setVisibility(VISIBLE); 590 mTabBar.setVisibility(VISIBLE); 591 AdapterHolder workUserAdapterHolder = mAdapters.get(AdapterHolder.WORK); 592 workUserAdapterHolder.mWidgetsListAdapter.setWidgets(allWidgets); 593 onActivePageChanged(mViewPager.getCurrentPage()); 594 } else { 595 onActivePageChanged(0); 596 } 597 // Update recommended widgets section so that it occupies appropriate space on screen to 598 // leave enough space for presence/absence of mNoWidgetsView. 599 boolean isNoWidgetsViewNeeded = 600 !mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.hasVisibleEntries() 601 || (mHasWorkProfile && mAdapters.get(AdapterHolder.WORK) 602 .mWidgetsListAdapter.hasVisibleEntries()); 603 if (mIsNoWidgetsViewNeeded != isNoWidgetsViewNeeded) { 604 mIsNoWidgetsViewNeeded = isNoWidgetsViewNeeded; 605 onRecommendedWidgetsBound(); 606 } 607 } 608 609 @Override enterSearchMode()610 public void enterSearchMode() { 611 if (mIsInSearchMode) return; 612 setViewVisibilityBasedOnSearch(/*isInSearchMode= */ true); 613 attachScrollbarToRecyclerView(mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView); 614 mActivityContext.getStatsLogManager().logger().log(LAUNCHER_WIDGETSTRAY_SEARCHED); 615 } 616 617 @Override exitSearchMode()618 public void exitSearchMode() { 619 if (!mIsInSearchMode) return; 620 onSearchResults(new ArrayList<>()); 621 setViewVisibilityBasedOnSearch(/*isInSearchMode=*/ false); 622 if (mHasWorkProfile) { 623 mViewPager.snapToPage(AdapterHolder.PRIMARY); 624 } 625 attachScrollbarToRecyclerView(mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView); 626 } 627 628 @Override onSearchResults(List<WidgetsListBaseEntry> entries)629 public void onSearchResults(List<WidgetsListBaseEntry> entries) { 630 mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.setWidgetsOnSearch(entries); 631 updateRecyclerViewVisibility(mAdapters.get(AdapterHolder.SEARCH)); 632 if (mIsTwoPane) { 633 mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.selectFirstHeaderEntry(); 634 } 635 mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.scrollToTop(); 636 } 637 setViewVisibilityBasedOnSearch(boolean isInSearchMode)638 private void setViewVisibilityBasedOnSearch(boolean isInSearchMode) { 639 mIsInSearchMode = isInSearchMode; 640 if (isInSearchMode) { 641 mRecommendedWidgetsTable.setVisibility(GONE); 642 if (mIsTwoPane) { 643 mSuggestedWidgetsContainer.setVisibility(GONE); 644 } 645 if (mHasWorkProfile) { 646 mViewPager.setVisibility(GONE); 647 mTabBar.setVisibility(GONE); 648 } else { 649 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView.setVisibility(GONE); 650 } 651 updateRecyclerViewVisibility(mAdapters.get(AdapterHolder.SEARCH)); 652 // Hide no search results view to prevent it from flashing on enter search. 653 mNoWidgetsView.setVisibility(GONE); 654 } else { 655 mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.setVisibility(GONE); 656 if (mIsTwoPane) { 657 mSuggestedWidgetsContainer.setVisibility(VISIBLE); 658 mSuggestedWidgetsHeader.callOnClick(); 659 } 660 // Visibility of recommended widgets, recycler views and headers are handled in methods 661 // below. 662 onRecommendedWidgetsBound(); 663 onWidgetsBound(); 664 } 665 } 666 resetExpandedHeaders()667 private void resetExpandedHeaders() { 668 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.resetExpandedHeader(); 669 mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.resetExpandedHeader(); 670 } 671 672 @Override onRecommendedWidgetsBound()673 public void onRecommendedWidgetsBound() { 674 if (mIsInSearchMode) { 675 return; 676 } 677 List<WidgetItem> recommendedWidgets = 678 mActivityContext.getPopupDataProvider().getRecommendedWidgets(); 679 if (recommendedWidgets.size() > 0) { 680 float noWidgetsViewHeight = 0; 681 if (mIsNoWidgetsViewNeeded) { 682 // Make sure recommended section leaves enough space for noWidgetsView. 683 Rect noWidgetsViewTextBounds = new Rect(); 684 mNoWidgetsView.getPaint() 685 .getTextBounds(mNoWidgetsView.getText().toString(), /* start= */ 0, 686 mNoWidgetsView.getText().length(), noWidgetsViewTextBounds); 687 noWidgetsViewHeight = noWidgetsViewTextBounds.height(); 688 } 689 doMeasure( 690 makeMeasureSpec(mActivityContext.getDeviceProfile().availableWidthPx, 691 MeasureSpec.EXACTLY), 692 makeMeasureSpec(mActivityContext.getDeviceProfile().availableHeightPx, 693 MeasureSpec.EXACTLY)); 694 float maxTableHeight = mIsTwoPane ? Float.MAX_VALUE : (mContent.getMeasuredHeight() 695 - mTabsHeight - getHeaderViewHeight() 696 - noWidgetsViewHeight) * RECOMMENDATION_TABLE_HEIGHT_RATIO; 697 698 List<ArrayList<WidgetItem>> recommendedWidgetsInTable = 699 WidgetsTableUtils.groupWidgetItemsUsingRowPxWithoutReordering( 700 recommendedWidgets, 701 mActivityContext, 702 mActivityContext.getDeviceProfile(), 703 mMaxSpanPerRow, 704 mWidgetCellHorizontalPadding); 705 mRecommendedWidgetsTable.setRecommendedWidgets( 706 recommendedWidgetsInTable, maxTableHeight); 707 } else { 708 mRecommendedWidgetsTable.setVisibility(GONE); 709 } 710 } 711 open(boolean animate)712 private void open(boolean animate) { 713 if (animate) { 714 if (getPopupContainer().getInsets().bottom > 0) { 715 mContent.setAlpha(0); 716 setTranslationShift(VERTICAL_START_POSITION); 717 } 718 mOpenCloseAnimator.setValues( 719 PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED)); 720 mOpenCloseAnimator 721 .setDuration(mActivityContext.getDeviceProfile().bottomSheetOpenDuration) 722 .setInterpolator(AnimationUtils.loadInterpolator( 723 getContext(), android.R.interpolator.linear_out_slow_in)); 724 mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() { 725 @Override 726 public void onAnimationEnd(Animator animation) { 727 mOpenCloseAnimator.removeListener(this); 728 } 729 }); 730 post(() -> { 731 mOpenCloseAnimator.start(); 732 mContent.animate().alpha(1).setDuration(FADE_IN_DURATION); 733 }); 734 } else { 735 setTranslationShift(TRANSLATION_SHIFT_OPENED); 736 post(this::announceAccessibilityChanges); 737 } 738 } 739 740 @Override handleClose(boolean animate)741 protected void handleClose(boolean animate) { 742 handleClose(animate, mActivityContext.getDeviceProfile().bottomSheetCloseDuration); 743 } 744 745 @Override isOfType(int type)746 protected boolean isOfType(int type) { 747 return (type & TYPE_WIDGETS_FULL_SHEET) != 0; 748 } 749 750 @Override onControllerInterceptTouchEvent(MotionEvent ev)751 public boolean onControllerInterceptTouchEvent(MotionEvent ev) { 752 // Disable swipe down when recycler view is scrolling or scroll view is scrolling 753 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 754 mNoIntercept = false; 755 WidgetsRecyclerView recyclerView = getRecyclerView(); 756 RecyclerViewFastScroller scroller = recyclerView.getScrollbar(); 757 if (scroller.getThumbOffsetY() >= 0 758 && getPopupContainer().isEventOverView(scroller, ev)) { 759 mNoIntercept = true; 760 } else if (getPopupContainer().isEventOverView(recyclerView, ev)) { 761 mNoIntercept = !recyclerView.shouldContainerScroll(ev, getPopupContainer()); 762 } else if (mIsTwoPane && getPopupContainer().isEventOverView(mRightPane, ev)) { 763 mNoIntercept = mRightPane.getScrollY() > 0; 764 } 765 766 if (mSearchBar.isSearchBarFocused() 767 && !getPopupContainer().isEventOverView(mSearchBarContainer, ev)) { 768 mSearchBar.clearSearchBarFocus(); 769 } 770 } 771 return super.onControllerInterceptTouchEvent(ev); 772 } 773 774 /** Shows the {@link WidgetsFullSheet} on the launcher. */ show(Launcher launcher, boolean animate)775 public static WidgetsFullSheet show(Launcher launcher, boolean animate) { 776 WidgetsFullSheet sheet = (WidgetsFullSheet) launcher.getLayoutInflater() 777 .inflate(LARGE_SCREEN_WIDGET_PICKER.get() 778 && launcher.getDeviceProfile().isTablet 779 && launcher.getDeviceProfile().isLandscape 780 ? R.layout.widgets_full_sheet_large_screen 781 : R.layout.widgets_full_sheet, launcher.getDragLayer(), false); 782 sheet.attachToContainer(); 783 sheet.mIsOpen = true; 784 sheet.open(animate); 785 return sheet; 786 } 787 788 @Override onInterceptTouchEvent(MotionEvent ev)789 public boolean onInterceptTouchEvent(MotionEvent ev) { 790 return isTouchOnScrollbar(ev) || super.onInterceptTouchEvent(ev); 791 } 792 793 @Override onTouchEvent(MotionEvent ev)794 public boolean onTouchEvent(MotionEvent ev) { 795 return maybeHandleTouchEvent(ev) || super.onTouchEvent(ev); 796 } 797 maybeHandleTouchEvent(MotionEvent ev)798 private boolean maybeHandleTouchEvent(MotionEvent ev) { 799 boolean isEventHandled = false; 800 801 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 802 mCurrentTouchEventRecyclerView = isTouchOnScrollbar(ev) ? getRecyclerView() : null; 803 } 804 805 if (mCurrentTouchEventRecyclerView != null) { 806 final float offsetX = mContent.getX(); 807 final float offsetY = mContent.getY(); 808 ev.offsetLocation(-offsetX, -offsetY); 809 isEventHandled = mCurrentTouchEventRecyclerView.dispatchTouchEvent(ev); 810 ev.offsetLocation(offsetX, offsetY); 811 } 812 813 if (ev.getAction() == MotionEvent.ACTION_UP 814 || ev.getAction() == MotionEvent.ACTION_CANCEL) { 815 mCurrentTouchEventRecyclerView = null; 816 } 817 818 return isEventHandled; 819 } 820 isTouchOnScrollbar(MotionEvent ev)821 private boolean isTouchOnScrollbar(MotionEvent ev) { 822 final float offsetX = mContent.getX(); 823 final float offsetY = mContent.getY(); 824 WidgetsRecyclerView rv = getRecyclerView(); 825 826 ev.offsetLocation(-offsetX, -offsetY); 827 boolean isOnScrollBar = rv != null && rv.getScrollbar() != null && rv.isHitOnScrollBar(ev); 828 ev.offsetLocation(offsetX, offsetY); 829 830 return isOnScrollBar; 831 } 832 833 /** Gets the {@link WidgetsRecyclerView} which shows all widgets in {@link WidgetsFullSheet}. */ 834 @VisibleForTesting getWidgetsView(Launcher launcher)835 public static WidgetsRecyclerView getWidgetsView(Launcher launcher) { 836 return launcher.findViewById(R.id.primary_widgets_list_view); 837 } 838 839 @Override addHintCloseAnim( float distanceToMove, Interpolator interpolator, PendingAnimation target)840 public void addHintCloseAnim( 841 float distanceToMove, Interpolator interpolator, PendingAnimation target) { 842 target.setFloat(getRecyclerView(), VIEW_TRANSLATE_Y, -distanceToMove, interpolator); 843 target.setViewAlpha(getRecyclerView(), 0.5f, interpolator); 844 } 845 846 @Override onCloseComplete()847 protected void onCloseComplete() { 848 super.onCloseComplete(); 849 removeCallbacks(mShowEducationTipTask); 850 if (mLatestEducationalTip != null) { 851 mLatestEducationalTip.close(false); 852 } 853 AccessibilityManagerCompat.sendStateEventToTest(getContext(), NORMAL_STATE_ORDINAL); 854 } 855 856 @Override getHeaderViewHeight()857 public int getHeaderViewHeight() { 858 return measureHeightWithVerticalMargins(mHeaderTitle) 859 + measureHeightWithVerticalMargins(mSearchBarContainer); 860 } 861 862 /** private the height, in pixel, + the vertical margins of a given view. */ measureHeightWithVerticalMargins(View view)863 private static int measureHeightWithVerticalMargins(View view) { 864 if (view.getVisibility() != VISIBLE) { 865 return 0; 866 } 867 MarginLayoutParams marginLayoutParams = (MarginLayoutParams) view.getLayoutParams(); 868 return view.getMeasuredHeight() + marginLayoutParams.bottomMargin 869 + marginLayoutParams.topMargin; 870 } 871 872 @Override onConfigurationChanged(Configuration newConfig)873 protected void onConfigurationChanged(Configuration newConfig) { 874 super.onConfigurationChanged(newConfig); 875 if (mIsInSearchMode) { 876 mSearchBar.reset(); 877 } 878 879 // Checks the orientation of the screen 880 if (LARGE_SCREEN_WIDGET_PICKER.get() 881 && mOrientation != newConfig.orientation 882 && mDeviceProfile.isTablet) { 883 mOrientation = newConfig.orientation; 884 handleClose(false); 885 show(Launcher.getLauncher(getContext()), false); 886 } 887 } 888 889 @Override onBackInvoked()890 public void onBackInvoked() { 891 if (mIsInSearchMode) { 892 mSearchBar.reset(); 893 animateSlideInViewToNoScale(); 894 } else { 895 super.onBackInvoked(); 896 } 897 } 898 899 @Override onDragStart(boolean start, float startDisplacement)900 public void onDragStart(boolean start, float startDisplacement) { 901 super.onDragStart(start, startDisplacement); 902 getWindowInsetsController().hide(WindowInsets.Type.ime()); 903 } 904 getViewToShowEducationTip()905 @Nullable private View getViewToShowEducationTip() { 906 if (mRecommendedWidgetsTable.getVisibility() == VISIBLE 907 && mRecommendedWidgetsTable.getChildCount() > 0) { 908 return ((ViewGroup) mRecommendedWidgetsTable.getChildAt(0)).getChildAt(0); 909 } 910 911 AdapterHolder adapterHolder = mAdapters.get(mIsInSearchMode 912 ? AdapterHolder.SEARCH 913 : mViewPager == null 914 ? AdapterHolder.PRIMARY 915 : mViewPager.getCurrentPage()); 916 WidgetsRowViewHolder viewHolderForTip = 917 (WidgetsRowViewHolder) IntStream.range( 918 0, adapterHolder.mWidgetsListAdapter.getItemCount()) 919 .mapToObj(adapterHolder.mWidgetsRecyclerView:: 920 findViewHolderForAdapterPosition) 921 .filter(viewHolder -> viewHolder instanceof WidgetsRowViewHolder) 922 .findFirst() 923 .orElse(null); 924 if (viewHolderForTip != null) { 925 return ((ViewGroup) viewHolderForTip.tableContainer.getChildAt(0)).getChildAt(0); 926 } 927 928 return null; 929 } 930 931 /** Shows education dialog for widgets. */ showEducationDialog()932 private WidgetsEduView showEducationDialog() { 933 mActivityContext.getSharedPrefs().edit() 934 .putBoolean(KEY_WIDGETS_EDUCATION_DIALOG_SEEN, true).apply(); 935 return WidgetsEduView.showEducationDialog(mActivityContext); 936 } 937 938 /** Returns {@code true} if education dialog has previously been shown. */ hasSeenEducationDialog()939 protected boolean hasSeenEducationDialog() { 940 return mActivityContext.getSharedPrefs() 941 .getBoolean(KEY_WIDGETS_EDUCATION_DIALOG_SEEN, false) 942 || Utilities.isRunningInTestHarness(); 943 } 944 setUpEducationViewsIfNeeded()945 private void setUpEducationViewsIfNeeded() { 946 if (!hasSeenEducationDialog()) { 947 postDelayed(() -> { 948 WidgetsEduView eduDialog = showEducationDialog(); 949 eduDialog.addOnCloseListener(() -> { 950 if (!hasSeenEducationTip()) { 951 addOnLayoutChangeListener(mLayoutChangeListenerToShowTips); 952 // Call #requestLayout() to trigger layout change listener in order to show 953 // arrow tip immediately if there is a widget to show it on. 954 requestLayout(); 955 } 956 }); 957 }, EDUCATION_DIALOG_DELAY_MS); 958 } else if (!hasSeenEducationTip()) { 959 addOnLayoutChangeListener(mLayoutChangeListenerToShowTips); 960 } 961 } 962 963 /** A holder class for holding adapters & their corresponding recycler view. */ 964 private final class AdapterHolder { 965 static final int PRIMARY = 0; 966 static final int WORK = 1; 967 static final int SEARCH = 2; 968 969 private final int mAdapterType; 970 private final WidgetsListAdapter mWidgetsListAdapter; 971 private final DefaultItemAnimator mWidgetsListItemAnimator; 972 973 private WidgetsRecyclerView mWidgetsRecyclerView; 974 AdapterHolder(int adapterType)975 AdapterHolder(int adapterType) { 976 mAdapterType = adapterType; 977 Context context = getContext(); 978 HeaderChangeListener headerChangeListener = new HeaderChangeListener() { 979 @Override 980 public void onHeaderChanged(@NonNull PackageUserKey selectedHeader) { 981 WidgetsListContentEntry contentEntry = mActivityContext.getPopupDataProvider() 982 .getSelectedAppWidgets(selectedHeader); 983 984 if (contentEntry == null || mRightPane == null) { 985 return; 986 } 987 988 if (mSuggestedWidgetsHeader != null) { 989 mSuggestedWidgetsHeader.setExpanded(false); 990 } 991 WidgetsRowViewHolder widgetsRowViewHolder = 992 mWidgetsListTableViewHolderBinder.newViewHolder(mRightPane); 993 mWidgetsListTableViewHolderBinder.bindViewHolder(widgetsRowViewHolder, 994 contentEntry, 995 ViewHolderBinder.POSITION_FIRST | ViewHolderBinder.POSITION_LAST, 996 Collections.EMPTY_LIST); 997 widgetsRowViewHolder.mDataCallback = data -> { 998 mWidgetsListTableViewHolderBinder.bindViewHolder(widgetsRowViewHolder, 999 contentEntry, 1000 ViewHolderBinder.POSITION_FIRST | ViewHolderBinder.POSITION_LAST, 1001 Collections.singletonList(data)); 1002 }; 1003 mRightPane.removeAllViews(); 1004 mRightPane.addView(widgetsRowViewHolder.itemView); 1005 } 1006 }; 1007 mWidgetsListAdapter = new WidgetsListAdapter( 1008 context, 1009 LayoutInflater.from(context), 1010 this::getEmptySpaceHeight, 1011 /* iconClickListener= */ WidgetsFullSheet.this, 1012 /* iconLongClickListener= */ WidgetsFullSheet.this, 1013 mIsTwoPane ? headerChangeListener : null); 1014 mWidgetsListAdapter.setHasStableIds(true); 1015 switch (mAdapterType) { 1016 case PRIMARY: 1017 mWidgetsListAdapter.setFilter(mPrimaryWidgetsFilter); 1018 break; 1019 case WORK: 1020 mWidgetsListAdapter.setFilter(mWorkWidgetsFilter); 1021 break; 1022 default: 1023 break; 1024 } 1025 mWidgetsListItemAnimator = new DefaultItemAnimator(); 1026 // Disable change animations because it disrupts the item focus upon adapter item 1027 // change. 1028 mWidgetsListItemAnimator.setSupportsChangeAnimations(false); 1029 } 1030 getEmptySpaceHeight()1031 private int getEmptySpaceHeight() { 1032 return mSearchScrollView.getHeaderHeight(); 1033 } 1034 setup(WidgetsRecyclerView recyclerView)1035 void setup(WidgetsRecyclerView recyclerView) { 1036 mWidgetsRecyclerView = recyclerView; 1037 mWidgetsRecyclerView.setOutlineProvider(mViewOutlineProvider); 1038 mWidgetsRecyclerView.setClipToOutline(true); 1039 mWidgetsRecyclerView.setClipChildren(false); 1040 mWidgetsRecyclerView.setAdapter(mWidgetsListAdapter); 1041 mWidgetsRecyclerView.bindFastScrollbar(mFastScroller); 1042 mWidgetsRecyclerView.setItemAnimator(mWidgetsListItemAnimator); 1043 mWidgetsRecyclerView.setHeaderViewDimensionsProvider(WidgetsFullSheet.this); 1044 if (!mIsTwoPane) { 1045 mWidgetsRecyclerView.setEdgeEffectFactory( 1046 ((SpringRelativeLayout) mContent).createEdgeEffectFactory()); 1047 } 1048 // Recycler view binds to fast scroller when it is attached to screen. Make sure 1049 // search recycler view is bound to fast scroller if user is in search mode at the time 1050 // of attachment. 1051 if (mAdapterType == PRIMARY || mAdapterType == WORK) { 1052 mWidgetsRecyclerView.addOnAttachStateChangeListener(mBindScrollbarInSearchMode); 1053 } 1054 mWidgetsListAdapter.setMaxHorizontalSpansPxPerRow(mMaxSpanPerRow); 1055 } 1056 } 1057 1058 /** 1059 * This is a listener for when the selected header gets changed in the left pane. 1060 */ 1061 public interface HeaderChangeListener { 1062 /** 1063 * Sets the right pane to have the widgets for the currently selected header from 1064 * the left pane. 1065 */ onHeaderChanged(@onNull PackageUserKey selectedHeader)1066 void onHeaderChanged(@NonNull PackageUserKey selectedHeader); 1067 } 1068 } 1069