Skip to content

Commit

Permalink
[Predictive Back][Bottom Sheet] Update Bottom Sheet to support predic…
Browse files Browse the repository at this point in the history
…tive back

- Enable predictive back by default for Bottom Sheet dialogs
- Update Catalog demo to enable predictive back for persistent/standard bottom sheet

PiperOrigin-RevId: 518896205
  • Loading branch information
dsn5ft authored and pekingme committed Mar 23, 2023
1 parent 0be665c commit d6fad95
Show file tree
Hide file tree
Showing 8 changed files with 400 additions and 13 deletions.
8 changes: 8 additions & 0 deletions catalog/build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
apply plugin: 'com.android.application'

dependencies {

constraints {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0") {
because("kotlin-stdlib-jdk8 is now a part of kotlin-stdlib")
}
}

api 'com.google.dagger:dagger:2.45'
annotationProcessor 'com.google.dagger:dagger-compiler:2.45'

Expand All @@ -12,6 +19,7 @@ dependencies {
api 'androidx.constraintlayout:constraintlayout:2.1.0'
api 'androidx.gridlayout:gridlayout:1.0.0'
api "androidx.multidex:multidex:2.0.1"
api "androidx.activity:activity:1.8.0-alpha02"
api "androidx.recyclerview:recyclerview:1.2.1"
api 'androidx.window:window:1.0.0-beta04'
api "androidx.window:window-java:1.0.0-beta04"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import io.material.catalog.R;

import android.app.Activity;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
Expand All @@ -28,9 +29,12 @@
import android.widget.FrameLayout;
import android.widget.TextView;
import android.widget.Toast;
import android.window.BackEvent;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
Expand All @@ -42,8 +46,37 @@

/** A fragment that displays the main BottomSheet demo for the Catalog app. */
public class BottomSheetMainDemoFragment extends DemoFragment {

private final OnBackPressedCallback persistentBottomSheetBackCallback =
new OnBackPressedCallback(/* enabled= */ false) {

@RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
@Override
public void handleOnBackStarted(@NonNull BackEvent backEvent) {
persistentBottomSheetBehavior.startBackProgress(backEvent);
}

@RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
@Override
public void handleOnBackProgressed(@NonNull BackEvent backEvent) {
persistentBottomSheetBehavior.updateBackProgress(backEvent);
}

@Override
public void handleOnBackPressed() {
persistentBottomSheetBehavior.handleBackInvoked();
}

@RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
@Override
public void handleOnBackCancelled() {
persistentBottomSheetBehavior.cancelBackProgress();
}
};

private WindowPreferencesManager windowPreferencesManager;
private BottomSheetDialog bottomSheetDialog;
private BottomSheetBehavior<View> persistentBottomSheetBehavior;
private WindowInsetsCompat windowInsets;
private int peekHeightPx;

Expand Down Expand Up @@ -119,8 +152,10 @@ public View onCreateDemoView(
.addBottomSheetCallback(createBottomSheetCallback(dialogText));
TextView bottomSheetText = view.findViewById(R.id.cat_persistent_bottomsheet_state);
View bottomSheetPersistent = view.findViewById(R.id.bottom_drawer);
BottomSheetBehavior.from(bottomSheetPersistent)
.addBottomSheetCallback(createBottomSheetCallback(bottomSheetText));
persistentBottomSheetBehavior = BottomSheetBehavior.from(bottomSheetPersistent);
persistentBottomSheetBehavior.addBottomSheetCallback(
createBottomSheetCallback(bottomSheetText));
setupBackHandling(persistentBottomSheetBehavior);

Button button1 = view.findViewById(R.id.cat_bottomsheet_button);
button1.setOnClickListener(
Expand Down Expand Up @@ -247,4 +282,34 @@ public void onSlide(@NonNull View bottomSheet, float slideOffset) {}
};
return bottomSheetCallback;
}

private void setupBackHandling(BottomSheetBehavior<View> behavior) {
requireActivity()
.getOnBackPressedDispatcher()
.addCallback(this, persistentBottomSheetBackCallback);
behavior.addBottomSheetCallback(
new BottomSheetCallback() {
@Override
public void onStateChanged(@NonNull View bottomSheet, int newState) {
switch (newState) {
case BottomSheetBehavior.STATE_EXPANDED:
case BottomSheetBehavior.STATE_HALF_EXPANDED:
persistentBottomSheetBackCallback.setEnabled(true);
break;
case BottomSheetBehavior.STATE_COLLAPSED:
case BottomSheetBehavior.STATE_HIDDEN:
persistentBottomSheetBackCallback.setEnabled(false);
break;
case BottomSheetBehavior.STATE_DRAGGING:
case BottomSheetBehavior.STATE_SETTLING:
default:
// Do nothing, only change callback enabled for "stable" states.
break;
}
}

@Override
public void onSlide(@NonNull View bottomSheet, float slideOffset) {}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import static java.lang.Math.max;
import static java.lang.Math.min;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.content.Context;
Expand All @@ -37,26 +39,31 @@
import android.util.SparseIntArray;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.RoundedCorner;
import android.view.VelocityTracker;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;
import android.view.ViewParent;
import android.view.WindowInsets;
import android.view.accessibility.AccessibilityEvent;
import android.window.BackEvent;
import androidx.annotation.FloatRange;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.StringRes;
import androidx.annotation.VisibleForTesting;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.coordinatorlayout.widget.CoordinatorLayout.LayoutParams;
import androidx.core.graphics.Insets;
import androidx.core.math.MathUtils;
import androidx.core.os.BuildCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
Expand All @@ -66,6 +73,8 @@
import androidx.customview.widget.ViewDragHelper;
import com.google.android.material.internal.ViewUtils;
import com.google.android.material.internal.ViewUtils.RelativePadding;
import com.google.android.material.motion.MaterialBackHandler;
import com.google.android.material.motion.MaterialBottomContainerBackHelper;
import com.google.android.material.resources.MaterialResources;
import com.google.android.material.shape.MaterialShapeDrawable;
import com.google.android.material.shape.ShapeAppearanceModel;
Expand All @@ -84,7 +93,8 @@
* window-like. For BottomSheetDialog use {@link BottomSheetDialog#setTitle(int)}, and for
* BottomSheetDialogFragment use {@link ViewCompat#setAccessibilityPaneTitle(View, CharSequence)}.
*/
public class BottomSheetBehavior<V extends View> extends CoordinatorLayout.Behavior<V> {
public class BottomSheetBehavior<V extends View> extends CoordinatorLayout.Behavior<V>
implements MaterialBackHandler {

/** Callback for monitoring events about bottom sheets. */
public abstract static class BottomSheetCallback {
Expand Down Expand Up @@ -318,6 +328,7 @@ void onLayout(@NonNull View bottomSheet) {}
@NonNull private final ArrayList<BottomSheetCallback> callbacks = new ArrayList<>();

@Nullable private VelocityTracker velocityTracker;
@Nullable MaterialBottomContainerBackHelper bottomContainerBackHelper;

int activePointerId;

Expand Down Expand Up @@ -456,6 +467,7 @@ public void onAttachedToLayoutParams(@NonNull LayoutParams layoutParams) {
// first time we layout with this behavior by checking (viewRef == null).
viewRef = null;
viewDragHelper = null;
bottomContainerBackHelper = null;
}

@Override
Expand All @@ -464,6 +476,7 @@ public void onDetachedFromLayoutParams() {
// Release references so we don't run unnecessary codepaths while not attached to a view.
viewRef = null;
viewDragHelper = null;
bottomContainerBackHelper = null;
}

@Override
Expand Down Expand Up @@ -533,6 +546,7 @@ public boolean onLayoutChild(
setWindowInsetsListener(child);
ViewCompat.setWindowInsetsAnimationCallback(child, new InsetsAnimationCallback(child));
viewRef = new WeakReference<>(child);
bottomContainerBackHelper = new MaterialBottomContainerBackHelper(child);
// Only set MaterialShapeDrawable as background if shapeTheming is enabled, otherwise will
// default to android:background declared in styles or layout.
if (materialShapeDrawable != null) {
Expand Down Expand Up @@ -1409,7 +1423,7 @@ private void updateDrawableForTargetState(@State int state, boolean animate) {
if (interpolatorAnimator.isRunning()) {
interpolatorAnimator.reverse();
} else {
float to = removeCorners ? 0f : 1f;
float to = removeCorners ? calculateInterpolationWithCornersRemoved() : 1f;
float from = 1f - to;
interpolatorAnimator.setFloatValues(from, to);
interpolatorAnimator.start();
Expand All @@ -1418,8 +1432,48 @@ private void updateDrawableForTargetState(@State int state, boolean animate) {
if (interpolatorAnimator != null && interpolatorAnimator.isRunning()) {
interpolatorAnimator.cancel();
}
materialShapeDrawable.setInterpolation(expandedCornersRemoved ? 0f : 1f);
materialShapeDrawable.setInterpolation(
expandedCornersRemoved ? calculateInterpolationWithCornersRemoved() : 1f);
}
}

private float calculateInterpolationWithCornersRemoved() {
if (materialShapeDrawable != null
&& viewRef != null
&& viewRef.get() != null
&& VERSION.SDK_INT >= VERSION_CODES.S) {
V view = viewRef.get();
int[] location = new int[2];
view.getLocationOnScreen(location);
// Only use device corner radius if sheet is touching top of screen.
if (location[1] == 0) {
final WindowInsets insets = view.getRootWindowInsets();
if (insets != null) {
float topLeftInterpolation =
calculateCornerInterpolation(
materialShapeDrawable.getTopLeftCornerResolvedSize(),
insets.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT));
float topRightInterpolation =
calculateCornerInterpolation(
materialShapeDrawable.getTopRightCornerResolvedSize(),
insets.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT));
return Math.max(topLeftInterpolation, topRightInterpolation);
}
}
}
return 0;
}

@RequiresApi(VERSION_CODES.S)
private float calculateCornerInterpolation(
float materialShapeDrawableCornerSize, @Nullable RoundedCorner deviceRoundedCorner) {
if (deviceRoundedCorner != null) {
float deviceCornerRadius = deviceRoundedCorner.getRadius();
if (deviceCornerRadius > 0 && materialShapeDrawableCornerSize > 0) {
return deviceCornerRadius / materialShapeDrawableCornerSize;
}
}
return 0;
}

private boolean isExpandedAndShouldRemoveCorners() {
Expand Down Expand Up @@ -1506,6 +1560,67 @@ boolean shouldHide(@NonNull View child, float yvel) {
return Math.abs(newTop - collapsedOffset) / (float) peek > HIDE_THRESHOLD;
}

@RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
@Override
public void startBackProgress(@NonNull BackEvent backEvent) {
if (bottomContainerBackHelper == null) {
return;
}
bottomContainerBackHelper.startBackProgress(backEvent);
}

@RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
@Override
public void updateBackProgress(@NonNull BackEvent backEvent) {
if (bottomContainerBackHelper == null) {
return;
}
bottomContainerBackHelper.updateBackProgress(backEvent);
}

@Override
public void handleBackInvoked() {
if (bottomContainerBackHelper == null) {
return;
}
BackEvent backEvent = bottomContainerBackHelper.onHandleBackInvoked();
if (backEvent == null || !BuildCompat.isAtLeastU()) {
// If using traditional button system nav or if pre-U, just hide or collapse the bottom sheet.
setState(hideable ? STATE_HIDDEN : STATE_COLLAPSED);
return;
}
if (hideable) {
bottomContainerBackHelper.finishBackProgressNotPersistent(
backEvent,
new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
// Hide immediately following the built-in predictive back slide down animation.
setStateInternal(STATE_HIDDEN);
}
});
} else {
bottomContainerBackHelper.finishBackProgressPersistent(
backEvent, /* animatorListener= */ null);
setState(STATE_COLLAPSED);
}
}

@RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
@Override
public void cancelBackProgress() {
if (bottomContainerBackHelper == null) {
return;
}
bottomContainerBackHelper.cancelBackProgress();
}

@VisibleForTesting
@Nullable
MaterialBottomContainerBackHelper getBackHelper() {
return bottomContainerBackHelper;
}

@Nullable
@VisibleForTesting
View findScrollingChild(View view) {
Expand Down Expand Up @@ -1556,7 +1671,7 @@ MaterialShapeDrawable getMaterialShapeDrawable() {
}

private void createShapeValueAnimator() {
interpolatorAnimator = ValueAnimator.ofFloat(0f, 1f);
interpolatorAnimator = ValueAnimator.ofFloat(calculateInterpolationWithCornersRemoved(), 1f);
interpolatorAnimator.setDuration(CORNER_ANIMATION_DURATION);
interpolatorAnimator.addUpdateListener(
new AnimatorUpdateListener() {
Expand Down
Loading

0 comments on commit d6fad95

Please sign in to comment.