Files
Lawnchair/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
T
Cole Faust 1b9fd985d4 Replace .toList() with .collect()
.toList() was only introduced to android in api level 34, which is newer than
this module's min_sdk_version. Replace it with .collect().

This was found while updating android lint.

Flag: EXEMPT refactor
Bug: 394096385
Test: Presubmits
Change-Id: Id8d1de1531b67a7daf448e45592b7ef78f685fc2
2025-02-03 14:19:12 -08:00

1217 lines
52 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 com.android.launcher3.Flags.enableCategorizedWidgetSuggestions;
import static com.android.launcher3.Flags.enableTieredWidgetsByDefaultInPicker;
import static com.android.launcher3.Flags.enableUnfoldedTwoPanePicker;
import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.SEARCH;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGETSTRAY_EXPAND_PRESS;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGETSTRAY_SEARCHED;
import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
import static com.android.launcher3.views.RecyclerViewFastScroller.FastScrollerLocation.WIDGET_SCROLLER;
import static java.lang.Math.abs;
import static java.util.Collections.emptyList;
import android.animation.Animator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Parcelable;
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.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.WindowInsets;
import android.view.WindowInsetsController;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.annotation.VisibleForTesting;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.RecyclerView;
import com.android.launcher3.BaseActivity;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
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.RecyclerViewFastScroller;
import com.android.launcher3.views.SpringRelativeLayout;
import com.android.launcher3.views.StickyHeaderLayout;
import com.android.launcher3.widget.BaseWidgetSheet;
import com.android.launcher3.widget.WidgetCell;
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
import com.android.launcher3.widget.picker.model.data.WidgetPickerData;
import com.android.launcher3.widget.picker.search.SearchModeListener;
import com.android.launcher3.widget.picker.search.WidgetsSearchBar;
import com.android.launcher3.widget.picker.search.WidgetsSearchBar.WidgetsSearchDataProvider;
import com.android.launcher3.workprofile.PersonalWorkPagedView;
import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
* Popup for showing the full list of available widgets
*/
public class WidgetsFullSheet extends BaseWidgetSheet
implements OnActivePageChangedListener,
WidgetsRecyclerView.HeaderViewDimensionsProvider, SearchModeListener,
WidgetsListAdapter.ExpandButtonClickListener {
private static final long FADE_IN_DURATION = 150;
// 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 with respect to bottom sheet's height.
private static final float RECOMMENDATION_TABLE_HEIGHT_RATIO = 0.45f;
private static final String RECOMMENDATIONS_SAVED_STATE_KEY =
"widgetsFullSheet:mRecommendationsCurrentPage";
private static final String SUPER_SAVED_STATE_KEY = "widgetsFullSheet:superHierarchyState";
private final UserCache mUserCache;
private final UserManagerState mUserManagerState = new UserManagerState();
private final UserHandle mCurrentUser = Process.myUserHandle();
private final Predicate<WidgetsListBaseEntry> mPrimaryWidgetsFilter =
entry -> mCurrentUser.equals(entry.mPkgItem.user);
private final Predicate<WidgetsListBaseEntry> mWorkWidgetsFilter;
protected final boolean mHasWorkProfile;
// Number of recommendations displayed
protected int mRecommendedWidgetsCount;
private List<WidgetItem> mRecommendedWidgets = new ArrayList<>();
private Map<WidgetRecommendationCategory, List<WidgetItem>> mRecommendedWidgetsMap =
new HashMap<>();
protected int mRecommendationsCurrentPage = 0;
protected final SparseArray<AdapterHolder> mAdapters = new SparseArray();
// Helps with removing focus from searchbar by analyzing motion events.
private final SearchClearFocusHelper mSearchClearFocusHelper = new SearchClearFocusHelper();
private final float mTouchSlop; // initialized in constructor
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(mFastScroller, WIDGET_SCROLLER);
}
}
@Override
public void onViewDetachedFromWindow(View view) {
}
};
@Px
private final int mTabsHeight;
@Nullable
private WidgetsRecyclerView mCurrentWidgetsRecyclerView;
@Nullable
private WidgetsRecyclerView mCurrentTouchEventRecyclerView;
@Nullable
PersonalWorkPagedView mViewPager;
protected boolean mIsInSearchMode;
private boolean mIsNoWidgetsViewNeeded;
@Px
protected int mMaxSpanPerRow;
protected DeviceProfile mDeviceProfile;
protected TextView mNoWidgetsView;
protected LinearLayout mSearchScrollView;
// Reference to the mSearchScrollView when it is is a sticky header.
private @Nullable StickyHeaderLayout mStickyHeaderLayout;
protected WidgetRecommendationsView mWidgetRecommendationsView;
protected LinearLayout mWidgetRecommendationsContainer;
protected View mTabBar;
protected View mSearchBarContainer;
protected WidgetsSearchBar mSearchBar;
protected TextView mHeaderTitle;
protected RecyclerViewFastScroller mFastScroller;
protected int mBottomPadding;
public WidgetsFullSheet(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mDeviceProfile = mActivityContext.getDeviceProfile();
mUserCache = UserCache.INSTANCE.get(context);
mHasWorkProfile = mUserCache.getUserProfiles()
.stream()
.anyMatch(user -> mUserCache.getUserInfo(user).isWork());
mWorkWidgetsFilter = entry -> mHasWorkProfile
&& mUserCache.getUserInfo(entry.mPkgItem.user).isWork()
&& !mUserManagerState.isUserQuiet(entry.mPkgItem.user);
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();
mUserManagerState.init(UserCache.INSTANCE.get(context),
context.getSystemService(UserManager.class));
mTabsHeight = mHasWorkProfile
? resources.getDimensionPixelSize(R.dimen.all_apps_header_pill_height)
: 0;
}
public WidgetsFullSheet(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mContent = findViewById(R.id.container);
setContentBackgroundWithParent(getContext().getDrawable(R.drawable.bg_widgets_full_sheet),
mContent);
mContent.setOutlineProvider(mViewOutlineProvider);
mContent.setClipToOutline(true);
setupSheet();
}
protected void setupSheet() {
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);
setupViews();
mWidgetRecommendationsContainer = mSearchScrollView.findViewById(
R.id.widget_recommendations_container);
mWidgetRecommendationsView = mSearchScrollView.findViewById(
R.id.widget_recommendations_view);
// To save the currently displayed page, so that, it can be requested when rebinding
// recommendations with different size constraints.
mWidgetRecommendationsView.addPageSwitchListener(
newPage -> mRecommendationsCurrentPage = newPage);
mWidgetRecommendationsView.initParentViews(mWidgetRecommendationsContainer);
mWidgetRecommendationsView.setWidgetCellLongClickListener(this);
mWidgetRecommendationsView.setWidgetCellOnClickListener(this);
mHeaderTitle = mSearchScrollView.findViewById(R.id.title);
onWidgetsBound();
}
protected void setupViews() {
mSearchScrollView = findViewById(R.id.search_and_recommendations_container);
if (mSearchScrollView instanceof StickyHeaderLayout) {
mStickyHeaderLayout = (StickyHeaderLayout) mSearchScrollView;
mStickyHeaderLayout.setCurrentRecyclerView(
findViewById(R.id.primary_widgets_list_view));
}
mNoWidgetsView = findViewById(R.id.no_widgets_text);
mFastScroller = findViewById(R.id.fast_scroller);
mFastScroller.setPopupView(findViewById(R.id.fast_scroller_popup));
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.setOutlineProvider(mViewOutlineProvider);
mViewPager.setClipToOutline(true);
mViewPager.setClipChildren(false);
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;
}
mTabBar = mSearchScrollView.findViewById(R.id.tabs);
mSearchBarContainer = mSearchScrollView.findViewById(R.id.search_bar_container);
mSearchBar = mSearchScrollView.findViewById(R.id.widgets_search_bar);
mSearchBar.initialize(new WidgetsSearchDataProvider() {
@Override
public List<WidgetsListBaseEntry> getWidgets() {
if (enableTieredWidgetsByDefaultInPicker()) {
// search all
return mActivityContext.getWidgetPickerDataProvider().get().getAllWidgets();
} else {
// Can be removed when inlining enableTieredWidgetsByDefaultInPicker flag
return getWidgetsToDisplay();
}
}
}, /* searchModeListener= */ this);
}
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);
}
private void attachScrollbarToRecyclerView(WidgetsRecyclerView recyclerView) {
if (mCurrentWidgetsRecyclerView != recyclerView) {
// Bind scrollbar if changing the recycler view. If widgets list updates, since
// scrollbar is already attached to the recycler view, it will automatically adjust as
// needed with recycler view's onScrollListener.
recyclerView.bindFastScrollbar(mFastScroller, WIDGET_SCROLLER);
// Only reset the scroll position & expanded apps if the currently shown recycler view
// has been updated.
reset();
resetExpandedHeaders();
mCurrentWidgetsRecyclerView = recyclerView;
if (mStickyHeaderLayout != null) {
mStickyHeaderLayout.setCurrentRecyclerView(recyclerView);
}
}
}
protected void updateRecyclerViewVisibility(AdapterHolder adapterHolder) {
// The first item is always an empty space entry. Look for any more items.
boolean isWidgetAvailable = adapterHolder.mWidgetsListAdapter.hasVisibleEntries();
if (adapterHolder.mAdapterType == AdapterHolder.SEARCH) {
mNoWidgetsView.setText(R.string.no_search_results);
adapterHolder.mWidgetsRecyclerView.setVisibility(isWidgetAvailable ? VISIBLE : GONE);
} else if (adapterHolder.mAdapterType == AdapterHolder.WORK
&& mUserCache.getUserProfiles().stream()
.filter(userHandle -> mUserCache.getUserInfo(userHandle).isWork())
.anyMatch(mUserManagerState::isUserQuiet)
&& 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();
if (mStickyHeaderLayout != null) {
mStickyHeaderLayout.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();
onWidgetsBound();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
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);
mBottomPadding = Math.max(insets.bottom, mNavBarScrimHeight);
setBottomPadding(mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView, mBottomPadding);
setBottomPadding(mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView, mBottomPadding);
if (mHasWorkProfile) {
setBottomPadding(mAdapters.get(AdapterHolder.WORK)
.mWidgetsRecyclerView, mBottomPadding);
}
((MarginLayoutParams) mNoWidgetsView.getLayoutParams()).bottomMargin = mBottomPadding;
if (mBottomPadding > 0) {
setupNavBarColor();
} else {
clearNavBarColor();
}
requestLayout();
}
@Override
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
WindowInsets w = super.onApplyWindowInsets(insets);
if (mInsets.bottom != mNavBarScrimHeight) {
setInsets(mInsets);
}
return w;
}
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) {
int availableWidth = MeasureSpec.getSize(widthMeasureSpec);
updateMaxSpansPerRow(availableWidth);
doMeasure(widthMeasureSpec, heightMeasureSpec);
}
/** Returns {@code true} if the max spans have been updated.
*
* @param availableWidth Total width available within parent (includes insets).
*/
private void updateMaxSpansPerRow(int availableWidth) {
@Px int maxHorizontalSpan = getAvailableWidthForSuggestions(
availableWidth - getInsetsWidth());
if (mMaxSpanPerRow != maxHorizontalSpan) {
mMaxSpanPerRow = maxHorizontalSpan;
mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.setMaxHorizontalSpansPxPerRow(
maxHorizontalSpan);
mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.setMaxHorizontalSpansPxPerRow(
maxHorizontalSpan);
if (mHasWorkProfile) {
mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.setMaxHorizontalSpansPxPerRow(
maxHorizontalSpan);
}
post(this::onRecommendedWidgetsBound);
}
}
/**
* Returns the width available to display suggestions.
*/
protected int getAvailableWidthForSuggestions(int pickerAvailableWidth) {
return pickerAvailableWidth - (2 * mContentHorizontalMargin);
}
@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);
}
/**
* Returns all displayable widgets.
*/
// Used by the two pane sheet to show 3-dot menu to toggle between default lists and all lists
// when enableTieredWidgetsByDefaultInPicker is OFF. This code path and the 3-dot menu can be
// safely deleted when it's alternative "enableTieredWidgetsByDefaultInPicker" flag is inlined.
protected List<WidgetsListBaseEntry> getWidgetsToDisplay() {
return mActivityContext.getWidgetPickerDataProvider().get().getAllWidgets();
}
@Override
public void onWidgetsBound() {
if (mIsInSearchMode) {
return;
}
List<WidgetsListBaseEntry> widgets;
List<WidgetsListBaseEntry> defaultWidgets = emptyList();
if (enableTieredWidgetsByDefaultInPicker()) {
WidgetPickerData dataProvider =
mActivityContext.getWidgetPickerDataProvider().get();
widgets = dataProvider.getAllWidgets();
defaultWidgets = dataProvider.getDefaultWidgets();
} else {
// This code path can be deleted once enableTieredWidgetsByDefaultInPicker is inlined.
widgets = getWidgetsToDisplay();
}
AdapterHolder primaryUserAdapterHolder = mAdapters.get(AdapterHolder.PRIMARY);
primaryUserAdapterHolder.mWidgetsListAdapter.setWidgets(widgets, defaultWidgets);
if (mHasWorkProfile) {
mViewPager.setVisibility(VISIBLE);
mTabBar.setVisibility(VISIBLE);
AdapterHolder workUserAdapterHolder = mAdapters.get(AdapterHolder.WORK);
workUserAdapterHolder.mWidgetsListAdapter.setWidgets(widgets, defaultWidgets);
onActivePageChanged(mViewPager.getCurrentPage());
} else {
onActivePageChanged(0);
}
// 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;
post(this::onRecommendedWidgetsBound);
}
}
@Override
public void onWidgetsListExpandButtonClick(View v) {
AdapterHolder currentAdapterHolder = mAdapters.get(getCurrentAdapterHolderType());
currentAdapterHolder.mWidgetsListAdapter.useExpandedList();
onWidgetsBound();
currentAdapterHolder.mWidgetsRecyclerView.announceForAccessibility(
mActivityContext.getString(R.string.widgets_list_expanded));
mActivityContext.getStatsLogManager().logger().log(LAUNCHER_WIDGETSTRAY_EXPAND_PRESS);
}
@Override
public void enterSearchMode(boolean shouldLog) {
if (mIsInSearchMode) return;
setViewVisibilityBasedOnSearch(/*isInSearchMode= */ true);
attachScrollbarToRecyclerView(mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView);
if (shouldLog) {
mActivityContext.getStatsLogManager().logger().log(LAUNCHER_WIDGETSTRAY_SEARCHED);
}
}
@Override
public void exitSearchMode() {
if (!mIsInSearchMode) return;
onSearchResults(new ArrayList<>());
// Remove all views when exiting the search mode; this prevents animating from stale results
// to new ones the next time we enter search mode. By the time recycler view is hidden,
// layout may not have happened to clear up existing results. So, instead of waiting for it
// to happen, we clear the views here.
mAdapters.get(AdapterHolder.SEARCH).reset();
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));
}
protected void setViewVisibilityBasedOnSearch(boolean isInSearchMode) {
mIsInSearchMode = isInSearchMode;
if (isInSearchMode) {
mWidgetRecommendationsContainer.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);
AdapterHolder currentAdapterHolder = mAdapters.get(getCurrentAdapterHolderType());
// Remove all views when exiting the search mode; this prevents animating / flashing old
// list position / state.
currentAdapterHolder.reset();
currentAdapterHolder.mWidgetsRecyclerView.setVisibility(VISIBLE);
post(this::onRecommendedWidgetsBound);
// Visibility of recycler views and headers are handled in methods below.
onWidgetsBound();
}
}
protected void resetExpandedHeaders() {
mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.resetExpandedHeader();
mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.resetExpandedHeader();
}
@Override
public void onRecommendedWidgetsBound() {
if (mIsInSearchMode) {
return;
}
if (enableCategorizedWidgetSuggestions()) {
// We avoid applying new recommendations when some are already displayed.
if (mRecommendedWidgetsMap.isEmpty()) {
mRecommendedWidgetsMap =
mActivityContext.getWidgetPickerDataProvider().get().getRecommendations();
}
mRecommendedWidgetsCount = mWidgetRecommendationsView.setRecommendations(
mRecommendedWidgetsMap,
mDeviceProfile,
/* availableHeight= */ getMaxAvailableHeightForRecommendations(),
/* availableWidth= */ mMaxSpanPerRow,
/* cellPadding= */ mWidgetCellHorizontalPadding,
/* requestedPage= */ mRecommendationsCurrentPage
);
} else {
if (mRecommendedWidgets.isEmpty()) {
mRecommendedWidgets = mActivityContext.getWidgetPickerDataProvider().get()
.getRecommendations()
.values().stream()
.flatMap(Collection::stream).collect(Collectors.toList());
mRecommendedWidgetsCount = mWidgetRecommendationsView.setRecommendations(
mRecommendedWidgets,
mDeviceProfile,
/* availableHeight= */ getMaxAvailableHeightForRecommendations(),
/* availableWidth= */ mMaxSpanPerRow,
/* cellPadding= */ mWidgetCellHorizontalPadding
);
}
}
mWidgetRecommendationsContainer.setVisibility(
mRecommendedWidgetsCount > 0 ? VISIBLE : GONE);
}
@Px
protected float getMaxAvailableHeightForRecommendations() {
// There isn't enough space to show recommendations in landscape orientation on phones with
// a full sheet design. Tablets use a two pane picker.
if (mDeviceProfile.isLandscape) {
return 0f;
}
return (mDeviceProfile.heightPx - mDeviceProfile.bottomSheetTopPadding)
* RECOMMENDATION_TABLE_HEIGHT_RATIO;
}
/** b/209579563: "Widgets" header should be focused first. */
@Override
protected View getAccessibilityInitialFocusView() {
return mHeaderTitle;
}
private void open(boolean animate) {
if (animate) {
if (getPopupContainer().getInsets().bottom > 0) {
mContent.setAlpha(0);
}
setUpOpenAnimation(mActivityContext.getDeviceProfile().bottomSheetOpenDuration);
Animator animator = mOpenCloseAnimation.getAnimationPlayer();
animator.setInterpolator(AnimationUtils.loadInterpolator(
getContext(), android.R.interpolator.linear_out_slow_in));
post(() -> {
animator.setDuration(mActivityContext.getDeviceProfile().bottomSheetOpenDuration)
.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) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
mNoIntercept = shouldScroll(ev);
}
// Clear focus only if user touched outside of search area and handling focus out ourselves
// was necessary (e.g. when it's not predictive back, but other user interaction).
if (mSearchBar.isSearchBarFocused()
&& !getPopupContainer().isEventOverView(mSearchBarContainer, ev)
&& mSearchClearFocusHelper.shouldClearFocus(ev, mTouchSlop)) {
mSearchBar.clearSearchBarFocus();
}
return super.onControllerInterceptTouchEvent(ev);
}
protected boolean shouldScroll(MotionEvent ev) {
boolean intercept = false;
WidgetsRecyclerView recyclerView = getRecyclerView();
RecyclerViewFastScroller scroller = recyclerView.getScrollbar();
// Disable swipe down when recycler view is scrolling
if (scroller.getThumbOffsetY() >= 0 && getPopupContainer().isEventOverView(scroller, ev)) {
intercept = true;
} else if (getPopupContainer().isEventOverView(recyclerView, ev)) {
intercept = !recyclerView.shouldContainerScroll(ev, getPopupContainer());
}
return intercept;
}
/** Shows the {@link WidgetsFullSheet} on the launcher. */
public static WidgetsFullSheet show(BaseActivity activity, boolean animate) {
WidgetsFullSheet sheet = (WidgetsFullSheet) activity.getLayoutInflater().inflate(
getWidgetSheetId(activity),
activity.getDragLayer(),
false);
sheet.attachToContainer();
sheet.mIsOpen = true;
sheet.open(animate);
return sheet;
}
/**
* Updates the widget picker's title and description in the header to the provided values (if
* present).
*/
public void mayUpdateTitleAndDescription(@Nullable String title,
@Nullable String descriptionRes) {
if (title != null) {
mHeaderTitle.setText(title);
}
// Full sheet doesn't support a description.
}
@Override
public void saveHierarchyState(SparseArray<Parcelable> sparseArray) {
Bundle bundle = new Bundle();
// With widget picker open, when we open shade to switch theme, Launcher re-creates the
// picker and calls save/restore hierarchy state. We save the state of recommendations
// across those updates.
bundle.putInt(RECOMMENDATIONS_SAVED_STATE_KEY, mRecommendationsCurrentPage);
mWidgetRecommendationsView.saveState(bundle);
SparseArray<Parcelable> superState = new SparseArray<>();
super.saveHierarchyState(superState);
bundle.putSparseParcelableArray(SUPER_SAVED_STATE_KEY, superState);
sparseArray.put(0, bundle);
}
@Override
public void restoreHierarchyState(SparseArray<Parcelable> sparseArray) {
Bundle state = (Bundle) sparseArray.get(0);
mRecommendationsCurrentPage = state.getInt(
RECOMMENDATIONS_SAVED_STATE_KEY, /*defaultValue=*/0);
mWidgetRecommendationsView.restoreState(state);
super.restoreHierarchyState(state.getSparseParcelableArray(SUPER_SAVED_STATE_KEY));
}
private static int getWidgetSheetId(BaseActivity activity) {
boolean isTwoPane = (activity.getDeviceProfile().isTablet
// Enables two pane picker for tablets in all orientations when the
// enableCategorizedWidgetSuggestions flag is on.
&& (activity.getDeviceProfile().isLandscape || enableCategorizedWidgetSuggestions())
&& !activity.getDeviceProfile().isTwoPanels)
// Enables two pane picker for unfolded foldables if the flag is on.
|| (activity.getDeviceProfile().isTwoPanels && enableUnfoldedTwoPanePicker());
return isTwoPane ? R.layout.widgets_two_pane_sheet : R.layout.widgets_full_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(BaseActivity launcher) {
return launcher.findViewById(R.id.primary_widgets_list_view);
}
@Override
public void addHintCloseAnim(
float distanceToMove, Interpolator interpolator, PendingAnimation target) {
target.addAnimatedFloat(mSwipeToDismissProgress, 0f, 1f, interpolator);
}
@Override
protected void onCloseComplete() {
super.onCloseComplete();
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. */
protected static int measureHeightWithVerticalMargins(View view) {
if (view == null || view.getVisibility() != VISIBLE) {
return 0;
}
MarginLayoutParams marginLayoutParams = (MarginLayoutParams) view.getLayoutParams();
return view.getMeasuredHeight() + marginLayoutParams.bottomMargin
+ marginLayoutParams.topMargin;
}
protected int getCurrentAdapterHolderType() {
if (mIsInSearchMode) {
return SEARCH;
} else if (mViewPager != null) {
return mViewPager.getCurrentPage();
} else {
return AdapterHolder.PRIMARY;
}
}
private void restorePreviousAdapterHolderType(int previousAdapterHolderType) {
if (previousAdapterHolderType == AdapterHolder.WORK && mViewPager != null) {
mViewPager.setCurrentPage(previousAdapterHolderType);
} else if (previousAdapterHolderType == AdapterHolder.SEARCH) {
enterSearchMode(false);
}
}
@Override
public void onDeviceProfileChanged(DeviceProfile dp) {
super.onDeviceProfileChanged(dp);
if (shouldRecreateLayout(/*oldDp=*/ mDeviceProfile, /*newDp=*/ dp)) {
SparseArray<Parcelable> widgetsState = new SparseArray<>();
saveHierarchyState(widgetsState);
handleClose(false);
WidgetsFullSheet sheet = show(BaseActivity.fromContext(getContext()), false);
sheet.restoreRecommendations(mRecommendedWidgets, mRecommendedWidgetsMap);
sheet.restoreHierarchyState(widgetsState);
sheet.restoreAdapterStates(mAdapters);
sheet.restorePreviousAdapterHolderType(getCurrentAdapterHolderType());
} else if (!isTwoPane()) {
reset();
resetExpandedHeaders();
}
mDeviceProfile = dp;
}
private void restoreRecommendations(List<WidgetItem> recommendedWidgets,
Map<WidgetRecommendationCategory, List<WidgetItem>> recommendedWidgetsMap) {
mRecommendedWidgets = recommendedWidgets;
mRecommendedWidgetsMap = recommendedWidgetsMap;
}
private void restoreAdapterStates(SparseArray<AdapterHolder> adapters) {
if (adapters.contains(AdapterHolder.WORK)) {
mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.restoreState(
adapters.get(AdapterHolder.WORK).mWidgetsListAdapter);
}
mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.restoreState(
adapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter);
mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.restoreState(
adapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter);
}
/**
* Indicates if layout should be re-created on device profile change - so that a different
* layout can be displayed.
*/
private static boolean shouldRecreateLayout(DeviceProfile oldDp, DeviceProfile newDp) {
// When folding/unfolding the foldables, we need to switch between the regular widget picker
// and the two pane picker, so we rebuild the picker with the correct layout.
boolean isFoldUnFold =
oldDp.isTwoPanels != newDp.isTwoPanels && enableUnfoldedTwoPanePicker();
// In tablets, on orientation change we switch between single and two pane picker unless the
// categorized suggestions flag was on. With the categorized suggestions feature, we use a
// two pane picker across all orientations.
boolean useDifferentLayoutOnOrientationChange =
(!enableCategorizedWidgetSuggestions() && (newDp.isTablet && !newDp.isTwoPanels
&& oldDp.isLandscape != newDp.isLandscape));
return isFoldUnFold || useDifferentLayoutOnOrientationChange;
}
/**
* In widget search mode, we should scale down content inside widget bottom sheet, rather
* than the whole bottom sheet, to indicate we will navigate back within the widget
* bottom sheet.
*/
@Override
public boolean shouldAnimateContentViewInBackSwipe() {
return mIsInSearchMode;
}
@Override
public void onBackInvoked() {
if (mIsInSearchMode) {
mSearchBar.reset();
// Posting animation to next frame will let widget sheet finish updating UI first, and
// make animation smoother.
post(this::animateSwipeToDismissProgressToStart);
} else {
super.onBackInvoked();
}
}
@Override
public void onDragStart(boolean start, float startDisplacement) {
super.onDragStart(start, startDisplacement);
WindowInsetsController insetsController = getWindowInsetsController();
if (insetsController != null) {
insetsController.hide(WindowInsets.Type.ime());
}
}
@Nullable
private View getViewToShowEducationTip() {
if (mWidgetRecommendationsContainer.getVisibility() == VISIBLE) {
return mWidgetRecommendationsView.getViewForEducationTip();
}
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;
}
protected boolean isTwoPane() {
return false;
}
/** Gets the sheet for widget picker, which is used for testing. */
@VisibleForTesting
public View getSheet() {
return mContent;
}
/** Opens the first header in widget picker and scrolls to the top of the RecyclerView. */
@VisibleForTesting
public void openFirstHeader() {
mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.selectFirstHeaderEntry();
mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView.scrollToTop();
}
@Override
protected int getHeaderTopClip(@NonNull WidgetCell cell) {
StickyHeaderLayout header = findViewById(R.id.search_and_recommendations_container);
if (header == null) {
return 0;
}
Rect cellRect = new Rect();
boolean cellIsPartiallyVisible = cell.getGlobalVisibleRect(cellRect);
if (cellIsPartiallyVisible) {
Rect occludingRect = new Rect();
for (View headerChild : header.getStickyChildren()) {
Rect childRect = new Rect();
boolean childVisible = headerChild.getGlobalVisibleRect(childRect);
if (childVisible && childRect.intersect(cellRect)) {
occludingRect.union(childRect);
}
}
if (!occludingRect.isEmpty() && cellRect.top < occludingRect.bottom) {
return occludingRect.bottom - cellRect.top;
}
}
return 0;
}
@Override
protected void scrollCellContainerByY(WidgetCell wc, int scrollByY) {
for (ViewParent parent = wc.getParent(); parent != null; parent = parent.getParent()) {
if (parent instanceof WidgetsRecyclerView recyclerView) {
// Scrollable container for main widget list.
recyclerView.smoothScrollBy(0, scrollByY);
return;
} else if (parent instanceof StickyHeaderLayout header) {
// Scrollable container for recommendations. We still scroll on the recycler (even
// though the recommendations are not in the recycler view) because the
// StickyHeaderLayout scroll is connected to the currently visible recycler view.
WidgetsRecyclerView recyclerView = findVisibleRecyclerView();
if (recyclerView != null) {
recyclerView.smoothScrollBy(0, scrollByY);
}
return;
} else if (parent == this) {
return;
}
}
}
@Nullable
private WidgetsRecyclerView findVisibleRecyclerView() {
if (mViewPager != null) {
return (WidgetsRecyclerView) mViewPager.getPageAt(mViewPager.getCurrentPage());
}
return findViewById(R.id.primary_widgets_list_view);
}
/** A holder class for holding adapters & their corresponding recycler view. */
final class AdapterHolder {
static final int PRIMARY = 0;
static final int WORK = 1;
static final int SEARCH = 2;
private final int mAdapterType;
final WidgetsListAdapter mWidgetsListAdapter;
private final DefaultItemAnimator mWidgetsListItemAnimator;
WidgetsRecyclerView mWidgetsRecyclerView;
AdapterHolder(int adapterType) {
mAdapterType = adapterType;
Context context = getContext();
mWidgetsListAdapter = new WidgetsListAdapter(
context,
LayoutInflater.from(context),
this::getEmptySpaceHeight,
/* iconClickListener= */ WidgetsFullSheet.this,
/* iconLongClickListener= */ WidgetsFullSheet.this,
/* expandButtonClickListener= */ WidgetsFullSheet.this,
isTwoPane());
mWidgetsListAdapter.setHasStableIds(true);
switch (mAdapterType) {
case PRIMARY:
mWidgetsListAdapter.setFilter(mPrimaryWidgetsFilter);
break;
case WORK:
mWidgetsListAdapter.setFilter(mWorkWidgetsFilter);
break;
default:
break;
}
mWidgetsListItemAnimator = new WidgetsListItemAnimator();
}
/**
* Swaps the adapter to existing adapter to prevent the recycler view from using stale view
* to animate in the new visibility update.
*
* <p>For instance, when clearing search text and re-entering search with new list shouldn't
* use stale results to animate in new results. Alternative is setting list animators to
* null, but, we need animations with the default item animator.
*/
private void reset() {
mWidgetsRecyclerView.swapAdapter(
mWidgetsListAdapter,
/*removeAndRecycleExistingViews=*/ true
);
}
private int getEmptySpaceHeight() {
return mStickyHeaderLayout != null ? mStickyHeaderLayout.getHeaderHeight() : 0;
}
void setup(WidgetsRecyclerView recyclerView) {
mWidgetsRecyclerView = recyclerView;
mWidgetsRecyclerView.setOutlineProvider(mViewOutlineProvider);
mWidgetsRecyclerView.setClipToOutline(true);
mWidgetsRecyclerView.setClipChildren(false);
mWidgetsRecyclerView.setAdapter(mWidgetsListAdapter);
mWidgetsRecyclerView.bindFastScrollbar(mFastScroller, WIDGET_SCROLLER);
mWidgetsRecyclerView.setItemAnimator(isTwoPane() ? null : mWidgetsListItemAnimator);
mWidgetsRecyclerView.setHeaderViewDimensionsProvider(WidgetsFullSheet.this);
if (!isTwoPane()) {
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.setMaxHorizontalSpansPxPerRow(mMaxSpanPerRow);
}
}
/**
* Helper to identify if searchbar's focus can be cleared when user performs an action
* outside search.
*/
private static class SearchClearFocusHelper {
private float mFirstInteractionX = -1f;
private float mFirstInteractionY = -1f;
/**
* For a given [MotionEvent] indicates if we should clear focus from search (and hide IME).
*/
boolean shouldClearFocus(MotionEvent ev, float touchSlop) {
int action = ev.getAction();
boolean clearFocus = false;
if (action == MotionEvent.ACTION_DOWN) {
mFirstInteractionX = ev.getX();
mFirstInteractionY = ev.getY();
} else if (action == MotionEvent.ACTION_CANCEL) {
// This is when user performed a gesture e.g. predictive back
// We don't handle it ourselves and let IME handle the close.
mFirstInteractionY = -1;
mFirstInteractionX = -1;
} else if (action == MotionEvent.ACTION_UP) {
// Its clear that user action wasn't predictive back - but press / scroll etc. that
// should hide the keyboard.
clearFocus = true;
mFirstInteractionY = -1;
mFirstInteractionX = -1;
} else if (action == MotionEvent.ACTION_MOVE) {
// Sometimes, on move, we may not receive ACTION_UP, but if the move was within
// touch slop and we didn't know if its moved or cancelled, we can clear focus.
// Example case: Apps list is small and you do a little scroll on list - in such, we
// want to still hide the keyboard.
if (mFirstInteractionX != -1 && mFirstInteractionY != -1) {
float distY = abs(mFirstInteractionY - ev.getY());
float distX = abs(mFirstInteractionX - ev.getX());
if (distY >= touchSlop || distX >= touchSlop) {
clearFocus = true;
mFirstInteractionY = -1;
mFirstInteractionX = -1;
}
}
}
return clearFocus;
}
}
}