diff --git a/res/layout/widget_recommendations.xml b/res/layout/widget_recommendations.xml new file mode 100644 index 0000000000..89821ac909 --- /dev/null +++ b/res/layout/widget_recommendations.xml @@ -0,0 +1,58 @@ + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/widget_recommendations_table.xml b/res/layout/widget_recommendations_table.xml new file mode 100644 index 0000000000..e3f05620cd --- /dev/null +++ b/res/layout/widget_recommendations_table.xml @@ -0,0 +1,21 @@ + + diff --git a/res/layout/widgets_full_sheet_paged_view.xml b/res/layout/widgets_full_sheet_paged_view.xml index 069d4bc9ab..1d370435bf 100644 --- a/res/layout/widgets_full_sheet_paged_view.xml +++ b/res/layout/widgets_full_sheet_paged_view.xml @@ -73,15 +73,18 @@ - + + android:orientation="vertical" + android:layout_marginHorizontal="@dimen/widget_list_horizontal_margin" + android:visibility="gone"> + + - + + android:orientation="vertical" + android:visibility="gone"> + + \ No newline at end of file diff --git a/res/layout/widgets_two_pane_sheet.xml b/res/layout/widgets_two_pane_sheet.xml index f692e24f40..8e45740fec 100644 --- a/res/layout/widgets_two_pane_sheet.xml +++ b/res/layout/widgets_two_pane_sheet.xml @@ -118,13 +118,16 @@ android:background="@drawable/widgets_surface_background" android:importantForAccessibility="yes" android:id="@+id/right_pane"> - + + android:background="@drawable/widgets_surface_background" + android:orientation="vertical" + android:visibility="gone"> + + diff --git a/res/values-sw720dp-land/dimens.xml b/res/values-sw720dp-land/dimens.xml index 4d0ac383d3..dd58ceeafb 100644 --- a/res/values-sw720dp-land/dimens.xml +++ b/res/values-sw720dp-land/dimens.xml @@ -32,7 +32,4 @@ 49dp 24dp - - - 0dp diff --git a/res/values-sw720dp/dimens.xml b/res/values-sw720dp/dimens.xml index 2b0382d2c4..3c79588258 100644 --- a/res/values-sw720dp/dimens.xml +++ b/res/values-sw720dp/dimens.xml @@ -38,9 +38,6 @@ 30dp - - 300dp - 24dp diff --git a/res/values/dimens.xml b/res/values/dimens.xml index c7190b67b6..c101762678 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -182,9 +182,8 @@ 6dp 117dp 0dp - - 8dp - + 8dp + 16dp 16dp @@ -454,7 +453,6 @@ 8dp - 0dp 36dp 32dp 4dp diff --git a/res/values/strings.xml b/res/values/strings.xml index 31c098cb2d..379cdda8db 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -71,7 +71,7 @@ Suggestions - Boost your day + Your Daily Essentials News For You Your Chill Zone Reach Your Fitness Goals diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java index 563dfe2f20..4afa8e0623 100644 --- a/src/com/android/launcher3/DeviceProfile.java +++ b/src/com/android/launcher3/DeviceProfile.java @@ -416,15 +416,18 @@ public class DeviceProfile { gridVisualizationPaddingY = res.getDimensionPixelSize( R.dimen.grid_visualization_vertical_cell_spacing); - // Tablet portrait mode uses a single pane widget picker and extra padding may be applied on - // top to avoid making it look too elongated. - final boolean applyExtraTopPadding = isTablet - && !isLandscape - && (aspectRatio > MIN_ASPECT_RATIO_FOR_EXTRA_TOP_PADDING); - bottomSheetTopPadding = mInsets.top // statusbar height - + (applyExtraTopPadding ? res.getDimensionPixelSize( - R.dimen.bottom_sheet_extra_top_padding) : 0) - + (isTablet ? 0 : edgeMarginPx); // phones need edgeMarginPx additional padding + { + // In large screens, in portrait mode, a bottom sheet can appear too elongated, so, we + // apply additional padding. + final boolean applyExtraTopPadding = isTablet + && !isLandscape + && (aspectRatio > MIN_ASPECT_RATIO_FOR_EXTRA_TOP_PADDING); + final int derivedTopPadding = heightPx / 6; + bottomSheetTopPadding = mInsets.top // statusbar height + + (applyExtraTopPadding ? derivedTopPadding : 0) + + (isTablet ? 0 : edgeMarginPx); // phones need edgeMarginPx additional padding + } + bottomSheetOpenDuration = res.getInteger(R.integer.config_bottomSheetOpenDuration); bottomSheetCloseDuration = res.getInteger(R.integer.config_bottomSheetCloseDuration); if (isTablet) { diff --git a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java index df369c62b8..1b5abaa451 100644 --- a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java +++ b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java @@ -367,7 +367,7 @@ public class PageIndicatorDots extends View implements Insettable, PageIndicator mNumPages = numMarkers; // If the last page gets removed we want to go to the previous page. - if (mNumPages == mActivePage) { + if (mNumPages > 0 && mNumPages == mActivePage) { mActivePage--; CURRENT_POSITION.set(this, (float) mActivePage); } diff --git a/src/com/android/launcher3/popup/PopupDataProvider.java b/src/com/android/launcher3/popup/PopupDataProvider.java index f1d837c7b4..fb463f7d24 100644 --- a/src/com/android/launcher3/popup/PopupDataProvider.java +++ b/src/com/android/launcher3/popup/PopupDataProvider.java @@ -222,6 +222,7 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan } /** Returns the recommended widgets mapped by their category. */ + @NonNull public Map> getCategorizedRecommendedWidgets() { Map allWidgetItems = mAllWidgets.stream() .filter(entry -> entry instanceof WidgetsListContentEntry) @@ -232,7 +233,8 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan Function.identity() )); return mRecommendedWidgets.stream() - .filter(itemInfo -> itemInfo instanceof PendingAddWidgetInfo) + .filter(itemInfo -> itemInfo instanceof PendingAddWidgetInfo + && ((PendingAddWidgetInfo) itemInfo).recommendationCategory != null) .collect(Collectors.groupingBy( it -> ((PendingAddWidgetInfo) it).recommendationCategory, Collectors.collectingAndThen( diff --git a/src/com/android/launcher3/widget/BaseWidgetSheet.java b/src/com/android/launcher3/widget/BaseWidgetSheet.java index 145ad8087e..54ce9732e2 100644 --- a/src/com/android/launcher3/widget/BaseWidgetSheet.java +++ b/src/com/android/launcher3/widget/BaseWidgetSheet.java @@ -16,6 +16,7 @@ package com.android.launcher3.widget; import static com.android.app.animation.Interpolators.EMPHASIZED; +import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions; import static com.android.launcher3.Flags.enableUnfoldedTwoPanePicker; import static com.android.launcher3.LauncherPrefs.WIDGETS_EDUCATION_TIP_SEEN; @@ -62,8 +63,10 @@ public abstract class BaseWidgetSheet extends AbstractSlideInView protected final Rect mInsets = new Rect(); - @Px protected int mContentHorizontalMargin; - @Px protected int mWidgetCellHorizontalPadding; + @Px + protected int mContentHorizontalMargin; + @Px + protected int mWidgetCellHorizontalPadding; protected int mNavBarScrimHeight; private final Paint mNavBarScrimPaint; @@ -196,7 +199,7 @@ public abstract class BaseWidgetSheet extends AbstractSlideInView DeviceProfile deviceProfile = mActivityContext.getDeviceProfile(); int widthUsed; if (deviceProfile.isTablet) { - widthUsed = Math.max(2 * getTabletMargin(deviceProfile), + widthUsed = Math.max(2 * getTabletHorizontalMargin(deviceProfile), 2 * (mInsets.left + mInsets.right)); } else if (mInsets.bottom > 0) { widthUsed = mInsets.left + mInsets.right; @@ -212,7 +215,11 @@ public abstract class BaseWidgetSheet extends AbstractSlideInView MeasureSpec.getSize(heightMeasureSpec)); } - private int getTabletMargin(DeviceProfile deviceProfile) { + private int getTabletHorizontalMargin(DeviceProfile deviceProfile) { + // All bottom-sheets showing widgets will be full-width across all devices. + if (enableCategorizedWidgetSuggestions()) { + return 0; + } if (deviceProfile.isLandscape && !deviceProfile.isTwoPanels) { return getResources().getDimensionPixelSize( R.dimen.widget_picker_landscape_tablet_left_right_margin); diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java index c75f9d114e..94c630a27a 100644 --- a/src/com/android/launcher3/widget/WidgetCell.java +++ b/src/com/android/launcher3/widget/WidgetCell.java @@ -367,6 +367,16 @@ public class WidgetCell extends LinearLayout { } } + /** + * Shows or hides the long description displayed below each widget. + * + * @param show a flag that shows the long description of the widget if {@code true}, hides it if + * {@code false}. + */ + public void showDescription(boolean show) { + mWidgetDescription.setVisibility(show ? VISIBLE : GONE); + } + @Override public boolean onTouchEvent(MotionEvent ev) { super.onTouchEvent(ev); diff --git a/src/com/android/launcher3/widget/WidgetsBottomSheet.java b/src/com/android/launcher3/widget/WidgetsBottomSheet.java index c347939d0a..ceb0072310 100644 --- a/src/com/android/launcher3/widget/WidgetsBottomSheet.java +++ b/src/com/android/launcher3/widget/WidgetsBottomSheet.java @@ -16,6 +16,7 @@ package com.android.launcher3.widget; +import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_BOTTOM_WIDGETS_TRAY; import android.content.Context; @@ -187,7 +188,13 @@ public class WidgetsBottomSheet extends BaseWidgetSheet { mWidgetCellHorizontalPadding) .forEach(row -> { TableRow tableRow = new TableRow(getContext()); - tableRow.setGravity(Gravity.TOP); + if (enableCategorizedWidgetSuggestions()) { + // Vertically center align items, so that even if they don't fill bounds, + // they can look organized when placed together in a row. + tableRow.setGravity(Gravity.CENTER_VERTICAL); + } else { + tableRow.setGravity(Gravity.TOP); + } row.forEach(widgetItem -> { WidgetCell widget = addItemCell(tableRow); widget.applyFromCellItem(widgetItem); diff --git a/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java new file mode 100644 index 0000000000..738d74efc8 --- /dev/null +++ b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2024 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.widget.util.WidgetsTableUtils.groupWidgetItemsUsingRowPxWithoutReordering; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.Nullable; +import androidx.annotation.Px; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.PagedView; +import com.android.launcher3.R; +import com.android.launcher3.model.WidgetItem; +import com.android.launcher3.pageindicators.PageIndicatorDots; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** + * A {@link PagedView} that displays widget recommendations in categories with dots as paged + * indicators. + */ +public final class WidgetRecommendationsView extends PagedView { + private @Px float mAvailableHeight = Float.MAX_VALUE; + + private static final int MAX_CATEGORIES = 3; + private TextView mRecommendationPageTitle; + private final List mCategoryTitles = new ArrayList<>(); + + @Nullable + private OnLongClickListener mWidgetCellOnLongClickListener; + @Nullable + private OnClickListener mWidgetCellOnClickListener; + + public WidgetRecommendationsView(Context context) { + this(context, /* attrs= */ null); + } + + public WidgetRecommendationsView(Context context, AttributeSet attrs) { + this(context, attrs, /* defStyleAttr= */ 0); + } + + public WidgetRecommendationsView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + public void initParentViews(View parent) { + super.initParentViews(parent); + mRecommendationPageTitle = parent.findViewById(R.id.recommendations_page_title); + } + + /** Sets a {@link android.view.View.OnLongClickListener} for all widget cells in this table. */ + public void setWidgetCellLongClickListener(OnLongClickListener onLongClickListener) { + mWidgetCellOnLongClickListener = onLongClickListener; + } + + /** Sets a {@link android.view.View.OnClickListener} for all widget cells in this table. */ + public void setWidgetCellOnClickListener(OnClickListener widgetCellOnClickListener) { + mWidgetCellOnClickListener = widgetCellOnClickListener; + } + + /** + * Displays all the provided recommendations in a single table if they fit. + * + * @param recommendedWidgets list of widgets to be displayed in recommendation section. + * @param availableHeight height in px that can be used to display the recommendations; + * recommendations that don't fit in this height won't be shown + * @param availableWidth width in px that the recommendations should display in + * @param cellPadding padding in px that should be applied to each widget in the + * recommendations + * @return {@code false} if no recommendations could fit in the available space. + */ + public boolean setRecommendations( + List recommendedWidgets, final @Px float availableHeight, + final @Px int availableWidth, final @Px int cellPadding) { + this.mAvailableHeight = availableHeight; + removeAllViews(); + + maybeDisplayInTable(recommendedWidgets, availableWidth, cellPadding); + updateTitleAndIndicator(); + return getChildCount() > 0; + } + + /** + * Displays the recommendations grouped by categories as pages. + *

In case of a single category, no title is displayed for it.

+ * + * @param recommendations a map of widget items per recommendation category + * @param availableHeight height in px that can be used to display the recommendations; + * recommendations that don't fit in this height won't be shown + * @param availableWidth width in px that the recommendations should display in + * @param cellPadding padding in px that should be applied to each widget in the + * recommendations + * @return {@code false} if no recommendations could fit in the available space. + */ + public boolean setRecommendations( + Map> recommendations, + final @Px float availableHeight, final @Px int availableWidth, + final @Px int cellPadding) { + this.mAvailableHeight = availableHeight; + Context context = getContext(); + removeAllViews(); + + int displayedCategories = 0; + + // Render top MAX_CATEGORIES in separate tables. Each table becomes a page. + for (Map.Entry> entry : + new TreeMap<>(recommendations).entrySet()) { + // If none of the recommendations for the category could fit in the mAvailableHeight, we + // don't want to add that category; and we look for the next one. + if (maybeDisplayInTable(entry.getValue(), availableWidth, cellPadding)) { + mCategoryTitles.add( + context.getResources().getString(entry.getKey().categoryTitleRes)); + displayedCategories++; + } + + if (displayedCategories == MAX_CATEGORIES) { + break; + } + } + + updateTitleAndIndicator(); + return getChildCount() > 0; + } + + /** Displays the page title and paging indicator if there are multiple pages. */ + private void updateTitleAndIndicator() { + int titleAndIndicatorVisibility = getPageCount() > 1 ? View.VISIBLE : View.GONE; + mRecommendationPageTitle.setVisibility(titleAndIndicatorVisibility); + mPageIndicator.setVisibility(titleAndIndicatorVisibility); + } + + @Override + protected void notifyPageSwitchListener(int prevPage) { + if (getPageCount() > 1) { + // Since the title is outside the paging scroll, we update the title on page switch. + mRecommendationPageTitle.setText(mCategoryTitles.get(getNextPage())); + super.notifyPageSwitchListener(prevPage); + requestLayout(); + } + } + + @Override + protected boolean canScroll(float absVScroll, float absHScroll) { + // Allow only horizontal scroll. + return (absHScroll > absVScroll) && super.canScroll(absVScroll, absHScroll); + } + + @Override + protected void onScrollChanged(int l, int t, int oldl, int oldt) { + super.onScrollChanged(l, t, oldl, oldt); + mPageIndicator.setScroll(l, mMaxScroll); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + boolean hasMultiplePages = getChildCount() > 0; + + if (hasMultiplePages) { + int finalWidth = MeasureSpec.getSize(widthMeasureSpec); + int desiredHeight = 0; + + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + measureChild(child, widthMeasureSpec, heightMeasureSpec); + if (mAvailableHeight == Float.MAX_VALUE) { + // When we are not limited by height, use currentPage's height. This is the case + // when the paged layout is placed in a scrollable container. We cannot use + // height + // of tallest child in such case, as it will display a scrollbar even for + // smaller + // pages that don't have more content. + if (i == mCurrentPage) { + int parentHeight = MeasureSpec.getSize(heightMeasureSpec); + desiredHeight = Math.max(parentHeight, child.getMeasuredHeight()); + } + } else { + // Use height of tallest child when we are limited to a certain height. + desiredHeight = Math.max(desiredHeight, child.getMeasuredHeight()); + } + } + + int finalHeight = resolveSizeAndState(desiredHeight, heightMeasureSpec, 0); + setMeasuredDimension(finalWidth, finalHeight); + } else { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + } + + /** + * Groups the provided recommendations into rows and displays them in a table if at least one + * fits. + *

Returns false if none of the recommendations could fit.

+ */ + private boolean maybeDisplayInTable(List recommendedWidgets, + final @Px int availableWidth, final @Px int cellPadding) { + Context context = getContext(); + DeviceProfile deviceProfile = Launcher.getLauncher(context).getDeviceProfile(); + LayoutInflater inflater = LayoutInflater.from(context); + + List> rows = groupWidgetItemsUsingRowPxWithoutReordering( + recommendedWidgets, + context, + deviceProfile, + availableWidth, + cellPadding); + + WidgetsRecommendationTableLayout recommendationsTable = + (WidgetsRecommendationTableLayout) inflater.inflate( + R.layout.widget_recommendations_table, + /* root=*/ this, + /* attachToRoot=*/ false); + recommendationsTable.setWidgetCellOnClickListener(mWidgetCellOnClickListener); + recommendationsTable.setWidgetCellLongClickListener(mWidgetCellOnLongClickListener); + + boolean displayedAtLeastOne = recommendationsTable.setRecommendedWidgets(rows, + mAvailableHeight); + if (displayedAtLeastOne) { + addView(recommendationsTable); + } + + return displayedAtLeastOne; + } + + /** Returns location of a widget cell for displaying the "touch and hold" education tip. */ + public View getViewForEducationTip() { + if (getChildCount() > 0) { + // first page (a table layout) -> first item (a widget cell). + return ((ViewGroup) getChildAt(0)).getChildAt(0); + } + return null; + } +} diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java index 4e704fdb10..237078e31c 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java +++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java @@ -17,6 +17,7 @@ package com.android.launcher3.widget.picker; import static android.view.View.MeasureSpec.makeMeasureSpec; +import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions; import static com.android.launcher3.Flags.enableUnfoldedTwoPanePicker; import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y; import static com.android.launcher3.LauncherPrefs.WIDGETS_EDUCATION_DIALOG_SEEN; @@ -45,6 +46,7 @@ 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 android.window.BackEvent; @@ -65,7 +67,6 @@ import com.android.launcher3.Utilities; 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; @@ -76,7 +77,6 @@ import com.android.launcher3.widget.BaseWidgetSheet; 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; @@ -162,12 +162,13 @@ public class WidgetsFullSheet extends BaseWidgetSheet @Nullable PersonalWorkPagedView mViewPager; private boolean mIsInSearchMode; private boolean mIsNoWidgetsViewNeeded; - @Px private int mMaxSpanPerRow; + @Px protected int mMaxSpanPerRow; protected DeviceProfile mDeviceProfile; protected TextView mNoWidgetsView; protected StickyHeaderLayout mSearchScrollView; - protected WidgetsRecommendationTableLayout mRecommendedWidgetsTable; + protected WidgetRecommendationsView mWidgetRecommendationsView; + protected LinearLayout mWidgetRecommendationsContainer; protected View mTabBar; protected View mSearchBarContainer; protected WidgetsSearchBar mSearchBar; @@ -221,9 +222,14 @@ public class WidgetsFullSheet extends BaseWidgetSheet setupViews(); - mRecommendedWidgetsTable = mSearchScrollView.findViewById(R.id.recommended_widget_table); - mRecommendedWidgetsTable.setWidgetCellLongClickListener(this); - mRecommendedWidgetsTable.setWidgetCellOnClickListener(this); + mWidgetRecommendationsContainer = mSearchScrollView.findViewById( + R.id.widget_recommendations_container); + mWidgetRecommendationsView = mSearchScrollView.findViewById( + R.id.widget_recommendations_view); + mWidgetRecommendationsView.initParentViews(mWidgetRecommendationsContainer); + mWidgetRecommendationsView.setWidgetCellLongClickListener(this); + mWidgetRecommendationsView.setWidgetCellOnClickListener(this); + mHeaderTitle = mSearchScrollView.findViewById(R.id.title); onRecommendedWidgetsBound(); @@ -551,7 +557,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet protected void setViewVisibilityBasedOnSearch(boolean isInSearchMode) { mIsInSearchMode = isInSearchMode; if (isInSearchMode) { - mRecommendedWidgetsTable.setVisibility(GONE); + mWidgetRecommendationsContainer.setVisibility(GONE); if (mHasWorkProfile) { mViewPager.setVisibility(GONE); mTabBar.setVisibility(GONE); @@ -580,40 +586,44 @@ public class WidgetsFullSheet extends BaseWidgetSheet if (mIsInSearchMode) { return; } - List recommendedWidgets = - mActivityContext.getPopupDataProvider().getRecommendedWidgets(); - mHasRecommendedWidgets = recommendedWidgets.size() > 0; - if (mHasRecommendedWidgets) { - 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(); - } - if (!isTwoPane()) { - doMeasure( - makeMeasureSpec(mActivityContext.getDeviceProfile().availableWidthPx, - MeasureSpec.EXACTLY), - makeMeasureSpec(mActivityContext.getDeviceProfile().availableHeightPx, - MeasureSpec.EXACTLY)); - } - float maxTableHeight = getMaxTableHeight(noWidgetsViewHeight); - List> recommendedWidgetsInTable = - WidgetsTableUtils.groupWidgetItemsUsingRowPxWithoutReordering( - recommendedWidgets, - mActivityContext, - mActivityContext.getDeviceProfile(), - mMaxSpanPerRow, - mWidgetCellHorizontalPadding); - mRecommendedWidgetsTable.setRecommendedWidgets( - recommendedWidgetsInTable, maxTableHeight); + if (enableCategorizedWidgetSuggestions()) { + mHasRecommendedWidgets = mWidgetRecommendationsView.setRecommendations( + mActivityContext.getPopupDataProvider().getCategorizedRecommendedWidgets(), + /* availableHeight= */ getMaxAvailableHeightForRecommendations(), + /* availableWidth= */ mMaxSpanPerRow, + /* cellPadding= */ mWidgetCellHorizontalPadding + ); } else { - mRecommendedWidgetsTable.setVisibility(GONE); + mHasRecommendedWidgets = mWidgetRecommendationsView.setRecommendations( + mActivityContext.getPopupDataProvider().getRecommendedWidgets(), + /* availableHeight= */ getMaxAvailableHeightForRecommendations(), + /* availableWidth= */ mMaxSpanPerRow, + /* cellPadding= */ mWidgetCellHorizontalPadding + ); } + mWidgetRecommendationsContainer.setVisibility(mHasRecommendedWidgets ? VISIBLE : GONE); + } + + @Px + private float getMaxAvailableHeightForRecommendations() { + 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(); + } + if (!isTwoPane()) { + doMeasure( + makeMeasureSpec(mActivityContext.getDeviceProfile().availableWidthPx, + MeasureSpec.EXACTLY), + makeMeasureSpec(mActivityContext.getDeviceProfile().availableHeightPx, + MeasureSpec.EXACTLY)); + } + return getMaxTableHeight(noWidgetsViewHeight); } /** b/209579563: "Widgets" header should be focused first. */ @@ -622,7 +632,8 @@ public class WidgetsFullSheet extends BaseWidgetSheet return mHeaderTitle; } - protected float getMaxTableHeight(float noWidgetsViewHeight) { + @Px + protected float getMaxTableHeight(@Px float noWidgetsViewHeight) { return (mContent.getMeasuredHeight() - mTabsHeight - getHeaderViewHeight() - noWidgetsViewHeight) @@ -699,7 +710,9 @@ public class WidgetsFullSheet extends BaseWidgetSheet private static int getWidgetSheetId(BaseActivity activity) { boolean isTwoPane = (activity.getDeviceProfile().isTablet - && activity.getDeviceProfile().isLandscape + // 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()); @@ -813,28 +826,40 @@ public class WidgetsFullSheet extends BaseWidgetSheet public void onDeviceProfileChanged(DeviceProfile dp) { super.onDeviceProfileChanged(dp); - if (mDeviceProfile.isLandscape != dp.isLandscape && dp.isTablet && !dp.isTwoPanels) { - handleClose(false); - show(BaseActivity.fromContext(getContext()), false); - } else if (!isTwoPane()) { - reset(); - resetExpandedHeaders(); - } - - // 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. - if (mDeviceProfile.isTwoPanels != dp.isTwoPanels && enableUnfoldedTwoPanePicker()) { + if (shouldRecreateLayout(/*oldDp=*/ mDeviceProfile, /*newDp=*/ dp)) { SparseArray widgetsState = new SparseArray<>(); saveHierarchyState(widgetsState); handleClose(false); WidgetsFullSheet sheet = show(BaseActivity.fromContext(getContext()), false); sheet.restoreHierarchyState(widgetsState); sheet.restorePreviousAdapterHolderType(getCurrentAdapterHolderType()); + } else if (!isTwoPane()) { + reset(); + resetExpandedHeaders(); } mDeviceProfile = dp; } + /** + * 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; + } + @Override public void onBackInvoked() { if (mIsInSearchMode) { @@ -855,9 +880,8 @@ public class WidgetsFullSheet extends BaseWidgetSheet } @Nullable private View getViewToShowEducationTip() { - if (mRecommendedWidgetsTable.getVisibility() == VISIBLE - && mRecommendedWidgetsTable.getChildCount() > 0) { - return ((ViewGroup) mRecommendedWidgetsTable.getChildAt(0)).getChildAt(0); + if (mWidgetRecommendationsContainer.getVisibility() == VISIBLE) { + return mWidgetRecommendationsView.getViewForEducationTip(); } AdapterHolder adapterHolder = mAdapters.get(mIsInSearchMode diff --git a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java index c7d2aa3fd8..f10ab48498 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java +++ b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java @@ -15,6 +15,8 @@ */ package com.android.launcher3.widget.picker; +import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions; + import android.content.Context; import android.graphics.Bitmap; import android.util.Log; @@ -147,7 +149,13 @@ public final class WidgetsListTableViewHolderBinder tableRow = (TableRow) table.getChildAt(i); } else { tableRow = new TableRow(table.getContext()); - tableRow.setGravity(Gravity.TOP); + if (enableCategorizedWidgetSuggestions()) { + // Vertically center align items, so that even if they don't fill bounds, they + // can look organized when placed together in a row. + tableRow.setGravity(Gravity.CENTER_VERTICAL); + } else { + tableRow.setGravity(Gravity.TOP); + } table.addView(tableRow); } if (tableRow.getChildCount() > widgetItems.size()) { diff --git a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java index 06cc65e4c1..ce1f4e0b7c 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java +++ b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java @@ -15,6 +15,7 @@ */ package com.android.launcher3.widget.picker; +import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION; import android.content.Context; @@ -61,7 +62,7 @@ public final class WidgetsRecommendationTableLayout extends TableLayout { super(context, attrs); // There are 1 row for title, 1 row for dimension and 2 rows for description. mWidgetsRecommendationTableVerticalPadding = 2 * getResources() - .getDimensionPixelSize(R.dimen.recommended_widgets_table_vertical_padding); + .getDimensionPixelSize(R.dimen.widget_recommendations_table_vertical_padding); mWidgetCellVerticalPadding = 2 * getResources() .getDimensionPixelSize(R.dimen.widget_cell_vertical_padding); mWidgetCellTextViewsHeight = 4 * getResources().getDimension(R.dimen.widget_cell_font_size); @@ -84,17 +85,20 @@ public final class WidgetsRecommendationTableLayout extends TableLayout { *

If the content can't fit {@code recommendationTableMaxHeight}, this view will remove a * last row from the {@code recommendedWidgets} until it fits or only one row left. If the only * row still doesn't fit, we scale down the preview image. + * + *

Returns {@code false} if none of the widgets could fit

*/ - public void setRecommendedWidgets(List> recommendedWidgets, + public boolean setRecommendedWidgets(List> recommendedWidgets, float recommendationTableMaxHeight) { mRecommendationTableMaxHeight = recommendationTableMaxHeight; RecommendationTableData data = fitRecommendedWidgetsToTableSpace(/* previewScale= */ 1f, recommendedWidgets); bindData(data); + return !data.mRecommendationTable.isEmpty(); } private void bindData(RecommendationTableData data) { - if (data.mRecommendationTable.size() == 0) { + if (data.mRecommendationTable.isEmpty()) { setVisibility(GONE); return; } @@ -104,12 +108,20 @@ public final class WidgetsRecommendationTableLayout extends TableLayout { for (int i = 0; i < data.mRecommendationTable.size(); i++) { List widgetItems = data.mRecommendationTable.get(i); TableRow tableRow = new TableRow(getContext()); - tableRow.setGravity(Gravity.TOP); - + if (enableCategorizedWidgetSuggestions()) { + // Vertically center align items, so that even if they don't fill bounds, they can + // look organized when placed together in a row. + tableRow.setGravity(Gravity.CENTER_VERTICAL); + } else { + tableRow.setGravity(Gravity.TOP); + } for (WidgetItem widgetItem : widgetItems) { WidgetCell widgetCell = addItemCell(tableRow); widgetCell.applyFromCellItem(widgetItem, data.mPreviewScale); widgetCell.showBadge(); + if (enableCategorizedWidgetSuggestions()) { + widgetCell.showDescription(false); + } } addView(tableRow); } diff --git a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java index 744c45b147..165b2feb62 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java +++ b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java @@ -15,6 +15,7 @@ */ package com.android.launcher3.widget.picker; +import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions; import static com.android.launcher3.Flags.enableUnfoldedTwoPanePicker; import android.content.Context; @@ -109,9 +110,15 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { mWidgetsListTableViewHolderBinder = new WidgetsListTableViewHolderBinder(mActivityContext, layoutInflater, this, this); - mRecommendedWidgetsTable = mContent.findViewById(R.id.recommended_widget_table); - mRecommendedWidgetsTable.setWidgetCellLongClickListener(this); - mRecommendedWidgetsTable.setWidgetCellOnClickListener(this); + + mWidgetRecommendationsContainer = mContent.findViewById( + R.id.widget_recommendations_container); + mWidgetRecommendationsView = mContent.findViewById( + R.id.widget_recommendations_view); + mWidgetRecommendationsView.initParentViews(mWidgetRecommendationsContainer); + mWidgetRecommendationsView.setWidgetCellLongClickListener(this); + mWidgetRecommendationsView.setWidgetCellOnClickListener(this); + mHeaderTitle = mContent.findViewById(R.id.title); mRightPane = mContent.findViewById(R.id.right_pane); mRightPane.setOutlineProvider(mViewOutlineProviderRightPane); @@ -213,7 +220,7 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { mSuggestedWidgetsHeader.setExpanded(true); resetExpandedHeaders(); mRightPane.removeAllViews(); - mRightPane.addView(mRecommendedWidgetsTable); + mRightPane.addView(mWidgetRecommendationsContainer); mRightPaneScrollView.setScrollY(0); mRightPane.setAccessibilityPaneTitle(suggestionsRightPaneTitle); mSuggestedWidgetsPackageUserKey = PackageUserKey.fromPackageItemInfo(packageItemInfo); @@ -224,7 +231,8 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { } @Override - protected float getMaxTableHeight(float noWidgetsViewHeight) { + @Px + protected float getMaxTableHeight(@Px float noWidgetsViewHeight) { return Float.MAX_VALUE; } @@ -308,15 +316,25 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { if (mSuggestedWidgetsHeader != null) { mSuggestedWidgetsHeader.setExpanded(false); } + + WidgetsListContentEntry contentEntryToBind; + if (enableCategorizedWidgetSuggestions()) { + // Setting max span size enables row to understand how to fit more than one item + // in a row. + contentEntryToBind = contentEntry.withMaxSpanSize(mMaxSpanPerRow); + } else { + contentEntryToBind = contentEntry; + } + WidgetsRowViewHolder widgetsRowViewHolder = mWidgetsListTableViewHolderBinder.newViewHolder(mRightPane); mWidgetsListTableViewHolderBinder.bindViewHolder(widgetsRowViewHolder, - contentEntry, + contentEntryToBind, ViewHolderBinder.POSITION_FIRST | ViewHolderBinder.POSITION_LAST, Collections.EMPTY_LIST); widgetsRowViewHolder.mDataCallback = data -> { mWidgetsListTableViewHolderBinder.bindViewHolder(widgetsRowViewHolder, - contentEntry, + contentEntryToBind, ViewHolderBinder.POSITION_FIRST | ViewHolderBinder.POSITION_LAST, Collections.singletonList(data)); };