Skip to content

Commit 400d695

Browse files
imhappidsn5ft
authored andcommitted
[Lists] Add ListItemRevealLayout, ListItemCardView, and relevant interfaces to introduce swiping in ListItemLayout
PiperOrigin-RevId: 818827578
1 parent 04c849e commit 400d695

File tree

13 files changed

+531
-87
lines changed

13 files changed

+531
-87
lines changed

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

Lines changed: 63 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -14,47 +14,69 @@
1414
See the License for the specific language governing permissions and
1515
limitations under the License.
1616
-->
17-
<com.google.android.material.listitem.ListItemLayout xmlns:android="http://schemas.android.com/apk/res/android"
18-
xmlns:tools="http://schemas.android.com/tools"
19-
xmlns:app="http://schemas.android.com/apk/res-auto"
20-
android:layout_width="match_parent"
21-
android:layout_height="wrap_content"
22-
style="?attr/listItemLayoutSegmentedStyle"
23-
android:paddingHorizontal="8dp">
24-
<com.google.android.material.card.MaterialCardView
25-
android:id="@+id/cat_list_item_card_view"
26-
android:layout_width="match_parent"
17+
<com.google.android.material.listitem.ListItemLayout
18+
xmlns:android="http://schemas.android.com/apk/res/android"
19+
xmlns:app="http://schemas.android.com/apk/res-auto"
20+
xmlns:tools="http://schemas.android.com/tools"
2721
android:layout_height="wrap_content"
28-
android:checkable="true"
29-
android:clickable="true"
30-
android:focusable="true">
31-
32-
<LinearLayout
33-
android:orientation="horizontal"
34-
android:layout_width="match_parent"
35-
android:layout_height="wrap_content"
36-
android:gravity="center_vertical"
37-
tools:ignore="UseCompoundDrawables">
38-
39-
<ImageView
40-
android:id="@+id/cat_list_item_start_icon"
41-
android:layout_width="20dp"
42-
android:layout_height="20dp"
43-
android:layout_marginTop="16dp"
44-
android:layout_marginBottom="16dp"
45-
android:layout_marginStart="16dp"
46-
android:layout_marginEnd="16dp"
47-
style="@style/Widget.Material3.ImageView.ListItem.Leading"
48-
app:srcCompat="@drawable/ic_star_icon_checkable_24px"
49-
android:contentDescription="@string/cat_list_item_icon_content_description" />
50-
51-
<TextView
52-
android:id="@+id/cat_list_item_text"
53-
android:layout_width="0dp"
22+
android:layout_width="match_parent"
23+
android:paddingHorizontal="8dp">
24+
25+
<com.google.android.material.listitem.ListItemCardView
26+
android:id="@+id/cat_list_item_card_view"
27+
android:checkable="true"
28+
android:clickable="true"
29+
android:focusable="true"
30+
style="?attr/listItemCardViewSegmentedStyle"
5431
android:layout_height="wrap_content"
55-
android:layout_weight="1"
56-
android:textAppearance="?attr/textAppearanceBodyLarge"/>
57-
58-
</LinearLayout>
59-
</com.google.android.material.card.MaterialCardView>
32+
android:layout_width="match_parent">
33+
<LinearLayout
34+
android:gravity="center_vertical"
35+
android:layout_height="wrap_content"
36+
android:layout_width="match_parent"
37+
android:orientation="horizontal"
38+
tools:ignore="UseCompoundDrawables">
39+
<ImageView
40+
android:id="@+id/cat_list_item_start_icon"
41+
style="@style/Widget.Material3.ImageView.ListItem.Leading"
42+
android:contentDescription="@string/cat_list_item_icon_content_description"
43+
android:layout_height="20dp"
44+
android:layout_width="20dp"
45+
android:layout_marginBottom="16dp"
46+
android:layout_marginEnd="16dp"
47+
android:layout_marginStart="16dp"
48+
android:layout_marginTop="16dp"
49+
app:srcCompat="@drawable/ic_star_icon_checkable_24px" />
50+
<TextView
51+
android:id="@+id/cat_list_item_text"
52+
android:layout_height="wrap_content"
53+
android:layout_width="0dp"
54+
android:layout_weight="1"
55+
android:textAppearance="?attr/textAppearanceBodyLarge" />
56+
</LinearLayout>
57+
</com.google.android.material.listitem.ListItemCardView>
58+
59+
<com.google.android.material.listitem.ListItemRevealLayout
60+
android:layout_height="match_parent"
61+
android:layout_width="wrap_content">
62+
<com.google.android.material.button.MaterialButton
63+
style="?attr/materialIconButtonFilledStyle"
64+
android:insetLeft="0dp"
65+
android:insetRight="0dp"
66+
android:layout_height="56dp"
67+
android:layout_marginEnd="2dp"
68+
android:layout_marginStart="2dp"
69+
android:layout_width="wrap_content"
70+
app:icon="@drawable/ic_add_24px"
71+
app:iconGravity="top" />
72+
<com.google.android.material.button.MaterialButton
73+
style="?attr/materialIconButtonFilledStyle"
74+
android:insetLeft="0dp"
75+
android:insetRight="0dp"
76+
android:layout_height="56dp"
77+
android:layout_marginEnd="2dp"
78+
android:layout_width="wrap_content"
79+
app:icon="@drawable/ic_add_24px"
80+
app:iconGravity="top" />
81+
</com.google.android.material.listitem.ListItemRevealLayout>
6082
</com.google.android.material.listitem.ListItemLayout>

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
android:layout_height="wrap_content"
2121
xmlns:app="http://schemas.android.com/apk/res-auto"
2222
android:paddingHorizontal="8dp">
23-
<com.google.android.material.card.MaterialCardView
23+
<com.google.android.material.listitem.ListItemCardView
2424
android:id="@+id/cat_list_item_card_view"
2525
android:layout_width="match_parent"
2626
android:layout_height="wrap_content"
@@ -55,5 +55,5 @@
5555
android:textAppearance="?attr/textAppearanceBodyLarge"/>
5656

5757
</LinearLayout>
58-
</com.google.android.material.card.MaterialCardView>
58+
</com.google.android.material.listitem.ListItemCardView>
5959
</com.google.android.material.listitem.ListItemLayout>

lib/java/com/google/android/material/dialog/res/values/themes_base.xml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,8 @@
280280
<item name="floatingToolbarVibrantStyle">@style/Widget.Material3.FloatingToolbar.Vibrant</item>
281281
<item name="linearProgressIndicatorStyle">@style/Widget.Material3.LinearProgressIndicator</item>
282282
<item name="listItemLayoutStyle">@style/Widget.Material3.ListItemLayout</item>
283-
<item name="listItemLayoutSegmentedStyle">@style/Widget.Material3.ListItemLayout.Segmented</item>
283+
<item name="listItemCardViewStyle">@style/Widget.Material3.ListItemCardView</item>
284+
<item name="listItemCardViewSegmentedStyle">@style/Widget.Material3.ListItemCardView.Segmented</item>
284285
<item name="loadingIndicatorStyle">@style/Widget.Material3.LoadingIndicator</item>
285286
<item name="materialIconButtonStyle">@style/Widget.Material3.Button.IconButton</item>
286287
<item name="materialIconButtonFilledStyle">@style/Widget.Material3.Button.IconButton.Filled</item>
@@ -600,7 +601,8 @@
600601
<item name="floatingToolbarVibrantStyle">@style/Widget.Material3.FloatingToolbar.Vibrant</item>
601602
<item name="linearProgressIndicatorStyle">@style/Widget.Material3.LinearProgressIndicator</item>
602603
<item name="listItemLayoutStyle">@style/Widget.Material3.ListItemLayout</item>
603-
<item name="listItemLayoutSegmentedStyle">@style/Widget.Material3.ListItemLayout.Segmented</item>
604+
<item name="listItemCardViewStyle">@style/Widget.Material3.ListItemCardView</item>
605+
<item name="listItemCardViewSegmentedStyle">@style/Widget.Material3.ListItemCardView.Segmented</item>
604606
<item name="loadingIndicatorStyle">@style/Widget.Material3.LoadingIndicator</item>
605607
<item name="materialIconButtonStyle">@style/Widget.Material3.Button.IconButton</item>
606608
<item name="materialIconButtonFilledStyle">@style/Widget.Material3.Button.IconButton.Filled</item>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.android.material.listitem;
17+
18+
import com.google.android.material.R;
19+
20+
import static com.google.android.material.theme.overlay.MaterialThemeOverlay.wrap;
21+
22+
import android.content.Context;
23+
import android.util.AttributeSet;
24+
import com.google.android.material.card.MaterialCardView;
25+
26+
/**
27+
* A {@link MaterialCardView} that is styled as a list item and can be swiped in a
28+
* {@link ListItemLayout} with a sibling {@link RevealableListItem}.
29+
*/
30+
public class ListItemCardView extends MaterialCardView implements SwipeableListItem {
31+
32+
private static final int DEF_STYLE_RES = R.style.Widget_Material3_ListItemCardView;
33+
34+
public ListItemCardView(Context context) {
35+
this(context, null);
36+
}
37+
38+
public ListItemCardView(Context context, AttributeSet attrs) {
39+
this(context, attrs, R.attr.listItemCardViewStyle);
40+
}
41+
42+
public ListItemCardView(Context context, AttributeSet attrs, int defStyleAttr) {
43+
super(wrap(context, attrs, defStyleAttr, DEF_STYLE_RES), attrs, defStyleAttr);
44+
}
45+
}

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

Lines changed: 106 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import com.google.android.material.R;
1919

2020
import static com.google.android.material.theme.overlay.MaterialThemeOverlay.wrap;
21+
import static java.lang.Math.max;
22+
import static java.lang.Math.min;
2123

2224
import android.content.Context;
2325
import android.util.AttributeSet;
@@ -30,7 +32,6 @@
3032
import androidx.annotation.NonNull;
3133
import androidx.annotation.Nullable;
3234
import androidx.customview.widget.ViewDragHelper;
33-
import java.lang.ref.WeakReference;
3435

3536
/**
3637
* A container layout for a List item.
@@ -61,7 +62,13 @@ public class ListItemLayout extends FrameLayout {
6162

6263
@Nullable private ViewDragHelper viewDragHelper;
6364
@Nullable private GestureDetector gestureDetector;
64-
private WeakReference<ListItemRevealLayout> swipeToRevealLayoutRef;
65+
66+
private int revealViewOffset;
67+
private int originalContentViewLeft;
68+
69+
private View contentView;
70+
@Nullable private View swipeToRevealLayout;
71+
private boolean originalClipToPadding;
6572

6673
public ListItemLayout(@NonNull Context context) {
6774
this(context, null);
@@ -115,32 +122,54 @@ public void updateAppearance(int position, int itemCount) {
115122
@Override
116123
public void addView(View child, int index, ViewGroup.LayoutParams params) {
117124
super.addView(child, index, params);
118-
if (swipeToRevealLayoutRef != null
119-
&& swipeToRevealLayoutRef.get() != null
120-
&& child instanceof ListItemRevealLayout) {
125+
if (swipeToRevealLayout != null && child instanceof ListItemRevealLayout) {
121126
throw new UnsupportedOperationException(
122127
"Only one ListItemRevealLayout is supported in a ListItemLayout.");
123-
} else if (child instanceof ListItemRevealLayout) {
124-
swipeToRevealLayoutRef = new WeakReference<>((ListItemRevealLayout) child);
128+
} else if (child instanceof RevealableListItem) {
129+
swipeToRevealLayout = child;
130+
originalClipToPadding = getClipToPadding();
131+
setClipToPadding(false);
132+
// Start the reveal view at a desired width of 0
133+
((RevealableListItem) child).setRevealedWidth(0);
134+
// Make sure reveal view has lower elevation
135+
child.setElevation(getElevation() - 1);
125136
}
126137
}
127138

128139
@Override
129140
public void onViewRemoved(View child) {
130141
super.onViewRemoved(child);
131-
if (child instanceof ListItemRevealLayout) {
142+
if (child == swipeToRevealLayout) {
132143
viewDragHelper = null;
133144
gestureDetector = null;
134-
swipeToRevealLayoutRef = null;
145+
swipeToRevealLayout = null;
146+
setClipToPadding(originalClipToPadding);
147+
}
148+
}
149+
150+
private void ensureContentViewIfRevealLayoutExists() {
151+
if (contentView != null || swipeToRevealLayout == null) {
152+
return;
153+
}
154+
155+
int childCount = getChildCount();
156+
for (int i = 0; i < childCount; i++) {
157+
if (getChildAt(i) instanceof SwipeableListItem) {
158+
if (contentView != null) {
159+
throw new UnsupportedOperationException(
160+
"Only one SwipeableListItem view is allowed in a ListItemLayout.");
161+
}
162+
contentView = getChildAt(i);
163+
}
135164
}
136165
}
137166

138167
@Override
139168
public boolean onTouchEvent(MotionEvent ev) {
140-
if (ensureSwipeToRevealSetupIfNeeded() && viewDragHelper != null && gestureDetector != null) {
169+
if (ensureSwipeToRevealSetupIfNeeded()) {
141170
// TODO - b/447218120: Check that at least one child is a ListItemRevealLayout and the other
142171
// is List content.
143-
// Process the event
172+
// Process the event regardless of the event type.
144173
viewDragHelper.processTouchEvent(ev);
145174
gestureDetector.onTouchEvent(ev);
146175

@@ -176,7 +205,7 @@ public boolean onInterceptTouchEvent(MotionEvent ev) {
176205
* variables are initialized if true.
177206
*/
178207
private boolean ensureSwipeToRevealSetupIfNeeded() {
179-
if (swipeToRevealLayoutRef == null || swipeToRevealLayoutRef.get() == null) {
208+
if (swipeToRevealLayout == null) {
180209
return false;
181210
}
182211
if (viewDragHelper == null || gestureDetector == null) {
@@ -186,8 +215,50 @@ private boolean ensureSwipeToRevealSetupIfNeeded() {
186215
new ViewDragHelper.Callback() {
187216
@Override
188217
public boolean tryCaptureView(@NonNull View child, int pointerId) {
218+
if (swipeToRevealLayout != null && contentView != null) {
219+
viewDragHelper.captureChildView(contentView, pointerId);
220+
}
189221
return false;
190222
}
223+
224+
@Override
225+
public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
226+
// TODO:b/443153708 - Support RTL
227+
LayoutParams lp = (LayoutParams) swipeToRevealLayout.getLayoutParams();
228+
return max(
229+
min(left, originalContentViewLeft),
230+
originalContentViewLeft
231+
- ((RevealableListItem) swipeToRevealLayout).getIntrinsicWidth()
232+
- lp.leftMargin
233+
- lp.rightMargin);
234+
}
235+
236+
@Override
237+
public int getViewHorizontalDragRange(@NonNull View child) {
238+
return ((RevealableListItem) swipeToRevealLayout).getIntrinsicWidth();
239+
}
240+
241+
@Override
242+
public void onViewPositionChanged(
243+
@NonNull View changedView, int left, int top, int dx, int dy) {
244+
super.onViewPositionChanged(changedView, left, top, dx, dy);
245+
// TODO:b/443153708 - Support RTL
246+
revealViewOffset = left - originalContentViewLeft;
247+
248+
LayoutParams revealViewLp = (LayoutParams) swipeToRevealLayout.getLayoutParams();
249+
LayoutParams contentViewLp = (LayoutParams) contentView.getLayoutParams();
250+
251+
// Desired width is how much we've displaced the content view minus any margins.
252+
int revealViewDesiredWidth =
253+
max(
254+
0,
255+
originalContentViewLeft
256+
- contentView.getLeft()
257+
- contentViewLp.rightMargin // only end margin matters here
258+
- revealViewLp.leftMargin
259+
- revealViewLp.rightMargin);
260+
((RevealableListItem) swipeToRevealLayout).setRevealedWidth(revealViewDesiredWidth);
261+
}
191262
});
192263

193264
gestureDetector =
@@ -207,6 +278,29 @@ public boolean onScroll(
207278
}
208279
});
209280
}
281+
282+
ensureContentViewIfRevealLayoutExists();
283+
210284
return true;
211285
}
286+
287+
@Override
288+
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
289+
super.onLayout(changed, left, top, right, bottom);
290+
if (contentView != null && swipeToRevealLayout != null) {
291+
originalContentViewLeft = contentView.getLeft();
292+
int originalContentViewRight = contentView.getRight();
293+
contentView.offsetLeftAndRight(revealViewOffset);
294+
// We always lay out swipeToRevealLayout such that the right is aligned to where the original
295+
// content view's right was. Note that if the content view had a right margin, it will
296+
// effectively be passed onto the reveal view.
297+
LayoutParams lp = (LayoutParams) swipeToRevealLayout.getLayoutParams();
298+
// TODO:b/443153708 - Support RTL
299+
swipeToRevealLayout.layout(
300+
originalContentViewRight - lp.rightMargin - swipeToRevealLayout.getMeasuredWidth(),
301+
swipeToRevealLayout.getTop(),
302+
originalContentViewRight - lp.rightMargin,
303+
swipeToRevealLayout.getBottom());
304+
}
305+
}
212306
}

0 commit comments

Comments
 (0)