Skip to content

Commit 07a3be5

Browse files
imhappipekingme
authored andcommitted
[Lists] Add swipe for action state
PiperOrigin-RevId: 828603206
1 parent 2e04704 commit 07a3be5

File tree

8 files changed

+309
-64
lines changed

8 files changed

+309
-64
lines changed

catalog/java/io/material/catalog/listitem/res/layout/cat_list_item_segmented_viewholder.xml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,16 @@
2121
android:layout_height="wrap_content"
2222
android:layout_width="match_parent"
2323
android:paddingHorizontal="8dp">
24-
24+
2525
<com.google.android.material.listitem.ListItemCardView
2626
android:id="@+id/cat_list_item_card_view"
2727
android:checkable="true"
2828
android:clickable="true"
2929
android:focusable="true"
3030
style="?attr/listItemCardViewSegmentedStyle"
3131
android:layout_height="wrap_content"
32-
android:layout_width="match_parent">
32+
android:layout_width="match_parent"
33+
app:swipeToPrimaryActionEnabled="true">
3334
<LinearLayout
3435
android:gravity="center_vertical"
3536
android:layout_height="wrap_content"
@@ -55,7 +56,7 @@
5556
android:textAppearance="?attr/textAppearanceBodyLarge" />
5657
</LinearLayout>
5758
</com.google.android.material.listitem.ListItemCardView>
58-
59+
5960
<com.google.android.material.listitem.ListItemRevealLayout
6061
android:layout_height="match_parent"
6162
android:layout_width="wrap_content">

lib/java/com/google/android/material/listitem/ListItemCardView.java

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,23 @@
2020
import static com.google.android.material.theme.overlay.MaterialThemeOverlay.wrap;
2121

2222
import android.content.Context;
23+
import androidx.appcompat.widget.TintTypedArray;
2324
import android.util.AttributeSet;
25+
import androidx.annotation.AttrRes;
26+
import androidx.annotation.NonNull;
27+
import androidx.annotation.Nullable;
28+
import androidx.annotation.StyleRes;
2429
import com.google.android.material.card.MaterialCardView;
30+
import com.google.android.material.internal.ThemeEnforcement;
2531

2632
/**
2733
* A {@link MaterialCardView} that is styled as a list item and can be swiped in a
2834
* {@link ListItemLayout} with a sibling {@link RevealableListItem}.
2935
*/
3036
public class ListItemCardView extends MaterialCardView implements SwipeableListItem {
3137

32-
private static final int DEF_STYLE_RES = R.style.Widget_Material3_ListItemCardView;
38+
private final int swipeMaxOvershoot;
39+
private boolean swipeToPrimaryActionEnabled;
3340

3441
public ListItemCardView(Context context) {
3542
this(context, null);
@@ -39,7 +46,42 @@ public ListItemCardView(Context context, AttributeSet attrs) {
3946
this(context, attrs, R.attr.listItemCardViewStyle);
4047
}
4148

42-
public ListItemCardView(Context context, AttributeSet attrs, int defStyleAttr) {
43-
super(wrap(context, attrs, defStyleAttr, DEF_STYLE_RES), attrs, defStyleAttr);
49+
public ListItemCardView(
50+
@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
51+
this(context, attrs, defStyleAttr, R.style.Widget_Material3_ListItemCardView);
52+
}
53+
54+
public ListItemCardView(Context context, AttributeSet attrs, int defStyleAttr, @StyleRes int defStyleRes) {
55+
super(wrap(context, attrs, defStyleAttr, defStyleRes), attrs, defStyleAttr);
56+
// Ensure we are using the correctly themed context rather than the context that was passed in.
57+
context = getContext();
58+
swipeMaxOvershoot = getResources().getDimensionPixelSize(R.dimen.m3_list_max_swipe_overshoot);
59+
60+
/* Custom attributes */
61+
TintTypedArray attributes =
62+
ThemeEnforcement.obtainTintedStyledAttributes(
63+
context, attrs, R.styleable.ListItemCardView, defStyleAttr, defStyleRes);
64+
swipeToPrimaryActionEnabled = attributes.getBoolean(R.styleable.ListItemCardView_swipeToPrimaryActionEnabled, false);
65+
attributes.recycle();
66+
}
67+
68+
@Override
69+
public int getSwipeMaxOvershoot() {
70+
return swipeMaxOvershoot;
71+
}
72+
73+
/**
74+
* Set whether or not to enable the swipe to action. This enables the ListItemCardView to be
75+
* swiped fully out of its parent {@link ListItemLayout}, in order to trigger an action.
76+
*/
77+
// TODO(b/447226552): Link the onSwipeStateChanged listener here when ready
78+
public void setSwipeToPrimaryActionEnabled(boolean swipeToPrimaryActionEnabled) {
79+
this.swipeToPrimaryActionEnabled = swipeToPrimaryActionEnabled;
80+
}
81+
82+
/** Returns whether or not the swipe to action is enabled. */
83+
@Override
84+
public boolean isSwipeToPrimaryActionEnabled() {
85+
return swipeToPrimaryActionEnabled;
4486
}
4587
}

lib/java/com/google/android/material/listitem/ListItemLayout.java

Lines changed: 89 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import static com.google.android.material.listitem.SwipeableListItem.STATE_DRAGGING;
2222
import static com.google.android.material.listitem.SwipeableListItem.STATE_OPEN;
2323
import static com.google.android.material.listitem.SwipeableListItem.STATE_SETTLING;
24+
import static com.google.android.material.listitem.SwipeableListItem.STATE_SWIPE_PRIMARY_ACTION;
2425
import static com.google.android.material.theme.overlay.MaterialThemeOverlay.wrap;
2526
import static java.lang.Math.max;
2627
import static java.lang.Math.min;
@@ -68,9 +69,6 @@ public class ListItemLayout extends FrameLayout {
6869
private static final int[] SINGLE_STATE_SET = {android.R.attr.state_single};
6970
private static final int SETTLING_DURATION = 350;
7071
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;
7472

7573
@Nullable private int[] positionState;
7674

@@ -84,7 +82,8 @@ public class ListItemLayout extends FrameLayout {
8482
@Nullable private View swipeToRevealLayout;
8583
private boolean originalClipToPadding;
8684

87-
private int swipeState = STATE_CLOSED;
85+
@SwipeState private int swipeState = STATE_CLOSED;
86+
@StableSwipeState private int lastStableSwipeState = STATE_CLOSED;
8887
private final StateSettlingTracker stateSettlingTracker = new StateSettlingTracker();
8988

9089
// Cubic bezier curve approximating a spring with damping = 0.6 and stiffness = 800
@@ -124,14 +123,14 @@ public ListItemLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
124123
}
125124

126125
public ListItemLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
127-
this(context, attrs, defStyleAttr, R.attr.listItemLayoutStyle);
126+
this(context, attrs, defStyleAttr, R.style.Widget_Material3_ListItemLayout);
128127
}
129128

130129
public ListItemLayout(
131130
@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
132131
super(wrap(context, attrs, defStyleAttr, defStyleRes), attrs, defStyleAttr);
132+
// Ensure we are using the correctly themed context rather than the context that was passed in.
133133
context = getContext();
134-
swipeMaxOvershoot = getResources().getDimensionPixelSize(R.dimen.m3_list_max_swipe_overshoot);
135134
}
136135

137136
@Override
@@ -215,8 +214,6 @@ private void ensureContentViewIfRevealLayoutExists() {
215214
@Override
216215
public boolean onTouchEvent(MotionEvent ev) {
217216
if (ensureSwipeToRevealSetupIfNeeded()) {
218-
// TODO - b/447218120: Check that at least one child is a ListItemRevealLayout and the other
219-
// is List content.
220217
// Process the event regardless of the event type.
221218
viewDragHelper.processTouchEvent(ev);
222219
gestureDetector.onTouchEvent(ev);
@@ -272,26 +269,46 @@ public boolean tryCaptureView(@NonNull View child, int pointerId) {
272269

273270
@Override
274271
public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
272+
if (!(contentView instanceof SwipeableListItem
273+
&& swipeToRevealLayout instanceof RevealableListItem)) {
274+
return 0;
275+
}
275276
// TODO:b/443153708 - Support RTL
276-
LayoutParams lp = (LayoutParams) swipeToRevealLayout.getLayoutParams();
277+
MarginLayoutParams revealViewLp =
278+
(LayoutParams) swipeToRevealLayout.getLayoutParams();
279+
MarginLayoutParams contentViewLp = (LayoutParams) contentView.getLayoutParams();
280+
int leftPositionClamp =
281+
((SwipeableListItem) contentView).isSwipeToPrimaryActionEnabled()
282+
? originalContentViewLeft
283+
- contentView.getMeasuredWidth()
284+
- contentViewLp.rightMargin
285+
: // left margin is accounted for in originalContentViewLeft
286+
originalContentViewLeft
287+
- ((RevealableListItem) swipeToRevealLayout).getIntrinsicWidth()
288+
- revealViewLp.leftMargin
289+
- revealViewLp.rightMargin;
277290
return max(
278291
min(left, originalContentViewLeft),
279-
originalContentViewLeft
280-
- ((RevealableListItem) swipeToRevealLayout).getIntrinsicWidth()
281-
- lp.leftMargin
282-
- lp.rightMargin
283-
- swipeMaxOvershoot);
292+
leftPositionClamp - ((SwipeableListItem) contentView).getSwipeMaxOvershoot());
284293
}
285294

286295
@Override
287296
public int getViewHorizontalDragRange(@NonNull View child) {
288-
return ((RevealableListItem) swipeToRevealLayout).getIntrinsicWidth()
289-
+ swipeMaxOvershoot;
297+
if (contentView instanceof SwipeableListItem
298+
&& swipeToRevealLayout instanceof RevealableListItem) {
299+
return ((RevealableListItem) swipeToRevealLayout).getIntrinsicWidth()
300+
+ ((SwipeableListItem) contentView).getSwipeMaxOvershoot();
301+
}
302+
return 0;
290303
}
291304

292305
@Override
293306
public void onViewPositionChanged(
294307
@NonNull View changedView, int left, int top, int dx, int dy) {
308+
if (!(contentView instanceof SwipeableListItem
309+
&& swipeToRevealLayout instanceof RevealableListItem)) {
310+
return;
311+
}
295312
super.onViewPositionChanged(changedView, left, top, dx, dy);
296313
// TODO:b/443153708 - Support RTL
297314
revealViewOffset = left - originalContentViewLeft;
@@ -314,19 +331,46 @@ public void onViewPositionChanged(
314331

315332
@Override
316333
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
317-
startSettling(contentView, calculateTargetSwipeState(xvel, releasedChild));
334+
if (contentView instanceof SwipeableListItem
335+
&& swipeToRevealLayout instanceof RevealableListItem) {
336+
startSettling(contentView, calculateTargetSwipeState(xvel, releasedChild));
337+
}
318338
}
319339

320340
private int calculateTargetSwipeState(float xvel, View swipeView) {
341+
if (!((SwipeableListItem) swipeView).isSwipeToPrimaryActionEnabled()) {
342+
if (xvel > DEFAULT_SIGNIFICANT_VEL_THRESHOLD) { // A fast fling to the right
343+
return STATE_CLOSED;
344+
}
345+
if (xvel < -DEFAULT_SIGNIFICANT_VEL_THRESHOLD) { // A fast fling to the left
346+
return STATE_OPEN;
347+
}
348+
// Settle to the closest point if velocity is not significant
349+
return Math.abs(swipeView.getLeft() - getSwipeRevealViewRevealedOffset())
350+
< Math.abs(swipeView.getLeft() - getSwipeViewClosedOffset())
351+
? STATE_OPEN
352+
: STATE_CLOSED;
353+
}
354+
355+
// Swipe to action is supported
321356
if (xvel > DEFAULT_SIGNIFICANT_VEL_THRESHOLD) { // A fast fling to the right
322-
return STATE_CLOSED;
357+
return lastStableSwipeState == STATE_SWIPE_PRIMARY_ACTION
358+
? STATE_OPEN
359+
: STATE_CLOSED;
323360
}
324361
if (xvel < -DEFAULT_SIGNIFICANT_VEL_THRESHOLD) { // A fast fling to the left
325-
return STATE_OPEN;
362+
return lastStableSwipeState == STATE_CLOSED
363+
? STATE_OPEN
364+
: STATE_SWIPE_PRIMARY_ACTION;
365+
}
366+
367+
// Settle to the closest point if velocity is not significant
368+
if (Math.abs(swipeView.getLeft() - getSwipeToActionOffset())
369+
< Math.abs(swipeView.getLeft() - getSwipeRevealViewRevealedOffset())) {
370+
return STATE_SWIPE_PRIMARY_ACTION;
326371
}
327372
if (Math.abs(swipeView.getLeft() - getSwipeRevealViewRevealedOffset())
328373
< Math.abs(swipeView.getLeft() - getSwipeViewClosedOffset())) {
329-
// Settle to the closest point if velocity is not significant
330374
return STATE_OPEN;
331375
}
332376
return STATE_CLOSED;
@@ -378,6 +422,17 @@ private int getSwipeViewClosedOffset() {
378422
return originalContentViewLeft;
379423
}
380424

425+
private int getSwipeToActionOffset() {
426+
if (contentView == null) {
427+
return 0;
428+
}
429+
LayoutParams lp = (LayoutParams) contentView.getLayoutParams();
430+
return originalContentViewLeft
431+
- contentView.getMeasuredWidth()
432+
- lp.leftMargin
433+
- lp.rightMargin;
434+
}
435+
381436
private int getOffsetForSwipeState(@StableSwipeState int swipeState) {
382437
if (swipeToRevealLayout == null) {
383438
throw new IllegalArgumentException(
@@ -388,6 +443,8 @@ private int getOffsetForSwipeState(@StableSwipeState int swipeState) {
388443
return getSwipeViewClosedOffset();
389444
case STATE_OPEN:
390445
return getSwipeRevealViewRevealedOffset();
446+
case STATE_SWIPE_PRIMARY_ACTION:
447+
return getSwipeToActionOffset();
391448
default:
392449
throw new IllegalArgumentException("Invalid state to get swipe offset: " + swipeState);
393450
}
@@ -418,7 +475,19 @@ private void startSettling(View contentView, @StableSwipeState int targetSwipeSt
418475
}
419476

420477
private void setSwipeStateInternal(@SwipeState int swipeState) {
478+
// If swipe to action is not supported but the swipe state to be set in
479+
// STATE_SWIPE_PRIMARY_ACTION, we do nothing.
480+
if (swipeState == STATE_SWIPE_PRIMARY_ACTION
481+
&& !(contentView instanceof SwipeableListItem
482+
&& ((SwipeableListItem) contentView).isSwipeToPrimaryActionEnabled())) {
483+
return;
484+
}
421485
this.swipeState = swipeState;
486+
if (swipeState == STATE_CLOSED
487+
|| swipeState == STATE_OPEN
488+
|| swipeState == STATE_SWIPE_PRIMARY_ACTION) {
489+
this.lastStableSwipeState = swipeState;
490+
}
422491
}
423492

424493
@Override

0 commit comments

Comments
 (0)