mirror of
https://github.com/material-components/material-components-android.git
synced 2026-01-20 03:51:33 +08:00
206 lines
9.4 KiB
Java
206 lines
9.4 KiB
Java
/*
|
|
* Copyright 2022 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 java.lang.Math.abs;
|
|
import static java.lang.Math.max;
|
|
import static java.lang.Math.round;
|
|
|
|
import android.content.Context;
|
|
import androidx.recyclerview.widget.RecyclerView.LayoutParams;
|
|
import android.view.View;
|
|
import androidx.annotation.NonNull;
|
|
|
|
/**
|
|
* A {@link CarouselStrategy} that knows how to size and fit large, medium and small items into a
|
|
* container to create a layout for quick browsing of multiple items at once.
|
|
*
|
|
* <p>Note that this strategy will adjust the size of large items. In order to ensure large, medium,
|
|
* and small items both fit perfectly into the available space and are numbered/arranged in a
|
|
* visually pleasing and opinionated way, this strategy finds the nearest number of large items that
|
|
* will fit into an approved arrangement that requires the least amount of size adjustment
|
|
* necessary.
|
|
*
|
|
* <p>This class will automatically be reversed by {@link CarouselLayoutManager} if being laid out
|
|
* right-to-left and does not need to make any account for layout direction itself.
|
|
*/
|
|
public final class MultiBrowseCarouselStrategy extends CarouselStrategy {
|
|
|
|
// The percentage by which a medium item needs to be larger than a small item and smaller
|
|
// than an large item. This is used to ensure a medium item is truly somewhere between the
|
|
// small and large sizes, making for a visually balanced arrangement.
|
|
// 0F would mean a medium item could be >= small item size and <= a large item size.
|
|
// .25F means the medium item must be >= 125% of the small item size and <= 75% of the
|
|
// large item size.
|
|
private static final float MEDIUM_SIZE_PERCENTAGE_DELTA = .25F;
|
|
|
|
// True if medium items should never be added and arrangements should consist of only large and
|
|
// small items. This will often result in a greater number of large items but more variability in
|
|
// large item size. This can be desirable when optimizing for the greatest number of fully
|
|
// unmasked items visible at once.
|
|
private final boolean forceCompactArrangement;
|
|
|
|
public MultiBrowseCarouselStrategy() {
|
|
this(false);
|
|
}
|
|
|
|
/**
|
|
* Create a new instance of {@link MultiBrowseCarouselStrategy}.
|
|
*
|
|
* @param forceCompactArrangement true if items should be fit in a way that maximizes the number
|
|
* of large, unmasked items. false if this strategy is free to determine an opinionated
|
|
* balance between item sizes.
|
|
*/
|
|
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 getSmallSize(@NonNull Context context) {
|
|
return context.getResources().getDimension(R.dimen.m3_carousel_small_item_size);
|
|
}
|
|
|
|
@Override
|
|
@NonNull
|
|
KeylineState onFirstChildMeasuredWithMargins(
|
|
@NonNull Carousel carousel, @NonNull View child) {
|
|
LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams();
|
|
float childHorizontalMargins = childLayoutParams.leftMargin + childLayoutParams.rightMargin;
|
|
|
|
float smallChildWidth = getSmallSize(child.getContext()) + childHorizontalMargins;
|
|
float extraSmallChildWidth = getExtraSmallSize(child.getContext()) + childHorizontalMargins;
|
|
|
|
float availableSpace = carousel.getContainerWidth();
|
|
|
|
// The minimum viable arrangement is 1 large and 1 small child. A single large item size
|
|
// cannot be greater than the available space minus a small child width.
|
|
float maxLargeChildSpace = availableSpace - smallChildWidth;
|
|
float largeChildWidth = child.getMeasuredWidth() + childHorizontalMargins;
|
|
|
|
int largeCount;
|
|
int mediumCount;
|
|
int smallCount;
|
|
float mediumChildWidth;
|
|
|
|
if (maxLargeChildSpace <= smallChildWidth) {
|
|
// There is not enough space to show a small and a large item. Remove the small item and
|
|
// default to showing a single, fullscreen item.
|
|
largeCount = 1;
|
|
largeChildWidth = availableSpace;
|
|
mediumCount = 0;
|
|
mediumChildWidth = 0;
|
|
smallCount = 0;
|
|
} else if (largeChildWidth >= maxLargeChildSpace) {
|
|
// There is only enough space to show 1 large, and 1 small item.
|
|
largeCount = 1;
|
|
largeChildWidth = maxLargeChildSpace;
|
|
mediumCount = 0;
|
|
mediumChildWidth = 0F;
|
|
smallCount = 1;
|
|
} else {
|
|
// There is enough space for some combination of large items, an optional medium item,
|
|
// and a small item. Find the arrangement where large items need to be adjusted in
|
|
// size by the least amount.
|
|
float mediumChildMinWidth =
|
|
smallChildWidth + (smallChildWidth * MEDIUM_SIZE_PERCENTAGE_DELTA);
|
|
// TODO: Ensure this is always <= expanded size even after expanded size is adjusted.
|
|
float mediumChildMaxWidth =
|
|
largeChildWidth - (largeChildWidth * MEDIUM_SIZE_PERCENTAGE_DELTA);
|
|
|
|
float largeRangeMin = availableSpace - (mediumChildMaxWidth + smallChildWidth);
|
|
float largeRangeMax = availableSpace - (mediumChildMinWidth + smallChildWidth);
|
|
|
|
// The standard arrangement is `x` large, 1 medium, and 1 small item where `x` is the
|
|
// maximum number of large items that can fit within the available space.
|
|
float standardLargeRangeCenter = (largeRangeMin + largeRangeMax) / 2;
|
|
float standardLargeQuotient = standardLargeRangeCenter / largeChildWidth;
|
|
int standardLargeCount = round(standardLargeQuotient);
|
|
float standardLargeChildWidth = largeChildWidth;
|
|
// If the largeChildWidth * count falls outside of the large min-max range, the width of
|
|
// large children for the standard arrangement needs to be adjusted. Make the smallest
|
|
// adjustment possible to bring the number of large children back to fit within the
|
|
// available space.
|
|
if (largeChildWidth * standardLargeCount < largeRangeMin) {
|
|
standardLargeChildWidth = largeRangeMin / standardLargeCount;
|
|
} else if (largeChildWidth * standardLargeCount > largeRangeMax) {
|
|
standardLargeChildWidth = largeRangeMax / standardLargeCount;
|
|
}
|
|
|
|
// The compact arrangement is `x` large, and 1 small item where `x` is the maximum
|
|
// number of large items that can fit within the available space.
|
|
float compactLargeQuotient = (availableSpace - smallChildWidth) / largeChildWidth;
|
|
int compactLargeCount = round(compactLargeQuotient);
|
|
// Adjust the largeChildWidth so largeChildWidth * largeCount fits perfectly within
|
|
// the available space.
|
|
float compactLargeChildWidth = (availableSpace - smallChildWidth) / compactLargeCount;
|
|
|
|
// Use the arrangement type which requires the large item size to be adjusted the least,
|
|
// retaining the developer specified item size as much as possible.
|
|
if (abs(largeChildWidth - standardLargeChildWidth)
|
|
<= abs(largeChildWidth - compactLargeChildWidth)
|
|
&& !forceCompactArrangement) {
|
|
largeCount = standardLargeCount;
|
|
largeChildWidth = standardLargeChildWidth;
|
|
mediumCount = 1;
|
|
mediumChildWidth = availableSpace - (largeChildWidth * largeCount) - smallChildWidth;
|
|
smallCount = 1;
|
|
} else {
|
|
largeCount = compactLargeCount;
|
|
largeChildWidth = compactLargeChildWidth;
|
|
mediumCount = 0;
|
|
mediumChildWidth = 0;
|
|
smallCount = 1;
|
|
}
|
|
}
|
|
|
|
float start = 0F;
|
|
float extraSmallHeadCenterX = start - (extraSmallChildWidth / 2F);
|
|
|
|
float largeStartCenterX = start + (largeChildWidth / 2F);
|
|
float largeEndCenterX = largeStartCenterX + (max(0, largeCount - 1) * largeChildWidth);
|
|
start = largeEndCenterX + largeChildWidth / 2F;
|
|
|
|
float mediumCenterX = mediumCount > 0 ? start + (mediumChildWidth / 2F) : largeEndCenterX;
|
|
start = mediumCount > 0 ? mediumCenterX + (mediumChildWidth / 2F) : start;
|
|
|
|
float smallStartCenterX = smallCount > 0 ? start + (smallChildWidth / 2F) : mediumCenterX;
|
|
|
|
float extraSmallTailCenterX = carousel.getContainerWidth() + (extraSmallChildWidth / 2F);
|
|
|
|
float extraSmallMask =
|
|
getChildMaskPercentage(extraSmallChildWidth, largeChildWidth, childHorizontalMargins);
|
|
float smallMask =
|
|
getChildMaskPercentage(smallChildWidth, largeChildWidth, childHorizontalMargins);
|
|
float mediumMask =
|
|
getChildMaskPercentage(mediumChildWidth, largeChildWidth, childHorizontalMargins);
|
|
float largeMask = 0F;
|
|
|
|
return new KeylineState.Builder(largeChildWidth)
|
|
.addKeyline(extraSmallHeadCenterX, extraSmallMask, extraSmallChildWidth)
|
|
.addKeylineRange(largeStartCenterX, largeMask, largeChildWidth, largeCount, true)
|
|
.addKeyline(mediumCenterX, mediumMask, mediumChildWidth)
|
|
.addKeylineRange(smallStartCenterX, smallMask, smallChildWidth, smallCount)
|
|
.addKeyline(extraSmallTailCenterX, extraSmallMask, extraSmallChildWidth)
|
|
.build();
|
|
}
|
|
}
|