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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值