Skip to content

Commit 1c27404

Browse files
imhappileticiarossi
authored andcommitted
[Carousel] Refactor to reuse logic between different Carousel strategy classes
- Moved Arrangement class outside of MultiBrowseStrategy - Added helper class CarouselStrategyHelper and moved common logic in MultiBrowseStrategy to CarouselStrategyHelper PiperOrigin-RevId: 528924778
1 parent 2f13532 commit 1c27404

4 files changed

Lines changed: 418 additions & 342 deletions

File tree

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
/*
2+
* Copyright 2023 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+
* https://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.google.android.material.carousel;
17+
18+
import static java.lang.Math.abs;
19+
import static java.lang.Math.max;
20+
import static java.lang.Math.min;
21+
22+
import androidx.annotation.NonNull;
23+
import androidx.core.math.MathUtils;
24+
25+
/**
26+
* A class that holds data about a combination of large, medium, and small items, knows how to alter
27+
* an arrangement to fit within an available space, and can assess the arrangement's
28+
* desirability according to a priority heuristic.
29+
*/
30+
final class Arrangement {
31+
32+
// Specifies a percentage of a medium item's size by which it can be increased or decreased to
33+
// help fit an arrangement into the carousel's available space.
34+
private static final float MEDIUM_ITEM_FLEX_PERCENTAGE = .1F;
35+
36+
final int priority;
37+
float smallSize;
38+
final int smallCount;
39+
final int mediumCount;
40+
float mediumSize;
41+
float largeSize;
42+
final int largeCount;
43+
final float cost;
44+
45+
/**
46+
* Creates a new arrangement by taking in a number of small, medium, and large items and the
47+
* size each would like to be and then fitting the sizes to work within the {@code
48+
* availableSpace}.
49+
*
50+
* <p>Note: The values for each item size after construction will likely differ from the target
51+
* values passed to the constructor since the constructor handles altering the sizes until the
52+
* total count is able to fit within the space see {@link #fit(float, float, float, float)} for
53+
* more details.
54+
*
55+
* @param priority the order in which this arrangement should be preferred against other
56+
* arrangements that fit
57+
* @param targetSmallSize the size of a small item in this arrangement
58+
* @param minSmallSize the minimum size a small item is allowed to be
59+
* @param maxSmallSize the maximum size a small item is allowed to be
60+
* @param smallCount the number of small items in this arrangement
61+
* @param targetMediumSize the size of medium items in this arrangement
62+
* @param mediumCount the number of medium items in this arrangement
63+
* @param targetLargeSize the size of large items in this arrangement
64+
* @param largeCount the number of large items in this arrangement
65+
* @param availableSpace the space this arrangement needs to fit within
66+
*/
67+
Arrangement(
68+
int priority,
69+
float targetSmallSize,
70+
float minSmallSize,
71+
float maxSmallSize,
72+
int smallCount,
73+
float targetMediumSize,
74+
int mediumCount,
75+
float targetLargeSize,
76+
int largeCount,
77+
float availableSpace) {
78+
this.priority = priority;
79+
this.smallSize = MathUtils.clamp(targetSmallSize, minSmallSize, maxSmallSize);
80+
this.smallCount = smallCount;
81+
this.mediumSize = targetMediumSize;
82+
this.mediumCount = mediumCount;
83+
this.largeSize = targetLargeSize;
84+
this.largeCount = largeCount;
85+
86+
fit(availableSpace, minSmallSize, maxSmallSize, targetLargeSize);
87+
this.cost = cost(targetLargeSize);
88+
}
89+
90+
@NonNull
91+
@Override
92+
public String toString() {
93+
return "Arrangement [priority="
94+
+ priority
95+
+ ", smallCount="
96+
+ smallCount
97+
+ ", smallSize="
98+
+ smallSize
99+
+ ", mediumCount="
100+
+ mediumCount
101+
+ ", mediumSize="
102+
+ mediumSize
103+
+ ", largeCount="
104+
+ largeCount
105+
+ ", largeSize="
106+
+ largeSize
107+
+ ", cost="
108+
+ cost
109+
+ "]";
110+
}
111+
112+
/** Gets the total space taken by this arrangement. */
113+
private float getSpace() {
114+
return (largeSize * largeCount) + (mediumSize * mediumCount) + (smallSize * smallCount);
115+
}
116+
117+
/**
118+
* Alters the item sizes of this arrangement until the space occupied fits within the {@code
119+
* availableSpace}.
120+
*
121+
* <p>This method tries to adjust the size of large items as little as possible by first adjusting
122+
* small items as much as possible, then adjusting medium items as much as possible, and finally
123+
* adjusting large items if the arrangement is still unable to fit.
124+
*
125+
* @param availableSpace the size of the carousel this arrangement needs to fit
126+
* @param minSmallSize the minimum size small items can be
127+
* @param maxSmallSize the maximum size small items can be
128+
* @param targetLargeSize the target size for large items
129+
*/
130+
private void fit(
131+
float availableSpace, float minSmallSize, float maxSmallSize, float targetLargeSize) {
132+
float delta = availableSpace - getSpace();
133+
// First, resize small items within their allowable min-max range to try to fit the
134+
// arrangement into the available space.
135+
if (smallCount > 0 && delta > 0) {
136+
// grow the small items
137+
smallSize += min(delta / smallCount, maxSmallSize - smallSize);
138+
} else if (smallCount > 0 && delta < 0) {
139+
// shrink the small items
140+
smallSize += max(delta / smallCount, minSmallSize - smallSize);
141+
}
142+
143+
largeSize =
144+
calculateLargeSize(availableSpace, smallCount, smallSize, mediumCount, largeCount);
145+
mediumSize = (largeSize + smallSize) / 2F;
146+
147+
// If the large size has been adjusted away from its target size to fit the arrangement,
148+
// counter this as much as possible by altering the medium item within its acceptable flex
149+
// range.
150+
if (mediumCount > 0 && largeSize != targetLargeSize) {
151+
float targetAdjustment = (targetLargeSize - largeSize) * largeCount;
152+
float availableMediumFlex = (mediumSize * MEDIUM_ITEM_FLEX_PERCENTAGE) * mediumCount;
153+
float distribute = min(abs(targetAdjustment), availableMediumFlex);
154+
if (targetAdjustment > 0F) {
155+
// Reduce the size of the medium item and give it back to the large items
156+
mediumSize -= (distribute / mediumCount);
157+
largeSize += (distribute / largeCount);
158+
} else {
159+
// Increase the size of the medium item and take from the large items
160+
mediumSize += (distribute / mediumCount);
161+
largeSize -= (distribute / largeCount);
162+
}
163+
}
164+
}
165+
166+
/**
167+
* Calculates the large size that is able to fit within the available space given item counts,
168+
* the small size, and that the medium size is {@code (largeSize + smallSize) / 2}.
169+
*
170+
* <p>This method solves the following equation for largeSize:
171+
*
172+
* <p>{@code availableSpace = (largeSize * largeCount) + (((largeSize + smallSize) / 2) *
173+
* mediumCount) + (smallSize * smallCount)}
174+
*
175+
* @param availableSpace the total available space
176+
* @param smallCount the number of small items in the arrangement
177+
* @param smallSize the size of small items in the arrangement
178+
* @param mediumCount the number of medium items in the arrangement
179+
* @param largeCount the number of large items in the arrangement
180+
* @return the large item size which will fit for the available space and other item constraints
181+
*/
182+
private float calculateLargeSize(
183+
float availableSpace, int smallCount, float smallSize, int mediumCount, int largeCount) {
184+
// Zero out small size if there are no small items
185+
smallSize = smallCount > 0 ? smallSize : 0F;
186+
return (availableSpace - (((float) smallCount) + ((float) mediumCount) / 2F) * smallSize)
187+
/ (((float) largeCount) + ((float) mediumCount) / 2F);
188+
}
189+
190+
private boolean isValid() {
191+
if (largeCount > 0 && smallCount > 0 && mediumCount > 0) {
192+
return largeSize > mediumSize && mediumSize > smallSize;
193+
} else if (largeCount > 0 && smallCount > 0) {
194+
return largeSize > smallSize;
195+
}
196+
197+
return true;
198+
}
199+
200+
/**
201+
* Calculates the cost of this arrangement to determine visual desirability and adherence to
202+
* inputs.
203+
*
204+
* @param targetLargeSize the size large items would like to be
205+
* @return a float representing the cost of this arrangement where the lower the cost the better
206+
*/
207+
private float cost(float targetLargeSize) {
208+
if (!isValid()) {
209+
return Float.MAX_VALUE;
210+
}
211+
// Arrangements have a lower cost if they have a priority closer to 1 and their largeSize is
212+
// altered as little as possible.
213+
return abs(targetLargeSize - largeSize) * priority;
214+
}
215+
216+
/**
217+
* Create an arrangement for all possible permutations for {@code smallCounts} and {@code
218+
* largeCounts}, fit each into the available space, and return the arrangement with the lowest
219+
* cost.
220+
*
221+
* <p>Keep in mind that the returned arrangements do not take into account the available space
222+
* from the carousel. They will all occupy varying degrees of more or less space. The caller needs
223+
* to handle sorting the returned list, picking the most desirable arrangement, and fitting the
224+
* arrangement to the size of the carousel.
225+
*
226+
* @param availableSpace the space the arrangement needs to fit
227+
* @param targetSmallSize the size small items would like to be
228+
* @param minSmallSize the minimum size small items are allowed to be
229+
* @param maxSmallSize the maximum size small items are allowed to be
230+
* @param smallCounts an array of small item counts for a valid arrangement ordered by priority
231+
* @param targetMediumSize the size medium items would like to be
232+
* @param mediumCounts an array of medium item counts for a valid arrangement ordered by priority
233+
* @param targetLargeSize the size large items would like to be
234+
* @param largeCounts an array of large item counts for a valid arrangement ordered by priority
235+
* @return the arrangement that is considered the most desirable and has been adjusted to fit
236+
* within the available space
237+
*/
238+
static Arrangement findLowestCostArrangement(
239+
float availableSpace,
240+
float targetSmallSize,
241+
float minSmallSize,
242+
float maxSmallSize,
243+
int[] smallCounts,
244+
float targetMediumSize,
245+
int[] mediumCounts,
246+
float targetLargeSize,
247+
int[] largeCounts) {
248+
Arrangement lowestCostArrangement = null;
249+
int priority = 1;
250+
for (int largeCount : largeCounts) {
251+
for (int mediumCount : mediumCounts) {
252+
for (int smallCount : smallCounts) {
253+
Arrangement arrangement =
254+
new Arrangement(
255+
priority,
256+
targetSmallSize,
257+
minSmallSize,
258+
maxSmallSize,
259+
smallCount,
260+
targetMediumSize,
261+
mediumCount,
262+
targetLargeSize,
263+
largeCount,
264+
availableSpace);
265+
if (lowestCostArrangement == null || arrangement.cost < lowestCostArrangement.cost) {
266+
lowestCostArrangement = arrangement;
267+
if (lowestCostArrangement.cost == 0F) {
268+
// If the new lowestCostArrangement has a cost of 0, we know it didn't have to alter
269+
// the large item size at all. We also know that arrangement permutations will be
270+
// generated in order of priority. We can exit early knowing there will not be an
271+
// arrangement with a better cost or priority.
272+
return lowestCostArrangement;
273+
}
274+
}
275+
priority++;
276+
}
277+
}
278+
}
279+
return lowestCostArrangement;
280+
}
281+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright 2023 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+
* https://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.google.android.material.carousel;
17+
18+
import com.google.android.material.R;
19+
20+
import static com.google.android.material.carousel.CarouselStrategy.getChildMaskPercentage;
21+
import static java.lang.Math.max;
22+
23+
import android.content.Context;
24+
import androidx.annotation.NonNull;
25+
26+
/**
27+
* A helper class with utility methods for {@link CarouselStrategy} implementations.
28+
*/
29+
final class CarouselStrategyHelper {
30+
31+
private CarouselStrategyHelper() {}
32+
33+
static float getExtraSmallSize(@NonNull Context context) {
34+
return context.getResources().getDimension(R.dimen.m3_carousel_gone_size);
35+
}
36+
37+
static float getSmallSizeMin(@NonNull Context context) {
38+
return context.getResources().getDimension(R.dimen.m3_carousel_small_item_size_min);
39+
}
40+
41+
static float getSmallSizeMax(@NonNull Context context) {
42+
return context.getResources().getDimension(R.dimen.m3_carousel_small_item_size_max);
43+
}
44+
45+
/**
46+
* Gets the {@link KeylineState} associated with the given parameters.
47+
*
48+
* @param context The context used to load resources.
49+
* @param childHorizontalMargins The child margins to use when calculating mask percentage.
50+
* @param availableSpace the space that the {@link KeylineState} needs to fit.
51+
* @param arrangement the {@link Arrangement} to translate into a {@link KeylineState}.
52+
* @return the {@link KeylineState} associated with the arrangement with the lowest cost
53+
* according to the item count array priorities and how close it is to the target sizes.
54+
*/
55+
static KeylineState createLeftAlignedKeylineState(
56+
@NonNull Context context,
57+
float childHorizontalMargins,
58+
float availableSpace,
59+
@NonNull Arrangement arrangement) {
60+
61+
float extraSmallChildWidth = getExtraSmallSize(context) + childHorizontalMargins;
62+
63+
float start = 0F;
64+
float extraSmallHeadCenterX = start - (extraSmallChildWidth / 2F);
65+
66+
float largeStartCenterX = start + (arrangement.largeSize / 2F);
67+
float largeEndCenterX =
68+
largeStartCenterX + (max(0, arrangement.largeCount - 1) * arrangement.largeSize);
69+
start = largeEndCenterX + arrangement.largeSize / 2F;
70+
71+
float mediumCenterX =
72+
arrangement.mediumCount > 0 ? start + (arrangement.mediumSize / 2F) : largeEndCenterX;
73+
start = arrangement.mediumCount > 0 ? mediumCenterX + (arrangement.mediumSize / 2F) : start;
74+
75+
float smallStartCenterX =
76+
arrangement.smallCount > 0 ? start + (arrangement.smallSize / 2F) : mediumCenterX;
77+
78+
float extraSmallTailCenterX = availableSpace + (extraSmallChildWidth / 2F);
79+
80+
float extraSmallMask =
81+
getChildMaskPercentage(extraSmallChildWidth, arrangement.largeSize, childHorizontalMargins);
82+
float smallMask =
83+
getChildMaskPercentage(
84+
arrangement.smallSize, arrangement.largeSize, childHorizontalMargins);
85+
float mediumMask =
86+
getChildMaskPercentage(
87+
arrangement.mediumSize, arrangement.largeSize, childHorizontalMargins);
88+
float largeMask = 0F;
89+
90+
KeylineState.Builder builder =
91+
new KeylineState.Builder(arrangement.largeSize)
92+
.addKeyline(extraSmallHeadCenterX, extraSmallMask, extraSmallChildWidth)
93+
.addKeylineRange(
94+
largeStartCenterX, largeMask, arrangement.largeSize, arrangement.largeCount, true);
95+
if (arrangement.mediumCount > 0) {
96+
builder.addKeyline(mediumCenterX, mediumMask, arrangement.mediumSize);
97+
}
98+
if (arrangement.smallCount > 0) {
99+
builder.addKeylineRange(
100+
smallStartCenterX, smallMask, arrangement.smallSize, arrangement.smallCount);
101+
}
102+
builder.addKeyline(extraSmallTailCenterX, extraSmallMask, extraSmallChildWidth);
103+
return builder.build();
104+
}
105+
106+
static int maxValue(int[] array) {
107+
int largest = Integer.MIN_VALUE;
108+
for (int j : array) {
109+
if (j > largest) {
110+
largest = j;
111+
}
112+
}
113+
114+
return largest;
115+
}
116+
}

0 commit comments

Comments
 (0)