3333import androidx .recyclerview .widget .RecyclerView .LayoutParams ;
3434import androidx .recyclerview .widget .RecyclerView .Recycler ;
3535import androidx .recyclerview .widget .RecyclerView .State ;
36+ import android .util .Log ;
3637import android .view .View ;
3738import android .view .ViewGroup ;
3839import android .view .accessibility .AccessibilityEvent ;
6263 */
6364public class CarouselLayoutManager extends LayoutManager implements Carousel {
6465
66+ private static final String TAG = "CarouselLayoutManager" ;
67+
6568 private int horizontalScrollOffset ;
6669
6770 // Min scroll is the offset number that offsets the list to the right/bottom as much as possible.
@@ -73,6 +76,7 @@ public class CarouselLayoutManager extends LayoutManager implements Carousel {
7376 // will move the list to the start of the container.
7477 private int maxHorizontalScroll ;
7578
79+ private boolean isDebuggingEnabled = false ;
7680 private final DebugItemDecoration debugItemDecoration = new DebugItemDecoration ();
7781 @ NonNull private CarouselStrategy carouselStrategy ;
7882 @ Nullable private KeylineStateList keylineStateList ;
@@ -214,6 +218,8 @@ private void fill(Recycler recycler, State state) {
214218 addViewsStart (recycler , firstPosition - 1 );
215219 addViewsEnd (recycler , state , lastPosition + 1 );
216220 }
221+
222+ validateChildOrderIfDebugging ();
217223 }
218224
219225 @ Override
@@ -224,6 +230,8 @@ public void onLayoutCompleted(State state) {
224230 } else {
225231 currentFillStartPosition = getPosition (getChildAt (0 ));
226232 }
233+
234+ validateChildOrderIfDebugging ();
227235 }
228236
229237 /**
@@ -247,7 +255,8 @@ private void addViewsStart(Recycler recycler, int startPosition) {
247255 if (isLocOffsetOutOfFillBoundsEnd (calculations .locOffset , calculations .range )) {
248256 continue ;
249257 }
250- addAndLayoutView (calculations .child , calculations .locOffset );
258+ // Add this child to the first index of the RecyclerView.
259+ addAndLayoutView (calculations .child , /* index= */ 0 , calculations .locOffset );
251260 }
252261 }
253262
@@ -273,7 +282,58 @@ private void addViewsEnd(Recycler recycler, State state, int startPosition) {
273282 if (isLocOffsetOutOfFillBoundsStart (calculations .locOffset , calculations .range )) {
274283 continue ;
275284 }
276- addAndLayoutView (calculations .child , calculations .locOffset );
285+ // Add this child to the last index of the RecyclerView
286+ addAndLayoutView (calculations .child , /* index= */ -1 , calculations .locOffset );
287+ }
288+ }
289+
290+ /** Used for debugging. Logs the internal representation of children to default logger. */
291+ private void logChildrenIfDebugging () {
292+ if (!isDebuggingEnabled ) {
293+ return ;
294+ }
295+
296+ if (Log .isLoggable (TAG , Log .DEBUG )) {
297+ Log .d (TAG , "internal representation of views on the screen" );
298+ for (int i = 0 ; i < getChildCount (); i ++) {
299+ View child = getChildAt (i );
300+ float centerX = getDecoratedCenterXWithMargins (child );
301+ Log .d (
302+ TAG ,
303+ "item position " + getPosition (child ) + ", center:" + centerX + ", child index:" + i );
304+ }
305+ Log .d (TAG , "==============" );
306+ }
307+ }
308+
309+ /**
310+ * Used for debugging. Validates that child views are laid out in correct order. This is important
311+ * because rest of the algorithm relies on this constraint.
312+ *
313+ * <p>Child 0 should be closest to adapter position 0 and last child should be closest to the last
314+ * adapter position.
315+ */
316+ private void validateChildOrderIfDebugging () {
317+ if (!isDebuggingEnabled || getChildCount () < 1 ) {
318+ return ;
319+ }
320+
321+ for (int i = 0 ; i < getChildCount () - 1 ; i ++) {
322+ int currPos = getPosition (getChildAt (i ));
323+ int nextPos = getPosition (getChildAt (i + 1 ));
324+ if (currPos > nextPos ) {
325+ logChildrenIfDebugging ();
326+ throw new IllegalStateException (
327+ "Detected invalid child order. Child at index ["
328+ + i
329+ + "] had adapter position ["
330+ + currPos
331+ + "] and child at index ["
332+ + (i + 1 )
333+ + "] had adapter position ["
334+ + nextPos
335+ + "]." );
336+ }
277337 }
278338 }
279339
@@ -294,7 +354,7 @@ private ChildCalculations makeChildCalculations(Recycler recycler, float start,
294354 View child = recycler .getViewForPosition (position );
295355 measureChildWithMargins (child , 0 , 0 );
296356
297- float centerX = addEnd ((int ) start , (int ) halfItemSize );
357+ int centerX = addEnd ((int ) start , (int ) halfItemSize );
298358 KeylineRange range =
299359 getSurroundingKeylineRange (currentKeylineState .getKeylines (), centerX , false );
300360
@@ -309,11 +369,13 @@ private ChildCalculations makeChildCalculations(Recycler recycler, float start,
309369 * scrolling axis.
310370 *
311371 * @param child the child view to add and lay out
372+ * @param index the index at which to add the child to the RecyclerView. Use 0 for adding to the
373+ * start of the list and -1 for adding to the end.
312374 * @param offsetCx where the center of the masked child should be placed along the scrolling axis
313375 */
314- private void addAndLayoutView (View child , float offsetCx ) {
376+ private void addAndLayoutView (View child , int index , float offsetCx ) {
315377 float halfItemSize = currentKeylineState .getItemSize () / 2F ;
316- addView (child );
378+ addView (child , index );
317379 layoutDecoratedWithMargins (
318380 child ,
319381 /* left= */ (int ) (offsetCx - halfItemSize ),
@@ -336,7 +398,7 @@ private void addAndLayoutView(View child, float offsetCx) {
336398 */
337399 private boolean isLocOffsetOutOfFillBoundsStart (float locOffset , KeylineRange range ) {
338400 float maskedSize = getMaskedItemSizeForLocOffset (locOffset , range );
339- int maskedEnd = addEnd ((int ) locOffset , (int ) (maskedSize / 2 ));
401+ int maskedEnd = addEnd ((int ) locOffset , (int ) (maskedSize / 2F ));
340402 return isLayoutRtl () ? maskedEnd > getContainerWidth () : maskedEnd < 0 ;
341403 }
342404
@@ -354,7 +416,7 @@ private boolean isLocOffsetOutOfFillBoundsStart(float locOffset, KeylineRange ra
354416 */
355417 private boolean isLocOffsetOutOfFillBoundsEnd (float locOffset , KeylineRange range ) {
356418 float maskedSize = getMaskedItemSizeForLocOffset (locOffset , range );
357- int maskedStart = addStart ((int ) locOffset , (int ) (maskedSize / 2 ));
419+ int maskedStart = addStart ((int ) locOffset , (int ) (maskedSize / 2F ));
358420 return isLayoutRtl () ? maskedStart < 0 : maskedStart > getContainerWidth ();
359421 }
360422
@@ -560,7 +622,7 @@ private int calculateStartHorizontalScroll(KeylineStateList stateList) {
560622 Keyline startFocalKeyline =
561623 isRtl ? startState .getLastFocalKeyline () : startState .getFirstFocalKeyline ();
562624 float firstItemDistanceFromStart = getPaddingStart () * (isRtl ? 1 : -1 );
563- float firstItemStart =
625+ int firstItemStart =
564626 addStart ((int ) startFocalKeyline .loc , (int ) (startState .getItemSize () / 2F ));
565627 return (int ) (firstItemDistanceFromStart + getParentStart () - firstItemStart );
566628 }
@@ -922,7 +984,7 @@ private int scrollBy(int distance, Recycler recycler, State state) {
922984 */
923985 private void offsetChildLeftAndRight (
924986 View child , float startOffset , float halfItemSize , Rect boundsRect ) {
925- float centerX = addEnd ((int ) startOffset , (int ) halfItemSize );
987+ int centerX = addEnd ((int ) startOffset , (int ) halfItemSize );
926988 KeylineRange range =
927989 getSurroundingKeylineRange (currentKeylineState .getKeylines (), centerX , false );
928990
@@ -975,14 +1037,20 @@ public int computeHorizontalScrollRange(@NonNull State state) {
9751037 }
9761038
9771039 /**
978- * Enables drawing that illustrates keylines and other internal concepts to help debug strategy.
1040+ * Enables features to help debug keylines and other internal layout manager logic.
1041+ *
1042+ * <p>This will draw lines on top of the RecyclerView that show where keylines are placed for the
1043+ * current {@link CarouselStrategy}. Enabling debugging will also throw an exception when an
1044+ * invalid child order is detected (child index and adapter position are incorrectly ordered). See
1045+ * {@link #validateChildOrderIfDebugging()} ()} ()} for more details.
9791046 *
9801047 * @param recyclerView The {@link RecyclerView} this layout manager is attached to.
981- * @param enabled Whether to draw debug lines.
1048+ * @param enabled Whether to draw debug lines and throw on state errors .
9821049 * @hide
9831050 */
9841051 @ RestrictTo (Scope .LIBRARY_GROUP )
985- public void setDrawDebugEnabled (@ NonNull RecyclerView recyclerView , boolean enabled ) {
1052+ public void setDebuggingEnabled (@ NonNull RecyclerView recyclerView , boolean enabled ) {
1053+ this .isDebuggingEnabled = enabled ;
9861054 recyclerView .removeItemDecoration (debugItemDecoration );
9871055 if (enabled ) {
9881056 recyclerView .addItemDecoration (debugItemDecoration );
0 commit comments