Skip to content

Commit

Permalink
[Carousel] Add logic for multibrowse strategy to change strategy when…
Browse files Browse the repository at this point in the history
… number of items is less than the number of keylines

Resolves #3598

PiperOrigin-RevId: 572078262
  • Loading branch information
imhappi authored and drchen committed Oct 10, 2023
1 parent 0356f24 commit cbb380d
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 14 deletions.
10 changes: 8 additions & 2 deletions lib/java/com/google/android/material/carousel/Arrangement.java
Expand Up @@ -35,8 +35,8 @@ final class Arrangement {

final int priority;
float smallSize;
final int smallCount;
final int mediumCount;
int smallCount;
int mediumCount;
float mediumSize;
float largeSize;
final int largeCount;
Expand Down Expand Up @@ -140,6 +140,8 @@ private void fit(
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;
Expand Down Expand Up @@ -278,4 +280,8 @@ static Arrangement findLowestCostArrangement(
}
return lowestCostArrangement;
}

int getItemCount() {
return smallCount + mediumCount + largeCount;
}
}
Expand Up @@ -303,6 +303,7 @@ public void onLayoutChildren(Recycler recycler, State state) {

detachAndScrapAttachedViews(recycler);
fill(recycler, state);
lastItemCount = getItemCount();
}

private void recalculateKeylineStateList(Recycler recycler) {
Expand Down Expand Up @@ -784,25 +785,13 @@ private int calculateEndScroll(State state, KeylineStateList stateList) {
KeylineState endState = isRtl ? stateList.getStartState() : stateList.getEndState();
Keyline endFocalKeyline =
isRtl ? endState.getFirstFocalKeyline() : endState.getLastFocalKeyline();
Keyline firstNonAnchorKeyline =
isRtl ? endState.getLastNonAnchorKeyline() : endState.getFirstNonAnchorKeyline();
// Get the total distance from the first item to the last item in the end-to-end model
float lastItemDistanceFromFirstItem =
(((state.getItemCount() - 1) * endState.getItemSize()) + getPaddingEnd())
* (isRtl ? -1F : 1F);

float endFocalLocDistanceFromStart = endFocalKeyline.loc - getParentStart();
float endFocalLocDistanceFromEnd = getParentEnd() - endFocalKeyline.loc;
if (firstNonAnchorKeyline == null
|| (firstNonAnchorKeyline.cutoff == 0
&& abs(endFocalLocDistanceFromStart) > abs(lastItemDistanceFromFirstItem))) {
// For keyline states that do not have a cutoff, this means that the last item comes before
// the last focal keyline which means all items should be within the focal range and there
// is nowhere to scroll. For keyline states that do have a cutoff, this does not hold true
// since an item is not guaranteed to be fully in the focal range so we calculate the end
// scroll amount normally.
return 0;
}

// We want the last item in the list to only be able to scroll to the end of the list. Subtract
// the distance to the end focal keyline and then add the distance needed to let the last
Expand Down Expand Up @@ -1435,6 +1424,7 @@ public void onItemsRemoved(@NonNull RecyclerView recyclerView, int positionStart

private void updateItemCount() {
int newItemCount = getItemCount();

if (newItemCount == this.lastItemCount || keylineStateList == null) {
return;
}
Expand Down
Expand Up @@ -53,6 +53,10 @@ public final class MultiBrowseCarouselStrategy extends CarouselStrategy {
private static final int[] SMALL_COUNTS = new int[] {1};
private static final int[] MEDIUM_COUNTS = new int[] {1, 0};

// Current count of number of keylines. We want to refresh the strategy if there are less items
// than this number.
private int keylineCount = 0;

@Override
@NonNull
KeylineState onFirstChildMeasuredWithMargins(@NonNull Carousel carousel, @NonNull View child) {
Expand Down Expand Up @@ -128,11 +132,55 @@ KeylineState onFirstChildMeasuredWithMargins(@NonNull Carousel carousel, @NonNul
targetLargeChildSize,
largeCounts);

keylineCount = arrangement.getItemCount();

if (ensureArrangementFitsItemCount(arrangement, carousel.getItemCount())) {
// In case counts changed after ensuring the previous arrangement fit the item
// counts, we call `findLowestCostArrangement` again with the item counts set.
arrangement =
Arrangement.findLowestCostArrangement(
availableSpace,
targetSmallChildSize,
smallChildSizeMin,
smallChildSizeMax,
new int[] {arrangement.smallCount},
targetMediumChildSize,
new int[] {arrangement.mediumCount},
targetLargeChildSize,
new int[] {arrangement.largeCount});
}

return createKeylineState(
child.getContext(),
childMargins,
availableSpace,
arrangement,
carousel.getCarouselAlignment());
}

boolean ensureArrangementFitsItemCount(Arrangement arrangement, int carouselItemCount) {
int keylineSurplus = arrangement.getItemCount() - carouselItemCount;
boolean changed =
keylineSurplus > 0 && (arrangement.smallCount > 0 || arrangement.mediumCount > 1);

while (keylineSurplus > 0) {
if (arrangement.smallCount > 0) {
arrangement.smallCount -= 1;
} else if (arrangement.mediumCount > 1) {
// Keep at least 1 medium so the large items don't fill the entire carousel in new strategy.
arrangement.mediumCount -= 1;
}
// large items don't need to be removed even if they are a surplus because large items
// are already fully unmasked.
keylineSurplus -= 1;
}

return changed;
}

@Override
boolean shouldRefreshKeylineState(Carousel carousel, int oldItemCount) {
return (oldItemCount < keylineCount && carousel.getItemCount() >= keylineCount)
|| (oldItemCount >= keylineCount && carousel.getItemCount() < keylineCount);
}
}
Expand Up @@ -18,6 +18,7 @@
import com.google.android.material.test.R;

import static com.google.android.material.carousel.CarouselHelper.createCarousel;
import static com.google.android.material.carousel.CarouselHelper.createCarouselWithItemCount;
import static com.google.android.material.carousel.CarouselHelper.createCarouselWithWidth;
import static com.google.android.material.carousel.CarouselHelper.createViewWithSize;
import static com.google.common.truth.Truth.assertThat;
Expand Down Expand Up @@ -238,4 +239,40 @@ public void testKnownCenterAlignmentArrangement_correctlyCalculatesKeylineLocati
assertThat(keylines.get(i).locOffset).isEqualTo(locOffsets[i]);
}
}

@Test
public void testLessItemsThanKeylines_updatesStrategy() {
MultiBrowseCarouselStrategy config = new MultiBrowseCarouselStrategy();
View view = createViewWithSize(ApplicationProvider.getApplicationContext(), 200, 200);

// With a carousel of size 500 and large item size of 200, the keylines will be
// {xsmall, large, large, medium, small, xsmall}
Carousel carousel =
createCarouselWithItemCount(
/* size= */ 500, CarouselLayoutManager.ALIGNMENT_START, /* itemCount= */ 4);
KeylineState keylineState =
config.onFirstChildMeasuredWithMargins(carousel, view);

// An item count of 4 should not affect the keyline number.
assertThat(keylineState.getKeylines()).hasSize(6);

carousel =
createCarouselWithItemCount(
/* size= */ 500, CarouselLayoutManager.ALIGNMENT_START, /* itemCount= */ 3);
keylineState =
config.onFirstChildMeasuredWithMargins(carousel, view);

// An item count of 3 should change the keyline number to be 3: {xsmall, large, large, medium,
// xsmall}
assertThat(keylineState.getKeylines()).hasSize(5);

carousel = createCarouselWithItemCount(500, CarouselLayoutManager.ALIGNMENT_START, 2);
keylineState =
config.onFirstChildMeasuredWithMargins(carousel, view);

// An item count of 2 should have the keyline number to be 5:
// {xsmall, large, large, medium, xsmall} because even with only 2 items, we still want a medium
// keyline so the carousel is not just large items.
assertThat(keylineState.getKeylines()).hasSize(5);
}
}

0 comments on commit cbb380d

Please sign in to comment.