BottomSheetBehavior

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;
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值