| /* |
| * 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.LauncherAnimUtils.VIEW_TRANSLATE_Y; |
| import static com.android.launcher3.testing.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.graphics.Rect; |
| import android.os.Process; |
| import android.os.UserHandle; |
| 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.TextView; |
| |
| import androidx.annotation.Nullable; |
| import androidx.annotation.VisibleForTesting; |
| import androidx.core.view.ViewCompat; |
| import androidx.recyclerview.widget.RecyclerView; |
| |
| import com.android.launcher3.DeviceProfile; |
| import com.android.launcher3.Insettable; |
| 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.PendingAnimation; |
| import com.android.launcher3.compat.AccessibilityManagerCompat; |
| import com.android.launcher3.model.WidgetItem; |
| import com.android.launcher3.views.ArrowTipView; |
| import com.android.launcher3.views.RecyclerViewFastScroller; |
| import com.android.launcher3.views.TopRoundedCornerView; |
| import com.android.launcher3.widget.BaseWidgetSheet; |
| import com.android.launcher3.widget.LauncherAppWidgetHost.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.picker.search.WidgetsSearchBarUIHelper; |
| 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 Insettable, ProviderChangedListener, OnActivePageChangedListener, |
| WidgetsRecyclerView.HeaderViewDimensionsProvider, SearchModeListener, |
| WidgetsSearchBarUIHelper { |
| private static final String TAG = WidgetsFullSheet.class.getSimpleName(); |
| |
| private static final long DEFAULT_OPEN_DURATION = 267; |
| private static final long FADE_IN_DURATION = 150; |
| private static final long EDUCATION_TIP_DELAY_MS = 200; |
| 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 WIDGETS_EDUCATION_TIP_SEEN = "launcher.widgets_education_tip_seen"; |
| |
| private final Rect mInsets = new Rect(); |
| 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 = |
| mPrimaryWidgetsFilter.negate(); |
| 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; |
| } |
| View viewForTip = getViewToShowEducationTip(); |
| if (viewForTip != null && ViewCompat.isLaidOut(viewForTip)) { |
| removeOnLayoutChangeListener(mLayoutChangeListenerToShowTips); |
| showEducationTipOnView(viewForTip); |
| } |
| }; |
| private final int mTabsHeight; |
| private final int mWidgetCellHorizontalPadding; |
| |
| @Nullable private PersonalWorkPagedView mViewPager; |
| private boolean mIsInSearchMode; |
| private int mMaxSpansPerRow = 4; |
| private View mTabsView; |
| private TextView mNoWidgetsView; |
| private SearchAndRecommendationViewHolder mSearchAndRecommendationViewHolder; |
| private SearchAndRecommendationsScrollController mSearchAndRecommendationsScrollController; |
| |
| 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)); |
| mTabsHeight = mHasWorkProfile |
| ? getContext().getResources() |
| .getDimensionPixelSize(R.dimen.all_apps_header_tab_height) |
| : 0; |
| mWidgetCellHorizontalPadding = 2 * getResources().getDimensionPixelOffset( |
| R.dimen.widget_cell_horizontal_padding); |
| } |
| |
| public WidgetsFullSheet(Context context, AttributeSet attrs) { |
| this(context, attrs, 0); |
| } |
| |
| @Override |
| protected void onFinishInflate() { |
| super.onFinishInflate(); |
| mContent = findViewById(R.id.container); |
| TopRoundedCornerView springLayout = (TopRoundedCornerView) mContent; |
| |
| LayoutInflater layoutInflater = LayoutInflater.from(getContext()); |
| int contentLayoutRes = mHasWorkProfile ? R.layout.widgets_full_sheet_paged_view |
| : R.layout.widgets_full_sheet_recyclerview; |
| layoutInflater.inflate(contentLayoutRes, springLayout, 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); |
| mTabsView = findViewById(R.id.tabs); |
| findViewById(R.id.tab_personal) |
| .setOnClickListener((View view) -> mViewPager.snapToPage(0)); |
| findViewById(R.id.tab_work) |
| .setOnClickListener((View view) -> mViewPager.snapToPage(1)); |
| fastScroller.setIsRecyclerViewFirstChildInParent(false); |
| mAdapters.get(AdapterHolder.WORK).setup(findViewById(R.id.work_widgets_list_view)); |
| } else { |
| mViewPager = null; |
| } |
| |
| layoutInflater.inflate(R.layout.widgets_full_sheet_search_and_recommendations, springLayout, |
| true); |
| mSearchAndRecommendationViewHolder = new SearchAndRecommendationViewHolder( |
| findViewById(R.id.search_and_recommendations_container)); |
| mSearchAndRecommendationsScrollController = new SearchAndRecommendationsScrollController( |
| mHasWorkProfile, |
| mTabsHeight, |
| mSearchAndRecommendationViewHolder, |
| findViewById(R.id.primary_widgets_list_view), |
| mHasWorkProfile ? findViewById(R.id.work_widgets_list_view) : null, |
| findViewById(R.id.search_widgets_list_view), |
| mTabsView, |
| mViewPager); |
| fastScroller.setOnFastScrollChangeListener(mSearchAndRecommendationsScrollController); |
| |
| mNoWidgetsView = findViewById(R.id.no_widgets_text); |
| |
| onRecommendedWidgetsBound(); |
| onWidgetsBound(); |
| |
| mSearchAndRecommendationViewHolder.mSearchBar.initialize( |
| mLauncher.getPopupDataProvider(), /* searchModeListener= */ this); |
| |
| if (!hasSeenEducationTip()) { |
| addOnLayoutChangeListener(mLayoutChangeListenerToShowTips); |
| } |
| } |
| |
| @Override |
| public void onActivePageChanged(int currentActivePage) { |
| AdapterHolder currentAdapterHolder = mAdapters.get(currentActivePage); |
| WidgetsRecyclerView currentRecyclerView = |
| mAdapters.get(currentActivePage).mWidgetsRecyclerView; |
| |
| updateRecyclerViewVisibility(currentAdapterHolder); |
| attachScrollbarToRecyclerView(currentRecyclerView); |
| resetExpandedHeaders(); |
| } |
| |
| private void attachScrollbarToRecyclerView(WidgetsRecyclerView recyclerView) { |
| recyclerView.bindFastScrollbar(); |
| mSearchAndRecommendationsScrollController.setCurrentRecyclerView(recyclerView); |
| reset(); |
| } |
| |
| private void updateRecyclerViewVisibility(AdapterHolder adapterHolder) { |
| boolean isWidgetAvailable = adapterHolder.mWidgetsListAdapter.getItemCount() > 0; |
| adapterHolder.mWidgetsRecyclerView.setVisibility(isWidgetAvailable ? VISIBLE : GONE); |
| |
| mNoWidgetsView.setText( |
| adapterHolder.mAdapterType == AdapterHolder.SEARCH |
| ? R.string.no_search_results |
| : 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(); |
| mSearchAndRecommendationsScrollController.reset(); |
| } |
| |
| @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(); |
| mLauncher.getAppWidgetHost().addProviderChangeListener(this); |
| notifyWidgetProvidersChanged(); |
| onRecommendedWidgetsBound(); |
| } |
| |
| @Override |
| protected void onDetachedFromWindow() { |
| super.onDetachedFromWindow(); |
| mLauncher.getAppWidgetHost().removeProviderChangeListener(this); |
| } |
| |
| @Override |
| public void setInsets(Rect insets) { |
| mInsets.set(insets); |
| |
| setBottomPadding(mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView, insets.bottom); |
| setBottomPadding(mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView, insets.bottom); |
| if (mHasWorkProfile) { |
| setBottomPadding(mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView, insets.bottom); |
| } |
| if (insets.bottom > 0) { |
| setupNavBarColor(); |
| } else { |
| clearNavBarColor(); |
| } |
| |
| ((TopRoundedCornerView) mContent).setNavBarScrimHeight(mInsets.bottom); |
| requestLayout(); |
| } |
| |
| private void setBottomPadding(RecyclerView recyclerView, int bottomPadding) { |
| recyclerView.setPadding( |
| recyclerView.getPaddingLeft(), |
| recyclerView.getPaddingTop(), |
| recyclerView.getPaddingRight(), |
| bottomPadding); |
| } |
| |
| @Override |
| protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| doMeasure(widthMeasureSpec, heightMeasureSpec); |
| |
| if (mSearchAndRecommendationsScrollController.updateMarginAndPadding()) { |
| doMeasure(widthMeasureSpec, heightMeasureSpec); |
| } |
| |
| if (updateMaxSpansPerRow()) { |
| doMeasure(widthMeasureSpec, heightMeasureSpec); |
| |
| if (mSearchAndRecommendationsScrollController.updateMarginAndPadding()) { |
| doMeasure(widthMeasureSpec, heightMeasureSpec); |
| } |
| } |
| } |
| |
| private void doMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| DeviceProfile deviceProfile = mLauncher.getDeviceProfile(); |
| int widthUsed; |
| if (mInsets.bottom > 0) { |
| widthUsed = mInsets.left + mInsets.right; |
| } else { |
| Rect padding = deviceProfile.workspacePadding; |
| widthUsed = Math.max(padding.left + padding.right, |
| 2 * (mInsets.left + mInsets.right)); |
| } |
| |
| int heightUsed = mInsets.top + deviceProfile.edgeMarginPx; |
| measureChildWithMargins(mContent, widthMeasureSpec, |
| widthUsed, heightMeasureSpec, heightUsed); |
| setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), |
| MeasureSpec.getSize(heightMeasureSpec)); |
| } |
| |
| /** Returns {@code true} if the max spans have been updated. */ |
| private boolean updateMaxSpansPerRow() { |
| if (getMeasuredWidth() == 0) return false; |
| |
| int previousMaxSpansPerRow = mMaxSpansPerRow; |
| mMaxSpansPerRow = getMeasuredWidth() |
| / (mLauncher.getDeviceProfile().cellWidthPx + mWidgetCellHorizontalPadding); |
| |
| if (previousMaxSpansPerRow != mMaxSpansPerRow) { |
| 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() { |
| mLauncher.refreshAndBindWidgetsForPackageUser(null); |
| } |
| |
| @Override |
| public void onWidgetsBound() { |
| if (mIsInSearchMode) { |
| return; |
| } |
| List<WidgetsListBaseEntry> allWidgets = mLauncher.getPopupDataProvider().getAllWidgets(); |
| |
| AdapterHolder primaryUserAdapterHolder = mAdapters.get(AdapterHolder.PRIMARY); |
| primaryUserAdapterHolder.mWidgetsListAdapter.setWidgets(allWidgets); |
| |
| if (mHasWorkProfile) { |
| mViewPager.setVisibility(VISIBLE); |
| mTabsView.setVisibility(VISIBLE); |
| AdapterHolder workUserAdapterHolder = mAdapters.get(AdapterHolder.WORK); |
| workUserAdapterHolder.mWidgetsListAdapter.setWidgets(allWidgets); |
| onActivePageChanged(mViewPager.getCurrentPage()); |
| } else { |
| updateRecyclerViewVisibility(primaryUserAdapterHolder); |
| } |
| } |
| |
| @Override |
| public void enterSearchMode() { |
| if (mIsInSearchMode) return; |
| setViewVisibilityBasedOnSearch(/*isInSearchMode= */ true); |
| attachScrollbarToRecyclerView(mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView); |
| resetExpandedHeaders(); |
| } |
| |
| @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); |
| |
| mSearchAndRecommendationsScrollController.updateMarginAndPadding(); |
| } |
| |
| @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) { |
| mSearchAndRecommendationViewHolder.mRecommendedWidgetsTable.setVisibility(GONE); |
| if (mHasWorkProfile) { |
| mViewPager.setVisibility(GONE); |
| mTabsView.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 = |
| mLauncher.getPopupDataProvider().getRecommendedWidgets(); |
| WidgetsRecommendationTableLayout table = |
| mSearchAndRecommendationViewHolder.mRecommendedWidgetsTable; |
| if (recommendedWidgets.size() > 0) { |
| float maxTableHeight = |
| (mLauncher.getDeviceProfile().availableHeightPx - mTabsHeight |
| - getHeaderViewHeight()) * RECOMMENDATION_TABLE_HEIGHT_RATIO; |
| List<ArrayList<WidgetItem>> recommendedWidgetsInTable = |
| WidgetsTableUtils.groupWidgetItemsIntoTable(recommendedWidgets, |
| mMaxSpansPerRow); |
| table.setRecommendedWidgets(recommendedWidgetsInTable, maxTableHeight); |
| } else { |
| table.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(DEFAULT_OPEN_DURATION) |
| .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, DEFAULT_OPEN_DURATION); |
| } |
| |
| @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; |
| RecyclerViewFastScroller scroller = getRecyclerView().getScrollbar(); |
| if (scroller.getThumbOffsetY() >= 0 |
| && getPopupContainer().isEventOverView(scroller, ev)) { |
| mNoIntercept = true; |
| } else if (getPopupContainer().isEventOverView(mContent, ev)) { |
| mNoIntercept = !getRecyclerView().shouldContainerScroll(ev, getPopupContainer()); |
| } |
| } |
| 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; |
| } |
| |
| /** 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(); |
| AccessibilityManagerCompat.sendStateEventToTest(getContext(), NORMAL_STATE_ORDINAL); |
| } |
| |
| @Override |
| public int getHeaderViewHeight() { |
| return measureHeightWithVerticalMargins(mSearchAndRecommendationViewHolder.mCollapseHandle) |
| + measureHeightWithVerticalMargins(mSearchAndRecommendationViewHolder.mHeaderTitle) |
| + measureHeightWithVerticalMargins( |
| (View) mSearchAndRecommendationViewHolder.mSearchBar); |
| } |
| |
| /** 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) { |
| mSearchAndRecommendationViewHolder.mSearchBar.reset(); |
| } |
| } |
| |
| @Override |
| public boolean onBackPressed() { |
| if (mIsInSearchMode) { |
| mSearchAndRecommendationViewHolder.mSearchBar.reset(); |
| return true; |
| } |
| return super.onBackPressed(); |
| } |
| |
| @Override |
| public void onDragStart(boolean start, float startDisplacement) { |
| super.onDragStart(start, startDisplacement); |
| getWindowInsetsController().hide(WindowInsets.Type.ime()); |
| } |
| |
| @Override |
| public void clearSearchBarFocus() { |
| mSearchAndRecommendationViewHolder.mSearchBar.clearSearchBarFocus(); |
| } |
| |
| private void showEducationTipOnView(View view) { |
| mLauncher.getSharedPrefs().edit().putBoolean(WIDGETS_EDUCATION_TIP_SEEN, true).apply(); |
| int[] coords = new int[2]; |
| view.getLocationOnScreen(coords); |
| ArrowTipView arrowTipView = new ArrowTipView(mLauncher); |
| arrowTipView.showAtLocation( |
| getContext().getString(R.string.long_press_widget_to_add), |
| /* arrowXCoord= */coords[0] + view.getWidth() / 2, |
| /* yCoord= */coords[1]); |
| } |
| |
| @Nullable private View getViewToShowEducationTip() { |
| if (mSearchAndRecommendationViewHolder.mRecommendedWidgetsTable.getVisibility() == VISIBLE |
| && mSearchAndRecommendationViewHolder.mRecommendedWidgetsTable.getChildCount() > 0 |
| ) { |
| return ((ViewGroup) mSearchAndRecommendationViewHolder.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.mTableContainer.getChildAt(0)).getChildAt(0); |
| } |
| |
| return null; |
| } |
| |
| private boolean hasSeenEducationTip() { |
| return mLauncher.getSharedPrefs().getBoolean(WIDGETS_EDUCATION_TIP_SEEN, false) |
| || Utilities.IS_RUNNING_IN_TEST_HARNESS; |
| } |
| |
| /** 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 WidgetsRecyclerView mWidgetsRecyclerView; |
| |
| AdapterHolder(int adapterType) { |
| mAdapterType = adapterType; |
| |
| Context context = getContext(); |
| LauncherAppState apps = LauncherAppState.getInstance(context); |
| mWidgetsListAdapter = new WidgetsListAdapter( |
| context, |
| LayoutInflater.from(context), |
| apps.getWidgetCache(), |
| apps.getIconCache(), |
| /* iconClickListener= */ WidgetsFullSheet.this, |
| /* iconLongClickListener= */ WidgetsFullSheet.this, |
| /* WidgetsSearchBarUIHelper= */ |
| mAdapterType == SEARCH ? WidgetsFullSheet.this : null); |
| mWidgetsListAdapter.setHasStableIds(true); |
| switch (mAdapterType) { |
| case PRIMARY: |
| mWidgetsListAdapter.setFilter(mPrimaryWidgetsFilter); |
| break; |
| case WORK: |
| mWidgetsListAdapter.setFilter(mWorkWidgetsFilter); |
| break; |
| default: |
| break; |
| } |
| } |
| |
| void setup(WidgetsRecyclerView recyclerView) { |
| mWidgetsRecyclerView = recyclerView; |
| mWidgetsRecyclerView.setAdapter(mWidgetsListAdapter); |
| // Disables animation because it disrupts the item focus upon adapter item change. |
| mWidgetsRecyclerView.setItemAnimator(null); |
| mWidgetsRecyclerView.setHeaderViewDimensionsProvider(WidgetsFullSheet.this); |
| mWidgetsRecyclerView.setEdgeEffectFactory( |
| ((TopRoundedCornerView) mContent).createEdgeEffectFactory()); |
| mWidgetsListAdapter.setApplyBitmapDeferred(false, mWidgetsRecyclerView); |
| mWidgetsListAdapter.setMaxHorizontalSpansPerRow(mMaxSpansPerRow); |
| } |
| } |
| |
| final class SearchAndRecommendationViewHolder { |
| final ViewGroup mContainer; |
| final View mCollapseHandle; |
| final WidgetsSearchBar mSearchBar; |
| final TextView mHeaderTitle; |
| final WidgetsRecommendationTableLayout mRecommendedWidgetsTable; |
| |
| SearchAndRecommendationViewHolder(ViewGroup searchAndRecommendationContainer) { |
| mContainer = searchAndRecommendationContainer; |
| mCollapseHandle = mContainer.findViewById(R.id.collapse_handle); |
| mSearchBar = mContainer.findViewById(R.id.widgets_search_bar); |
| mHeaderTitle = mContainer.findViewById(R.id.title); |
| mRecommendedWidgetsTable = mContainer.findViewById(R.id.recommended_widget_table); |
| mRecommendedWidgetsTable.setWidgetCellOnTouchListener((view, event) -> { |
| getRecyclerView().onTouchEvent(event); |
| return false; |
| }); |
| mRecommendedWidgetsTable.setWidgetCellLongClickListener(WidgetsFullSheet.this); |
| mRecommendedWidgetsTable.setWidgetCellOnClickListener(WidgetsFullSheet.this); |
| } |
| } |
| } |