Files
Lawnchair/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
T
Fengjiang Li 6bb8d79549 Predictive back: widget to all apps
This CL adds a layer of OnBackPressedHanlderRouter to Launcher:
1. 4 OnBackPressedHandler(s) are added in such order: auto cancel action mode handler, drag handler, view handler and state handler
2. first handler who can handle back will handle the entire back gesture
3. Let WidgetsFullSheet to handle widget to all apps transition

Bug: b/260956481
Test: manual
Change-Id: Idbce3dcec746226dd68aaabaddc8fe01334e9673
2023-01-24 15:51:46 -08:00

886 lines
37 KiB
Java

/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.widget.picker;
import static android.view.View.MeasureSpec.makeMeasureSpec;
import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
import static com.android.launcher3.allapps.AllAppsTransitionController.SWIPE_ALL_APPS_TO_HOME_MIN_SCALE;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGETSTRAY_SEARCHED;
import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.PropertyValuesHolder;
import android.content.Context;
import android.content.pm.LauncherApps;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Rect;
import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.AttributeSet;
import android.util.Pair;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.FloatRange;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.RecyclerView;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.compat.AccessibilityManagerCompat;
import com.android.launcher3.model.UserManagerState;
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.pm.UserCache;
import com.android.launcher3.views.ArrowTipView;
import com.android.launcher3.views.RecyclerViewFastScroller;
import com.android.launcher3.views.SpringRelativeLayout;
import com.android.launcher3.views.StickyHeaderLayout;
import com.android.launcher3.views.WidgetsEduView;
import com.android.launcher3.widget.BaseWidgetSheet;
import com.android.launcher3.widget.LauncherWidgetHolder.ProviderChangedListener;
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
import com.android.launcher3.widget.picker.search.SearchModeListener;
import com.android.launcher3.widget.picker.search.WidgetsSearchBar;
import com.android.launcher3.widget.util.WidgetsTableUtils;
import com.android.launcher3.workprofile.PersonalWorkPagedView;
import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.IntStream;
/**
* Popup for showing the full list of available widgets
*/
public class WidgetsFullSheet extends BaseWidgetSheet
implements ProviderChangedListener, OnActivePageChangedListener,
WidgetsRecyclerView.HeaderViewDimensionsProvider, SearchModeListener {
private static final long FADE_IN_DURATION = 150;
private static final long EDUCATION_TIP_DELAY_MS = 200;
private static final long EDUCATION_DIALOG_DELAY_MS = 500;
private static final float VERTICAL_START_POSITION = 0.3f;
// The widget recommendation table can easily take over the entire screen on devices with small
// resolution or landscape on phone. This ratio defines the max percentage of content area that
// the table can display.
private static final float RECOMMENDATION_TABLE_HEIGHT_RATIO = 0.75f;
private static final String KEY_WIDGETS_EDUCATION_DIALOG_SEEN =
"launcher.widgets_education_dialog_seen";
private final UserManagerState mUserManagerState = new UserManagerState();
private final boolean mHasWorkProfile;
private final SparseArray<AdapterHolder> mAdapters = new SparseArray();
private final UserHandle mCurrentUser = Process.myUserHandle();
private final Predicate<WidgetsListBaseEntry> mPrimaryWidgetsFilter =
entry -> mCurrentUser.equals(entry.mPkgItem.user);
private final Predicate<WidgetsListBaseEntry> mWorkWidgetsFilter =
entry -> !mCurrentUser.equals(entry.mPkgItem.user)
&& !mUserManagerState.isUserQuiet(entry.mPkgItem.user);
@Nullable private ArrowTipView mLatestEducationalTip;
private final OnLayoutChangeListener mLayoutChangeListenerToShowTips =
new OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
if (hasSeenEducationTip()) {
removeOnLayoutChangeListener(this);
return;
}
// Widgets are loaded asynchronously, We are adding a delay because we only want
// to show the tip when the widget preview has finished loading and rendering in
// this view.
removeCallbacks(mShowEducationTipTask);
postDelayed(mShowEducationTipTask, EDUCATION_TIP_DELAY_MS);
}
};
private final Runnable mShowEducationTipTask = () -> {
if (hasSeenEducationTip()) {
removeOnLayoutChangeListener(mLayoutChangeListenerToShowTips);
return;
}
mLatestEducationalTip = showEducationTipOnViewIfPossible(getViewToShowEducationTip());
if (mLatestEducationalTip != null) {
removeOnLayoutChangeListener(mLayoutChangeListenerToShowTips);
}
};
private final OnAttachStateChangeListener mBindScrollbarInSearchMode =
new OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View view) {
WidgetsRecyclerView searchRecyclerView =
mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView;
if (mIsInSearchMode && searchRecyclerView != null) {
searchRecyclerView.bindFastScrollbar();
}
}
@Override
public void onViewDetachedFromWindow(View view) {
}
};
private final int mTabsHeight;
private final int mWidgetSheetContentHorizontalPadding;
@Nullable private WidgetsRecyclerView mCurrentWidgetsRecyclerView;
@Nullable private PersonalWorkPagedView mViewPager;
private boolean mIsInSearchMode;
private boolean mIsNoWidgetsViewNeeded;
private int mMaxSpansPerRow = DEFAULT_MAX_HORIZONTAL_SPANS;
private TextView mNoWidgetsView;
private StickyHeaderLayout mSearchScrollView;
private WidgetsRecommendationTableLayout mRecommendedWidgetsTable;
private View mTabBar;
private View mSearchBarContainer;
private WidgetsSearchBar mSearchBar;
private TextView mHeaderTitle;
private @Nullable WidgetsRecyclerView mCurrentTouchEventRecyclerView;
public WidgetsFullSheet(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mHasWorkProfile = context.getSystemService(LauncherApps.class).getProfiles().size() > 1;
mAdapters.put(AdapterHolder.PRIMARY, new AdapterHolder(AdapterHolder.PRIMARY));
mAdapters.put(AdapterHolder.WORK, new AdapterHolder(AdapterHolder.WORK));
mAdapters.put(AdapterHolder.SEARCH, new AdapterHolder(AdapterHolder.SEARCH));
Resources resources = getResources();
mTabsHeight = mHasWorkProfile
? resources.getDimensionPixelSize(R.dimen.all_apps_header_pill_height)
: 0;
mWidgetSheetContentHorizontalPadding = 2 * resources.getDimensionPixelSize(
R.dimen.widget_cell_horizontal_padding);
mUserManagerState.init(UserCache.INSTANCE.get(context),
context.getSystemService(UserManager.class));
}
public WidgetsFullSheet(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mContent = findViewById(R.id.container);
LayoutInflater layoutInflater = LayoutInflater.from(getContext());
int contentLayoutRes = mHasWorkProfile ? R.layout.widgets_full_sheet_paged_view
: R.layout.widgets_full_sheet_recyclerview;
layoutInflater.inflate(contentLayoutRes, mContent, true);
RecyclerViewFastScroller fastScroller = findViewById(R.id.fast_scroller);
mAdapters.get(AdapterHolder.PRIMARY).setup(findViewById(R.id.primary_widgets_list_view));
mAdapters.get(AdapterHolder.SEARCH).setup(findViewById(R.id.search_widgets_list_view));
if (mHasWorkProfile) {
mViewPager = findViewById(R.id.widgets_view_pager);
mViewPager.initParentViews(this);
mViewPager.getPageIndicator().setOnActivePageChangedListener(this);
mViewPager.getPageIndicator().setActiveMarker(AdapterHolder.PRIMARY);
findViewById(R.id.tab_personal)
.setOnClickListener((View view) -> mViewPager.snapToPage(0));
findViewById(R.id.tab_work)
.setOnClickListener((View view) -> mViewPager.snapToPage(1));
mAdapters.get(AdapterHolder.WORK).setup(findViewById(R.id.work_widgets_list_view));
setDeviceManagementResources();
} else {
mViewPager = null;
}
mNoWidgetsView = findViewById(R.id.no_widgets_text);
mSearchScrollView = findViewById(R.id.search_and_recommendations_container);
mSearchScrollView.setCurrentRecyclerView(findViewById(R.id.primary_widgets_list_view));
mRecommendedWidgetsTable = mSearchScrollView.findViewById(R.id.recommended_widget_table);
mRecommendedWidgetsTable.setWidgetCellLongClickListener(this);
mRecommendedWidgetsTable.setWidgetCellOnClickListener(this);
mTabBar = mSearchScrollView.findViewById(R.id.tabs);
mSearchBarContainer = mSearchScrollView.findViewById(R.id.search_bar_container);
mSearchBar = mSearchScrollView.findViewById(R.id.widgets_search_bar);
mHeaderTitle = mSearchScrollView.findViewById(R.id.title);
onRecommendedWidgetsBound();
onWidgetsBound();
mSearchBar.initialize(
mActivityContext.getPopupDataProvider(), /* searchModeListener= */ this);
setUpEducationViewsIfNeeded();
}
private void setDeviceManagementResources() {
if (mActivityContext.getStringCache() != null) {
Button personalTab = findViewById(R.id.tab_personal);
personalTab.setText(mActivityContext.getStringCache().widgetsPersonalTab);
Button workTab = findViewById(R.id.tab_work);
workTab.setText(mActivityContext.getStringCache().widgetsWorkTab);
}
}
@Override
public void onActivePageChanged(int currentActivePage) {
AdapterHolder currentAdapterHolder = mAdapters.get(currentActivePage);
WidgetsRecyclerView currentRecyclerView =
mAdapters.get(currentActivePage).mWidgetsRecyclerView;
updateRecyclerViewVisibility(currentAdapterHolder);
attachScrollbarToRecyclerView(currentRecyclerView);
}
@Override
public void onBackProgressed(@FloatRange(from = 0.0, to = 1.0) float progress) {
float deceleratedProgress =
Interpolators.PREDICTIVE_BACK_DECELERATED_EASE.getInterpolation(progress);
float scaleProgress = SWIPE_ALL_APPS_TO_HOME_MIN_SCALE
+ (1 - SWIPE_ALL_APPS_TO_HOME_MIN_SCALE) * (1 - deceleratedProgress);
SCALE_PROPERTY.set(this, scaleProgress);
}
private void attachScrollbarToRecyclerView(WidgetsRecyclerView recyclerView) {
recyclerView.bindFastScrollbar();
if (mCurrentWidgetsRecyclerView != recyclerView) {
// Only reset the scroll position & expanded apps if the currently shown recycler view
// has been updated.
reset();
resetExpandedHeaders();
mCurrentWidgetsRecyclerView = recyclerView;
mSearchScrollView.setCurrentRecyclerView(recyclerView);
}
}
private void updateRecyclerViewVisibility(AdapterHolder adapterHolder) {
// The first item is always an empty space entry. Look for any more items.
boolean isWidgetAvailable = adapterHolder.mWidgetsListAdapter.hasVisibleEntries();
adapterHolder.mWidgetsRecyclerView.setVisibility(isWidgetAvailable ? VISIBLE : GONE);
if (adapterHolder.mAdapterType == AdapterHolder.SEARCH) {
mNoWidgetsView.setText(R.string.no_search_results);
} else if (adapterHolder.mAdapterType == AdapterHolder.WORK
&& mUserManagerState.isAnyProfileQuietModeEnabled()
&& mActivityContext.getStringCache() != null) {
mNoWidgetsView.setText(mActivityContext.getStringCache().workProfilePausedTitle);
} else {
mNoWidgetsView.setText(R.string.no_widgets_available);
}
mNoWidgetsView.setVisibility(isWidgetAvailable ? GONE : VISIBLE);
}
private void reset() {
mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView.scrollToTop();
if (mHasWorkProfile) {
mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView.scrollToTop();
}
mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.scrollToTop();
mSearchScrollView.reset(/* animate= */ true);
}
@VisibleForTesting
public WidgetsRecyclerView getRecyclerView() {
if (mIsInSearchMode) {
return mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView;
}
if (!mHasWorkProfile || mViewPager.getCurrentPage() == AdapterHolder.PRIMARY) {
return mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView;
}
return mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView;
}
@Override
protected Pair<View, String> getAccessibilityTarget() {
return Pair.create(getRecyclerView(), getContext().getString(
mIsOpen ? R.string.widgets_list : R.string.widgets_list_closed));
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mActivityContext.getAppWidgetHolder().addProviderChangeListener(this);
notifyWidgetProvidersChanged();
onRecommendedWidgetsBound();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mActivityContext.getAppWidgetHolder().removeProviderChangeListener(this);
mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView
.removeOnAttachStateChangeListener(mBindScrollbarInSearchMode);
if (mHasWorkProfile) {
mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView
.removeOnAttachStateChangeListener(mBindScrollbarInSearchMode);
}
}
@Override
public void setInsets(Rect insets) {
super.setInsets(insets);
int bottomPadding = Math.max(insets.bottom, mNavBarScrimHeight);
setBottomPadding(mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView, bottomPadding);
setBottomPadding(mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView, bottomPadding);
if (mHasWorkProfile) {
setBottomPadding(mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView, bottomPadding);
}
((MarginLayoutParams) mNoWidgetsView.getLayoutParams()).bottomMargin = bottomPadding;
if (bottomPadding > 0) {
setupNavBarColor();
} else {
clearNavBarColor();
}
requestLayout();
}
private void setBottomPadding(RecyclerView recyclerView, int bottomPadding) {
recyclerView.setPadding(
recyclerView.getPaddingLeft(),
recyclerView.getPaddingTop(),
recyclerView.getPaddingRight(),
bottomPadding);
}
@Override
protected void onContentHorizontalMarginChanged(int contentHorizontalMarginInPx) {
setContentViewChildHorizontalMargin(mSearchScrollView, contentHorizontalMarginInPx);
if (mViewPager == null) {
setContentViewChildHorizontalPadding(
mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView,
contentHorizontalMarginInPx);
} else {
setContentViewChildHorizontalPadding(
mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView,
contentHorizontalMarginInPx);
setContentViewChildHorizontalPadding(
mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView,
contentHorizontalMarginInPx);
}
setContentViewChildHorizontalPadding(
mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView,
contentHorizontalMarginInPx);
}
private static void setContentViewChildHorizontalMargin(View view, int horizontalMarginInPx) {
ViewGroup.MarginLayoutParams layoutParams =
(ViewGroup.MarginLayoutParams) view.getLayoutParams();
layoutParams.setMarginStart(horizontalMarginInPx);
layoutParams.setMarginEnd(horizontalMarginInPx);
}
private static void setContentViewChildHorizontalPadding(View view, int horizontalPaddingInPx) {
view.setPadding(horizontalPaddingInPx, view.getPaddingTop(), horizontalPaddingInPx,
view.getPaddingBottom());
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
doMeasure(widthMeasureSpec, heightMeasureSpec);
if (updateMaxSpansPerRow()) {
doMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
/** Returns {@code true} if the max spans have been updated. */
private boolean updateMaxSpansPerRow() {
if (getMeasuredWidth() == 0) return false;
View content = mHasWorkProfile
? mViewPager
: mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView;
int maxHorizontalSpans = computeMaxHorizontalSpans(content,
mWidgetSheetContentHorizontalPadding);
if (mMaxSpansPerRow != maxHorizontalSpans) {
mMaxSpansPerRow = maxHorizontalSpans;
mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.setMaxHorizontalSpansPerRow(
mMaxSpansPerRow);
mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.setMaxHorizontalSpansPerRow(
mMaxSpansPerRow);
if (mHasWorkProfile) {
mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.setMaxHorizontalSpansPerRow(
mMaxSpansPerRow);
}
onRecommendedWidgetsBound();
return true;
}
return false;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int width = r - l;
int height = b - t;
// Content is laid out as center bottom aligned
int contentWidth = mContent.getMeasuredWidth();
int contentLeft = (width - contentWidth - mInsets.left - mInsets.right) / 2 + mInsets.left;
mContent.layout(contentLeft, height - mContent.getMeasuredHeight(),
contentLeft + contentWidth, height);
setTranslationShift(mTranslationShift);
}
@Override
public void notifyWidgetProvidersChanged() {
mActivityContext.refreshAndBindWidgetsForPackageUser(null);
}
@Override
public void onWidgetsBound() {
if (mIsInSearchMode) {
return;
}
List<WidgetsListBaseEntry> allWidgets =
mActivityContext.getPopupDataProvider().getAllWidgets();
AdapterHolder primaryUserAdapterHolder = mAdapters.get(AdapterHolder.PRIMARY);
primaryUserAdapterHolder.mWidgetsListAdapter.setWidgets(allWidgets);
if (mHasWorkProfile) {
mViewPager.setVisibility(VISIBLE);
mTabBar.setVisibility(VISIBLE);
AdapterHolder workUserAdapterHolder = mAdapters.get(AdapterHolder.WORK);
workUserAdapterHolder.mWidgetsListAdapter.setWidgets(allWidgets);
onActivePageChanged(mViewPager.getCurrentPage());
} else {
updateRecyclerViewVisibility(primaryUserAdapterHolder);
}
// Update recommended widgets section so that it occupies appropriate space on screen to
// leave enough space for presence/absence of mNoWidgetsView.
boolean isNoWidgetsViewNeeded =
!mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.hasVisibleEntries()
|| (mHasWorkProfile && mAdapters.get(AdapterHolder.WORK)
.mWidgetsListAdapter.hasVisibleEntries());
if (mIsNoWidgetsViewNeeded != isNoWidgetsViewNeeded) {
mIsNoWidgetsViewNeeded = isNoWidgetsViewNeeded;
onRecommendedWidgetsBound();
}
}
@Override
public void enterSearchMode() {
if (mIsInSearchMode) return;
setViewVisibilityBasedOnSearch(/*isInSearchMode= */ true);
attachScrollbarToRecyclerView(mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView);
mActivityContext.getStatsLogManager().logger().log(LAUNCHER_WIDGETSTRAY_SEARCHED);
}
@Override
public void exitSearchMode() {
if (!mIsInSearchMode) return;
onSearchResults(new ArrayList<>());
setViewVisibilityBasedOnSearch(/*isInSearchMode=*/ false);
if (mHasWorkProfile) {
mViewPager.snapToPage(AdapterHolder.PRIMARY);
}
attachScrollbarToRecyclerView(mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView);
}
@Override
public void onSearchResults(List<WidgetsListBaseEntry> entries) {
mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.setWidgetsOnSearch(entries);
updateRecyclerViewVisibility(mAdapters.get(AdapterHolder.SEARCH));
mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.scrollToTop();
}
private void setViewVisibilityBasedOnSearch(boolean isInSearchMode) {
mIsInSearchMode = isInSearchMode;
if (isInSearchMode) {
mRecommendedWidgetsTable.setVisibility(GONE);
if (mHasWorkProfile) {
mViewPager.setVisibility(GONE);
mTabBar.setVisibility(GONE);
} else {
mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView.setVisibility(GONE);
}
updateRecyclerViewVisibility(mAdapters.get(AdapterHolder.SEARCH));
// Hide no search results view to prevent it from flashing on enter search.
mNoWidgetsView.setVisibility(GONE);
} else {
mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.setVisibility(GONE);
// Visibility of recommended widgets, recycler views and headers are handled in methods
// below.
onRecommendedWidgetsBound();
onWidgetsBound();
}
}
private void resetExpandedHeaders() {
mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.resetExpandedHeader();
mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.resetExpandedHeader();
}
@Override
public void onRecommendedWidgetsBound() {
if (mIsInSearchMode) {
return;
}
List<WidgetItem> recommendedWidgets =
mActivityContext.getPopupDataProvider().getRecommendedWidgets();
if (recommendedWidgets.size() > 0) {
float noWidgetsViewHeight = 0;
if (mIsNoWidgetsViewNeeded) {
// Make sure recommended section leaves enough space for noWidgetsView.
Rect noWidgetsViewTextBounds = new Rect();
mNoWidgetsView.getPaint()
.getTextBounds(mNoWidgetsView.getText().toString(), /* start= */ 0,
mNoWidgetsView.getText().length(), noWidgetsViewTextBounds);
noWidgetsViewHeight = noWidgetsViewTextBounds.height();
}
doMeasure(
makeMeasureSpec(mActivityContext.getDeviceProfile().availableWidthPx,
MeasureSpec.EXACTLY),
makeMeasureSpec(mActivityContext.getDeviceProfile().availableHeightPx,
MeasureSpec.EXACTLY));
float maxTableHeight = (mContent.getMeasuredHeight()
- mTabsHeight - getHeaderViewHeight()
- noWidgetsViewHeight) * RECOMMENDATION_TABLE_HEIGHT_RATIO;
List<ArrayList<WidgetItem>> recommendedWidgetsInTable =
WidgetsTableUtils.groupWidgetItemsIntoTableWithoutReordering(
recommendedWidgets, mMaxSpansPerRow);
mRecommendedWidgetsTable.setRecommendedWidgets(
recommendedWidgetsInTable, maxTableHeight);
} else {
mRecommendedWidgetsTable.setVisibility(GONE);
}
}
private void open(boolean animate) {
if (animate) {
if (getPopupContainer().getInsets().bottom > 0) {
mContent.setAlpha(0);
setTranslationShift(VERTICAL_START_POSITION);
}
mOpenCloseAnimator.setValues(
PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
mOpenCloseAnimator
.setDuration(mActivityContext.getDeviceProfile().bottomSheetOpenDuration)
.setInterpolator(AnimationUtils.loadInterpolator(
getContext(), android.R.interpolator.linear_out_slow_in));
mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mOpenCloseAnimator.removeListener(this);
}
});
post(() -> {
mOpenCloseAnimator.start();
mContent.animate().alpha(1).setDuration(FADE_IN_DURATION);
});
} else {
setTranslationShift(TRANSLATION_SHIFT_OPENED);
post(this::announceAccessibilityChanges);
}
}
@Override
protected void handleClose(boolean animate) {
handleClose(animate, mActivityContext.getDeviceProfile().bottomSheetCloseDuration);
}
@Override
protected boolean isOfType(int type) {
return (type & TYPE_WIDGETS_FULL_SHEET) != 0;
}
@Override
public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
// Disable swipe down when recycler view is scrolling
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
mNoIntercept = false;
WidgetsRecyclerView recyclerView = getRecyclerView();
RecyclerViewFastScroller scroller = recyclerView.getScrollbar();
if (scroller.getThumbOffsetY() >= 0
&& getPopupContainer().isEventOverView(scroller, ev)) {
mNoIntercept = true;
} else if (getPopupContainer().isEventOverView(recyclerView, ev)) {
mNoIntercept = !recyclerView.shouldContainerScroll(ev, getPopupContainer());
}
if (mSearchBar.isSearchBarFocused()
&& !getPopupContainer().isEventOverView(mSearchBarContainer, ev)) {
mSearchBar.clearSearchBarFocus();
}
}
return super.onControllerInterceptTouchEvent(ev);
}
/** Shows the {@link WidgetsFullSheet} on the launcher. */
public static WidgetsFullSheet show(Launcher launcher, boolean animate) {
WidgetsFullSheet sheet = (WidgetsFullSheet) launcher.getLayoutInflater()
.inflate(R.layout.widgets_full_sheet, launcher.getDragLayer(), false);
sheet.attachToContainer();
sheet.mIsOpen = true;
sheet.open(animate);
return sheet;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return isTouchOnScrollbar(ev) || super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
return maybeHandleTouchEvent(ev) || super.onTouchEvent(ev);
}
private boolean maybeHandleTouchEvent(MotionEvent ev) {
boolean isEventHandled = false;
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
mCurrentTouchEventRecyclerView = isTouchOnScrollbar(ev) ? getRecyclerView() : null;
}
if (mCurrentTouchEventRecyclerView != null) {
final float offsetX = mContent.getX();
final float offsetY = mContent.getY();
ev.offsetLocation(-offsetX, -offsetY);
isEventHandled = mCurrentTouchEventRecyclerView.dispatchTouchEvent(ev);
ev.offsetLocation(offsetX, offsetY);
}
if (ev.getAction() == MotionEvent.ACTION_UP
|| ev.getAction() == MotionEvent.ACTION_CANCEL) {
mCurrentTouchEventRecyclerView = null;
}
return isEventHandled;
}
private boolean isTouchOnScrollbar(MotionEvent ev) {
final float offsetX = mContent.getX();
final float offsetY = mContent.getY();
WidgetsRecyclerView rv = getRecyclerView();
ev.offsetLocation(-offsetX, -offsetY);
boolean isOnScrollBar = rv != null && rv.getScrollbar() != null && rv.isHitOnScrollBar(ev);
ev.offsetLocation(offsetX, offsetY);
return isOnScrollBar;
}
/** Gets the {@link WidgetsRecyclerView} which shows all widgets in {@link WidgetsFullSheet}. */
@VisibleForTesting
public static WidgetsRecyclerView getWidgetsView(Launcher launcher) {
return launcher.findViewById(R.id.primary_widgets_list_view);
}
@Override
public void addHintCloseAnim(
float distanceToMove, Interpolator interpolator, PendingAnimation target) {
target.setFloat(getRecyclerView(), VIEW_TRANSLATE_Y, -distanceToMove, interpolator);
target.setViewAlpha(getRecyclerView(), 0.5f, interpolator);
}
@Override
protected void onCloseComplete() {
super.onCloseComplete();
removeCallbacks(mShowEducationTipTask);
if (mLatestEducationalTip != null) {
mLatestEducationalTip.close(false);
}
AccessibilityManagerCompat.sendStateEventToTest(getContext(), NORMAL_STATE_ORDINAL);
}
@Override
public int getHeaderViewHeight() {
return measureHeightWithVerticalMargins(mHeaderTitle)
+ measureHeightWithVerticalMargins(mSearchBarContainer);
}
/** private the height, in pixel, + the vertical margins of a given view. */
private static int measureHeightWithVerticalMargins(View view) {
if (view.getVisibility() != VISIBLE) {
return 0;
}
MarginLayoutParams marginLayoutParams = (MarginLayoutParams) view.getLayoutParams();
return view.getMeasuredHeight() + marginLayoutParams.bottomMargin
+ marginLayoutParams.topMargin;
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (mIsInSearchMode) {
mSearchBar.reset();
}
}
@Override
public void onBackInvoked() {
if (mIsInSearchMode) {
mSearchBar.reset();
} else {
super.onBackInvoked();
}
}
@Override
public void onDragStart(boolean start, float startDisplacement) {
super.onDragStart(start, startDisplacement);
getWindowInsetsController().hide(WindowInsets.Type.ime());
}
@Nullable private View getViewToShowEducationTip() {
if (mRecommendedWidgetsTable.getVisibility() == VISIBLE
&& mRecommendedWidgetsTable.getChildCount() > 0) {
return ((ViewGroup) mRecommendedWidgetsTable.getChildAt(0)).getChildAt(0);
}
AdapterHolder adapterHolder = mAdapters.get(mIsInSearchMode
? AdapterHolder.SEARCH
: mViewPager == null
? AdapterHolder.PRIMARY
: mViewPager.getCurrentPage());
WidgetsRowViewHolder viewHolderForTip =
(WidgetsRowViewHolder) IntStream.range(
0, adapterHolder.mWidgetsListAdapter.getItemCount())
.mapToObj(adapterHolder.mWidgetsRecyclerView::
findViewHolderForAdapterPosition)
.filter(viewHolder -> viewHolder instanceof WidgetsRowViewHolder)
.findFirst()
.orElse(null);
if (viewHolderForTip != null) {
return ((ViewGroup) viewHolderForTip.tableContainer.getChildAt(0)).getChildAt(0);
}
return null;
}
/** Shows education dialog for widgets. */
private WidgetsEduView showEducationDialog() {
mActivityContext.getSharedPrefs().edit()
.putBoolean(KEY_WIDGETS_EDUCATION_DIALOG_SEEN, true).apply();
return WidgetsEduView.showEducationDialog(mActivityContext);
}
/** Returns {@code true} if education dialog has previously been shown. */
protected boolean hasSeenEducationDialog() {
return mActivityContext.getSharedPrefs()
.getBoolean(KEY_WIDGETS_EDUCATION_DIALOG_SEEN, false)
|| Utilities.IS_RUNNING_IN_TEST_HARNESS;
}
private void setUpEducationViewsIfNeeded() {
if (!hasSeenEducationDialog()) {
postDelayed(() -> {
WidgetsEduView eduDialog = showEducationDialog();
eduDialog.addOnCloseListener(() -> {
if (!hasSeenEducationTip()) {
addOnLayoutChangeListener(mLayoutChangeListenerToShowTips);
// Call #requestLayout() to trigger layout change listener in order to show
// arrow tip immediately if there is a widget to show it on.
requestLayout();
}
});
}, EDUCATION_DIALOG_DELAY_MS);
} else if (!hasSeenEducationTip()) {
addOnLayoutChangeListener(mLayoutChangeListenerToShowTips);
}
}
/** A holder class for holding adapters & their corresponding recycler view. */
private final class AdapterHolder {
static final int PRIMARY = 0;
static final int WORK = 1;
static final int SEARCH = 2;
private final int mAdapterType;
private final WidgetsListAdapter mWidgetsListAdapter;
private final DefaultItemAnimator mWidgetsListItemAnimator;
private WidgetsRecyclerView mWidgetsRecyclerView;
AdapterHolder(int adapterType) {
mAdapterType = adapterType;
Context context = getContext();
LauncherAppState apps = LauncherAppState.getInstance(context);
mWidgetsListAdapter = new WidgetsListAdapter(
context,
LayoutInflater.from(context),
apps.getIconCache(),
this::getEmptySpaceHeight,
/* iconClickListener= */ WidgetsFullSheet.this,
/* iconLongClickListener= */ WidgetsFullSheet.this);
mWidgetsListAdapter.setHasStableIds(true);
switch (mAdapterType) {
case PRIMARY:
mWidgetsListAdapter.setFilter(mPrimaryWidgetsFilter);
break;
case WORK:
mWidgetsListAdapter.setFilter(mWorkWidgetsFilter);
break;
default:
break;
}
mWidgetsListItemAnimator = new DefaultItemAnimator();
// Disable change animations because it disrupts the item focus upon adapter item
// change.
mWidgetsListItemAnimator.setSupportsChangeAnimations(false);
}
private int getEmptySpaceHeight() {
return mSearchScrollView.getHeaderHeight();
}
void setup(WidgetsRecyclerView recyclerView) {
mWidgetsRecyclerView = recyclerView;
mWidgetsRecyclerView.setAdapter(mWidgetsListAdapter);
mWidgetsRecyclerView.setItemAnimator(mWidgetsListItemAnimator);
mWidgetsRecyclerView.setHeaderViewDimensionsProvider(WidgetsFullSheet.this);
mWidgetsRecyclerView.setEdgeEffectFactory(
((SpringRelativeLayout) mContent).createEdgeEffectFactory());
// Recycler view binds to fast scroller when it is attached to screen. Make sure
// search recycler view is bound to fast scroller if user is in search mode at the time
// of attachment.
if (mAdapterType == PRIMARY || mAdapterType == WORK) {
mWidgetsRecyclerView.addOnAttachStateChangeListener(mBindScrollbarInSearchMode);
}
mWidgetsListAdapter.setMaxHorizontalSpansPerRow(mMaxSpansPerRow);
}
}
}