From 562285e051771a8f32624f6efe67da3ed1b932cc Mon Sep 17 00:00:00 2001 From: afohrman Date: Wed, 26 Apr 2023 14:35:21 -0400 Subject: [PATCH] [Predictive Back][Side Sheet] Added predictive back support for standard side sheets. PiperOrigin-RevId: 527326750 --- .../sidesheet/SideSheetMainDemoFragment.java | 146 +++++++++++++----- .../android/material/sidesheet/Sheet.java | 3 +- .../material/sidesheet/SideSheetBehavior.java | 82 ++++++++++ 3 files changed, 192 insertions(+), 39 deletions(-) diff --git a/catalog/java/io/material/catalog/sidesheet/SideSheetMainDemoFragment.java b/catalog/java/io/material/catalog/sidesheet/SideSheetMainDemoFragment.java index 30589939289..c60be2b129c 100644 --- a/catalog/java/io/material/catalog/sidesheet/SideSheetMainDemoFragment.java +++ b/catalog/java/io/material/catalog/sidesheet/SideSheetMainDemoFragment.java @@ -20,6 +20,7 @@ import static android.view.View.NO_ID; +import android.os.Build.VERSION_CODES; import android.os.Bundle; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; @@ -30,11 +31,14 @@ import android.view.ViewGroup; import android.widget.Button; import android.widget.TextView; +import android.window.BackEvent; +import androidx.activity.OnBackPressedCallback; import androidx.annotation.GravityInt; import androidx.annotation.IdRes; import androidx.annotation.LayoutRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.annotation.StringRes; import androidx.annotation.StyleRes; import androidx.coordinatorlayout.widget.CoordinatorLayout.LayoutParams; @@ -96,7 +100,8 @@ public View onCreateDemoView( view, R.id.standard_side_sheet_container, R.id.show_standard_side_sheet_button, - R.id.close_icon_button); + R.id.close_icon_button, + /* shouldHandleBack= */ true); setSideSheetCallback( standardRightSideSheet, R.id.side_sheet_state_text, R.id.side_sheet_slide_offset_text); @@ -107,7 +112,8 @@ public View onCreateDemoView( view, R.id.standard_detached_side_sheet_container, R.id.show_standard_detached_side_sheet_button, - R.id.detached_close_icon_button); + R.id.detached_close_icon_button, + /* shouldHandleBack= */ true); setSideSheetCallback( detachedStandardSideSheet, @@ -120,7 +126,8 @@ public View onCreateDemoView( view, R.id.vertically_scrolling_side_sheet_container, R.id.show_vertically_scrolling_side_sheet_button, - R.id.vertically_scrolling_side_sheet_close_icon_button); + R.id.vertically_scrolling_side_sheet_close_icon_button, + /* shouldHandleBack= */ true); setSideSheetCallback( verticallyScrollingSideSheet, @@ -141,7 +148,8 @@ public View onCreateDemoView( view, R.id.coplanar_side_sheet_container, R.id.show_coplanar_side_sheet_button, - R.id.coplanar_side_sheet_close_icon_button); + R.id.coplanar_side_sheet_close_icon_button, + /* shouldHandleBack= */ false); setSideSheetCallback( coplanarSideSheet, @@ -154,7 +162,8 @@ public View onCreateDemoView( view, R.id.coplanar_detached_side_sheet_container, R.id.show_coplanar_detached_side_sheet_button, - R.id.coplanar_detached_side_sheet_close_icon_button); + R.id.coplanar_detached_side_sheet_close_icon_button, + /* shouldHandleBack= */ false); setSideSheetCallback( detachedCoplanarSideSheet, @@ -165,8 +174,7 @@ public View onCreateDemoView( } private void setUpSheetGravityButtonToggleGroup(@NonNull View view) { - sheetGravityButtonToggleGroup = - view.findViewById(R.id.sheet_gravity_button_toggle_group); + sheetGravityButtonToggleGroup = view.findViewById(R.id.sheet_gravity_button_toggle_group); // Check the button corresponding to end sheet gravity, which is the default. sheetGravityButtonToggleGroup.check(R.id.end_gravity_button); sheetGravityButtonToggleGroup.addOnButtonCheckedListener( @@ -185,11 +193,39 @@ private void setUpSheetGravityButtonToggleGroup(@NonNull View view) { }); } + private void setupBackHandling(SideSheetBehavior sideSheetBehavior) { + OnBackPressedCallback nonModalOnBackPressedCallback = + createNonModalOnBackPressedCallback(sideSheetBehavior); + requireActivity().getOnBackPressedDispatcher().addCallback(this, nonModalOnBackPressedCallback); + sideSheetBehavior.addCallback( + new SideSheetCallback() { + @Override + public void onStateChanged(@NonNull View sheet, int newState) { + switch (newState) { + case SideSheetBehavior.STATE_EXPANDED: + case SideSheetBehavior.STATE_SETTLING: + nonModalOnBackPressedCallback.setEnabled(true); + break; + case SideSheetBehavior.STATE_HIDDEN: + nonModalOnBackPressedCallback.setEnabled(false); + break; + case SideSheetBehavior.STATE_DRAGGING: + default: + break; + } + } + + @Override + public void onSlide(@NonNull View sheet, float slideOffset) {} + }); + } + private View setUpSideSheet( @NonNull View view, @IdRes int sideSheetContainerId, @IdRes int showSideSheetButtonId, - @IdRes int closeIconButtonId) { + @IdRes int closeIconButtonId, + boolean shouldHandleBack) { View sideSheet = view.findViewById(sideSheetContainerId); SideSheetBehavior sideSheetBehavior = SideSheetBehavior.from(sideSheet); @@ -199,6 +235,10 @@ private View setUpSideSheet( View standardSideSheetCloseIconButton = sideSheet.findViewById(closeIconButtonId); standardSideSheetCloseIconButton.setOnClickListener(v -> hideSideSheet(sideSheetBehavior)); + if (shouldHandleBack) { + setupBackHandling(sideSheetBehavior); + } + sideSheetViews.add(sideSheet); return sideSheet; @@ -258,37 +298,39 @@ private void setUpModalSheet( @StringRes int sheetTitleStringRes, @NonNull Button showSheetButton, @IdRes int closeIconButtonIdRes) { - showSheetButton.setOnClickListener(v1 -> { - SideSheetDialog sheetDialog = - sheetThemeOverlayRes == NO_ID - ? new SideSheetDialog(requireContext()) - : new SideSheetDialog(requireContext(), sheetThemeOverlayRes); - - sheetDialog.setContentView(sheetContentLayoutRes); - View modalSheetContent = sheetDialog.findViewById(sheetContentRootIdRes); - if (modalSheetContent != null) { - TextView modalSideSheetTitle = modalSheetContent.findViewById(sheetTitleIdRes); - modalSideSheetTitle.setText(sheetTitleStringRes); - } - new WindowPreferencesManager(requireContext()) - .applyEdgeToEdgePreference(sheetDialog.getWindow()); - - sheetDialog - .getBehavior() - .addCallback( - createSideSheetCallback( - sheetDialog.findViewById(R.id.side_sheet_state_text), - sheetDialog.findViewById(R.id.side_sheet_slide_offset_text))); - - sheetDialog.setSheetEdge(getGravityForIdRes(sheetGravityButtonToggleGroup.getCheckedButtonId())); - - View modalSideSheetCloseIconButton = sheetDialog.findViewById(closeIconButtonIdRes); - if (modalSideSheetCloseIconButton != null) { - modalSideSheetCloseIconButton.setOnClickListener(v2 -> sheetDialog.hide()); - } + showSheetButton.setOnClickListener( + v1 -> { + SideSheetDialog sheetDialog = + sheetThemeOverlayRes == NO_ID + ? new SideSheetDialog(requireContext()) + : new SideSheetDialog(requireContext(), sheetThemeOverlayRes); + + sheetDialog.setContentView(sheetContentLayoutRes); + View modalSheetContent = sheetDialog.findViewById(sheetContentRootIdRes); + if (modalSheetContent != null) { + TextView modalSideSheetTitle = modalSheetContent.findViewById(sheetTitleIdRes); + modalSideSheetTitle.setText(sheetTitleStringRes); + } + new WindowPreferencesManager(requireContext()) + .applyEdgeToEdgePreference(sheetDialog.getWindow()); + + sheetDialog + .getBehavior() + .addCallback( + createSideSheetCallback( + sheetDialog.findViewById(R.id.side_sheet_state_text), + sheetDialog.findViewById(R.id.side_sheet_slide_offset_text))); + + sheetDialog.setSheetEdge( + getGravityForIdRes(sheetGravityButtonToggleGroup.getCheckedButtonId())); + + View modalSideSheetCloseIconButton = sheetDialog.findViewById(closeIconButtonIdRes); + if (modalSideSheetCloseIconButton != null) { + modalSideSheetCloseIconButton.setOnClickListener(v2 -> sheetDialog.hide()); + } - sheetDialog.show(); - }); + sheetDialog.show(); + }); } private void setUpToolbar(@NonNull View view) { @@ -364,4 +406,32 @@ public void onSlide(@NonNull View sheet, float slideOffset) { } }; } + + private OnBackPressedCallback createNonModalOnBackPressedCallback( + SideSheetBehavior behavior) { + return new OnBackPressedCallback(/* enabled= */ false) { + @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE) + @Override + public void handleOnBackStarted(@NonNull BackEvent backEvent) { + behavior.startBackProgress(backEvent); + } + + @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE) + @Override + public void handleOnBackProgressed(@NonNull BackEvent backEvent) { + behavior.updateBackProgress(backEvent); + } + + @Override + public void handleOnBackPressed() { + behavior.handleBackInvoked(); + } + + @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE) + @Override + public void handleOnBackCancelled() { + behavior.cancelBackProgress(); + } + }; + } } diff --git a/lib/java/com/google/android/material/sidesheet/Sheet.java b/lib/java/com/google/android/material/sidesheet/Sheet.java index a30fff0b8ce..958811969a5 100644 --- a/lib/java/com/google/android/material/sidesheet/Sheet.java +++ b/lib/java/com/google/android/material/sidesheet/Sheet.java @@ -20,6 +20,7 @@ import androidx.annotation.IntDef; import androidx.annotation.RestrictTo; +import com.google.android.material.motion.MaterialBackHandler; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -27,7 +28,7 @@ * Interface for sheet constants and {@code IntDefs} to be shared between the different {@link * Sheet} implementations. */ -interface Sheet { +interface Sheet extends MaterialBackHandler { /** The sheet is dragging. */ int STATE_DRAGGING = 1; diff --git a/lib/java/com/google/android/material/sidesheet/SideSheetBehavior.java b/lib/java/com/google/android/material/sidesheet/SideSheetBehavior.java index 3e9643c231e..9a5c7174374 100644 --- a/lib/java/com/google/android/material/sidesheet/SideSheetBehavior.java +++ b/lib/java/com/google/android/material/sidesheet/SideSheetBehavior.java @@ -21,9 +21,12 @@ import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; import static java.lang.Math.min; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; +import android.os.Build.VERSION_CODES; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; @@ -37,13 +40,18 @@ import android.view.ViewGroup; import android.view.ViewGroup.MarginLayoutParams; import android.view.ViewParent; +import android.window.BackEvent; +import androidx.annotation.GravityInt; import androidx.annotation.IdRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.annotation.RestrictTo; +import androidx.annotation.VisibleForTesting; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.coordinatorlayout.widget.CoordinatorLayout.LayoutParams; import androidx.core.math.MathUtils; +import androidx.core.os.BuildCompat; import androidx.core.view.GravityCompat; import androidx.core.view.ViewCompat; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; @@ -51,6 +59,7 @@ import androidx.core.view.accessibility.AccessibilityViewCommand; import androidx.customview.view.AbsSavedState; import androidx.customview.widget.ViewDragHelper; +import com.google.android.material.motion.MaterialSideContainerBackHelper; import com.google.android.material.resources.MaterialResources; import com.google.android.material.shape.MaterialShapeDrawable; import com.google.android.material.shape.ShapeAppearanceModel; @@ -115,6 +124,7 @@ public class SideSheetBehavior extends CoordinatorLayout.Behavio @IdRes private int coplanarSiblingViewId = View.NO_ID; @Nullable private VelocityTracker velocityTracker; + @Nullable private MaterialSideContainerBackHelper sideContainerBackHelper; private int initialX; @@ -191,6 +201,14 @@ private void setSheetEdge(@SheetEdge int sheetEdge) { } } + @GravityInt + private int getGravityFromSheetEdge() { + if (sheetDelegate != null) { + return sheetDelegate.getSheetEdge() == Sheet.EDGE_RIGHT ? Gravity.RIGHT : Gravity.LEFT; + } + return Gravity.RIGHT; + } + private boolean hasRightMargin() { LayoutParams layoutParams = getViewLayoutParams(); return layoutParams != null && layoutParams.rightMargin > 0; @@ -259,6 +277,7 @@ public void onAttachedToLayoutParams(@NonNull LayoutParams layoutParams) { // first time we layout with this behavior by checking (viewRef == null). viewRef = null; viewDragHelper = null; + sideContainerBackHelper = null; } @Override @@ -267,6 +286,7 @@ public void onDetachedFromLayoutParams() { // Release references so we don't run unnecessary codepaths while not attached to a view. viewRef = null; viewDragHelper = null; + sideContainerBackHelper = null; } @Override @@ -333,6 +353,8 @@ public boolean onLayoutChild( // First layout with this behavior. viewRef = new WeakReference<>(child); + sideContainerBackHelper = new MaterialSideContainerBackHelper(child); + // Only set MaterialShapeDrawable as background if shapeTheming is enabled, otherwise will // default to android:background declared in styles or layout. if (materialShapeDrawable != null) { @@ -951,6 +973,66 @@ public int getLastStableState() { return lastStableState; } + @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE) + @Override + public void startBackProgress(@NonNull BackEvent backEvent) { + if (sideContainerBackHelper == null) { + return; + } + sideContainerBackHelper.startBackProgress(backEvent); + } + + @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE) + @Override + public void updateBackProgress(@NonNull BackEvent backEvent) { + if (sideContainerBackHelper == null) { + return; + } + sideContainerBackHelper.updateBackProgress( + backEvent, getGravityFromSheetEdge()); + } + + @Override + public void handleBackInvoked() { + if (sideContainerBackHelper == null) { + return; + } + BackEvent backEvent = sideContainerBackHelper.onHandleBackInvoked(); + if (backEvent == null || !BuildCompat.isAtLeastU()) { + setState(STATE_HIDDEN); + return; + } + + sideContainerBackHelper.finishBackProgress( + backEvent, + getGravityFromSheetEdge(), + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + setStateInternal(STATE_HIDDEN); + if (viewRef != null && viewRef.get() != null) { + viewRef.get().requestLayout(); + } + } + }, + /* finishAnimatorUpdateListener= */ null); + } + + @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE) + @Override + public void cancelBackProgress() { + if (sideContainerBackHelper == null) { + return; + } + sideContainerBackHelper.cancelBackProgress(); + } + + @VisibleForTesting + @Nullable + MaterialSideContainerBackHelper getBackHelper() { + return sideContainerBackHelper; + } + class StateSettlingTracker { @StableSheetState private int targetState; private boolean isContinueSettlingRunnablePosted;