1717
1818import com .google .android .material .R ;
1919
20+ import static com .google .android .material .listitem .SwipeableListItem .STATE_CLOSED ;
21+ import static com .google .android .material .listitem .SwipeableListItem .STATE_DRAGGING ;
22+ import static com .google .android .material .listitem .SwipeableListItem .STATE_OPEN ;
23+ import static com .google .android .material .listitem .SwipeableListItem .STATE_SETTLING ;
2024import static com .google .android .material .theme .overlay .MaterialThemeOverlay .wrap ;
2125import static java .lang .Math .max ;
2226import static java .lang .Math .min ;
2327
28+ import android .animation .TimeInterpolator ;
2429import android .content .Context ;
2530import android .util .AttributeSet ;
2631import android .view .GestureDetector ;
2732import android .view .GestureDetector .SimpleOnGestureListener ;
2833import android .view .MotionEvent ;
2934import android .view .View ;
3035import android .view .ViewGroup ;
36+ import android .view .animation .Interpolator ;
37+ import android .view .animation .PathInterpolator ;
3138import android .widget .FrameLayout ;
3239import androidx .annotation .NonNull ;
3340import androidx .annotation .Nullable ;
3441import androidx .customview .widget .ViewDragHelper ;
42+ import com .google .android .material .listitem .SwipeableListItem .StableSwipeState ;
43+ import com .google .android .material .listitem .SwipeableListItem .SwipeState ;
3544
3645/**
3746 * A container layout for a List item.
@@ -57,6 +66,11 @@ public class ListItemLayout extends FrameLayout {
5766 private static final int [] MIDDLE_STATE_SET = { android .R .attr .state_middle };
5867 private static final int [] LAST_STATE_SET = { android .R .attr .state_last };
5968 private static final int [] SINGLE_STATE_SET = {android .R .attr .state_single };
69+ private static final int SETTLING_DURATION = 350 ;
70+ private static final int DEFAULT_SIGNIFICANT_VEL_THRESHOLD = 500 ;
71+ // The overshoot that the user can swipe the reveal view by before it settles
72+ // back to the closest stable swipe state.
73+ private final int swipeMaxOvershoot ;
6074
6175 @ Nullable private int [] positionState ;
6276
@@ -70,6 +84,37 @@ public class ListItemLayout extends FrameLayout {
7084 @ Nullable private View swipeToRevealLayout ;
7185 private boolean originalClipToPadding ;
7286
87+ private int swipeState = STATE_CLOSED ;
88+ private final StateSettlingTracker stateSettlingTracker = new StateSettlingTracker ();
89+
90+ // Cubic bezier curve approximating a spring with damping = 0.6 and stiffness = 800
91+ private static final TimeInterpolator CUBIC_BEZIER_INTERPOLATOR =
92+ new PathInterpolator (0.42f , 1.67f , 0.21f , 0.9f );
93+
94+ private class StateSettlingTracker {
95+ @ StableSwipeState private int targetSwipeState ;
96+ private boolean isContinueSettlingRunnablePosted ;
97+
98+ private final Runnable continueSettlingRunnable =
99+ () -> {
100+ isContinueSettlingRunnablePosted = false ;
101+ if (viewDragHelper != null && viewDragHelper .continueSettling (true )) {
102+ continueSettlingToState (targetSwipeState );
103+ } else if (swipeState == STATE_SETTLING ) {
104+ setSwipeStateInternal (targetSwipeState );
105+ }
106+ // In other cases, settling has been interrupted by certain UX interactions. Do nothing.
107+ };
108+
109+ private void continueSettlingToState (@ StableSwipeState int targetSwipeState ) {
110+ this .targetSwipeState = targetSwipeState ;
111+ if (!isContinueSettlingRunnablePosted ) {
112+ post (continueSettlingRunnable );
113+ isContinueSettlingRunnablePosted = true ;
114+ }
115+ }
116+ }
117+
73118 public ListItemLayout (@ NonNull Context context ) {
74119 this (context , null );
75120 }
@@ -85,6 +130,8 @@ public ListItemLayout(@NonNull Context context, @Nullable AttributeSet attrs, in
85130 public ListItemLayout (
86131 @ NonNull Context context , @ Nullable AttributeSet attrs , int defStyleAttr , int defStyleRes ) {
87132 super (wrap (context , attrs , defStyleAttr , defStyleRes ), attrs , defStyleAttr );
133+ context = getContext ();
134+ swipeMaxOvershoot = getResources ().getDimensionPixelSize (R .dimen .m3_list_max_swipe_overshoot );
88135 }
89136
90137 @ Override
@@ -232,12 +279,14 @@ public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
232279 originalContentViewLeft
233280 - ((RevealableListItem ) swipeToRevealLayout ).getIntrinsicWidth ()
234281 - lp .leftMargin
235- - lp .rightMargin );
282+ - lp .rightMargin
283+ - swipeMaxOvershoot );
236284 }
237285
238286 @ Override
239287 public int getViewHorizontalDragRange (@ NonNull View child ) {
240- return ((RevealableListItem ) swipeToRevealLayout ).getIntrinsicWidth ();
288+ return ((RevealableListItem ) swipeToRevealLayout ).getIntrinsicWidth ()
289+ + swipeMaxOvershoot ;
241290 }
242291
243292 @ Override
@@ -262,6 +311,33 @@ public void onViewPositionChanged(
262311 ((RevealableListItem ) swipeToRevealLayout )
263312 .setRevealedWidth (revealViewDesiredWidth );
264313 }
314+
315+ @ Override
316+ public void onViewReleased (@ NonNull View releasedChild , float xvel , float yvel ) {
317+ startSettling (contentView , calculateTargetSwipeState (xvel , releasedChild ));
318+ }
319+
320+ private int calculateTargetSwipeState (float xvel , View swipeView ) {
321+ if (xvel > DEFAULT_SIGNIFICANT_VEL_THRESHOLD ) { // A fast fling to the right
322+ return STATE_CLOSED ;
323+ }
324+ if (xvel < -DEFAULT_SIGNIFICANT_VEL_THRESHOLD ) { // A fast fling to the left
325+ return STATE_OPEN ;
326+ }
327+ if (Math .abs (swipeView .getLeft () - getSwipeRevealViewRevealedOffset ())
328+ < Math .abs (swipeView .getLeft () - getSwipeViewClosedOffset ())) {
329+ // Settle to the closest point if velocity is not significant
330+ return STATE_OPEN ;
331+ }
332+ return STATE_CLOSED ;
333+ }
334+
335+ @ Override
336+ public void onViewDragStateChanged (int state ) {
337+ if (state == ViewDragHelper .STATE_DRAGGING ) {
338+ setSwipeStateInternal (STATE_DRAGGING );
339+ }
340+ }
265341 });
266342
267343 gestureDetector =
@@ -287,6 +363,64 @@ public boolean onScroll(
287363 return true ;
288364 }
289365
366+ private int getSwipeRevealViewRevealedOffset () {
367+ if (swipeToRevealLayout == null ) {
368+ return 0 ;
369+ }
370+ LayoutParams lp = (LayoutParams ) swipeToRevealLayout .getLayoutParams ();
371+ return originalContentViewLeft
372+ - ((RevealableListItem ) swipeToRevealLayout ).getIntrinsicWidth ()
373+ - lp .leftMargin
374+ - lp .rightMargin ;
375+ }
376+
377+ private int getSwipeViewClosedOffset () {
378+ return originalContentViewLeft ;
379+ }
380+
381+ private int getOffsetForSwipeState (@ StableSwipeState int swipeState ) {
382+ if (swipeToRevealLayout == null ) {
383+ throw new IllegalArgumentException (
384+ "Cannot get offset for swipe without a SwipeableListItem and a RevealableListItem." );
385+ }
386+ switch (swipeState ) {
387+ case STATE_CLOSED :
388+ return getSwipeViewClosedOffset ();
389+ case STATE_OPEN :
390+ return getSwipeRevealViewRevealedOffset ();
391+ default :
392+ throw new IllegalArgumentException ("Invalid state to get swipe offset: " + swipeState );
393+ }
394+ }
395+
396+ private void startSettling (View contentView , @ StableSwipeState int targetSwipeState ) {
397+ if (viewDragHelper == null ) {
398+ return ;
399+ }
400+ int left = getOffsetForSwipeState (targetSwipeState );
401+ // If we are going to the revealed state, we want to settle with a 'bounce' so we use a cubic
402+ // bezier interpolator. Otherwise, we are closing and we don't want a bounce.
403+ boolean settling =
404+ (targetSwipeState == STATE_OPEN )
405+ ? viewDragHelper .smoothSlideViewTo (
406+ contentView ,
407+ left ,
408+ contentView .getTop (),
409+ SETTLING_DURATION ,
410+ (Interpolator ) CUBIC_BEZIER_INTERPOLATOR )
411+ : viewDragHelper .smoothSlideViewTo (contentView , left , contentView .getTop ());
412+ if (settling ) {
413+ setSwipeStateInternal (STATE_SETTLING );
414+ stateSettlingTracker .continueSettlingToState (targetSwipeState );
415+ } else {
416+ setSwipeStateInternal (targetSwipeState );
417+ }
418+ }
419+
420+ private void setSwipeStateInternal (@ SwipeState int swipeState ) {
421+ this .swipeState = swipeState ;
422+ }
423+
290424 @ Override
291425 protected void onLayout (boolean changed , int left , int top , int right , int bottom ) {
292426 super .onLayout (changed , left , top , right , bottom );
0 commit comments