4242import androidx .annotation .Nullable ;
4343import androidx .annotation .RestrictTo ;
4444import androidx .annotation .RestrictTo .Scope ;
45+ import androidx .annotation .VisibleForTesting ;
4546import androidx .core .graphics .ColorUtils ;
4647import androidx .core .math .MathUtils ;
4748import androidx .core .util .Preconditions ;
5051import java .util .ArrayList ;
5152import java .util .Collections ;
5253import java .util .List ;
54+ import java .util .Map ;
5355
5456/**
5557 * A {@link LayoutManager} that can mask and offset items along the scrolling axis, creating a
@@ -67,16 +69,19 @@ public class CarouselLayoutManager extends LayoutManager
6769
6870 private static final String TAG = "CarouselLayoutManager" ;
6971
70- private int horizontalScrollOffset ;
72+ @ VisibleForTesting
73+ int horizontalScrollOffset ;
7174
7275 // Min scroll is the offset number that offsets the list to the right/bottom as much as possible.
7376 // In LTR layouts, this will be the scroll offset to move to the start of the container. In RTL,
7477 // this will move the list to the end of the container.
75- private int minHorizontalScroll ;
78+ @ VisibleForTesting
79+ int minHorizontalScroll ;
7680 // Max scroll is the offset number that moves the list to the left/top of the list as much as
7781 // possible. In LTR layouts, this will move the list to the end of the container. In RTL, this
7882 // will move the list to the start of the container.
79- private int maxHorizontalScroll ;
83+ @ VisibleForTesting
84+ int maxHorizontalScroll ;
8085
8186 private boolean isDebuggingEnabled = false ;
8287 private final DebugItemDecoration debugItemDecoration = new DebugItemDecoration ();
@@ -90,6 +95,9 @@ public class CarouselLayoutManager extends LayoutManager
9095 // number of loop iterations to fill the RecyclerView.
9196 private int currentFillStartPosition = 0 ;
9297
98+ // Tracks the keyline state associated with each item in the RecyclerView.
99+ @ Nullable private Map <Integer , KeylineState > keylineStatePositionMap ;
100+
93101 /**
94102 * An internal object used to store and run checks on a child to be potentially added to the
95103 * RecyclerView and laid out.
@@ -176,6 +184,12 @@ public void onLayoutChildren(Recycler recycler, State state) {
176184 if (isInitialLoad ) {
177185 // Scroll to the start of the list on first load.
178186 horizontalScrollOffset = startHorizontalScroll ;
187+ keylineStatePositionMap =
188+ keylineStateList .getKeylineStateForPositionMap (
189+ getItemCount (),
190+ minHorizontalScroll ,
191+ maxHorizontalScroll ,
192+ isLayoutRtl ());
179193 } else {
180194 // Clamp the horizontal scroll offset by the new min and max by pinging the scroll by
181195 // calculator with a 0 delta.
@@ -885,9 +899,11 @@ public void onInitializeAccessibilityEvent(@NonNull AccessibilityEvent event) {
885899 * position}'s center at the start-most focal keyline. The returned value might be less or greater
886900 * than the min and max scroll offsets but this will be clamped in {@link #scrollBy(int, Recycler,
887901 * State)} (Recycler, State)} by {@link #calculateShouldHorizontallyScrollBy(int, int, int, int)}.
902+ *
903+ * @param position The position to get the scroll offset to.
904+ * @param keylineState The keyline state in which to calculate the scroll offset to.
888905 */
889- private int getScrollOffsetForPosition (int position ) {
890- KeylineState keylineState = keylineStateList .getDefaultState ();
906+ private int getScrollOffsetForPosition (int position , KeylineState keylineState ) {
891907 if (isLayoutRtl ()) {
892908 return (int )
893909 ((getContainerWidth () - keylineState .getLastFocalKeyline ().loc )
@@ -908,7 +924,8 @@ public PointF computeScrollVectorForPosition(int targetPosition) {
908924 return null ;
909925 }
910926
911- return new PointF (getOffsetToScrollToPosition (targetPosition ), 0F );
927+ KeylineState keylineForScroll = getKeylineStateForPosition (targetPosition );
928+ return new PointF (getOffsetToScrollToPosition (targetPosition , keylineForScroll ), 0F );
912929 }
913930
914931 /**
@@ -917,17 +934,60 @@ public PointF computeScrollVectorForPosition(int targetPosition) {
917934 * <p>This will calculate the horizontal scroll offset needed to place a child at {@code
918935 * position}'s center at the start-most focal keyline.
919936 */
920- int getOffsetToScrollToPosition (int position ) {
921- int targetScrollOffset = getScrollOffsetForPosition (position );
937+ int getOffsetToScrollToPosition (int position , @ NonNull KeylineState keylineState ) {
938+ int targetScrollOffset = getScrollOffsetForPosition (position , keylineState );
922939 return targetScrollOffset - horizontalScrollOffset ;
923940 }
924941
942+ /**
943+ * Gets the offset needed to snap to a position from the current scroll offset.
944+ *
945+ * <p>This will calculate the horizontal scroll offset needed to place a child at {@code
946+ * position}'s center at the start-most focal keyline of the target keyline state to snap to.
947+ *
948+ * <p>Sometimes we may want to do a partial snap. Eg. When there is a fling event, the snap
949+ * distance is fetched before it finishes scrolling and the target keyline state is not yet
950+ * updated. Once the fling event finishes scrolling, the snap is triggered again with the correct
951+ * target keyline state. If {@code partialSnap} is true, then we want to snap to whichever is
952+ * smaller between {@code targetKeylineStateForSnap}, which is the closest keyline state step to
953+ * the current keyline state, or the KeylineState at the correct position in {@code
954+ * keylineStatePositionList}. Note that if there is any distance left to be snapped when the
955+ * fling-scroll stops, the snap helper will handle it.
956+ */
957+ int getOffsetToScrollToPositionForSnap (int position , boolean partialSnap ) {
958+ KeylineState targetKeylineStateForSnap = keylineStateList .getShiftedState (
959+ horizontalScrollOffset , minHorizontalScroll , maxHorizontalScroll , true );
960+ int targetSnapOffset = getOffsetToScrollToPosition (position , targetKeylineStateForSnap );
961+ int positionOffset = targetSnapOffset ;
962+ if (keylineStatePositionMap != null ) {
963+ positionOffset = getOffsetToScrollToPosition (position , getKeylineStateForPosition (position ));
964+ }
965+ if (partialSnap ) {
966+ return Math .abs (positionOffset ) < Math .abs (targetSnapOffset )
967+ ? positionOffset
968+ : targetSnapOffset ;
969+ }
970+ return targetSnapOffset ;
971+ }
972+
973+ private KeylineState getKeylineStateForPosition (int position ) {
974+ if (keylineStatePositionMap != null ) {
975+ KeylineState keylineState = keylineStatePositionMap .get (
976+ MathUtils .clamp (position , 0 , max (0 , getItemCount () - 1 )));
977+ if (keylineState != null ) {
978+ return keylineState ;
979+ }
980+ }
981+ return keylineStateList .getDefaultState ();
982+ }
983+
925984 @ Override
926985 public void scrollToPosition (int position ) {
927986 if (keylineStateList == null ) {
928987 return ;
929988 }
930- horizontalScrollOffset = getScrollOffsetForPosition (position );
989+ horizontalScrollOffset =
990+ getScrollOffsetForPosition (position , getKeylineStateForPosition (position ));
931991 currentFillStartPosition = MathUtils .clamp (position , 0 , max (0 , getItemCount () - 1 ));
932992 updateCurrentKeylineStateForScrollOffset ();
933993 requestLayout ();
@@ -945,9 +1005,15 @@ public PointF computeScrollVectorForPosition(int targetPosition) {
9451005
9461006 @ Override
9471007 public int calculateDxToMakeVisible (View view , int snapPreference ) {
1008+ if (keylineStateList == null ) {
1009+ return 0 ;
1010+ }
9481011 // Override dx calculations so the target view is brought all the way into the focal
9491012 // range instead of just being made visible.
950- float targetScrollOffset = getScrollOffsetForPosition (getPosition (view ));
1013+ KeylineState scrollToKeyline = getKeylineStateForPosition (getPosition (view ));
1014+
1015+ float targetScrollOffset =
1016+ getScrollOffsetForPosition (getPosition (view ), scrollToKeyline );
9511017 return (int ) (horizontalScrollOffset - targetScrollOffset );
9521018 }
9531019 };
@@ -976,7 +1042,9 @@ public boolean requestChildRectangleOnScreen(
9761042 return false ;
9771043 }
9781044
979- int dx = getOffsetToScrollToPosition (getPosition (child ));
1045+ int dx =
1046+ getOffsetToScrollToPosition (
1047+ getPosition (child ), getKeylineStateForPosition (getPosition (child )));
9801048 if (!focusedChildVisible ) {
9811049 if (dx != 0 ) {
9821050 // TODO(b/266816148): Implement smoothScrollBy when immediate is false.
0 commit comments