Created
August 27, 2020 17:21
-
-
Save Skyyo/5209819a61eccd6be69cd67fa7755ec5 to your computer and use it in GitHub Desktop.
Custom side sheet, until https://material.io/components/sheets-side get's published. #slide #sheet
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.animation.ValueAnimator; | |
import android.animation.ValueAnimator.AnimatorUpdateListener; | |
import android.annotation.SuppressLint; | |
import android.content.Context; | |
import android.content.res.ColorStateList; | |
import android.content.res.TypedArray; | |
import android.os.Build.VERSION; | |
import android.os.Build.VERSION_CODES; | |
import android.os.Parcel; | |
import android.os.Parcelable; | |
import android.util.AttributeSet; | |
import android.util.Log; | |
import android.util.TypedValue; | |
import android.view.MotionEvent; | |
import android.view.VelocityTracker; | |
import android.view.View; | |
import android.view.ViewConfiguration; | |
import android.view.ViewGroup; | |
import android.view.ViewParent; | |
import androidx.annotation.FloatRange; | |
import androidx.annotation.IntDef; | |
import androidx.annotation.NonNull; | |
import androidx.annotation.Nullable; | |
import androidx.annotation.StyleableRes; | |
import androidx.annotation.VisibleForTesting; | |
import androidx.appcompat.content.res.AppCompatResources; | |
import androidx.coordinatorlayout.widget.CoordinatorLayout; | |
import androidx.coordinatorlayout.widget.CoordinatorLayout.LayoutParams; | |
import androidx.core.math.MathUtils; | |
import androidx.core.view.ViewCompat; | |
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; | |
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat; | |
import androidx.core.view.accessibility.AccessibilityViewCommand; | |
import androidx.customview.view.AbsSavedState; | |
import androidx.customview.widget.ViewDragHelper; | |
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; | |
import java.util.ArrayList; | |
import java.util.HashMap; | |
import java.util.Map; | |
/** | |
* An interaction behavior plugin for a child view of {@link CoordinatorLayout} to make it work as a | |
* right sheet. | |
* | |
* <p>To send useful accessibility events, set a title on right sheets that are windows or are | |
* window-like. For RightSheetDialog use {@link RightSheetBehaviour setTitle(int)}, and for | |
* BottomSheetDialogFragment use {@link ViewCompat#setAccessibilityPaneTitle(View, CharSequence)}. | |
*/ | |
public class RightSheetBehaviour<V extends View> extends CoordinatorLayout.Behavior<V> { | |
/** | |
* The right sheet is dragging. | |
*/ | |
public static final int STATE_DRAGGING = 1; | |
/** | |
* The right sheet is settling. | |
*/ | |
public static final int STATE_SETTLING = 2; | |
/** | |
* The right sheet is expanded. | |
*/ | |
public static final int STATE_EXPANDED = 3; | |
/** | |
* The right sheet is collapsed. | |
*/ | |
public static final int STATE_COLLAPSED = 4; | |
/** | |
* The right sheet is hidden. | |
*/ | |
public static final int STATE_HIDDEN = 5; | |
/** | |
* The right sheet is half-expanded (used when mFitToContents is false). | |
*/ | |
public static final int STATE_HALF_EXPANDED = 6; | |
/** | |
* Peek at the 16:9 ratio keyline of its parent. | |
* | |
* <p>This can be used as a parameter for {@link #setPeekWidth(int)}. {@link #getPeekWidth()} | |
* will return this when the value is set. | |
*/ | |
public static final int PEEK_WIDTH_AUTO = -1; | |
/** | |
* This flag will preserve the peekWidth int value on configuration change. | |
*/ | |
public static final int SAVE_PEEK_WIDTH = 0x1; | |
/** | |
* This flag will preserve the fitToContents boolean value on configuration change. | |
*/ | |
public static final int SAVE_FIT_TO_CONTENTS = 1 << 1; | |
/** | |
* This flag will preserve the hideable boolean value on configuration change. | |
*/ | |
public static final int SAVE_HIDEABLE = 1 << 2; | |
/** | |
* This flag will preserve the skipCollapsed boolean value on configuration change. | |
*/ | |
public static final int SAVE_SKIP_COLLAPSED = 1 << 3; | |
/** | |
* This flag will preserve all aforementioned values on configuration change. | |
*/ | |
public static final int SAVE_ALL = -1; | |
/** | |
* This flag will not preserve the aforementioned values set at runtime if the view is destroyed | |
* and recreated. The only value preserved will be the positional state, e.g. collapsed, hidden, | |
* expanded, etc. This is the default behavior. | |
*/ | |
public static final int SAVE_NONE = 0; | |
private static final String TAG = "RightSheetBehaviour"; | |
private static final int SIGNIFICANT_VEL_THRESHOLD = 500; | |
private static final float HIDE_THRESHOLD = 0.5f; | |
private static final float HIDE_FRICTION = 0.1f; | |
private static final int CORNER_ANIMATION_DURATION = 500; | |
private static final int DEF_STYLE_RES = R.style.Widget_Design_RightSheet_Modal; | |
@NonNull | |
private final ArrayList<RightSheetCallback> callbacks = new ArrayList<>(); | |
@SaveFlags | |
private int saveFlags = SAVE_NONE; | |
private boolean fitToContents = true; | |
private boolean updateImportantForAccessibilityOnSiblings = false; | |
private float maximumVelocity; | |
/** | |
* Peek width set by the user. | |
*/ | |
private int peekWidth; | |
/** | |
* Whether or not to use automatic peek width. | |
*/ | |
private boolean peekWidthAuto; | |
/** | |
* Minimum peek width permitted. | |
*/ | |
private int peekWidthMin; | |
/** | |
* True if Behavior has a non-null value for the @shapeAppearance attribute | |
*/ | |
private boolean shapeThemingEnabled; | |
private MaterialShapeDrawable materialShapeDrawable; | |
/** | |
* Default Shape Appearance to be used in right sheet | |
*/ | |
private ShapeAppearanceModel shapeAppearanceModelDefault; | |
private boolean isShapeExpanded; | |
private SettleRunnable settleRunnable = null; | |
@Nullable | |
private ValueAnimator interpolatorAnimator; | |
private int expandedOffset; | |
private int fitToContentsOffset; | |
private int halfExpandedOffset; | |
private float halfExpandedRatio = 0.5f; | |
private int collapsedOffset; | |
private float elevation = -1; | |
private boolean hideable; | |
private boolean skipCollapsed; | |
private boolean draggable = true; | |
@State | |
private int state = STATE_COLLAPSED; | |
@Nullable | |
private ViewDragHelper viewDragHelper; | |
private boolean ignoreEvents; | |
private int lastNestedScrollDx; | |
private boolean nestedScrolled; | |
private int parentWidth; | |
private int parentHeight; | |
@Nullable | |
private WeakReference<V> viewRef; | |
@Nullable | |
private WeakReference<View> nestedScrollingChildRef; | |
@Nullable | |
private VelocityTracker velocityTracker; | |
private int activePointerId; | |
private int initialX; | |
private boolean touchingScrollingChild; | |
@Nullable | |
private Map<View, Integer> importantForAccessibilityMap; | |
private final ViewDragHelper.Callback dragCallback = | |
new ViewDragHelper.Callback() { | |
@Override | |
public boolean tryCaptureView(@NonNull View child, int pointerId) { | |
if (state == STATE_DRAGGING) { | |
return false; | |
} | |
if (touchingScrollingChild) { | |
return false; | |
} | |
if (state == STATE_EXPANDED && activePointerId == pointerId) { | |
View scroll = nestedScrollingChildRef != null ? nestedScrollingChildRef.get() : null; | |
if (scroll != null && scroll.canScrollHorizontally(-1)) { | |
// Let the content scroll up | |
return false; | |
} | |
} | |
return viewRef != null && viewRef.get() == child; | |
} | |
@Override | |
public void onViewPositionChanged( | |
@NonNull View changedView, | |
int left, | |
int top, | |
int dx, | |
int dy | |
) { | |
dispatchOnSlide(left); | |
} | |
@Override | |
public void onViewDragStateChanged(int state) { | |
if (state == ViewDragHelper.STATE_DRAGGING && draggable) { | |
setStateInternal(STATE_DRAGGING); | |
} | |
} | |
private boolean releasedLow(@NonNull View child) { | |
// Needs to be at least half way to the right. | |
return child.getLeft() > (parentWidth + getExpandedOffset()) / 2; | |
} | |
@Override | |
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) { | |
int left; | |
@State int targetState; | |
if (xvel < 0) { // Moving left | |
if (fitToContents) { | |
left = fitToContentsOffset; | |
targetState = STATE_EXPANDED; | |
} else { | |
int currentLeft = releasedChild.getLeft(); | |
if (currentLeft > halfExpandedOffset) { | |
left = halfExpandedOffset; | |
targetState = STATE_HALF_EXPANDED; | |
} else { | |
left = expandedOffset; | |
targetState = STATE_EXPANDED; | |
} | |
} | |
} else if (hideable && shouldHide(releasedChild, xvel)) { | |
// Hide if the view was either released low or it was a significant horizontal swipe | |
// otherwise settle to closest expanded state. | |
if ((Math.abs(yvel) < Math.abs(xvel) && xvel > SIGNIFICANT_VEL_THRESHOLD) | |
|| releasedLow(releasedChild)) { | |
left = parentWidth; | |
targetState = STATE_HIDDEN; | |
} else if (fitToContents) { | |
left = fitToContentsOffset; | |
targetState = STATE_EXPANDED; | |
} else if (Math.abs(releasedChild.getLeft() - expandedOffset) | |
< Math.abs(releasedChild.getLeft() - halfExpandedOffset)) { | |
left = expandedOffset; | |
targetState = STATE_EXPANDED; | |
} else { | |
left = halfExpandedOffset; | |
targetState = STATE_HALF_EXPANDED; | |
} | |
} else if (xvel == 0.f || Math.abs(yvel) > Math.abs(xvel)) { | |
// If the X velocity is 0 or the swipe was mostly vertical indicated by the Y velocity | |
// being greater than the X velocity, settle to the nearest correct width. | |
int currentLeft = releasedChild.getLeft(); | |
if (fitToContents) { | |
if (Math.abs(currentLeft - fitToContentsOffset) | |
< Math.abs(currentLeft - collapsedOffset)) { | |
left = fitToContentsOffset; | |
targetState = STATE_EXPANDED; | |
} else { | |
left = collapsedOffset; | |
targetState = STATE_COLLAPSED; | |
} | |
} else { | |
if (currentLeft < halfExpandedOffset) { | |
if (currentLeft < Math.abs(currentLeft - collapsedOffset)) { | |
left = expandedOffset; | |
targetState = STATE_EXPANDED; | |
} else { | |
left = halfExpandedOffset; | |
targetState = STATE_HALF_EXPANDED; | |
} | |
} else { | |
if (Math.abs(currentLeft - halfExpandedOffset) | |
< Math.abs(currentLeft - collapsedOffset)) { | |
left = halfExpandedOffset; | |
targetState = STATE_HALF_EXPANDED; | |
} else { | |
left = collapsedOffset; | |
targetState = STATE_COLLAPSED; | |
} | |
} | |
} | |
} else { // Moving Right | |
if (fitToContents) { | |
left = collapsedOffset; | |
targetState = STATE_COLLAPSED; | |
} else { | |
// Settle to the nearest correct width. | |
int currentLeft = releasedChild.getLeft(); | |
if (Math.abs(currentLeft - halfExpandedOffset) | |
< Math.abs(currentLeft - collapsedOffset)) { | |
left = halfExpandedOffset; | |
targetState = STATE_HALF_EXPANDED; | |
} else { | |
left = collapsedOffset; | |
targetState = STATE_COLLAPSED; | |
} | |
} | |
} | |
startSettlingAnimation(releasedChild, targetState, left, true); | |
} | |
@Override | |
public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) { | |
return MathUtils.clamp( | |
left, getExpandedOffset(), hideable ? parentWidth : collapsedOffset | |
); | |
} | |
@Override | |
public int getViewHorizontalDragRange(@NonNull View child) { | |
if (hideable) { | |
return parentWidth; | |
} else { | |
return collapsedOffset; | |
} | |
} | |
}; | |
public RightSheetBehaviour() { | |
} | |
@SuppressLint("PrivateResource") | |
public RightSheetBehaviour(@NonNull Context context, @Nullable AttributeSet attrs) { | |
super(context, attrs); | |
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.BottomSheetBehavior_Layout); | |
this.shapeThemingEnabled = a.hasValue(R.styleable.BottomSheetBehavior_Layout_shapeAppearance); | |
boolean hasBackgroundTint = a.hasValue(R.styleable.BottomSheetBehavior_Layout_backgroundTint); | |
if (hasBackgroundTint) { | |
ColorStateList rightSheetColor = | |
getColorStateList( | |
context, a, R.styleable.BottomSheetBehavior_Layout_backgroundTint); | |
createMaterialShapeDrawable(context, attrs, hasBackgroundTint, rightSheetColor); | |
} else { | |
createMaterialShapeDrawable(context, attrs, hasBackgroundTint); | |
} | |
createShapeValueAnimator(); | |
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { | |
this.elevation = a.getDimension(R.styleable.BottomSheetBehavior_Layout_android_elevation, -1); | |
} | |
TypedValue value = a.peekValue(R.styleable.BottomSheetBehavior_Layout_behavior_peekHeight); | |
if (value != null && value.data == PEEK_WIDTH_AUTO) { | |
setPeekWidth(value.data); | |
} else { | |
setPeekWidth( | |
a.getDimensionPixelSize( | |
R.styleable.BottomSheetBehavior_Layout_behavior_peekHeight, PEEK_WIDTH_AUTO)); | |
} | |
setHideable(a.getBoolean(R.styleable.BottomSheetBehavior_Layout_behavior_hideable, false)); | |
setFitToContents( | |
a.getBoolean(R.styleable.BottomSheetBehavior_Layout_behavior_fitToContents, true)); | |
setSkipCollapsed( | |
a.getBoolean(R.styleable.BottomSheetBehavior_Layout_behavior_skipCollapsed, false)); | |
//setDraggable(a.getBoolean(R.styleable.BottomSheetBehavior_Layout_behavior_draggable, true)); | |
setSaveFlags(a.getInt(R.styleable.BottomSheetBehavior_Layout_behavior_saveFlags, SAVE_NONE)); | |
setHalfExpandedRatio( | |
a.getFloat(R.styleable.BottomSheetBehavior_Layout_behavior_halfExpandedRatio, 0.5f)); | |
value = a.peekValue(R.styleable.BottomSheetBehavior_Layout_behavior_expandedOffset); | |
if (value != null && value.type == TypedValue.TYPE_FIRST_INT) { | |
setExpandedOffset(value.data); | |
} else { | |
setExpandedOffset( | |
a.getDimensionPixelOffset( | |
R.styleable.BottomSheetBehavior_Layout_behavior_expandedOffset, 0 | |
) | |
); | |
} | |
a.recycle(); | |
ViewConfiguration configuration = ViewConfiguration.get(context); | |
maximumVelocity = configuration.getScaledMaximumFlingVelocity(); | |
} | |
/** | |
* A utility function to get the {@link RightSheetBehaviour} associated with the {@code view}. | |
* | |
* @param view The {@link View} with {@link RightSheetBehaviour}. | |
* @return The {@link RightSheetBehaviour} associated with the {@code view}. | |
*/ | |
@NonNull | |
@SuppressWarnings("unchecked") | |
public static <V extends View> RightSheetBehaviour<V> from(@NonNull V view) { | |
ViewGroup.LayoutParams params = view.getLayoutParams(); | |
if (!(params instanceof LayoutParams)) { | |
throw new IllegalArgumentException("The view is not a child of CoordinatorLayout"); | |
} | |
CoordinatorLayout.Behavior<?> behavior = | |
((LayoutParams) params).getBehavior(); | |
if (!(behavior instanceof RightSheetBehaviour)) { | |
throw new IllegalArgumentException("The view is not associated with RightSheetBehaviour"); | |
} | |
return (RightSheetBehaviour<V>) behavior; | |
} | |
static ColorStateList getColorStateList( | |
@NonNull Context context, | |
@NonNull TypedArray attributes, | |
@StyleableRes int index | |
) { | |
if (attributes.hasValue(index)) { | |
int resourceId = attributes.getResourceId(index, 0); | |
if (resourceId != 0) { | |
ColorStateList value = AppCompatResources.getColorStateList(context, resourceId); | |
if (value != null) { | |
return value; | |
} | |
} | |
} | |
return attributes.getColorStateList(index); | |
} | |
@NonNull | |
@Override | |
public Parcelable onSaveInstanceState(@NonNull CoordinatorLayout parent, @NonNull V child) { | |
return new SavedState(super.onSaveInstanceState(parent, child), this); | |
} | |
@Override | |
public void onRestoreInstanceState( | |
@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull Parcelable state) { | |
SavedState ss = (SavedState) state; | |
super.onRestoreInstanceState(parent, child, ss.getSuperState()); | |
// Restore Optional State values designated by saveFlags | |
restoreOptionalState(ss); | |
// Intermediate states are restored as collapsed state | |
if (ss.state == STATE_DRAGGING || ss.state == STATE_SETTLING) { | |
this.state = STATE_COLLAPSED; | |
} else { | |
this.state = ss.state; | |
} | |
} | |
@Override | |
public void onAttachedToLayoutParams(@NonNull LayoutParams layoutParams) { | |
super.onAttachedToLayoutParams(layoutParams); | |
// These may already be null, but just be safe, explicitly assign them. This lets us know the | |
// first time we layout with this behavior by checking (viewRef == null). | |
viewRef = null; | |
viewDragHelper = null; | |
} | |
@Override | |
public void onDetachedFromLayoutParams() { | |
super.onDetachedFromLayoutParams(); | |
// Release references so we don't run unnecessary codepaths while not attached to a view. | |
viewRef = null; | |
viewDragHelper = null; | |
} | |
@Override | |
public boolean onLayoutChild( | |
@NonNull CoordinatorLayout parent, @NonNull V child, int layoutDirection) { | |
if (ViewCompat.getFitsSystemWindows(parent) && !ViewCompat.getFitsSystemWindows(child)) { | |
child.setFitsSystemWindows(true); | |
} | |
if (viewRef == null) { | |
// First layout with this behavior. | |
peekWidthMin = | |
parent.getResources().getDimensionPixelSize(R.dimen.design_right_sheet_peek_height_min); | |
viewRef = new WeakReference<>(child); | |
// Only set MaterialShapeDrawable as background if shapeTheming is enabled, otherwise will | |
// default to android:background declared in styles or layout. | |
if (shapeThemingEnabled && materialShapeDrawable != null) { | |
ViewCompat.setBackground(child, materialShapeDrawable); | |
} | |
// Set elevation on MaterialShapeDrawable | |
if (materialShapeDrawable != null) { | |
// Use elevation attr if set on right sheet; 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); | |
} | |
updateAccessibilityActions(); | |
if (ViewCompat.getImportantForAccessibility(child) | |
== ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { | |
ViewCompat.setImportantForAccessibility(child, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); | |
} | |
} | |
if (viewDragHelper == null) { | |
viewDragHelper = ViewDragHelper.create(parent, dragCallback); | |
} | |
int savedLeft = child.getLeft(); | |
// First let the parent lay it out | |
parent.onLayoutChild(child, layoutDirection); | |
// Offset the right sheet | |
parentWidth = parent.getWidth(); | |
parentHeight = parent.getHeight(); | |
fitToContentsOffset = Math.max(0, parentWidth - child.getWidth()); | |
calculateHalfExpandedOffset(); | |
calculateCollapsedOffset(); | |
if (state == STATE_EXPANDED) { | |
ViewCompat.offsetLeftAndRight(child, getExpandedOffset()); | |
} else if (state == STATE_HALF_EXPANDED) { | |
ViewCompat.offsetLeftAndRight(child, halfExpandedOffset); | |
} else if (hideable && state == STATE_HIDDEN) { | |
ViewCompat.offsetLeftAndRight(child, parentWidth); | |
} else if (state == STATE_COLLAPSED) { | |
ViewCompat.offsetLeftAndRight(child, collapsedOffset); | |
} else if (state == STATE_DRAGGING || state == STATE_SETTLING) { | |
ViewCompat.offsetLeftAndRight(child, savedLeft - child.getLeft()); | |
} | |
nestedScrollingChildRef = new WeakReference<>(findScrollingChild(child)); | |
return true; | |
} | |
@Override | |
public boolean onInterceptTouchEvent( | |
@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull MotionEvent event) { | |
if (!child.isShown() || !draggable) { | |
ignoreEvents = true; | |
return false; | |
} | |
int action = event.getActionMasked(); | |
// Record the velocity | |
if (action == MotionEvent.ACTION_DOWN) { | |
reset(); | |
} | |
if (velocityTracker == null) { | |
velocityTracker = VelocityTracker.obtain(); | |
} | |
velocityTracker.addMovement(event); | |
switch (action) { | |
case MotionEvent.ACTION_UP: | |
case MotionEvent.ACTION_CANCEL: | |
touchingScrollingChild = false; | |
activePointerId = MotionEvent.INVALID_POINTER_ID; | |
// Reset the ignore flag | |
if (ignoreEvents) { | |
ignoreEvents = false; | |
return false; | |
} | |
break; | |
case MotionEvent.ACTION_DOWN: | |
initialX = (int) event.getX(); | |
int initialY = (int) event.getY(); | |
// Only intercept nested scrolling events here if the view not being moved by the | |
// ViewDragHelper. | |
if (state != STATE_SETTLING) { | |
View scroll = nestedScrollingChildRef != null ? nestedScrollingChildRef.get() : null; | |
if (scroll != null && parent.isPointInChildBounds(scroll, initialX, initialY)) { | |
activePointerId = event.getPointerId(event.getActionIndex()); | |
touchingScrollingChild = true; | |
} | |
} | |
ignoreEvents = | |
activePointerId == MotionEvent.INVALID_POINTER_ID | |
&& !parent.isPointInChildBounds(child, initialX, initialY); | |
break; | |
default: // fall out | |
} | |
if (!ignoreEvents | |
&& viewDragHelper != null | |
&& viewDragHelper.shouldInterceptTouchEvent(event)) { | |
return true; | |
} | |
// We have to handle cases that the ViewDragHelper does not capture the right sheet because | |
// it is not the top most view of its parent. This is not necessary when the touch event is | |
// happening over the scrolling content as nested scrolling logic handles that case. | |
View scroll = nestedScrollingChildRef != null ? nestedScrollingChildRef.get() : null; | |
return action == MotionEvent.ACTION_MOVE | |
&& scroll != null | |
&& !ignoreEvents | |
&& state != STATE_DRAGGING | |
&& !parent.isPointInChildBounds(scroll, (int) event.getX(), (int) event.getY()) | |
&& viewDragHelper != null | |
&& Math.abs(initialX - event.getX()) > viewDragHelper.getTouchSlop(); | |
} | |
@Override | |
public boolean onTouchEvent( | |
@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull MotionEvent event) { | |
if (!child.isShown()) { | |
return false; | |
} | |
int action = event.getActionMasked(); | |
if (state == STATE_DRAGGING && action == MotionEvent.ACTION_DOWN) { | |
return true; | |
} | |
if (viewDragHelper != null) { | |
viewDragHelper.processTouchEvent(event); | |
} | |
// Record the velocity | |
if (action == MotionEvent.ACTION_DOWN) { | |
reset(); | |
} | |
if (velocityTracker == null) { | |
velocityTracker = VelocityTracker.obtain(); | |
} | |
velocityTracker.addMovement(event); | |
// The ViewDragHelper tries to capture only the top-most View. We have to explicitly tell it | |
// to capture the right sheet in case it is not captured and the touch slop is passed. | |
if (action == MotionEvent.ACTION_MOVE && !ignoreEvents) { | |
if (Math.abs(initialX - event.getX()) > viewDragHelper.getTouchSlop()) { | |
viewDragHelper.captureChildView(child, event.getPointerId(event.getActionIndex())); | |
} | |
} | |
return !ignoreEvents; | |
} | |
@Override | |
public boolean onStartNestedScroll( | |
@NonNull CoordinatorLayout coordinatorLayout, | |
@NonNull V child, | |
@NonNull View directTargetChild, | |
@NonNull View target, | |
int axes, | |
int type) { | |
lastNestedScrollDx = 0; | |
nestedScrolled = false; | |
return (axes & ViewCompat.SCROLL_AXIS_HORIZONTAL) != 0; | |
} | |
@Override | |
public void onNestedPreScroll( | |
@NonNull CoordinatorLayout coordinatorLayout, | |
@NonNull V child, | |
@NonNull View target, | |
int dx, | |
int dy, | |
@NonNull int[] consumed, | |
int type | |
) { | |
if (type == ViewCompat.TYPE_NON_TOUCH) { | |
// Ignore fling here. The ViewDragHelper handles it. | |
return; | |
} | |
View scrollingChild = nestedScrollingChildRef != null ? nestedScrollingChildRef.get() : null; | |
if (target != scrollingChild) { | |
return; | |
} | |
int currentLeft = child.getLeft(); | |
int newLeft = currentLeft - dx; | |
if (dx > 0) { // Left | |
if (newLeft < getExpandedOffset()) { | |
consumed[1] = currentLeft - getExpandedOffset(); | |
ViewCompat.offsetLeftAndRight(child, -consumed[1]); | |
setStateInternal(STATE_EXPANDED); | |
} else { | |
if (!draggable) { | |
// Prevent dragging | |
return; | |
} | |
consumed[1] = dx; | |
ViewCompat.offsetLeftAndRight(child, -dx); | |
setStateInternal(STATE_DRAGGING); | |
} | |
} else if (dx < 0) { // Right | |
if (!target.canScrollHorizontally(-1)) { | |
if (newLeft <= collapsedOffset || hideable) { | |
if (!draggable) { | |
// Prevent dragging | |
return; | |
} | |
consumed[1] = dx; | |
ViewCompat.offsetLeftAndRight(child, -dx); | |
setStateInternal(STATE_DRAGGING); | |
} else { | |
consumed[1] = currentLeft - collapsedOffset; | |
ViewCompat.offsetLeftAndRight(child, -consumed[1]); | |
setStateInternal(STATE_COLLAPSED); | |
} | |
} | |
} | |
dispatchOnSlide(child.getLeft()); | |
lastNestedScrollDx = dx; | |
nestedScrolled = true; | |
} | |
@Override | |
public void onStopNestedScroll( | |
@NonNull CoordinatorLayout coordinatorLayout, | |
@NonNull V child, | |
@NonNull View target, | |
int type | |
) { | |
if (child.getLeft() == getExpandedOffset()) { | |
setStateInternal(STATE_EXPANDED); | |
return; | |
} | |
if (nestedScrollingChildRef == null | |
|| target != nestedScrollingChildRef.get() | |
|| !nestedScrolled | |
) { | |
return; | |
} | |
int left; | |
int targetState; | |
if (lastNestedScrollDx > 0) { | |
if (fitToContents) { | |
left = fitToContentsOffset; | |
targetState = STATE_EXPANDED; | |
} else { | |
int currentLeft = child.getLeft(); | |
if (currentLeft > halfExpandedOffset) { | |
left = halfExpandedOffset; | |
targetState = STATE_HALF_EXPANDED; | |
} else { | |
left = expandedOffset; | |
targetState = STATE_EXPANDED; | |
} | |
} | |
} else if (hideable && shouldHide(child, getXVelocity())) { | |
left = parentWidth; | |
targetState = STATE_HIDDEN; | |
} else if (lastNestedScrollDx == 0) { | |
int currentLeft = child.getLeft(); | |
if (fitToContents) { | |
if (Math.abs(currentLeft - fitToContentsOffset) < Math.abs(currentLeft - collapsedOffset)) { | |
left = fitToContentsOffset; | |
targetState = STATE_EXPANDED; | |
} else { | |
left = collapsedOffset; | |
targetState = STATE_COLLAPSED; | |
} | |
} else { | |
if (currentLeft < halfExpandedOffset) { | |
if (currentLeft < Math.abs(currentLeft - collapsedOffset)) { | |
left = expandedOffset; | |
targetState = STATE_EXPANDED; | |
} else { | |
left = halfExpandedOffset; | |
targetState = STATE_HALF_EXPANDED; | |
} | |
} else { | |
if (Math.abs(currentLeft - halfExpandedOffset) < Math.abs(currentLeft - collapsedOffset)) { | |
left = halfExpandedOffset; | |
targetState = STATE_HALF_EXPANDED; | |
} else { | |
left = collapsedOffset; | |
targetState = STATE_COLLAPSED; | |
} | |
} | |
} | |
} else { | |
if (fitToContents) { | |
left = collapsedOffset; | |
targetState = STATE_COLLAPSED; | |
} else { | |
// Settle to nearest width. | |
int currentLeft = child.getLeft(); | |
if (Math.abs(currentLeft - halfExpandedOffset) < Math.abs(currentLeft - collapsedOffset)) { | |
left = halfExpandedOffset; | |
targetState = STATE_HALF_EXPANDED; | |
} else { | |
left = collapsedOffset; | |
targetState = STATE_COLLAPSED; | |
} | |
} | |
} | |
startSettlingAnimation(child, targetState, left, false); | |
nestedScrolled = false; | |
} | |
@Override | |
public void onNestedScroll( | |
@NonNull CoordinatorLayout coordinatorLayout, | |
@NonNull V child, | |
@NonNull View target, | |
int dxConsumed, | |
int dyConsumed, | |
int dxUnconsumed, | |
int dyUnconsumed, | |
int type, | |
@NonNull int[] consumed) { | |
// Overridden to prevent the default consumption of the entire scroll distance. | |
} | |
@Override | |
public boolean onNestedPreFling( | |
@NonNull CoordinatorLayout coordinatorLayout, | |
@NonNull V child, | |
@NonNull View target, | |
float velocityX, | |
float velocityY) { | |
if (nestedScrollingChildRef != null) { | |
return target == nestedScrollingChildRef.get() | |
&& (state != STATE_EXPANDED | |
|| super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY)); | |
} else { | |
return false; | |
} | |
} | |
/** | |
* @return whether the width of the expanded sheet is determined by the width of its contents, | |
* or if it is expanded in two stages (half the width of the parent container, full width of | |
* parent container). | |
*/ | |
public boolean isFitToContents() { | |
return fitToContents; | |
} | |
/** | |
* Sets whether the width of the expanded sheet is determined by the width of its contents, or | |
* if it is expanded in two stages (half the width of the parent container, full width of parent | |
* container). Default value is true. | |
* | |
* @param fitToContents whether or not to fit the expanded sheet to its contents. | |
*/ | |
public void setFitToContents(boolean fitToContents) { | |
if (this.fitToContents == fitToContents) { | |
return; | |
} | |
this.fitToContents = fitToContents; | |
// If sheet is already laid out, recalculate the collapsed offset based on new setting. | |
// Otherwise, let onLayoutChild handle this later. | |
if (viewRef != null) { | |
calculateCollapsedOffset(); | |
} | |
// 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); | |
updateAccessibilityActions(); | |
} | |
/** | |
* Sets the width of the right sheet when it is collapsed while optionally animating between the | |
* old width and the new width. | |
* | |
* @param peekWidth The width of the collapsed right sheet in pixels, or {@link | |
* #PEEK_WIDTH_AUTO} to configure the sheet to peek automatically at 16:9 ratio keyline. | |
* @param animate Whether to animate between the old width and the new width. | |
* @attr ref | |
* R.styleable#BottomSheetBehavior_Layout_behavior_peekWidth | |
*/ | |
public final void setPeekWidth(int peekWidth, boolean animate) { | |
boolean layout = false; | |
if (peekWidth == PEEK_WIDTH_AUTO) { | |
if (!peekWidthAuto) { | |
peekWidthAuto = true; | |
layout = true; | |
} | |
} else if (peekWidthAuto || this.peekWidth != peekWidth) { | |
peekWidthAuto = false; | |
this.peekWidth = Math.max(0, peekWidth); | |
layout = true; | |
} | |
// If sheet is already laid out, recalculate the collapsed offset based on new setting. | |
// Otherwise, let onLayoutChild handle this later. | |
if (layout && viewRef != null) { | |
calculateCollapsedOffset(); | |
if (state == STATE_COLLAPSED) { | |
V view = viewRef.get(); | |
if (view != null) { | |
if (animate) { | |
settleToStatePendingLayout(state); | |
} else { | |
view.requestLayout(); | |
} | |
} | |
} | |
} | |
} | |
/** | |
* Gets the width of the right sheet when it is collapsed. | |
* | |
* @return The width of the collapsed right sheet in pixels, or {@link #PEEK_WIDTH_AUTO} if the | |
* sheet is configured to peek automatically at 16:9 ratio keyline | |
* @attr ref | |
* R.styleable#BottomSheetBehavior_Layout_behavior_peekWidth | |
*/ | |
public int getPeekWidth() { | |
return peekWidthAuto ? PEEK_WIDTH_AUTO : peekWidth; | |
} | |
/** | |
* Sets the width of the right sheet when it is collapsed. | |
* | |
* @param peekWidth The width of the collapsed right sheet in pixels, or {@link | |
* #PEEK_WIDTH_AUTO} to configure the sheet to peek automatically at 16:9 ratio keyline. | |
* @attr ref | |
* R.styleable#BottomSheetBehavior_Layout_behavior_peekWidth | |
*/ | |
public void setPeekWidth(int peekWidth) { | |
setPeekWidth(peekWidth, false); | |
} | |
/** | |
* Gets the ratio for the width of the RightSheet in the {@link #STATE_HALF_EXPANDED} state. | |
* | |
* @attr R.styleable#BottomSheetBehavior_Layout_behavior_halfExpandedRatio | |
*/ | |
@FloatRange(from = 0.0f, to = 1.0f) | |
public float getHalfExpandedRatio() { | |
return halfExpandedRatio; | |
} | |
/** | |
* Determines the width of the RightSheet in the {@link #STATE_HALF_EXPANDED} state. The | |
* material guidelines recommended a value of 0.5, which results in the sheet filling half of the | |
* parent. The width of the RightSheet will be smaller as this ratio is decreased and taller as | |
* it is increased. The default value is 0.5. | |
* | |
* @param ratio a float between 0 and 1, representing the {@link #STATE_HALF_EXPANDED} ratio. | |
* @attr R.styleable#RigthSheetBehavior_Layout_behavior_halfExpandedRatio | |
*/ | |
public void setHalfExpandedRatio(@FloatRange(from = 0.0f, to = 1.0f) float ratio) { | |
if ((ratio <= 0) || (ratio >= 1)) { | |
throw new IllegalArgumentException("ratio must be a float value between 0 and 1"); | |
} | |
this.halfExpandedRatio = ratio; | |
// If sheet is already laid out, recalculate the half expanded offset based on new setting. | |
// Otherwise, let onLayoutChild handle this later. | |
if (viewRef != null) { | |
calculateHalfExpandedOffset(); | |
} | |
} | |
/** | |
* Returns the current expanded offset. If {@code fitToContents} is true, it will automatically | |
* pick the offset depending on the width of the content. | |
* | |
* @attr R.styleable#BottomSheetBehavior_Layout_behavior_expandedOffset | |
*/ | |
public int getExpandedOffset() { | |
return fitToContents ? fitToContentsOffset : expandedOffset; | |
} | |
/** | |
* Determines the top offset of the RightSheet in the {@link #STATE_EXPANDED} state when | |
* fitsToContent is false. The default value is 0, which results in the sheet matching the | |
* parent's top. | |
* | |
* @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 R.styleable#BottomSheetBehavior_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; | |
} | |
/** | |
* Gets whether this right sheet can hide when it is swiped down. | |
* | |
* @return {@code true} if this right sheet can hide. | |
* @attr ref R.styleable#BottomSheetBehavior_Layout_behavior_hideable | |
*/ | |
public boolean isHideable() { | |
return hideable; | |
} | |
/** | |
* Sets whether this right sheet can hide when it is swiped down. | |
* | |
* @param hideable {@code true} to make this right sheet hideable. | |
* @attr ref R.styleable#BottomSheetBehavior_Layout_behavior_hideable | |
*/ | |
public void setHideable(boolean hideable) { | |
if (this.hideable != hideable) { | |
this.hideable = hideable; | |
if (!hideable && state == STATE_HIDDEN) { | |
// Lift up to collapsed state | |
setState(STATE_COLLAPSED); | |
} | |
updateAccessibilityActions(); | |
} | |
} | |
/** | |
* Sets whether this right sheet should skip the collapsed state when it is being hidden after it | |
* is expanded once. | |
* | |
* @return Whether the right sheet should skip the collapsed state. | |
* @attr ref R.styleable#BottomSheetBehavior_Layout_behavior_skipCollapsed | |
*/ | |
public boolean getSkipCollapsed() { | |
return skipCollapsed; | |
} | |
/** | |
* Sets whether this right sheet should skip the collapsed state when it is being hidden after it | |
* is expanded once. Setting this to true has no effect unless the sheet is hideable. | |
* | |
* @param skipCollapsed True if the right sheet should skip the collapsed state. | |
* @attr ref R.styleable#BottomSheetBehavior_Layout_behavior_skipCollapsed | |
*/ | |
public void setSkipCollapsed(boolean skipCollapsed) { | |
this.skipCollapsed = skipCollapsed; | |
} | |
public boolean isDraggable() { | |
return draggable; | |
} | |
/** | |
* Sets whether this right sheet is can be collapsed/expanded by dragging. Note: When disabling | |
* dragging, an app will require to implement a custom way to expand/collapse the right sheet | |
* | |
* @param draggable {@code false} to prevent dragging the sheet to collapse and expand | |
* @attr ref R.styleable#BottomSheetBehavior_Layout_behavior_draggable | |
*/ | |
public void setDraggable(boolean draggable) { | |
this.draggable = draggable; | |
} | |
/** | |
* Returns the save flags. | |
* | |
* @attr ref R.styleable#BottomSheetBehavior_Layout_behavior_saveFlags | |
* @see #setSaveFlags(int) | |
*/ | |
@SaveFlags | |
public int getSaveFlags() { | |
return this.saveFlags; | |
} | |
/** | |
* Sets save flags to be preserved in right sheet on configuration change. | |
* | |
* @param flags bitwise int of {@link #SAVE_PEEK_WIDTH}, {@link #SAVE_FIT_TO_CONTENTS}, {@link | |
* #SAVE_HIDEABLE}, {@link #SAVE_SKIP_COLLAPSED}, {@link #SAVE_ALL} and {@link #SAVE_NONE}. | |
* @attr ref R.styleable#BottomSheetBehavior_Layout_behavior_saveFlags | |
* @see #getSaveFlags() | |
*/ | |
public void setSaveFlags(@SaveFlags int flags) { | |
this.saveFlags = flags; | |
} | |
/** | |
* Sets a callback to be notified of right sheet events. | |
* | |
* @param callback The callback to notify when right sheet events occur. | |
* @deprecated use {@link #addRightSheetCallback(RightSheetCallback)} and {@link | |
* #removeRightSheetCallback(RightSheetCallback)} instead | |
*/ | |
@Deprecated | |
public void setRightSheetCallback(RightSheetCallback callback) { | |
Log.w( | |
TAG, | |
"RightSheetBehaviour now supports multiple callbacks. `setRightSheetCallback()` 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" | |
+ " `addRightSheetCallback()` and `removeRightSheetCallback()` instead to set your" | |
+ " own callbacks."); | |
callbacks.clear(); | |
if (callback != null) { | |
callbacks.add(callback); | |
} | |
} | |
/** | |
* Adds a callback to be notified of right sheet events. | |
* | |
* @param callback The callback to notify when right sheet events occur. | |
*/ | |
public void addRightSheetCallback(@NonNull RightSheetCallback callback) { | |
if (!callbacks.contains(callback)) { | |
callbacks.add(callback); | |
} | |
} | |
/** | |
* Removes a previously added callback. | |
* | |
* @param callback The callback to remove. | |
*/ | |
public void removeRightSheetCallback(@NonNull RightSheetCallback callback) { | |
callbacks.remove(callback); | |
} | |
private void settleToStatePendingLayout(@State int state) { | |
final V child = viewRef.get(); | |
if (child == null) { | |
return; | |
} | |
// Start the animation; wait until a pending layout if there is one. | |
ViewParent parent = child.getParent(); | |
if (parent != null && parent.isLayoutRequested() && ViewCompat.isAttachedToWindow(child)) { | |
final int finalState = state; | |
child.post( | |
new Runnable() { | |
@Override | |
public void run() { | |
settleToState(child, finalState); | |
} | |
}); | |
} else { | |
settleToState(child, state); | |
} | |
} | |
/** | |
* Gets the current state of the right sheet. | |
* | |
* @return One of {@link #STATE_EXPANDED}, {@link #STATE_HALF_EXPANDED}, {@link #STATE_COLLAPSED}, | |
* {@link #STATE_DRAGGING}, {@link #STATE_SETTLING}, or {@link #STATE_HALF_EXPANDED}. | |
*/ | |
@State | |
public int getState() { | |
return state; | |
} | |
/** | |
* Sets the state of the right sheet. The right sheet will transition to that state with | |
* animation. | |
* | |
* @param state One of {@link #STATE_COLLAPSED}, {@link #STATE_EXPANDED}, {@link #STATE_HIDDEN}, | |
* or {@link #STATE_HALF_EXPANDED}. | |
*/ | |
public void setState(@State int state) { | |
if (state == this.state) { | |
return; | |
} | |
if (viewRef == null) { | |
// The view is not laid out yet; modify mState and let onLayoutChild handle it later | |
if (state == STATE_COLLAPSED | |
|| state == STATE_EXPANDED | |
|| state == STATE_HALF_EXPANDED | |
|| (hideable && state == STATE_HIDDEN)) { | |
this.state = state; | |
} | |
return; | |
} | |
settleToStatePendingLayout(state); | |
} | |
private void setStateInternal(@State int state) { | |
if (this.state == state) { | |
return; | |
} | |
this.state = state; | |
if (viewRef == null) { | |
return; | |
} | |
View rightSheet = viewRef.get(); | |
if (rightSheet == null) { | |
return; | |
} | |
if (state == STATE_EXPANDED) { | |
updateImportantForAccessibility(true); | |
} else if (state == STATE_HALF_EXPANDED || state == STATE_HIDDEN || state == STATE_COLLAPSED) { | |
updateImportantForAccessibility(false); | |
} | |
updateDrawableForTargetState(state); | |
for (int i = 0; i < callbacks.size(); i++) { | |
callbacks.get(i).onStateChanged(rightSheet, state); | |
} | |
updateAccessibilityActions(); | |
} | |
private void updateDrawableForTargetState(@State int state) { | |
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(); | |
} | |
} | |
} | |
} | |
private int calculatePeekWidth() { | |
if (peekWidthAuto) { | |
return Math.max(peekWidthMin, parentWidth - parentHeight * 9 / 16); | |
} | |
return peekWidth; | |
} | |
// TO-CHECK | |
private void calculateCollapsedOffset() { | |
int peek = calculatePeekWidth(); | |
if (fitToContents) { | |
collapsedOffset = Math.max(parentWidth - peek, fitToContentsOffset); | |
} else { | |
collapsedOffset = parentWidth - peek; | |
} | |
} | |
// TO-CHECK | |
private void calculateHalfExpandedOffset() { | |
this.halfExpandedOffset = (int) (parentWidth * (1 - halfExpandedRatio)); | |
} | |
private void reset() { | |
activePointerId = ViewDragHelper.INVALID_POINTER; | |
if (velocityTracker != null) { | |
velocityTracker.recycle(); | |
velocityTracker = null; | |
} | |
} | |
private void restoreOptionalState(@NonNull SavedState ss) { | |
if (this.saveFlags == SAVE_NONE) { | |
return; | |
} | |
if (this.saveFlags == SAVE_ALL || (this.saveFlags & SAVE_PEEK_WIDTH) == SAVE_PEEK_WIDTH) { | |
this.peekWidth = ss.peekWidth; | |
} | |
if (this.saveFlags == SAVE_ALL | |
|| (this.saveFlags & SAVE_FIT_TO_CONTENTS) == SAVE_FIT_TO_CONTENTS) { | |
this.fitToContents = ss.fitToContents; | |
} | |
if (this.saveFlags == SAVE_ALL || (this.saveFlags & SAVE_HIDEABLE) == SAVE_HIDEABLE) { | |
this.hideable = ss.hideable; | |
} | |
if (this.saveFlags == SAVE_ALL | |
|| (this.saveFlags & SAVE_SKIP_COLLAPSED) == SAVE_SKIP_COLLAPSED) { | |
this.skipCollapsed = ss.skipCollapsed; | |
} | |
} | |
private boolean shouldHide(@NonNull View child, float xvel) { | |
if (skipCollapsed) { | |
return true; | |
} | |
if (child.getLeft() < collapsedOffset) { | |
// It should not hide, but collapse. | |
return false; | |
} | |
int peek = calculatePeekWidth(); | |
final float newLeft = child.getLeft() + xvel * HIDE_FRICTION; | |
return Math.abs(newLeft - collapsedOffset) / (float) peek > HIDE_THRESHOLD; | |
} | |
@Nullable | |
@VisibleForTesting | |
private View findScrollingChild(View view) { | |
if (ViewCompat.isNestedScrollingEnabled(view)) { | |
return view; | |
} | |
if (view instanceof ViewGroup) { | |
ViewGroup group = (ViewGroup) view; | |
for (int i = 0, count = group.getChildCount(); i < count; i++) { | |
View scrollingChild = findScrollingChild(group.getChildAt(i)); | |
if (scrollingChild != null) { | |
return scrollingChild; | |
} | |
} | |
} | |
return null; | |
} | |
private void createMaterialShapeDrawable( | |
@NonNull Context context, AttributeSet attrs, boolean hasBackgroundTint) { | |
this.createMaterialShapeDrawable(context, attrs, hasBackgroundTint, null); | |
} | |
private void createMaterialShapeDrawable( | |
@NonNull Context context, | |
AttributeSet attrs, | |
boolean hasBackgroundTint, | |
@Nullable ColorStateList rightSheetColor) { | |
if (this.shapeThemingEnabled) { | |
this.shapeAppearanceModelDefault = | |
ShapeAppearanceModel.builder(context, attrs, R.attr.bottomSheetStyle, DEF_STYLE_RES) | |
.build(); | |
this.materialShapeDrawable = new MaterialShapeDrawable(shapeAppearanceModelDefault); | |
this.materialShapeDrawable.initializeElevationOverlay(context); | |
if (hasBackgroundTint && rightSheetColor != null) { | |
materialShapeDrawable.setFillColor(rightSheetColor); | |
} else { | |
// If the tint isn't set, use the theme default background color. | |
TypedValue defaultColor = new TypedValue(); | |
context.getTheme().resolveAttribute(android.R.attr.colorBackground, defaultColor, true); | |
materialShapeDrawable.setTint(defaultColor.data); | |
} | |
} | |
} | |
private void createShapeValueAnimator() { | |
interpolatorAnimator = ValueAnimator.ofFloat(0f, 1f); | |
interpolatorAnimator.setDuration(CORNER_ANIMATION_DURATION); | |
interpolatorAnimator.addUpdateListener( | |
new AnimatorUpdateListener() { | |
@Override | |
public void onAnimationUpdate(@NonNull ValueAnimator animation) { | |
float value = (float) animation.getAnimatedValue(); | |
if (materialShapeDrawable != null) { | |
materialShapeDrawable.setInterpolation(value); | |
} | |
} | |
}); | |
} | |
private float getXVelocity() { | |
if (velocityTracker == null) { | |
return 0; | |
} | |
velocityTracker.computeCurrentVelocity(1000, maximumVelocity); | |
return velocityTracker.getXVelocity(activePointerId); | |
} | |
private void settleToState(@NonNull View child, int state) { | |
int left; | |
if (state == STATE_COLLAPSED) { | |
left = collapsedOffset; | |
} else if (state == STATE_HALF_EXPANDED) { | |
left = halfExpandedOffset; | |
if (fitToContents && left <= fitToContentsOffset) { | |
// Skip to the expanded state if we would scroll past the width of the contents. | |
state = STATE_EXPANDED; | |
left = fitToContentsOffset; | |
} | |
} else if (state == STATE_EXPANDED) { | |
left = getExpandedOffset(); | |
} else if (hideable && state == STATE_HIDDEN) { | |
left = parentWidth; | |
} else { | |
throw new IllegalArgumentException("Illegal state argument: " + state); | |
} | |
startSettlingAnimation(child, state, left, false); | |
} | |
private void startSettlingAnimation(View child, int state, int left, boolean settleFromViewDragHelper) { | |
boolean startedSettling = | |
settleFromViewDragHelper | |
? viewDragHelper.settleCapturedViewAt(left, child.getTop()) | |
: viewDragHelper.smoothSlideViewTo(child, left, child.getTop()); | |
if (startedSettling) { | |
setStateInternal(STATE_SETTLING); | |
// STATE_SETTLING won't animate the material shape, so do that here with the target state. | |
updateDrawableForTargetState(state); | |
if (settleRunnable == null) { | |
// If the singleton SettleRunnable instance has not been instantiated, create it. | |
settleRunnable = new SettleRunnable(child, state); | |
} | |
// If the SettleRunnable has not been posted, post it with the correct state. | |
if (settleRunnable.isPosted == false) { | |
settleRunnable.targetState = state; | |
ViewCompat.postOnAnimation(child, settleRunnable); | |
settleRunnable.isPosted = true; | |
} else { | |
// Otherwise, if it has been posted, just update the target state. | |
settleRunnable.targetState = state; | |
} | |
} else { | |
setStateInternal(state); | |
} | |
} | |
private void dispatchOnSlide(int left) { | |
View sheet = viewRef.get(); | |
if (sheet != null && !callbacks.isEmpty()) { | |
float slideOffset = | |
(left > collapsedOffset || collapsedOffset == getExpandedOffset()) | |
? (float) (collapsedOffset - left) / (parentWidth - collapsedOffset) | |
: (float) (collapsedOffset - left) / (collapsedOffset - getExpandedOffset()); | |
for (int i = 0; i < callbacks.size(); i++) { | |
callbacks.get(i).onSlide(sheet, slideOffset); | |
} | |
} | |
} | |
@VisibleForTesting | |
int getPeekWidthMin() { | |
return peekWidthMin; | |
} | |
/** | |
* Disables the shaped corner {@link ShapeAppearanceModel} interpolation transition animations. | |
* Will have no effect unless the sheet utilizes a {@link MaterialShapeDrawable} with set shape | |
* theming properties. Only For use in UI testing. | |
*/ | |
@VisibleForTesting | |
public void disableShapeAnimations() { | |
// Sets the shape value animator to null, prevents animations from occuring during testing. | |
interpolatorAnimator = null; | |
} | |
/** | |
* Sets whether the RightSheet should update the accessibility status of its {@link * | |
* CoordinatorLayout} siblings when expanded. | |
* | |
* <p>Set this to true if the expanded state of the sheet blocks access to siblings (e.g., when | |
* the sheet expands over the full screen). | |
*/ | |
public void setUpdateImportantForAccessibilityOnSiblings( | |
boolean updateImportantForAccessibilityOnSiblings) { | |
this.updateImportantForAccessibilityOnSiblings = updateImportantForAccessibilityOnSiblings; | |
} | |
private void updateImportantForAccessibility(boolean expanded) { | |
if (viewRef == null) { | |
return; | |
} | |
ViewParent viewParent = viewRef.get().getParent(); | |
if (!(viewParent instanceof CoordinatorLayout)) { | |
return; | |
} | |
CoordinatorLayout parent = (CoordinatorLayout) viewParent; | |
final int childCount = parent.getChildCount(); | |
if ((VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) && expanded) { | |
if (importantForAccessibilityMap == null) { | |
importantForAccessibilityMap = new HashMap<>(childCount); | |
} else { | |
// The important for accessibility values of the child views have been saved already. | |
return; | |
} | |
} | |
for (int i = 0; i < childCount; i++) { | |
final View child = parent.getChildAt(i); | |
if (child == viewRef.get()) { | |
continue; | |
} | |
if (expanded) { | |
// Saves the important for accessibility value of the child view. | |
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) { | |
importantForAccessibilityMap.put(child, child.getImportantForAccessibility()); | |
} | |
if (updateImportantForAccessibilityOnSiblings) { | |
ViewCompat.setImportantForAccessibility( | |
child, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); | |
} | |
} else { | |
if (updateImportantForAccessibilityOnSiblings | |
&& importantForAccessibilityMap != null | |
&& importantForAccessibilityMap.containsKey(child)) { | |
// Restores the original important for accessibility value of the child view. | |
ViewCompat.setImportantForAccessibility(child, importantForAccessibilityMap.get(child)); | |
} | |
} | |
} | |
if (!expanded) { | |
importantForAccessibilityMap = null; | |
} | |
} | |
@SuppressLint("SwitchIntDef") | |
private void updateAccessibilityActions() { | |
if (viewRef == null) { | |
return; | |
} | |
V child = viewRef.get(); | |
if (child == null) { | |
return; | |
} | |
ViewCompat.removeAccessibilityAction(child, AccessibilityNodeInfoCompat.ACTION_COLLAPSE); | |
ViewCompat.removeAccessibilityAction(child, AccessibilityNodeInfoCompat.ACTION_EXPAND); | |
ViewCompat.removeAccessibilityAction(child, AccessibilityNodeInfoCompat.ACTION_DISMISS); | |
if (hideable && state != STATE_HIDDEN) { | |
addAccessibilityActionForState(child, AccessibilityActionCompat.ACTION_DISMISS, STATE_HIDDEN); | |
} | |
switch (state) { | |
case STATE_EXPANDED: { | |
int nextState = fitToContents ? STATE_COLLAPSED : STATE_HALF_EXPANDED; | |
addAccessibilityActionForState( | |
child, AccessibilityActionCompat.ACTION_COLLAPSE, nextState); | |
break; | |
} | |
case STATE_HALF_EXPANDED: { | |
addAccessibilityActionForState( | |
child, AccessibilityActionCompat.ACTION_COLLAPSE, STATE_COLLAPSED); | |
addAccessibilityActionForState( | |
child, AccessibilityActionCompat.ACTION_EXPAND, STATE_EXPANDED); | |
break; | |
} | |
case STATE_COLLAPSED: { | |
int nextState = fitToContents ? STATE_EXPANDED : STATE_HALF_EXPANDED; | |
addAccessibilityActionForState(child, AccessibilityActionCompat.ACTION_EXPAND, nextState); | |
break; | |
} | |
default: // fall out | |
} | |
} | |
private void addAccessibilityActionForState( | |
V child, AccessibilityActionCompat action, final int state) { | |
ViewCompat.replaceAccessibilityAction( | |
child, | |
action, | |
null, | |
new AccessibilityViewCommand() { | |
@Override | |
public boolean perform(@NonNull View view, @Nullable CommandArguments arguments) { | |
setState(state); | |
return true; | |
} | |
}); | |
} | |
@IntDef({ | |
STATE_EXPANDED, | |
STATE_COLLAPSED, | |
STATE_DRAGGING, | |
STATE_SETTLING, | |
STATE_HIDDEN, | |
STATE_HALF_EXPANDED | |
}) | |
@Retention(RetentionPolicy.SOURCE) | |
public @interface State { | |
} | |
@IntDef( | |
flag = true, | |
value = { | |
SAVE_PEEK_WIDTH, | |
SAVE_FIT_TO_CONTENTS, | |
SAVE_HIDEABLE, | |
SAVE_SKIP_COLLAPSED, | |
SAVE_ALL, | |
SAVE_NONE, | |
}) | |
@Retention(RetentionPolicy.SOURCE) | |
public @interface SaveFlags { | |
} | |
/** | |
* Callback for monitoring events about right sheets. | |
*/ | |
public abstract static class RightSheetCallback { | |
/** | |
* Called when the right sheet changes its state. | |
* | |
* @param rightSheet The right sheet view. | |
* @param newState The new state. This will be one of {@link #STATE_DRAGGING}, {@link | |
* #STATE_SETTLING}, {@link #STATE_EXPANDED}, {@link #STATE_COLLAPSED}, {@link | |
* #STATE_HIDDEN}, or {@link #STATE_HALF_EXPANDED}. | |
*/ | |
public abstract void onStateChanged(@NonNull View rightSheet, @State int newState); | |
/** | |
* Called when the right sheet is being dragged. | |
* | |
* @param rightSheet The right sheet view. | |
* @param slideOffset The new offset of this right sheet within [-1,1] range. Offset increases | |
* as this right sheet is moving left. From 0 to 1 the sheet is between collapsed and | |
* expanded states and from -1 to 0 it is between hidden and collapsed states. | |
*/ | |
public abstract void onSlide(@NonNull View rightSheet, float slideOffset); | |
} | |
/** | |
* State persisted across instances | |
*/ | |
protected static class SavedState extends AbsSavedState { | |
public static final Creator<SavedState> CREATOR = | |
new ClassLoaderCreator<SavedState>() { | |
@NonNull | |
@Override | |
public SavedState createFromParcel(@NonNull Parcel in, ClassLoader loader) { | |
return new SavedState(in, loader); | |
} | |
@Nullable | |
@Override | |
public SavedState createFromParcel(@NonNull Parcel in) { | |
return new SavedState(in, null); | |
} | |
@NonNull | |
@Override | |
public SavedState[] newArray(int size) { | |
return new SavedState[size]; | |
} | |
}; | |
@State | |
final int state; | |
int peekWidth; | |
boolean fitToContents; | |
boolean hideable; | |
boolean skipCollapsed; | |
public SavedState(@NonNull Parcel source) { | |
this(source, null); | |
} | |
public SavedState(@NonNull Parcel source, ClassLoader loader) { | |
super(source, loader); | |
//noinspection ResourceType | |
state = source.readInt(); | |
peekWidth = source.readInt(); | |
fitToContents = source.readInt() == 1; | |
hideable = source.readInt() == 1; | |
skipCollapsed = source.readInt() == 1; | |
} | |
public SavedState(Parcelable superState, @NonNull RightSheetBehaviour<?> behavior) { | |
super(superState); | |
this.state = behavior.state; | |
this.peekWidth = behavior.peekWidth; | |
this.fitToContents = behavior.fitToContents; | |
this.hideable = behavior.hideable; | |
this.skipCollapsed = behavior.skipCollapsed; | |
} | |
/** | |
* This constructor does not respect flags: {@link RightSheetBehaviour#SAVE_PEEK_WIDTH}, {@link | |
* RightSheetBehaviour#SAVE_FIT_TO_CONTENTS}, {@link RightSheetBehaviour#SAVE_HIDEABLE}, {@link | |
* RightSheetBehaviour#SAVE_SKIP_COLLAPSED}. It is as if {@link RightSheetBehaviour#SAVE_NONE} | |
* were set. | |
* | |
* @deprecated Use {@link SavedState(Parcelable, RightSheetBehaviour )} instead. | |
*/ | |
@Deprecated | |
public SavedState(Parcelable superstate, int state) { | |
super(superstate); | |
this.state = state; | |
} | |
@Override | |
public void writeToParcel(@NonNull Parcel out, int flags) { | |
super.writeToParcel(out, flags); | |
out.writeInt(state); | |
out.writeInt(peekWidth); | |
out.writeInt(fitToContents ? 1 : 0); | |
out.writeInt(hideable ? 1 : 0); | |
out.writeInt(skipCollapsed ? 1 : 0); | |
} | |
} | |
private class SettleRunnable implements Runnable { | |
private final View view; | |
@State | |
int targetState; | |
private boolean isPosted; | |
SettleRunnable(View view, @State int targetState) { | |
this.view = view; | |
this.targetState = targetState; | |
} | |
@Override | |
public void run() { | |
if (viewDragHelper != null && viewDragHelper.continueSettling(true)) { | |
ViewCompat.postOnAnimation(view, this); | |
} else { | |
setStateInternal(targetState); | |
} | |
this.isPosted = false; | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private lateinit var rightSheetBehavior: RightSheetBehaviour<FrameLayout> | |
rightSheetBehavior = RightSheetBehaviour.from(binding.rightSheet) | |
rightSheetBehavior.addRightSheetCallback(object : RightSheetBehaviour.RightSheetCallback() { | |
override fun onSlide(rightSheet: View, slideOffset: Float) { } | |
override fun onStateChanged(rightSheet: View, newState: Int) { | |
if (rightSheetBehavior.state == RightSheetBehaviour.STATE_COLLAPSED) { | |
} else if (rightSheetBehavior.state == RightSheetBehaviour.STATE_EXPANDED) { | |
} | |
} | |
}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" | |
xmlns:app="http://schemas.android.com/apk/res-auto" | |
xmlns:tools="http://schemas.android.com/tools" | |
android:id="@+id/cl" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent"> | |
<FrameLayout | |
android:id="@+id/rightSheet" | |
style="?attr/bottomSheetStyle" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
app:layout_behavior="@string/right_sheet_behaviour"> | |
//strings.xml | |
<string name="right_sheet_behaviour" translatable="false">com.your.package.name.RightSheetBehaviour</string> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment