21
21
import static com .google .android .material .animation .AnimationUtils .lerp ;
22
22
import static java .lang .Math .abs ;
23
23
import static java .lang .Math .max ;
24
+ import static java .lang .Math .min ;
Has a conversation. Original line has a conversation. 24
25
25
26
import android .graphics .Canvas ;
26
27
import android .graphics .Color ;
27
28
import android .graphics .Paint ;
28
29
import android .graphics .PointF ;
29
30
import android .graphics .Rect ;
31
+ import android .graphics .RectF ;
30
32
import androidx .recyclerview .widget .LinearSmoothScroller ;
31
33
import androidx .recyclerview .widget .RecyclerView ;
32
34
import androidx .recyclerview .widget .RecyclerView .LayoutManager ;
@@ -94,21 +96,25 @@ public class CarouselLayoutManager extends LayoutManager
94
96
* RecyclerView and laid out.
95
97
*/
96
98
private static final class ChildCalculations {
97
- View child ;
98
- float locOffset ;
99
- KeylineRange range ;
99
+ final View child ;
100
+ final float center ;
101
+ final float offsetCenter ;
102
+ final KeylineRange range ;
100
103
101
104
/**
102
105
* Creates new calculations object.
103
106
*
104
107
* @param child The child being calculated for
105
- * @param locOffset the offset location along the scrolling axis where this child will be laid
106
- * out
108
+ * @param center the location of the center of the {@code child} along the scrolling axis in the
109
+ * end-to-end model
110
+ * @param offsetCenter the offset location of the center of the {@code child} along the
111
+ * scrolling axis where this child will be laid out
107
112
* @param range the keyline range that surrounds {@code locOffset}
108
113
*/
109
- ChildCalculations (View child , float locOffset , KeylineRange range ) {
114
+ ChildCalculations (View child , float center , float offsetCenter , KeylineRange range ) {
110
115
this .child = child ;
111
- this .locOffset = locOffset ;
116
+ this .center = center ;
117
+ this .offsetCenter = offsetCenter ;
112
118
this .range = range ;
113
119
}
114
120
}
@@ -250,18 +256,18 @@ private void addViewsStart(Recycler recycler, int startPosition) {
250
256
int start = calculateChildStartForFill (startPosition );
251
257
for (int i = startPosition ; i >= 0 ; i --) {
252
258
ChildCalculations calculations = makeChildCalculations (recycler , start , i );
253
- if (isLocOffsetOutOfFillBoundsStart (calculations .locOffset , calculations .range )) {
259
+ if (isLocOffsetOutOfFillBoundsStart (calculations .offsetCenter , calculations .range )) {
254
260
break ;
255
261
}
256
262
start = addStart (start , (int ) currentKeylineState .getItemSize ());
257
263
258
264
// If this child's start is beyond the end of the container, don't add the child but continue
259
265
// to loop so we can eventually get to children that are within bounds.
260
- if (isLocOffsetOutOfFillBoundsEnd (calculations .locOffset , calculations .range )) {
266
+ if (isLocOffsetOutOfFillBoundsEnd (calculations .offsetCenter , calculations .range )) {
261
267
continue ;
262
268
}
263
269
// Add this child to the first index of the RecyclerView.
264
- addAndLayoutView (calculations .child , /* index= */ 0 , calculations . locOffset );
270
+ addAndLayoutView (calculations .child , /* index= */ 0 , calculations );
265
271
}
266
272
}
267
273
@@ -277,18 +283,18 @@ private void addViewsEnd(Recycler recycler, State state, int startPosition) {
277
283
int start = calculateChildStartForFill (startPosition );
278
284
for (int i = startPosition ; i < state .getItemCount (); i ++) {
279
285
ChildCalculations calculations = makeChildCalculations (recycler , start , i );
280
- if (isLocOffsetOutOfFillBoundsEnd (calculations .locOffset , calculations .range )) {
286
+ if (isLocOffsetOutOfFillBoundsEnd (calculations .offsetCenter , calculations .range )) {
281
287
break ;
282
288
}
283
289
start = addEnd (start , (int ) currentKeylineState .getItemSize ());
284
290
285
291
// If this child's end is beyond the start of the container, don't add the child but continue
286
292
// to loop so we can eventually get to children that are within bounds.
287
- if (isLocOffsetOutOfFillBoundsStart (calculations .locOffset , calculations .range )) {
293
+ if (isLocOffsetOutOfFillBoundsStart (calculations .offsetCenter , calculations .range )) {
288
294
continue ;
289
295
}
290
296
// Add this child to the last index of the RecyclerView
291
- addAndLayoutView (calculations .child , /* index= */ -1 , calculations . locOffset );
297
+ addAndLayoutView (calculations .child , /* index= */ -1 , calculations );
292
298
}
293
299
}
294
300
@@ -359,14 +365,12 @@ private ChildCalculations makeChildCalculations(Recycler recycler, float start,
359
365
View child = recycler .getViewForPosition (position );
360
366
measureChildWithMargins (child , 0 , 0 );
361
367
362
- int centerX = addEnd ((int ) start , (int ) halfItemSize );
368
+ int center = addEnd ((int ) start , (int ) halfItemSize );
363
369
KeylineRange range =
364
- getSurroundingKeylineRange (currentKeylineState .getKeylines (), centerX , false );
365
-
366
- float offsetCx = calculateChildOffsetCenterForLocation (child , centerX , range );
367
- updateChildMaskForLocation (child , centerX , range );
370
+ getSurroundingKeylineRange (currentKeylineState .getKeylines (), center , false );
368
371
369
- return new ChildCalculations (child , offsetCx , range );
372
+ float offsetCenter = calculateChildOffsetCenterForLocation (child , center , range );
373
+ return new ChildCalculations (child , center , offsetCenter , range );
370
374
}
371
375
372
376
/**
@@ -376,17 +380,18 @@ private ChildCalculations makeChildCalculations(Recycler recycler, float start,
376
380
* @param child the child view to add and lay out
377
381
* @param index the index at which to add the child to the RecyclerView. Use 0 for adding to the
378
382
* start of the list and -1 for adding to the end.
379
- * @param offsetCx where the center of the masked child should be placed along the scrolling axis
383
+ * @param calculations the child calculations to be used to layout this view
380
384
*/
381
- private void addAndLayoutView (View child , int index , float offsetCx ) {
385
+ private void addAndLayoutView (View child , int index , ChildCalculations calculations ) {
382
386
float halfItemSize = currentKeylineState .getItemSize () / 2F ;
383
387
addView (child , index );
384
388
layoutDecoratedWithMargins (
385
389
child ,
386
- /* left= */ (int ) (offsetCx - halfItemSize ),
390
+ /* left= */ (int ) (calculations . offsetCenter - halfItemSize ),
387
391
/* top= */ getParentTop (),
388
- /* right= */ (int ) (offsetCx + halfItemSize ),
392
+ /* right= */ (int ) (calculations . offsetCenter + halfItemSize ),
389
393
/* bottom= */ getParentBottom ());
394
+ updateChildMaskForLocation (child , calculations .center , calculations .range );
390
395
}
391
396
392
397
/**
@@ -745,18 +750,42 @@ private float getMaskedItemSizeForLocOffset(float locOffset, KeylineRange range)
745
750
*/
746
751
private void updateChildMaskForLocation (
747
752
View child , float childCenterLocation , KeylineRange range ) {
748
- if (child instanceof Maskable ) {
749
- // Interpolate the mask value based on the location of this view between it's two
750
- // surrounding keylines.
751
- float maskProgress =
752
- lerp (
753
- range .left .mask ,
754
- range .right .mask ,
755
- range .left .loc ,
756
- range .right .loc ,
757
- childCenterLocation );
758
- ((Maskable ) child ).setMaskXPercentage (maskProgress );
753
+ if (!(child instanceof Maskable )) {
754
+ return ;
759
755
}
756
+
757
+ // Interpolate the mask value based on the location of this view between it's two
758
+ // surrounding keylines.
759
+ float maskProgress =
760
+ lerp (
761
+ range .left .mask ,
762
+ range .right .mask ,
763
+ range .left .loc ,
764
+ range .right .loc ,
765
+ childCenterLocation );
766
+
767
+ float childHeight = child .getHeight ();
768
+ float childWidth = child .getWidth ();
769
+ // Translate the percentage into an actual pixel value of how much of this view should be
770
+ // masked away.
771
+ float maskWidth = lerp (0F , childWidth / 2F , 0F , 1F , maskProgress );
772
+ RectF maskRect = new RectF (maskWidth , 0F , (childWidth - maskWidth ), childHeight );
773
+
774
+ // If the carousel is a CONTAINED carousel, ensure the mask collapses against the side of the
775
+ // container instead of bleeding and being clipped by the RecyclerView's bounds.
776
+ if (carouselStrategy .isContained ()) {
777
+ float offsetCx = calculateChildOffsetCenterForLocation (child , childCenterLocation , range );
778
+ float maskedLeft = offsetCx - (maskRect .width () / 2F );
779
+ float maskedRight = offsetCx + (maskRect .width () / 2F );
780
+
781
+ if (maskedLeft < getParentLeft ()) {
782
+ maskRect .left = min (maskRect .left + (getParentLeft () - maskedLeft ), childWidth / 2F );
783
+ }
784
+ if (maskedRight > getParentRight ()) {
785
+ maskRect .right = max (maskRect .right - (maskedRight - getParentRight ()), childWidth / 2F );
786
+ }
787
+ }
788
+ ((Maskable ) child ).setMaskRectF (maskRect );
760
789
}
761
790
762
791
@ Override
@@ -797,12 +826,20 @@ public void measureChildWithMargins(@NonNull View child, int widthUsed, int heig
797
826
child .measure (widthSpec , heightSpec );
798
827
}
799
828
829
+ private int getParentLeft () {
830
+ return 0 ;
831
+ }
832
+
800
833
private int getParentStart () {
801
- return isLayoutRtl () ? getWidth () : 0 ;
834
+ return isLayoutRtl () ? getParentRight () : getParentLeft ();
835
+ }
836
+
837
+ private int getParentRight () {
838
+ return getWidth ();
802
839
}
803
840
804
841
private int getParentEnd () {
805
- return isLayoutRtl () ? 0 : getWidth ();
842
+ return isLayoutRtl () ? getParentLeft () : getParentRight ();
806
843
}
807
844
808
845
private int getParentTop () {
@@ -890,8 +927,7 @@ public void scrollToPosition(int position) {
890
927
if (keylineStateList == null ) {
891
928
return ;
892
929
}
893
- horizontalScrollOffset =
894
- getScrollOffsetForPosition (position );
930
+ horizontalScrollOffset = getScrollOffsetForPosition (position );
895
931
currentFillStartPosition = MathUtils .clamp (position , 0 , max (0 , getItemCount () - 1 ));
896
932
updateCurrentKeylineStateForScrollOffset ();
897
933
requestLayout ();
@@ -911,8 +947,7 @@ public PointF computeScrollVectorForPosition(int targetPosition) {
911
947
public int calculateDxToMakeVisible (View view , int snapPreference ) {
912
948
// Override dx calculations so the target view is brought all the way into the focal
913
949
// range instead of just being made visible.
914
- float targetScrollOffset =
915
- getScrollOffsetForPosition (getPosition (view ));
950
+ float targetScrollOffset = getScrollOffsetForPosition (getPosition (view ));
916
951
return (int ) (horizontalScrollOffset - targetScrollOffset );
917
952
}
918
953
};
@@ -1006,13 +1041,12 @@ private void offsetChildLeftAndRight(
1006
1041
int centerX = addEnd ((int ) startOffset , (int ) halfItemSize );
1007
1042
KeylineRange range =
1008
1043
getSurroundingKeylineRange (currentKeylineState .getKeylines (), centerX , false );
1009
-
1010
1044
float offsetCx = calculateChildOffsetCenterForLocation (child , centerX , range );
1011
- updateChildMaskForLocation (child , centerX , range );
1012
1045
1013
1046
// Offset the child so its center is at offsetCx
1014
1047
super .getDecoratedBoundsWithMargins (child , boundsRect );
1015
1048
float actualCx = boundsRect .left + halfItemSize ;
1049
+ updateChildMaskForLocation (child , centerX , range );
1016
1050
child .offsetLeftAndRight ((int ) (offsetCx - actualCx ));
1017
1051
}
1018
1052