/* * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.launcher3.widget.picker; import android.view.View; import android.view.ViewGroup.MarginLayoutParams; import android.widget.RelativeLayout; import androidx.annotation.Nullable; import com.android.launcher3.views.RecyclerViewFastScroller; import com.android.launcher3.widget.picker.WidgetsFullSheet.SearchAndRecommendationViewHolder; import com.android.launcher3.workprofile.PersonalWorkPagedView; /** * A controller which measures & updates {@link WidgetsFullSheet}'s views padding, margin and * vertical displacement upon scrolling. */ final class SearchAndRecommendationsScrollController implements RecyclerViewFastScroller.OnFastScrollChangeListener { private final boolean mHasWorkProfile; private final SearchAndRecommendationViewHolder mViewHolder; private final WidgetsRecyclerView mPrimaryRecyclerView; private final WidgetsRecyclerView mSearchRecyclerView; private final int mTabsHeight; // The following are only non null if mHasWorkProfile is true. @Nullable private final WidgetsRecyclerView mWorkRecyclerView; @Nullable private final View mPrimaryWorkTabsView; @Nullable private final PersonalWorkPagedView mPrimaryWorkViewPager; private WidgetsRecyclerView mCurrentRecyclerView; /** * The vertical distance, in pixels, until the search is pinned at the top of the screen when * the user scrolls down the recycler view. */ private int mCollapsibleHeightForSearch = 0; /** * The vertical distance, in pixels, until the recommendation table disappears from the top of * the screen when the user scrolls down the recycler view. */ private int mCollapsibleHeightForRecommendation = 0; /** * The vertical distance, in pixels, until the tabs is pinned at the top of the screen when the * user scrolls down the recycler view. * *
Always 0 if there is no work profile. */ private int mCollapsibleHeightForTabs = 0; SearchAndRecommendationsScrollController( boolean hasWorkProfile, int tabsHeight, SearchAndRecommendationViewHolder viewHolder, WidgetsRecyclerView primaryRecyclerView, @Nullable WidgetsRecyclerView workRecyclerView, WidgetsRecyclerView searchRecyclerView, @Nullable View personalWorkTabsView, @Nullable PersonalWorkPagedView primaryWorkViewPager) { mHasWorkProfile = hasWorkProfile; mViewHolder = viewHolder; mPrimaryRecyclerView = primaryRecyclerView; mCurrentRecyclerView = mPrimaryRecyclerView; mWorkRecyclerView = workRecyclerView; mSearchRecyclerView = searchRecyclerView; mPrimaryWorkTabsView = personalWorkTabsView; mPrimaryWorkViewPager = primaryWorkViewPager; mCurrentRecyclerView = mPrimaryRecyclerView; mTabsHeight = tabsHeight; } /** Sets the current active {@link WidgetsRecyclerView}. */ public void setCurrentRecyclerView(WidgetsRecyclerView currentRecyclerView) { mCurrentRecyclerView = currentRecyclerView; mCurrentRecyclerView = currentRecyclerView; mViewHolder.mHeaderTitle.setTranslationY(0); mViewHolder.mRecommendedWidgetsTable.setTranslationY(0); mViewHolder.mSearchBar.setTranslationY(0); if (mHasWorkProfile) { mPrimaryWorkTabsView.setTranslationY(0); } } /** * Updates the margin and padding of {@link WidgetsFullSheet} to accumulate collapsible views. * * @return {@code true} if margins or/and padding of views in the search and recommendations * container have been updated. */ public boolean updateMarginAndPadding() { boolean hasMarginOrPaddingUpdated = false; mCollapsibleHeightForSearch = measureHeightWithVerticalMargins(mViewHolder.mHeaderTitle); mCollapsibleHeightForRecommendation = measureHeightWithVerticalMargins(mViewHolder.mHeaderTitle) + measureHeightWithVerticalMargins(mViewHolder.mCollapseHandle) + measureHeightWithVerticalMargins((View) mViewHolder.mSearchBar) + measureHeightWithVerticalMargins(mViewHolder.mRecommendedWidgetsTable); int topContainerHeight = measureHeightWithVerticalMargins(mViewHolder.mContainer); if (mHasWorkProfile) { mCollapsibleHeightForTabs = measureHeightWithVerticalMargins(mViewHolder.mHeaderTitle) + measureHeightWithVerticalMargins(mViewHolder.mRecommendedWidgetsTable); // In a work profile setup, the full widget sheet contains the following views: // ------- (pinned) -| // Widgets (collapsible) -|---> LinearLayout for search & recommendations // Search bar (pinned) -| // Widgets recommendation (collapsible)-| // Personal | Work (pinned) // View Pager // // Views after the search & recommendations are not bound by RelativelyLayout param. // To position them on the expected location, padding & margin are added to these views // Tabs should have a padding of the height of the search & recommendations container. RelativeLayout.LayoutParams tabsLayoutParams = (RelativeLayout.LayoutParams) mPrimaryWorkTabsView.getLayoutParams(); tabsLayoutParams.topMargin = topContainerHeight; mPrimaryWorkTabsView.setLayoutParams(tabsLayoutParams); // Instead of setting the top offset directly, we split the top offset into two values: // 1. topOffsetAfterAllViewsCollapsed: this is the top offset after all collapsible // views are no longer visible on the screen. // This value is set as the margin for the view pager. // 2. mMaxCollapsibleDistance // This value is set as the padding for the recycler views in order to work with // clipToPadding="false", which is an attribute for not showing top / bottom padding // when a recycler view has not reached the top or bottom of the list. // e.g. a list of 10 entries, only 3 entries are visible at a time. // case 1: recycler view is scrolled to the top. Top padding is visible/ // (top padding) // item 1 // item 2 // item 3 // // case 2: recycler view is scrolled to the middle. No padding is visible. // item 4 // item 5 // item 6 // // case 3: recycler view is scrolled to the end. bottom padding is visible. // item 8 // item 9 // item 10 // (bottom padding): not set in this case. // // When the views are first inflated, the sum of topOffsetAfterAllViewsCollapsed and // mMaxCollapsibleDistance should equal to the top container height. int topOffsetAfterAllViewsCollapsed = topContainerHeight + mTabsHeight - mCollapsibleHeightForTabs; RelativeLayout.LayoutParams viewPagerLayoutParams = (RelativeLayout.LayoutParams) mPrimaryWorkViewPager.getLayoutParams(); if (viewPagerLayoutParams.topMargin != topOffsetAfterAllViewsCollapsed) { viewPagerLayoutParams.topMargin = topOffsetAfterAllViewsCollapsed; mPrimaryWorkViewPager.setLayoutParams(viewPagerLayoutParams); hasMarginOrPaddingUpdated = true; } if (mPrimaryRecyclerView.getPaddingTop() != mCollapsibleHeightForTabs) { mPrimaryRecyclerView.setPadding( mPrimaryRecyclerView.getPaddingLeft(), mCollapsibleHeightForTabs, mPrimaryRecyclerView.getPaddingRight(), mPrimaryRecyclerView.getPaddingBottom()); hasMarginOrPaddingUpdated = true; } if (mWorkRecyclerView.getPaddingTop() != mCollapsibleHeightForTabs) { mWorkRecyclerView.setPadding( mWorkRecyclerView.getPaddingLeft(), mCollapsibleHeightForTabs, mWorkRecyclerView.getPaddingRight(), mWorkRecyclerView.getPaddingBottom()); hasMarginOrPaddingUpdated = true; } } else { if (mPrimaryRecyclerView.getPaddingTop() != topContainerHeight) { mPrimaryRecyclerView.setPadding( mPrimaryRecyclerView.getPaddingLeft(), topContainerHeight, mPrimaryRecyclerView.getPaddingRight(), mPrimaryRecyclerView.getPaddingBottom()); hasMarginOrPaddingUpdated = true; } } if (mSearchRecyclerView.getPaddingTop() != topContainerHeight) { mSearchRecyclerView.setPadding( mSearchRecyclerView.getPaddingLeft(), topContainerHeight, mSearchRecyclerView.getPaddingRight(), mSearchRecyclerView.getPaddingBottom()); hasMarginOrPaddingUpdated = true; } return hasMarginOrPaddingUpdated; } /** * Changes the displacement of collapsible views (e.g. title & widget recommendations) and fixed * views (e.g. recycler views, tabs) upon scrolling. */ @Override public void onThumbOffsetYChanged(int unused) { // Always use the recycler view offset because fast scroller offset has a different scale. int recyclerViewYOffset = mCurrentRecyclerView.getCurrentScrollY(); if (recyclerViewYOffset < 0) return; if (mCollapsibleHeightForRecommendation > 0) { int yDisplacement = Math.max(-recyclerViewYOffset, -mCollapsibleHeightForRecommendation); mViewHolder.mHeaderTitle.setTranslationY(yDisplacement); mViewHolder.mRecommendedWidgetsTable.setTranslationY(yDisplacement); } if (mCollapsibleHeightForSearch > 0) { int searchYDisplacement = Math.max(-recyclerViewYOffset, -mCollapsibleHeightForSearch); mViewHolder.mSearchBar.setTranslationY(searchYDisplacement); } if (mHasWorkProfile && mCollapsibleHeightForTabs > 0) { int yDisplacementForTabs = Math.max(-recyclerViewYOffset, -mCollapsibleHeightForTabs); mPrimaryWorkTabsView.setTranslationY(yDisplacementForTabs); } } /** Resets any previous view translation. */ public void reset() { mViewHolder.mHeaderTitle.setTranslationY(0); mViewHolder.mSearchBar.setTranslationY(0); if (mHasWorkProfile) { mPrimaryWorkTabsView.setTranslationY(0); } } /** private the height, in pixel, + the vertical margins of a given view. */ private static int measureHeightWithVerticalMargins(View view) { if (view.getVisibility() != View.VISIBLE) { return 0; } MarginLayoutParams marginLayoutParams = (MarginLayoutParams) view.getLayoutParams(); return view.getMeasuredHeight() + marginLayoutParams.bottomMargin + marginLayoutParams.topMargin; } }