Files
Flexera-Launcher/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
T
Andy Wickham 346771550e [revert ^2] Enable Floating Search and update tests accordingly.
Tests need to be updated to account for the new placement of the
searchbar because it overlapped with touch events for scrolling:
 - Scroll back to top: Instead of scrolling from the top of the
   container which could overlap status bar in landscape, scroll
   from the bottom of the top-most visible app icon.
 - Scroll down: swipe up from bottom padding to top of top-most
   visible icon.
 - Close all apps: swipe down more quickly from top icon insetad
   of the search bar (more quickly helps it be detected as a
   fling on more cramped devices).

For Launcher3, the floating flag is not fully supported yet, so
there were some layout issues which are now resolved by ignoring
the flag if the searchbar is still at the top.

Fix: 261873937
Test: Ran tests, manual
Change-Id: I406fbcbe12acddb1dd4b862a380576a48cabbebc
2023-02-03 14:24:22 -08:00

1222 lines
48 KiB
Java

/*
* 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.allapps.ActivityAllAppsContainerView.AdapterHolder.SEARCH;
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 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.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.Log;
import android.util.SparseArray;
import android.util.TypedValue;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.RelativeLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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.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.DefaultSearchAdapterProvider;
import com.android.launcher3.allapps.search.SearchAdapterProvider;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.keyboard.FocusedItemDecorator;
import com.android.launcher3.model.StringCache;
import com.android.launcher3.model.data.ItemInfo;
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 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 long DEFAULT_SEARCH_TRANSITION_DURATION_MS = 300;
// 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 final WorkProfileManager mWorkManager;
protected final Point mFastScrollerOffset = new Point();
protected final int mScrimColor;
protected final float mHeaderThreshold;
// 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 mAllAppsStore = new AllAppsStore();
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;
/**
* 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 float[] mBottomSheetCornerRadii;
private ScrimView mScrimView;
private int mHeaderColor;
private int mBottomSheetBackgroundColor;
private int mTabsProtectionAlpha;
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);
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());
mAH = Arrays.asList(null, null, null);
mNavBarScrimPaint = new Paint();
mNavBarScrimPaint.setColor(Themes.getAttrColor(context, R.attr.allAppsNavBarScrimColor));
mAllAppsStore.addUpdateListener(this::onAppsUpdated);
mActivityContext.addOnDeviceProfileChangeListener(this);
// 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();
}
});
initContent();
mSearchTransitionController = new SearchTransitionController(this);
}
/**
* 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 = createMainAdapterProvider();
mAH.set(AdapterHolder.MAIN, new AdapterHolder(AdapterHolder.MAIN,
new AlphabeticalAppsList<>(mActivityContext, mAllAppsStore, null)));
mAH.set(AdapterHolder.WORK, new AdapterHolder(AdapterHolder.WORK,
new AlphabeticalAppsList<>(mActivityContext, mAllAppsStore, mWorkManager)));
mAH.set(SEARCH, new AdapterHolder(SEARCH,
new AlphabeticalAppsList<>(mActivityContext, 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);
// Add the search box above everything else.
mSearchContainer = inflateSearchBox();
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
};
final TypedValue value = new TypedValue();
getContext().getTheme().resolveAttribute(android.R.attr.colorBackground, value, true);
mBottomSheetBackgroundColor = value.data;
updateBackground(mActivityContext.getDeviceProfile());
mSearchUiManager.initializeSearch(this);
}
public SearchUiManager getSearchUiManager() {
return mSearchUiManager;
}
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);
}
private void animateToSearchState(boolean goingToSearch) {
animateToSearchState(goingToSearch, DEFAULT_SEARCH_TRANSITION_DURATION_MS);
}
private void animateToSearchState(boolean goingToSearch, long durationMs) {
if (!mSearchTransitionController.isRunning() && goingToSearch == isSearching()) {
return;
}
if (goingToSearch) {
// Fade out the button to pause work apps.
mWorkManager.onActivePageChanged(SEARCH);
}
mSearchTransitionController.animateToSearchState(goingToSearch, durationMs,
/* onEndRunnable = */ () -> {
mIsSearching = goingToSearch;
updateSearchResultsVisibility();
int previousPage = getCurrentPage();
if (mRebindAdaptersAfterSearchAnimation) {
rebindAdapters(false);
mRebindAdaptersAfterSearchAnimation = false;
}
if (!goingToSearch) {
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, and the container keeps on receiving
// touch input, container should move down.
if (dragLayer.isEventOverView(mSearchContainer, ev)) {
return true;
}
// Scroll if not within the container view (e.g. over large-screen scrim).
if (!dragLayer.isEventOverView(getVisibleContainerView(), ev)) {
return true;
}
if (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;
}
return rv.shouldContainerScroll(ev, dragLayer);
}
public void reset(boolean animate) {
for (int i = 0; i < mAH.size(); i++) {
if (mAH.get(i).mRecyclerView != null) {
mAH.get(i).mRecyclerView.scrollToTop();
}
}
if (mHeader != null && mHeader.getVisibility() == VISIBLE) {
mHeader.reset(animate);
}
// Reset the base recycler view after transitioning home.
updateHeaderScroll(0);
// Reset the search bar after transitioning home.
mSearchUiManager.resetSearch();
// Animate to A-Z with 0 time to reset the animation with proper state management.
animateToSearchState(false, 0);
}
@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 (mAH.get(currentActivePage).mRecyclerView != null) {
mAH.get(currentActivePage).mRecyclerView.bindFastScrollbar();
}
// Header keeps track of active recycler view to properly render header protection.
mHeader.setActiveRV(currentActivePage);
reset(true /* animate */);
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 (isSearching()) {
mUsingTabs = showTabs;
mWorkManager.detachWorkModeSwitch();
return;
}
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);
if (mUsingTabs) {
mAH.get(AdapterHolder.MAIN).setup(mViewPager.getChildAt(0), mPersonalMatcher);
mAH.get(AdapterHolder.WORK).setup(mViewPager.getChildAt(1), mWorkManager.getMatcher());
mAH.get(AdapterHolder.WORK).mRecyclerView.setId(R.id.apps_list_view_work);
if (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);
}
mActivityContext.hideKeyboard();
});
findViewById(R.id.tab_work)
.setOnClickListener((View view) -> {
if (mViewPager.snapToPage(AdapterHolder.WORK)) {
mActivityContext.getStatsLogManager().logger()
.log(LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB);
}
mActivityContext.hideKeyboard();
});
setDeviceManagementResources();
onActivePageChanged(mViewPager.getNextPage());
} else {
mAH.get(AdapterHolder.MAIN).setup(findViewById(R.id.apps_list_view), null);
mAH.get(AdapterHolder.WORK).mRecyclerView = null;
}
setupHeader();
if (isSearchBarOnBottom()) {
// Keep the scroller above the search bar.
RelativeLayout.LayoutParams scrollerLayoutParams =
(LayoutParams) findViewById(R.id.fast_scroller).getLayoutParams();
scrollerLayoutParams.addRule(RelativeLayout.ABOVE, R.id.search_container_all_apps);
scrollerLayoutParams.removeRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
scrollerLayoutParams.bottomMargin = 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);
}
protected View 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);
mWorkManager.reset();
post(() -> mAH.get(AdapterHolder.WORK).applyPadding());
} else {
mWorkManager.detachWorkModeSwitch();
mViewPager = null;
}
removeCustomRules(rvContainer);
removeCustomRules(getSearchRecyclerView());
if (!isSearchSupported()) {
layoutWithoutSearchContainer(rvContainer, showTabs);
} else if (isSearchBarOnBottom()) {
alignParentTop(rvContainer, showTabs);
alignParentTop(getSearchRecyclerView(), /* tabs= */ false);
layoutAboveSearchContainer(rvContainer);
layoutAboveSearchContainer(getSearchRecyclerView());
} else {
layoutBelowSearchContainer(rvContainer, showTabs);
layoutBelowSearchContainer(getSearchRecyclerView(), /* tabs= */ false);
}
return rvContainer;
}
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();
mAH.forEach(adapterHolder -> {
adapterHolder.mPadding.top = padding;
adapterHolder.applyPadding();
if (adapterHolder.mRecyclerView != null) {
adapterHolder.mRecyclerView.scrollToTop();
}
});
removeCustomRules(mHeader);
if (!isSearchSupported()) {
layoutWithoutSearchContainer(mHeader, false /* includeTabsMargin */);
} else if (isSearchBarOnBottom()) {
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();
}
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 at the bottom of the container (as opposed to the top). */
private boolean isSearchBarOnBottom() {
return FeatureFlags.ENABLE_FLOATING_SEARCH_BAR.get()
&& ((RelativeLayout.LayoutParams) mSearchContainer.getLayoutParams()).getRule(
ALIGN_PARENT_BOTTOM) == RelativeLayout.TRUE;
}
private void layoutBelowSearchContainer(View v, boolean includeTabsMargin) {
if (!(v.getLayoutParams() instanceof RelativeLayout.LayoutParams)) {
return;
}
RelativeLayout.LayoutParams layoutParams = (LayoutParams) v.getLayoutParams();
layoutParams.addRule(RelativeLayout.ALIGN_TOP, R.id.search_container_all_apps);
int topMargin = getContext().getResources().getDimensionPixelSize(
R.dimen.all_apps_header_top_margin);
if (includeTabsMargin) {
topMargin += getContext().getResources().getDimensionPixelSize(
R.dimen.all_apps_header_pill_height);
}
layoutParams.topMargin = topMargin;
}
private void layoutAboveSearchContainer(View v) {
if (!(v.getLayoutParams() instanceof RelativeLayout.LayoutParams)) {
return;
}
RelativeLayout.LayoutParams layoutParams = (LayoutParams) v.getLayoutParams();
layoutParams.addRule(RelativeLayout.ABOVE, R.id.search_container_all_apps);
}
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);
}
protected BaseAllAppsAdapter<T> createAdapter(AlphabeticalAppsList<T> appsList) {
return new AllAppsGridAdapter<>(mActivityContext, getLayoutInflater(), appsList,
mMainAdapterProvider);
}
// 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 box
*/
protected View inflateSearchBox() {
return getLayoutInflater().inflate(R.layout.search_container_all_apps, this, false);
}
/** Creates the adapter provider for the main section. */
protected SearchAdapterProvider<?> createMainAdapterProvider() {
return new DefaultSearchAdapterProvider(mActivityContext);
}
/** 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);
}
/**
* Sets the long click listener for icons
*/
public void setOnIconLongClickListener(OnLongClickListener listener) {
for (AdapterHolder holder : mAH) {
holder.mAdapter.setOnIconLongClickListener(listener);
}
}
public AllAppsStore getAppsStore() {
return mAllAppsStore;
}
public WorkProfileManager getWorkManager() {
return mWorkManager;
}
@Override
public void onDeviceProfileChanged(DeviceProfile dp) {
for (AdapterHolder holder : mAH) {
holder.mAdapter.setAppsPerRow(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();
}
}
updateBackground(dp);
}
protected void updateBackground(DeviceProfile deviceProfile) {
mBottomSheetBackground.setVisibility(deviceProfile.isTablet ? 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 differently, so a static background is
// added in TaskbarAllAppsContainerView and header protection is not yet supported.
}
private void onAppsUpdated() {
mHasWorkApps = Stream.of(mAllAppsStore.getApps()).anyMatch(mWorkManager.getMatcher());
if (!isSearching()) {
rebindAdapters();
if (mHasWorkApps) {
mWorkManager.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 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.
**/
protected View 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 LayoutInflater.from(getContext());
}
@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();
mlp.leftMargin = insets.left;
mlp.rightMargin = insets.right;
setLayoutParams(mlp);
if (grid.isVerticalBarLayout()) {
setPadding(grid.workspacePadding.left, 0, grid.workspacePadding.right, 0);
} else {
int topPadding = grid.allAppsTopPadding;
if (isSearchBarOnBottom() && !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 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 =
adapterHolder.mPadding.right = grid.allAppsLeftRightPadding;
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);
}
}
protected 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());
}
@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 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();
}
/** The scroll bar for the active apps recycler view. */
public RecyclerViewFastScroller getScrollBar() {
AllAppsRecyclerView rv = getActiveAppsRecyclerView();
return rv == null ? null : rv.getScrollbar();
}
/**
* 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) {
boolean isTablet = mActivityContext.getDeviceProfile().isTablet;
// Draw full background panel for tablets.
if (isTablet) {
mHeaderPaint.setColor(mBottomSheetBackgroundColor);
View panel = (View) mBottomSheetBackground;
float translationY = ((View) panel.getParent()).getTranslationY();
mTmpRectF.set(panel.getLeft(), panel.getTop() + translationY,
panel.getRight(), 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;
}
final float offset = (getVisibleContainerView().getHeight() * (1 - scale) / 2);
final float bottom =
scale * (getHeaderBottom() + getVisibleContainerView().getPaddingTop()) + offset;
FloatingHeaderView headerView = getFloatingHeaderView();
if (isTablet) {
// Start adding header protection if search bar or tabs will attach to the top.
if (!isSearchBarOnBottom() || mUsingTabs) {
View panel = (View) mBottomSheetBackground;
float translationY = ((View) panel.getParent()).getTranslationY();
mTmpRectF.set(panel.getLeft(), panel.getTop() + translationY, panel.getRight(),
bottom);
mTmpPath.reset();
mTmpPath.addRoundRect(mTmpRectF, mBottomSheetCornerRadii, Direction.CW);
canvas.drawPath(mTmpPath, mHeaderPaint);
}
} else {
canvas.drawRect(0, 0, canvas.getWidth(), bottom, mHeaderPaint);
}
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));
}
int left = 0;
int right = canvas.getWidth();
if (isTablet) {
left = mBottomSheetBackground.getLeft();
right = mBottomSheetBackground.getRight();
}
canvas.drawRect(left, bottom, right, bottom + tabsHeight, 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 (isSearchBarOnBottom()) {
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 mActivityContext.getDeviceProfile().isTablet ? mBottomSheetBackground : this;
}
protected void onInitializeRecyclerView(RecyclerView rv) {
rv.addOnScrollListener(mScrollListener);
}
/** 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;
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.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);
FocusedItemDecorator focusedItemDecorator = new FocusedItemDecorator(mRecyclerView);
mRecyclerView.addItemDecoration(focusedItemDecorator);
mAdapter.setIconFocusListener(focusedItemDecorator.getFocusListener());
applyPadding();
}
void applyPadding() {
if (mRecyclerView != null) {
int bottomOffset = 0;
if (isWork() && mWorkManager.getWorkModeSwitch() != null) {
bottomOffset = mInsets.bottom + mWorkManager.getWorkModeSwitch().getHeight();
}
mRecyclerView.setPadding(mPadding.left, mPadding.top, mPadding.right,
mPadding.bottom + bottomOffset);
}
}
private boolean isWork() {
return mType == WORK;
}
private boolean isSearch() {
return mType == SEARCH;
}
}
}