diff --git a/lib/java/com/google/android/material/carousel/Arrangement.java b/lib/java/com/google/android/material/carousel/Arrangement.java new file mode 100644 index 00000000000..58f0da1be0e --- /dev/null +++ b/lib/java/com/google/android/material/carousel/Arrangement.java @@ -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}. + * + *
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}. + * + *
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}. + * + *
This method solves the following equation for largeSize: + * + *
{@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. + * + *
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; + } +} diff --git a/lib/java/com/google/android/material/carousel/CarouselStrategyHelper.java b/lib/java/com/google/android/material/carousel/CarouselStrategyHelper.java new file mode 100644 index 00000000000..60b720976b1 --- /dev/null +++ b/lib/java/com/google/android/material/carousel/CarouselStrategyHelper.java @@ -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; + } +} diff --git a/lib/java/com/google/android/material/carousel/MultiBrowseCarouselStrategy.java b/lib/java/com/google/android/material/carousel/MultiBrowseCarouselStrategy.java index 34bb1a911f9..2240203b3c3 100644 --- a/lib/java/com/google/android/material/carousel/MultiBrowseCarouselStrategy.java +++ b/lib/java/com/google/android/material/carousel/MultiBrowseCarouselStrategy.java @@ -16,21 +16,20 @@ package com.google.android.material.carousel; -import com.google.android.material.R; - -import static java.lang.Math.abs; +import static com.google.android.material.carousel.CarouselStrategyHelper.createLeftAlignedKeylineState; +import static com.google.android.material.carousel.CarouselStrategyHelper.getSmallSizeMax; +import static com.google.android.material.carousel.CarouselStrategyHelper.getSmallSizeMin; +import static com.google.android.material.carousel.CarouselStrategyHelper.maxValue; import static java.lang.Math.ceil; import static java.lang.Math.floor; import static java.lang.Math.max; import static java.lang.Math.min; -import android.content.Context; import androidx.recyclerview.widget.RecyclerView.LayoutParams; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo.Scope; -import androidx.annotation.VisibleForTesting; import androidx.core.math.MathUtils; /** @@ -48,10 +47,6 @@ */ public final class MultiBrowseCarouselStrategy extends CarouselStrategy { - // 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; - private static final int[] SMALL_COUNTS = new int[] {1}; private static final int[] MEDIUM_COUNTS = new int[] {1, 0}; private static final int[] MEDIUM_COUNTS_COMPACT = new int[] {0}; @@ -80,18 +75,6 @@ public MultiBrowseCarouselStrategy(boolean forceCompactArrangement) { this.forceCompactArrangement = forceCompactArrangement; } - private float getExtraSmallSize(@NonNull Context context) { - return context.getResources().getDimension(R.dimen.m3_carousel_gone_size); - } - - private float getSmallSizeMin(@NonNull Context context) { - return context.getResources().getDimension(R.dimen.m3_carousel_small_item_size_min); - } - - private float getSmallSizeMax(@NonNull Context context) { - return context.getResources().getDimension(R.dimen.m3_carousel_small_item_size_max); - } - @Override @NonNull KeylineState onFirstChildMeasuredWithMargins(@NonNull Carousel carousel, @NonNull View child) { @@ -135,324 +118,21 @@ KeylineState onFirstChildMeasuredWithMargins(@NonNull Carousel carousel, @NonNul largeCounts[i] = largeCountMax - i; } - Arrangement arrangement = - findLowestCostArrangement( - availableSpace, - targetSmallChildWidth, - smallChildWidthMin, - smallChildWidthMax, - smallCounts, - targetMediumChildWidth, - mediumCounts, - targetLargeChildWidth, - largeCounts); - - float extraSmallChildWidth = getExtraSmallSize(child.getContext()) + 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 = carousel.getContainerWidth() + (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(); - } - - /** - * Create an arrangement for all possible permutations for {@code smallCounts}, {@code - * mediumCounts}, and {@code largeCounts}, fit each into the available space, and return the - * arrangement with the lowest cost. - * - *
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 arrangmenet 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 - * @param targetMediumSize the size medium items would like to be - * @param mediumCounts an array of medium item counts for a valid arrangement - * @param targetLargeSize the size large items would like to be - * @param largeCounts an array of large item counts for a valid arrangement - * @return the arrangement that is considered the most desirable and has been adjusted to fit - * within the available space - */ - private 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; - } - - private static int maxValue(int[] array) { - int largest = Integer.MIN_VALUE; - for (int j : array) { - if (j > largest) { - largest = j; - } - } - - return largest; - } - - /** - * An object 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. - */ - @VisibleForTesting - static final class Arrangement { - 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}. - * - *
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}. - * - *
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 medium items can be - */ - 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}. - * - *
This method solves the following equation for largeSize: - * - *
{@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; - } + Arrangement arrangement = Arrangement.findLowestCostArrangement( + availableSpace, + targetSmallChildWidth, + smallChildWidthMin, + smallChildWidthMax, + smallCounts, + targetMediumChildWidth, + mediumCounts, + targetLargeChildWidth, + largeCounts); + + return createLeftAlignedKeylineState( + child.getContext(), + childHorizontalMargins, + availableSpace, + arrangement); } } diff --git a/lib/javatests/com/google/android/material/carousel/ArrangementTest.java b/lib/javatests/com/google/android/material/carousel/ArrangementTest.java index 4c6233f2f37..c69222c36e6 100644 --- a/lib/javatests/com/google/android/material/carousel/ArrangementTest.java +++ b/lib/javatests/com/google/android/material/carousel/ArrangementTest.java @@ -17,13 +17,12 @@ import static com.google.common.truth.Truth.assertThat; -import com.google.android.material.carousel.MultiBrowseCarouselStrategy.Arrangement; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.internal.DoNotInstrument; -/** Tests for {@link MultiBrowseCarouselStrategy.Arrangement}. */ +/** Tests for {@link Arrangement}. */ @RunWith(RobolectricTestRunner.class) @DoNotInstrument public final class ArrangementTest {