[Lists] Support skipping the open swipe state so clients can go straight to the primary action

PiperOrigin-RevId: 843871144
This commit is contained in:
rightnao 2025-12-13 00:20:58 +00:00 committed by Hunter Stich
parent 75f0a4e812
commit d5934ee5ba
10 changed files with 126 additions and 50 deletions

View File

@ -28,8 +28,7 @@
android:clickable="true"
android:focusable="true"
android:layout_height="wrap_content"
android:layout_width="match_parent"
app:swipeToPrimaryActionEnabled="true">
android:layout_width="match_parent">
<LinearLayout
android:gravity="center_vertical"
android:layout_height="wrap_content"
@ -73,7 +72,8 @@
<com.google.android.material.listitem.ListItemRevealLayout
android:layout_height="match_parent"
android:layout_width="wrap_content">
android:layout_width="wrap_content"
app:primaryActionSwipeMode="indirect">
<com.google.android.material.button.MaterialButton
style="?attr/materialIconButtonFilledTonalStyle"
android:id="@+id/cat_list_action_add_button"

View File

@ -313,19 +313,19 @@ dismissed.
#### ListItemCardView attributes
Element | Attribute | Related methods | Default value
----------------------------------- | --------------------------------- | -------------------------------------------------------------------- | -------------
**Color** | `app:cardBackgroundColor` | `setCardBackgroundColor`<br/>`getCardBackgroundColor` | `@color/transparent` (standard style)</br>`?attr/colorSurfaceBright` (segmented style) </br> `?attr/colorSecondaryContainer` (selected)
**Shape** | `app:shapeAppearance` | `setShapeAppearanceModel`<br/>`getShapeAppearanceModel` | `?attr/listItemShapeAppearanceSingle` </br> `?attr/listItemShapeAppearanceFirst` </br> `?attr/listItemShapeAppearanceMiddle` </br> `?attr/listItemShapeAppearanceLast`
**Ripple color** | `app:rippleColor` | `setRippleColor`<br/>`setRippleColorResource`<br/>`getRippleColor` | `?attr/colorOnSurface` at 10% opacity (8% when hovered)
**Swipe enabled** | `app:swipeEnabled` | `setSwipeEnabled`<br/>`isSwipeEnabled` | `true`
**Swipe to primary action enabled** | `app:swipeToPrimaryActionEnabled` | `setSwipeToPrimaryActionEnabled`<br/>`isSwipeToPrimaryActionEnabled` | `false`
Element | Attribute | Related methods | Default value
-------------------------------------------- | --------------------------------- | ---------------------------------------------------------------------- | -------------
**Color** | `app:cardBackgroundColor` | `setCardBackgroundColor`<br/>`getCardBackgroundColor` | `@color/transparent` (standard style)</br>`?attr/colorSurfaceBright` (segmented style) </br> `?attr/colorSecondaryContainer` (selected)
**Shape** | `app:shapeAppearance` | `setShapeAppearanceModel`<br/>`getShapeAppearanceModel` | `?attr/listItemShapeAppearanceSingle` </br> `?attr/listItemShapeAppearanceFirst` </br> `?attr/listItemShapeAppearanceMiddle` </br> `?attr/listItemShapeAppearanceLast`
**Ripple color** | `app:rippleColor` | `setRippleColor`<br/>`setRippleColorResource`<br/>`getRippleColor` | `?attr/colorOnSurface` at 10% opacity (8% when hovered)
**Swipe enabled** | `app:swipeEnabled` | `setSwipeEnabled`<br/>`isSwipeEnabled` | `true`
#### ListItemRevealLayout attributes
Element | Attribute | Related methods | Default value
------------------ | --------------------------- | ----------------------------------------- | -------------
**Min child size** | `app:minRevealedChildWidth` | `setMinChildWidth`<br/>`getMinChildWidth` | `6dp`
Element | Attribute | Related methods | Default value
----------------------------- | ---------------------------- | ----------------------------------------------------------- | -------------
**Min child size** | `app:minRevealedChildWidth` | `setMinChildWidth`<br/>`getMinChildWidth` | `6dp`
**Primary Action Swipe Mode** | `app:primaryActionSwipeMode` | `setPrimaryActionSwipeMode`<br/>`getPrimaryActionSwipeMode` | `disabled`
### Accessibility

View File

@ -62,7 +62,6 @@ public class ListItemCardView extends MaterialCardView implements SwipeableListI
private boolean isSwiped = false;
private final int swipeMaxOvershoot;
private boolean swipeToPrimaryActionEnabled;
private boolean swipeEnabled;
@NonNull private final LinkedHashSet<SwipeCallback> swipeCallbacks = new LinkedHashSet<>();
@ -89,7 +88,6 @@ public class ListItemCardView extends MaterialCardView implements SwipeableListI
TintTypedArray attributes =
ThemeEnforcement.obtainTintedStyledAttributes(
context, attrs, R.styleable.ListItemCardView, defStyleAttr, defStyleRes);
swipeToPrimaryActionEnabled = attributes.getBoolean(R.styleable.ListItemCardView_swipeToPrimaryActionEnabled, false);
swipeEnabled = attributes.getBoolean(R.styleable.ListItemCardView_swipeEnabled, true);
attributes.recycle();
}
@ -102,6 +100,7 @@ public class ListItemCardView extends MaterialCardView implements SwipeableListI
/**
* Whether or not to enabling swiping when there is a sibling {@link RevealableListItem}.
*/
@Override
public void setSwipeEnabled(boolean swipeEnabled) {
this.swipeEnabled = swipeEnabled;
}
@ -111,23 +110,6 @@ public class ListItemCardView extends MaterialCardView implements SwipeableListI
return swipeEnabled;
}
/**
* Set whether or not to enable the swipe to action. This enables the ListItemCardView to be
* swiped fully out of its parent {@link ListItemLayout}, in order to trigger an action.
*
* <p>Users should add a {@link SwipeCallback} via {@link #addSwipeCallback} to listen for swipe
* state changes and trigger an action.
*/
public void setSwipeToPrimaryActionEnabled(boolean swipeToPrimaryActionEnabled) {
this.swipeToPrimaryActionEnabled = swipeToPrimaryActionEnabled;
}
/** Returns whether or not the swipe to action is enabled. */
@Override
public boolean isSwipeToPrimaryActionEnabled() {
return swipeToPrimaryActionEnabled;
}
@Override
protected int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);

View File

@ -300,7 +300,8 @@ public class ListItemLayout extends FrameLayout {
SwipeableListItem swipeableItem = (SwipeableListItem) contentView;
int maxSwipeDistance;
if (swipeableItem.isSwipeToPrimaryActionEnabled()) {
if (revealableItem.getPrimaryActionSwipeMode()
!= RevealableListItem.PRIMARY_ACTION_SWIPE_DISABLED) {
MarginLayoutParams contentViewLp = (MarginLayoutParams) contentView.getLayoutParams();
maxSwipeDistance = contentView.getMeasuredWidth() + contentViewLp.getMarginEnd();
} else {
@ -353,10 +354,14 @@ public class ListItemLayout extends FrameLayout {
}
private int calculateTargetSwipeState(float xvel, View swipeView) {
if (swipeToRevealLayout == null) {
return STATE_CLOSED;
}
if (getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
xvel *= -1;
}
if (!((SwipeableListItem) swipeView).isSwipeToPrimaryActionEnabled()) {
if (((RevealableListItem) swipeToRevealLayout).getPrimaryActionSwipeMode()
== RevealableListItem.PRIMARY_ACTION_SWIPE_DISABLED) {
if (xvel > DEFAULT_SIGNIFICANT_VEL_THRESHOLD) { // A fast fling to the right
return STATE_CLOSED;
}
@ -371,11 +376,18 @@ public class ListItemLayout extends FrameLayout {
}
// Swipe to action is supported
boolean swipeToPrimaryActionDirect =
((RevealableListItem) swipeToRevealLayout).getPrimaryActionSwipeMode()
== RevealableListItem.PRIMARY_ACTION_SWIPE_DIRECT;
if (xvel > DEFAULT_SIGNIFICANT_VEL_THRESHOLD) { // A fast fling to the right
return lastStableSwipeState == STATE_SWIPE_PRIMARY_ACTION ? STATE_OPEN : STATE_CLOSED;
return lastStableSwipeState == STATE_SWIPE_PRIMARY_ACTION
? (swipeToPrimaryActionDirect ? STATE_CLOSED : STATE_OPEN)
: STATE_CLOSED;
}
if (xvel < -DEFAULT_SIGNIFICANT_VEL_THRESHOLD) { // A fast fling to the left
return lastStableSwipeState == STATE_CLOSED ? STATE_OPEN : STATE_SWIPE_PRIMARY_ACTION;
return lastStableSwipeState == STATE_CLOSED
? (swipeToPrimaryActionDirect ? STATE_SWIPE_PRIMARY_ACTION : STATE_OPEN)
: STATE_SWIPE_PRIMARY_ACTION;
}
// Settle to the closest point if velocity is not significant
@ -385,7 +397,7 @@ public class ListItemLayout extends FrameLayout {
}
if (Math.abs(swipeView.getLeft() - getSwipeRevealViewRevealedOffset())
< Math.abs(swipeView.getLeft() - getSwipeViewClosedOffset())) {
return STATE_OPEN;
return swipeToPrimaryActionDirect ? STATE_SWIPE_PRIMARY_ACTION : STATE_OPEN;
}
return STATE_CLOSED;
}
@ -533,7 +545,11 @@ public class ListItemLayout extends FrameLayout {
((SwipeableListItem) contentView).onSwipe(revealViewOffset);
int fullSwipedOffset = getSwipeToActionOffset();
int fadeOutThreshold = (fullSwipedOffset + getSwipeRevealViewRevealedOffset()) / 2;
int fadeOutThreshold =
getSwipeRevealViewRevealedOffset() == getSwipeToActionOffset()
? (fullSwipedOffset + getSwipeViewClosedOffset()) / 2
: (fullSwipedOffset + getSwipeRevealViewRevealedOffset()) / 2;
float contentViewAlpha =
AnimationUtils.lerp(
/* startValue= */ 1f,
@ -573,9 +589,10 @@ public class ListItemLayout extends FrameLayout {
}
// If swipe to action is not supported but the swipe state to be set in
// STATE_SWIPE_PRIMARY_ACTION, we do nothing.
if (!(contentView instanceof SwipeableListItem)
if (!(swipeToRevealLayout instanceof RevealableListItem)
|| (swipeState == STATE_SWIPE_PRIMARY_ACTION
&& !((SwipeableListItem) contentView).isSwipeToPrimaryActionEnabled())) {
&& ((RevealableListItem) swipeToRevealLayout).getPrimaryActionSwipeMode()
== RevealableListItem.PRIMARY_ACTION_SWIPE_DISABLED)) {
return;
}
this.swipeState = swipeState;

View File

@ -59,6 +59,9 @@ public class ListItemRevealLayout extends ViewGroup implements RevealableListIte
private int originalWidthMeasureSpec = UNSET;
private int originalHeightMeasureSpec = UNSET;
@PrimaryActionSwipeMode
private int primaryActionSwipeMode;
public ListItemRevealLayout(Context context) {
this(context, null);
}
@ -86,6 +89,9 @@ public class ListItemRevealLayout extends ViewGroup implements RevealableListIte
attributes.getDimensionPixelSize(
R.styleable.ListItemRevealLayout_minChildWidth,
getResources().getDimensionPixelSize(R.dimen.m3_list_reveal_min_child_width));
primaryActionSwipeMode = attributes.getInt(
R.styleable.ListItemRevealLayout_primaryActionSwipeMode,
PRIMARY_ACTION_SWIPE_DISABLED);
attributes.recycle();
}
@ -120,7 +126,8 @@ public class ListItemRevealLayout extends ViewGroup implements RevealableListIte
} else if (childCount == 0) {
// If there's no children, just set to desired width without doing anything.
setMeasuredDimension(revealedWidth, intrinsicHeight);
} else if (revealedWidth > intrinsicWidth + overswipeAllowance
} else if (primaryActionSwipeMode != PRIMARY_ACTION_SWIPE_DISABLED
&& revealedWidth > intrinsicWidth + overswipeAllowance
&& fullRevealableWidth > intrinsicWidth) {
measureByGrowingPrimarySwipeAction(fullRevealableWidth);
} else {
@ -447,4 +454,22 @@ public class ListItemRevealLayout extends ViewGroup implements RevealableListIte
}
return null;
}
/**
* Sets the swipe-to-primary-action behavior of this RevealableListItem when swiping with a
* sibling {@link SwipeableListItem}.
*
* <p>Use {@link SwipeableListItem#onSwipeStateChanged(int)} to listen for when the primary
* action is triggered to initiate the action.
*/
@Override
public void setPrimaryActionSwipeMode(@PrimaryActionSwipeMode int primaryActionSwipeMode) {
this.primaryActionSwipeMode = primaryActionSwipeMode;
}
@Override
@PrimaryActionSwipeMode
public int getPrimaryActionSwipeMode() {
return primaryActionSwipeMode;
}
}

View File

@ -15,11 +15,46 @@
*/
package com.google.android.material.listitem;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import androidx.annotation.IntDef;
import androidx.annotation.Px;
import androidx.annotation.RestrictTo;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/** Interface for the part of a ListItem that is able to be revealed when swiped. */
public interface RevealableListItem {
/** Disable the primary action. */
int PRIMARY_ACTION_SWIPE_DISABLED = 0;
/**
* When swiping with a sibling {@link SwipeableListItem}, allow swiping to intermediary states
* before the primary action.
*/
int PRIMARY_ACTION_SWIPE_INDIRECT = 1;
/**
* When swiping with a sibling {@link SwipeableListItem}, swipe directly to the primary action.
*/
int PRIMARY_ACTION_SWIPE_DIRECT = 2;
/**
* Mode which defines the behavior when swiping to reveal the primary action of the
* RevealableListItem.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
@IntDef({
PRIMARY_ACTION_SWIPE_DISABLED,
PRIMARY_ACTION_SWIPE_INDIRECT,
PRIMARY_ACTION_SWIPE_DIRECT,
})
@Retention(RetentionPolicy.SOURCE)
@interface PrimaryActionSwipeMode {}
/**
* Sets the revealed width of RevealableListItem, in pixels.
*/
@ -30,4 +65,17 @@ public interface RevealableListItem {
* has not yet been measured.
*/
@Px int getIntrinsicWidth();
/**
* Returns the {@link PrimaryActionSwipeMode} for the RevealableListItem that defines the swipe
* to primary action behavior when swiping with a sibling {@link SwipeableListItem}.
*/
@PrimaryActionSwipeMode
int getPrimaryActionSwipeMode();
/**
* Sets the {@link PrimaryActionSwipeMode} for the RevealableListItem that defines the swipe
* to primary action behavior when swiping with a sibling {@link SwipeableListItem}.
*/
void setPrimaryActionSwipeMode(@PrimaryActionSwipeMode int swipeToPrimaryActionMode);
}

View File

@ -93,9 +93,6 @@ public interface SwipeableListItem {
*/
boolean isSwipeEnabled();
/**
* Whether or not to allow the SwipeableListItem can be fully swiped to trigger the primary
* action.
*/
boolean isSwipeToPrimaryActionEnabled();
/** Sets whether or not to allow the SwipeableListItem to be swiped. */
void setSwipeEnabled(boolean swipeEnabled);
}

View File

@ -21,8 +21,8 @@
<public name="state_swiped" type="attr" />
<public name="minChildWidth" type="attr"/>
<public name="swipeToPrimaryActionEnabled" type="attr"/>
<public name="swipeEnabled" type="attr"/>
<public name="primaryActionSwipeMode" type="attr"/>
<public name="Widget.Material3.ListItemLayout" type="style"/>
<public name="ThemeOverlay.Material3.ListItemLayout.Segmented" type="style"/>

View File

@ -25,8 +25,6 @@
<attr name="listItemRevealLayoutStyle" format="reference"/>
<declare-styleable name="ListItemCardView">
<!-- Whether or not to enable the swipe to action. -->
<attr name="swipeToPrimaryActionEnabled" format="boolean"/>
<!-- Whether or not to enable swiping, if a sibling RevealableListItem exists. -->
<attr name="swipeEnabled" format="boolean"/>
</declare-styleable>
@ -39,6 +37,15 @@
<declare-styleable name="ListItemRevealLayout">
<!-- Minimum width any children are measured as. -->
<attr name="minChildWidth" format="dimension" />
<!-- Defines the behavior when swiping to reveal the primary action of the ListItemRevealLayout. -->
<attr name="primaryActionSwipeMode">
<!-- The primary action is disabled. -->
<enum name="disabled" value="0"/>
<!-- Swiping reveals intermediary states before fully revealing the primary action. -->
<enum name="indirect" value="1"/>
<!-- Swiping directly reveals the primary action without stopping at intermediary states. -->
<enum name="direct" value="2"/>
</attr>
</declare-styleable>
<!-- Shape appearance of a single item in the list. -->

View File

@ -34,7 +34,6 @@
<item name="contentPaddingBottom">10dp</item>
<item name="contentPaddingLeft">16dp</item>
<item name="contentPaddingRight">16dp</item>
<item name="swipeToPrimaryActionEnabled">false</item>
<item name="swipeEnabled">true</item>
</style>
@ -46,6 +45,7 @@
<item name="enforceMaterialTheme">true</item>
<item name="materialThemeOverlay">@style/ThemeOverlay.Material3.ListItemRevealLayout</item>
<item name="minChildWidth">@dimen/m3_list_reveal_min_child_width</item>
<item name="primaryActionSwipeMode">disabled</item>
</style>
<style name="Widget.Material3Expressive.ListItemRevealLayout" parent="Widget.Material3.ListItemRevealLayout">