From 933a05a2beb60ade74461d5b92ee3aa004e5d433 Mon Sep 17 00:00:00 2001 From: Shamali P Date: Tue, 6 Feb 2024 22:17:16 +0000 Subject: [PATCH 1/4] Align items in the center within each table row within suggestions. When differently-sized widgets OR widgets of same size but not filling space in previews are shown side by side, the top alignment looks odd. Besides, in the updated UX, we want them to be center aligned. Bug: 318410881 Bug: 319152349 Flag: N/A Test: Screenshot update in cl chain. Change-Id: I52cd7d6a1b5397a3c4c9dea8b072792b3211a8d3 --- .../launcher3/widget/WidgetsBottomSheet.java | 9 ++++++++- .../launcher3/widget/picker/WidgetsFullSheet.java | 2 +- .../picker/WidgetsListTableViewHolderBinder.java | 10 +++++++++- .../picker/WidgetsRecommendationTableLayout.java | 10 ++++++++-- .../widget/picker/WidgetsTwoPaneSheet.java | 15 +++++++++++++-- 5 files changed, 39 insertions(+), 7 deletions(-) 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/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java index 4e704fdb10..2e51fb6045 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java +++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java @@ -162,7 +162,7 @@ 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; 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..2d17033d5d 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; @@ -104,8 +105,13 @@ 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); diff --git a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java index 744c45b147..4326515071 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; @@ -308,15 +309,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)); }; From 5fbb0cccad866fe73e9deb51204293c48e7f8edb Mon Sep 17 00:00:00 2001 From: Shamali P Date: Tue, 6 Feb 2024 23:05:35 +0000 Subject: [PATCH 2/4] Use derived padding instead of static 300dp for large portrait displays In tablet, portrait mode, a padding of 300dp was applied to make it less elongated, but that doesn't scale well across different devices. So, we use 1/6th of height as the additional padding. This fixes the existing logic. Bug: 318410881 Bug: 315055849 Flag: N/A Test: Screenshot update in cl chain. Change-Id: Ia9cfe481131f086b66f625069cbf9a7c0343c2c9 --- res/values-sw720dp-land/dimens.xml | 3 --- res/values-sw720dp/dimens.xml | 3 --- res/values/dimens.xml | 1 - src/com/android/launcher3/DeviceProfile.java | 21 +++++++++++--------- 4 files changed, 12 insertions(+), 16 deletions(-) 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 c18700028f..618996140c 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -454,7 +454,6 @@ 8dp - 0dp 36dp 32dp 4dp 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) { From 62339981db75911399d4886bbfcee1228db25417 Mon Sep 17 00:00:00 2001 From: Shamali P Date: Wed, 7 Feb 2024 04:11:10 +0000 Subject: [PATCH 3/4] Use full-width two picker in both orientations in tablets The change is really for enabling display of categorical suggestions, so using the same flag. Bug: 315055849 Bug: 318410881 Test: See screenshots Flag: ACONFIG com.android.launcher3.enable_categorized_widget_recommendations DEVELOPMENT Change-Id: Ia98fdd6b827f31a63264128617f6a16ee0716bcc --- .../launcher3/widget/BaseWidgetSheet.java | 15 +++++-- .../widget/picker/WidgetsFullSheet.java | 39 +++++++++++++------ 2 files changed, 38 insertions(+), 16 deletions(-) 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/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java index 2e51fb6045..e321d830ab 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; @@ -699,7 +700,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 +816,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) { From b3a4d65bb7860c20264092fcab68b88472feb744 Mon Sep 17 00:00:00 2001 From: Shamali P Date: Wed, 7 Feb 2024 15:56:25 +0000 Subject: [PATCH 4/4] Initial UI changes for displaying categorized suggestions. Ordering changes and some more UI changes in follow-up. Bug: 318410881 Test: See screenshots Flag: ACONFIG com.android.launcher3.enable_categorized_widget_recommendations DEVELOPMENT Change-Id: I77e7f4dcdda32e2921ae56721cddbe261832f0d8 --- res/layout/widget_recommendations.xml | 58 ++++ res/layout/widget_recommendations_table.xml | 21 ++ res/layout/widgets_full_sheet_paged_view.xml | 13 +- .../widgets_full_sheet_recyclerview.xml | 11 +- res/layout/widgets_two_pane_sheet.xml | 13 +- res/values/dimens.xml | 5 +- res/values/strings.xml | 2 +- .../pageindicators/PageIndicatorDots.java | 2 +- .../launcher3/popup/PopupDataProvider.java | 4 +- .../android/launcher3/widget/WidgetCell.java | 10 + .../picker/WidgetRecommendationsView.java | 258 ++++++++++++++++++ .../widget/picker/WidgetsFullSheet.java | 93 ++++--- .../WidgetsRecommendationTableLayout.java | 12 +- .../widget/picker/WidgetsTwoPaneSheet.java | 17 +- 14 files changed, 449 insertions(+), 70 deletions(-) create mode 100644 res/layout/widget_recommendations.xml create mode 100644 res/layout/widget_recommendations_table.xml create mode 100644 src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java 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/dimens.xml b/res/values/dimens.xml index 618996140c..2f0f13001b 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -182,9 +182,8 @@ 6dp 117dp 0dp - - 8dp - + 8dp + 16dp 16dp diff --git a/res/values/strings.xml b/res/values/strings.xml index a4e7ec48aa..231aec4666 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/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/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/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 e321d830ab..237078e31c 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java +++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java @@ -46,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; @@ -66,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; @@ -77,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; @@ -168,7 +167,8 @@ public class WidgetsFullSheet extends BaseWidgetSheet protected TextView mNoWidgetsView; protected StickyHeaderLayout mSearchScrollView; - protected WidgetsRecommendationTableLayout mRecommendedWidgetsTable; + protected WidgetRecommendationsView mWidgetRecommendationsView; + protected LinearLayout mWidgetRecommendationsContainer; protected View mTabBar; protected View mSearchBarContainer; protected WidgetsSearchBar mSearchBar; @@ -222,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(); @@ -552,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); @@ -581,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. */ @@ -623,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) @@ -870,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/WidgetsRecommendationTableLayout.java b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java index 2d17033d5d..ce1f4e0b7c 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java +++ b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java @@ -62,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); @@ -85,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; } @@ -116,6 +119,9 @@ public final class WidgetsRecommendationTableLayout extends TableLayout { 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 4326515071..165b2feb62 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java +++ b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java @@ -110,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); @@ -214,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); @@ -225,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; }