1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.launcher3.widget.util;
17 
18 import android.content.Context;
19 import android.util.Size;
20 
21 import androidx.annotation.Px;
22 
23 import com.android.launcher3.DeviceProfile;
24 import com.android.launcher3.model.WidgetItem;
25 import com.android.launcher3.widget.picker.util.WidgetPreviewContainerSize;
26 
27 import java.util.ArrayList;
28 import java.util.Comparator;
29 import java.util.List;
30 import java.util.stream.Collectors;
31 
32 /** An utility class which groups {@link WidgetItem}s into a table. */
33 public final class WidgetsTableUtils {
34     private static final int MAX_ITEMS_IN_ROW = 3;
35 
36     /**
37      * Groups widgets in the following order:
38      * 1. Widgets always go before shortcuts.
39      * 2. Widgets with smaller vertical spans will be shown first.
40      * 3. If widgets have the same vertical spans, then widgets with a smaller horizontal spans will
41      *    go first.
42      * 4. If both widgets have the same horizontal and vertical spans, they will use the same order
43      *    from the given {@code widgetItems}.
44      */
45     private static final Comparator<WidgetItem> WIDGET_SHORTCUT_COMPARATOR = (item, otherItem) -> {
46         if (item.widgetInfo != null && otherItem.widgetInfo == null) return -1;
47 
48         if (item.widgetInfo == null && otherItem.widgetInfo != null) return 1;
49         if (item.spanY == otherItem.spanY) {
50             if (item.spanX == otherItem.spanX) return 0;
51             return item.spanX > otherItem.spanX ? 1 : -1;
52         }
53         return item.spanY > otherItem.spanY ? 1 : -1;
54     };
55 
56     /**
57      * Comparator that enables displaying rows in increasing order of their size (totalW * H);
58      * except for shortcuts which always show at the bottom.
59      */
60     public static final Comparator<ArrayList<WidgetItem>> WIDGETS_TABLE_ROW_SIZE_COMPARATOR =
61             Comparator.comparingInt(row -> {
62                 if (row.stream().anyMatch(WidgetItem::isShortcut)) {
63                     return Integer.MAX_VALUE;
64                 } else {
65                     int rowWidth = row.stream().mapToInt(w -> w.spanX).sum();
66                     int rowHeight = row.get(0).spanY;
67                     return (rowWidth * rowHeight);
68                 }
69             });
70 
71     /**
72      * Groups {@code widgetItems} items into a 2D array which matches their appearance in a UI
73      * table. This takes liberty to rearrange widgets to make the table visually appealing.
74      */
groupWidgetItemsUsingRowPxWithReordering( List<WidgetItem> widgetItems, Context context, final DeviceProfile dp, final @Px int rowPx, final @Px int cellPadding)75     public static List<ArrayList<WidgetItem>> groupWidgetItemsUsingRowPxWithReordering(
76             List<WidgetItem> widgetItems, Context context, final DeviceProfile dp,
77             final @Px int rowPx, final @Px int cellPadding) {
78         List<WidgetItem> sortedWidgetItems = widgetItems.stream().sorted(WIDGET_SHORTCUT_COMPARATOR)
79                 .collect(Collectors.toList());
80         List<ArrayList<WidgetItem>> rows = groupWidgetItemsUsingRowPxWithoutReordering(
81                 sortedWidgetItems, context, dp, rowPx,
82                 cellPadding);
83         return rows.stream().sorted(WIDGETS_TABLE_ROW_SIZE_COMPARATOR).toList();
84     }
85 
86     /**
87      * Groups {@code widgetItems} into a 2D array which matches their appearance in a UI table while
88      * maintaining their order. This function is a variant of
89      * {@code groupWidgetItemsIntoTableWithoutReordering} in that this uses widget container's
90      * pixels for calculation.
91      *
92      * <p>Grouping:
93      * 1. Widgets and shortcuts never group together in the same row.
94      * 2. Widgets are grouped together only if they have same preview container size.
95      * 3. Widgets are grouped together in the same row until the total of individual container sizes
96      *    exceed the total allowed pixels for the row.
97      * 3. The ordered shortcuts are grouped together in the same row until their individual
98      *    occupying pixels exceed the total allowed pixels for the cell.
99      * 4. If there is only one widget in a row, its width may exceed the {@code rowPx}.
100      *
101      * <p>See WidgetTableUtilsTest
102      */
groupWidgetItemsUsingRowPxWithoutReordering( List<WidgetItem> widgetItems, Context context, final DeviceProfile dp, final @Px int rowPx, final @Px int cellPadding)103     public static List<ArrayList<WidgetItem>> groupWidgetItemsUsingRowPxWithoutReordering(
104             List<WidgetItem> widgetItems, Context context, final DeviceProfile dp,
105             final @Px int rowPx, final @Px int cellPadding) {
106         List<ArrayList<WidgetItem>> widgetItemsTable = new ArrayList<>();
107         ArrayList<WidgetItem> widgetItemsAtRow = null;
108         // A row displays only items of same container size.
109         WidgetPreviewContainerSize containerSizeForRow = null;
110         @Px int currentRowWidth = 0;
111 
112         for (WidgetItem widgetItem : widgetItems) {
113             if (widgetItemsAtRow == null) {
114                 widgetItemsAtRow = new ArrayList<>();
115                 widgetItemsTable.add(widgetItemsAtRow);
116             }
117             int numOfWidgetItems = widgetItemsAtRow.size();
118 
119             WidgetPreviewContainerSize containerSize =
120                     WidgetPreviewContainerSize.Companion.forItem(widgetItem, dp);
121             Size containerSizePx = WidgetSizes.getWidgetSizePx(dp, containerSize.spanX,
122                     containerSize.spanY);
123             @Px int containerWidth = containerSizePx.getWidth() + (2 * cellPadding);
124 
125             if (numOfWidgetItems == 0) {
126                 widgetItemsAtRow.add(widgetItem);
127                 containerSizeForRow = containerSize;
128                 currentRowWidth = containerWidth;
129             } else if (widgetItemsAtRow.size() < MAX_ITEMS_IN_ROW
130                     && (currentRowWidth + containerWidth) <= rowPx
131                     && widgetItem.hasSameType(widgetItemsAtRow.get(numOfWidgetItems - 1))
132                     && containerSize.equals(containerSizeForRow)) {
133                 // Group items in the same row if
134                 // 1. they are with the same type, i.e. a row can only have widgets or shortcuts but
135                 //    never a mix of both.
136                 // 2. Each widget in the given row has same preview container size.
137                 widgetItemsAtRow.add(widgetItem);
138                 currentRowWidth += containerWidth;
139             } else {
140                 widgetItemsAtRow = new ArrayList<>();
141                 widgetItemsTable.add(widgetItemsAtRow);
142                 widgetItemsAtRow.add(widgetItem);
143                 containerSizeForRow = containerSize;
144                 currentRowWidth = containerWidth;
145             }
146         }
147         return widgetItemsTable;
148     }
149 }
150