最新示例见:https://github.com/ZYF99/BottomSheetTest.git
示例新增解决方法+弹窗回弹+回弹速度调节,本文暂未对示例内容进行更新,敬请期待!
最终效果
使用BottomSheetBehavior引发的问题
问题1:BottomSheetBehavior+ViewPager+多页RecyclerView组合,只有第一页列表可滑动
在CoordinatorLayout
中对弹出的ViewGroup
直接使用 com.google.android.material.bottomsheet.BottomSheetBehavior
,本身是没有问题的,但当我们嵌套了ViewPager
+多页RecyclerView
这个组合,就会导致只有第一页RecyclerView
能滑动,其余页的滑动事件全部被behavior
处理掉了,也就是滑动全部成为了弹出隐藏你得弹出框,很显然,列表是需要先于弹出框消费滑动的,于是我们看看BottomSheetBehavior
的源码,如下:
@Nullable
@VisibleForTesting
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;
}
关键的一段代码,在寻找滑动
View
的时候,对ViewGroup
的子View
进行了遍历,再递归的寻找子View
下可滑动的控件,当找到第一个可滑动控件时,将其返回,作为消费滑动事件的控件。
这里就明白了只有第一个RecyclerView
能滑动的原因:无论findScrollingChild
什么时机被触发,永远都只会返回ViewPager
的第一页中的RecyclerView
。知道了原因,修改就很简单了:将ViewPager
拿出来,单独进行一波单独遍历。那么我们新建一个MyViewPagerBottomSheetBehavior.java
,将BottomSheetBehavior
代码拷贝,改造findScrollingChild
后如下:
@Nullable
@VisibleForTesting
View findScrollingChild(View view) {
if (ViewCompat.isNestedScrollingEnabled(view)) {
return view;
}
if (view instanceof ViewPager) {
ViewPager viewPager = (ViewPager) view;
View currentViewPagerChild = ViewPagerUtils.getCurrentView(viewPager); //通过ViewPagerUtils找到当前在界面上的页面
View scrollingChild = findScrollingChild(currentViewPagerChild);
if (scrollingChild != null) {
return scrollingChild;
}
return currentViewPagerChild;
}
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;
}
第一个问题解决了~
问题2:滑动事件全部被 RecyclerView
消费掉,滑动弹出和关闭功能消失了
导致这个问题的原因也很简单,我们的滑动事件在 RecyclerView
加入前,都是由 BottomSheetBehavior
来消费的,当我们加入 RecyclerView
这种可滑动控件后,滑动事件都被其消费,这与 ViewPager
无关。
多数情况下我们需要
RecyclerView
消费事件(滑动),但我们同时希望当RecyclerView
滑动到顶部时,将事件又重新交给Behavior
消费,这样就可以做到,列表在顶部时滑动开启/关闭弹出框
实现需要分为以下几步
- 为
Behavior
添加Flag
,标记列表是否滑动到最顶端
由于需要适配ViewPager多页情况,一个Flag不能解决,需要一个HashMap,将页数与Flag关联起来
//针对viewpager联动
boolean isFirstFind = true; //第一次寻找联动View,为viewPager添加滑动监听
private HashMap isScrollViewOnTopMap = new HashMap<>(); //关联页数与能否滑动flag的Map
private View globalView;//保存下来的view,方便刷新寻找滑动控件时使用
private int currentPagePosition = 0; //当前ViewPager的页数
@Nullable
@VisibleForTesting
View findScrollingChild(View view) {
..........................
}
- 定义一个刷新滑动控件的方法,仅供
MyViewPagerBehavior
内部使用
private void notifyScrollView() {
nestedScrollingChildRef = new WeakReference<>(findScrollingChild(globalView));
}
- 从
MyViewPagerBehavior
内对外暴露一个设置Flag
的方法
public void setCurrentScrollViewOnTop(boolean scrollViewOnTop) {
isScrollViewOnTopMap.put(currentPagePosition, scrollViewOnTop);
Log.d("~~~~~~~~~",isScrollViewOnTopMap.toString());
notifyScrollView();
}
- 更改
findScrollingChild(View view)
方法,适配列表在顶部时,滑动由behavior
处理
@Nullable
@VisibleForTesting
View findScrollingChild(View view) {
if(view==null){
return null;
}
globalView = view; //为全局view赋值
boolean b = isScrollViewOnTopMap.getOrDefault(currentPagePosition,false );//列表是否处于顶部
if (b) {//处于顶部,返回null,代表内部滑动控件不消费任何滑动事件,交由behavior处理
return null;
}
if (ViewCompat.isNestedScrollingEnabled(view)) {
return view;
}
if (view instanceof ViewPager) {
ViewPager viewPager = (ViewPager) view;
if (isFirstFind) {//初次寻找滑动view,添加viewpager翻页监听
for (int i = 0; i < Objects.requireNonNull(viewPager.getAdapter()).getCount(); i++) {
isScrollViewOnTopMap.put(i,true); //将所有顶部标记置为false,默认不在顶部
}
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
//翻页后,将现在的页数更新
currentPagePosition = position;
}
@Override
public void onPageSelected(int position) {
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
isFirstFind = false;
}
View currentViewPagerChild = ViewPagerUtils.getCurrentView(viewPager); //找到ViewPager当前的view
View scrollingChild = findScrollingChild(currentViewPagerChild);
if (scrollingChild != null) {
return scrollingChild;
}
return currentViewPagerChild;
}
- 监听
RecyclerView
滑动,在对顶部时,通知Behavior
这是在Fragment或者Activity中,你得behavior一定要自己去取,可能就在你得Fragment/Activity
中通过MyViewPagerBehavior.from()
就能获取,也可能你要从其他Activity
和Fragment
注入进来,具体只有你自己了解;不要忘了:- behavior变量类型一定要写刚才创建的
MyViewPagerBottomSheetBehavior
- xml中更换behavior为
MyViewPagerBottomSheetBehavior
- behavior变量类型一定要写刚才创建的
//上拉加载
binding.rvList.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
if (behavior != null) {
Log.d("!!!!!!!!!", String.valueOf(!recyclerView.canScrollVertically(-1)) + dy);
if (dy == 0) {
//指尖未真实上下滑动(针对从viewpager其他页面切换过来时),不做任何操作
return;
}
if (!recyclerView.canScrollVertically(-1) && dy < 0) {
//不可以下拉,并且手势是下拉,通知behavior已经列表已经在顶部了
Log.d("~~~~~", "划不动了");
behavior.setCurrentScrollViewOnTop(true);
} else {
//可以下拉或者手势不是下拉,通知behavior已经列表不在顶部
behavior.setCurrentScrollViewOnTop(false);
}
}
}
});
贴上完整的MyViewPagerBottomSheetBehavior
package com.xxx.wordingtech.ui.widget;
import com.google.android.material.R;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
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;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.FloatRange;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
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 android.util.AttributeSet;
import android.util.Log;
import android.util.Pair;
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 android.view.WindowInsets;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.coordinatorlayout.widget.CoordinatorLayout.LayoutParams;
import androidx.customview.view.AbsSavedState;
import androidx.customview.widget.ViewDragHelper;
import androidx.viewpager.widget.ViewPager;
import androidx.viewpager.widget.ViewPagerUtils;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
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;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
public class MyViewPagerBottomSheetBehavior extends CoordinatorLayout.Behavior {
/**
* Callback for monitoring events about bottom sheets.
*/
public abstract static class BottomSheetCallback {
/**
* Called when the bottom sheet changes its state.
*
* @param bottomSheet The bottom 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 bottomSheet, @State int newState);
/**
* Called when the bottom sheet is being dragged.
*
* @param bottomSheet The bottom sheet view.
* @param slideOffset The new 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.
*/
public abstract void onSlide(@NonNull View bottomSheet, float slideOffset);
}
/**
* The bottom sheet is dragging.
*/
public static final int STATE_DRAGGING = 1;
/**
* The bottom sheet is settling.
*/
public static final int STATE_SETTLING = 2;
/**
* The bottom sheet is expanded.
*/
public static final int STATE_EXPANDED = 3;
/**
* The bottom sheet is collapsed.
*/
public static final int STATE_COLLAPSED = 4;
/**
* The bottom sheet is hidden.
*/
public static final int STATE_HIDDEN = 5;
/**
* The bottom sheet is half-expanded (used when mFitToContents is false).
*/
public static final int STATE_HALF_EXPANDED = 6;
/**
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
@IntDef({
STATE_EXPANDED,
STATE_COLLAPSED,
STATE_DRAGGING,
STATE_SETTLING,
STATE_HIDDEN,
STATE_HALF_EXPANDED
})
@Retention(RetentionPolicy.SOURCE)
public @interface State {
}
/**
* Peek at the 16:9 ratio keyline of its parent.
*
* This can be used as a parameter for {@link #setPeekHeight(int)}. {@link #getPeekHeight()}
* will return this when the value is set.
*/
public static final int PEEK_HEIGHT_AUTO = -1;
/**
* This flag will preserve the peekHeight int value on configuration change.
*/
public static final int SAVE_PEEK_HEIGHT = 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;
/**
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
@IntDef(
flag = true,
value = {
SAVE_PEEK_HEIGHT,
SAVE_FIT_TO_CONTENTS,
SAVE_HIDEABLE,
SAVE_SKIP_COLLAPSED,
SAVE_ALL,
SAVE_NONE,
})
@Retention(RetentionPolicy.SOURCE)
public @interface SaveFlags {
}
private static final String TAG = "BottomSheetBehavior";
@SaveFlags
private int saveFlags = SAVE_NONE;
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 boolean fitToContents = true;
private boolean updateImportantForAccessibilityOnSiblings = false;
private float maximumVelocity;
/**
* Peek height set by the user.
*/
private int peekHeight;
/**
* Whether or not to use automatic peek height.
*/
private boolean peekHeightAuto;
/**
* Minimum peek height permitted.
*/
private int peekHeightMin;
/**
* True if Behavior has a non-null value for the @shapeAppearance attribute
*/
private boolean shapeThemingEnabled;
private MaterialShapeDrawable materialShapeDrawable;
private boolean gestureInsetBottomIgnored;
/**
* Default Shape Appearance to be used in bottomsheet
*/
private ShapeAppearanceModel shapeAppearanceModelDefault;
private boolean isShapeExpanded;
private SettleRunnable settleRunnable = null;
@Nullable
private ValueAnimator interpolatorAnimator;
private static final int DEF_STYLE_RES = R.style.Widget_Design_BottomSheet_Modal;
int expandedOffset;
int fitToContentsOffset;
int halfExpandedOffset;
float halfExpandedRatio = 0.5f;
int collapsedOffset;
float elevation = -1;
boolean hideable;
private boolean skipCollapsed;
private boolean draggable = true;
@State
int state = STATE_COLLAPSED;
@Nullable
ViewDragHelper viewDragHelper;
private boolean ignoreEvents;
private int lastNestedScrollDy;
private boolean nestedScrolled;
int parentWidth;
int parentHeight;
@Nullable
WeakReference viewRef;
@Nullable
WeakReference nestedScrollingChildRef;
@NonNull
private final ArrayList callbacks = new ArrayList<>();
@Nullable
private VelocityTracker velocityTracker;
int activePointerId;
private int initialY;
boolean touchingScrollingChild;
@Nullable
private Map importantForAccessibilityMap;
public MyViewPagerBottomSheetBehavior() {
}
public MyViewPagerBottomSheetBehavior(@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) {
@SuppressLint("RestrictedApi") ColorStateList bottomSheetColor =
MaterialResources.getColorStateList(
context, a, R.styleable.BottomSheetBehavior_Layout_backgroundTint);
createMaterialShapeDrawable(context, attrs, hasBackgroundTint, bottomSheetColor);
} 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_HEIGHT_AUTO) {
setPeekHeight(value.data);
} else {
setPeekHeight(
a.getDimensionPixelSize(
R.styleable.BottomSheetBehavior_Layout_behavior_peekHeight, PEEK_HEIGHT_AUTO));
}
setHideable(a.getBoolean(R.styleable.BottomSheetBehavior_Layout_behavior_hideable, false));
setGestureInsetBottomIgnored(
a.getBoolean(R.styleable.BottomSheetBehavior_Layout_gestureInsetBottomIgnored, 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();
}
@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.
peekHeightMin =
parent.getResources().getDimensionPixelSize(R.dimen.design_bottom_sheet_peek_height_min);
setSystemGestureInsets(parent);
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 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);
}
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 savedTop = child.getTop();
// First let the parent lay it out
parent.onLayoutChild(child, layoutDirection);
// Offset the bottom sheet
parentWidth = parent.getWidth();
parentHeight = parent.getHeight();
fitToContentsOffset = Math.max(0, parentHeight - child.getHeight());
calculateHalfExpandedOffset();
calculateCollapsedOffset();
if (state == STATE_EXPANDED) {
ViewCompat.offsetTopAndBottom(child, getExpandedOffset());
} else if (state == STATE_HALF_EXPANDED) {
ViewCompat.offsetTopAndBottom(child, halfExpandedOffset);
} else if (hideable && state == STATE_HIDDEN) {
ViewCompat.offsetTopAndBottom(child, parentHeight);
} else if (state == STATE_COLLAPSED) {
ViewCompat.offsetTopAndBottom(child, collapsedOffset);
} else if (state == STATE_DRAGGING || state == STATE_SETTLING) {
ViewCompat.offsetTopAndBottom(child, savedTop - child.getTop());
}
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:
int initialX = (int) event.getX();
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 bottom 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(initialY - event.getY()) > 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 bottom sheet in case it is not captured and the touch slop is passed.
if (action == MotionEvent.ACTION_MOVE && !ignoreEvents) {
if (Math.abs(initialY - event.getY()) > 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) {
lastNestedScrollDy = 0;
nestedScrolled = false;
return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 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 currentTop = child.getTop();
int newTop = currentTop - dy;
if (dy > 0) { // Upward
if (newTop < getExpandedOffset()) {
consumed[1] = currentTop - getExpandedOffset();
ViewCompat.offsetTopAndBottom(child, -consumed[1]);
setStateInternal(STATE_EXPANDED);
} else {
if (!draggable) {
// Prevent dragging
return;
}
consumed[1] = dy;
ViewCompat.offsetTopAndBottom(child, -dy);
setStateInternal(STATE_DRAGGING);
}
} else if (dy < 0) { // Downward
if (!target.canScrollVertically(-1)) {
if (newTop <= collapsedOffset || hideable) {
if (!draggable) {
// Prevent dragging
return;
}
consumed[1] = dy;
ViewCompat.offsetTopAndBottom(child, -dy);
setStateInternal(STATE_DRAGGING);
} else {
consumed[1] = currentTop - collapsedOffset;
ViewCompat.offsetTopAndBottom(child, -consumed[1]);
setStateInternal(STATE_COLLAPSED);
}
}
}
dispatchOnSlide(child.getTop());
lastNestedScrollDy = dy;
nestedScrolled = true;
}
@Override
public void onStopNestedScroll(
@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child,
@NonNull View target,
int type) {
if (child.getTop() == getExpandedOffset()) {
setStateInternal(STATE_EXPANDED);
return;
}
if (nestedScrollingChildRef == null
|| target != nestedScrollingChildRef.get()
|| !nestedScrolled) {
return;
}
int top;
int targetState;
if (lastNestedScrollDy > 0) {
if (fitToContents) {
top = fitToContentsOffset;
targetState = STATE_EXPANDED;
} else {
int currentTop = child.getTop();
if (currentTop > halfExpandedOffset) {
top = halfExpandedOffset;
targetState = STATE_HALF_EXPANDED;
} else {
top = expandedOffset;
targetState = STATE_EXPANDED;
}
}
} else if (hideable && shouldHide(child, getYVelocity())) {
top = parentHeight;
targetState = STATE_HIDDEN;
} else if (lastNestedScrollDy == 0) {
int currentTop = child.getTop();
if (fitToContents) {
if (Math.abs(currentTop - fitToContentsOffset) < Math.abs(currentTop - collapsedOffset)) {
top = fitToContentsOffset;
targetState = STATE_EXPANDED;
} else {
top = collapsedOffset;
targetState = STATE_COLLAPSED;
}
} else {
if (currentTop < halfExpandedOffset) {
if (currentTop < Math.abs(currentTop - collapsedOffset)) {
top = expandedOffset;
targetState = STATE_EXPANDED;
} else {
top = halfExpandedOffset;
targetState = STATE_HALF_EXPANDED;
}
} else {
if (Math.abs(currentTop - halfExpandedOffset) < Math.abs(currentTop - collapsedOffset)) {
top = halfExpandedOffset;
targetState = STATE_HALF_EXPANDED;
} else {
top = collapsedOffset;
targetState = STATE_COLLAPSED;
}
}
}
} else {
if (fitToContents) {
top = collapsedOffset;
targetState = STATE_COLLAPSED;
} else {
// Settle to nearest height.
int currentTop = child.getTop();
if (Math.abs(currentTop - halfExpandedOffset) < Math.abs(currentTop - collapsedOffset)) {
top = halfExpandedOffset;
targetState = STATE_HALF_EXPANDED;
} else {
top = collapsedOffset;
targetState = STATE_COLLAPSED;
}
}
}
startSettlingAnimation(child, targetState, top, 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 height of the expanded sheet is determined by the height of its contents,
* or if it is expanded in two stages (half the height of the parent container, full height of
* parent container).
*/
public boolean isFitToContents() {
return fitToContents;
}
/**
* Sets whether the height of the expanded sheet is determined by the height of its contents, or
* if it is expanded in two stages (half the height of the parent container, full height 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 height of the bottom sheet when it is collapsed.
*
* @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
*/
public void setPeekHeight(int peekHeight) {
setPeekHeight(peekHeight, false);
}
/**
* Sets the height of the bottom sheet when it is collapsed while optionally animating between the
* old height and the new height.
*
* @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.
* @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
*/
public final void setPeekHeight(int peekHeight, boolean animate) {
boolean layout = false;
if (peekHeight == PEEK_HEIGHT_AUTO) {
if (!peekHeightAuto) {
peekHeightAuto = true;
layout = true;
}
} else if (peekHeightAuto || this.peekHeight != peekHeight) {
peekHeightAuto = false;
this.peekHeight = Math.max(0, peekHeight);
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 height of the bottom sheet when it is collapsed.
*
* @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
*/
public int getPeekHeight() {
return peekHeightAuto ? PEEK_HEIGHT_AUTO : peekHeight;
}
/**
* Determines the height of the BottomSheet 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 height of the BottomSheet 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 ref
* com.google.android.material.R.styleable#BottomSheetBehavior_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();
}
}
/**
* 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
*/
@FloatRange(from = 0.0f, to = 1.0f)
public float getHalfExpandedRatio() {
return halfExpandedRatio;
}
/**
* Determines the top offset of the BottomSheet 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 ref
* com.google.android.material.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;
}
/**
* Returns the current expanded offset. If {@code fitToContents} is true, it will automatically
* pick the offset depending on the height of the content.
*
* @attr ref
* com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_expandedOffset
*/
public int getExpandedOffset() {
return fitToContents ? fitToContentsOffset : expandedOffset;
}
/**
* Sets whether this bottom sheet can hide when it is swiped down.
*
* @param hideable {@code true} to make this bottom sheet hideable.
* @attr ref com.google.android.material.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();
}
}
/**
* 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
*/
public boolean isHideable() {
return hideable;
}
/**
* Sets whether this bottom 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 bottom sheet should skip the collapsed state.
* @attr ref
* com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_skipCollapsed
*/
public void setSkipCollapsed(boolean skipCollapsed) {
this.skipCollapsed = skipCollapsed;
}
/**
* Sets whether this bottom sheet should skip the collapsed state when it is being hidden after it
* is expanded once.
*
* @return Whether the bottom sheet should skip the collapsed state.
* @attr ref
* com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_skipCollapsed
*/
public boolean getSkipCollapsed() {
return skipCollapsed;
}
/**
* Sets whether this bottom 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 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
*/
public void setDraggable(boolean draggable) {
this.draggable = draggable;
}
public boolean isDraggable() {
return draggable;
}
/**
* 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}.
* @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_saveFlags
* @see #getSaveFlags()
*/
public void setSaveFlags(@SaveFlags int flags) {
this.saveFlags = flags;
}
/**
* Returns the save flags.
*
* @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_saveFlags
* @see #setSaveFlags(int)
*/
@SaveFlags
public int getSaveFlags() {
return this.saveFlags;
}
/**
* Sets a callback to be notified of bottom sheet events.
*
* @param callback The callback to notify when bottom sheet events occur.
* @deprecated use {@link #addBottomSheetCallback(BottomSheetCallback)} and {@link
* #removeBottomSheetCallback(BottomSheetCallback)} instead
*/
@Deprecated
public void setBottomSheetCallback(BottomSheetCallback callback) {
Log.w(
TAG,
"BottomSheetBehavior 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"
+ " own callbacks.");
callbacks.clear();
if (callback != null) {
callbacks.add(callback);
}
}
/**
* Adds a callback to be notified of bottom sheet events.
*
* @param callback The callback to notify when bottom sheet events occur.
*/
public void addBottomSheetCallback(@NonNull BottomSheetCallback callback) {
if (!callbacks.contains(callback)) {
callbacks.add(callback);
}
}
/**
* Removes a previously added callback.
*
* @param callback The callback to remove.
*/
public void removeBottomSheetCallback(@NonNull BottomSheetCallback callback) {
callbacks.remove(callback);
}
/**
* Sets the state of the bottom sheet. The bottom 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);
}
/**
* Sets whether this bottom sheet should adjust it's position based on the system gesture area on
* Android Q and above.
*
* Note: the bottom sheet will only adjust it's position if it would be unable to be scrolled
* upwards because the peekHeight is less than the gesture inset margins,(because that would cause
* a gesture conflict), gesture navigation is enabled, and this {@code ignoreGestureInsetBottom}
* flag is false.
*/
public void setGestureInsetBottomIgnored(boolean gestureInsetBottomIgnored) {
this.gestureInsetBottomIgnored = gestureInsetBottomIgnored;
}
/**
* Returns whether this bottom sheet should adjust it's position based on the system gesture area.
*/
public boolean isGestureInsetBottomIgnored() {
return gestureInsetBottomIgnored;
}
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 bottom 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;
}
void setStateInternal(@State int state) {
if (this.state == state) {
return;
}
this.state = state;
if (viewRef == null) {
return;
}
View bottomSheet = viewRef.get();
if (bottomSheet == 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(bottomSheet, 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 calculatePeekHeight() {
if (peekHeightAuto) {
return Math.max(peekHeightMin, parentHeight - parentWidth * 9 / 16);
}
return peekHeight;
}
private void calculateCollapsedOffset() {
int peek = calculatePeekHeight();
if (fitToContents) {
collapsedOffset = Math.max(parentHeight - peek, fitToContentsOffset);
} else {
collapsedOffset = parentHeight - peek;
}
}
private void calculateHalfExpandedOffset() {
this.halfExpandedOffset = (int) (parentHeight * (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_HEIGHT) == SAVE_PEEK_HEIGHT) {
this.peekHeight = ss.peekHeight;
}
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;
}
}
boolean shouldHide(@NonNull View child, float yvel) {
if (skipCollapsed) {
return true;
}
if (child.getTop() < collapsedOffset) {
// It should not hide, but collapse.
return false;
}
int peek = calculatePeekHeight();
final float newTop = child.getTop() + yvel * HIDE_FRICTION;
return Math.abs(newTop - collapsedOffset) / (float) peek > HIDE_THRESHOLD;
}
//针对viewpager联动
boolean isFirstFind = true; //第一次寻找联动View,为viewPager添加滑动监听
private HashMap isScrollViewOnTopMap = new HashMap<>(); //关联页数与能否滑动flag的Map
private int currentPagePosition = 0; //当前ViewPager的页数
public void setCurrentScrollViewOnTop(boolean scrollViewOnTop) {
isScrollViewOnTopMap.put(currentPagePosition, scrollViewOnTop);
Log.d("~~~~~~~~~",isScrollViewOnTopMap.toString());
notifyScrollView();
}
private View globalView;
private void notifyScrollView() {
nestedScrollingChildRef = new WeakReference<>(findScrollingChild(globalView));
}
@Nullable
@VisibleForTesting
View findScrollingChild(View view) {
if(view==null){
return null;
}
globalView = view;
boolean b = isScrollViewOnTopMap.getOrDefault(currentPagePosition,false );
if (b) {
return null;
}
if (ViewCompat.isNestedScrollingEnabled(view)) {
return view;
}
if (view instanceof ViewPager) {
ViewPager viewPager = (ViewPager) view;
if (isFirstFind) {
for (int i = 0; i < Objects.requireNonNull(viewPager.getAdapter()).getCount(); i++) {
isScrollViewOnTopMap.put(i,true);
}
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
currentPagePosition = position;
}
@Override
public void onPageSelected(int position) {
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
isFirstFind = false;
}
View currentViewPagerChild = ViewPagerUtils.getCurrentView(viewPager);
View scrollingChild = findScrollingChild(currentViewPagerChild);
if (scrollingChild != null) {
return scrollingChild;
}
return currentViewPagerChild;
}
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 bottomSheetColor) {
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 && bottomSheetColor != null) {
materialShapeDrawable.setFillColor(bottomSheetColor);
} 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 void setSystemGestureInsets(@NonNull CoordinatorLayout parent) {
if (VERSION.SDK_INT >= VERSION_CODES.Q && !isGestureInsetBottomIgnored()) {
WindowInsets windowInsets = parent.getRootWindowInsets();
if (windowInsets != null) {
int systemMandatoryInsetsBottom = windowInsets.getSystemGestureInsets().bottom;
peekHeight += systemMandatoryInsetsBottom;
}
}
}
private float getYVelocity() {
if (velocityTracker == null) {
return 0;
}
velocityTracker.computeCurrentVelocity(1000, maximumVelocity);
return velocityTracker.getYVelocity(activePointerId);
}
void settleToState(@NonNull View child, int state) {
int top;
if (state == STATE_COLLAPSED) {
top = collapsedOffset;
} else if (state == STATE_HALF_EXPANDED) {
top = halfExpandedOffset;
if (fitToContents && top <= fitToContentsOffset) {
// Skip to the expanded state if we would scroll past the height of the contents.
state = STATE_EXPANDED;
top = fitToContentsOffset;
}
} else if (state == STATE_EXPANDED) {
top = getExpandedOffset();
} else if (hideable && state == STATE_HIDDEN) {
top = parentHeight;
} else {
throw new IllegalArgumentException("Illegal state argument: " + state);
}
startSettlingAnimation(child, state, top, false);
}
void startSettlingAnimation(View child, int state, int top, boolean settleFromViewDragHelper) {
boolean startedSettling =
settleFromViewDragHelper
? viewDragHelper.settleCapturedViewAt(child.getLeft(), top)
: viewDragHelper.smoothSlideViewTo(child, child.getLeft(), top);
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 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.canScrollVertically(-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(top);
}
@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 bottom.
return child.getTop() > (parentHeight + getExpandedOffset()) / 2;
}
@Override
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
int top;
@State int targetState;
if (yvel < 0) { // Moving up
if (fitToContents) {
top = fitToContentsOffset;
targetState = STATE_EXPANDED;
} else {
int currentTop = releasedChild.getTop();
if (currentTop > halfExpandedOffset) {
top = halfExpandedOffset;
targetState = STATE_HALF_EXPANDED;
} else {
top = expandedOffset;
targetState = STATE_EXPANDED;
}
}
} 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)
|| releasedLow(releasedChild)) {
top = parentHeight;
targetState = STATE_HIDDEN;
} else if (fitToContents) {
top = fitToContentsOffset;
targetState = STATE_EXPANDED;
} else if (Math.abs(releasedChild.getTop() - expandedOffset)
< Math.abs(releasedChild.getTop() - halfExpandedOffset)) {
top = expandedOffset;
targetState = STATE_EXPANDED;
} else {
top = halfExpandedOffset;
targetState = STATE_HALF_EXPANDED;
}
} else if (yvel == 0.f || Math.abs(xvel) > Math.abs(yvel)) {
// If the Y velocity is 0 or the swipe was mostly horizontal indicated by the X velocity
// being greater than the Y velocity, settle to the nearest correct height.
int currentTop = releasedChild.getTop();
if (fitToContents) {
if (Math.abs(currentTop - fitToContentsOffset)
< Math.abs(currentTop - collapsedOffset)) {
top = fitToContentsOffset;
targetState = STATE_EXPANDED;
} else {
top = collapsedOffset;
targetState = STATE_COLLAPSED;
}
} else {
if (currentTop < halfExpandedOffset) {
if (currentTop < Math.abs(currentTop - collapsedOffset)) {
top = expandedOffset;
targetState = STATE_EXPANDED;
} else {
top = halfExpandedOffset;
targetState = STATE_HALF_EXPANDED;
}
} else {
if (Math.abs(currentTop - halfExpandedOffset)
< Math.abs(currentTop - collapsedOffset)) {
top = halfExpandedOffset;
targetState = STATE_HALF_EXPANDED;
} else {
top = collapsedOffset;
targetState = STATE_COLLAPSED;
}
}
}
} else { // Moving Down
if (fitToContents) {
top = collapsedOffset;
targetState = STATE_COLLAPSED;
} else {
// Settle to the nearest correct height.
int currentTop = releasedChild.getTop();
if (Math.abs(currentTop - halfExpandedOffset)
< Math.abs(currentTop - collapsedOffset)) {
top = halfExpandedOffset;
targetState = STATE_HALF_EXPANDED;
} else {
top = collapsedOffset;
targetState = STATE_COLLAPSED;
}
}
}
startSettlingAnimation(releasedChild, targetState, top, true);
}
@Override
public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
return MathUtils.clamp(
top, getExpandedOffset(), hideable ? parentHeight : collapsedOffset);
}
@Override
public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
return child.getLeft();
}
@Override
public int getViewVerticalDragRange(@NonNull View child) {
if (hideable) {
return parentHeight;
} else {
return collapsedOffset;
}
}
};
void dispatchOnSlide(int top) {
View bottomSheet = viewRef.get();
if (bottomSheet != null && !callbacks.isEmpty()) {
float slideOffset =
(top > collapsedOffset || collapsedOffset == getExpandedOffset())
? (float) (collapsedOffset - top) / (parentHeight - collapsedOffset)
: (float) (collapsedOffset - top) / (collapsedOffset - getExpandedOffset());
for (int i = 0; i < callbacks.size(); i++) {
callbacks.get(i).onSlide(bottomSheet, slideOffset);
}
}
}
@VisibleForTesting
int getPeekHeightMin() {
return peekHeightMin;
}
/**
* 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.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
@VisibleForTesting
public void disableShapeAnimations() {
// Sets the shape value animator to null, prevents animations from occuring during testing.
interpolatorAnimator = null;
}
private class SettleRunnable implements Runnable {
private final View view;
private boolean isPosted;
@State
int targetState;
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;
}
}
/**
* State persisted across instances
*/
protected static class SavedState extends AbsSavedState {
@State
final int state;
int peekHeight;
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();
peekHeight = source.readInt();
fitToContents = source.readInt() == 1;
hideable = source.readInt() == 1;
skipCollapsed = source.readInt() == 1;
}
public SavedState(Parcelable superState, @NonNull MyViewPagerBottomSheetBehavior> behavior) {
super(superState);
this.state = behavior.getState();
this.peekHeight = behavior.getPeekHeight();
this.fitToContents = behavior.isFitToContents();
this.hideable = behavior.isHideable();
this.skipCollapsed = behavior.getSkipCollapsed();
}
@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(peekHeight);
out.writeInt(fitToContents ? 1 : 0);
out.writeInt(hideable ? 1 : 0);
out.writeInt(skipCollapsed ? 1 : 0);
}
public static final Creator CREATOR =
new ClassLoaderCreator() {
@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];
}
};
}
/**
* A utility function to get the {@link BottomSheetBehavior} associated with the {@code view}.
*
* @param view The {@link View} with {@link BottomSheetBehavior}.
* @return The {@link BottomSheetBehavior} associated with the {@code view}.
*/
@NonNull
@SuppressWarnings("unchecked")
public static MyViewPagerBottomSheetBehavior from(@NonNull V view) {
ViewGroup.LayoutParams params = view.getLayoutParams();
if (!(params instanceof CoordinatorLayout.LayoutParams)) {
throw new IllegalArgumentException("The view is not a child of CoordinatorLayout");
}
CoordinatorLayout.Behavior> behavior =
((CoordinatorLayout.LayoutParams) params).getBehavior();
if (!(behavior instanceof MyViewPagerBottomSheetBehavior)) {
throw new IllegalArgumentException("The view is not associated with BottomSheetBehavior");
}
return (MyViewPagerBottomSheetBehavior) behavior;
}
/**
* Sets whether the BottomSheet should update the accessibility status of its {@link
* CoordinatorLayout} siblings when expanded.
*
* 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 ((Build.VERSION.SDK_INT >= Build.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 (Build.VERSION.SDK_INT >= Build.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;
}
}
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;
}
});
}
}