| /* |
| * Copyright (C) 2022 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.allapps; |
| |
| import static com.android.launcher3.Flags.enableExpandingPauseWorkButton; |
| import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.MAIN; |
| import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.SEARCH; |
| import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_WORK_DISABLED_CARD; |
| import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_WORK_EDU_CARD; |
| import static com.android.launcher3.config.FeatureFlags.ALL_APPS_GONE_VISIBILITY; |
| import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_RV_PREINFLATION; |
| import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_COUNT; |
| import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB; |
| import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB; |
| import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; |
| import static com.android.launcher3.util.ScrollableLayoutManager.PREDICTIVE_BACK_MIN_SCALE; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.animation.ValueAnimator; |
| import android.content.Context; |
| import android.graphics.Canvas; |
| import android.graphics.Color; |
| import android.graphics.Outline; |
| import android.graphics.Paint; |
| import android.graphics.Path; |
| import android.graphics.Path.Direction; |
| import android.graphics.Point; |
| import android.graphics.Rect; |
| import android.graphics.RectF; |
| import android.os.Bundle; |
| import android.os.Parcelable; |
| import android.os.Process; |
| import android.os.UserManager; |
| import android.util.AttributeSet; |
| import android.util.FloatProperty; |
| import android.util.Log; |
| import android.util.SparseArray; |
| import android.view.KeyEvent; |
| import android.view.LayoutInflater; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.ViewOutlineProvider; |
| import android.view.WindowInsets; |
| import android.widget.Button; |
| import android.widget.RelativeLayout; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.annotation.Px; |
| import androidx.annotation.VisibleForTesting; |
| import androidx.core.graphics.ColorUtils; |
| import androidx.recyclerview.widget.RecyclerView; |
| |
| import com.android.launcher3.DeviceProfile; |
| import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener; |
| import com.android.launcher3.DragSource; |
| import com.android.launcher3.DropTarget.DragObject; |
| import com.android.launcher3.Flags; |
| import com.android.launcher3.Insettable; |
| import com.android.launcher3.InsettableFrameLayout; |
| import com.android.launcher3.R; |
| import com.android.launcher3.Utilities; |
| import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem; |
| import com.android.launcher3.allapps.search.AllAppsSearchUiDelegate; |
| import com.android.launcher3.allapps.search.SearchAdapterProvider; |
| import com.android.launcher3.config.FeatureFlags; |
| import com.android.launcher3.keyboard.FocusedItemDecorator; |
| import com.android.launcher3.keyboard.ViewGroupFocusHelper; |
| import com.android.launcher3.model.StringCache; |
| import com.android.launcher3.model.data.ItemInfo; |
| import com.android.launcher3.pm.UserCache; |
| import com.android.launcher3.recyclerview.AllAppsRecyclerViewPool; |
| import com.android.launcher3.util.ItemInfoMatcher; |
| import com.android.launcher3.util.Themes; |
| import com.android.launcher3.views.ActivityContext; |
| import com.android.launcher3.views.BaseDragLayer; |
| import com.android.launcher3.views.RecyclerViewFastScroller; |
| import com.android.launcher3.views.ScrimView; |
| import com.android.launcher3.views.SpringRelativeLayout; |
| import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.function.Predicate; |
| import java.util.stream.Stream; |
| |
| /** |
| * All apps container view with search support for use in a dragging activity. |
| * |
| * @param <T> Type of context inflating all apps. |
| */ |
| public class ActivityAllAppsContainerView<T extends Context & ActivityContext> |
| extends SpringRelativeLayout implements DragSource, Insettable, |
| OnDeviceProfileChangeListener, PersonalWorkSlidingTabStrip.OnActivePageChangedListener, |
| ScrimView.ScrimDrawingController { |
| |
| |
| public static final FloatProperty<ActivityAllAppsContainerView<?>> BOTTOM_SHEET_ALPHA = |
| new FloatProperty<>("bottomSheetAlpha") { |
| @Override |
| public Float get(ActivityAllAppsContainerView<?> containerView) { |
| return containerView.mBottomSheetAlpha; |
| } |
| |
| @Override |
| public void setValue(ActivityAllAppsContainerView<?> containerView, float v) { |
| containerView.setBottomSheetAlpha(v); |
| } |
| }; |
| |
| public static final float PULL_MULTIPLIER = .02f; |
| public static final float FLING_VELOCITY_MULTIPLIER = 1200f; |
| protected static final String BUNDLE_KEY_CURRENT_PAGE = "launcher.allapps.current_page"; |
| private static final int SCROLL_TO_BOTTOM_DURATION = 500; |
| // As of this writing, search transition does not seem to work properly, so set duration to 0. |
| private static final long DEFAULT_SEARCH_TRANSITION_DURATION_MS = 0; |
| // Render the header protection at all times to debug clipping issues. |
| private static final boolean DEBUG_HEADER_PROTECTION = false; |
| /** Context of an activity or window that is inflating this container. */ |
| |
| protected final T mActivityContext; |
| protected final List<AdapterHolder> mAH; |
| protected final Predicate<ItemInfo> mPersonalMatcher = ItemInfoMatcher.ofUser( |
| Process.myUserHandle()); |
| protected WorkProfileManager mWorkManager; |
| protected final PrivateProfileManager mPrivateProfileManager; |
| protected final Point mFastScrollerOffset = new Point(); |
| protected final int mScrimColor; |
| protected final float mHeaderThreshold; |
| protected final AllAppsSearchUiDelegate mSearchUiDelegate; |
| |
| // Used to animate Search results out and A-Z apps in, or vice-versa. |
| private final SearchTransitionController mSearchTransitionController; |
| private final Paint mHeaderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); |
| private final Rect mInsets = new Rect(); |
| private final AllAppsStore<T> mAllAppsStore; |
| private final RecyclerView.OnScrollListener mScrollListener = |
| new RecyclerView.OnScrollListener() { |
| @Override |
| public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { |
| updateHeaderScroll(recyclerView.computeVerticalScrollOffset()); |
| } |
| }; |
| private final Paint mNavBarScrimPaint; |
| private final int mHeaderProtectionColor; |
| private final Path mTmpPath = new Path(); |
| private final RectF mTmpRectF = new RectF(); |
| protected AllAppsPagedView mViewPager; |
| protected FloatingHeaderView mHeader; |
| protected View mBottomSheetBackground; |
| protected RecyclerViewFastScroller mFastScroller; |
| |
| /** |
| * View that defines the search box. Result is rendered inside {@link #mSearchRecyclerView}. |
| */ |
| protected View mSearchContainer; |
| protected SearchUiManager mSearchUiManager; |
| protected boolean mUsingTabs; |
| protected RecyclerViewFastScroller mTouchHandler; |
| |
| /** {@code true} when rendered view is in search state instead of the scroll state. */ |
| private boolean mIsSearching; |
| private boolean mRebindAdaptersAfterSearchAnimation; |
| private int mNavBarScrimHeight = 0; |
| private SearchRecyclerView mSearchRecyclerView; |
| protected SearchAdapterProvider<?> mMainAdapterProvider; |
| private View mBottomSheetHandleArea; |
| private boolean mHasWorkApps; |
| private boolean mHasPrivateApps; |
| private float[] mBottomSheetCornerRadii; |
| private ScrimView mScrimView; |
| private int mHeaderColor; |
| private int mBottomSheetBackgroundColor; |
| private float mBottomSheetAlpha = 1f; |
| private boolean mForceBottomSheetVisible; |
| private int mTabsProtectionAlpha; |
| @Nullable private AllAppsTransitionController mAllAppsTransitionController; |
| |
| private PrivateSpaceHeaderViewController mPrivateSpaceHeaderViewController; |
| |
| public ActivityAllAppsContainerView(Context context) { |
| this(context, null); |
| } |
| |
| public ActivityAllAppsContainerView(Context context, AttributeSet attrs) { |
| this(context, attrs, 0); |
| } |
| |
| public ActivityAllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) { |
| super(context, attrs, defStyleAttr); |
| mActivityContext = ActivityContext.lookupContext(context); |
| mAllAppsStore = new AllAppsStore<>(mActivityContext); |
| |
| mScrimColor = Themes.getAttrColor(context, R.attr.allAppsScrimColor); |
| mHeaderThreshold = getResources().getDimensionPixelSize( |
| R.dimen.dynamic_grid_cell_border_spacing); |
| mHeaderProtectionColor = Themes.getAttrColor(context, R.attr.allappsHeaderProtectionColor); |
| |
| mWorkManager = new WorkProfileManager( |
| mActivityContext.getSystemService(UserManager.class), |
| this, |
| mActivityContext.getStatsLogManager(), |
| UserCache.INSTANCE.get(mActivityContext)); |
| mPrivateProfileManager = new PrivateProfileManager( |
| mActivityContext.getSystemService(UserManager.class), |
| this, |
| mActivityContext.getStatsLogManager(), |
| UserCache.INSTANCE.get(mActivityContext)); |
| mAH = Arrays.asList(null, null, null); |
| mNavBarScrimPaint = new Paint(); |
| mNavBarScrimPaint.setColor(Themes.getNavBarScrimColor(mActivityContext)); |
| |
| AllAppsStore.OnUpdateListener onAppsUpdated = this::onAppsUpdated; |
| mAllAppsStore.addUpdateListener(onAppsUpdated); |
| |
| // This is a focus listener that proxies focus from a view into the list view. This is to |
| // work around the search box from getting first focus and showing the cursor. |
| setOnFocusChangeListener((v, hasFocus) -> { |
| if (hasFocus && getActiveRecyclerView() != null) { |
| getActiveRecyclerView().requestFocus(); |
| } |
| }); |
| mSearchUiDelegate = createSearchUiDelegate(); |
| initContent(); |
| |
| mSearchTransitionController = new SearchTransitionController(this); |
| } |
| |
| /** Creates the delegate for initializing search. */ |
| protected AllAppsSearchUiDelegate createSearchUiDelegate() { |
| return new AllAppsSearchUiDelegate(this); |
| } |
| |
| public AllAppsSearchUiDelegate getSearchUiDelegate() { |
| return mSearchUiDelegate; |
| } |
| |
| /** |
| * Initializes the view hierarchy and internal variables. Any initialization which actually uses |
| * these members should be done in {@link #onFinishInflate()}. |
| * In terms of subclass initialization, the following would be parallel order for activity: |
| * initContent -> onPreCreate |
| * constructor/init -> onCreate |
| * onFinishInflate -> onPostCreate |
| */ |
| protected void initContent() { |
| mMainAdapterProvider = mSearchUiDelegate.createMainAdapterProvider(); |
| if (Flags.enablePrivateSpace()) { |
| mPrivateSpaceHeaderViewController = |
| new PrivateSpaceHeaderViewController(this, mPrivateProfileManager); |
| } |
| |
| mAH.set(AdapterHolder.MAIN, new AdapterHolder(AdapterHolder.MAIN, |
| new AlphabeticalAppsList<>(mActivityContext, |
| mAllAppsStore, |
| null, |
| mPrivateProfileManager))); |
| mAH.set(AdapterHolder.WORK, new AdapterHolder(AdapterHolder.WORK, |
| new AlphabeticalAppsList<>(mActivityContext, mAllAppsStore, mWorkManager, null))); |
| mAH.set(SEARCH, new AdapterHolder(SEARCH, |
| new AlphabeticalAppsList<>(mActivityContext, null, null, null))); |
| |
| getLayoutInflater().inflate(R.layout.all_apps_content, this); |
| mHeader = findViewById(R.id.all_apps_header); |
| mBottomSheetBackground = findViewById(R.id.bottom_sheet_background); |
| mBottomSheetHandleArea = findViewById(R.id.bottom_sheet_handle_area); |
| mSearchRecyclerView = findViewById(R.id.search_results_list_view); |
| mFastScroller = findViewById(R.id.fast_scroller); |
| mFastScroller.setPopupView(findViewById(R.id.fast_scroller_popup)); |
| |
| mSearchContainer = inflateSearchBar(); |
| if (!isSearchBarFloating()) { |
| // Add the search box above everything else in this container (if the flag is enabled, |
| // it's added to drag layer in onAttach instead). |
| addView(mSearchContainer); |
| } |
| mSearchUiManager = (SearchUiManager) mSearchContainer; |
| } |
| |
| @Override |
| protected void onFinishInflate() { |
| super.onFinishInflate(); |
| |
| mAH.get(SEARCH).setup(mSearchRecyclerView, |
| /* Filter out A-Z apps */ itemInfo -> false); |
| rebindAdapters(true /* force */); |
| float cornerRadius = Themes.getDialogCornerRadius(getContext()); |
| mBottomSheetCornerRadii = new float[]{ |
| cornerRadius, |
| cornerRadius, // Top left radius in px |
| cornerRadius, |
| cornerRadius, // Top right radius in px |
| 0, |
| 0, // Bottom right |
| 0, |
| 0 // Bottom left |
| }; |
| mBottomSheetBackgroundColor = |
| Themes.getAttrColor(getContext(), R.attr.materialColorSurfaceDim); |
| updateBackgroundVisibility(mActivityContext.getDeviceProfile()); |
| mSearchUiManager.initializeSearch(this); |
| } |
| |
| @Override |
| protected void onAttachedToWindow() { |
| super.onAttachedToWindow(); |
| if (isSearchBarFloating()) { |
| // Note: for Taskbar this is removed in TaskbarAllAppsController#cleanUpOverlay when the |
| // panel is closed. Can't do so in onDetach because we are also a child of drag layer |
| // so can't remove its views during that dispatch. |
| mActivityContext.getDragLayer().addView(mSearchContainer); |
| mSearchUiDelegate.onInitializeSearchBar(); |
| } |
| mActivityContext.addOnDeviceProfileChangeListener(this); |
| } |
| |
| @Override |
| protected void onDetachedFromWindow() { |
| super.onDetachedFromWindow(); |
| mActivityContext.removeOnDeviceProfileChangeListener(this); |
| } |
| |
| public SearchUiManager getSearchUiManager() { |
| return mSearchUiManager; |
| } |
| |
| public View getBottomSheetBackground() { |
| return mBottomSheetBackground; |
| } |
| |
| /** |
| * Temporarily force the bottom sheet to be visible on non-tablets. |
| * |
| * @param force {@code true} means bottom sheet will be visible on phones until {@code reset()}. |
| */ |
| public void forceBottomSheetVisible(boolean force) { |
| mForceBottomSheetVisible = force; |
| updateBackgroundVisibility(mActivityContext.getDeviceProfile()); |
| } |
| |
| public View getSearchView() { |
| return mSearchContainer; |
| } |
| |
| /** Invoke when the current search session is finished. */ |
| public void onClearSearchResult() { |
| getMainAdapterProvider().clearHighlightedItem(); |
| animateToSearchState(false); |
| rebindAdapters(); |
| } |
| |
| /** |
| * Sets results list for search |
| */ |
| public void setSearchResults(ArrayList<AdapterItem> results) { |
| getMainAdapterProvider().clearHighlightedItem(); |
| if (getSearchResultList().setSearchResults(results)) { |
| getSearchRecyclerView().onSearchResultsChanged(); |
| } |
| if (results != null) { |
| animateToSearchState(true); |
| } |
| } |
| |
| /** |
| * Sets results list for search. |
| * |
| * @param searchResultCode indicates if the result is final or intermediate for a given query |
| * since we can get search results from multiple sources. |
| */ |
| public void setSearchResults(ArrayList<AdapterItem> results, int searchResultCode) { |
| setSearchResults(results); |
| mSearchUiDelegate.onSearchResultsChanged(results, searchResultCode); |
| } |
| |
| private void animateToSearchState(boolean goingToSearch) { |
| animateToSearchState(goingToSearch, DEFAULT_SEARCH_TRANSITION_DURATION_MS); |
| } |
| |
| public void setAllAppsTransitionController( |
| AllAppsTransitionController allAppsTransitionController) { |
| mAllAppsTransitionController = allAppsTransitionController; |
| } |
| |
| private void animateToSearchState(boolean goingToSearch, long durationMs) { |
| if (!mSearchTransitionController.isRunning() && goingToSearch == isSearching()) { |
| return; |
| } |
| mFastScroller.setVisibility(goingToSearch ? INVISIBLE : VISIBLE); |
| if (goingToSearch) { |
| // Fade out the button to pause work apps. |
| mWorkManager.onActivePageChanged(SEARCH); |
| } else if (mAllAppsTransitionController != null) { |
| // If exiting search, revert predictive back scale on all apps |
| mAllAppsTransitionController.animateAllAppsToNoScale(); |
| } |
| setScrollbarVisibility(!goingToSearch); |
| mSearchTransitionController.animateToState(goingToSearch, durationMs, |
| /* onEndRunnable = */ () -> { |
| mIsSearching = goingToSearch; |
| updateSearchResultsVisibility(); |
| int previousPage = getCurrentPage(); |
| if (mRebindAdaptersAfterSearchAnimation) { |
| rebindAdapters(false); |
| mRebindAdaptersAfterSearchAnimation = false; |
| } |
| |
| if (goingToSearch) { |
| mSearchUiDelegate.onAnimateToSearchStateCompleted(); |
| } else { |
| setSearchResults(null); |
| if (mViewPager != null) { |
| mViewPager.setCurrentPage(previousPage); |
| } |
| onActivePageChanged(previousPage); |
| } |
| }); |
| } |
| |
| public boolean shouldContainerScroll(MotionEvent ev) { |
| BaseDragLayer dragLayer = mActivityContext.getDragLayer(); |
| // IF the MotionEvent is inside the search box or handle area, and the container keeps on |
| // receiving touch input, container should move down. |
| if (dragLayer.isEventOverView(mSearchContainer, ev) |
| || dragLayer.isEventOverView(mBottomSheetHandleArea, ev)) { |
| return true; |
| } |
| AllAppsRecyclerView rv = getActiveRecyclerView(); |
| if (rv == null) { |
| return true; |
| } |
| if (rv.getScrollbar() != null |
| && rv.getScrollbar().getThumbOffsetY() >= 0 |
| && dragLayer.isEventOverView(rv.getScrollbar(), ev)) { |
| return false; |
| } |
| // Scroll if not within the container view (e.g. over large-screen scrim). |
| if (!dragLayer.isEventOverView(getVisibleContainerView(), ev)) { |
| return true; |
| } |
| return rv.shouldContainerScroll(ev, dragLayer); |
| } |
| |
| /** |
| * Resets the UI to be ready for fresh interactions in the future. Exits search and returns to |
| * A-Z apps list. |
| * |
| * @param animate Whether to animate the header during the reset (e.g. switching profile tabs). |
| */ |
| public void reset(boolean animate) { |
| reset(animate, true); |
| } |
| |
| /** |
| * Resets the UI to be ready for fresh interactions in the future. |
| * |
| * @param animate Whether to animate the header during the reset (e.g. switching profile tabs). |
| * @param exitSearch Whether to force exit the search state and return to A-Z apps list. |
| */ |
| public void reset(boolean animate, boolean exitSearch) { |
| for (int i = 0; i < mAH.size(); i++) { |
| if (mAH.get(i).mRecyclerView != null) { |
| mAH.get(i).mRecyclerView.scrollToTop(); |
| } |
| } |
| if (mTouchHandler != null) { |
| mTouchHandler.endFastScrolling(); |
| } |
| if (mHeader != null && mHeader.getVisibility() == VISIBLE) { |
| mHeader.reset(animate); |
| } |
| forceBottomSheetVisible(false); |
| // Reset the base recycler view after transitioning home. |
| updateHeaderScroll(0); |
| if (exitSearch) { |
| // Reset the search bar after transitioning home. |
| MAIN_EXECUTOR.getHandler().post(mSearchUiManager::resetSearch); |
| // Animate to A-Z with 0 time to reset the animation with proper state management. |
| animateToSearchState(false, 0); |
| } |
| if (isSearching()) { |
| mWorkManager.reset(); |
| } |
| } |
| |
| /** |
| * Exits search and returns to A-Z apps list. Scroll to the bottom. |
| */ |
| public void resetAndScrollToBottom() { |
| if (mTouchHandler != null) { |
| mTouchHandler.endFastScrolling(); |
| } |
| |
| // Reset the base recycler view after transitioning home. |
| updateHeaderScroll(0); |
| |
| // Animate to A-Z with 0 time to reset the animation with proper state management. |
| animateToSearchState(false, 0); |
| |
| MAIN_EXECUTOR.getHandler().post(() -> { |
| // Reset the search bar after transitioning home. |
| mSearchUiManager.resetSearch(); |
| // Switch to the main tab |
| switchToTab(ActivityAllAppsContainerView.AdapterHolder.MAIN); |
| // Scroll to bottom |
| getActiveRecyclerView().scrollToBottomWithMotion(SCROLL_TO_BOTTOM_DURATION); |
| }); |
| } |
| |
| @Override |
| public boolean dispatchKeyEvent(KeyEvent event) { |
| mSearchUiManager.preDispatchKeyEvent(event); |
| return super.dispatchKeyEvent(event); |
| } |
| |
| public String getDescription() { |
| if (!mUsingTabs && isSearching()) { |
| return getContext().getString(R.string.all_apps_search_results); |
| } else { |
| StringCache cache = mActivityContext.getStringCache(); |
| if (mUsingTabs) { |
| if (cache != null) { |
| return isPersonalTab() |
| ? cache.allAppsPersonalTabAccessibility |
| : cache.allAppsWorkTabAccessibility; |
| } else { |
| return isPersonalTab() |
| ? getContext().getString(R.string.all_apps_button_personal_label) |
| : getContext().getString(R.string.all_apps_button_work_label); |
| } |
| } |
| return getContext().getString(R.string.all_apps_button_label); |
| } |
| } |
| |
| public boolean isSearching() { |
| return mIsSearching; |
| } |
| |
| @Override |
| public void onActivePageChanged(int currentActivePage) { |
| if (mSearchTransitionController.isRunning()) { |
| // Will be called at the end of the animation. |
| return; |
| } |
| if (currentActivePage != SEARCH) { |
| mActivityContext.hideKeyboard(); |
| } |
| if (mAH.get(currentActivePage).mRecyclerView != null) { |
| mAH.get(currentActivePage).mRecyclerView.bindFastScrollbar(mFastScroller); |
| } |
| // Header keeps track of active recycler view to properly render header protection. |
| mHeader.setActiveRV(currentActivePage); |
| reset(true /* animate */, !isSearching() /* exitSearch */); |
| |
| mWorkManager.onActivePageChanged(currentActivePage); |
| } |
| |
| protected void rebindAdapters() { |
| rebindAdapters(false /* force */); |
| } |
| |
| protected void rebindAdapters(boolean force) { |
| if (mSearchTransitionController.isRunning()) { |
| mRebindAdaptersAfterSearchAnimation = true; |
| return; |
| } |
| updateSearchResultsVisibility(); |
| |
| boolean showTabs = shouldShowTabs(); |
| if (showTabs == mUsingTabs && !force) { |
| return; |
| } |
| |
| if (!FeatureFlags.ENABLE_SEARCH_RESULT_BACKGROUND_DRAWABLES.get()) { |
| RecyclerView.ItemDecoration decoration = getMainAdapterProvider().getDecorator(); |
| getSearchRecyclerView().removeItemDecoration(decoration); |
| getSearchRecyclerView().addItemDecoration(decoration); |
| } |
| |
| // replaceAppsRVcontainer() needs to use both mUsingTabs value to remove the old view AND |
| // showTabs value to create new view. Hence the mUsingTabs new value assignment MUST happen |
| // after this call. |
| replaceAppsRVContainer(showTabs); |
| mUsingTabs = showTabs; |
| |
| mAllAppsStore.unregisterIconContainer(mAH.get(AdapterHolder.MAIN).mRecyclerView); |
| mAllAppsStore.unregisterIconContainer(mAH.get(AdapterHolder.WORK).mRecyclerView); |
| mAllAppsStore.unregisterIconContainer(mAH.get(AdapterHolder.SEARCH).mRecyclerView); |
| |
| final AllAppsRecyclerView mainRecyclerView; |
| final AllAppsRecyclerView workRecyclerView; |
| if (mUsingTabs) { |
| mainRecyclerView = (AllAppsRecyclerView) mViewPager.getChildAt(0); |
| workRecyclerView = (AllAppsRecyclerView) mViewPager.getChildAt(1); |
| mAH.get(AdapterHolder.MAIN).setup(mainRecyclerView, mPersonalMatcher); |
| mAH.get(AdapterHolder.WORK).setup(workRecyclerView, mWorkManager.getItemInfoMatcher()); |
| workRecyclerView.setId(R.id.apps_list_view_work); |
| if (enableExpandingPauseWorkButton() |
| || FeatureFlags.ENABLE_EXPANDING_PAUSE_WORK_BUTTON.get()) { |
| mAH.get(AdapterHolder.WORK).mRecyclerView.addOnScrollListener( |
| mWorkManager.newScrollListener()); |
| } |
| mViewPager.getPageIndicator().setActiveMarker(AdapterHolder.MAIN); |
| findViewById(R.id.tab_personal) |
| .setOnClickListener((View view) -> { |
| if (mViewPager.snapToPage(AdapterHolder.MAIN)) { |
| mActivityContext.getStatsLogManager().logger() |
| .log(LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB); |
| } |
| }); |
| findViewById(R.id.tab_work) |
| .setOnClickListener((View view) -> { |
| if (mViewPager.snapToPage(AdapterHolder.WORK)) { |
| mActivityContext.getStatsLogManager().logger() |
| .log(LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB); |
| } |
| }); |
| setDeviceManagementResources(); |
| if (mHeader.isSetUp()) { |
| onActivePageChanged(mViewPager.getNextPage()); |
| } |
| } else { |
| mainRecyclerView = findViewById(R.id.apps_list_view); |
| workRecyclerView = null; |
| mAH.get(AdapterHolder.MAIN).setup(mainRecyclerView, mPersonalMatcher); |
| mAH.get(AdapterHolder.WORK).mRecyclerView = null; |
| } |
| setUpCustomRecyclerViewPool( |
| mainRecyclerView, |
| workRecyclerView, |
| mAllAppsStore.getRecyclerViewPool()); |
| setupHeader(); |
| |
| if (isSearchBarFloating()) { |
| // Keep the scroller above the search bar. |
| RelativeLayout.LayoutParams scrollerLayoutParams = |
| (LayoutParams) mFastScroller.getLayoutParams(); |
| scrollerLayoutParams.bottomMargin = mSearchContainer.getHeight() |
| + getResources().getDimensionPixelSize( |
| R.dimen.fastscroll_bottom_margin_floating_search); |
| } |
| |
| mAllAppsStore.registerIconContainer(mAH.get(AdapterHolder.MAIN).mRecyclerView); |
| mAllAppsStore.registerIconContainer(mAH.get(AdapterHolder.WORK).mRecyclerView); |
| mAllAppsStore.registerIconContainer(mAH.get(AdapterHolder.SEARCH).mRecyclerView); |
| } |
| |
| /** |
| * If {@link ENABLE_ALL_APPS_RV_PREINFLATION} is enabled, wire custom |
| * {@link RecyclerView.RecycledViewPool} to main and work {@link AllAppsRecyclerView}. |
| * |
| * Then if {@link ALL_APPS_GONE_VISIBILITY} is enabled, update max pool size. This is because |
| * all apps rv's hidden visibility is changed to {@link View#GONE} from {@link View#INVISIBLE), |
| * thus we cannot rely on layout pass to update pool size. |
| */ |
| private static void setUpCustomRecyclerViewPool( |
| @NonNull AllAppsRecyclerView mainRecyclerView, |
| @Nullable AllAppsRecyclerView workRecyclerView, |
| @NonNull AllAppsRecyclerViewPool recycledViewPool) { |
| if (!ENABLE_ALL_APPS_RV_PREINFLATION.get()) { |
| return; |
| } |
| final boolean hasWorkProfile = workRecyclerView != null; |
| recycledViewPool.setHasWorkProfile(hasWorkProfile); |
| mainRecyclerView.setRecycledViewPool(recycledViewPool); |
| if (workRecyclerView != null) { |
| workRecyclerView.setRecycledViewPool(recycledViewPool); |
| } |
| if (ALL_APPS_GONE_VISIBILITY.get()) { |
| mainRecyclerView.updatePoolSize(hasWorkProfile); |
| } |
| } |
| |
| private void replaceAppsRVContainer(boolean showTabs) { |
| for (int i = AdapterHolder.MAIN; i <= AdapterHolder.WORK; i++) { |
| AdapterHolder adapterHolder = mAH.get(i); |
| if (adapterHolder.mRecyclerView != null) { |
| adapterHolder.mRecyclerView.setLayoutManager(null); |
| adapterHolder.mRecyclerView.setAdapter(null); |
| } |
| } |
| View oldView = getAppsRecyclerViewContainer(); |
| int index = indexOfChild(oldView); |
| removeView(oldView); |
| int layout = showTabs ? R.layout.all_apps_tabs : R.layout.all_apps_rv_layout; |
| final View rvContainer = getLayoutInflater().inflate(layout, this, false); |
| addView(rvContainer, index); |
| if (showTabs) { |
| mViewPager = (AllAppsPagedView) rvContainer; |
| mViewPager.initParentViews(this); |
| mViewPager.getPageIndicator().setOnActivePageChangedListener(this); |
| mViewPager.setOutlineProvider(new ViewOutlineProvider() { |
| @Override |
| public void getOutline(View view, Outline outline) { |
| @Px final int bottomOffsetPx = |
| (int) (ActivityAllAppsContainerView.this.getMeasuredHeight() |
| * PREDICTIVE_BACK_MIN_SCALE); |
| outline.setRect( |
| 0, |
| 0, |
| view.getMeasuredWidth(), |
| view.getMeasuredHeight() + bottomOffsetPx); |
| } |
| }); |
| |
| mWorkManager.reset(); |
| post(() -> mAH.get(AdapterHolder.WORK).applyPadding()); |
| |
| } else { |
| mWorkManager.detachWorkModeSwitch(); |
| mViewPager = null; |
| } |
| |
| removeCustomRules(rvContainer); |
| removeCustomRules(getSearchRecyclerView()); |
| if (!isSearchSupported()) { |
| layoutWithoutSearchContainer(rvContainer, showTabs); |
| } else if (isSearchBarFloating()) { |
| alignParentTop(rvContainer, showTabs); |
| alignParentTop(getSearchRecyclerView(), /* tabs= */ false); |
| } else { |
| layoutBelowSearchContainer(rvContainer, showTabs); |
| layoutBelowSearchContainer(getSearchRecyclerView(), /* tabs= */ false); |
| } |
| |
| updateSearchResultsVisibility(); |
| } |
| |
| void setupHeader() { |
| mHeader.setVisibility(View.VISIBLE); |
| boolean tabsHidden = !mUsingTabs; |
| mHeader.setup( |
| mAH.get(AdapterHolder.MAIN).mRecyclerView, |
| mAH.get(AdapterHolder.WORK).mRecyclerView, |
| (SearchRecyclerView) mAH.get(SEARCH).mRecyclerView, |
| getCurrentPage(), |
| tabsHidden); |
| |
| int padding = mHeader.getMaxTranslation(); |
| for (int i = 0; i < mAH.size(); i++) { |
| final AdapterHolder adapterHolder = mAH.get(i); |
| // Search and other adapters need to be handled a bit differently; otherwise, when |
| // when leaving search, the All Apps view may be noticeably shifted downward because |
| // its padding was unnecessarily impacted, and never restored, upon entering search. |
| if (i != AdapterHolder.SEARCH && !tabsHidden && mHeader.getFloatingRowsHeight() == 0) { |
| // Only the Search adapter needs padding when there are tabs but no floating rows. |
| adapterHolder.mPadding.top = 0; |
| } else { |
| adapterHolder.mPadding.top = padding; |
| } |
| adapterHolder.applyPadding(); |
| if (adapterHolder.mRecyclerView != null) { |
| adapterHolder.mRecyclerView.scrollToTop(); |
| } |
| } |
| |
| removeCustomRules(mHeader); |
| if (!isSearchSupported()) { |
| layoutWithoutSearchContainer(mHeader, false /* includeTabsMargin */); |
| } else if (isSearchBarFloating()) { |
| alignParentTop(mHeader, false /* includeTabsMargin */); |
| } else { |
| layoutBelowSearchContainer(mHeader, false /* includeTabsMargin */); |
| } |
| } |
| |
| protected void updateHeaderScroll(int scrolledOffset) { |
| float prog1 = Utilities.boundToRange((float) scrolledOffset / mHeaderThreshold, 0f, 1f); |
| int headerColor = getHeaderColor(prog1); |
| int tabsAlpha = mHeader.getPeripheralProtectionHeight() == 0 ? 0 |
| : (int) (Utilities.boundToRange( |
| (scrolledOffset + mHeader.mSnappedScrolledY) / mHeaderThreshold, 0f, 1f) |
| * 255); |
| if (headerColor != mHeaderColor || mTabsProtectionAlpha != tabsAlpha) { |
| mHeaderColor = headerColor; |
| mTabsProtectionAlpha = tabsAlpha; |
| invalidateHeader(); |
| } |
| getSearchView().setBackgroundResource(R.drawable.bg_all_apps_searchbox); |
| if (mSearchUiManager.getEditText() == null) { |
| return; |
| } |
| |
| float prog = Utilities.boundToRange((float) scrolledOffset / mHeaderThreshold, 0f, 1f); |
| boolean bgVisible = mSearchUiManager.getBackgroundVisibility(); |
| if (scrolledOffset == 0 && !isSearching()) { |
| bgVisible = true; |
| } else if (scrolledOffset > mHeaderThreshold) { |
| bgVisible = false; |
| } |
| mSearchUiManager.setBackgroundVisibility(bgVisible, 1 - prog); |
| } |
| |
| protected int getHeaderColor(float blendRatio) { |
| return ColorUtils.setAlphaComponent( |
| ColorUtils.blendARGB(mScrimColor, mHeaderProtectionColor, blendRatio), |
| (int) (mSearchContainer.getAlpha() * 255)); |
| } |
| |
| /** |
| * @return true if the search bar is floating above this container (at the bottom of the screen) |
| */ |
| protected boolean isSearchBarFloating() { |
| return mSearchUiDelegate.isSearchBarFloating(); |
| } |
| |
| /** |
| * Whether the <em>floating</em> search bar should appear as a small pill when not focused. |
| * <p> |
| * Note: This method mirrors one in LauncherState. For subclasses that use Launcher, it likely |
| * makes sense to use that method to derive an appropriate value for the current/target state. |
| */ |
| public boolean shouldFloatingSearchBarBePillWhenUnfocused() { |
| return false; |
| } |
| |
| /** |
| * How far from the bottom of the screen the <em>floating</em> search bar should rest when the |
| * IME is not present. |
| * <p> |
| * To hide offscreen, use a negative value. |
| * <p> |
| * Note: if the provided value is non-negative but less than the current bottom insets, the |
| * insets will be applied. As such, you can use 0 to default to this. |
| * <p> |
| * Note: This method mirrors one in LauncherState. For subclasses that use Launcher, it likely |
| * makes sense to use that method to derive an appropriate value for the current/target state. |
| */ |
| public int getFloatingSearchBarRestingMarginBottom() { |
| return 0; |
| } |
| |
| /** |
| * How far from the start of the screen the <em>floating</em> search bar should rest. |
| * <p> |
| * To use original margin, return a negative value. |
| * <p> |
| * Note: This method mirrors one in LauncherState. For subclasses that use Launcher, it likely |
| * makes sense to use that method to derive an appropriate value for the current/target state. |
| */ |
| public int getFloatingSearchBarRestingMarginStart() { |
| DeviceProfile dp = mActivityContext.getDeviceProfile(); |
| return dp.allAppsLeftRightMargin + dp.getAllAppsIconStartMargin(mActivityContext); |
| } |
| |
| /** |
| * How far from the end of the screen the <em>floating</em> search bar should rest. |
| * <p> |
| * To use original margin, return a negative value. |
| * <p> |
| * Note: This method mirrors one in LauncherState. For subclasses that use Launcher, it likely |
| * makes sense to use that method to derive an appropriate value for the current/target state. |
| */ |
| public int getFloatingSearchBarRestingMarginEnd() { |
| DeviceProfile dp = mActivityContext.getDeviceProfile(); |
| return dp.allAppsLeftRightMargin + dp.getAllAppsIconStartMargin(mActivityContext); |
| } |
| |
| private void layoutBelowSearchContainer(View v, boolean includeTabsMargin) { |
| if (!(v.getLayoutParams() instanceof RelativeLayout.LayoutParams)) { |
| return; |
| } |
| |
| RelativeLayout.LayoutParams layoutParams = (LayoutParams) v.getLayoutParams(); |
| layoutParams.addRule(RelativeLayout.BELOW, R.id.search_container_all_apps); |
| |
| int topMargin = getContext().getResources().getDimensionPixelSize( |
| R.dimen.all_apps_search_bar_bottom_adjustment); |
| if (includeTabsMargin) { |
| topMargin += getContext().getResources().getDimensionPixelSize( |
| R.dimen.all_apps_header_pill_height) |
| + getContext().getResources().getDimensionPixelSize( |
| R.dimen.all_apps_tabs_margin_top); |
| } |
| layoutParams.topMargin = topMargin; |
| } |
| |
| private void alignParentTop(View v, boolean includeTabsMargin) { |
| if (!(v.getLayoutParams() instanceof RelativeLayout.LayoutParams)) { |
| return; |
| } |
| |
| RelativeLayout.LayoutParams layoutParams = (LayoutParams) v.getLayoutParams(); |
| layoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP); |
| layoutParams.topMargin = |
| includeTabsMargin |
| ? getContext().getResources().getDimensionPixelSize( |
| R.dimen.all_apps_header_pill_height) |
| : 0; |
| } |
| |
| private void removeCustomRules(View v) { |
| if (!(v.getLayoutParams() instanceof RelativeLayout.LayoutParams)) { |
| return; |
| } |
| |
| RelativeLayout.LayoutParams layoutParams = (LayoutParams) v.getLayoutParams(); |
| layoutParams.removeRule(RelativeLayout.ABOVE); |
| layoutParams.removeRule(RelativeLayout.ALIGN_TOP); |
| layoutParams.removeRule(RelativeLayout.ALIGN_PARENT_TOP); |
| layoutParams.removeRule(RelativeLayout.BELOW); |
| } |
| |
| protected BaseAllAppsAdapter<T> createAdapter(AlphabeticalAppsList<T> appsList) { |
| return new AllAppsGridAdapter<>(mActivityContext, getLayoutInflater(), appsList, |
| mMainAdapterProvider, mPrivateSpaceHeaderViewController); |
| } |
| |
| // TODO(b/216683257): Remove when Taskbar All Apps supports search. |
| protected boolean isSearchSupported() { |
| return true; |
| } |
| |
| private void layoutWithoutSearchContainer(View v, boolean includeTabsMargin) { |
| if (!(v.getLayoutParams() instanceof RelativeLayout.LayoutParams)) { |
| return; |
| } |
| |
| RelativeLayout.LayoutParams layoutParams = (LayoutParams) v.getLayoutParams(); |
| layoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP); |
| layoutParams.topMargin = getContext().getResources().getDimensionPixelSize(includeTabsMargin |
| ? R.dimen.all_apps_header_pill_height |
| : R.dimen.all_apps_header_top_margin); |
| } |
| |
| public boolean isInAllApps() { |
| // TODO: Make this abstract |
| return true; |
| } |
| |
| /** |
| * Inflates the search bar |
| */ |
| protected View inflateSearchBar() { |
| return mSearchUiDelegate.inflateSearchBar(); |
| } |
| |
| /** The adapter provider for the main section. */ |
| public final SearchAdapterProvider<?> getMainAdapterProvider() { |
| return mMainAdapterProvider; |
| } |
| |
| @Override |
| protected void dispatchRestoreInstanceState(SparseArray<Parcelable> sparseArray) { |
| try { |
| // Many slice view id is not properly assigned, and hence throws null |
| // pointer exception in the underneath method. Catching the exception |
| // simply doesn't restore these slice views. This doesn't have any |
| // user visible effect because because we query them again. |
| super.dispatchRestoreInstanceState(sparseArray); |
| } catch (Exception e) { |
| Log.e("AllAppsContainerView", "restoreInstanceState viewId = 0", e); |
| } |
| |
| Bundle state = (Bundle) sparseArray.get(R.id.work_tab_state_id, null); |
| if (state != null) { |
| int currentPage = state.getInt(BUNDLE_KEY_CURRENT_PAGE, 0); |
| if (currentPage == AdapterHolder.WORK && mViewPager != null) { |
| mViewPager.setCurrentPage(currentPage); |
| rebindAdapters(); |
| } else { |
| reset(true); |
| } |
| } |
| } |
| |
| @Override |
| protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) { |
| super.dispatchSaveInstanceState(container); |
| Bundle state = new Bundle(); |
| state.putInt(BUNDLE_KEY_CURRENT_PAGE, getCurrentPage()); |
| container.put(R.id.work_tab_state_id, state); |
| } |
| |
| public AllAppsStore<T> getAppsStore() { |
| return mAllAppsStore; |
| } |
| |
| public WorkProfileManager getWorkManager() { |
| return mWorkManager; |
| } |
| |
| /** Returns whether Private Profile has been setup. */ |
| public boolean hasPrivateProfile() { |
| return mHasPrivateApps; |
| } |
| |
| @Override |
| public void onDeviceProfileChanged(DeviceProfile dp) { |
| for (AdapterHolder holder : mAH) { |
| holder.mAdapter.setAppsPerRow(dp.numShownAllAppsColumns); |
| holder.mAppsList.setNumAppsPerRowAllApps(dp.numShownAllAppsColumns); |
| if (holder.mRecyclerView != null) { |
| // Remove all views and clear the pool, while keeping the data same. After this |
| // call, all the viewHolders will be recreated. |
| holder.mRecyclerView.swapAdapter(holder.mRecyclerView.getAdapter(), true); |
| holder.mRecyclerView.getRecycledViewPool().clear(); |
| } |
| } |
| updateBackgroundVisibility(dp); |
| |
| int navBarScrimColor = Themes.getNavBarScrimColor(mActivityContext); |
| if (mNavBarScrimPaint.getColor() != navBarScrimColor) { |
| mNavBarScrimPaint.setColor(navBarScrimColor); |
| invalidate(); |
| } |
| } |
| |
| protected void updateBackgroundVisibility(DeviceProfile deviceProfile) { |
| boolean visible = deviceProfile.isTablet || mForceBottomSheetVisible; |
| mBottomSheetBackground.setVisibility(visible ? View.VISIBLE : View.GONE); |
| // Note: For tablets, the opaque background and header protection are added in drawOnScrim. |
| // For the taskbar entrypoint, the scrim is drawn by its abstract slide in view container, |
| // so its header protection is derived from this scrim instead. |
| } |
| |
| private void setBottomSheetAlpha(float alpha) { |
| // Bottom sheet alpha is always 1 for tablets. |
| mBottomSheetAlpha = mActivityContext.getDeviceProfile().isTablet ? 1f : alpha; |
| } |
| |
| @VisibleForTesting |
| public void onAppsUpdated() { |
| mHasWorkApps = Stream.of(mAllAppsStore.getApps()) |
| .anyMatch(mWorkManager.getItemInfoMatcher()); |
| mHasPrivateApps = Stream.of(mAllAppsStore.getApps()) |
| .anyMatch(mPrivateProfileManager.getItemInfoMatcher()); |
| if (!isSearching()) { |
| rebindAdapters(); |
| } |
| if (mHasWorkApps) { |
| mWorkManager.reset(); |
| } |
| if (mHasPrivateApps) { |
| mPrivateProfileManager.reset(); |
| } |
| |
| mActivityContext.getStatsLogManager().logger() |
| .withCardinality(mAllAppsStore.getApps().length) |
| .log(LAUNCHER_ALLAPPS_COUNT); |
| } |
| |
| @Override |
| public boolean onInterceptTouchEvent(MotionEvent ev) { |
| // The AllAppsContainerView houses the QSB and is hence visible from the Workspace |
| // Overview states. We shouldn't intercept for the scrubber in these cases. |
| if (!isInAllApps()) { |
| mTouchHandler = null; |
| return false; |
| } |
| |
| if (ev.getAction() == MotionEvent.ACTION_DOWN) { |
| AllAppsRecyclerView rv = getActiveRecyclerView(); |
| if (rv != null && rv.getScrollbar() != null |
| && rv.getScrollbar().isHitInParent(ev.getX(), ev.getY(), mFastScrollerOffset)) { |
| mTouchHandler = rv.getScrollbar(); |
| } else { |
| mTouchHandler = null; |
| } |
| } |
| if (mTouchHandler != null) { |
| return mTouchHandler.handleTouchEvent(ev, mFastScrollerOffset); |
| } |
| return false; |
| } |
| |
| @Override |
| public boolean onTouchEvent(MotionEvent ev) { |
| if (!isInAllApps()) { |
| return false; |
| } |
| |
| if (ev.getAction() == MotionEvent.ACTION_DOWN) { |
| AllAppsRecyclerView rv = getActiveRecyclerView(); |
| if (rv != null && rv.getScrollbar() != null |
| && rv.getScrollbar().isHitInParent(ev.getX(), ev.getY(), mFastScrollerOffset)) { |
| mTouchHandler = rv.getScrollbar(); |
| } else { |
| mTouchHandler = null; |
| |
| } |
| } |
| if (mTouchHandler != null) { |
| mTouchHandler.handleTouchEvent(ev, mFastScrollerOffset); |
| return true; |
| } |
| if (isSearching() |
| && mActivityContext.getDragLayer().isEventOverView(getVisibleContainerView(), ev)) { |
| // if in search state, consume touch event. |
| return true; |
| } |
| return false; |
| } |
| |
| /** The current active recycler view (A-Z list from one of the profiles, or search results). */ |
| public AllAppsRecyclerView getActiveRecyclerView() { |
| if (isSearching()) { |
| return getSearchRecyclerView(); |
| } |
| return getActiveAppsRecyclerView(); |
| } |
| |
| /** The current focus change listener in the search container. */ |
| public OnFocusChangeListener getSearchFocusChangeListener() { |
| return mAH.get(AdapterHolder.SEARCH).mOnFocusChangeListener; |
| } |
| |
| /** The current apps recycler view in the container. */ |
| private AllAppsRecyclerView getActiveAppsRecyclerView() { |
| if (!mUsingTabs || isPersonalTab()) { |
| return mAH.get(AdapterHolder.MAIN).mRecyclerView; |
| } else { |
| return mAH.get(AdapterHolder.WORK).mRecyclerView; |
| } |
| } |
| |
| /** |
| * The container for A-Z apps (the ViewPager for main+work tabs, or main RV). This is currently |
| * hidden while searching. |
| */ |
| public ViewGroup getAppsRecyclerViewContainer() { |
| return mViewPager != null ? mViewPager : findViewById(R.id.apps_list_view); |
| } |
| |
| /** The RV for search results, which is hidden while A-Z apps are visible. */ |
| public SearchRecyclerView getSearchRecyclerView() { |
| return mSearchRecyclerView; |
| } |
| |
| protected boolean isPersonalTab() { |
| return mViewPager == null || mViewPager.getNextPage() == 0; |
| } |
| |
| /** |
| * Switches the current page to the provided {@code tab} if tabs are supported, otherwise does |
| * nothing. |
| */ |
| public void switchToTab(int tab) { |
| if (mUsingTabs) { |
| mViewPager.setCurrentPage(tab); |
| } |
| } |
| |
| public LayoutInflater getLayoutInflater() { |
| return mSearchUiDelegate.getLayoutInflater(); |
| } |
| |
| @Override |
| public void onDropCompleted(View target, DragObject d, boolean success) {} |
| |
| @Override |
| public void setInsets(Rect insets) { |
| mInsets.set(insets); |
| DeviceProfile grid = mActivityContext.getDeviceProfile(); |
| |
| applyAdapterSideAndBottomPaddings(grid); |
| |
| MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams(); |
| // Ignore left/right insets on tablet because we are already centered in-screen. |
| if (grid.isTablet) { |
| mlp.leftMargin = mlp.rightMargin = 0; |
| } else { |
| mlp.leftMargin = insets.left; |
| mlp.rightMargin = insets.right; |
| } |
| setLayoutParams(mlp); |
| |
| if (!grid.isVerticalBarLayout() || FeatureFlags.enableResponsiveWorkspace()) { |
| int topPadding = grid.allAppsPadding.top; |
| if (isSearchBarFloating() && !grid.isTablet) { |
| topPadding += getResources().getDimensionPixelSize( |
| R.dimen.all_apps_additional_top_padding_floating_search); |
| } |
| setPadding(grid.allAppsLeftRightMargin, topPadding, grid.allAppsLeftRightMargin, 0); |
| } |
| InsettableFrameLayout.dispatchInsets(this, insets); |
| } |
| |
| /** |
| * Returns a padding in case a scrim is shown on the bottom of the view and a padding is needed. |
| */ |
| protected int computeNavBarScrimHeight(WindowInsets insets) { |
| return 0; |
| } |
| |
| /** |
| * Returns the current height of nav bar scrim |
| */ |
| public int getNavBarScrimHeight() { |
| return mNavBarScrimHeight; |
| } |
| |
| @Override |
| public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) { |
| mNavBarScrimHeight = computeNavBarScrimHeight(insets); |
| applyAdapterSideAndBottomPaddings(mActivityContext.getDeviceProfile()); |
| return super.dispatchApplyWindowInsets(insets); |
| } |
| |
| @Override |
| protected void dispatchDraw(Canvas canvas) { |
| super.dispatchDraw(canvas); |
| |
| if (mNavBarScrimHeight > 0) { |
| canvas.drawRect(0, getHeight() - mNavBarScrimHeight, getWidth(), getHeight(), |
| mNavBarScrimPaint); |
| } |
| } |
| |
| protected void setScrollbarVisibility(boolean visible) { |
| AllAppsRecyclerView rv = getActiveRecyclerView(); |
| if (rv != null && rv.getScrollbar() != null) { |
| rv.getScrollbar().setVisibility(visible ? VISIBLE : GONE); |
| } |
| } |
| |
| protected void updateSearchResultsVisibility() { |
| if (isSearching()) { |
| getSearchRecyclerView().setVisibility(VISIBLE); |
| getAppsRecyclerViewContainer().setVisibility(GONE); |
| mHeader.setVisibility(GONE); |
| } else { |
| getSearchRecyclerView().setVisibility(GONE); |
| getAppsRecyclerViewContainer().setVisibility(VISIBLE); |
| mHeader.setVisibility(VISIBLE); |
| } |
| if (mHeader.isSetUp()) { |
| mHeader.setActiveRV(getCurrentPage()); |
| } |
| } |
| |
| private void applyAdapterSideAndBottomPaddings(DeviceProfile grid) { |
| int bottomPadding = Math.max(mInsets.bottom, mNavBarScrimHeight); |
| mAH.forEach(adapterHolder -> { |
| adapterHolder.mPadding.bottom = bottomPadding; |
| adapterHolder.mPadding.left = grid.allAppsPadding.left; |
| adapterHolder.mPadding.right = grid.allAppsPadding.right; |
| adapterHolder.applyPadding(); |
| }); |
| } |
| |
| private void setDeviceManagementResources() { |
| if (mActivityContext.getStringCache() != null) { |
| Button personalTab = findViewById(R.id.tab_personal); |
| personalTab.setText(mActivityContext.getStringCache().allAppsPersonalTab); |
| |
| Button workTab = findViewById(R.id.tab_work); |
| workTab.setText(mActivityContext.getStringCache().allAppsWorkTab); |
| } |
| } |
| |
| /** |
| * Returns true if the container has work apps. |
| */ |
| public boolean shouldShowTabs() { |
| return mHasWorkApps; |
| } |
| |
| // Used by tests only |
| private boolean isDescendantViewVisible(int viewId) { |
| final View view = findViewById(viewId); |
| if (view == null) return false; |
| |
| if (!view.isShown()) return false; |
| |
| return view.getGlobalVisibleRect(new Rect()); |
| } |
| |
| /** Called in Launcher#bindStringCache() to update the UI when cache is updated. */ |
| public void updateWorkUI() { |
| setDeviceManagementResources(); |
| if (mWorkManager.getWorkModeSwitch() != null) { |
| mWorkManager.getWorkModeSwitch().updateStringFromCache(); |
| } |
| inflateWorkCardsIfNeeded(); |
| } |
| |
| private void inflateWorkCardsIfNeeded() { |
| AllAppsRecyclerView workRV = mAH.get(AdapterHolder.WORK).mRecyclerView; |
| if (workRV != null) { |
| for (int i = 0; i < workRV.getChildCount(); i++) { |
| View currentView = workRV.getChildAt(i); |
| int currentItemViewType = workRV.getChildViewHolder(currentView).getItemViewType(); |
| if (currentItemViewType == VIEW_TYPE_WORK_EDU_CARD) { |
| ((WorkEduCard) currentView).updateStringFromCache(); |
| } else if (currentItemViewType == VIEW_TYPE_WORK_DISABLED_CARD) { |
| ((WorkPausedCard) currentView).updateStringFromCache(); |
| } |
| } |
| } |
| } |
| |
| @VisibleForTesting |
| public void setWorkManager(WorkProfileManager workManager) { |
| mWorkManager = workManager; |
| } |
| |
| @VisibleForTesting |
| public boolean isPersonalTabVisible() { |
| return isDescendantViewVisible(R.id.tab_personal); |
| } |
| |
| @VisibleForTesting |
| public boolean isWorkTabVisible() { |
| return isDescendantViewVisible(R.id.tab_work); |
| } |
| |
| public AlphabeticalAppsList<T> getSearchResultList() { |
| return mAH.get(SEARCH).mAppsList; |
| } |
| |
| public AlphabeticalAppsList<T> getPersonalAppList() { |
| return mAH.get(MAIN).mAppsList; |
| } |
| |
| public FloatingHeaderView getFloatingHeaderView() { |
| return mHeader; |
| } |
| |
| @VisibleForTesting |
| public View getContentView() { |
| return isSearching() ? getSearchRecyclerView() : getAppsRecyclerViewContainer(); |
| } |
| |
| /** The current page visible in all apps. */ |
| public int getCurrentPage() { |
| return isSearching() |
| ? SEARCH |
| : mViewPager == null ? AdapterHolder.MAIN : mViewPager.getNextPage(); |
| } |
| |
| public PrivateProfileManager getPrivateProfileManager() { |
| return mPrivateProfileManager; |
| } |
| |
| /** |
| * Adds an update listener to animator that adds springs to the animation. |
| */ |
| public void addSpringFromFlingUpdateListener(ValueAnimator animator, |
| float velocity /* release velocity */, |
| float progress /* portion of the distance to travel*/) { |
| animator.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationStart(Animator animator) { |
| float distance = (1 - progress) * getHeight(); // px |
| float settleVelocity = Math.min(0, distance |
| / (AllAppsTransitionController.INTERP_COEFF * animator.getDuration()) |
| + velocity); |
| absorbSwipeUpVelocity(Math.max(1000, Math.abs( |
| Math.round(settleVelocity * FLING_VELOCITY_MULTIPLIER)))); |
| } |
| }); |
| } |
| |
| /** Invoked when the container is pulled. */ |
| public void onPull(float deltaDistance, float displacement) { |
| absorbPullDeltaDistance(PULL_MULTIPLIER * deltaDistance, PULL_MULTIPLIER * displacement); |
| // Current motion spec is to actually push and not pull |
| // on this surface. However, until EdgeEffect.onPush (b/190612804) is |
| // implemented at view level, we will simply pull |
| } |
| |
| @Override |
| public void getDrawingRect(Rect outRect) { |
| super.getDrawingRect(outRect); |
| outRect.offset(0, (int) getTranslationY()); |
| } |
| |
| @Override |
| public void setTranslationY(float translationY) { |
| super.setTranslationY(translationY); |
| invalidateHeader(); |
| } |
| |
| public void setScrimView(ScrimView scrimView) { |
| mScrimView = scrimView; |
| } |
| |
| @Override |
| public void drawOnScrimWithScale(Canvas canvas, float scale) { |
| final View panel = mBottomSheetBackground; |
| final boolean hasBottomSheet = panel.getVisibility() == VISIBLE; |
| final float translationY = ((View) panel.getParent()).getTranslationY(); |
| |
| final float horizontalScaleOffset = (1 - scale) * panel.getWidth() / 2; |
| final float verticalScaleOffset = (1 - scale) * (panel.getHeight() - getHeight() / 2); |
| |
| final float topNoScale = panel.getTop() + translationY; |
| final float topWithScale = topNoScale + verticalScaleOffset; |
| final float leftWithScale = panel.getLeft() + horizontalScaleOffset; |
| final float rightWithScale = panel.getRight() - horizontalScaleOffset; |
| // Draw full background panel for tablets. |
| if (hasBottomSheet) { |
| mHeaderPaint.setColor(mBottomSheetBackgroundColor); |
| mHeaderPaint.setAlpha((int) (255 * mBottomSheetAlpha)); |
| |
| mTmpRectF.set( |
| leftWithScale, |
| topWithScale, |
| rightWithScale, |
| panel.getBottom()); |
| mTmpPath.reset(); |
| mTmpPath.addRoundRect(mTmpRectF, mBottomSheetCornerRadii, Direction.CW); |
| canvas.drawPath(mTmpPath, mHeaderPaint); |
| } |
| |
| if (DEBUG_HEADER_PROTECTION) { |
| mHeaderPaint.setColor(Color.MAGENTA); |
| mHeaderPaint.setAlpha(255); |
| } else { |
| mHeaderPaint.setColor(mHeaderColor); |
| mHeaderPaint.setAlpha((int) (getAlpha() * Color.alpha(mHeaderColor))); |
| } |
| if (mHeaderPaint.getColor() == mScrimColor || mHeaderPaint.getColor() == 0) { |
| return; |
| } |
| |
| // Draw header on background panel |
| final float headerBottomNoScale = |
| getHeaderBottom() + getVisibleContainerView().getPaddingTop(); |
| final float headerHeightNoScale = headerBottomNoScale - topNoScale; |
| final float headerBottomWithScaleOnTablet = topWithScale + headerHeightNoScale * scale; |
| final float headerBottomOffset = (getVisibleContainerView().getHeight() * (1 - scale) / 2); |
| final float headerBottomWithScaleOnPhone = headerBottomNoScale * scale + headerBottomOffset; |
| final FloatingHeaderView headerView = getFloatingHeaderView(); |
| if (hasBottomSheet) { |
| // Start adding header protection if search bar or tabs will attach to the top. |
| if (!isSearchBarFloating() || mUsingTabs) { |
| mTmpRectF.set( |
| leftWithScale, |
| topWithScale, |
| rightWithScale, |
| headerBottomWithScaleOnTablet); |
| mTmpPath.reset(); |
| mTmpPath.addRoundRect(mTmpRectF, mBottomSheetCornerRadii, Direction.CW); |
| canvas.drawPath(mTmpPath, mHeaderPaint); |
| } |
| } else { |
| canvas.drawRect(0, 0, canvas.getWidth(), headerBottomWithScaleOnPhone, mHeaderPaint); |
| } |
| |
| // If tab exist (such as work profile), extend header with tab height |
| final int tabsHeight = headerView.getPeripheralProtectionHeight(); |
| if (mTabsProtectionAlpha > 0 && tabsHeight != 0) { |
| if (DEBUG_HEADER_PROTECTION) { |
| mHeaderPaint.setColor(Color.BLUE); |
| mHeaderPaint.setAlpha(255); |
| } else { |
| mHeaderPaint.setAlpha((int) (getAlpha() * mTabsProtectionAlpha)); |
| } |
| float left = 0f; |
| float right = canvas.getWidth(); |
| if (hasBottomSheet) { |
| left = mBottomSheetBackground.getLeft() + horizontalScaleOffset; |
| right = mBottomSheetBackground.getRight() - horizontalScaleOffset; |
| } |
| |
| final float tabTopWithScale = hasBottomSheet |
| ? headerBottomWithScaleOnTablet |
| : headerBottomWithScaleOnPhone; |
| final float tabBottomWithScale = tabTopWithScale + tabsHeight * scale; |
| |
| canvas.drawRect( |
| left, |
| tabTopWithScale, |
| right, |
| tabBottomWithScale, |
| mHeaderPaint); |
| } |
| } |
| |
| /** |
| * redraws header protection |
| */ |
| public void invalidateHeader() { |
| if (mScrimView != null) { |
| mScrimView.invalidate(); |
| } |
| } |
| |
| /** Returns the position of the bottom edge of the header */ |
| public int getHeaderBottom() { |
| int bottom = (int) getTranslationY() + mHeader.getClipTop(); |
| if (isSearchBarFloating()) { |
| if (mActivityContext.getDeviceProfile().isTablet) { |
| return bottom + mBottomSheetBackground.getTop(); |
| } |
| return bottom; |
| } |
| return bottom + mHeader.getTop(); |
| } |
| |
| /** |
| * Returns a view that denotes the visible part of all apps container view. |
| */ |
| public View getVisibleContainerView() { |
| return mBottomSheetBackground.getVisibility() == VISIBLE ? mBottomSheetBackground : this; |
| } |
| |
| protected void onInitializeRecyclerView(RecyclerView rv) { |
| rv.addOnScrollListener(mScrollListener); |
| mSearchUiDelegate.onInitializeRecyclerView(rv); |
| } |
| |
| /** Returns the instance of @{code SearchTransitionController}. */ |
| public SearchTransitionController getSearchTransitionController() { |
| return mSearchTransitionController; |
| } |
| |
| /** Holds a {@link BaseAllAppsAdapter} and related fields. */ |
| public class AdapterHolder { |
| public static final int MAIN = 0; |
| public static final int WORK = 1; |
| public static final int SEARCH = 2; |
| |
| private final int mType; |
| public final BaseAllAppsAdapter<T> mAdapter; |
| final RecyclerView.LayoutManager mLayoutManager; |
| final AlphabeticalAppsList<T> mAppsList; |
| final Rect mPadding = new Rect(); |
| AllAppsRecyclerView mRecyclerView; |
| private OnFocusChangeListener mOnFocusChangeListener; |
| |
| AdapterHolder(int type, AlphabeticalAppsList<T> appsList) { |
| mType = type; |
| mAppsList = appsList; |
| mAdapter = createAdapter(mAppsList); |
| mAppsList.setAdapter(mAdapter); |
| mLayoutManager = mAdapter.getLayoutManager(); |
| } |
| |
| void setup(@NonNull View rv, @Nullable Predicate<ItemInfo> matcher) { |
| mAppsList.updateItemFilter(matcher); |
| mRecyclerView = (AllAppsRecyclerView) rv; |
| mRecyclerView.bindFastScrollbar(mFastScroller); |
| mRecyclerView.setEdgeEffectFactory(createEdgeEffectFactory()); |
| mRecyclerView.setApps(mAppsList); |
| mRecyclerView.setLayoutManager(mLayoutManager); |
| mRecyclerView.setAdapter(mAdapter); |
| mRecyclerView.setHasFixedSize(true); |
| // No animations will occur when changes occur to the items in this RecyclerView. |
| mRecyclerView.setItemAnimator(null); |
| onInitializeRecyclerView(mRecyclerView); |
| // Use ViewGroupFocusHelper for SearchRecyclerView to draw focus outline for the |
| // buttons in the view (e.g. query builder button and setting button) |
| FocusedItemDecorator focusedItemDecorator = isSearch() ? new FocusedItemDecorator( |
| new ViewGroupFocusHelper(mRecyclerView)) : new FocusedItemDecorator( |
| mRecyclerView); |
| mRecyclerView.addItemDecoration(focusedItemDecorator); |
| mOnFocusChangeListener = focusedItemDecorator.getFocusListener(); |
| mAdapter.setIconFocusListener(mOnFocusChangeListener); |
| applyPadding(); |
| } |
| |
| void applyPadding() { |
| if (mRecyclerView != null) { |
| int bottomOffset = 0; |
| if (isWork() && mWorkManager.getWorkModeSwitch() != null) { |
| bottomOffset = mInsets.bottom + mWorkManager.getWorkModeSwitch().getHeight(); |
| } |
| if (isSearchBarFloating()) { |
| bottomOffset += mSearchContainer.getHeight(); |
| } |
| mRecyclerView.setPadding(mPadding.left, mPadding.top, mPadding.right, |
| mPadding.bottom + bottomOffset); |
| } |
| } |
| |
| private boolean isWork() { |
| return mType == WORK; |
| } |
| |
| private boolean isSearch() { |
| return mType == SEARCH; |
| } |
| } |
| } |