mirror of
https://github.com/material-components/material-components-android.git
synced 2026-01-16 09:52:53 +08:00
293 lines
12 KiB
Java
293 lines
12 KiB
Java
/*
|
|
* 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.annotation.Nullable;
|
|
import androidx.annotation.RestrictTo;
|
|
import androidx.annotation.RestrictTo.Scope;
|
|
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.
|
|
*/
|
|
@RestrictTo(Scope.LIBRARY_GROUP)
|
|
public 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;
|
|
int smallCount;
|
|
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
|
|
*/
|
|
public 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);
|
|
}
|
|
|
|
// Zero out small size if there are no small items
|
|
smallSize = smallCount > 0 ? smallSize : 0F;
|
|
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
|
|
*/
|
|
@Nullable
|
|
public static Arrangement findLowestCostArrangement(
|
|
float availableSpace,
|
|
float targetSmallSize,
|
|
float minSmallSize,
|
|
float maxSmallSize,
|
|
@NonNull int[] smallCounts,
|
|
float targetMediumSize,
|
|
@NonNull int[] mediumCounts,
|
|
float targetLargeSize,
|
|
@NonNull 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;
|
|
}
|
|
|
|
int getItemCount() {
|
|
return smallCount + mediumCount + largeCount;
|
|
}
|
|
}
|