Coordinator学习笔记(模仿百度地图的效果)
BottomSheetBehavior
这是什么?
官方文档是
An interaction behavior plugin for a child view of CoordinatorLayout
to make it work as a bottom sheet.
用我的渣英语翻译过来就是
这是一个让一个属于CoordinatorLayout的子view的交互行为变的和bottom sheet的插件
集成方法
只需要在coordinatorLayout的一级子view的属性中添加一句
app:layout_behavior="@string/bottom_sheet_behavior"
其实这句话是指向了android默认behavior实现类,这样就可以让你的布局像bottom sheet一样使用了。
其他属性
重要的方法
/** * 在CoordinatorLayout和指定的view进行关联时调用 * @param parent CoordinatorLayout * @param child 添加了Behavior的布局 * @param layoutDirection 方向 * @return */ onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) Called when the parent CoordinatorLayout is about the layout the given child view. //在CoordinatorLayout和指定的view进行关联时调用,在这里进行view的测绘动作 /** * 在这里处理是否允许滑动的逻辑 * @param coordinatorLayout * @param child * @param target * @param dx * @param dy * @param consumed */ void onNestedPreScroll (CoordinatorLayout coordinatorLayout, V child, View target, int dx, int dy, int[] consumed) Called when a nested scroll in progress is about to update, before the target has consumed any of the scrolled distance. /** * 在我的测试中,只有拖动NestedScrollView布局里面的元素的时候,这个才会被调用,在这里处理用户放手后,滚动方向,速度和最后滚动的位置 * @param coordinatorLayout * @param child * @param target */ void onStopNestedScroll (CoordinatorLayout coordinatorLayout, V child, View target) Called when a nested scroll has ended. /** * 作用和onStopNestedScroll类似,只不过正好和它相反,只有拖动的不是NestedScrollView里面的元素时,回调才会发生 * @param releasedChild * @param xvel * @param yvel */ @Override public void onViewReleased(View releasedChild, float xvel, float yvel) //这个回调不是BottomSheetBehavior的方法,而是ViewDragHelper.Callback的
源码分析
重要的点都写在注释里
/** * 在CoordinatorLayout和指定的view进行关联时调用 * @param parent CoordinatorLayout * @param child 添加了Behavior的布局 * @param layoutDirection 方向 * @return */ @Override public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) { Log.d("qin","onLayoutChild"); 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()); //这个是计算最后你的布局展开时和CoordinatorLayout顶部的距离,源码中不能为负数 // mMinOffset = dp2px(-100); //如果你的布局比CoordinatorLayout高,而且你想你的布局展开时,把你的头部滑上去,可以去除负数的限制,里面填写你想滑上去的高度 mMaxOffset = Math.max(mParentHeight - peekHeight, mMinOffset);//这个是计算最后你的布局收缩时和CoordinatorLayout顶部的距离,源码中不能小于展开时的高度 /** * 这里根据默认的初始状态,初始化界面 */ if (mState == STATE_EXPANDED) { ViewCompat.offsetTopAndBottom(child, mMinOffset); } else if (mHideable && mState == STATE_HIDDEN) { ViewCompat.offsetTopAndBottom(child, mParentHeight-dp2px(lastHeight)); } else if (mState == STATE_COLLAPSED) { 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; }
/** * 在这里处理是否允许滑动的逻辑 * @param coordinatorLayout * @param child * @param target * @param dx * @param dy * @param consumed */ @Override public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dx, int dy, int[] consumed) { // Log.d("qin","onNestedPreScroll"+dy); View scrollingChild = mNestedScrollingChildRef.get(); 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 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); } } } dispatchOnSlide(child.getTop()); mLastNestedScrollDy = dy; mNestedScrolled = true; }
/** * 在我的测试中,只有拖动NestedScrollView布局里面的元素的时候,这个才会被调用,在这里处理用户放手后,滚动方向,速度和最后滚动的位置 * @param coordinatorLayout * @param child * @param target */ @Override public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) { Log.d("qin","onStopNestedScroll"); if (child.getTop() == mMinOffset) { setStateInternal(STATE_EXPANDED); return; } if (target != mNestedScrollingChildRef.get() || !mNestedScrolled) { return; } int top; int targetState; if (mLastNestedScrollDy > 0) { //由onNestedPreScroll赋值 /** * 只要发现是向上滑动的,马上变成扩展状态,如果想先进入卡片收缩状态,可以修改这里 */ top = mMinOffset; targetState = STATE_EXPANDED; } else if (mHideable && shouldHide(child, getYVelocity())) { top=mParentHeight-dp2px(lastHeight); targetState = STATE_HIDDEN; } else if (mLastNestedScrollDy == 0) { int currentTop = child.getTop(); if (Math.abs(currentTop - mMinOffset) < Math.abs(currentTop - mMaxOffset)) { top = mMinOffset; targetState = STATE_EXPANDED; } else { top = mMaxOffset; targetState = STATE_COLLAPSED; } } else { top = mMaxOffset; targetState = STATE_COLLAPSED; } if (mViewDragHelper.smoothSlideViewTo(child, child.getLeft(), top)) { setStateInternal(STATE_SETTLING); ViewCompat.postOnAnimation(child, new MyBottomSheetBehavior.SettleRunnable(child, targetState)); } else { setStateInternal(targetState); } mNestedScrolled = false; }
/** * 作用和onStopNestedScroll类似,只不过正好和它相反,只有拖动的不是NestedScrollView里面的元素时,回调才会发生 * @param releasedChild * @param xvel * @param yvel 如果向上划,则为负数,向下为正数 */ @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { int top; Log.d("qin","onViewReleased=="+yvel); @BottomSheetBehavior.State int targetState; if (yvel < 0) { // Moving up /** * 只要发现是向上滑动的,马上变成扩展状态,如果想先进入卡片收缩状态,可以修改这里 */ top = mMinOffset; targetState = STATE_EXPANDED; } else if (mHideable && shouldHide(releasedChild, yvel)) { // mParentHeight=mParentHeight-100; // top = 100; top=mParentHeight-dp2px(lastHeight); targetState = STATE_HIDDEN; } else if (yvel == 0.f) { int currentTop = releasedChild.getTop(); if (Math.abs(currentTop - mMinOffset) < Math.abs(currentTop - mMaxOffset)) { top = mMinOffset; targetState = STATE_EXPANDED; } else { top = mMaxOffset; targetState = STATE_COLLAPSED; } } else { /** * 通过这里可以发现,在扩展状态下,只需要稍稍拉动卡片,就会使卡片回到折叠状态 * 如果是在隐藏的状态下,要往下拉动到shouldHide为true的时候才会隐藏,否则,一直都是卡片折叠的状态 */ top = mMaxOffset; targetState = STATE_COLLAPSED; } if (mViewDragHelper.settleCapturedViewAt(releasedChild.getLeft(), top)) { Log.d("qin","settleCapturedViewAt==true"); setStateInternal(STATE_SETTLING); ViewCompat.postOnAnimation(releasedChild, new MyBottomSheetBehavior.SettleRunnable(releasedChild, targetState)); } else { Log.d("qin","settleCapturedViewAt==false"); setStateInternal(targetState); } }
可以看到onViewReleased和onStopNestedScroll正好形成了互补,基本上覆盖了所有的情况,所以两个里面代码和逻辑也基本上类似。唯一的差别是,onViewReleased可以直接获得滑动距离和方向,而onStopNestedScroll则需要从onNestedPreScroll获取。
自定义BottomSheetBehavior
好哒,看了上面的源码,我们来尝试着修改一下BottomSheetBehavior。默认的BottomSheetBehavior固定下来的状态一共有3个,分别是 展开,收缩,隐藏,那么我们想再增加一个状态,“更加折叠”这个状态可不可以呢?答案是肯定的
如果想实现这样的效果,直接修改android自带的BottomSheetBehavior这样肯定是不可以的。所以我们需要新建一个类,名字我暂且叫做“MyBottomSheetBehavior”,然后将BottomSheetBehavior的代码直接拷过来,并解决其中的错误。
好了,MyBottomSheetBehavior新建好了。如果我们想它生效的话,就需要将我们布局中,原来的
app:layout_behavior="@string/bottom_sheet_behavior"
更换为
app:layout_behavior="com.lanlengran.coordinatorlayouttest.MyBottomSheetBehavior"
当然,这具体的值需要根据你自己MyBottomSheetBehavior放置的位置修改,不能直接照抄。
现在终于,我们的准备工作都做好了。
我们看一下MyBottomSheetBehavior的代码,在原来的类型上增加一种
/** * 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; /** * 这是我们新增的状态 */ public static final int STATE_COLLAPSED_MORE = 6; /** @hide */ @RestrictTo(LIBRARY_GROUP) @IntDef({STATE_EXPANDED, STATE_COLLAPSED, STATE_DRAGGING, STATE_SETTLING, STATE_HIDDEN,STATE_COLLAPSED_MORE}) //注意这里也要添加
状态增加好了,我们还需要新建两个变量,一个用来保存“更加折叠”状态下卡片的高度和在“更加折叠”状态下卡片距离父控件的距离。
public static final int moreCollapsedHeight=146; int mMaxOffsetForMore;
现在我们只需要模仿原来的代码,去写我们的代码就可以了,首先是在初始化的时候,也就是在onLayoutChild中,算出在“更加折叠”状态下卡片距离父控件的距离
/** * 在CoordinatorLayout和指定的view进行关联时调用 * @param parent CoordinatorLayout * @param child 添加了Behavior的布局 * @param layoutDirection 方向 * @return */ @Override public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) { Log.d("qin","onLayoutChild"); 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()); //这个是计算最后你的布局展开时和CoordinatorLayout顶部的距离,源码中不能为负数 // mMinOffset = dp2px(-100); //如果你的布局比CoordinatorLayout高,而且你想你的布局展开时,把你的头部滑上去,可以去除负数的限制,里面填写你想滑上去的高度 mMaxOffset = Math.max(mParentHeight - peekHeight, mMinOffset);//这个是计算最后你的布局收缩时和CoordinatorLayout顶部的距离,源码中不能小于展开时的高度 mMaxOffsetForMore =mMaxOffset+dp2px(moreCollapsedHeight);//这个是我们布局收缩时和CoordinatorLayout顶部的距离 /** * 这里根据默认的初始状态,初始化界面 */ if (mState == STATE_EXPANDED) { ViewCompat.offsetTopAndBottom(child, mMinOffset); } else if (mHideable && mState == STATE_HIDDEN) { ViewCompat.offsetTopAndBottom(child, mParentHeight-dp2px(lastHeight)); } else if (mState == STATE_COLLAPSED) { ViewCompat.offsetTopAndBottom(child, mMaxOffset); } else if (mState == STATE_DRAGGING || mState == STATE_SETTLING) { ViewCompat.offsetTopAndBottom(child, savedTop - child.getTop()); }else if (mState == STATE_COLLAPSED_MORE) { ViewCompat.offsetTopAndBottom(child, mMaxOffsetForMore); } if (mViewDragHelper == null) { mViewDragHelper = ViewDragHelper.create(parent, mDragCallback); } mViewRef = new WeakReference<>(child); mNestedScrollingChildRef = new WeakReference<>(findScrollingChild(child)); return true; }
然后在onStopNestedScroll中添加以下的代码,核心代码我都写了注释,聪明的你,一定可以看懂!
/** * 在我的测试中,只有拖动NestedScrollView布局里面的元素的时候,这个才会被调用,在这里处理用户放手后,滚动方向,速度和最后滚动的位置 * @param coordinatorLayout * @param child * @param target */ @Override public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) { Log.d("qin","onStopNestedScroll"+mLastNestedScrollDy); if (child.getTop() == mMinOffset) { setStateInternal(STATE_EXPANDED); return; } if (target != mNestedScrollingChildRef.get() || !mNestedScrolled) { return; } int top; int targetState; if (mLastNestedScrollDy > 0) { //由onNestedPreScroll赋值 /** * 只要发现是向上滑动的,马上变成扩展状态,如果想先进入卡片收缩状态,可以修改这里 */ top = mMinOffset; targetState = STATE_EXPANDED; } else if (mHideable && shouldHide(child, getYVelocity())) { top=mParentHeight-dp2px(lastHeight); targetState = STATE_HIDDEN; } else if (mLastNestedScrollDy == 0) { int currentTop = child.getTop(); if (currentTop<mMaxOffset){ //如果现在卡片距离是介于卡片折叠和扩展的状态 if (Math.abs(currentTop - mMinOffset) < Math.abs(currentTop - mMaxOffset)) { //卡片的状态更加接近于扩展状态 top = mMinOffset; targetState = STATE_EXPANDED; } else { //卡片的状态更加接近于折叠状态 top = mMaxOffset; targetState = STATE_COLLAPSED; } }else { //如果现在卡片距离是介于卡片折叠和更加折叠的状态 if (Math.abs(currentTop - mMaxOffset) < Math.abs(currentTop - mMaxOffsetForMore)) { //卡片的状态更加接近于折叠状态 top = mMaxOffset; targetState = STATE_COLLAPSED; } else { //卡片的状态更加接近于更加折叠状态 top = mMaxOffsetForMore; targetState = STATE_COLLAPSED_MORE; } } } else { int currentTop = child.getTop(); if (currentTop<mMaxOffset){ //如果现在卡片距离是介于卡片折叠和扩展的状态,由于是向下滑动,所以直接滑动到折叠状态 top = mMaxOffset; targetState = STATE_COLLAPSED; Log.d("qin","STATE_COLLAPSED=="); }else { //如果现在卡片距离是介于卡片折叠和更加折叠的状态,由于是向下滑动,所以直接滑动到更加折叠状态 top = mMaxOffsetForMore; targetState = STATE_COLLAPSED_MORE; Log.d("qin","STATE_COLLAPSED_MORE=="); } } Log.d("qin","currentTop=="+top); if (mViewDragHelper.smoothSlideViewTo(child, child.getLeft(), top)) { setStateInternal(STATE_SETTLING); ViewCompat.postOnAnimation(child, new MyBottomSheetBehavior.SettleRunnable(child, targetState)); } else { setStateInternal(targetState); } mNestedScrolled = false; }
而onViewReleased几乎就和onStopNestedScroll一模一样,就是变量名字稍有差别,我们把代码直接拷过来
/** * 作用和onStopNestedScroll类似,只不过正好和它相反,只有拖动的不是NestedScrollView里面的元素时,回调才会发生 * @param releasedChild * @param xvel * @param yvel 如果向上划,则为负数,向下为正数 */ @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { int top; Log.d("qin","onViewReleased=="+yvel); @BottomSheetBehavior.State int targetState; if (yvel < 0) { // Moving up /** * 只要发现是向上滑动的,马上变成扩展状态,如果想先进入卡片收缩状态,可以修改这里 */ top = mMinOffset; targetState = STATE_EXPANDED; } else if (mHideable && shouldHide(releasedChild, yvel)) { // mParentHeight=mParentHeight-100; // top = 100; top=mParentHeight-dp2px(lastHeight); targetState = STATE_HIDDEN; } else if (yvel == 0.f) { int currentTop = releasedChild.getTop(); if (currentTop<mMaxOffset){ if (Math.abs(currentTop - mMinOffset) < Math.abs(currentTop - mMaxOffset)) { top = mMinOffset; targetState = STATE_EXPANDED; } else { top = mMaxOffset; targetState = STATE_COLLAPSED; } }else { if (Math.abs(currentTop - mMaxOffset) < Math.abs(currentTop - mMaxOffsetForMore)) { top = mMaxOffset; targetState = STATE_COLLAPSED; } else { top = mMaxOffsetForMore; targetState = STATE_COLLAPSED_MORE; } } } else { int currentTop = releasedChild.getTop(); if (currentTop<mMaxOffset){ top = mMaxOffset; targetState = STATE_COLLAPSED; }else { top = mMaxOffsetForMore; targetState = STATE_COLLAPSED_MORE; } } if (mViewDragHelper.settleCapturedViewAt(releasedChild.getLeft(), top)) { setStateInternal(STATE_SETTLING); ViewCompat.postOnAnimation(releasedChild, new MyBottomSheetBehavior.SettleRunnable(releasedChild, targetState)); } else { setStateInternal(targetState); } }
完成了上面的步骤,我们基本已经完成了其中的核心代码,但是别急着运行。还记得我们在源码分析时候的一个函数吗?也就是onNestedPreScroll函数,在这里处理的是,是否允许卡片向下滑动。所以我们要小小的修改一下这里的代码,否则,我们的卡片无法向下滑动,也就实现不了我们的效果。所以我们修改如下
/** * 在这里处理是否允许滑动的逻辑 * @param coordinatorLayout * @param child * @param target * @param dx * @param dy * @param consumed */ @Override public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dx, int dy, int[] consumed) { // Log.d("qin","onNestedPreScroll"+dy); View scrollingChild = mNestedScrollingChildRef.get(); 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 if (!ViewCompat.canScrollVertically(target, -1)) { //模仿原来的代码,添加一个,只要卡片距离顶端的高度小于,我们更加折叠状态距离顶端的高度就运行滑动 if (newTop <= mMaxOffset || mHideable||newTop<=mMaxOffsetForMore) { consumed[1] = dy; ViewCompat.offsetTopAndBottom(child, -dy); setStateInternal(STATE_DRAGGING); } else { consumed[1] = currentTop - mMaxOffset; ViewCompat.offsetTopAndBottom(child, -consumed[1]); setStateInternal(STATE_COLLAPSED); } } } dispatchOnSlide(child.getTop()); mLastNestedScrollDy = dy; mNestedScrolled = true; }
既然onStopNestedScroll有对应的控制是否允许滑动的函数,那么它的兄弟onViewReleased肯定也有对应的函数,那么我们同样要修改下,把mMaxOffset更改成我们的mMaxOffsetForMore即可
@Override public int clampViewPositionVertical(View child, int top, int dy) { return MathUtils.constrain(top, mMinOffset, mHideable ? mParentHeight : mMaxOffsetForMore); }
现在,我们运行下,就实现了四个状态,分别是:扩展,折叠,更加折叠,隐藏。
DEMO源码地址:
开源中国的地址:https://gitee.com/lanlengran/CoordinatorLayoutTest/tree/master
github的地址:https://github.com/richmond-rui/CoordinatorLayoutTest