Skip to content

Commit 10d72ee

Browse files
afohrmanpaulfthomas
authored andcommitted
[Adaptive][Side Sheets] Added initial modal side sheet.
PiperOrigin-RevId: 482846478
1 parent 0c204b8 commit 10d72ee

27 files changed

+2592
-0
lines changed

lib/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ def srcDirs = [
7272
'com/google/android/material/ripple',
7373
'com/google/android/material/shape',
7474
'com/google/android/material/shadow',
75+
'com/google/android/material/sidesheet',
7576
'com/google/android/material/slider',
7677
'com/google/android/material/snackbar',
7778
'com/google/android/material/stateful',
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
/*
2+
* Copyright (C) 2022 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+
17+
package com.google.android.material.sidesheet;
18+
19+
import static com.google.android.material.sidesheet.Sheet.STATE_DRAGGING;
20+
import static com.google.android.material.sidesheet.Sheet.STATE_EXPANDED;
21+
import static com.google.android.material.sidesheet.Sheet.STATE_HIDDEN;
22+
import static java.lang.Math.max;
23+
24+
import android.view.View;
25+
import androidx.annotation.NonNull;
26+
import androidx.coordinatorlayout.widget.CoordinatorLayout;
27+
import androidx.core.view.ViewCompat;
28+
import androidx.customview.widget.ViewDragHelper;
29+
import com.google.android.material.sidesheet.Sheet.SheetEdge;
30+
import com.google.android.material.sidesheet.Sheet.StableSheetState;
31+
32+
/**
33+
* A delegate for {@link SideSheetBehavior} to handle positioning logic for sheets based on the
34+
* right edge of the screen that expand from right to left.
35+
*/
36+
final class RightSheetDelegate extends SheetDelegate {
37+
38+
final SideSheetBehavior<? extends View> sheetBehavior;
39+
40+
RightSheetDelegate(@NonNull SideSheetBehavior<? extends View> sheetBehavior) {
41+
this.sheetBehavior = sheetBehavior;
42+
}
43+
44+
@SheetEdge
45+
@Override
46+
int getSheetEdge() {
47+
return SideSheetBehavior.RIGHT;
48+
}
49+
50+
/** Returns the sheet's offset in pixels from the origin edge when hidden. */
51+
@Override
52+
int getHiddenOffset() {
53+
// Return the parent's width in pixels, which results in the sheet being offset entirely off of
54+
// screen.
55+
return sheetBehavior.getParentWidth();
56+
}
57+
58+
/** Returns the sheet's offset in pixels from the origin edge when expanded. */
59+
@Override
60+
int getExpandedOffset() {
61+
// Calculate the expanded offset based on the width of the content.
62+
return max(0, getHiddenOffset() - sheetBehavior.getChildWidth());
63+
}
64+
65+
/** Whether the view has been released from a drag close to the origin edge. */
66+
private boolean isReleasedCloseToOriginEdge(@NonNull View releasedChild) {
67+
// To be considered released close to the origin (right) edge, the released child's left must
68+
// be at least halfway to the origin (right) edge.
69+
return releasedChild.getLeft() > (getHiddenOffset() - getExpandedOffset()) / 2;
70+
}
71+
72+
@Override
73+
@StableSheetState
74+
int calculateTargetStateOnViewReleased(
75+
@NonNull View releasedChild, float xVelocity, float yVelocity) {
76+
@StableSheetState int targetState;
77+
if (xVelocity < 0) { // Moving left, expanding outwards.
78+
targetState = STATE_EXPANDED;
79+
80+
} else if (shouldHide(releasedChild, xVelocity)) {
81+
// Hide if the view was either released close to the origin/right edge or it was a significant
82+
// horizontal swipe; otherwise settle to expanded state.
83+
if (isSwipeSignificant(xVelocity, yVelocity) || isReleasedCloseToOriginEdge(releasedChild)) {
84+
targetState = STATE_HIDDEN;
85+
} else {
86+
targetState = STATE_EXPANDED;
87+
}
88+
} else if (xVelocity == 0f || !SheetUtils.isSwipeMostlyHorizontal(xVelocity, yVelocity)) {
89+
// If the X velocity is 0 or the swipe was mostly vertical, indicated by the Y
90+
// velocity being greater than the X velocity, settle to the nearest correct state.
91+
int currentLeft = releasedChild.getLeft();
92+
if (Math.abs(currentLeft - getExpandedOffset()) < Math.abs(currentLeft - getHiddenOffset())) {
93+
targetState = STATE_EXPANDED;
94+
} else {
95+
targetState = STATE_HIDDEN;
96+
}
97+
} else { // Moving right; collapse inwards and hide.
98+
targetState = STATE_HIDDEN;
99+
}
100+
return targetState;
101+
}
102+
103+
private boolean isSwipeSignificant(float xVelocity, float yVelocity) {
104+
return SheetUtils.isSwipeMostlyHorizontal(xVelocity, yVelocity)
105+
&& yVelocity > sheetBehavior.getSignificantVelocityThreshold();
106+
}
107+
108+
@Override
109+
<V extends View> void setTargetStateOnNestedPreScroll(
110+
@NonNull CoordinatorLayout coordinatorLayout,
111+
@NonNull V child,
112+
@NonNull View target,
113+
int dx,
114+
int dy,
115+
@NonNull int[] consumed,
116+
int type) {
117+
int currentLeft = child.getLeft();
118+
int newLeft = currentLeft - dx;
119+
if (dx < 0) { // Moving towards the left.
120+
if (newLeft > getExpandedOffset()) {
121+
consumed[1] = currentLeft - getExpandedOffset();
122+
ViewCompat.offsetLeftAndRight(child, -consumed[1]);
123+
sheetBehavior.setStateInternal(STATE_EXPANDED);
124+
} else {
125+
if (!sheetBehavior.isDraggable()) {
126+
// Prevent dragging
127+
return;
128+
}
129+
130+
consumed[1] = dx;
131+
ViewCompat.offsetLeftAndRight(child, -dx);
132+
sheetBehavior.setStateInternal(STATE_DRAGGING);
133+
}
134+
} else if (dx > 0) { // Moving towards the right.
135+
if (!target.canScrollHorizontally(-1)) {
136+
if (newLeft <= getHiddenOffset()) {
137+
if (!sheetBehavior.isDraggable()) {
138+
// Prevent dragging
139+
return;
140+
}
141+
142+
consumed[1] = dx;
143+
ViewCompat.offsetLeftAndRight(child, dx);
144+
sheetBehavior.setStateInternal(STATE_DRAGGING);
145+
} else {
146+
consumed[1] = currentLeft - getHiddenOffset();
147+
ViewCompat.offsetLeftAndRight(child, consumed[1]);
148+
sheetBehavior.setStateInternal(STATE_HIDDEN);
149+
}
150+
}
151+
}
152+
}
153+
154+
@Override
155+
@StableSheetState
156+
<V extends View> int calculateTargetStateOnStopNestedScroll(@NonNull V child) {
157+
@StableSheetState int targetState;
158+
if (sheetBehavior.getLastNestedScrollDx() > 0) {
159+
targetState = STATE_EXPANDED;
160+
} else if (sheetBehavior.shouldHide(child, sheetBehavior.getXVelocity())) {
161+
targetState = STATE_HIDDEN;
162+
} else if (sheetBehavior.getLastNestedScrollDx() == 0) {
163+
int currentLeft = child.getLeft();
164+
165+
if (Math.abs(currentLeft - getExpandedOffset()) < Math.abs(currentLeft - getHiddenOffset())) {
166+
targetState = STATE_EXPANDED;
167+
} else {
168+
targetState = STATE_HIDDEN;
169+
}
170+
} else {
171+
targetState = STATE_HIDDEN;
172+
}
173+
return targetState;
174+
}
175+
176+
@Override
177+
<V extends View> boolean hasReachedExpandedOffset(@NonNull V child) {
178+
return child.getLeft() == getExpandedOffset();
179+
}
180+
181+
@Override
182+
boolean shouldHide(@NonNull View child, float velocity) {
183+
final float newRight = child.getRight() + velocity * sheetBehavior.getHideFriction();
184+
return Math.abs(newRight) > sheetBehavior.getHideThreshold();
185+
}
186+
187+
@Override
188+
boolean isSettling(View child, int state, boolean isReleasingView) {
189+
int left = sheetBehavior.getOutwardEdgeOffsetForState(state);
190+
ViewDragHelper viewDragHelper = sheetBehavior.getViewDragHelper();
191+
return viewDragHelper != null
192+
&& (isReleasingView
193+
? viewDragHelper.settleCapturedViewAt(left, child.getTop())
194+
: viewDragHelper.smoothSlideViewTo(child, left, child.getTop()));
195+
}
196+
197+
@Override
198+
<V extends View> int getOutwardEdge(@NonNull V child) {
199+
return child.getLeft();
200+
}
201+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright (C) 2022 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+
17+
package com.google.android.material.sidesheet;
18+
19+
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20+
21+
import androidx.annotation.IntDef;
22+
import androidx.annotation.RestrictTo;
23+
import java.lang.annotation.Retention;
24+
import java.lang.annotation.RetentionPolicy;
25+
26+
/**
27+
* Interface for sheet constants and {@code IntDefs} to be shared between the different {@link
28+
* Sheet} implementations.
29+
*/
30+
public interface Sheet {
31+
32+
/** The sheet is dragging. */
33+
int STATE_DRAGGING = 1;
34+
35+
/** The sheet is settling. */
36+
int STATE_SETTLING = 2;
37+
38+
/** The sheet is expanded. */
39+
int STATE_EXPANDED = 3;
40+
41+
/** The sheet is hidden. */
42+
int STATE_HIDDEN = 5;
43+
44+
/**
45+
* States that a sheet can be in.
46+
*
47+
* <p>Note: {@link Sheet} implementations are not guaranteed to support all states.
48+
*/
49+
@RestrictTo(LIBRARY_GROUP)
50+
@IntDef({
51+
STATE_EXPANDED,
52+
STATE_DRAGGING,
53+
STATE_SETTLING,
54+
STATE_HIDDEN,
55+
})
56+
@Retention(RetentionPolicy.SOURCE)
57+
@interface SheetState {}
58+
59+
/**
60+
* Stable states that can be set by the a sheet's {@code setState(int)} method. These includes all
61+
* the possible states a sheet can be in when it's settled.
62+
*
63+
* @hide
64+
*/
65+
@RestrictTo(LIBRARY_GROUP)
66+
@IntDef({STATE_EXPANDED, STATE_HIDDEN})
67+
@Retention(RetentionPolicy.SOURCE)
68+
@interface StableSheetState {}
69+
70+
/** The sheet is based on the right edge; it slides from the right edge towards the left. */
71+
int RIGHT = 0;
72+
73+
/** The edge of the screen that a sheet slides out of. */
74+
@IntDef({RIGHT})
75+
@Retention(RetentionPolicy.SOURCE)
76+
@interface SheetEdge {}
77+
78+
/**
79+
* Gets the current state of the sheet.
80+
*
81+
* @return One of {@link #STATE_EXPANDED}, {@link #STATE_DRAGGING}, or {@link #STATE_SETTLING}.
82+
*/
83+
@SheetState
84+
int getState();
85+
86+
/** Sets the current state of the sheet. Must be one of {@link StableSheetState}. */
87+
void setState(@StableSheetState int state);
88+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright (C) 2022 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+
17+
package com.google.android.material.sidesheet;
18+
19+
import android.view.View;
20+
import androidx.annotation.NonNull;
21+
import androidx.coordinatorlayout.widget.CoordinatorLayout;
22+
import com.google.android.material.sidesheet.Sheet.SheetEdge;
23+
import com.google.android.material.sidesheet.Sheet.StableSheetState;
24+
import com.google.android.material.sidesheet.SideSheetBehavior.StateSettlingTracker;
25+
26+
/**
27+
* A delegate for {@link SideSheetBehavior} to handle logic specific to the sheet's edge position.
28+
*/
29+
abstract class SheetDelegate {
30+
31+
/**
32+
* Returns the edge of the screen in which the sheet is positioned. Must be a {@link SheetEdge}
33+
* value.
34+
*/
35+
@SheetEdge
36+
abstract int getSheetEdge();
37+
38+
/**
39+
* Determines whether the sheet is currently settling to a target {@link StableSheetState} using
40+
* {@link StateSettlingTracker}.
41+
*/
42+
abstract boolean isSettling(View child, int state, boolean isReleasingView);
43+
44+
/** Returns the sheet's offset from the origin edge when hidden. */
45+
abstract int getHiddenOffset();
46+
47+
/** Returns the sheet's offset from the origin edge when expanded. */
48+
abstract int getExpandedOffset();
49+
50+
/**
51+
* Calculates the target {@link StableSheetState} state of the sheet after it's released from a
52+
* drag, using the x and y velocity of the drag to determine the state.
53+
*
54+
* @return the target state
55+
*/
56+
@StableSheetState
57+
abstract int calculateTargetStateOnViewReleased(
58+
@NonNull View releasedChild, float xVelocity, float yVelocity);
59+
60+
/**
61+
* Sets the target sheet state from the {@link
62+
* SideSheetBehavior#onNestedPreScroll(CoordinatorLayout, View, View, int, int, int[], int)}
63+
* callback, based on scroll and position information provided by the method parameters.
64+
*/
65+
abstract <V extends View> void setTargetStateOnNestedPreScroll(
66+
@NonNull CoordinatorLayout coordinatorLayout,
67+
@NonNull V child,
68+
@NonNull View target,
69+
int dx,
70+
int dy,
71+
@NonNull int[] consumed,
72+
int type);
73+
74+
@StableSheetState
75+
abstract <V extends View> int calculateTargetStateOnStopNestedScroll(@NonNull V child);
76+
77+
/**
78+
* Whether the sheet has reached the expanded offset at the end of a nested scroll, used to
79+
* determine when to set the {@link SideSheetBehavior.SheetState} to {@link
80+
* SideSheetBehavior.SheetState#STATE_EXPANDED}.
81+
*/
82+
abstract <V extends View> boolean hasReachedExpandedOffset(@NonNull V child);
83+
84+
/**
85+
* Whether the sheet should hide, based on the position of child, velocity of the drag event, and
86+
* {@link SideSheetBehavior#getHideThreshold()}.
87+
*/
88+
abstract boolean shouldHide(@NonNull View child, float velocity);
89+
90+
/**
91+
* Returns the edge of the sheet that the sheet expands towards, calling the child parameter's
92+
* edge depending on which edge of the screen the sheet is positioned. For a right based sheet,
93+
* this would return {@code child.getLeft()}.
94+
*/
95+
abstract <V extends View> int getOutwardEdge(@NonNull V child);
96+
}

0 commit comments

Comments
 (0)