-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Carousel] Refactor to reuse logic between different Carousel strateg…
…y classes - Moved Arrangement class outside of MultiBrowseStrategy - Added helper class CarouselStrategyHelper and moved common logic in MultiBrowseStrategy to CarouselStrategyHelper PiperOrigin-RevId: 528924778
- Loading branch information
1 parent
2f13532
commit 1c27404
Showing
4 changed files
with
418 additions
and
342 deletions.
There are no files selected for viewing
281 changes: 281 additions & 0 deletions
281
lib/java/com/google/android/material/carousel/Arrangement.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,281 @@ | ||
/* | ||
* Copyright 2023 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 | ||
* | ||
* https://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.google.android.material.carousel; | ||
|
||
import static java.lang.Math.abs; | ||
import static java.lang.Math.max; | ||
import static java.lang.Math.min; | ||
|
||
import androidx.annotation.NonNull; | ||
import androidx.core.math.MathUtils; | ||
|
||
/** | ||
* A class that holds data about a combination of large, medium, and small items, knows how to alter | ||
* an arrangement to fit within an available space, and can assess the arrangement's | ||
* desirability according to a priority heuristic. | ||
*/ | ||
final class Arrangement { | ||
|
||
// Specifies a percentage of a medium item's size by which it can be increased or decreased to | ||
// help fit an arrangement into the carousel's available space. | ||
private static final float MEDIUM_ITEM_FLEX_PERCENTAGE = .1F; | ||
|
||
final int priority; | ||
float smallSize; | ||
final int smallCount; | ||
final int mediumCount; | ||
float mediumSize; | ||
float largeSize; | ||
final int largeCount; | ||
final float cost; | ||
|
||
/** | ||
* Creates a new arrangement by taking in a number of small, medium, and large items and the | ||
* size each would like to be and then fitting the sizes to work within the {@code | ||
* availableSpace}. | ||
* | ||
* <p>Note: The values for each item size after construction will likely differ from the target | ||
* values passed to the constructor since the constructor handles altering the sizes until the | ||
* total count is able to fit within the space see {@link #fit(float, float, float, float)} for | ||
* more details. | ||
* | ||
* @param priority the order in which this arrangement should be preferred against other | ||
* arrangements that fit | ||
* @param targetSmallSize the size of a small item in this arrangement | ||
* @param minSmallSize the minimum size a small item is allowed to be | ||
* @param maxSmallSize the maximum size a small item is allowed to be | ||
* @param smallCount the number of small items in this arrangement | ||
* @param targetMediumSize the size of medium items in this arrangement | ||
* @param mediumCount the number of medium items in this arrangement | ||
* @param targetLargeSize the size of large items in this arrangement | ||
* @param largeCount the number of large items in this arrangement | ||
* @param availableSpace the space this arrangement needs to fit within | ||
*/ | ||
Arrangement( | ||
int priority, | ||
float targetSmallSize, | ||
float minSmallSize, | ||
float maxSmallSize, | ||
int smallCount, | ||
float targetMediumSize, | ||
int mediumCount, | ||
float targetLargeSize, | ||
int largeCount, | ||
float availableSpace) { | ||
this.priority = priority; | ||
this.smallSize = MathUtils.clamp(targetSmallSize, minSmallSize, maxSmallSize); | ||
this.smallCount = smallCount; | ||
this.mediumSize = targetMediumSize; | ||
this.mediumCount = mediumCount; | ||
this.largeSize = targetLargeSize; | ||
this.largeCount = largeCount; | ||
|
||
fit(availableSpace, minSmallSize, maxSmallSize, targetLargeSize); | ||
this.cost = cost(targetLargeSize); | ||
} | ||
|
||
@NonNull | ||
@Override | ||
public String toString() { | ||
return "Arrangement [priority=" | ||
+ priority | ||
+ ", smallCount=" | ||
+ smallCount | ||
+ ", smallSize=" | ||
+ smallSize | ||
+ ", mediumCount=" | ||
+ mediumCount | ||
+ ", mediumSize=" | ||
+ mediumSize | ||
+ ", largeCount=" | ||
+ largeCount | ||
+ ", largeSize=" | ||
+ largeSize | ||
+ ", cost=" | ||
+ cost | ||
+ "]"; | ||
} | ||
|
||
/** Gets the total space taken by this arrangement. */ | ||
private float getSpace() { | ||
return (largeSize * largeCount) + (mediumSize * mediumCount) + (smallSize * smallCount); | ||
} | ||
|
||
/** | ||
* Alters the item sizes of this arrangement until the space occupied fits within the {@code | ||
* availableSpace}. | ||
* | ||
* <p>This method tries to adjust the size of large items as little as possible by first adjusting | ||
* small items as much as possible, then adjusting medium items as much as possible, and finally | ||
* adjusting large items if the arrangement is still unable to fit. | ||
* | ||
* @param availableSpace the size of the carousel this arrangement needs to fit | ||
* @param minSmallSize the minimum size small items can be | ||
* @param maxSmallSize the maximum size small items can be | ||
* @param targetLargeSize the target size for large items | ||
*/ | ||
private void fit( | ||
float availableSpace, float minSmallSize, float maxSmallSize, float targetLargeSize) { | ||
float delta = availableSpace - getSpace(); | ||
// First, resize small items within their allowable min-max range to try to fit the | ||
// arrangement into the available space. | ||
if (smallCount > 0 && delta > 0) { | ||
// grow the small items | ||
smallSize += min(delta / smallCount, maxSmallSize - smallSize); | ||
} else if (smallCount > 0 && delta < 0) { | ||
// shrink the small items | ||
smallSize += max(delta / smallCount, minSmallSize - smallSize); | ||
} | ||
|
||
largeSize = | ||
calculateLargeSize(availableSpace, smallCount, smallSize, mediumCount, largeCount); | ||
mediumSize = (largeSize + smallSize) / 2F; | ||
|
||
// If the large size has been adjusted away from its target size to fit the arrangement, | ||
// counter this as much as possible by altering the medium item within its acceptable flex | ||
// range. | ||
if (mediumCount > 0 && largeSize != targetLargeSize) { | ||
float targetAdjustment = (targetLargeSize - largeSize) * largeCount; | ||
float availableMediumFlex = (mediumSize * MEDIUM_ITEM_FLEX_PERCENTAGE) * mediumCount; | ||
float distribute = min(abs(targetAdjustment), availableMediumFlex); | ||
if (targetAdjustment > 0F) { | ||
// Reduce the size of the medium item and give it back to the large items | ||
mediumSize -= (distribute / mediumCount); | ||
largeSize += (distribute / largeCount); | ||
} else { | ||
// Increase the size of the medium item and take from the large items | ||
mediumSize += (distribute / mediumCount); | ||
largeSize -= (distribute / largeCount); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Calculates the large size that is able to fit within the available space given item counts, | ||
* the small size, and that the medium size is {@code (largeSize + smallSize) / 2}. | ||
* | ||
* <p>This method solves the following equation for largeSize: | ||
* | ||
* <p>{@code availableSpace = (largeSize * largeCount) + (((largeSize + smallSize) / 2) * | ||
* mediumCount) + (smallSize * smallCount)} | ||
* | ||
* @param availableSpace the total available space | ||
* @param smallCount the number of small items in the arrangement | ||
* @param smallSize the size of small items in the arrangement | ||
* @param mediumCount the number of medium items in the arrangement | ||
* @param largeCount the number of large items in the arrangement | ||
* @return the large item size which will fit for the available space and other item constraints | ||
*/ | ||
private float calculateLargeSize( | ||
float availableSpace, int smallCount, float smallSize, int mediumCount, int largeCount) { | ||
// Zero out small size if there are no small items | ||
smallSize = smallCount > 0 ? smallSize : 0F; | ||
return (availableSpace - (((float) smallCount) + ((float) mediumCount) / 2F) * smallSize) | ||
/ (((float) largeCount) + ((float) mediumCount) / 2F); | ||
} | ||
|
||
private boolean isValid() { | ||
if (largeCount > 0 && smallCount > 0 && mediumCount > 0) { | ||
return largeSize > mediumSize && mediumSize > smallSize; | ||
} else if (largeCount > 0 && smallCount > 0) { | ||
return largeSize > smallSize; | ||
} | ||
|
||
return true; | ||
} | ||
|
||
/** | ||
* Calculates the cost of this arrangement to determine visual desirability and adherence to | ||
* inputs. | ||
* | ||
* @param targetLargeSize the size large items would like to be | ||
* @return a float representing the cost of this arrangement where the lower the cost the better | ||
*/ | ||
private float cost(float targetLargeSize) { | ||
if (!isValid()) { | ||
return Float.MAX_VALUE; | ||
} | ||
// Arrangements have a lower cost if they have a priority closer to 1 and their largeSize is | ||
// altered as little as possible. | ||
return abs(targetLargeSize - largeSize) * priority; | ||
} | ||
|
||
/** | ||
* Create an arrangement for all possible permutations for {@code smallCounts} and {@code | ||
* largeCounts}, fit each into the available space, and return the arrangement with the lowest | ||
* cost. | ||
* | ||
* <p>Keep in mind that the returned arrangements do not take into account the available space | ||
* from the carousel. They will all occupy varying degrees of more or less space. The caller needs | ||
* to handle sorting the returned list, picking the most desirable arrangement, and fitting the | ||
* arrangement to the size of the carousel. | ||
* | ||
* @param availableSpace the space the arrangement needs to fit | ||
* @param targetSmallSize the size small items would like to be | ||
* @param minSmallSize the minimum size small items are allowed to be | ||
* @param maxSmallSize the maximum size small items are allowed to be | ||
* @param smallCounts an array of small item counts for a valid arrangement ordered by priority | ||
* @param targetMediumSize the size medium items would like to be | ||
* @param mediumCounts an array of medium item counts for a valid arrangement ordered by priority | ||
* @param targetLargeSize the size large items would like to be | ||
* @param largeCounts an array of large item counts for a valid arrangement ordered by priority | ||
* @return the arrangement that is considered the most desirable and has been adjusted to fit | ||
* within the available space | ||
*/ | ||
static Arrangement findLowestCostArrangement( | ||
float availableSpace, | ||
float targetSmallSize, | ||
float minSmallSize, | ||
float maxSmallSize, | ||
int[] smallCounts, | ||
float targetMediumSize, | ||
int[] mediumCounts, | ||
float targetLargeSize, | ||
int[] largeCounts) { | ||
Arrangement lowestCostArrangement = null; | ||
int priority = 1; | ||
for (int largeCount : largeCounts) { | ||
for (int mediumCount : mediumCounts) { | ||
for (int smallCount : smallCounts) { | ||
Arrangement arrangement = | ||
new Arrangement( | ||
priority, | ||
targetSmallSize, | ||
minSmallSize, | ||
maxSmallSize, | ||
smallCount, | ||
targetMediumSize, | ||
mediumCount, | ||
targetLargeSize, | ||
largeCount, | ||
availableSpace); | ||
if (lowestCostArrangement == null || arrangement.cost < lowestCostArrangement.cost) { | ||
lowestCostArrangement = arrangement; | ||
if (lowestCostArrangement.cost == 0F) { | ||
// If the new lowestCostArrangement has a cost of 0, we know it didn't have to alter | ||
// the large item size at all. We also know that arrangement permutations will be | ||
// generated in order of priority. We can exit early knowing there will not be an | ||
// arrangement with a better cost or priority. | ||
return lowestCostArrangement; | ||
} | ||
} | ||
priority++; | ||
} | ||
} | ||
} | ||
return lowestCostArrangement; | ||
} | ||
} |
116 changes: 116 additions & 0 deletions
116
lib/java/com/google/android/material/carousel/CarouselStrategyHelper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
/* | ||
* Copyright 2023 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 | ||
* | ||
* https://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.google.android.material.carousel; | ||
|
||
import com.google.android.material.R; | ||
|
||
import static com.google.android.material.carousel.CarouselStrategy.getChildMaskPercentage; | ||
import static java.lang.Math.max; | ||
|
||
import android.content.Context; | ||
import androidx.annotation.NonNull; | ||
|
||
/** | ||
* A helper class with utility methods for {@link CarouselStrategy} implementations. | ||
*/ | ||
final class CarouselStrategyHelper { | ||
|
||
private CarouselStrategyHelper() {} | ||
|
||
static float getExtraSmallSize(@NonNull Context context) { | ||
return context.getResources().getDimension(R.dimen.m3_carousel_gone_size); | ||
} | ||
|
||
static float getSmallSizeMin(@NonNull Context context) { | ||
return context.getResources().getDimension(R.dimen.m3_carousel_small_item_size_min); | ||
} | ||
|
||
static float getSmallSizeMax(@NonNull Context context) { | ||
return context.getResources().getDimension(R.dimen.m3_carousel_small_item_size_max); | ||
} | ||
|
||
/** | ||
* Gets the {@link KeylineState} associated with the given parameters. | ||
* | ||
* @param context The context used to load resources. | ||
* @param childHorizontalMargins The child margins to use when calculating mask percentage. | ||
* @param availableSpace the space that the {@link KeylineState} needs to fit. | ||
* @param arrangement the {@link Arrangement} to translate into a {@link KeylineState}. | ||
* @return the {@link KeylineState} associated with the arrangement with the lowest cost | ||
* according to the item count array priorities and how close it is to the target sizes. | ||
*/ | ||
static KeylineState createLeftAlignedKeylineState( | ||
@NonNull Context context, | ||
float childHorizontalMargins, | ||
float availableSpace, | ||
@NonNull Arrangement arrangement) { | ||
|
||
float extraSmallChildWidth = getExtraSmallSize(context) + childHorizontalMargins; | ||
|
||
float start = 0F; | ||
float extraSmallHeadCenterX = start - (extraSmallChildWidth / 2F); | ||
|
||
float largeStartCenterX = start + (arrangement.largeSize / 2F); | ||
float largeEndCenterX = | ||
largeStartCenterX + (max(0, arrangement.largeCount - 1) * arrangement.largeSize); | ||
start = largeEndCenterX + arrangement.largeSize / 2F; | ||
|
||
float mediumCenterX = | ||
arrangement.mediumCount > 0 ? start + (arrangement.mediumSize / 2F) : largeEndCenterX; | ||
start = arrangement.mediumCount > 0 ? mediumCenterX + (arrangement.mediumSize / 2F) : start; | ||
|
||
float smallStartCenterX = | ||
arrangement.smallCount > 0 ? start + (arrangement.smallSize / 2F) : mediumCenterX; | ||
|
||
float extraSmallTailCenterX = availableSpace + (extraSmallChildWidth / 2F); | ||
|
||
float extraSmallMask = | ||
getChildMaskPercentage(extraSmallChildWidth, arrangement.largeSize, childHorizontalMargins); | ||
float smallMask = | ||
getChildMaskPercentage( | ||
arrangement.smallSize, arrangement.largeSize, childHorizontalMargins); | ||
float mediumMask = | ||
getChildMaskPercentage( | ||
arrangement.mediumSize, arrangement.largeSize, childHorizontalMargins); | ||
float largeMask = 0F; | ||
|
||
KeylineState.Builder builder = | ||
new KeylineState.Builder(arrangement.largeSize) | ||
.addKeyline(extraSmallHeadCenterX, extraSmallMask, extraSmallChildWidth) | ||
.addKeylineRange( | ||
largeStartCenterX, largeMask, arrangement.largeSize, arrangement.largeCount, true); | ||
if (arrangement.mediumCount > 0) { | ||
builder.addKeyline(mediumCenterX, mediumMask, arrangement.mediumSize); | ||
} | ||
if (arrangement.smallCount > 0) { | ||
builder.addKeylineRange( | ||
smallStartCenterX, smallMask, arrangement.smallSize, arrangement.smallCount); | ||
} | ||
builder.addKeyline(extraSmallTailCenterX, extraSmallMask, extraSmallChildWidth); | ||
return builder.build(); | ||
} | ||
|
||
static int maxValue(int[] array) { | ||
int largest = Integer.MIN_VALUE; | ||
for (int j : array) { | ||
if (j > largest) { | ||
largest = j; | ||
} | ||
} | ||
|
||
return largest; | ||
} | ||
} |
Oops, something went wrong.