Skip to content

Commit c47e6a4

Browse files
Material Design Teamdsn5ft
authored andcommitted
Set threshold for hiding the bottom sheet so that dragging it down doesn't accidently dismiss it.
PiperOrigin-RevId: 274646607
1 parent 9c19d33 commit c47e6a4

File tree

2 files changed

+108
-14
lines changed

2 files changed

+108
-14
lines changed

lib/java/com/google/android/material/bottomsheet/BottomSheetBehavior.java

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,8 @@ public abstract static class BottomSheetCallback {
178178

179179
@SaveFlags private int saveFlags = SAVE_NONE;
180180

181+
private static final int SIGNIFICANT_VEL_THRESHOLD = 500;
182+
181183
private static final float HIDE_THRESHOLD = 0.5f;
182184

183185
private static final float HIDE_FRICTION = 0.1f;
@@ -1025,13 +1027,15 @@ private void updateDrawableForTargetState(@State int state) {
10251027
}
10261028
}
10271029

1028-
private void calculateCollapsedOffset() {
1029-
int peek;
1030+
private int calculatePeekHeight() {
10301031
if (peekHeightAuto) {
1031-
peek = Math.max(peekHeightMin, parentHeight - parentWidth * 9 / 16);
1032-
} else {
1033-
peek = peekHeight;
1032+
return Math.max(peekHeightMin, parentHeight - parentWidth * 9 / 16);
10341033
}
1034+
return peekHeight;
1035+
}
1036+
1037+
private void calculateCollapsedOffset() {
1038+
int peek = calculatePeekHeight();
10351039

10361040
if (fitToContents) {
10371041
collapsedOffset = Math.max(parentHeight - peek, fitToContentsOffset);
@@ -1080,8 +1084,9 @@ boolean shouldHide(@NonNull View child, float yvel) {
10801084
// It should not hide, but collapse.
10811085
return false;
10821086
}
1087+
int peek = calculatePeekHeight();
10831088
final float newTop = child.getTop() + yvel * HIDE_FRICTION;
1084-
return Math.abs(newTop - collapsedOffset) / (float) peekHeight > HIDE_THRESHOLD;
1089+
return Math.abs(newTop - collapsedOffset) / (float) peek > HIDE_THRESHOLD;
10851090
}
10861091

10871092
@Nullable
@@ -1228,6 +1233,11 @@ public void onViewDragStateChanged(int state) {
12281233
}
12291234
}
12301235

1236+
private boolean releasedLow(@NonNull View child) {
1237+
// Needs to be at least half way to the bottom.
1238+
return child.getTop() > (parentHeight + getExpandedOffset()) / 2;
1239+
}
1240+
12311241
@Override
12321242
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
12331243
int top;
@@ -1246,13 +1256,24 @@ public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel)
12461256
targetState = STATE_EXPANDED;
12471257
}
12481258
}
1249-
} else if (hideable
1250-
&& shouldHide(releasedChild, yvel)
1251-
&& (releasedChild.getTop() > collapsedOffset || Math.abs(xvel) < Math.abs(yvel))) {
1252-
// Hide if we shouldn't collapse and the view was either released low or it was a
1253-
// vertical swipe.
1254-
top = parentHeight;
1255-
targetState = STATE_HIDDEN;
1259+
} else if (hideable && shouldHide(releasedChild, yvel)) {
1260+
// Hide if the view was either released low or it was a significant vertical swipe
1261+
// otherwise settle to closest expanded state.
1262+
if ((Math.abs(xvel) < Math.abs(yvel) && yvel > SIGNIFICANT_VEL_THRESHOLD)
1263+
|| releasedLow(releasedChild)) {
1264+
top = parentHeight;
1265+
targetState = STATE_HIDDEN;
1266+
} else if (fitToContents) {
1267+
top = fitToContentsOffset;
1268+
targetState = STATE_EXPANDED;
1269+
} else if (Math.abs(releasedChild.getTop() - expandedOffset)
1270+
< Math.abs(releasedChild.getTop() - halfExpandedOffset)) {
1271+
top = expandedOffset;
1272+
targetState = STATE_EXPANDED;
1273+
} else {
1274+
top = halfExpandedOffset;
1275+
targetState = STATE_HALF_EXPANDED;
1276+
}
12561277
} else if (yvel == 0.f || Math.abs(xvel) > Math.abs(yvel)) {
12571278
// If the Y velocity is 0 or the swipe was mostly horizontal indicated by the X velocity
12581279
// being greater than the Y velocity, settle to the nearest correct height.
@@ -1331,7 +1352,7 @@ void dispatchOnSlide(int top) {
13311352
View bottomSheet = viewRef.get();
13321353
if (bottomSheet != null && !callbacks.isEmpty()) {
13331354
float slideOffset =
1334-
(top > collapsedOffset)
1355+
(top > collapsedOffset || collapsedOffset == getExpandedOffset())
13351356
? (float) (collapsedOffset - top) / (parentHeight - collapsedOffset)
13361357
: (float) (collapsedOffset - top) / (collapsedOffset - getExpandedOffset());
13371358
for (int i = 0; i < callbacks.size(); i++) {

tests/javatests/com/google/android/material/bottomsheet/BottomSheetBehaviorTest.java

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,79 @@ public void testSkipCollapsedFullyExpanded() throws Throwable {
428428
testSkipCollapsed();
429429
}
430430

431+
private void testSkipCollapsed_smallSwipe(int expectedState, float swipeViewHeightPercentage)
432+
throws Throwable {
433+
getBehavior().setSkipCollapsed(true);
434+
checkSetState(BottomSheetBehavior.STATE_EXPANDED, ViewMatchers.isDisplayed());
435+
Espresso.onView(ViewMatchers.withId(R.id.bottom_sheet))
436+
.perform(
437+
DesignViewActions.withCustomConstraints(
438+
new GeneralSwipeAction(
439+
Swipe.SLOW,
440+
// Manually calculate the starting coordinates to make sure that the touch
441+
// actually falls onto the view on Gingerbread
442+
view -> {
443+
int[] location = new int[2];
444+
view.getLocationInWindow(location);
445+
return new float[] {view.getWidth() / 2, location[1] + 1};
446+
},
447+
// Manually calculate the ending coordinates to make sure that the bottom
448+
// sheet is collapsed, not hidden
449+
view -> {
450+
return new float[] {
451+
// x: center of the bottom sheet
452+
view.getWidth() / 2,
453+
// y: some percentage down the view
454+
view.getHeight() * swipeViewHeightPercentage,
455+
};
456+
},
457+
Press.FINGER),
458+
ViewMatchers.isDisplayingAtLeast(5)));
459+
registerIdlingResourceCallback();
460+
try {
461+
if (expectedState == BottomSheetBehavior.STATE_HIDDEN) {
462+
Espresso.onView(ViewMatchers.withId(R.id.bottom_sheet))
463+
.check(ViewAssertions.matches(not(ViewMatchers.isDisplayed())));
464+
} else {
465+
Espresso.onView(ViewMatchers.withId(R.id.bottom_sheet))
466+
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()));
467+
}
468+
assertThat(getBehavior().getState(), is(expectedState));
469+
} finally {
470+
unregisterIdlingResourceCallback();
471+
}
472+
}
473+
474+
@Test
475+
@MediumTest
476+
public void testSkipCollapsed_smallSwipe_remainsExpanded() throws Throwable {
477+
testSkipCollapsed_smallSwipe(
478+
BottomSheetBehavior.STATE_EXPANDED, /* swipeViewHeightPercentage = */ 0.5f);
479+
}
480+
481+
@Test
482+
@MediumTest
483+
public void testSkipCollapsedFullyExpanded_smallSwipe_remainsExpanded() throws Throwable {
484+
getBehavior().setFitToContents(false);
485+
testSkipCollapsed_smallSwipe(
486+
BottomSheetBehavior.STATE_HALF_EXPANDED, /* swipeViewHeightPercentage = */ 0.5f);
487+
}
488+
489+
@Test
490+
@MediumTest
491+
public void testSkipCollapsed_smallSwipePastThreshold_getsHidden() throws Throwable {
492+
testSkipCollapsed_smallSwipe(
493+
BottomSheetBehavior.STATE_HIDDEN, /* swipeViewHeightPercentage = */ 0.75f);
494+
}
495+
496+
@Test
497+
@MediumTest
498+
public void testSkipCollapsedFullyExpanded_smallSwipePastThreshold_getsHidden() throws Throwable {
499+
getBehavior().setFitToContents(false);
500+
testSkipCollapsed_smallSwipe(
501+
BottomSheetBehavior.STATE_HIDDEN, /* swipeViewHeightPercentage = */ 0.75f);
502+
}
503+
431504
@Test
432505
@MediumTest
433506
public void testSwipeUpToExpand() {

0 commit comments

Comments
 (0)