Skip to content

Commit ff52862

Browse files
imhappipekingme
authored andcommittedJul 27, 2023
[Carousel] Add a layout listener to recyclerview to refresh keyline state upon size change
PiperOrigin-RevId: 551280769
1 parent 0171624 commit ff52862

File tree

2 files changed

+45
-32
lines changed

2 files changed

+45
-32
lines changed
 

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

+43-24
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import android.util.AttributeSet;
4141
import android.util.Log;
4242
import android.view.View;
43+
import android.view.View.OnLayoutChangeListener;
4344
import android.view.ViewGroup;
4445
import android.view.accessibility.AccessibilityEvent;
4546
import androidx.annotation.IntDef;
@@ -115,14 +116,18 @@ public class CarouselLayoutManager extends LayoutManager
115116

116117
private CarouselOrientationHelper orientationHelper;
117118

118-
/**
119-
* Aligns large items to the start of the carousel.
120-
*/
119+
private final OnLayoutChangeListener recyclerViewSizeChangeListener =
120+
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
121+
// If RV dimen values have changed, refresh the keyline state.
122+
if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) {
123+
v.post(this::refreshKeylineState);
124+
}
125+
};
126+
127+
/** Aligns large items to the start of the carousel. */
121128
public static final int ALIGNMENT_START = 0;
122129

123-
/**
124-
* Aligns large items to the center of the carousel.
125-
*/
130+
/** Aligns large items to the center of the carousel. */
126131
public static final int ALIGNMENT_CENTER = 1;
127132

128133
/**
@@ -226,9 +231,14 @@ public void setCarouselStrategy(@NonNull CarouselStrategy carouselStrategy) {
226231
@Override
227232
public void onAttachedToWindow(RecyclerView view) {
228233
super.onAttachedToWindow(view);
229-
// TODO: b/291123558 - Keylines should be also be refreshed when
230-
// there's any change affecting dimens of the RecyclerView.
231234
refreshKeylineState();
235+
view.addOnLayoutChangeListener(recyclerViewSizeChangeListener);
236+
}
237+
238+
@Override
239+
public void onDetachedFromWindow(RecyclerView view, Recycler recycler) {
240+
super.onDetachedFromWindow(view, recycler);
241+
view.removeOnLayoutChangeListener(recyclerViewSizeChangeListener);
232242
}
233243

234244
@Override
@@ -241,16 +251,11 @@ public void onLayoutChildren(Recycler recycler, State state) {
241251

242252
boolean isRtl = isLayoutRtl();
243253

254+
boolean isInitialLoad = keylineStateList == null;
244255
// If a keyline state hasn't been created, use the first child as a representative of how each
245256
// child would like to be measured and allow the strategy to create a keyline state.
246-
boolean isInitialLoad = keylineStateList == null;
247257
if (isInitialLoad) {
248-
View firstChild = recycler.getViewForPosition(0);
249-
measureChildWithMargins(firstChild, 0, 0);
250-
KeylineState keylineState =
251-
carouselStrategy.onFirstChildMeasuredWithMargins(this, firstChild);
252-
keylineStateList =
253-
KeylineStateList.from(this, isRtl ? KeylineState.reverse(keylineState) : keylineState);
258+
recalculateKeylineStateList(recycler);
254259
}
255260

256261
// Ensure our scroll limits are initialized and valid for the data set size.
@@ -277,12 +282,21 @@ public void onLayoutChildren(Recycler recycler, State state) {
277282
// Ensure currentFillStartPosition is valid if the number of items in the adapter has changed.
278283
currentFillStartPosition = MathUtils.clamp(currentFillStartPosition, 0, state.getItemCount());
279284

280-
updateCurrentKeylineStateForScrollOffset();
285+
updateCurrentKeylineStateForScrollOffset(keylineStateList);
281286

282287
detachAndScrapAttachedViews(recycler);
283288
fill(recycler, state);
284289
}
285290

291+
private void recalculateKeylineStateList(Recycler recycler) {
292+
View firstChild = recycler.getViewForPosition(0);
293+
measureChildWithMargins(firstChild, 0, 0);
294+
KeylineState keylineState = carouselStrategy.onFirstChildMeasuredWithMargins(this, firstChild);
295+
keylineStateList =
296+
KeylineStateList.from(
297+
this, isLayoutRtl() ? KeylineState.reverse(keylineState) : keylineState);
298+
}
299+
286300
/**
287301
* Recalculates the {@link KeylineState} and {@link KeylineStateList} for the current strategy.
288302
*/
@@ -683,7 +697,8 @@ private static KeylineRange getSurroundingKeylineRange(
683697
*
684698
* <p>This method should be called any time a change in the scroll offset occurs.
685699
*/
686-
private void updateCurrentKeylineStateForScrollOffset() {
700+
private void updateCurrentKeylineStateForScrollOffset(
701+
@NonNull KeylineStateList keylineStateList) {
687702
if (maxScroll <= minScroll) {
688703
// We don't have enough items in the list to scroll and we should use the keyline state
689704
// that aligns items to the start of the container.
@@ -697,9 +712,9 @@ private void updateCurrentKeylineStateForScrollOffset() {
697712
}
698713

699714
/**
700-
* Calculates the horizontal distance children should be scrolled by for a given {@code dx}.
715+
* Calculates the distance children should be scrolled by for a given {@code delta}.
701716
*
702-
* @param dx the delta, resulting from a gesture or other event, that the list would like to be
717+
* @param delta the delta, resulting from a gesture or other event, that the list would like to be
703718
* scrolled by
704719
* @param currentScroll the current horizontal scroll offset that is always between the min and
705720
* max horizontal scroll
@@ -724,7 +739,7 @@ private static int calculateShouldScrollBy(
724739
* Calculates the total offset needed to scroll the first item in the list to the center of the
725740
* start focal keyline.
726741
*/
727-
private int calculateStartScroll(KeylineStateList stateList) {
742+
private int calculateStartScroll(@NonNull KeylineStateList stateList) {
728743
boolean isRtl = isLayoutRtl();
729744
KeylineState startState = isRtl ? stateList.getEndState() : stateList.getStartState();
730745
Keyline startFocalKeyline =
@@ -1106,7 +1121,7 @@ public void scrollToPosition(int position) {
11061121
}
11071122
scrollOffset = getScrollOffsetForPosition(position, getKeylineStateForPosition(position));
11081123
currentFillStartPosition = MathUtils.clamp(position, 0, max(0, getItemCount() - 1));
1109-
updateCurrentKeylineStateForScrollOffset();
1124+
updateCurrentKeylineStateForScrollOffset(keylineStateList);
11101125
requestLayout();
11111126
}
11121127

@@ -1211,10 +1226,14 @@ private int scrollBy(int distance, Recycler recycler, State state) {
12111226
return 0;
12121227
}
12131228

1229+
if (keylineStateList == null) {
1230+
recalculateKeylineStateList(recycler);
1231+
}
1232+
12141233
// Calculate how much the carousel should scroll and update the scroll offset.
12151234
int scrolledBy = calculateShouldScrollBy(distance, scrollOffset, minScroll, maxScroll);
12161235
scrollOffset += scrolledBy;
1217-
updateCurrentKeylineStateForScrollOffset();
1236+
updateCurrentKeylineStateForScrollOffset(keylineStateList);
12181237

12191238
float halfItemSize = currentKeylineState.getItemSize() / 2F;
12201239
int startPosition = getPosition(getChildAt(0));
@@ -1278,7 +1297,7 @@ public int computeHorizontalScrollOffset(@NonNull State state) {
12781297
*/
12791298
@Override
12801299
public int computeHorizontalScrollExtent(@NonNull State state) {
1281-
return (int) keylineStateList.getDefaultState().getItemSize();
1300+
return keylineStateList == null ? 0 : (int) keylineStateList.getDefaultState().getItemSize();
12821301
}
12831302

12841303
/**
@@ -1300,7 +1319,7 @@ public int computeVerticalScrollOffset(@NonNull State state) {
13001319

13011320
@Override
13021321
public int computeVerticalScrollExtent(@NonNull State state) {
1303-
return (int) keylineStateList.getDefaultState().getItemSize();
1322+
return keylineStateList == null ? 0 : (int) keylineStateList.getDefaultState().getItemSize();
13041323
}
13051324

13061325
@Override

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

+2-8
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import org.junit.runner.RunWith;
4444
import org.robolectric.Robolectric;
4545
import org.robolectric.RobolectricTestRunner;
46+
import org.robolectric.annotation.Config;
4647

4748
/** RTL tests for {@link CarouselLayoutManager}. */
4849
@RunWith(RobolectricTestRunner.class)
@@ -87,17 +88,10 @@ public void testFirstAdapterItem_isDrawnAtRightOfContainer() throws Throwable {
8788
assertThat(firstChild.getRight()).isEqualTo(DEFAULT_RECYCLER_VIEW_WIDTH);
8889
}
8990

91+
@Config(qualifiers = "sw1320dp-w1320dp")
9092
@Test
9193
public void testScrollBeyondMaxHorizontalScroll_shouldLimitToMaxScrollOffset() throws Throwable {
9294
KeylineState keylineState = getTestCenteredKeylineState();
93-
layoutManager.setCarouselStrategy(
94-
new CarouselStrategy() {
95-
@Override
96-
KeylineState onFirstChildMeasuredWithMargins(
97-
@NonNull Carousel carousel, @NonNull View child) {
98-
return keylineState;
99-
}
100-
});
10195
setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(10));
10296
scrollToPosition(recyclerView, layoutManager, 200);
10397

0 commit comments

Comments
 (0)
Please sign in to comment.