package com.flipboard.bottomsheet; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; import android.annotation.TargetApi; import android.content.Context; import android.graphics.Color; import android.graphics.Point; import android.graphics.Rect; import android.os.Build; import android.support.annotation.NonNull; import android.util.AttributeSet; import android.util.Property; import android.view.Gravity; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.WindowManager; import android.view.animation.DecelerateInterpolator; import android.widget.FrameLayout; import java.util.concurrent.CopyOnWriteArraySet; import flipboard.bottomsheet.R; public class BottomSheetLayout extends FrameLayout { private static final Property<BottomSheetLayout, Float> SHEET_TRANSLATION = new Property<BottomSheetLayout, Float>(Float.class, "sheetTranslation") { @Override public Float get(BottomSheetLayout object) { return object.sheetTranslation; } @Override public void set(BottomSheetLayout object, Float value) { object.setSheetTranslation(value); } }; private Runnable runAfterDismiss; /** * Utility class which registers if the animation has been canceled so that subclasses may respond differently in onAnimationEnd */ private static class CancelDetectionAnimationListener extends AnimatorListenerAdapter { protected boolean canceled; @Override public void onAnimationCancel(Animator animation) { canceled = true; } } private static class IdentityViewTransformer extends BaseViewTransformer { @Override public void transformView(float translation, float maxTranslation, float peekedTranslation, BottomSheetLayout parent, View view) { // no-op } } public enum State { HIDDEN, PREPARING, PEEKED, EXPANDED } public interface OnSheetStateChangeListener { void onSheetStateChanged(State state); } private static final long ANIMATION_DURATION = 300; private Rect contentClipRect = new Rect(); private State state = State.HIDDEN; private boolean peekOnDismiss = false; private TimeInterpolator animationInterpolator = new DecelerateInterpolator(1.6f); public boolean bottomSheetOwnsTouch; private boolean sheetViewOwnsTouch; private float sheetTranslation; private VelocityTracker velocityTracker; private float minFlingVelocity; private float touchSlop; private ViewTransformer defaultViewTransformer = new IdentityViewTransformer(); private ViewTransformer viewTransformer; private boolean shouldDimContentView = true; private boolean useHardwareLayerWhileAnimating = true; private Animator currentAnimator; private CopyOnWriteArraySet<OnSheetDismissedListener> onSheetDismissedListeners = new CopyOnWriteArraySet<>(); private CopyOnWriteArraySet<OnSheetStateChangeListener> onSheetStateChangeListeners = new CopyOnWriteArraySet<>(); private OnLayoutChangeListener sheetViewOnLayoutChangeListener; private View dimView; private boolean interceptContentTouch = true; private int currentSheetViewHeight; private boolean hasIntercepted; private float peekKeyline; private float peek; /** Some values we need to manage width on tablets */ private int screenWidth = 0; private final boolean isTablet = getResources().getBoolean(R.bool.bottomsheet_is_tablet); private final int defaultSheetWidth = getResources().getDimensionPixelSize(R.dimen.bottomsheet_default_sheet_width); private int sheetStartX = 0; private int sheetEndX = 0; /** Snapshot of the touch's y position on a down event */ private float downY; /** Snapshot of the touch's x position on a down event */ private float downX; /** Snapshot of the sheet's translation at the time of the last down event */ private float downSheetTranslation; /** Snapshot of the sheet's state at the time of the last down event */ private State downState; public BottomSheetLayout(Context context) { super(context); init(); } public BottomSheetLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public BottomSheetLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public BottomSheetLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(); } private void init() { ViewConfiguration viewConfiguration = ViewConfiguration.get(getContext()); minFlingVelocity = viewConfiguration.getScaledMinimumFlingVelocity(); touchSlop = viewConfiguration.getScaledTouchSlop(); dimView = new View(getContext()); dimView.setBackgroundColor(Color.BLACK); dimView.setAlpha(0); dimView.setVisibility(INVISIBLE); setFocusableInTouchMode(true); Point point = new Point(); ((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getSize(point); screenWidth = point.x; sheetEndX = screenWidth; peek = 0; //getHeight() return 0 at start! peekKeyline = point.y - (screenWidth / (16.0f / 9.0f)); } /** * Don't call addView directly, use setContentView() and showWithSheetView() */ @Override public void addView(@NonNull View child) { if (getChildCount() > 0) { throw new IllegalArgumentException("You may not declare more then one child of bottom sheet. The sheet view must be added dynamically with showWithSheetView()"); } setContentView(child); } @Override public void addView(@NonNull View child, int index) { addView(child); } @Override public void addView(@NonNull View child, int index, @NonNull ViewGroup.LayoutParams params) { addView(child); } @Override public void addView(@NonNull View child, @NonNull ViewGroup.LayoutParams params) { addView(child); } @Override public void addView(@NonNull View child, int width, int height) { addView(child); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); velocityTracker = VelocityTracker.obtain(); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); velocityTracker.clear(); cancelCurrentAnimation(); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); int bottomClip = (int) (getHeight() - Math.ceil(sheetTranslation)); this.contentClipRect.set(0, 0, getWidth(), bottomClip); } @Override public boolean onKeyPreIme(int keyCode, @NonNull KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK && isSheetShowing()) { if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { KeyEvent.DispatcherState state = getKeyDispatcherState(); if (state != null) { state.startTracking(event, this); } return true; } else if (event.getAction() == KeyEvent.ACTION_UP) { KeyEvent.DispatcherState dispatcherState = getKeyDispatcherState(); if (dispatcherState != null) { dispatcherState.handleUpEvent(event); } if (isSheetShowing() && event.isTracking() && !event.isCanceled()) { if (state == State.EXPANDED && peekOnDismiss) { peekSheet(); } else { dismissSheet(); } return true; } } } return super.onKeyPreIme(keyCode, event); } private void setSheetTranslation(float newTranslation) { this.sheetTranslation = Math.min(newTranslation, getMaxSheetTranslation()); int bottomClip = (int) (getHeight() - Math.ceil(sheetTranslation)); this.contentClipRect.set(0, 0, getWidth(), bottomClip); getSheetView().setTranslationY(getHeight() - sheetTranslation); transformView(sheetTranslation); if (shouldDimContentView) { float dimAlpha = getDimAlpha(sheetTranslation); dimView.setAlpha(dimAlpha); dimView.setVisibility(dimAlpha > 0 ? VISIBLE : INVISIBLE); } } private void transformView(float sheetTranslation) { if (viewTransformer != null) { viewTransformer.transformView(sheetTranslation, getMaxSheetTranslation(), getPeekSheetTranslation(), this, getContentView()); } else if (defaultViewTransformer != null) { defaultViewTransformer.transformView(sheetTranslation, getMaxSheetTranslation(), getPeekSheetTranslation(), this, getContentView()); } } private float getDimAlpha(float sheetTranslation) { if (viewTransformer != null) { return viewTransformer.getDimAlpha(sheetTranslation, getMaxSheetTranslation(), getPeekSheetTranslation(), this, getContentView()); } else if (defaultViewTransformer != null) { return defaultViewTransformer.getDimAlpha(sheetTranslation, getMaxSheetTranslation(), getPeekSheetTranslation(), this, getContentView()); } return 0; } public boolean onInterceptTouchEvent(@NonNull MotionEvent ev) { boolean downAction = ev.getActionMasked() == MotionEvent.ACTION_DOWN; if (downAction) { hasIntercepted = false; } if (interceptContentTouch || (ev.getY() > getHeight() - sheetTranslation && isXInSheet(ev.getX()))) { hasIntercepted = downAction && isSheetShowing(); } else { hasIntercepted = false; } return hasIntercepted; } @Override public boolean onTouchEvent(@NonNull MotionEvent event) { if (!isSheetShowing()) { return false; } if (isAnimating()) { return false; } if (!hasIntercepted) { return onInterceptTouchEvent(event); } if (event.getAction() == MotionEvent.ACTION_DOWN) { // Snapshot the state of things when finger touches the screen. // This allows us to calculate deltas without losing precision which we would have if we calculated deltas based on the previous touch. bottomSheetOwnsTouch = false; sheetViewOwnsTouch = false; downY = event.getY(); downX = event.getX(); downSheetTranslation = sheetTranslation; downState = state; velocityTracker.clear(); } velocityTracker.addMovement(event); // The max translation is a hard limit while the min translation is where we start dragging more slowly and allow the sheet to be dismissed. float maxSheetTranslation = getMaxSheetTranslation(); float peekSheetTranslation = getPeekSheetTranslation(); float deltaY = downY - event.getY(); float deltaX = downX - event.getX(); if (!bottomSheetOwnsTouch && !sheetViewOwnsTouch) { bottomSheetOwnsTouch = Math.abs(deltaY) > touchSlop; sheetViewOwnsTouch = Math.abs(deltaX) > touchSlop; if (bottomSheetOwnsTouch) { if (state == State.PEEKED) { MotionEvent cancelEvent = MotionEvent.obtain(event); cancelEvent.offsetLocation(0, sheetTranslation - getHeight()); cancelEvent.setAction(MotionEvent.ACTION_CANCEL); getSheetView().dispatchTouchEvent(cancelEvent); cancelEvent.recycle(); } sheetViewOwnsTouch = false; downY = event.getY(); downX = event.getX(); deltaY = 0; deltaX = 0; } } // This is not the actual new sheet translation but a first approximation it will be adjusted to account for max and min translations etc. float newSheetTranslation = downSheetTranslation + deltaY; if (bottomSheetOwnsTouch) { // If we are scrolling down and the sheet cannot scroll further, go out of expanded mode. boolean scrollingDown = deltaY < 0; boolean canScrollUp = canScrollUp(getSheetView(), event.getX(), event.getY() + (sheetTranslation - getHeight())); if (state == State.EXPANDED && scrollingDown && !canScrollUp) { // Reset variables so deltas are correctly calculated from the point at which the sheet was 'detached' from the top. downY = event.getY(); downSheetTranslation = sheetTranslation; velocityTracker.clear(); setState(State.PEEKED); setSheetLayerTypeIfEnabled(LAYER_TYPE_HARDWARE); newSheetTranslation = sheetTranslation; // Dispatch a cancel event to the sheet to make sure its touch handling is cleaned up nicely. MotionEvent cancelEvent = MotionEvent.obtain(event); cancelEvent.setAction(MotionEvent.ACTION_CANCEL); getSheetView().dispatchTouchEvent(cancelEvent); cancelEvent.recycle(); } // If we are at the top of the view we should go into expanded mode. if (state == State.PEEKED && newSheetTranslation > maxSheetTranslation) { setSheetTranslation(maxSheetTranslation); // Dispatch a down event to the sheet to make sure its touch handling is initiated correctly. newSheetTranslation = Math.min(maxSheetTranslation, newSheetTranslation); MotionEvent downEvent = MotionEvent.obtain(event); downEvent.setAction(MotionEvent.ACTION_DOWN); getSheetView().dispatchTouchEvent(downEvent); downEvent.recycle(); setState(State.EXPANDED); setSheetLayerTypeIfEnabled(LAYER_TYPE_NONE); } if (state == State.EXPANDED) { // Dispatch the touch to the sheet if we are expanded so it can handle its own internal scrolling. event.offsetLocation(0, sheetTranslation - getHeight()); getSheetView().dispatchTouchEvent(event); } else { // Make delta less effective when sheet is below the minimum translation. // This makes it feel like scrolling in jello which gives the user an indication that the sheet will be dismissed if they let go. if (newSheetTranslation < peekSheetTranslation) { newSheetTranslation = peekSheetTranslation - (peekSheetTranslation - newSheetTranslation) / 4f; } setSheetTranslation(newSheetTranslation); if (event.getAction() == MotionEvent.ACTION_CANCEL) { // If touch is canceled, go back to previous state, a canceled touch should never commit an action. if (downState == State.EXPANDED) { expandSheet(); } else { peekSheet(); } } if (event.getAction() == MotionEvent.ACTION_UP) { if (newSheetTranslation < peekSheetTranslation) { dismissSheet(); } else { // If touch is released, go to a new state depending on velocity. // If the velocity is not high enough we use the position of the sheet to determine the new state. velocityTracker.computeCurrentVelocity(1000); float velocityY = velocityTracker.getYVelocity(); if (Math.abs(velocityY) < minFlingVelocity) { if (sheetTranslation > getHeight() / 2) { expandSheet(); } else { peekSheet(); } } else { if (velocityY < 0) { expandSheet(); } else { peekSheet(); } } } } } } else { // If the user clicks outside of the bottom sheet area we should dismiss the bottom sheet. boolean touchOutsideBottomSheet = event.getY() < getHeight() - sheetTranslation || !isXInSheet(event.getX()); if (event.getAction() == MotionEvent.ACTION_UP && touchOutsideBottomSheet && interceptContentTouch) { dismissSheet(); return true; } event.offsetLocation(isTablet ? getX() - sheetStartX : 0, sheetTranslation - getHeight()); getSheetView().dispatchTouchEvent(event); } return true; } private boolean isXInSheet(float x) { return !isTablet || x >= sheetStartX && x <= sheetEndX; } private boolean isAnimating() { return currentAnimator != null; } private void cancelCurrentAnimation() { if (currentAnimator != null) { currentAnimator.cancel(); } } private boolean canScrollUp(View view, float x, float y) { if (view instanceof ViewGroup) { ViewGroup vg = (ViewGroup) view; for (int i = 0; i < vg.getChildCount(); i++) { View child = vg.getChildAt(i); int childLeft = child.getLeft() - view.getScrollX(); int childTop = child.getTop() - view.getScrollY(); int childRight = child.getRight() - view.getScrollX(); int childBottom = child.getBottom() - view.getScrollY(); boolean intersects = x > childLeft && x < childRight && y > childTop && y < childBottom; if (intersects && canScrollUp(child, x - childLeft, y - childTop)) { return true; } } } return view.canScrollVertically(-1); } private void setSheetLayerTypeIfEnabled(int layerType) { if (useHardwareLayerWhileAnimating) { getSheetView().setLayerType(layerType, null); } } private void setState(State state) { if (state != this.state) { this.state = state; for (OnSheetStateChangeListener onSheetStateChangeListener : onSheetStateChangeListeners) { onSheetStateChangeListener.onSheetStateChanged(state); } } } private boolean hasTallerKeylineHeightSheet() { return getSheetView() == null || getSheetView().getHeight() > peekKeyline; } private boolean hasFullHeightSheet() { return getSheetView() == null || getSheetView().getHeight() == getHeight(); } /** * Set dim and translation to the initial state * */ private void initializeSheetValues() { this.sheetTranslation = 0; this.contentClipRect.set(0, 0, getWidth(), getHeight()); getSheetView().setTranslationY(getHeight()); dimView.setAlpha(0); dimView.setVisibility(INVISIBLE); } /** * Set the presented sheet to be in an expanded state. */ public void expandSheet() { cancelCurrentAnimation(); setSheetLayerTypeIfEnabled(LAYER_TYPE_NONE); ObjectAnimator anim = ObjectAnimator.ofFloat(this, SHEET_TRANSLATION, getMaxSheetTranslation()); anim.setDuration(ANIMATION_DURATION); anim.setInterpolator(animationInterpolator); anim.addListener(new CancelDetectionAnimationListener() { @Override public void onAnimationEnd(@NonNull Animator animation) { if (!canceled) { currentAnimator = null; } } }); anim.start(); currentAnimator = anim; setState(State.EXPANDED); } /** * Set the presented sheet to be in a peeked state. */ public void peekSheet() { cancelCurrentAnimation(); setSheetLayerTypeIfEnabled(LAYER_TYPE_HARDWARE); ObjectAnimator anim = ObjectAnimator.ofFloat(this, SHEET_TRANSLATION, getPeekSheetTranslation()); anim.setDuration(ANIMATION_DURATION); anim.setInterpolator(animationInterpolator); anim.addListener(new CancelDetectionAnimationListener() { @Override public void onAnimationEnd(@NonNull Animator animation) { if (!canceled) { currentAnimator = null; } } }); anim.start(); currentAnimator = anim; setState(State.PEEKED); } /** * @return The peeked state translation for the presented sheet view. Translation is counted from the bottom of the view. */ public float getPeekSheetTranslation() { return peek == 0 ? getDefaultPeekTranslation() : peek; } private float getDefaultPeekTranslation() { return hasTallerKeylineHeightSheet() ? peekKeyline : getSheetView().getHeight(); } /** * Set custom height for PEEKED state. * * @param peek Peek height in pixels */ public void setPeekSheetTranslation(float peek) { this.peek = peek; } /** * @return The maximum translation for the presented sheet view. Translation is counted from the bottom of the view. */ public float getMaxSheetTranslation() { return hasFullHeightSheet() ? getHeight() - getPaddingTop() : getSheetView().getHeight(); } /** * @return The currently presented sheet view. If no sheet is currently presented null will returned. */ public View getContentView() { return getChildCount() > 0 ? getChildAt(0) : null; } /** * @return The currently presented sheet view. If no sheet is currently presented null will returned. */ public View getSheetView() { return getChildCount() > 2 ? getChildAt(2) : null; } /** * Set the content view of the bottom sheet. This is the view which is shown under the sheet * being presented. This is usually the root view of your application. * * @param contentView The content view of your application. */ public void setContentView(View contentView) { super.addView(contentView, -1, generateDefaultLayoutParams()); super.addView(dimView, -1, generateDefaultLayoutParams()); } /** * Convenience for showWithSheetView(sheetView, null, null). * * @param sheetView The sheet to be presented. */ public void showWithSheetView(View sheetView) { showWithSheetView(sheetView, null); } /** * Present a sheet view to the user. * If another sheet is currently presented, it will be dismissed, and the new sheet will be shown after that * * @param sheetView The sheet to be presented. * @param viewTransformer The view transformer to use when presenting the sheet. */ public void showWithSheetView(final View sheetView, final ViewTransformer viewTransformer) { if (state != State.HIDDEN) { Runnable runAfterDismissThis = new Runnable() { @Override public void run() { showWithSheetView(sheetView, viewTransformer); } }; dismissSheet(runAfterDismissThis); return; } setState(State.PREPARING); LayoutParams params = (LayoutParams) sheetView.getLayoutParams(); if (params == null) { params = new LayoutParams(isTablet ? LayoutParams.WRAP_CONTENT : LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL); } if (isTablet && params.width == FrameLayout.LayoutParams.WRAP_CONTENT) { // Center by default if they didn't specify anything if (params.gravity == -1) { params.gravity = Gravity.CENTER_HORIZONTAL; } params.width = defaultSheetWidth; // Update start and end coordinates for touch reference int horizontalSpacing = screenWidth - defaultSheetWidth; sheetStartX = horizontalSpacing / 2; sheetEndX = screenWidth - sheetStartX; } super.addView(sheetView, -1, params); initializeSheetValues(); this.viewTransformer = viewTransformer; // Don't start animating until the sheet has been drawn once. This ensures that we don't do layout while animating and that // the drawing cache for the view has been warmed up. tl;dr it reduces lag. getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { getViewTreeObserver().removeOnPreDrawListener(this); post(new Runnable() { @Override public void run() { // Make sure sheet view is still here when first draw happens. // In the case of a large lag it could be that the view is dismissed before it is drawn resulting in sheet view being null here. if (getSheetView() != null) { peekSheet(); } } }); return true; } }); // sheetView should always be anchored to the bottom of the screen currentSheetViewHeight = sheetView.getMeasuredHeight(); sheetViewOnLayoutChangeListener = new OnLayoutChangeListener() { @Override public void onLayoutChange(View sheetView, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { int newSheetViewHeight = sheetView.getMeasuredHeight(); if (state != State.HIDDEN) { // The sheet can no longer be in the expanded state if it has shrunk if (newSheetViewHeight < currentSheetViewHeight) { if (state == State.EXPANDED) { setState(State.PEEKED); } setSheetTranslation(newSheetViewHeight); } else if (currentSheetViewHeight > 0 && newSheetViewHeight > currentSheetViewHeight && state == State.PEEKED) { if (newSheetViewHeight == getMaxSheetTranslation()) { setState(State.EXPANDED); } setSheetTranslation(newSheetViewHeight); } } currentSheetViewHeight = newSheetViewHeight; } }; sheetView.addOnLayoutChangeListener(sheetViewOnLayoutChangeListener); } /** * Dismiss the sheet currently being presented. */ public void dismissSheet() { dismissSheet(null); } private void dismissSheet(Runnable runAfterDismissThis) { if (state == State.HIDDEN) { runAfterDismiss = null; return; } // This must be set every time, including if the parameter is null // Otherwise a new sheet might be shown when the caller called dismiss after a showWithSheet call, which would be runAfterDismiss = runAfterDismissThis; final View sheetView = getSheetView(); sheetView.removeOnLayoutChangeListener(sheetViewOnLayoutChangeListener); cancelCurrentAnimation(); ObjectAnimator anim = ObjectAnimator.ofFloat(this, SHEET_TRANSLATION, 0); anim.setDuration(ANIMATION_DURATION); anim.setInterpolator(animationInterpolator); anim.addListener(new CancelDetectionAnimationListener() { @Override public void onAnimationEnd(Animator animation) { if (!canceled) { currentAnimator = null; setState(State.HIDDEN); setSheetLayerTypeIfEnabled(LAYER_TYPE_NONE); removeView(sheetView); for (OnSheetDismissedListener onSheetDismissedListener : onSheetDismissedListeners) { onSheetDismissedListener.onDismissed(BottomSheetLayout.this); } // Remove sheet specific properties viewTransformer = null; if (runAfterDismiss != null) { runAfterDismiss.run(); runAfterDismiss = null; } } } }); anim.start(); currentAnimator = anim; sheetStartX = 0; sheetEndX = screenWidth; } /** * Controls the behavior on back button press when the state is {@link State#EXPANDED}. * * @param peekOnDismiss true to show the peeked state on back press or false to completely hide * the Bottom Sheet. Default is false. */ public void setPeekOnDismiss(boolean peekOnDismiss) { this.peekOnDismiss = peekOnDismiss; } /** * Returns the current peekOnDismiss value, which controls the behavior response to back presses * when the current state is {@link State#EXPANDED}. * * @return the current peekOnDismiss value */ public boolean getPeekOnDismiss() { return peekOnDismiss; } /** * Controls whether or not child view interaction is possible when the bottomsheet is open. * * @param interceptContentTouch true to intercept content view touches or false to allow * interaction with Bottom Sheet's content view */ public void setInterceptContentTouch(boolean interceptContentTouch) { this.interceptContentTouch = interceptContentTouch; } /** * @return true if we are intercepting content view touches or false to allow interaction with * Bottom Sheet's content view. Default value is true. */ public boolean getInterceptContentTouch() { return interceptContentTouch; } /** * @return The current state of the sheet. */ public State getState() { return state; } /** * @return Whether or not a sheet is currently presented. */ public boolean isSheetShowing() { return state != State.HIDDEN; } /** * Set the default view transformer to use for showing a sheet. Usually applications will use * a similar transformer for most use cases of bottom sheet so this is a convenience instead of * passing a new transformer each time a sheet is shown. This choice is overridden by any * view transformer passed to showWithSheetView(). * * @param defaultViewTransformer The view transformer user by default. */ public void setDefaultViewTransformer(ViewTransformer defaultViewTransformer) { this.defaultViewTransformer = defaultViewTransformer; } /** * Enable or disable dimming of the content view while a sheet is presented. If enabled a * transparent black dim is overlaid on top of the content view indicating that the sheet is the * foreground view. This dim is animated into place is coordination with the sheet view. * Defaults to true. * * @param shouldDimContentView whether or not to dim the content view. */ public void setShouldDimContentView(boolean shouldDimContentView) { this.shouldDimContentView = shouldDimContentView; } /** * @return whether the content view is being dimmed while presenting a sheet or not. */ public boolean shouldDimContentView() { return shouldDimContentView; } /** * Enable or disable the use of a hardware layer for the presented sheet while animating. * This settings defaults to true and should only be changed if you know that putting the * sheet in a layer will negatively effect performance. One such example is if the sheet contains * a view which needs to frequently be re-drawn. * * @param useHardwareLayerWhileAnimating whether or not to use a hardware layer. */ public void setUseHardwareLayerWhileAnimating(boolean useHardwareLayerWhileAnimating) { this.useHardwareLayerWhileAnimating = useHardwareLayerWhileAnimating; } /** * Adds an {@link OnSheetStateChangeListener} which will be notified when the state of the presented sheet changes. * The listener will not be automatically removed, so remember to remove it when it's no longer needed * (probably when the sheet is HIDDEN) * * @param onSheetStateChangeListener the listener to be notified. */ public void addOnSheetStateChangeListener(@NonNull OnSheetStateChangeListener onSheetStateChangeListener) { checkNotNull(onSheetStateChangeListener, "onSheetStateChangeListener == null"); this.onSheetStateChangeListeners.add(onSheetStateChangeListener); } /** * Adds an {@link OnSheetDismissedListener} which will be notified when the state of the presented sheet changes. * The listener will not be automatically removed, so remember to remove it when it's no longer needed * (probably when the sheet is HIDDEN) * * @param onSheetDismissedListener the listener to be notified. */ public void addOnSheetDismissedListener(@NonNull OnSheetDismissedListener onSheetDismissedListener) { checkNotNull(onSheetDismissedListener, "onSheetDismissedListener == null"); this.onSheetDismissedListeners.add(onSheetDismissedListener); } /** * Removes a previously added {@link OnSheetStateChangeListener}. * * @param onSheetStateChangeListener the listener to be removed. */ public void removeOnSheetStateChangeListener(@NonNull OnSheetStateChangeListener onSheetStateChangeListener) { checkNotNull(onSheetStateChangeListener, "onSheetStateChangeListener == null"); this.onSheetStateChangeListeners.remove(onSheetStateChangeListener); } /** * Removes a previously added {@link OnSheetDismissedListener}. * * @param onSheetDismissedListener the listener to be removed. */ public void removeOnSheetDismissedListener(@NonNull OnSheetDismissedListener onSheetDismissedListener) { checkNotNull(onSheetDismissedListener, "onSheetDismissedListener == null"); this.onSheetDismissedListeners.remove(onSheetDismissedListener); } /** * Returns whether or not BottomSheetLayout will assume it's being shown on a tablet. * * @param context Context instance to retrieve resources * @return True if BottomSheetLayout will assume it's being shown on a tablet, false if not */ public static boolean isTablet(Context context) { return context.getResources().getBoolean(R.bool.bottomsheet_is_tablet); } /** * Returns the predicted default width of the sheet if it were shown. * * @param context Context instance to retrieve resources and display metrics * @return Predicted width of the sheet if shown */ public static int predictedDefaultWidth(Context context) { if (isTablet(context)) { return context.getResources().getDimensionPixelSize(R.dimen.bottomsheet_default_sheet_width); } else { return context.getResources().getDisplayMetrics().widthPixels; } } private static <T> T checkNotNull(T value, String message) { if (value == null) { throw new NullPointerException(message); } return value; } }