Skip to content

Commit 99e09b0

Browse files
afohrmandsn5ft
authored andcommitted
[Adaptive] [Side Sheets] Add SideSheetCallback listener to SideSheetBehavior to track @SheetState state change events.
Includes a fix for a really strange issue where setting the background color from the callback worked, but if setText was called, it would cause the sheet to flash off the screen when STATE_EXPANDED was reached. PiperOrigin-RevId: 493409073 (cherry picked from commit 2468d6c)
1 parent 697156a commit 99e09b0

File tree

6 files changed

+277
-1
lines changed

6 files changed

+277
-1
lines changed

lib/java/com/google/android/material/sidesheet/RightSheetDelegate.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,4 +198,12 @@ boolean isSettling(View child, int state, boolean isReleasingView) {
198198
<V extends View> int getOutwardEdge(@NonNull V child) {
199199
return child.getLeft();
200200
}
201+
202+
@Override
203+
float calculateSlideOffsetBasedOnOutwardEdge(int left) {
204+
float hiddenOffset = getHiddenOffset();
205+
float sheetWidth = hiddenOffset - getExpandedOffset();
206+
207+
return (hiddenOffset - left) / sheetWidth;
208+
}
201209
}

lib/java/com/google/android/material/sidesheet/SheetDelegate.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,13 @@ abstract <V extends View> void setTargetStateOnNestedPreScroll(
9393
* this would return {@code child.getLeft()}.
9494
*/
9595
abstract <V extends View> int getOutwardEdge(@NonNull V child);
96+
97+
/**
98+
* Returns the calculated slide offset based on which edge of the screen the sheet is based on.
99+
* The offset value increases as the sheet moves towards the outward edge.
100+
*
101+
* @return slide offset represented as a float value between 0 and 1. A value of 0 means that the
102+
* sheet is hidden and a value of 1 means that the sheet is fully expanded.
103+
*/
104+
abstract float calculateSlideOffsetBasedOnOutwardEdge(int outwardEdge);
96105
}

lib/java/com/google/android/material/sidesheet/SideSheetBehavior.java

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,15 @@
5252
import androidx.core.view.accessibility.AccessibilityViewCommand;
5353
import androidx.customview.view.AbsSavedState;
5454
import androidx.customview.widget.ViewDragHelper;
55+
import com.google.android.material.internal.ViewUtils;
5556
import com.google.android.material.resources.MaterialResources;
5657
import com.google.android.material.shape.MaterialShapeDrawable;
5758
import com.google.android.material.shape.ShapeAppearanceModel;
5859
import java.lang.ref.WeakReference;
5960
import java.util.HashMap;
61+
import java.util.LinkedHashSet;
6062
import java.util.Map;
63+
import java.util.Set;
6164

6265
/**
6366
* An interaction behavior plugin for a child view of {@link CoordinatorLayout} to make it work as a
@@ -123,6 +126,8 @@ public class SideSheetBehavior<V extends View> extends CoordinatorLayout.Behavio
123126
private int initialX;
124127
private int initialY;
125128

129+
@NonNull private final Set<SideSheetCallback> callbacks = new LinkedHashSet<>();
130+
126131
private boolean touchingScrollingChild;
127132

128133
@Nullable private Map<View, Integer> importantForAccessibilityMap;
@@ -319,6 +324,11 @@ public boolean onLayoutChild(
319324
ViewCompat.offsetLeftAndRight(child, currentOffset);
320325

321326
nestedScrollingChildRef = new WeakReference<>(findScrollingChild(child));
327+
328+
for (SideSheetCallback callback : callbacks) {
329+
callback.onLayout(child);
330+
}
331+
322332
return true;
323333
}
324334

@@ -335,7 +345,11 @@ private int calculateCurrentOffset(int savedOutwardEdge, V child) {
335345

336346
switch (state) {
337347
case STATE_EXPANDED:
338-
currentOffset = savedOutwardEdge;
348+
// TODO (b/261619910): This is a workaround for a bug where the expanded offset was getting
349+
// recalculated if onLayoutChild() was called while the sheet was in the process of
350+
// expanding/offsetting. Revisit this and refactor if necessary when adding left based
351+
// sheets.
352+
currentOffset = ViewUtils.isLayoutRtl(child) ? getExpandedOffset() : 0;
339353
break;
340354
case STATE_DRAGGING:
341355
case STATE_SETTLING:
@@ -494,6 +508,7 @@ public void onNestedPreScroll(
494508
}
495509
sheetDelegate.setTargetStateOnNestedPreScroll(
496510
coordinatorLayout, child, target, dx, dy, consumed, type);
511+
dispatchOnSlide(child, sheetDelegate.getOutwardEdge(child));
497512
lastNestedScrollDx = dx;
498513
nestedScrolled = true;
499514
}
@@ -601,6 +616,24 @@ float getHideThreshold() {
601616
return HIDE_THRESHOLD;
602617
}
603618

619+
/**
620+
* Adds a callback to be notified of side sheet events.
621+
*
622+
* @param callback The callback to notify when side sheet events occur.
623+
*/
624+
public void addCallback(@NonNull SideSheetCallback callback) {
625+
callbacks.add(callback);
626+
}
627+
628+
/**
629+
* Removes a previously added callback.
630+
*
631+
* @param callback The callback to remove.
632+
*/
633+
public void removeCallback(@NonNull SideSheetCallback callback) {
634+
callbacks.remove(callback);
635+
}
636+
604637
/**
605638
* Sets the state of the sheet. The sheet will transition to that state with animation.
606639
*
@@ -679,6 +712,10 @@ void setStateInternal(@SheetState int state) {
679712
updateImportantForAccessibility(false);
680713
}
681714

715+
for (SideSheetCallback callback : callbacks) {
716+
callback.onStateChanged(sheet, state);
717+
}
718+
682719
updateAccessibilityActions();
683720
}
684721

@@ -794,6 +831,12 @@ public boolean tryCaptureView(@NonNull View child, int pointerId) {
794831
return viewRef != null && viewRef.get() == child;
795832
}
796833

834+
@Override
835+
public void onViewPositionChanged(
836+
@NonNull View changedView, int left, int top, int dx, int dy) {
837+
dispatchOnSlide(changedView, left);
838+
}
839+
797840
@Override
798841
public void onViewDragStateChanged(@SheetState int state) {
799842
if (state == ViewDragHelper.STATE_DRAGGING && draggable) {
@@ -825,6 +868,15 @@ public int getViewHorizontalDragRange(@NonNull View child) {
825868
}
826869
};
827870

871+
private void dispatchOnSlide(@NonNull View child, int outwardEdge) {
872+
if (!callbacks.isEmpty()) {
873+
float slideOffset = sheetDelegate.calculateSlideOffsetBasedOnOutwardEdge(outwardEdge);
874+
for (SideSheetCallback callback : callbacks) {
875+
callback.onSlide(child, slideOffset);
876+
}
877+
}
878+
}
879+
828880
/**
829881
* Checks whether a nested scroll should be enabled. If {@code false} all nested scrolls will be
830882
* consumed by the side sheet.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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 com.google.android.material.sidesheet.Sheet.SheetState;
22+
23+
/** Callback that monitors side sheet events. */
24+
public abstract class SideSheetCallback {
25+
26+
/**
27+
* Called when the sheet changes its state.
28+
*
29+
* @param sheet The sheet view.
30+
* @param newState The new state. This should be one of {@link SideSheetBehavior#STATE_DRAGGING},
31+
* {@link SideSheetBehavior#STATE_SETTLING}, {@link SideSheetBehavior#STATE_EXPANDED} or
32+
* {@link SideSheetBehavior#STATE_HIDDEN}.
33+
*/
34+
public abstract void onStateChanged(@NonNull View sheet, @SheetState int newState);
35+
36+
/**
37+
* Called when the sheet is being dragged.
38+
*
39+
* @param sheet The sheet view.
40+
* @param slideOffset The new offset of this sheet within [0,1] range. Offset increases as this
41+
* sheet is moving towards the outward edge. A value of 0 means that the sheet is hidden, and
42+
* a value of 1 means that the sheet is fully expanded.
43+
*/
44+
public abstract void onSlide(@NonNull View sheet, float slideOffset);
45+
46+
void onLayout(@NonNull View sheet) {}
47+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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 com.google.android.material.test.R;
20+
21+
import static android.graphics.Color.RED;
22+
import static com.google.common.truth.Truth.assertThat;
23+
import static org.robolectric.Shadows.shadowOf;
24+
25+
import android.graphics.drawable.ColorDrawable;
26+
import android.os.Bundle;
27+
import android.os.Looper;
28+
import androidx.appcompat.app.AppCompatActivity;
29+
import android.view.View;
30+
import androidx.annotation.NonNull;
31+
import androidx.coordinatorlayout.widget.CoordinatorLayout;
32+
import androidx.core.view.ViewCompat;
33+
import org.junit.Before;
34+
import org.junit.Test;
35+
import org.junit.runner.RunWith;
36+
import org.robolectric.Robolectric;
37+
import org.robolectric.RobolectricTestRunner;
38+
39+
/** Tests for {@link com.google.android.material.sidesheet.SideSheetBehavior}. */
40+
@RunWith(RobolectricTestRunner.class)
41+
public class SideSheetCallbackTest {
42+
43+
private View sideSheet;
44+
private SideSheetBehavior<View> sideSheetBehavior;
45+
46+
@Before
47+
public void setUp() throws Exception {
48+
AppCompatActivity activity = Robolectric.buildActivity(TestActivity.class).setup().get();
49+
CoordinatorLayout coordinatorLayout =
50+
(CoordinatorLayout) activity.getLayoutInflater().inflate(R.layout.test_side_sheet, null);
51+
sideSheet = coordinatorLayout.findViewById(R.id.test_side_sheet_container);
52+
sideSheetBehavior = SideSheetBehavior.from(sideSheet);
53+
54+
activity.setContentView(coordinatorLayout);
55+
56+
// Wait until the layout is measured.
57+
shadowOf(Looper.getMainLooper()).idle();
58+
}
59+
60+
@Test
61+
public void test_setSheetRedOnExpandWithCallback_sheetIsRedOnExpand() {
62+
// Create a callback and add it to the side sheet behavior.
63+
sideSheetBehavior.addCallback(
64+
new SideSheetCallback() {
65+
@Override
66+
public void onStateChanged(@NonNull View sheet, int newState) {
67+
if (newState == SideSheetBehavior.STATE_EXPANDED) {
68+
ViewCompat.setBackground(sideSheet, new ColorDrawable(RED));
69+
}
70+
}
71+
72+
@Override
73+
public void onSlide(@NonNull View sheet, float slideOffset) {}
74+
});
75+
76+
sideSheetBehavior.expand();
77+
shadowOf(Looper.getMainLooper()).idle();
78+
79+
assertThat(sideSheetBehavior.getState()).isEqualTo(SideSheetBehavior.STATE_EXPANDED);
80+
assertThat(sideSheet.getBackground()).isInstanceOf(ColorDrawable.class);
81+
assertThat(((ColorDrawable) sideSheet.getBackground()).getColor()).isEqualTo(RED);
82+
}
83+
84+
@Test
85+
public void test_removeCallback_callbackIsRemoved() {
86+
SideSheetCallback sideSheetCallback = createExpandedRedSideSheetCallback();
87+
// Ensure that side sheet doesn't already have a background.
88+
ViewCompat.setBackground(sideSheet, null);
89+
90+
sideSheetBehavior.addCallback(sideSheetCallback);
91+
sideSheetBehavior.removeCallback(sideSheetCallback);
92+
93+
sideSheetBehavior.expand();
94+
shadowOf(Looper.getMainLooper()).idle();
95+
96+
assertThat(sideSheetBehavior.getState()).isEqualTo(SideSheetBehavior.STATE_EXPANDED);
97+
assertThat(sideSheet.getBackground()).isNull();
98+
}
99+
100+
private SideSheetCallback createExpandedRedSideSheetCallback() {
101+
return new SideSheetCallback() {
102+
@Override
103+
public void onStateChanged(@NonNull View sheet, int newState) {
104+
if (newState == SideSheetBehavior.STATE_EXPANDED) {
105+
ViewCompat.setBackground(sideSheet, new ColorDrawable(RED));
106+
}
107+
}
108+
109+
@Override
110+
public void onSlide(@NonNull View sheet, float slideOffset) {}
111+
};
112+
}
113+
114+
private static class TestActivity extends AppCompatActivity {
115+
@Override
116+
protected void onCreate(Bundle bundle) {
117+
super.onCreate(bundle);
118+
setTheme(R.style.Theme_Material3_Light_NoActionBar);
119+
}
120+
}
121+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
Copyright 2022 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+
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
18+
xmlns:app="http://schemas.android.com/apk/res-auto"
19+
xmlns:tools="http://schemas.android.com/tools"
20+
android:id="@+id/test_coordinator_layout"
21+
android:layout_width="match_parent"
22+
android:layout_height="match_parent"
23+
android:fitsSystemWindows="true">
24+
25+
<LinearLayout
26+
android:id="@+id/test_side_sheet_container"
27+
style="@style/Widget.Material3.SideSheet"
28+
android:layout_width="256dp"
29+
android:layout_height="match_parent"
30+
android:orientation="vertical"
31+
app:layout_behavior="@string/side_sheet_behavior"
32+
tools:targetApi="lollipop">
33+
<TextView
34+
android:layout_width="wrap_content"
35+
android:layout_height="match_parent"
36+
android:text="Test Side Sheet Content" />
37+
</LinearLayout>
38+
39+
</androidx.coordinatorlayout.widget.CoordinatorLayout>

0 commit comments

Comments
 (0)