WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit 1e6a8b7

Browse files
imhappipekingme
authored andcommitted
[Lists] Add dependency to androidx.customview for ViewDragHelper and added swiped states + settling
PiperOrigin-RevId: 822221345
1 parent 98031c4 commit 1e6a8b7

File tree

6 files changed

+207
-2
lines changed

6 files changed

+207
-2
lines changed

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ androidXCore = "1.16.0"
1616
androidXComposeMaterialIconsCore = "1.7.8"
1717
androidXComposeMaterialIconsExtended = "1.7.8"
1818
androidXComposeMaterial3 = "1.4.0-alpha14"
19+
androidXCustomView = "1.2.0"
1920
androidXDrawerLayout = "1.1.1"
2021
androidXDynamicAnimation = "1.1.0"
2122
androidXEspresso = "3.1.0"
@@ -59,6 +60,7 @@ androidx-core = { group = "androidx.core", name = "core", version.ref = "android
5960
androidx-compose-material-icons-core = { group = "androidx.compose.material", name = "material-icons-core", version.ref = "androidXComposeMaterialIconsCore" }
6061
androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "androidXComposeMaterialIconsExtended" }
6162
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "androidXComposeMaterial3" }
63+
androidx-customview = { group = "androidx.customview", name = "customview", version.ref = "androidXCustomView" }
6264
androidx-drawerlayout = { group = "androidx.drawerlayout", name = "drawerlayout", version.ref = "androidXDrawerLayout" }
6365
androidx-dynamicanimation = { group = "androidx.dynamicanimation", name = "dynamicanimation", version.ref = "androidXDynamicAnimation" }
6466
androidx-fragment = { group = "androidx.fragment", name = "fragment", version.ref = "androidXFragment" }

lib/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ dependencies {
1414
api libs.androidx.coordinatorlayout
1515
api libs.androidx.constraintlayout
1616
api libs.androidx.core
17+
api libs.androidx.customview
1718
api libs.androidx.drawerlayout
1819
api libs.androidx.dynamicanimation
1920
api libs.androidx.fragment

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

Lines changed: 136 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,30 @@
1717

1818
import 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;
2024
import static com.google.android.material.theme.overlay.MaterialThemeOverlay.wrap;
2125
import static java.lang.Math.max;
2226
import static java.lang.Math.min;
2327

28+
import android.animation.TimeInterpolator;
2429
import android.content.Context;
2530
import android.util.AttributeSet;
2631
import android.view.GestureDetector;
2732
import android.view.GestureDetector.SimpleOnGestureListener;
2833
import android.view.MotionEvent;
2934
import android.view.View;
3035
import android.view.ViewGroup;
36+
import android.view.animation.Interpolator;
37+
import android.view.animation.PathInterpolator;
3138
import android.widget.FrameLayout;
3239
import androidx.annotation.NonNull;
3340
import androidx.annotation.Nullable;
3441
import 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);

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ public int getIntrinsicWidth() {
258258

259259
@Override
260260
public void setRevealedWidth(int revealedWidth) {
261+
revealedWidth = max(0, revealedWidth);
261262
if (this.revealedWidth == revealedWidth) {
262263
return;
263264
}

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,13 @@
1515
*/
1616
package com.google.android.material.listitem;
1717

18+
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
19+
20+
import androidx.annotation.IntDef;
1821
import androidx.annotation.RestrictTo;
1922
import androidx.annotation.RestrictTo.Scope;
23+
import java.lang.annotation.Retention;
24+
import java.lang.annotation.RetentionPolicy;
2025

2126
/**
2227
* Interface for the part of a ListItem that is able to be swiped.
@@ -25,4 +30,46 @@
2530
*/
2631
@RestrictTo(Scope.LIBRARY_GROUP)
2732
public interface SwipeableListItem {
33+
/** The state at which the {@link SwipeableListItem} is being dragged. */
34+
public static final int STATE_DRAGGING = 1;
35+
36+
/** The state at which the {@link SwipeableListItem} is settling to the nearest settling point. */
37+
public static final int STATE_SETTLING = 2;
38+
39+
/**
40+
* The state at which the associated {@link RevealableListItem} is closed and nothing is revealed.
41+
*/
42+
public static final int STATE_CLOSED = 3;
43+
44+
/**
45+
* The state at which the associated {@link RevealableListItem} is revealed to its intrinsic
46+
* width.
47+
*/
48+
public static final int STATE_OPEN = 4;
49+
50+
/**
51+
* States that the {@link SwipeableListItem} can be in.
52+
*
53+
* @hide
54+
*/
55+
@RestrictTo(LIBRARY_GROUP)
56+
@IntDef({
57+
STATE_DRAGGING,
58+
STATE_SETTLING,
59+
STATE_CLOSED,
60+
STATE_OPEN,
61+
})
62+
@Retention(RetentionPolicy.SOURCE)
63+
@interface SwipeState {}
64+
65+
/**
66+
* Stable states that the {@link SwipeableListItem} can be in. These are states that the
67+
* {@link SwipeableListItem} can settle to.
68+
*
69+
* @hide
70+
*/
71+
@RestrictTo(LIBRARY_GROUP)
72+
@IntDef({STATE_CLOSED, STATE_OPEN})
73+
@Retention(RetentionPolicy.SOURCE)
74+
@interface StableSwipeState {}
2875
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
Copyright 2025 The Android Open Source Project
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
-->
17+
18+
<resources>
19+
<dimen name="m3_list_max_swipe_overshoot">16dp</dimen>
20+
</resources>

0 commit comments

Comments
 (0)