Skip to content

Commit cbb380d

Browse files
imhappidrchen
authored andcommittedOct 10, 2023
[Carousel] Add logic for multibrowse strategy to change strategy when number of items is less than the number of keylines
Resolves #3598 PiperOrigin-RevId: 572078262
1 parent 0356f24 commit cbb380d

File tree

4 files changed

+95
-14
lines changed

4 files changed

+95
-14
lines changed
 

‎lib/java/com/google/android/material/carousel/Arrangement.java

+8-2
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ final class Arrangement {
3535

3636
final int priority;
3737
float smallSize;
38-
final int smallCount;
39-
final int mediumCount;
38+
int smallCount;
39+
int mediumCount;
4040
float mediumSize;
4141
float largeSize;
4242
final int largeCount;
@@ -140,6 +140,8 @@ private void fit(
140140
smallSize += max(delta / smallCount, minSmallSize - smallSize);
141141
}
142142

143+
// Zero out small size if there are no small items
144+
smallSize = smallCount > 0 ? smallSize : 0F;
143145
largeSize =
144146
calculateLargeSize(availableSpace, smallCount, smallSize, mediumCount, largeCount);
145147
mediumSize = (largeSize + smallSize) / 2F;
@@ -278,4 +280,8 @@ static Arrangement findLowestCostArrangement(
278280
}
279281
return lowestCostArrangement;
280282
}
283+
284+
int getItemCount() {
285+
return smallCount + mediumCount + largeCount;
286+
}
281287
}

‎lib/java/com/google/android/material/carousel/CarouselLayoutManager.java

+2-12
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ public void onLayoutChildren(Recycler recycler, State state) {
303303

304304
detachAndScrapAttachedViews(recycler);
305305
fill(recycler, state);
306+
lastItemCount = getItemCount();
306307
}
307308

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

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

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

14361425
private void updateItemCount() {
14371426
int newItemCount = getItemCount();
1427+
14381428
if (newItemCount == this.lastItemCount || keylineStateList == null) {
14391429
return;
14401430
}

‎lib/java/com/google/android/material/carousel/MultiBrowseCarouselStrategy.java

+48
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ public final class MultiBrowseCarouselStrategy extends CarouselStrategy {
5353
private static final int[] SMALL_COUNTS = new int[] {1};
5454
private static final int[] MEDIUM_COUNTS = new int[] {1, 0};
5555

56+
// Current count of number of keylines. We want to refresh the strategy if there are less items
57+
// than this number.
58+
private int keylineCount = 0;
59+
5660
@Override
5761
@NonNull
5862
KeylineState onFirstChildMeasuredWithMargins(@NonNull Carousel carousel, @NonNull View child) {
@@ -128,11 +132,55 @@ KeylineState onFirstChildMeasuredWithMargins(@NonNull Carousel carousel, @NonNul
128132
targetLargeChildSize,
129133
largeCounts);
130134

135+
keylineCount = arrangement.getItemCount();
136+
137+
if (ensureArrangementFitsItemCount(arrangement, carousel.getItemCount())) {
138+
// In case counts changed after ensuring the previous arrangement fit the item
139+
// counts, we call `findLowestCostArrangement` again with the item counts set.
140+
arrangement =
141+
Arrangement.findLowestCostArrangement(
142+
availableSpace,
143+
targetSmallChildSize,
144+
smallChildSizeMin,
145+
smallChildSizeMax,
146+
new int[] {arrangement.smallCount},
147+
targetMediumChildSize,
148+
new int[] {arrangement.mediumCount},
149+
targetLargeChildSize,
150+
new int[] {arrangement.largeCount});
151+
}
152+
131153
return createKeylineState(
132154
child.getContext(),
133155
childMargins,
134156
availableSpace,
135157
arrangement,
136158
carousel.getCarouselAlignment());
137159
}
160+
161+
boolean ensureArrangementFitsItemCount(Arrangement arrangement, int carouselItemCount) {
162+
int keylineSurplus = arrangement.getItemCount() - carouselItemCount;
163+
boolean changed =
164+
keylineSurplus > 0 && (arrangement.smallCount > 0 || arrangement.mediumCount > 1);
165+
166+
while (keylineSurplus > 0) {
167+
if (arrangement.smallCount > 0) {
168+
arrangement.smallCount -= 1;
169+
} else if (arrangement.mediumCount > 1) {
170+
// Keep at least 1 medium so the large items don't fill the entire carousel in new strategy.
171+
arrangement.mediumCount -= 1;
172+
}
173+
// large items don't need to be removed even if they are a surplus because large items
174+
// are already fully unmasked.
175+
keylineSurplus -= 1;
176+
}
177+
178+
return changed;
179+
}
180+
181+
@Override
182+
boolean shouldRefreshKeylineState(Carousel carousel, int oldItemCount) {
183+
return (oldItemCount < keylineCount && carousel.getItemCount() >= keylineCount)
184+
|| (oldItemCount >= keylineCount && carousel.getItemCount() < keylineCount);
185+
}
138186
}

‎lib/javatests/com/google/android/material/carousel/MultiBrowseCarouselStrategyTest.java

+37
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import com.google.android.material.test.R;
1919

2020
import static com.google.android.material.carousel.CarouselHelper.createCarousel;
21+
import static com.google.android.material.carousel.CarouselHelper.createCarouselWithItemCount;
2122
import static com.google.android.material.carousel.CarouselHelper.createCarouselWithWidth;
2223
import static com.google.android.material.carousel.CarouselHelper.createViewWithSize;
2324
import static com.google.common.truth.Truth.assertThat;
@@ -238,4 +239,40 @@ public void testKnownCenterAlignmentArrangement_correctlyCalculatesKeylineLocati
238239
assertThat(keylines.get(i).locOffset).isEqualTo(locOffsets[i]);
239240
}
240241
}
242+
243+
@Test
244+
public void testLessItemsThanKeylines_updatesStrategy() {
245+
MultiBrowseCarouselStrategy config = new MultiBrowseCarouselStrategy();
246+
View view = createViewWithSize(ApplicationProvider.getApplicationContext(), 200, 200);
247+
248+
// With a carousel of size 500 and large item size of 200, the keylines will be
249+
// {xsmall, large, large, medium, small, xsmall}
250+
Carousel carousel =
251+
createCarouselWithItemCount(
252+
/* size= */ 500, CarouselLayoutManager.ALIGNMENT_START, /* itemCount= */ 4);
253+
KeylineState keylineState =
254+
config.onFirstChildMeasuredWithMargins(carousel, view);
255+
256+
// An item count of 4 should not affect the keyline number.
257+
assertThat(keylineState.getKeylines()).hasSize(6);
258+
259+
carousel =
260+
createCarouselWithItemCount(
261+
/* size= */ 500, CarouselLayoutManager.ALIGNMENT_START, /* itemCount= */ 3);
262+
keylineState =
263+
config.onFirstChildMeasuredWithMargins(carousel, view);
264+
265+
// An item count of 3 should change the keyline number to be 3: {xsmall, large, large, medium,
266+
// xsmall}
267+
assertThat(keylineState.getKeylines()).hasSize(5);
268+
269+
carousel = createCarouselWithItemCount(500, CarouselLayoutManager.ALIGNMENT_START, 2);
270+
keylineState =
271+
config.onFirstChildMeasuredWithMargins(carousel, view);
272+
273+
// An item count of 2 should have the keyline number to be 5:
274+
// {xsmall, large, large, medium, xsmall} because even with only 2 items, we still want a medium
275+
// keyline so the carousel is not just large items.
276+
assertThat(keylineState.getKeylines()).hasSize(5);
277+
}
241278
}

0 commit comments

Comments
 (0)
Please sign in to comment.