Merge changes from topic "preview-sizes" into main

* changes:
  Limit the recommendations space in two pane picker
  Update the widget previews to use the container sizes
  Avoid cropping image previews.
  Add preview container size helper to display previews in picker.
This commit is contained in:
Shamali Patwa
2024-03-13 13:32:05 +00:00
committed by Android (Google) Code Review
19 changed files with 749 additions and 268 deletions
+2 -1
View File
@@ -17,7 +17,8 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/widget_cell_horizontal_padding"
android:layout_marginStart="@dimen/widget_cell_horizontal_padding"
android:layout_marginEnd="@dimen/widget_cell_horizontal_padding"
android:paddingVertical="@dimen/widget_cell_vertical_padding"
android:layout_weight="1"
android:orientation="vertical"
@@ -17,5 +17,4 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/widget_recommendations_table_horizontal_padding"
android:paddingVertical="@dimen/widget_recommendations_table_vertical_padding" />
+1
View File
@@ -37,6 +37,7 @@
<!-- Widget picker-->
<dimen name="widget_list_horizontal_margin">30dp</dimen>
<dimen name="widget_cell_horizontal_padding">16dp</dimen>
<!-- Folder spaces -->
<dimen name="folder_footer_horiz_padding">24dp</dimen>
+3 -3
View File
@@ -176,7 +176,7 @@
<!-- Widget tray -->
<dimen name="widget_cell_vertical_padding">8dp</dimen>
<dimen name="widget_cell_horizontal_padding">16dp</dimen>
<dimen name="widget_cell_horizontal_padding">8dp</dimen>
<dimen name="widget_cell_font_size">14sp</dimen>
<dimen name="widget_cell_app_icon_size">24dp</dimen>
<dimen name="widget_cell_app_icon_padding">8dp</dimen>
@@ -187,7 +187,6 @@
<dimen name="widget_picker_landscape_tablet_left_right_margin">117dp</dimen>
<dimen name="widget_picker_two_panels_left_right_margin">0dp</dimen>
<dimen name="widget_recommendations_table_vertical_padding">8dp</dimen>
<dimen name="widget_recommendations_table_horizontal_padding">16dp</dimen>
<!-- Bottom margin for the search and recommended widgets container without work profile -->
<dimen name="search_and_recommended_widgets_container_bottom_margin">16dp</dimen>
<!-- Bottom margin for the search and recommended widgets container with work profile -->
@@ -198,7 +197,8 @@
<dimen name="widget_list_header_view_vertical_padding">20dp</dimen>
<dimen name="widget_list_entry_spacing">2dp</dimen>
<dimen name="widget_list_horizontal_margin">16dp</dimen>
<!-- Less margin on sides to let widgets table width be close to the workspace width. -->
<dimen name="widget_list_horizontal_margin">11dp</dimen>
<!-- Margin applied to the recycler view with search bar & the list of widget apps below it. -->
<dimen name="widget_list_left_pane_horizontal_margin">0dp</dimen>
<dimen name="widget_list_horizontal_margin_two_pane">24dp</dimen>
@@ -57,6 +57,8 @@ import com.android.launcher3.model.data.ItemInfoWithIcon;
import com.android.launcher3.model.data.PackageItemInfo;
import com.android.launcher3.util.CancellableTask;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.widget.picker.util.WidgetPreviewContainerSize;
import com.android.launcher3.widget.util.WidgetSizes;
import java.util.function.Consumer;
@@ -80,7 +82,7 @@ public class WidgetCell extends LinearLayout {
* The requested scale of the preview container. It can be lower than this as well.
*/
private float mPreviewContainerScale = 1f;
private Size mPreviewContainerSize = new Size(0, 0);
private FrameLayout mWidgetImageContainer;
private WidgetImageView mWidgetImage;
private ImageView mWidgetBadge;
@@ -176,6 +178,8 @@ public class WidgetCell extends LinearLayout {
mWidgetDims.setText(null);
mWidgetDescription.setText(null);
mWidgetDescription.setVisibility(GONE);
showDescription(true);
showDimensions(true);
if (mActiveRequest != null) {
mActiveRequest.cancel();
@@ -186,6 +190,7 @@ public class WidgetCell extends LinearLayout {
mWidgetImageContainer.removeView(mAppWidgetHostViewPreview);
}
mAppWidgetHostViewPreview = null;
mPreviewContainerSize = new Size(0, 0);
mAppWidgetHostViewScale = 1f;
mPreviewContainerScale = 1f;
mItem = null;
@@ -201,30 +206,21 @@ public class WidgetCell extends LinearLayout {
* Applies the item to this view
*/
public void applyFromCellItem(WidgetItem item) {
applyFromCellItem(item, 1f);
}
/**
* Applies the item to this view
*/
public void applyFromCellItem(WidgetItem item, float previewScale) {
applyFromCellItem(item, previewScale, this::applyPreview, null);
applyFromCellItem(item, this::applyPreview, /*cachedPreview=*/null);
}
/**
* Applies the item to this view
* @param item item to apply
* @param previewScale factor to scale the preview
* @param callback callback when preview is loaded in case the preview is being loaded or cached
* @param cachedPreview previously cached preview bitmap is present
*/
public void applyFromCellItem(WidgetItem item, float previewScale,
@NonNull Consumer<Bitmap> callback, @Nullable Bitmap cachedPreview) {
mPreviewContainerScale = previewScale;
public void applyFromCellItem(WidgetItem item, @NonNull Consumer<Bitmap> callback,
@Nullable Bitmap cachedPreview) {
Context context = getContext();
mItem = item;
mWidgetSize = getWidgetItemSizePx(getContext(), mActivity.getDeviceProfile(), mItem);
initPreviewContainerSizeAndScale();
mWidgetName.setText(mItem.label);
mWidgetName.setContentDescription(
@@ -278,6 +274,17 @@ public class WidgetCell extends LinearLayout {
}
}
private void initPreviewContainerSizeAndScale() {
WidgetPreviewContainerSize previewSize = WidgetPreviewContainerSize.Companion.forItem(mItem,
mActivity.getDeviceProfile());
mPreviewContainerSize = WidgetSizes.getWidgetSizePx(mActivity.getDeviceProfile(),
previewSize.spanX, previewSize.spanY);
float scaleX = (float) mPreviewContainerSize.getWidth() / mWidgetSize.getWidth();
float scaleY = (float) mPreviewContainerSize.getHeight() / mWidgetSize.getHeight();
mPreviewContainerScale = Math.min(scaleX, scaleY);
}
private void setAppWidgetHostViewPreview(
NavigableAppWidgetHostView appWidgetHostViewPreview,
LauncherAppWidgetProviderInfo providerInfo,
@@ -383,6 +390,16 @@ public class WidgetCell extends LinearLayout {
mWidgetDescription.setVisibility(show ? VISIBLE : GONE);
}
/**
* Shows or hides the dimensions displayed below each widget.
*
* @param show a flag that shows the dimensions of the widget if {@code true}, hides it if
* {@code false}.
*/
public void showDimensions(boolean show) {
mWidgetDims.setVisibility(show ? VISIBLE : GONE);
}
/**
* Set whether the app icon, for the app that provides the widget, should be shown next to the
* title text of the widget.
@@ -448,17 +465,22 @@ public class WidgetCell extends LinearLayout {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
ViewGroup.LayoutParams containerLp = mWidgetImageContainer.getLayoutParams();
mAppWidgetHostViewScale = mPreviewContainerScale;
int maxWidth = MeasureSpec.getSize(widthMeasureSpec);
containerLp.width = Math.round(mWidgetSize.getWidth() * mAppWidgetHostViewScale);
// mPreviewContainerScale ensures the needed scaling with respect to original widget size.
mAppWidgetHostViewScale = mPreviewContainerScale;
containerLp.width = mPreviewContainerSize.getWidth();
containerLp.height = mPreviewContainerSize.getHeight();
// If we don't have enough available width, scale the preview container to fit.
if (containerLp.width > maxWidth) {
containerLp.width = maxWidth;
mAppWidgetHostViewScale = (float) containerLp.width / mWidgetSize.getWidth();
mAppWidgetHostViewScale = (float) containerLp.width / mPreviewContainerSize.getWidth();
containerLp.height = Math.round(
mPreviewContainerSize.getHeight() * mAppWidgetHostViewScale);
}
containerLp.height = Math.round(mWidgetSize.getHeight() * mAppWidgetHostViewScale);
// No need to call mWidgetImageContainer.setLayoutParams as we are in measure pass
// No need to call mWidgetImageContainer.setLayoutParams as we are in measure pass
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@@ -82,15 +82,27 @@ public class WidgetImageView extends View {
private void updateDstRectF() {
float myWidth = getWidth();
float myHeight = getHeight();
float bitmapWidth = mDrawable.getIntrinsicWidth();
final float bitmapWidth = mDrawable.getIntrinsicWidth();
final float bitmapHeight = mDrawable.getIntrinsicHeight();
final float bitmapAspectRatio = bitmapWidth / bitmapHeight;
final float containerAspectRatio = myWidth / myHeight;
final float scale = bitmapWidth > myWidth ? myWidth / bitmapWidth : 1;
float scaledWidth = bitmapWidth * scale;
float scaledHeight = mDrawable.getIntrinsicHeight() * scale;
// Scale by width if image has larger aspect ratio than the container else by height; and
// avoid cropping the previews
final float scale = bitmapAspectRatio > containerAspectRatio ? myWidth / bitmapWidth
: myHeight / bitmapHeight;
mDstRectF.left = (myWidth - scaledWidth) / 2;
mDstRectF.right = (myWidth + scaledWidth) / 2;
final float scaledWidth = bitmapWidth * scale;
final float scaledHeight = bitmapHeight * scale;
// Avoid cropping by checking bounds after scaling.
if (scaledWidth > myWidth) {
mDstRectF.left = 0;
mDstRectF.right = scaledWidth;
} else {
mDstRectF.left = (myWidth - scaledWidth) / 2;
mDstRectF.right = (myWidth + scaledWidth) / 2;
}
if (scaledHeight > myHeight) {
mDstRectF.top = 0;
mDstRectF.bottom = scaledHeight;
@@ -16,7 +16,6 @@
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;
@@ -188,13 +187,9 @@ public class WidgetsBottomSheet extends BaseWidgetSheet {
mWidgetCellHorizontalPadding)
.forEach(row -> {
TableRow tableRow = new TableRow(getContext());
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);
}
// 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);
row.forEach(widgetItem -> {
WidgetCell widget = addItemCell(tableRow);
widget.applyFromCellItem(widgetItem);
@@ -36,41 +36,49 @@ public final class WidgetsListHeaderEntry extends WidgetsListBaseEntry {
(context, entry) -> entry.mWidgets.stream()
.map(item -> item.label).sorted().collect(Collectors.joining(", "));
private static final BiFunction<Context, WidgetsListHeaderEntry, String> SUBTITLE_DEFAULT =
(context, entry) -> {
List<WidgetItem> items = entry.mWidgets;
int wc = (int) items.stream().filter(item -> item.widgetInfo != null).count();
int sc = Math.max(0, items.size() - wc);
@Nullable
private static String buildWidgetsCountString(Context context, int wc, int sc) {
Resources resources = context.getResources();
if (wc == 0 && sc == 0) {
return null;
}
Resources resources = context.getResources();
if (wc == 0 && sc == 0) {
return null;
}
String subtitle;
if (wc > 0 && sc > 0) {
String widgetsCount = PluralMessageFormat.getIcuPluralString(context,
R.string.widgets_count, wc);
String shortcutsCount = PluralMessageFormat.getIcuPluralString(context,
R.string.shortcuts_count, sc);
subtitle = resources.getString(R.string.widgets_and_shortcuts_count,
widgetsCount, shortcutsCount);
} else if (wc > 0) {
subtitle = PluralMessageFormat.getIcuPluralString(context,
R.string.widgets_count, wc);
} else {
subtitle = PluralMessageFormat.getIcuPluralString(context,
R.string.shortcuts_count, sc);
}
return subtitle;
};
String subtitle;
if (wc > 0 && sc > 0) {
String widgetsCount = PluralMessageFormat.getIcuPluralString(context,
R.string.widgets_count, wc);
String shortcutsCount = PluralMessageFormat.getIcuPluralString(context,
R.string.shortcuts_count, sc);
subtitle = resources.getString(R.string.widgets_and_shortcuts_count,
widgetsCount, shortcutsCount);
} else if (wc > 0) {
subtitle = PluralMessageFormat.getIcuPluralString(context,
R.string.widgets_count, wc);
} else {
subtitle = PluralMessageFormat.getIcuPluralString(context,
R.string.shortcuts_count, sc);
}
return subtitle;
}
private final boolean mIsWidgetListShown;
/** Selected widgets displayed */
private final int mVisibleWidgetsCount;
private final boolean mIsSearchEntry;
private WidgetsListHeaderEntry(PackageItemInfo pkgItem, String titleSectionName,
List<WidgetItem> items, int visibleWidgetsCount,
boolean isSearchEntry, boolean isWidgetListShown) {
super(pkgItem, titleSectionName, items);
mVisibleWidgetsCount = visibleWidgetsCount;
mIsSearchEntry = isSearchEntry;
mIsWidgetListShown = isWidgetListShown;
}
private WidgetsListHeaderEntry(PackageItemInfo pkgItem, String titleSectionName,
List<WidgetItem> items, boolean isSearchEntry, boolean isWidgetListShown) {
super(pkgItem, titleSectionName, items);
mVisibleWidgetsCount = (int) items.stream().filter(w -> w.widgetInfo != null).count();
mIsSearchEntry = isSearchEntry;
mIsWidgetListShown = isWidgetListShown;
}
@@ -91,8 +99,13 @@ public final class WidgetsListHeaderEntry extends WidgetsListBaseEntry {
@Nullable
public String getSubtitle(Context context) {
return mIsSearchEntry
? SUBTITLE_SEARCH.apply(context, this) : SUBTITLE_DEFAULT.apply(context, this);
if (mIsSearchEntry) {
return SUBTITLE_SEARCH.apply(context, this);
} else {
int shortcutsCount = Math.max(0,
(int) mWidgets.stream().filter(WidgetItem::isShortcut).count());
return buildWidgetsCountString(context, mVisibleWidgetsCount, shortcutsCount);
}
}
@Override
@@ -102,6 +115,7 @@ public final class WidgetsListHeaderEntry extends WidgetsListBaseEntry {
return mWidgets.equals(otherEntry.mWidgets) && mPkgItem.equals(otherEntry.mPkgItem)
&& mTitleSectionName.equals(otherEntry.mTitleSectionName)
&& mIsWidgetListShown == otherEntry.mIsWidgetListShown
&& mVisibleWidgetsCount == otherEntry.mVisibleWidgetsCount
&& mIsSearchEntry == otherEntry.mIsSearchEntry;
}
@@ -112,6 +126,7 @@ public final class WidgetsListHeaderEntry extends WidgetsListBaseEntry {
mPkgItem,
mTitleSectionName,
mWidgets,
mVisibleWidgetsCount,
mIsSearchEntry,
/* isWidgetListShown= */ true);
}
@@ -122,7 +137,28 @@ public final class WidgetsListHeaderEntry extends WidgetsListBaseEntry {
pkgItem,
titleSectionName,
items,
/* forSearch */ false,
/* isSearchEntry= */ false,
/* isWidgetListShown= */ false);
}
/**
* Creates a widget list holder for an header ("app" / "suggestions") which has widgets or/and
* shortcuts.
*
* @param pkgItem package item info for the header section
* @param titleSectionName title string for the header
* @param items all items for the given header
* @param visibleWidgetsCount widgets count when only selected widgets are shown due to
* limited space.
*/
public static WidgetsListHeaderEntry create(PackageItemInfo pkgItem, String titleSectionName,
List<WidgetItem> items, int visibleWidgetsCount) {
return new WidgetsListHeaderEntry(
pkgItem,
titleSectionName,
items,
visibleWidgetsCount,
/* isSearchEntry= */ false,
/* isWidgetListShown= */ false);
}
@@ -132,7 +168,7 @@ public final class WidgetsListHeaderEntry extends WidgetsListBaseEntry {
pkgItem,
titleSectionName,
items,
/* forSearch */ true,
/* isSearchEntry */ true,
/* isWidgetListShown= */ false);
}
}
@@ -93,18 +93,19 @@ public final class WidgetRecommendationsView extends PagedView<PageIndicatorDots
* @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.
* @return number of recommendations that could fit in the available space.
*/
public boolean setRecommendations(
public int setRecommendations(
List<WidgetItem> recommendedWidgets, DeviceProfile deviceProfile,
final @Px float availableHeight, final @Px int availableWidth,
final @Px int cellPadding) {
this.mAvailableHeight = availableHeight;
removeAllViews();
clear();
maybeDisplayInTable(recommendedWidgets, deviceProfile, availableWidth, cellPadding);
int displayedWidgets = maybeDisplayInTable(recommendedWidgets, deviceProfile,
availableWidth, cellPadding);
updateTitleAndIndicator();
return getChildCount() > 0;
return displayedWidgets;
}
/**
@@ -118,9 +119,9 @@ public final class WidgetRecommendationsView extends PagedView<PageIndicatorDots
* @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.
* @return number of recommendations that could fit in the available space.
*/
public boolean setRecommendations(
public int setRecommendations(
Map<WidgetRecommendationCategory, List<WidgetItem>> recommendations,
DeviceProfile deviceProfile,
final @Px float availableHeight, final @Px int availableWidth,
@@ -128,19 +129,23 @@ public final class WidgetRecommendationsView extends PagedView<PageIndicatorDots
this.mAvailableHeight = availableHeight;
Context context = getContext();
mPageIndicator.setPauseScroll(true, deviceProfile.isTwoPanels);
removeAllViews();
clear();
int displayedCategories = 0;
int totalDisplayedWidgets = 0;
// Render top MAX_CATEGORIES in separate tables. Each table becomes a page.
for (Map.Entry<WidgetRecommendationCategory, List<WidgetItem>> 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(), deviceProfile, availableWidth, cellPadding)) {
int displayedCount = maybeDisplayInTable(entry.getValue(), deviceProfile,
availableWidth, cellPadding);
if (displayedCount > 0) {
mCategoryTitles.add(
context.getResources().getString(entry.getKey().categoryTitleRes));
displayedCategories++;
totalDisplayedWidgets += displayedCount;
}
if (displayedCategories == MAX_CATEGORIES) {
@@ -150,7 +155,12 @@ public final class WidgetRecommendationsView extends PagedView<PageIndicatorDots
updateTitleAndIndicator();
mPageIndicator.setPauseScroll(false, deviceProfile.isTwoPanels);
return getChildCount() > 0;
return totalDisplayedWidgets;
}
private void clear() {
mCategoryTitles.clear();
removeAllViews();
}
/** Displays the page title and paging indicator if there are multiple pages. */
@@ -199,21 +209,8 @@ public final class WidgetRecommendationsView extends PagedView<PageIndicatorDots
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());
}
// Use height of tallest child as we have limited height.
desiredHeight = Math.max(desiredHeight, child.getMeasuredHeight());
}
int finalHeight = resolveSizeAndState(desiredHeight, heightMeasureSpec, 0);
@@ -228,12 +225,14 @@ public final class WidgetRecommendationsView extends PagedView<PageIndicatorDots
* fits.
* <p>Returns false if none of the recommendations could fit.</p>
*/
private boolean maybeDisplayInTable(List<WidgetItem> recommendedWidgets,
private int maybeDisplayInTable(List<WidgetItem> recommendedWidgets,
DeviceProfile deviceProfile,
final @Px int availableWidth, final @Px int cellPadding) {
Context context = getContext();
LayoutInflater inflater = LayoutInflater.from(context);
// Since we are limited by space, we don't sort recommendations - to show most relevant
// (if possible).
List<ArrayList<WidgetItem>> rows = groupWidgetItemsUsingRowPxWithoutReordering(
recommendedWidgets,
context,
@@ -249,13 +248,13 @@ public final class WidgetRecommendationsView extends PagedView<PageIndicatorDots
recommendationsTable.setWidgetCellOnClickListener(mWidgetCellOnClickListener);
recommendationsTable.setWidgetCellLongClickListener(mWidgetCellOnLongClickListener);
boolean displayedAtLeastOne = recommendationsTable.setRecommendedWidgets(rows,
int displayedCount = recommendationsTable.setRecommendedWidgets(rows,
deviceProfile, mAvailableHeight);
if (displayedAtLeastOne) {
if (displayedCount > 0) {
addView(recommendationsTable);
}
return displayedAtLeastOne;
return displayedCount;
}
/** Returns location of a widget cell for displaying the "touch and hold" education tip. */
@@ -107,7 +107,8 @@ public class WidgetsFullSheet extends BaseWidgetSheet
entry -> mCurrentUser.equals(entry.mPkgItem.user);
private final Predicate<WidgetsListBaseEntry> mWorkWidgetsFilter;
protected final boolean mHasWorkProfile;
protected boolean mHasRecommendedWidgets;
// Number of recommendations displayed
protected int mRecommendedWidgetsCount;
protected final SparseArray<AdapterHolder> mAdapters = new SparseArray();
@Nullable private ArrowTipView mLatestEducationalTip;
private final OnLayoutChangeListener mLayoutChangeListenerToShowTips =
@@ -581,7 +582,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet
}
if (enableCategorizedWidgetSuggestions()) {
mHasRecommendedWidgets = mWidgetRecommendationsView.setRecommendations(
mRecommendedWidgetsCount = mWidgetRecommendationsView.setRecommendations(
mActivityContext.getPopupDataProvider().getCategorizedRecommendedWidgets(),
mDeviceProfile,
/* availableHeight= */ getMaxAvailableHeightForRecommendations(),
@@ -589,7 +590,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet
/* cellPadding= */ mWidgetCellHorizontalPadding
);
} else {
mHasRecommendedWidgets = mWidgetRecommendationsView.setRecommendations(
mRecommendedWidgetsCount = mWidgetRecommendationsView.setRecommendations(
mActivityContext.getPopupDataProvider().getRecommendedWidgets(),
mDeviceProfile,
/* availableHeight= */ getMaxAvailableHeightForRecommendations(),
@@ -597,7 +598,8 @@ public class WidgetsFullSheet extends BaseWidgetSheet
/* cellPadding= */ mWidgetCellHorizontalPadding
);
}
mWidgetRecommendationsContainer.setVisibility(mHasRecommendedWidgets ? VISIBLE : GONE);
mWidgetRecommendationsContainer.setVisibility(
mRecommendedWidgetsCount > 0 ? VISIBLE : GONE);
}
@Px
@@ -790,7 +792,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet
}
/** private the height, in pixel, + the vertical margins of a given view. */
private static int measureHeightWithVerticalMargins(View view) {
protected static int measureHeightWithVerticalMargins(View view) {
if (view.getVisibility() != VISIBLE) {
return 0;
}
@@ -15,8 +15,6 @@
*/
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;
@@ -121,7 +119,7 @@ public final class WidgetsListTableViewHolderBinder
widget.setVisibility(View.VISIBLE);
// When preview loads, notify adapter to rebind the item and possibly animate
widget.applyFromCellItem(widgetItem, 1f,
widget.applyFromCellItem(widgetItem,
bitmap -> holder.onPreviewLoaded(Pair.create(widgetItem, bitmap)),
holder.previewCache.get(widgetItem));
widget.requestLayout();
@@ -150,13 +148,9 @@ public final class WidgetsListTableViewHolderBinder
tableRow = (TableRow) table.getChildAt(i);
} else {
tableRow = new TableRow(table.getContext());
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);
}
// 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);
table.addView(tableRow);
}
if (tableRow.getChildCount() > widgetItems.size()) {
@@ -17,11 +17,13 @@ package com.android.launcher3.widget.picker;
import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions;
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION;
import static com.android.launcher3.widget.util.WidgetSizes.getWidgetSizePx;
import static com.android.launcher3.widget.util.WidgetsTableUtils.WIDGETS_TABLE_ROW_SIZE_COMPARATOR;
import static java.lang.Math.max;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Size;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
@@ -30,26 +32,23 @@ import android.widget.TableLayout;
import android.widget.TableRow;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.widget.WidgetCell;
import com.android.launcher3.widget.util.WidgetSizes;
import com.android.launcher3.widget.picker.util.WidgetPreviewContainerSize;
import java.util.ArrayList;
import java.util.List;
/** A {@link TableLayout} for showing recommended widgets. */
public final class WidgetsRecommendationTableLayout extends TableLayout {
private static final String TAG = "WidgetsRecommendationTableLayout";
private static final float DOWN_SCALE_RATIO = 0.9f;
private static final float MAX_DOWN_SCALE_RATIO = 0.5f;
private final float mWidgetsRecommendationTableVerticalPadding;
private final float mWidgetCellVerticalPadding;
private final float mWidgetCellTextViewsHeight;
private float mRecommendationTableMaxHeight = Float.MAX_VALUE;
@Nullable private OnLongClickListener mWidgetCellOnLongClickListener;
@Nullable private OnClickListener mWidgetCellOnClickListener;
@@ -82,47 +81,40 @@ public final class WidgetsRecommendationTableLayout extends TableLayout {
* desired {@code recommendationTableMaxHeight}.
*
* <p>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.
* last row from the {@code recommendedWidgets} until it fits or only one row left.
*
* <p>Returns {@code false} if none of the widgets could fit</p>
*/
public boolean setRecommendedWidgets(List<ArrayList<WidgetItem>> recommendedWidgets,
DeviceProfile deviceProfile,
float recommendationTableMaxHeight) {
mRecommendationTableMaxHeight = recommendationTableMaxHeight;
RecommendationTableData data = fitRecommendedWidgetsToTableSpace(/* previewScale= */ 1f,
deviceProfile,
recommendedWidgets);
bindData(data);
return !data.mRecommendationTable.isEmpty();
public int setRecommendedWidgets(List<ArrayList<WidgetItem>> recommendedWidgets,
DeviceProfile deviceProfile, float recommendationTableMaxHeight) {
List<ArrayList<WidgetItem>> rows = selectRowsThatFitInAvailableHeight(recommendedWidgets,
recommendationTableMaxHeight, deviceProfile);
bindData(rows);
return rows.stream().mapToInt(ArrayList::size).sum();
}
private void bindData(RecommendationTableData data) {
if (data.mRecommendationTable.isEmpty()) {
private void bindData(List<ArrayList<WidgetItem>> recommendationTable) {
if (recommendationTable.isEmpty()) {
setVisibility(GONE);
return;
}
removeAllViews();
for (int i = 0; i < data.mRecommendationTable.size(); i++) {
List<WidgetItem> widgetItems = data.mRecommendationTable.get(i);
for (int i = 0; i < recommendationTable.size(); i++) {
List<WidgetItem> widgetItems = recommendationTable.get(i);
TableRow tableRow = new TableRow(getContext());
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);
}
// 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);
for (WidgetItem widgetItem : widgetItems) {
WidgetCell widgetCell = addItemCell(tableRow);
widgetCell.applyFromCellItem(widgetItem, data.mPreviewScale);
widgetCell.applyFromCellItem(widgetItem);
widgetCell.showAppIconInWidgetTitle(true);
widgetCell.showBadge();
if (enableCategorizedWidgetSuggestions()) {
widgetCell.showDescription(false);
widgetCell.showDimensions(false);
}
}
addView(tableRow);
@@ -144,58 +136,32 @@ public final class WidgetsRecommendationTableLayout extends TableLayout {
return widget;
}
private RecommendationTableData fitRecommendedWidgetsToTableSpace(
float previewScale,
DeviceProfile deviceProfile,
List<ArrayList<WidgetItem>> recommendedWidgetsInTable) {
if (previewScale < MAX_DOWN_SCALE_RATIO) {
Log.w(TAG, "Hide recommended widgets. Can't down scale previews to " + previewScale);
return new RecommendationTableData(List.of(), previewScale);
}
private List<ArrayList<WidgetItem>> selectRowsThatFitInAvailableHeight(
List<ArrayList<WidgetItem>> recommendedWidgets, @Px float recommendationTableMaxHeight,
DeviceProfile deviceProfile) {
List<ArrayList<WidgetItem>> filteredRows = new ArrayList<>();
// A naive estimation of the widgets recommendation table height without inflation.
float totalHeight = mWidgetsRecommendationTableVerticalPadding;
for (int i = 0; i < recommendedWidgetsInTable.size(); i++) {
List<WidgetItem> widgetItems = recommendedWidgetsInTable.get(i);
for (int i = 0; i < recommendedWidgets.size(); i++) {
List<WidgetItem> widgetItems = recommendedWidgets.get(i);
float rowHeight = 0;
for (int j = 0; j < widgetItems.size(); j++) {
WidgetItem widgetItem = widgetItems.get(j);
Size widgetSize = WidgetSizes.getWidgetItemSizePx(getContext(), deviceProfile,
widgetItem);
float previewHeight = widgetSize.getHeight() * previewScale;
rowHeight = Math.max(rowHeight,
previewHeight + mWidgetCellTextViewsHeight + mWidgetCellVerticalPadding);
WidgetPreviewContainerSize previewContainerSize =
WidgetPreviewContainerSize.Companion.forItem(widgetItem, deviceProfile);
float widgetItemHeight = getWidgetSizePx(deviceProfile, previewContainerSize.spanX,
previewContainerSize.spanY).getHeight();
rowHeight = max(rowHeight,
widgetItemHeight + mWidgetCellTextViewsHeight + mWidgetCellVerticalPadding);
}
if (totalHeight + rowHeight <= recommendationTableMaxHeight) {
totalHeight += rowHeight;
filteredRows.add(new ArrayList<>(widgetItems));
}
totalHeight += rowHeight;
}
if (totalHeight < mRecommendationTableMaxHeight) {
return new RecommendationTableData(recommendedWidgetsInTable, previewScale);
}
if (recommendedWidgetsInTable.size() > 1) {
// We don't want to scale down widgets preview unless we really need to. Reduce the
// num of row by 1 to see if it fits.
return fitRecommendedWidgetsToTableSpace(
previewScale,
deviceProfile,
recommendedWidgetsInTable.subList(/* fromIndex= */0,
/* toIndex= */recommendedWidgetsInTable.size() - 1));
}
float nextPreviewScale = previewScale * DOWN_SCALE_RATIO;
return fitRecommendedWidgetsToTableSpace(nextPreviewScale, deviceProfile,
recommendedWidgetsInTable);
}
/** Data class for the widgets recommendation table and widgets preview scaling. */
private class RecommendationTableData {
private final List<ArrayList<WidgetItem>> mRecommendationTable;
private final float mPreviewScale;
RecommendationTableData(List<ArrayList<WidgetItem>> recommendationTable,
float previewScale) {
mRecommendationTable = recommendationTable;
mPreviewScale = previewScale;
}
// Perform re-ordering once we have filtered out recommendations that fit.
return filteredRows.stream().sorted(WIDGETS_TABLE_ROW_SIZE_COMPARATOR).toList();
}
}
@@ -167,7 +167,7 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet {
@Override
public void onWidgetsBound() {
super.onWidgetsBound();
if (!mHasRecommendedWidgets && mSelectedHeader == null) {
if (mRecommendedWidgetsCount == 0 && mSelectedHeader == null) {
mAdapters.get(mActivePage).mWidgetsListAdapter.selectFirstHeaderEntry();
mAdapters.get(mActivePage).mWidgetsRecyclerView.scrollToTop();
}
@@ -177,7 +177,7 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet {
public void onRecommendedWidgetsBound() {
super.onRecommendedWidgetsBound();
if (mSuggestedWidgetsContainer == null && mHasRecommendedWidgets) {
if (mSuggestedWidgetsContainer == null && mRecommendedWidgetsCount > 0) {
setupSuggestedWidgets(LayoutInflater.from(getContext()));
mSuggestedWidgetsHeader.callOnClick();
}
@@ -209,8 +209,9 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet {
packageItemInfo.title = suggestionsHeaderTitle;
WidgetsListHeaderEntry widgetsListHeaderEntry = WidgetsListHeaderEntry.create(
packageItemInfo,
suggestionsHeaderTitle,
mActivityContext.getPopupDataProvider().getRecommendedWidgets())
/*titleSectionName=*/ suggestionsHeaderTitle,
/*items=*/ mActivityContext.getPopupDataProvider().getRecommendedWidgets(),
/*visibleWidgetsCount=*/ mRecommendedWidgetsCount)
.withWidgetListShown();
mSuggestedWidgetsHeader.applyFromItemInfoWithIcon(widgetsListHeaderEntry);
@@ -233,7 +234,7 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet {
@Override
@Px
protected float getMaxTableHeight(@Px float noWidgetsViewHeight) {
return Float.MAX_VALUE;
return mContent.getMeasuredHeight() - measureHeightWithVerticalMargins(mHeaderTitle);
}
@Override
@@ -0,0 +1,91 @@
/*
* 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.util
import com.android.launcher3.DeviceProfile
import com.android.launcher3.model.WidgetItem
import kotlin.math.abs
/** Size of a preview container in terms of the grid spans. */
data class WidgetPreviewContainerSize(@JvmField val spanX: Int, @JvmField val spanY: Int) {
companion object {
/**
* Returns the size of the preview container in which the given widget's preview should be
* displayed (by scaling it if necessary).
*/
fun forItem(item: WidgetItem, dp: DeviceProfile): WidgetPreviewContainerSize {
val sizes =
if (dp.isTablet && !dp.isTwoPanels) {
TABLET_WIDGET_PREVIEW_SIZES
} else {
HANDHELD_WIDGET_PREVIEW_SIZES
}
for ((index, containerSize) in sizes.withIndex()) {
if (containerSize.spanX == item.spanX && containerSize.spanY == item.spanY) {
return containerSize // Exact match!
}
if (containerSize.spanX <= item.spanX && containerSize.spanY <= item.spanY) {
return findClosestFittingContainer(
containerSizes = sizes.toList(),
startIndex = index,
item = item
)
}
}
// Use largest container if no match found
return sizes.elementAt(0)
}
private fun findClosestFittingContainer(
containerSizes: List<WidgetPreviewContainerSize>,
startIndex: Int,
item: WidgetItem
): WidgetPreviewContainerSize {
// Checks if it's a smaller container, but close enough to keep the down-scale minimal.
fun hasAcceptableSize(currentIndex: Int): Boolean {
val container = containerSizes[currentIndex]
val isSmallerThanItem =
container.spanX <= item.spanX && container.spanY <= item.spanY
val isCloseToItemSize =
(item.spanY - container.spanY <= 1) && (item.spanX - container.spanX <= 1)
return isSmallerThanItem && isCloseToItemSize
}
var currentIndex = startIndex
var match = containerSizes[currentIndex]
val itemCellSizeRatio = item.spanX.toFloat() / item.spanY
var lastCellSizeRatioDiff = Float.MAX_VALUE
// Look for a smaller container (up to an acceptable extent) with closest cell size
// ratio.
while (currentIndex <= containerSizes.lastIndex && hasAcceptableSize(currentIndex)) {
val current = containerSizes[currentIndex]
val currentCellSizeRatio = current.spanX.toFloat() / current.spanY
val currentCellSizeRatioDiff = abs(itemCellSizeRatio - currentCellSizeRatio)
if (currentCellSizeRatioDiff < lastCellSizeRatioDiff) {
lastCellSizeRatioDiff = currentCellSizeRatioDiff
match = containerSizes[currentIndex]
}
currentIndex++
}
return match
}
}
}
@@ -0,0 +1,52 @@
/*
* 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.util
/**
* An ordered list of recommended sizes for the preview containers in handheld devices.
*
* Size of the preview container in which a widget's preview can be displayed.
*/
val HANDHELD_WIDGET_PREVIEW_SIZES: List<WidgetPreviewContainerSize> =
listOf(
WidgetPreviewContainerSize(spanX = 4, spanY = 3),
WidgetPreviewContainerSize(spanX = 4, spanY = 2),
WidgetPreviewContainerSize(spanX = 2, spanY = 3),
WidgetPreviewContainerSize(spanX = 2, spanY = 2),
WidgetPreviewContainerSize(spanX = 4, spanY = 1),
WidgetPreviewContainerSize(spanX = 2, spanY = 1),
WidgetPreviewContainerSize(spanX = 1, spanY = 1),
)
/**
* An ordered list of recommended sizes for the preview containers in tablet devices (with larger
* grids).
*
* Size of the preview container in which a widget's preview can be displayed (by scaling the
* preview if necessary).
*/
val TABLET_WIDGET_PREVIEW_SIZES: List<WidgetPreviewContainerSize> =
listOf(
WidgetPreviewContainerSize(spanX = 3, spanY = 4),
WidgetPreviewContainerSize(spanX = 3, spanY = 3),
WidgetPreviewContainerSize(spanX = 3, spanY = 2),
WidgetPreviewContainerSize(spanX = 2, spanY = 3),
WidgetPreviewContainerSize(spanX = 2, spanY = 2),
WidgetPreviewContainerSize(spanX = 3, spanY = 1),
WidgetPreviewContainerSize(spanX = 2, spanY = 1),
WidgetPreviewContainerSize(spanX = 1, spanY = 1),
)
@@ -16,11 +16,13 @@
package com.android.launcher3.widget.util;
import android.content.Context;
import android.util.Size;
import androidx.annotation.Px;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.widget.picker.util.WidgetPreviewContainerSize;
import java.util.ArrayList;
import java.util.Comparator;
@@ -33,8 +35,8 @@ public final class WidgetsTableUtils {
/**
* Groups widgets in the following order:
* 1. Widgets always go before shortcuts.
* 2. Widgets with smaller horizontal spans will be shown first.
* 3. If widgets have the same horizontal spans, then widgets with a smaller vertical spans will
* 2. Widgets with smaller vertical spans will be shown first.
* 3. If widgets have the same vertical spans, then widgets with a smaller horizontal spans will
* go first.
* 4. If both widgets have the same horizontal and vertical spans, they will use the same order
* from the given {@code widgetItems}.
@@ -43,13 +45,28 @@ public final class WidgetsTableUtils {
if (item.widgetInfo != null && otherItem.widgetInfo == null) return -1;
if (item.widgetInfo == null && otherItem.widgetInfo != null) return 1;
if (item.spanX == otherItem.spanX) {
if (item.spanY == otherItem.spanY) return 0;
return item.spanY > otherItem.spanY ? 1 : -1;
if (item.spanY == otherItem.spanY) {
if (item.spanX == otherItem.spanX) return 0;
return item.spanX > otherItem.spanX ? 1 : -1;
}
return item.spanX > otherItem.spanX ? 1 : -1;
return item.spanY > otherItem.spanY ? 1 : -1;
};
/**
* Comparator that enables displaying rows in increasing order of their size (totalW * H);
* except for shortcuts which always show at the bottom.
*/
public static final Comparator<ArrayList<WidgetItem>> WIDGETS_TABLE_ROW_SIZE_COMPARATOR =
Comparator.comparingInt(row -> {
if (row.stream().anyMatch(WidgetItem::isShortcut)) {
return Integer.MAX_VALUE;
} else {
int rowWidth = row.stream().mapToInt(w -> w.spanX).sum();
int rowHeight = row.get(0).spanY;
return (rowWidth * rowHeight);
}
});
/**
* Groups {@code widgetItems} items into a 2D array which matches their appearance in a UI
* table. This takes liberty to rearrange widgets to make the table visually appealing.
@@ -59,72 +76,70 @@ public final class WidgetsTableUtils {
final @Px int rowPx, final @Px int cellPadding) {
List<WidgetItem> sortedWidgetItems = widgetItems.stream().sorted(WIDGET_SHORTCUT_COMPARATOR)
.collect(Collectors.toList());
return groupWidgetItemsUsingRowPxWithoutReordering(sortedWidgetItems, context, dp, rowPx,
List<ArrayList<WidgetItem>> rows = groupWidgetItemsUsingRowPxWithoutReordering(
sortedWidgetItems, context, dp, rowPx,
cellPadding);
return rows.stream().sorted(WIDGETS_TABLE_ROW_SIZE_COMPARATOR).toList();
}
/**
* Groups {@code widgetItems} into a 2D array which matches their appearance in a UI table while
* maintaining their order. This function is a variant of
* {@code groupWidgetItemsIntoTableWithoutReordering} in that this uses widget pixels for
* calculation.
* {@code groupWidgetItemsIntoTableWithoutReordering} in that this uses widget container's
* pixels for calculation.
*
* <p>Grouping:
* 1. Widgets and shortcuts never group together in the same row.
* 2. The ordered widgets are grouped together in the same row until their individual occupying
* pixels exceed the total allowed pixels for the cell.
* 2. Widgets are grouped together only if they have same preview container size.
* 3. Widgets are grouped together in the same row until the total of individual container sizes
* exceed the total allowed pixels for the row.
* 3. The ordered shortcuts are grouped together in the same row until their individual
* occupying pixels exceed the total allowed pixels for the cell.
* 4. If there is only one widget in a row, its width may exceed the {@code rowPx}.
*
* <p>Let's say the {@code rowPx} is set to 600 and we have 5 widgets. Widgets can be grouped
* in the same row if each of their individual occupying pixels does not exceed
* {@code rowPx} / 5 - 2 * {@code cellPadding}.
* Example 1: Row 1: 200x200, 200x300, 100x100. Average horizontal pixels is 200 and no widgets
* exceed that width. This is okay.
* Example 2: Row 1: 200x200, 400x300, 100x100. Average horizontal pixels is 200 and one widget
* exceed that width. This is not allowed.
* Example 3: Row 1: 700x400. This is okay because this is the only item in the row.
* <p>See WidgetTableUtilsTest
*/
public static List<ArrayList<WidgetItem>> groupWidgetItemsUsingRowPxWithoutReordering(
List<WidgetItem> widgetItems, Context context, final DeviceProfile dp,
final @Px int rowPx, final @Px int cellPadding) {
List<ArrayList<WidgetItem>> widgetItemsTable = new ArrayList<>();
ArrayList<WidgetItem> widgetItemsAtRow = null;
// A row displays only items of same container size.
WidgetPreviewContainerSize containerSizeForRow = null;
@Px int currentRowWidth = 0;
for (WidgetItem widgetItem : widgetItems) {
if (widgetItemsAtRow == null) {
widgetItemsAtRow = new ArrayList<>();
widgetItemsTable.add(widgetItemsAtRow);
}
int numOfWidgetItems = widgetItemsAtRow.size();
@Px int individualSpan = (rowPx / (numOfWidgetItems + 1)) - (2 * cellPadding);
WidgetPreviewContainerSize containerSize =
WidgetPreviewContainerSize.Companion.forItem(widgetItem, dp);
Size containerSizePx = WidgetSizes.getWidgetSizePx(dp, containerSize.spanX,
containerSize.spanY);
@Px int containerWidth = containerSizePx.getWidth() + (2 * cellPadding);
if (numOfWidgetItems == 0) {
widgetItemsAtRow.add(widgetItem);
} else if (
// Since the size of the widget cell is determined by dividing the maximum span
// pixels evenly, making sure that each widget would have enough span pixels to
// show their contents.
widgetItem.hasSameType(widgetItemsAtRow.get(numOfWidgetItems - 1))
&& widgetItemsAtRow.stream().allMatch(
item -> WidgetSizes.getWidgetItemSizePx(context, dp, item)
.getWidth() <= individualSpan)
&& WidgetSizes.getWidgetItemSizePx(context, dp, widgetItem)
.getWidth() <= individualSpan) {
containerSizeForRow = containerSize;
currentRowWidth = containerWidth;
} else if ((currentRowWidth + containerWidth) <= rowPx
&& widgetItem.hasSameType(widgetItemsAtRow.get(numOfWidgetItems - 1))
&& containerSize.equals(containerSizeForRow)) {
// Group items in the same row if
// 1. they are with the same type, i.e. a row can only have widgets or shortcuts but
// never a mix of both.
// 2. Each widget will have horizontal cell span pixels that is at least as large as
// it is required to fit in the horizontal content, unless the widget horizontal
// span pixels is larger than the maximum allowed.
// If an item has horizontal span pixels larger than the maximum allowed pixels
// per row, we just place it in its own row regardless of the horizontal span
// limit.
// 2. Each widget in the given row has same preview container size.
widgetItemsAtRow.add(widgetItem);
currentRowWidth += containerWidth;
} else {
widgetItemsAtRow = new ArrayList<>();
widgetItemsTable.add(widgetItemsAtRow);
widgetItemsAtRow.add(widgetItem);
containerSizeForRow = containerSize;
currentRowWidth = containerWidth;
}
}
return widgetItemsTable;
@@ -0,0 +1,99 @@
/*
* 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 android.content.Context
import android.graphics.Rect
import android.graphics.drawable.Drawable
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.android.launcher3.util.ActivityContextWrapper
import com.android.launcher3.widget.WidgetImageView
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.spy
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.whenever
@MediumTest
@RunWith(AndroidJUnit4::class)
class WidgetImageViewTest {
private lateinit var context: Context
private lateinit var widgetImageView: WidgetImageView
@Mock private lateinit var testDrawable: Drawable
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
context = ActivityContextWrapper(ApplicationProvider.getApplicationContext())
widgetImageView = spy(WidgetImageView(context))
}
@Test
fun getBitmapBounds_aspectRatioLargerThanView_scaledByWidth() {
// view - 100 x 100
whenever(widgetImageView.width).thenReturn(100)
whenever(widgetImageView.height).thenReturn(100)
// bitmap - 200 x 100
whenever(testDrawable.intrinsicWidth).thenReturn(200)
whenever(testDrawable.intrinsicHeight).thenReturn(100)
widgetImageView.drawable = testDrawable
val bitmapBounds = widgetImageView.bitmapBounds
// new scaled width of bitmap is = 100, and height is scaled to 1/2 = 50
assertThat(bitmapBounds).isEqualTo(Rect(0, 25, 100, 75))
}
@Test
fun getBitmapBounds_aspectRatioSmallerThanView_scaledByHeight() {
// view - 100 x 100
whenever(widgetImageView.width).thenReturn(100)
whenever(widgetImageView.height).thenReturn(100)
// bitmap - 100 x 200
whenever(testDrawable.intrinsicWidth).thenReturn(100)
whenever(testDrawable.intrinsicHeight).thenReturn(200)
widgetImageView.drawable = testDrawable
val bitmapBounds = widgetImageView.bitmapBounds
// new scaled height of bitmap is = 100, and width is scaled to 1/2 = 50
assertThat(bitmapBounds).isEqualTo(Rect(25, 0, 75, 100))
}
@Test
fun getBitmapBounds_noScale_returnsOriginalDrawableBounds() {
// view - 200 x 100
whenever(widgetImageView.width).thenReturn(200)
whenever(widgetImageView.height).thenReturn(100)
// bitmap - 200 x 100
whenever(testDrawable.intrinsicWidth).thenReturn(200)
whenever(testDrawable.intrinsicHeight).thenReturn(100)
widgetImageView.drawable = testDrawable
val bitmapBounds = widgetImageView.bitmapBounds
// no scaling
assertThat(bitmapBounds).isEqualTo(Rect(0, 0, 200, 100))
}
}
@@ -0,0 +1,154 @@
/*
* 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.util
import android.content.ComponentName
import android.content.Context
import android.graphics.Point
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.launcher3.DeviceProfile
import com.android.launcher3.InvariantDeviceProfile
import com.android.launcher3.LauncherAppState
import com.android.launcher3.icons.IconCache
import com.android.launcher3.model.WidgetItem
import com.android.launcher3.util.ActivityContextWrapper
import com.android.launcher3.util.WidgetUtils.createAppWidgetProviderInfo
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo
import com.google.common.truth.Truth.assertWithMessage
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidJUnit4::class)
class WidgetPreviewContainerSizesTest {
private lateinit var context: Context
private lateinit var deviceProfile: DeviceProfile
private lateinit var testInvariantProfile: InvariantDeviceProfile
@Mock private lateinit var iconCache: IconCache
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
context = ActivityContextWrapper(ApplicationProvider.getApplicationContext())
testInvariantProfile = LauncherAppState.getIDP(context)
deviceProfile = testInvariantProfile.getDeviceProfile(context).copy(context)
}
@Test
fun widgetPreviewContainerSize_forItem_returnsCorrectContainerSize() {
val testSizes = getTestSizes(deviceProfile)
val expectedPreviewContainers = testSizes.values.toList()
for ((index, widgetSize) in testSizes.keys.withIndex()) {
val widgetItem = createWidgetItem(widgetSize, context, testInvariantProfile, iconCache)
assertWithMessage("size for $widgetSize should be: ${expectedPreviewContainers[index]}")
.that(WidgetPreviewContainerSize.forItem(widgetItem, deviceProfile))
.isEqualTo(expectedPreviewContainers[index])
}
}
companion object {
private const val TEST_PACKAGE = "com.google.test"
private val HANDHELD_TEST_SIZES: Map<Point, WidgetPreviewContainerSize> =
mapOf(
// 1x1
Point(1, 1) to WidgetPreviewContainerSize(1, 1),
// 2x1
Point(2, 1) to WidgetPreviewContainerSize(2, 1),
Point(3, 1) to WidgetPreviewContainerSize(2, 1),
// 4x1
Point(4, 1) to WidgetPreviewContainerSize(4, 1),
// 2x2
Point(2, 2) to WidgetPreviewContainerSize(2, 2),
Point(3, 3) to WidgetPreviewContainerSize(2, 2),
Point(3, 2) to WidgetPreviewContainerSize(2, 2),
// 2x3
Point(2, 3) to WidgetPreviewContainerSize(2, 3),
Point(3, 4) to WidgetPreviewContainerSize(2, 3),
Point(3, 5) to WidgetPreviewContainerSize(2, 3),
// 4x2
Point(4, 2) to WidgetPreviewContainerSize(4, 2),
// 4x3
Point(4, 3) to WidgetPreviewContainerSize(4, 3),
Point(4, 4) to WidgetPreviewContainerSize(4, 3),
)
private val TABLET_TEST_SIZES: Map<Point, WidgetPreviewContainerSize> =
mapOf(
// 1x1
Point(1, 1) to WidgetPreviewContainerSize(1, 1),
// 2x1
Point(2, 1) to WidgetPreviewContainerSize(2, 1),
// 3x1
Point(3, 1) to WidgetPreviewContainerSize(3, 1),
Point(4, 1) to WidgetPreviewContainerSize(3, 1),
// 2x2
Point(2, 2) to WidgetPreviewContainerSize(2, 2),
// 2x3
Point(2, 3) to WidgetPreviewContainerSize(2, 3),
// 3x2
Point(3, 2) to WidgetPreviewContainerSize(3, 2),
Point(4, 2) to WidgetPreviewContainerSize(3, 2),
Point(5, 2) to WidgetPreviewContainerSize(3, 2),
// 3x3
Point(3, 3) to WidgetPreviewContainerSize(3, 3),
Point(4, 4) to WidgetPreviewContainerSize(3, 3),
// 3x4
Point(5, 4) to WidgetPreviewContainerSize(3, 4),
Point(3, 4) to WidgetPreviewContainerSize(3, 4),
Point(5, 5) to WidgetPreviewContainerSize(3, 4),
Point(6, 4) to WidgetPreviewContainerSize(3, 4),
Point(6, 5) to WidgetPreviewContainerSize(3, 4),
)
private fun getTestSizes(dp: DeviceProfile) =
if (dp.isTablet && !dp.isTwoPanels) {
TABLET_TEST_SIZES
} else {
HANDHELD_TEST_SIZES
}
private fun createWidgetItem(
widgetSize: Point,
context: Context,
invariantDeviceProfile: InvariantDeviceProfile,
iconCache: IconCache
): WidgetItem {
val providerInfo =
createAppWidgetProviderInfo(
ComponentName.createRelative(
TEST_PACKAGE,
/*cls=*/ ".WidgetProvider_" + widgetSize.x + "x" + widgetSize.y
)
)
val widgetInfo =
LauncherAppWidgetProviderInfo.fromProviderInfo(context, providerInfo).apply {
spanX = widgetSize.x
spanY = widgetSize.y
}
return WidgetItem(widgetInfo, invariantDeviceProfile, iconCache, context)
}
}
}
@@ -63,6 +63,7 @@ public final class WidgetsTableUtilsTest {
private static final String TEST_PACKAGE = "com.google.test";
private static final int SPACE_SIZE = 10;
// Note - actual widget size includes SPACE_SIZE (border) + cell padding.
private static final int CELL_SIZE = 50;
private static final int NUM_OF_COLS = 5;
private static final int NUM_OF_ROWS = 5;
@@ -105,7 +106,7 @@ public final class WidgetsTableUtilsTest {
@Test
public void groupWidgetItemsIntoTableWithReordering_widgetsOnly_maxSpanPxPerRow220_cellPadding0_shouldGroupWidgetsInTable() {
public void groupWithReordering_widgetsOnly_maxSpanPxPerRow220_cellPadding0() {
List<WidgetItem> widgetItems = List.of(mWidget4x4, mWidget2x3, mWidget1x1, mWidget2x4,
mWidget2x2);
@@ -113,17 +114,20 @@ public final class WidgetsTableUtilsTest {
WidgetsTableUtils.groupWidgetItemsUsingRowPxWithReordering(widgetItems, mContext,
mTestDeviceProfile, 220, 0);
// Row 0: 1x1(50px), 2x2(110px)
// Row 1: 2x3(110px), 2x4(110px)
// Row 2: 4x4(230px)
assertThat(widgetItemInTable).hasSize(3);
assertThat(widgetItemInTable.get(0)).containsExactly(mWidget1x1, mWidget2x2);
assertThat(widgetItemInTable.get(1)).containsExactly(mWidget2x3, mWidget2x4);
assertThat(widgetItemInTable.get(2)).containsExactly(mWidget4x4);
// With reordering, rows displayed in order of increasing size.
// Row 0: 1x1(50px)
// Row 1: 2x2(in a 2x2 container - 110px)
// Row 2: 2x3(in a 2x3 container - 110px), 2x4(in a 2x3 container - 110px)
// Row 3: 4x4(in a 3x3 container in tablet - 170px; 4x3 on phone - 230px)
assertThat(widgetItemInTable).hasSize(4);
assertThat(widgetItemInTable.get(0)).containsExactly(mWidget1x1);
assertThat(widgetItemInTable.get(1)).containsExactly(mWidget2x2);
assertThat(widgetItemInTable.get(2)).containsExactly(mWidget2x3, mWidget2x4);
assertThat(widgetItemInTable.get(3)).containsExactly(mWidget4x4);
}
@Test
public void groupWidgetItemsIntoTableWithReordering_widgetsOnly_maxSpanPxPerRow220_cellPadding10_shouldGroupWidgetsInTable() {
public void groupWithReordering_widgetsOnly_maxSpanPxPerRow220_cellPadding10() {
List<WidgetItem> widgetItems = List.of(mWidget4x4, mWidget2x3, mWidget1x1, mWidget2x4,
mWidget2x2);
@@ -131,9 +135,13 @@ public final class WidgetsTableUtilsTest {
WidgetsTableUtils.groupWidgetItemsUsingRowPxWithReordering(widgetItems, mContext,
mTestDeviceProfile, 220, 10);
// Row 0: 1x1(50px), 2x2(110px)
// Row 1: 2x3(110px), 2x4(110px)
// Row 2: 4x4(230px)
// With reordering, but space taken up by cell padding, so, no grouping (even if 2x2 and 2x3
// use same preview container).
// Row 0: 1x1(50px)
// Row 1: 2x2(in a 2x2 container: 130px)
// Row 2: 2x3(in a 2x3 container: 130px)
// Row 3: 2x4(in a 2x3 container: 130px)
// Row 4: 4x4(in a 3x3 container in tablet - 190px; 4x3 on phone - 250px)
assertThat(widgetItemInTable).hasSize(5);
assertThat(widgetItemInTable.get(0)).containsExactly(mWidget1x1);
assertThat(widgetItemInTable.get(1)).containsExactly(mWidget2x2);
@@ -143,7 +151,29 @@ public final class WidgetsTableUtilsTest {
}
@Test
public void groupWidgetItemsIntoTableWithReordering_widgetsOnly_maxSpanPxPerRow350_cellPadding0_shouldGroupWidgetsInTable() {
public void groupWithReordering_widgetsOnly_maxSpanPxPerRow260_cellPadding10() {
List<WidgetItem> widgetItems = List.of(mWidget4x4, mWidget2x3, mWidget1x1, mWidget2x4,
mWidget2x2);
List<ArrayList<WidgetItem>> widgetItemInTable =
WidgetsTableUtils.groupWidgetItemsUsingRowPxWithReordering(widgetItems, mContext,
mTestDeviceProfile, 260, 10);
// With reordering, even with cellPadding, enough space to group 2x3 and 2x4 (which also use
// same container)
// Row 0: 1x1(50px)
// Row 1: 2x2(in a 2x2 container: 130px)
// Row 2: 2x3(in a 2x3 container: 130px), 2x4(in a 2x3 container: 130px)
// Row 3: 4x4(in a 3x3 container in tablet - 190px; 4x3 on phone - 250px)
assertThat(widgetItemInTable).hasSize(4);
assertThat(widgetItemInTable.get(0)).containsExactly(mWidget1x1);
assertThat(widgetItemInTable.get(1)).containsExactly(mWidget2x2);
assertThat(widgetItemInTable.get(2)).containsExactly(mWidget2x3, mWidget2x4);
assertThat(widgetItemInTable.get(3)).containsExactly(mWidget4x4);
}
@Test
public void groupWithReordering_widgetsOnly_maxSpanPxPerRow350_cellPadding0() {
List<WidgetItem> widgetItems = List.of(mWidget4x4, mWidget2x3, mWidget1x1, mWidget2x4,
mWidget2x2);
@@ -151,17 +181,20 @@ public final class WidgetsTableUtilsTest {
WidgetsTableUtils.groupWidgetItemsUsingRowPxWithReordering(widgetItems, mContext,
mTestDeviceProfile, 350, 0);
// Row 0: 1x1(50px), 2x2(110px), 2x3(110px)
// Row 1: 2x4(110px)
// Row 2: 4x4(230px)
assertThat(widgetItemInTable).hasSize(3);
assertThat(widgetItemInTable.get(0)).containsExactly(mWidget1x1, mWidget2x2, mWidget2x3);
assertThat(widgetItemInTable.get(1)).containsExactly(mWidget2x4);
assertThat(widgetItemInTable.get(2)).containsExactly(mWidget4x4);
// With reordering, rows displayed in order of increasing size.
// Row 0: 1x1(50px)
// Row 1: 2x2(in a 2x2 container: 110px)
// Row 2: 2x3(in a 2x3 container: 110px), 2x4(in a 2x3 container: 110px)
// Row 3: 4x4(in a 3x3 container in tablet - 170px; 4x3 on phone - 230px)
assertThat(widgetItemInTable).hasSize(4);
assertThat(widgetItemInTable.get(0)).containsExactly(mWidget1x1);
assertThat(widgetItemInTable.get(1)).containsExactly(mWidget2x2);
assertThat(widgetItemInTable.get(2)).containsExactly(mWidget2x3, mWidget2x4);
assertThat(widgetItemInTable.get(3)).containsExactly(mWidget4x4);
}
@Test
public void groupWidgetItemsIntoTableWithReordering_mixItems_maxSpanPxPerRow350_cellPadding0_shouldGroupWidgetsInTable() {
public void groupWithReordering_mixItems_maxSpanPxPerRow350_cellPadding0() {
List<WidgetItem> widgetItems = List.of(mWidget4x4, mShortcut3, mWidget2x3, mShortcut1,
mWidget1x1, mShortcut2, mWidget2x4, mWidget2x2);
@@ -169,19 +202,22 @@ public final class WidgetsTableUtilsTest {
WidgetsTableUtils.groupWidgetItemsUsingRowPxWithReordering(widgetItems, mContext,
mTestDeviceProfile, 350, 0);
// Row 0: 1x1(50px), 2x2(110px), 2x3(110px)
// Row 1: 2x4(110px),
// Row 2: 4x4(230px)
// Row 3: shortcut3(50px), shortcut1(50px), shortcut2(50px)
assertThat(widgetItemInTable).hasSize(4);
assertThat(widgetItemInTable.get(0)).containsExactly(mWidget1x1, mWidget2x2, mWidget2x3);
assertThat(widgetItemInTable.get(1)).containsExactly(mWidget2x4);
assertThat(widgetItemInTable.get(2)).containsExactly(mWidget4x4);
assertThat(widgetItemInTable.get(3)).containsExactly(mShortcut3, mShortcut2, mShortcut1);
// With reordering - rows displays in order of increasing size:
// Row 0: 1x1(50px)
// Row 1: 2x2(110px)
// Row 2: 2x3 (in a 2x3 container 110px), 2x4 (in a 2x3 container 110px)
// Row 3: 4x4 (in a 3x3 container in tablet - 170px; 4x3 on phone - 230px)
// Row 4: shortcut3, shortcut1, shortcut2 (shortcuts are always displayed at bottom)
assertThat(widgetItemInTable).hasSize(5);
assertThat(widgetItemInTable.get(0)).containsExactly(mWidget1x1);
assertThat(widgetItemInTable.get(1)).containsExactly(mWidget2x2);
assertThat(widgetItemInTable.get(2)).containsExactly(mWidget2x3, mWidget2x4);
assertThat(widgetItemInTable.get(3)).containsExactly(mWidget4x4);
assertThat(widgetItemInTable.get(4)).containsExactly(mShortcut3, mShortcut2, mShortcut1);
}
@Test
public void groupWidgetItemsIntoTableWithoutReordering_maxSpanPxPerRow220_cellPadding0_shouldMaintainTheOrder() {
public void groupWithoutReordering_maxSpanPxPerRow220_cellPadding0() {
List<WidgetItem> widgetItems =
List.of(mWidget4x4, mWidget2x3, mWidget1x1, mWidget2x4, mWidget2x2);
@@ -189,13 +225,19 @@ public final class WidgetsTableUtilsTest {
WidgetsTableUtils.groupWidgetItemsUsingRowPxWithoutReordering(widgetItems, mContext,
mTestDeviceProfile, 220, 0);
// Row 0: 4x4(230px)
// Row 1: 2x3(110px), 1x1(50px)
// Row 2: 2x4(110px), 2x2(110px)
assertThat(widgetItemInTable).hasSize(3);
// Without reordering, widgets are grouped only if the next one fits and uses same preview
// container:
// Row 0: 4x4(in a 3x3 container in tablet - 170px; 4x3 on phone - 230px)
// Row 1: 2x3(in a 2x3 container - 110px)
// Row 2: 1x1(50px)
// Row 3: 2x4(in a 2x3 container - 110px)
// Row 4: 2x2(in a 2x2 container - 110px)
assertThat(widgetItemInTable).hasSize(5);
assertThat(widgetItemInTable.get(0)).containsExactly(mWidget4x4);
assertThat(widgetItemInTable.get(1)).containsExactly(mWidget2x3, mWidget1x1);
assertThat(widgetItemInTable.get(2)).containsExactly(mWidget2x4, mWidget2x2);
assertThat(widgetItemInTable.get(1)).containsExactly(mWidget2x3);
assertThat(widgetItemInTable.get(2)).containsExactly(mWidget1x1);
assertThat(widgetItemInTable.get(3)).containsExactly(mWidget2x4);
assertThat(widgetItemInTable.get(4)).containsExactly(mWidget2x2);
}
private void initDP() {