BottomSheetBehavior
标签(空格分隔): android
BottomSheetBehavior支持如下属性
<declare-styleable name="BottomSheetBehavior_Layout">
<attr format="dimension" name="behavior_peekHeight">
<enum name="auto" value="-1"/>
</attr>
<attr format="boolean" name="behavior_hideable"/>
<attr format="boolean" name="behavior_skipCollapsed"/>
</declare-styleable>
含义
//折叠的高度
app:behavior_peekHeight="10dp" setPeekHeight
//是否可以隐藏
app:behavior_hideable="true" setHideable
//是否跳过折叠状态
app:behavior_skipCollapsed="true" setSkipCollapsed
首先加载BottomSheetBehavior,根据属性配置,设置能否隐藏,折叠高度,是否跳过折叠状态
public static <V extends View> BottomSheetBehavior<V> from(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 BottomSheetBehavior)) {
throw new IllegalArgumentException(
"The view is not associated with BottomSheetBehavior");
}
return (BottomSheetBehavior<V>) behavior;
}
public BottomSheetBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.BottomSheetBehavior_Layout);
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));
setSkipCollapsed(a.getBoolean(R.styleable.BottomSheetBehavior_Layout_behavior_skipCollapsed,
false));
a.recycle();
ViewConfiguration configuration = ViewConfiguration.get(context);
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
}
然后就是安排布局
@Override
public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
if (ViewCompat.getFitsSystemWindows(parent) && !ViewCompat.getFitsSystemWindows(child)) {
ViewCompat.setFitsSystemWindows(child, true);
}
int savedTop = child.getTop();
// First let the parent lay it out
parent.onLayoutChild(child, layoutDirection);
// Offset the bottom sheet
mParentHeight = parent.getHeight();
int peekHeight;
if (mPeekHeightAuto) {
if (mPeekHeightMin == 0) {
mPeekHeightMin = parent.getResources().getDimensionPixelSize(
R.dimen.design_bottom_sheet_peek_height_min);
}
peekHeight = Math.max(mPeekHeightMin, mParentHeight - parent.getWidth() * 9 / 16);
} else {
peekHeight = mPeekHeight;
}
mMinOffset = Math.max(0, mParentHeight - child.getHeight());
mMaxOffset = Math.max(mParentHeight - peekHeight, mMinOffset);
if (mState == STATE_EXPANDED) {
//如果展开
ViewCompat.offsetTopAndBottom(child, mMinOffset);
} else if (mHideable && mState == STATE_HIDDEN) {
//如果隐藏
ViewCompat.offsetTopAndBottom(child, mParentHeight);
} else if (mState == STATE_COLLAPSED) {
//如果折叠,则以折叠高度为准(peekHeight)
ViewCompat.offsetTopAndBottom(child, mMaxOffset);
} else if (mState == STATE_DRAGGING || mState == STATE_SETTLING) {
//如果是拖动或固定(拖动才会有位置变化)
ViewCompat.offsetTopAndBottom(child, savedTop - child.getTop());
}
if (mViewDragHelper == null) {
mViewDragHelper = ViewDragHelper.create(parent, mDragCallback);
}
mViewRef = new WeakReference<>(child);
mNestedScrollingChildRef = new WeakReference<>(findScrollingChild(child));
return true;
}
控制显示变化
public final void setState(final @State int state) {
if (state == mState) {
return;
}
if (mViewRef == null) {
// The view is not laid out yet; modify mState and let onLayoutChild handle it later
if (state == STATE_COLLAPSED || state == STATE_EXPANDED ||
(mHideable && state == STATE_HIDDEN)) {
mState = state;
}
return;
}
final V child = mViewRef.get();
if (child == null) {
return;
}
// Start the animation; wait until a pending layout if there is one.
ViewParent parent = child.getParent();
//如果已经requested并且附加到window
if (parent != null && parent.isLayoutRequested() && ViewCompat.isAttachedToWindow(child)) {
child.post(new Runnable() {
@Override
public void run() {
startSettlingAnimation(child, state);
}
});
} else {
startSettlingAnimation(child, state);
}
}
具体的启动改变,setStateInternal中回调了BottomSheetCallback#onStateChanged
void startSettlingAnimation(View child, int state) {
//先根据state设置好top位置
int top;
if (state == STATE_COLLAPSED) {
top = mMaxOffset;
} else if (state == STATE_EXPANDED) {
top = mMinOffset;
} else if (mHideable && state == STATE_HIDDEN) {
top = mParentHeight;
} else {
throw new IllegalArgumentException("Illegal state argument: " + state);
}
//先改为settling
setStateInternal(STATE_SETTLING);
//移动到指定位置后改为指定state
if (mViewDragHelper.smoothSlideViewTo(child, child.getLeft(), top)) {
ViewCompat.postOnAnimation(child, new SettleRunnable(child, state));
}
}
设置状态很容易踩坑,使用的时候需要注意。
mMinOffset = Math.max(0, mParentHeight - child.getHeight());
mMaxOffset = Math.max(mParentHeight - peekHeight, mMinOffset);
注意上面2句,在结合上面的具体移动。在没有peekHeight的时候STATE_COLLAPSED和STATE_EXPANDED这两个状态切换会出现mMinOffset==mMaxOffset的情况,也就是top没改变的情况,从而导致mViewDragHelper.smoothSlideViewTo(child, child.getLeft(), top)
返回false导致最后state状态更新失败,回到STATE_SETTLING。总之在切换状态时注意2个状态之间top是否会变化,如果不能引起变化,则都会回到STATE_SETTLING
关于事件拦截与处理,滑动交给ViewDragHelper处理
//寻找behavior是否包含有NestedScrollingChild
private View findScrollingChild(View view) {
if (view instanceof NestedScrollingChild) {
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;
}
@Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
//如果不可见不拦截
if (!child.isShown()) {
mIgnoreEvents = true;
return false;
}
int action = MotionEventCompat.getActionMasked(event);
// Record the velocity
if (action == MotionEvent.ACTION_DOWN) {
reset();
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);
switch (action) {
//恢复一些默认标记
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mTouchingScrollingChild = false;
mActivePointerId = MotionEvent.INVALID_POINTER_ID;
// Reset the ignore flag
if (mIgnoreEvents) {
mIgnoreEvents = false;
return false;
}
break;
case MotionEvent.ACTION_DOWN:
int initialX = (int) event.getX();
mInitialY = (int) event.getY();
View scroll = mNestedScrollingChildRef.get();
//如果有ns控件,并处于控件内
if (scroll != null && parent.isPointInChildBounds(scroll, initialX, mInitialY)) {
mActivePointerId = event.getPointerId(event.getActionIndex());
mTouchingScrollingChild = true;
}
//判断是否忽略
mIgnoreEvents = mActivePointerId == MotionEvent.INVALID_POINTER_ID &&
!parent.isPointInChildBounds(child, initialX, mInitialY);
break;
}
//不忽略,VDH要处理,拦截
if (!mIgnoreEvents && mViewDragHelper.shouldInterceptTouchEvent(event)) {
return true;
}
//VDH不处理情况
View scroll = mNestedScrollingChildRef.get();
return action == MotionEvent.ACTION_MOVE && scroll != null &&
!mIgnoreEvents && mState != STATE_DRAGGING &&
!parent.isPointInChildBounds(scroll, (int) event.getX(), (int) event.getY()) &&
Math.abs(mInitialY - event.getY()) > mViewDragHelper.getTouchSlop();
}
关于滑动的协同处理,前提是behavior内有ns控件。
//只协同处理竖直方向的滑动
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child,
View directTargetChild, View target, int nestedScrollAxes) {
mLastNestedScrollDy = 0;
mNestedScrolled = false;
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
//这个方法主要处理的滑动控制,当遇到ns控件时,怎样协同处理ns的滚动,和自身的折叠展开等状态的控制
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dx,
int dy, int[] consumed) {
View scrollingChild = mNestedScrollingChildRef.get();
//如果不是ns控件不是发起者,不预处理
if (target != scrollingChild) {
return;
}
int currentTop = child.getTop();
int newTop = currentTop - dy;
if (dy > 0) { // Upward
//如果上移超过了阈值,则以阈值为准
if (newTop < mMinOffset) {
//计算消耗,移动,设置状态
consumed[1] = currentTop - mMinOffset;
ViewCompat.offsetTopAndBottom(child, -consumed[1]);
//此时属于展开状态
setStateInternal(STATE_EXPANDED);
} else {
consumed[1] = dy;
ViewCompat.offsetTopAndBottom(child, -dy);
//属于拖动状态
setStateInternal(STATE_DRAGGING);
}
} else if (dy < 0) { // Downward
//需要检测是否可以垂直滚动,如NestedScrollView。如果可以滚动,则不预处理,先让其滚动
//直到不能滚动时,则预处理接管事件,进行拖动和折叠
if (!ViewCompat.canScrollVertically(target, -1)) {
if (newTop <= mMaxOffset || mHideable) {
consumed[1] = dy;
ViewCompat.offsetTopAndBottom(child, -dy);
setStateInternal(STATE_DRAGGING);
} else {
consumed[1] = currentTop - mMaxOffset;
ViewCompat.offsetTopAndBottom(child, -consumed[1]);
setStateInternal(STATE_COLLAPSED);
}
}
}
//回调BottomSheetCallback#onSlide
dispatchOnSlide(child.getTop());
mLastNestedScrollDy = dy;
mNestedScrolled = true;
}