From 29162820ae422c99354aabef1f8c276169070304 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 24 Jun 2023 17:30:06 -0600 Subject: [PATCH] ui: update bottomsheetbehavior Update BottomSheetBehavior to the latest commit in 1.10.0-alpha04. This adds a few new APIs that I still can't really use. I could in the future. --- .../BackportBottomSheetBehavior.java | 446 +++++++++++++----- 1 file changed, 334 insertions(+), 112 deletions(-) diff --git a/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetBehavior.java b/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetBehavior.java index c6560c151..ab55a48dc 100644 --- a/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetBehavior.java +++ b/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetBehavior.java @@ -16,10 +16,14 @@ package com.google.android.material.bottomsheet; +import com.google.android.material.R; + import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; 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; @@ -32,8 +36,10 @@ import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.util.Log; +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; @@ -41,13 +47,15 @@ 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 androidx.activity.BackEventCompat; 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; @@ -62,14 +70,13 @@ import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.Accessibilit import androidx.core.view.accessibility.AccessibilityViewCommand; import androidx.customview.view.AbsSavedState; import androidx.customview.widget.ViewDragHelper; - -import com.google.android.material.R; 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; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; @@ -84,13 +91,11 @@ import java.util.Map; *

To send useful accessibility events, set a title on bottom sheets that are windows or are * window-like. For BottomSheetDialog use {@link BottomSheetDialog#setTitle(int)}, and for * BottomSheetDialogFragment use {@link ViewCompat#setAccessibilityPaneTitle(View, CharSequence)}. - * - * Modified at several points by Alexander Capehart to backport miscellaneous fixes not currently - * obtainable in the currently used MDC library. */ -public class BackportBottomSheetBehavior extends CoordinatorLayout.Behavior { +public class BackportBottomSheetBehavior extends CoordinatorLayout.Behavior + implements MaterialBackHandler { - /** Listener for monitoring events about bottom sheets. */ + /** Callback for monitoring events about bottom sheets. */ public abstract static class BottomSheetCallback { /** @@ -203,11 +208,11 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo @Retention(RetentionPolicy.SOURCE) public @interface SaveFlags {} - private static final String TAG = "BottomSheetBehavior"; + private static final String TAG = "BackportBottomSheetBehavior"; @SaveFlags private int saveFlags = SAVE_NONE; - private static final int SIGNIFICANT_VEL_THRESHOLD = 500; + @VisibleForTesting static final int DEFAULT_SIGNIFICANT_VEL_THRESHOLD = 500; private static final float HIDE_THRESHOLD = 0.5f; @@ -217,12 +222,21 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo private static final int NO_MAX_SIZE = -1; + private static final int VIEW_INDEX_BOTTOM_SHEET = 0; + + private static final int INVALID_POSITION = -1; + + @VisibleForTesting + static final int VIEW_INDEX_ACCESSIBILITY_DELEGATE_VIEW = 1; + private boolean fitToContents = true; private boolean updateImportantForAccessibilityOnSiblings = false; private float maximumVelocity; + private int significantVelocityThreshold; + /** Peek height set by the user. */ private int peekHeight; @@ -256,10 +270,12 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo private int insetBottom; private int insetTop; + private boolean shouldRemoveExpandedCorners; + /** Default Shape Appearance to be used in bottomsheet */ private ShapeAppearanceModel shapeAppearanceModelDefault; - private boolean isShapeExpanded; + private boolean expandedCornersRemoved; private final StateSettlingTracker stateSettlingTracker = new StateSettlingTracker(); @@ -304,22 +320,25 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo int parentHeight; @Nullable WeakReference viewRef; + @Nullable WeakReference accessibilityDelegateViewRef; @Nullable WeakReference nestedScrollingChildRef; @NonNull private final ArrayList callbacks = new ArrayList<>(); @Nullable private VelocityTracker velocityTracker; + @Nullable MaterialBottomContainerBackHelper bottomContainerBackHelper; int activePointerId; - private int initialY; + private int initialY = INVALID_POSITION; boolean touchingScrollingChild; @Nullable private Map importantForAccessibilityMap; - private int expandHalfwayActionId = View.NO_ID; + @VisibleForTesting + final SparseIntArray expandHalfwayActionIds = new SparseIntArray(); public BackportBottomSheetBehavior() {} @@ -387,6 +406,11 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo R.styleable.BottomSheetBehavior_Layout_behavior_expandedOffset, 0)); } + setSignificantVelocityThreshold( + a.getInt( + R.styleable.BottomSheetBehavior_Layout_behavior_significantVelocityThreshold, + DEFAULT_SIGNIFICANT_VEL_THRESHOLD)); + // Reading out if we are handling padding, so we can apply it to the content. paddingBottomSystemWindowInsets = a.getBoolean(R.styleable.BottomSheetBehavior_Layout_paddingBottomSystemWindowInsets, false); @@ -404,6 +428,8 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo a.getBoolean(R.styleable.BottomSheetBehavior_Layout_marginRightSystemWindowInsets, false); marginTopSystemWindowInsets = a.getBoolean(R.styleable.BottomSheetBehavior_Layout_marginTopSystemWindowInsets, false); + shouldRemoveExpandedCorners = + a.getBoolean(R.styleable.BottomSheetBehavior_Layout_shouldRemoveExpandedCorners, true); a.recycle(); ViewConfiguration configuration = ViewConfiguration.get(context); @@ -440,6 +466,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo // first time we layout with this behavior by checking (viewRef == null). viewRef = null; viewDragHelper = null; + bottomContainerBackHelper = null; } @Override @@ -448,6 +475,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo // Release references so we don't run unnecessary codepaths while not attached to a view. viewRef = null; viewDragHelper = null; + bottomContainerBackHelper = null; } @Override @@ -515,7 +543,9 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo peekHeightMin = parent.getResources().getDimensionPixelSize(R.dimen.design_bottom_sheet_peek_height_min); 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) { @@ -523,9 +553,6 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo // Use elevation attr if set on bottomsheet; otherwise, use elevation of child view. materialShapeDrawable.setElevation( elevation == -1 ? ViewCompat.getElevation(child) : elevation); - // Update the material shape based on initial state. - isShapeExpanded = state == STATE_EXPANDED; - materialShapeDrawable.setInterpolation(isShapeExpanded ? 0f : 1f); } else if (backgroundTint != null) { ViewCompat.setBackgroundTintList(child, backgroundTint); } @@ -549,11 +576,12 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo if (parentHeight - childHeight < insetTop) { if (paddingTopSystemWindowInsets) { // If the bottomsheet would land in the middle of the status bar when fully expanded add - // extra space to make sure it goes all the way. - childHeight = parentHeight; + // extra space to make sure it goes all the way up or up to max height if it is specified. + childHeight = (maxHeight == NO_MAX_SIZE) ? parentHeight : min(parentHeight, maxHeight); } else { // If we don't want the bottomsheet to go under the status bar we cap its height - childHeight = parentHeight - insetTop; + int insetHeight = parentHeight - insetTop; + childHeight = (maxHeight == NO_MAX_SIZE) ? insetHeight : min(insetHeight, maxHeight); } } fitToContentsOffset = max(0, parentHeight - childHeight); @@ -571,6 +599,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo } else if (state == STATE_DRAGGING || state == STATE_SETTLING) { ViewCompat.offsetTopAndBottom(child, savedTop - child.getTop()); } + updateDrawableForTargetState(state, /* animate= */ false); nestedScrollingChildRef = new WeakReference<>(findScrollingChild(child)); @@ -640,6 +669,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo && state != STATE_DRAGGING && !parent.isPointInChildBounds(scroll, (int) event.getX(), (int) event.getY()) && viewDragHelper != null + && initialY != INVALID_POSITION && Math.abs(initialY - event.getY()) > viewDragHelper.getTouchSlop(); } @@ -723,8 +753,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo } } else if (dy < 0) { // Downward if (!target.canScrollVertically(-1)) { - // MODIFICATION: Add isHideableWhenDragging method - if (newTop <= collapsedOffset || (hideable && isHideableWhenDragging())) { + if (newTop <= collapsedOffset || canBeHiddenByDragging()) { if (!draggable) { // Prevent dragging return; @@ -778,7 +807,6 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo } } } - // MODIFICATION: Add isHideableWhenDragging method } else if (hideable && shouldHide(child, getYVelocity()) && isHideableWhenDragging()) { targetState = STATE_HIDDEN; } else if (lastNestedScrollDy == 0) { @@ -888,6 +916,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo // Fix incorrect expanded settings depending on whether or not we are fitting sheet to contents. setStateInternal((this.fitToContents && state == STATE_HALF_EXPANDED) ? STATE_EXPANDED : state); + updateDrawableForTargetState(state, /* animate= */ true); updateAccessibilityActions(); } @@ -897,7 +926,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo * be adjusted as expected. * * @param maxWidth The maximum width in pixels to be set - * @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_android_maxWidth + * @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_android_maxWidth * @see #getMaxWidth() */ public void setMaxWidth(@Px int maxWidth) { @@ -907,7 +936,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo /** * Returns the bottom sheet's maximum width, or -1 if no maximum width is set. * - * @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_android_maxWidth + * @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_android_maxWidth * @see #setMaxWidth(int) */ @Px @@ -920,7 +949,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo * BottomSheetDialog#show()} in order for the height to be adjusted as expected. * * @param maxHeight The maximum height in pixels to be set - * @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_android_maxHeight + * @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_android_maxHeight * @see #getMaxHeight() */ public void setMaxHeight(@Px int maxHeight) { @@ -930,7 +959,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo /** * Returns the bottom sheet's maximum height, or -1 if no maximum height is set. * - * @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_android_maxHeight + * @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_android_maxHeight * @see #setMaxHeight(int) */ @Px @@ -944,7 +973,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo * @param peekHeight The height of the collapsed bottom sheet in pixels, or {@link * #PEEK_HEIGHT_AUTO} to configure the sheet to peek automatically at 16:9 ratio keyline. * @attr ref - * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_peekHeight + * com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_peekHeight */ public void setPeekHeight(int peekHeight) { setPeekHeight(peekHeight, false); @@ -958,7 +987,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo * #PEEK_HEIGHT_AUTO} to configure the sheet to peek automatically at 16:9 ratio keyline. * @param animate Whether to animate between the old height and the new height. * @attr ref - * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_peekHeight + * com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_peekHeight */ public final void setPeekHeight(int peekHeight, boolean animate) { boolean layout = false; @@ -1001,7 +1030,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo * @return The height of the collapsed bottom sheet in pixels, or {@link #PEEK_HEIGHT_AUTO} if the * sheet is configured to peek automatically at 16:9 ratio keyline * @attr ref - * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_peekHeight + * com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_peekHeight */ public int getPeekHeight() { return peekHeightAuto ? PEEK_HEIGHT_AUTO : peekHeight; @@ -1015,7 +1044,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo * * @param ratio a float between 0 and 1, representing the {@link #STATE_HALF_EXPANDED} ratio. * @attr ref - * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_halfExpandedRatio + * com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_halfExpandedRatio */ public void setHalfExpandedRatio( @FloatRange(from = 0.0f, to = 1.0f, fromInclusive = false, toInclusive = false) float ratio) { @@ -1035,7 +1064,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo * Gets the ratio for the height of the BottomSheet in the {@link #STATE_HALF_EXPANDED} state. * * @attr ref - * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_halfExpandedRatio + * com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_halfExpandedRatio */ @FloatRange(from = 0.0f, to = 1.0f) public float getHalfExpandedRatio() { @@ -1050,13 +1079,14 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo * @param offset an integer value greater than equal to 0, representing the {@link * #STATE_EXPANDED} offset. Value must not exceed the offset in the half expanded state. * @attr ref - * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_expandedOffset + * com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_expandedOffset */ public void setExpandedOffset(int offset) { if (offset < 0) { throw new IllegalArgumentException("offset must be greater than or equal to 0"); } this.expandedOffset = offset; + updateDrawableForTargetState(state, /* animate= */ true); } /** @@ -1064,7 +1094,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo * pick the offset depending on the height of the content. * * @attr ref - * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_expandedOffset + * com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_expandedOffset */ public int getExpandedOffset() { return fitToContents @@ -1072,8 +1102,6 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo : Math.max(expandedOffset, paddingTopSystemWindowInsets ? 0 : insetTop); } - // MODIFICATION: Add calculateSlideOffset method - /** * Calculates the current offset of the bottom sheet. * @@ -1082,26 +1110,21 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo * @return The offset of this bottom sheet within [-1,1] range. Offset increases * as this bottom sheet is moving upward. From 0 to 1 the sheet is between collapsed and * expanded states and from -1 to 0 it is between hidden and collapsed states. Returns - * {@code Float.MIN_VALUE} if the bottom sheet is not laid out. + * -1 if the bottom sheet is not laid out (therefore it's hidden). */ public float calculateSlideOffset() { - if (viewRef == null) { - return Float.MIN_VALUE; + if (viewRef == null || viewRef.get() == null) { + return -1; } - View bottomSheet = viewRef.get(); - if (bottomSheet != null) { - return calculateSlideOffset(bottomSheet.getTop()); - } - - return Float.MIN_VALUE; + return calculateSlideOffsetWithTop(viewRef.get().getTop()); } /** - * Sets whether this bottom sheet can hide when it is swiped down. + * Sets whether this bottom sheet can hide. * * @param hideable {@code true} to make this bottom sheet hideable. - * @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_hideable + * @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_hideable */ public void setHideable(boolean hideable) { if (this.hideable != hideable) { @@ -1118,7 +1141,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo * Gets whether this bottom sheet can hide when it is swiped down. * * @return {@code true} if this bottom sheet can hide. - * @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_hideable + * @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_hideable */ public boolean isHideable() { return hideable; @@ -1130,7 +1153,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo * * @param skipCollapsed True if the bottom sheet should skip the collapsed state. * @attr ref - * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_skipCollapsed + * com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_skipCollapsed */ public void setSkipCollapsed(boolean skipCollapsed) { this.skipCollapsed = skipCollapsed; @@ -1142,7 +1165,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo * * @return Whether the bottom sheet should skip the collapsed state. * @attr ref - * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_skipCollapsed + * com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_skipCollapsed */ public boolean getSkipCollapsed() { return skipCollapsed; @@ -1153,7 +1176,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo * dragging, an app will require to implement a custom way to expand/collapse the bottom sheet * * @param draggable {@code false} to prevent dragging the sheet to collapse and expand - * @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_draggable + * @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_draggable */ public void setDraggable(boolean draggable) { this.draggable = draggable; @@ -1163,13 +1186,35 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo return draggable; } + /* + * Sets the velocity threshold considered significant enough to trigger a slide + * to the next stable state. + * + * @param significantVelocityThreshold The velocity threshold that warrants a vertical swipe. + * @see #getSignificantVelocityThreshold() + * @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_significantVelocityThreshold + */ + public void setSignificantVelocityThreshold(int significantVelocityThreshold) { + this.significantVelocityThreshold = significantVelocityThreshold; + } + + /* + * Returns the significant velocity threshold. + * + * @see #setSignificantVelocityThreshold(int) + * @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_significantVelocityThreshold + */ + public int getSignificantVelocityThreshold() { + return this.significantVelocityThreshold; + } + /** * Sets save flags to be preserved in bottomsheet on configuration change. * * @param flags bitwise int of {@link #SAVE_PEEK_HEIGHT}, {@link #SAVE_FIT_TO_CONTENTS}, {@link * #SAVE_HIDEABLE}, {@link #SAVE_SKIP_COLLAPSED}, {@link #SAVE_ALL} and {@link #SAVE_NONE}. * @see #getSaveFlags() - * @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_saveFlags + * @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_saveFlags */ public void setSaveFlags(@SaveFlags int flags) { this.saveFlags = flags; @@ -1178,7 +1223,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo * Returns the save flags. * * @see #setSaveFlags(int) - * @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_saveFlags + * @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_saveFlags */ @SaveFlags public int getSaveFlags() { @@ -1208,9 +1253,9 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo } /** - * Sets a listener to be notified of bottom sheet events. + * Sets a callback to be notified of bottom sheet events. * - * @param callback The listener to notify when bottom sheet events occur. + * @param callback The callback to notify when bottom sheet events occur. * @deprecated use {@link #addBottomSheetCallback(BottomSheetCallback)} and {@link * #removeBottomSheetCallback(BottomSheetCallback)} instead */ @@ -1218,7 +1263,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo public void setBottomSheetCallback(BottomSheetCallback callback) { Log.w( TAG, - "BottomSheetBehavior now supports multiple callbacks. `setBottomSheetCallback()` removes" + "BackportBottomSheetBehavior now supports multiple callbacks. `setBottomSheetCallback()` removes" + " all existing callbacks, including ones set internally by library authors, which" + " may result in unintended behavior. This may change in the future. Please use" + " `addBottomSheetCallback()` and `removeBottomSheetCallback()` instead to set your" @@ -1230,9 +1275,9 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo } /** - * Adds a listener to be notified of bottom sheet events. + * Adds a callback to be notified of bottom sheet events. * - * @param callback The listener to notify when bottom sheet events occur. + * @param callback The callback to notify when bottom sheet events occur. */ public void addBottomSheetCallback(@NonNull BottomSheetCallback callback) { if (!callbacks.contains(callback)) { @@ -1241,9 +1286,9 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo } /** - * Removes a previously added listener. + * Removes a previously added callback. * - * @param callback The listener to remove. + * @param callback The callback to remove. */ public void removeBottomSheetCallback(@NonNull BottomSheetCallback callback) { callbacks.remove(callback); @@ -1325,6 +1370,26 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo return gestureInsetBottomIgnored; } + /** + * Sets whether the bottom sheet should remove its corners when it reaches the expanded state. + * + *

If false, the bottom sheet will only remove its corners if it is expanded and reaches the + * top of the screen. + */ + public void setShouldRemoveExpandedCorners(boolean shouldRemoveExpandedCorners) { + if (this.shouldRemoveExpandedCorners != shouldRemoveExpandedCorners) { + this.shouldRemoveExpandedCorners = shouldRemoveExpandedCorners; + updateDrawableForTargetState(getState(), /* animate= */ true); + } + } + + /** + * Returns whether the bottom sheet will remove its corners when it reaches the expanded state. + */ + public boolean isShouldRemoveExpandedCorners() { + return shouldRemoveExpandedCorners; + } + /** * Gets the current state of the bottom sheet. * @@ -1376,33 +1441,91 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo updateImportantForAccessibility(false); } - updateDrawableForTargetState(state); + updateDrawableForTargetState(state, /* animate= */ true); for (int i = 0; i < callbacks.size(); i++) { callbacks.get(i).onStateChanged(bottomSheet, state); } updateAccessibilityActions(); } - private void updateDrawableForTargetState(@State int state) { + private void updateDrawableForTargetState(@State int state, boolean animate) { if (state == STATE_SETTLING) { // Special case: we want to know which state we're settling to, so wait for another call. return; } - boolean expand = state == STATE_EXPANDED; - if (isShapeExpanded != expand) { - isShapeExpanded = expand; - if (materialShapeDrawable != null && interpolatorAnimator != null) { - if (interpolatorAnimator.isRunning()) { - interpolatorAnimator.reverse(); - } else { - float to = expand ? 0f : 1f; - float from = 1f - to; - interpolatorAnimator.setFloatValues(from, to); - interpolatorAnimator.start(); + boolean removeCorners = isExpandedAndShouldRemoveCorners(); + if (expandedCornersRemoved == removeCorners || materialShapeDrawable == null) { + return; + } + expandedCornersRemoved = removeCorners; + if (animate && interpolatorAnimator != null) { + if (interpolatorAnimator.isRunning()) { + interpolatorAnimator.reverse(); + } else { + float to = removeCorners ? calculateInterpolationWithCornersRemoved() : 1f; + float from = 1f - to; + interpolatorAnimator.setFloatValues(from, to); + interpolatorAnimator.start(); + } + } else { + if (interpolatorAnimator != null && interpolatorAnimator.isRunning()) { + interpolatorAnimator.cancel(); + } + 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(); + // Only use device corner radius if sheet is touching top of screen. + if (isAtTopOfScreen()) { + 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 isAtTopOfScreen() { + if (viewRef == null || viewRef.get() == null) { + return false; + } + int[] location = new int[2]; + viewRef.get().getLocationOnScreen(location); + return location[1] == 0; + } + + private boolean isExpandedAndShouldRemoveCorners() { + // Only remove corners when it's full screen. + return state == STATE_EXPANDED && (shouldRemoveExpandedCorners || isAtTopOfScreen()); } private int calculatePeekHeight() { @@ -1432,9 +1555,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo this.halfExpandedOffset = (int) (parentHeight * (1 - halfExpandedRatio)); } - // MODIFICATION: Add calculateSlideOffset method - - private float calculateSlideOffset(int top) { + private float calculateSlideOffsetWithTop(int top) { return (top > collapsedOffset || collapsedOffset == getExpandedOffset()) ? (float) (collapsedOffset - top) / (parentHeight - collapsedOffset) @@ -1443,6 +1564,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo private void reset() { activePointerId = ViewDragHelper.INVALID_POINTER; + initialY = INVALID_POSITION; if (velocityTracker != null) { velocityTracker.recycle(); velocityTracker = null; @@ -1473,6 +1595,9 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo if (skipCollapsed) { return true; } + if (!isHideableWhenDragging()) { + return false; + } if (child.getTop() < collapsedOffset) { // It should not hide, but collapse. return false; @@ -1482,9 +1607,73 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo return Math.abs(newTop - collapsedOffset) / (float) peek > HIDE_THRESHOLD; } + @Override + public void startBackProgress(@NonNull BackEventCompat backEvent) { + if (bottomContainerBackHelper == null) { + return; + } + bottomContainerBackHelper.startBackProgress(backEvent); + } + + @Override + public void updateBackProgress(@NonNull BackEventCompat backEvent) { + if (bottomContainerBackHelper == null) { + return; + } + bottomContainerBackHelper.updateBackProgress(backEvent); + } + + @Override + public void handleBackInvoked() { + if (bottomContainerBackHelper == null) { + return; + } + BackEventCompat backEvent = bottomContainerBackHelper.onHandleBackInvoked(); + if (backEvent == null || VERSION.SDK_INT < VERSION_CODES.UPSIDE_DOWN_CAKE) { + // 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); + if (viewRef != null && viewRef.get() != null) { + viewRef.get().requestLayout(); + } + } + }); + } else { + bottomContainerBackHelper.finishBackProgressPersistent( + backEvent, /* animatorListener= */ null); + setState(STATE_COLLAPSED); + } + } + + @Override + public void cancelBackProgress() { + if (bottomContainerBackHelper == null) { + return; + } + bottomContainerBackHelper.cancelBackProgress(); + } + + @VisibleForTesting + @Nullable + MaterialBottomContainerBackHelper getBackHelper() { + return bottomContainerBackHelper; + } + @Nullable @VisibleForTesting View findScrollingChild(View view) { + if (view.getVisibility() != View.VISIBLE) { + return null; + } if (ViewCompat.isNestedScrollingEnabled(view)) { return view; } @@ -1524,12 +1713,12 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo } } - MaterialShapeDrawable getMaterialShapeDrawable() { + protected MaterialShapeDrawable getMaterialShapeDrawable() { return materialShapeDrawable; } private void createShapeValueAnimator() { - interpolatorAnimator = ValueAnimator.ofFloat(0f, 1f); + interpolatorAnimator = ValueAnimator.ofFloat(calculateInterpolationWithCornersRemoved(), 1f); interpolatorAnimator.setDuration(CORNER_ANIMATION_DURATION); interpolatorAnimator.addUpdateListener( new AnimatorUpdateListener() { @@ -1650,7 +1839,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo if (settling) { setStateInternal(STATE_SETTLING); // STATE_SETTLING won't animate the material shape, so do that here with the target state. - updateDrawableForTargetState(state); + updateDrawableForTargetState(state, /* animate= */ true); stateSettlingTracker.continueSettlingToState(state); } else { setStateInternal(state); @@ -1741,11 +1930,10 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo } } } - // MODIFICATION: Add isHideableWhenDragging method - } else if (hideable && shouldHide(releasedChild, yvel) && isHideableWhenDragging()) { + } else if (hideable && shouldHide(releasedChild, yvel)) { // Hide if the view was either released low or it was a significant vertical swipe // otherwise settle to closest expanded state. - if ((Math.abs(xvel) < Math.abs(yvel) && yvel > SIGNIFICANT_VEL_THRESHOLD) + if ((Math.abs(xvel) < Math.abs(yvel) && yvel > significantVelocityThreshold) || releasedLow(releasedChild)) { targetState = STATE_HIDDEN; } else if (fitToContents) { @@ -1814,9 +2002,10 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo @Override public int clampViewPositionVertical(@NonNull View child, int top, int dy) { - // MODIFICATION: Add isHideableWhenDragging method return MathUtils.clamp( - top, getExpandedOffset(), (hideable && isHideableWhenDragging()) ? parentHeight : collapsedOffset); + top, + getExpandedOffset(), + getViewVerticalDragRange(child)); } @Override @@ -1826,8 +2015,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo @Override public int getViewVerticalDragRange(@NonNull View child) { - // MODIFICATION: Add isHideableWhenDragging method - if (hideable && isHideableWhenDragging()) { + if (canBeHiddenByDragging()) { return parentHeight; } else { return collapsedOffset; @@ -1838,8 +2026,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo void dispatchOnSlide(int top) { View bottomSheet = viewRef.get(); if (bottomSheet != null && !callbacks.isEmpty()) { - // MODIFICATION: Add calculateSlideOffset method - float slideOffset = calculateSlideOffset(top); + float slideOffset = calculateSlideOffsetWithTop(top); for (int i = 0; i < callbacks.size(); i++) { callbacks.get(i).onSlide(bottomSheet, slideOffset); } @@ -1898,7 +2085,8 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo } /** - * Checks whether hiding gestures should be enabled if {@code isHideable} is true. + * Checks whether hiding gestures should be enabled while {@code isHideable} is set to true. + * * @hide */ @RestrictTo(LIBRARY_GROUP) @@ -1906,6 +2094,10 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo return true; } + private boolean canBeHiddenByDragging() { + return isHideable() && isHideableWhenDragging(); + } + /** * Checks whether the bottom sheet should be expanded after it has been released after dragging. * @@ -2067,7 +2259,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) params).getBehavior(); if (!(behavior instanceof BackportBottomSheetBehavior)) { - throw new IllegalArgumentException("The view is not associated with BottomSheetBehavior"); + throw new IllegalArgumentException("The view is not associated with BackportBottomSheetBehavior"); } return (BackportBottomSheetBehavior) behavior; } @@ -2139,30 +2331,43 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo } } + void setAccessibilityDelegateView(@Nullable View accessibilityDelegateView) { + if (accessibilityDelegateView == null && accessibilityDelegateViewRef != null) { + clearAccessibilityAction( + accessibilityDelegateViewRef.get(), VIEW_INDEX_ACCESSIBILITY_DELEGATE_VIEW); + accessibilityDelegateViewRef = null; + return; + } + accessibilityDelegateViewRef = new WeakReference<>(accessibilityDelegateView); + updateAccessibilityActions(accessibilityDelegateView, VIEW_INDEX_ACCESSIBILITY_DELEGATE_VIEW); + } + private void updateAccessibilityActions() { - if (viewRef == null) { - return; + if (viewRef != null) { + updateAccessibilityActions(viewRef.get(), VIEW_INDEX_BOTTOM_SHEET); } - V child = viewRef.get(); - if (child == null) { - return; + if (accessibilityDelegateViewRef != null) { + updateAccessibilityActions( + accessibilityDelegateViewRef.get(), VIEW_INDEX_ACCESSIBILITY_DELEGATE_VIEW); } - ViewCompat.removeAccessibilityAction(child, AccessibilityNodeInfoCompat.ACTION_COLLAPSE); - ViewCompat.removeAccessibilityAction(child, AccessibilityNodeInfoCompat.ACTION_EXPAND); - ViewCompat.removeAccessibilityAction(child, AccessibilityNodeInfoCompat.ACTION_DISMISS); + } - if (expandHalfwayActionId != View.NO_ID) { - ViewCompat.removeAccessibilityAction(child, expandHalfwayActionId); + private void updateAccessibilityActions(View view, int viewIndex) { + if (view == null) { + return; } + clearAccessibilityAction(view, viewIndex); + if (!fitToContents && state != STATE_HALF_EXPANDED) { - expandHalfwayActionId = + expandHalfwayActionIds.put( + viewIndex, addAccessibilityActionForState( - child, R.string.bottomsheet_action_expand_halfway, STATE_HALF_EXPANDED); + view, R.string.bottomsheet_action_expand_halfway, STATE_HALF_EXPANDED)); } - if (hideable && state != STATE_HIDDEN) { + if ((hideable && isHideableWhenDragging()) && state != STATE_HIDDEN) { replaceAccessibilityActionForState( - child, AccessibilityActionCompat.ACTION_DISMISS, STATE_HIDDEN); + view, AccessibilityActionCompat.ACTION_DISMISS, STATE_HIDDEN); } switch (state) { @@ -2170,36 +2375,54 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo { int nextState = fitToContents ? STATE_COLLAPSED : STATE_HALF_EXPANDED; replaceAccessibilityActionForState( - child, AccessibilityActionCompat.ACTION_COLLAPSE, nextState); + view, AccessibilityActionCompat.ACTION_COLLAPSE, nextState); break; } case STATE_HALF_EXPANDED: { replaceAccessibilityActionForState( - child, AccessibilityActionCompat.ACTION_COLLAPSE, STATE_COLLAPSED); + view, AccessibilityActionCompat.ACTION_COLLAPSE, STATE_COLLAPSED); replaceAccessibilityActionForState( - child, AccessibilityActionCompat.ACTION_EXPAND, STATE_EXPANDED); + view, AccessibilityActionCompat.ACTION_EXPAND, STATE_EXPANDED); break; } case STATE_COLLAPSED: { int nextState = fitToContents ? STATE_EXPANDED : STATE_HALF_EXPANDED; replaceAccessibilityActionForState( - child, AccessibilityActionCompat.ACTION_EXPAND, nextState); + view, AccessibilityActionCompat.ACTION_EXPAND, nextState); break; } - default: // fall out + case STATE_HIDDEN: + case STATE_DRAGGING: + case STATE_SETTLING: + // Accessibility actions are not applicable, do nothing + } + } + + private void clearAccessibilityAction(View view, int viewIndex) { + if (view == null) { + return; + } + ViewCompat.removeAccessibilityAction(view, AccessibilityNodeInfoCompat.ACTION_COLLAPSE); + ViewCompat.removeAccessibilityAction(view, AccessibilityNodeInfoCompat.ACTION_EXPAND); + ViewCompat.removeAccessibilityAction(view, AccessibilityNodeInfoCompat.ACTION_DISMISS); + + int expandHalfwayActionId = expandHalfwayActionIds.get(viewIndex, View.NO_ID); + if (expandHalfwayActionId != View.NO_ID) { + ViewCompat.removeAccessibilityAction(view, expandHalfwayActionId); + expandHalfwayActionIds.delete(viewIndex); } } private void replaceAccessibilityActionForState( - V child, AccessibilityActionCompat action, @State int state) { + View child, AccessibilityActionCompat action, @State int state) { ViewCompat.replaceAccessibilityAction( child, action, null, createAccessibilityViewCommandForState(state)); } private int addAccessibilityActionForState( - V child, @StringRes int stringResId, @State int state) { + View child, @StringRes int stringResId, @State int state) { return ViewCompat.addAccessibilityAction( child, child.getResources().getString(stringResId), @@ -2216,4 +2439,3 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo }; } } -