Android自定义ViewGroup实现棺材布局(仿燃兔App游戏详情界面)

可惜了, 现在燃兔倒了, 看不到它的界面, 发个自己做的效果图吧:

这里写图片描述
这里写图片描述

哈哈, 大概的效果就是这样子.
我们先来解剖一下它:
  • 首先它有一个TopBar, 一个BottomBar;
  • 上面可以滑动的view,我们就叫他棺材盖吧,哈哈是不是很形象;
  • 棺材盖打开之后,下面的棺材底还可以滑动, 还有一个关闭按钮,停留在底部;
好像没有了吧, 我们再来仔细看一遍:

这里写图片描述

原来还有一个HeaderView呢, 他跟棺材底第一个view一样的宽高跟内容, 所以不仔细看是看不出有两个的. 如果他们是分离的话, 那关闭的动画就比较容易做了. 对了,还有一个白色的渐变效果.  


我们先大概说一下它的逻辑:
  • TopBar和BottomBar在最上面, TopBar根据手指滑动的速度和方向来确定显示或收起;
  • 棺材盖跟HeaderView有一个滚动视差, 并且棺材盖向下滑动到了一定距离后, 触发打开棺材盖: TopBar,BottomBar随之隐藏, 打开后, 棺材底可以接受触摸事件.
  • 点击关闭按钮, 先是一块白色的东西遮住棺材底, HeaderView跟棺材盖出现,并且下次打开的时候,棺材底看起来总是未滚动过的状态, 即使上次滑动到了底部.

好了, 我们再来想想代码应该怎么写:
  • 首先这个肯定是ViewGroup而不是View了.
  • 其次, 因为我们的棺材盖是可以打开,关闭,上下滑动的, 所以我们还要自己做滑动跟惯性效果, 那这样的话,我们在onMeasure的时候, 就不应该限制棺材盖跟棺材底的高度,他有多高就给他多高. onLayout的时候就用getMeasuredHeight,这样我们的滑动就可以做了.
  • 还有我们的这个ViewGroup打算限制布局中的子View数量,这样一来,也比较符合棺材这个设定,二来,我们也方便管理,哈哈。
  • 限制外部添加子View的数量为2: 一个棺材盖,一个棺材底就够了,至于TopBar, BottomBar, HeaderView那些用@layout的方法来添加,这样做的话,xml布局就比较清晰明了。




先来定义一下属性:

    <declare-styleable name="CoffinLayout">
        <attr name="lid_offset" format="dimension" />
        <attr name="lid_elevation" format="dimension" />
        <attr name="trigger_open_offset" format="dimension" />
        <attr name="residual_view" format="reference" />
        <attr name="header_view" format="reference" />
        <attr name="transition_color" format="color" />
        <attr name="top_bar" format="reference"/>
        <attr name="bottom_bar" format="reference"/>
    </declare-styleable>
  • lid_offset: 棺材盖的偏移量,这样就灵活好多;
  • lid_elevation: 棺材盖的阴影;
  • trigger_open_offset: 触发打开棺材盖的距离;
  • residual_view: 棺材盖打开后,显示在屏幕底部的View;
  • tr-ansition_color: 过渡View的颜色,就是棺材盖下面渐变的View,打开前用来遮挡棺材底;


好了,属性我们准备好了,再来限制外部添加View的数量:
我们重写全部addView方法,并在最后一个addView方法里面限制:

    @Override
    public void addView(View child) {
        addView(child, -1);
    }

    @Override
    public void addView(View child, int index) {
        if (child == null) {
            throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
        }
        LayoutParams params = child.getLayoutParams();
        if (params == null) {
            params = generateDefaultLayoutParams();
            if (params == null) {
                throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
            }
        }
        addView(child, index, params);
    }

    @Override
    public void addView(View child, int width, int height) {
        final LayoutParams params = generateDefaultLayoutParams();
        params.width = width;
        params.height = height;
        addView(child, -1, params);
    }

    @Override
    public void addView(View child, ViewGroup.LayoutParams params) {
        addView(child, -1, params);
    }

    @Override
    public void addView(View child, int index, LayoutParams params) {
        switch (getChildCount()) {
            case 0:
                mBottomView = child = packingBottomView(child);
                break;
            case 1:
                addResidualView(index);
                mLidView = child = packingLidView(child);
                addHeaderView(index);
                if (child != null) {
                    super.addView(child, index, params);
                }
                addTopBar(index);
                addBottomBar(index);
                return;
            case 6:
                throw new IllegalStateException("CoffinLayout child can't > 2");
            default:
                break;
        }
        if (child != null) {
            super.addView(child, index, params);
        }
    }
主要是看最后一个方法,当前子View为0时,则认定本次添加的View是棺材底,为1时,则棺材盖。
我们在添加棺材底之前,还调用了一个packingBottomView方法,这个方法就是在棺材底上面,添加过渡View:
    /**
     * 给他包装一下, 加上一个过渡的view
     *
     * @param view 棺材底
     * @return 包装后的棺材底
     */
    private View packingBottomView(View view) {
        if (mTransitionView != null && view != null) {
            FrameLayout frameLayout = new FrameLayout(getContext());
            frameLayout.addView(view);
            frameLayout.addView(mTransitionView);
            return frameLayout;
        }
        return null;
    }
我们在添加棺材盖之前, 还先后添加了ResidualView(打开后显示在底部的View), HeaderView, 在棺材盖添加了之后,调用了addTopBar和addBottomBar方法,分别添加了它们两个。我们现在来看一下子View的顺序:
    TopBar
        HeaderView
            BottomView
          ResidualView
      LidView
    BottomBar
哈哈,这样是不是就清晰了好多。我们现在来看一下packingLidView方法做了什么:

    /**
     * 给他包装一下, 加上阴影
     *
     * @param view 棺材盖
     * @return 包装后的棺材盖
     */
    private View packingLidView(View view) {
        if (mElevationView != null && view != null) {
            LinearLayout linearLayout = new LinearLayout(getContext());
            linearLayout.setOrientation(LinearLayout.VERTICAL);
            linearLayout.addView(mElevationView);
            linearLayout.addView(view);
            return linearLayout;
        }
        return null;
    }
原来就是在外面套了一层LinearLayout,加上了阴影, 至于阴影是怎么做的呢, 很简单,就一个View把GradientDrawable作为Background.


我们现在来看一下onMeasure方法是怎么不限制棺材盖和棺材底的高度的:


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            //子view想要多高,就给它多高
            view.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
        }
        //测量棺材底
        View view = ((ViewGroup) mBottomView).getChildAt(0);
        if (view != null) {
            view.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
        }
        mTransitionView.measure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
    }
哈哈,我们是用了 MeasureSpec.UNSPECIFIED 这个不常用的模式来测量它们,这样我们在onLayout中分别调用它们的getMeasuredHeight方法就能拿到它们需要的高度了:

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        switch (getChildCount()) {
            case 6:
                //BottomBar当然是放在底部了
                mBottomBar.layout(0, b - mBottomBar.getLayoutParams().height, r, b);
            case 5:
                //TopBar当然是顶部了
                mTopBar.layout(0, 0, r, mTopBar.getLayoutParams().height);
            case 4:
                //顶部 + 偏移量
                mHeaderView.layout(0, mHeaderViewOffset, r, mHeaderViewOffset + mHeaderView.getLayoutParams().height);
                mHeaderView.setTranslationY(0);
            case 3:
            case 2:
                //棺材盖: 棺材盖固定的偏移量 + 当前的偏移量
                mLidView.layout(0, mLidOffset + mLidViewOffset, r, mLidOffset + mLidViewOffset + mLidView.getMeasuredHeight());
                if (mResidualView != null) {
                    //棺材盖上面用来切换开关的view: 放在底部
                    mResidualView.layout(0, b, r, b + mResidualView.getLayoutParams().height);
                }
            case 1:
                //棺材底: 顶部 + 偏移量
                mBottomView.layout(0, mBottomViewOffset, r, mBottomViewOffset + mBottomView.getMeasuredHeight());
                //过渡view: 与棺材底偏移量相反 (因为它要始终显示在屏幕内)
                mTransitionView.layout(0, -mBottomViewOffset, r, -mBottomViewOffset + mTransitionView.getHeight());
                break;
            default:
                break;
        }
    }


好了,现在我们可以先看看效果了:

布局的话, 我们的TopBar, BottomBar, HeaderView, ResidualView都是通过@layout的方式引用的, 要注意的是我们的lid_offset(棺材盖偏移量)刚好跟棺材底的第一个View的高度是一样的, 因为这样就可以把那个View完全显示出来, 还有HeaderView我们要做成跟棺材底的第一个View一样的高度以及显示内容.

    <?xml version="1.0" encoding="utf-8"?>
    <com.test.viewtest.views.CoffinLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/coffin_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="false"
        app:bottom_bar="@layout/bottom_bar"
        app:header_view="@layout/header_view"
        app:lid_elevation="8dp"
        app:lid_offset="240dp"
        app:residual_view="@layout/residual_view"
        app:top_bar="@layout/top_bar"
        app:trigger_open_offset="100dp">

       <!--棺材底-->
       <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <ImageView
                android:layout_width="match_parent"
                android:layout_height="240dp"
                android:adjustViewBounds="true"
                android:scaleType="fitXY"
                android:src="@drawable/ic_0" />

            <android.support.v7.widget.RecyclerView
                android:id="@+id/bottom_recycler_view"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />
        </LinearLayout>

       <!--棺材盖-->
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <android.support.v7.widget.RecyclerView
                android:id="@+id/top_recycler_view"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@android:color/white" />

            <android.support.v7.widget.RecyclerView
                android:id="@+id/horizontal_recycler_view"
                android:layout_width="match_parent"
                android:layout_height="250dp"
                android:background="@android:color/white" />

        </LinearLayout>
    </com.test.viewtest.views.CoffinLayout>

看看效果:

这里写图片描述

好了,现在子View们都已就位了,还差个滚动的效果,说到滚动,我们就要用到Scroller了,但是只用Scroller是做不了像ScrollView那样的滑动效果的,因此我们还要用到一个VelocityTracker, 这个可以获取到手指移动的速率, 我们就用这个配合Scroller来完成惯性滚动效果。


既然是要监听触摸事件,身为爸爸身为ViewGroup,就要重写onInterceptTouchEvent方法了:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        //正在播放开关动画: 拦截
        if (isLidOpeningOrClosing()) {
            return true;
        }
        //已经开始拖动: 拦截
        final int action = ev.getAction();
        if ((action == MotionEvent.ACTION_MOVE) && (isBeingDragged)) {
            return true;
        }
        //爸爸需要拦截: 拦截
        if (super.onInterceptTouchEvent(ev)) {
            return true;
        }
        //不能拖动: 放行
        if (!canScroll()) {
            return false;
        }
        int y = (int) ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //停止惯性滚动并刷新y坐标
                if (!isLidOpeningOrClosing()) {
                    abortScrollerAnimation();
                }
                mLastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                int offset = y - mLastY;
                //判断是否触发拖动事件
                if (Math.abs(offset) > mTouchSlop) {
                    mLastY = y;
                    isBeingDragged = true;
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_OUTSIDE:
                isBeingDragged = false;
                break;
        }
        return isBeingDragged;
    }

那我们拦截了之后,就要怎么处理呢:
  • 棺材盖我们设定有5种状态: 半开,全开,合上,正在打开,正在关闭;
  • move事件我们就判断当前棺材盖的状态,如果棺材盖未打开,那就滚动棺材盖,反之,则滚动棺材底.
  • up事件: 如果棺材盖是半开的状态,则判断是否向下滑动,如果滑动的距离达到我们给定的距离,就触发打开棺材盖,如果距离不够,就回弹; 如果棺材盖是全开或合上状态,则根据手指速率,开始惯性滚动:

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mVelocityTracker.addMovement(event);
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (!isLidOpeningOrClosing()) {
                    abortScrollerAnimation();
                } else {
                    return false;
                }
                mLastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                if (mCurrentStatus == STATE_NAKED) {
                    offsetBottomView(y);
                } else {
                    offsetLidView(y);
                }
                if (mCurrentStatus != STATE_NAKED) {
                    mVelocityTracker.computeCurrentVelocity(1000);
                    float velocityY = mVelocityTracker.getYVelocity();
                    //根据手指滑动的速率和方向来判断是否要隐藏或显示TopBar
                    if (Math.abs(velocityY) > 4000) {
                        if (velocityY > 0) {
                            if (mTopBar != null && mTopBar.getTranslationY() == -mTopBar.getLayoutParams().height) {
                                showTopBar();
                            }
                        } else {
                            if (mTopBar != null && mTopBar.getTranslationY() == 0) {
                                hideTopBar();
                            }
                        }
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_OUTSIDE:
            case MotionEvent.ACTION_CANCEL:
                boolean isHandle = false;
                if (mCurrentStatus == STATE_HALF) {
                    //大于触发距离, 则打开棺材盖, 反之
                    if (mLidView.getTop() >= mTriggerOffset) {
                        openCoffin();
                        isHandle = true;
                    } else if (mLidView.getTop() > mLidOffset) {
                        closeCoffin();
                        isHandle = true;
                    }
                }
                //没有触发打开或关闭棺材盖的动画, 则开始惯性滚动
                if (!isHandle) {
                    mVelocityTracker.computeCurrentVelocity(1000);
                    mScroller.fling(0, 0, 0, (int) mVelocityTracker.getYVelocity(),
                            0, 0, Integer.MIN_VALUE, Integer.MAX_VALUE);
                    invalidate();
                }
                //标记状态
                isBeingDragged = false;
                break;
            default:
                break;
        }
        return true;
    }
那现在我们再来看一下computeScroll方法:
  • 棺材盖未打开: 滚动棺材盖,并做越界处理;
  • 棺材盖已打开: 滚动棺材底,并做越界处理;
  • 本次滚动结束: 更新状态;


    /**
     * 计算平滑滚动
     */
    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            int y = mScroller.getCurrY();
            //是新的一轮则刷新offset
            if (isNewScroll) {
                isNewScroll = false;
                mScrollOffset = y;
            }
            //未开盖: 滚动棺材盖
            if (mCurrentStatus != STATE_NAKED) {
                //判断是否还可以滚动
                if (mLidView != null && mLidView.getBottom() >= getBottom()) {
                    int offset = y - mScrollOffset;
                    //判断是否越界: 如果越界,则本次偏移量为可以滑动的最大值
                    if (mLidView.getBottom() + offset < getBottom()) {
                        offset = getBottom() - mLidView.getBottom();
                    } else if (mScroller.getCurrVelocity() > 0 && offset > 0) {//手指滑动, 并且是向下滑
                        if (mLidView.getTop() + offset >= mLidOffset && !isLidOpeningOrClosing()) {
                            offset = mLidOffset - mLidView.getTop();
                        }
                    }
                    offsetChildView(offset);
                }
            } else {//已开盖: 滚动棺材底
                //判断是否还可以滚动
                if (mBottomView != null && mBottomView.getBottom() >= getBottom()
                        && mBottomView.getTop() <= getTop()) {
                    int offset = y - mScrollOffset;
                    //判断是否越界: 如果越界,则本次偏移量为可以滑动的最大值
                    if (mBottomView.getBottom() + offset < getBottom()) {
                        offset = getBottom() - mBottomView.getBottom();
                    } else if (mBottomView.getTop() + offset > getTop()) {
                        offset = getTop() - mBottomView.getTop();
                    }
                    mBottomViewOffset += offset;
                    mBottomView.offsetTopAndBottom(offset);
                    mTransitionView.offsetTopAndBottom(-offset);
                }
            }
            mScrollOffset = y;
            invalidate();
        }
        if (mScroller.isFinished()) {
            isNewScroll = true;
            //滚动结束, 更新状态
            if (mCurrentStatus == STATE_OPENING) {
                mTransitionView.setVisibility(INVISIBLE);
                mHeaderView.setVisibility(INVISIBLE);
                if (mResidualView != null) {
                    showResidualView();
                }
                mCurrentStatus = STATE_NAKED;
                notifyListener();
            } else if (mCurrentStatus == STATE_CLOSING) {
                int offset = getTop() - mBottomView.getTop();
                mBottomViewOffset += offset;
                mBottomView.offsetTopAndBottom(offset);
                mTransitionView.offsetTopAndBottom(-offset);
                if (mResidualView != null) {
                    mResidualView.setTranslationY(0);
                }
                mCurrentStatus = STATE_HALF;
                notifyListener();
            }
        }
    }


跟一般的平滑滚动情况不同, 因为我们要做的滚动是可以分离的, 所以肯定不能用scrollTo.
做过跟随手指移动的小伙伴就会对那3个方法很熟悉了:
  1. layout(int l, int t, int r, int b)
  2. offsetLeftAndRight(int offset)
  3. offsetTopAndBottom(int offset)
我们只需要上下滑动, 所以用offsetTopAndBottom方法就可以了.
现在滑动是准备好了, 还有棺材盖跟HeaderView的视差效果, 这个其实也很简单, 我们在棺材盖offset的时候, 再根据当前的top值, 来决定HeaderView的偏移情况, 如果top值大于HeaderView的高度, 那就不需要做处理了, 否则将棺材盖本次需要offset的值除以2, 得到HeaderView的值, 那么棺材盖在向上滚动的时候, HeaderView就在他下面慢慢地也向上滚动了. 我们看看代码吧:

    /**
     * 更新棺材盖的位置
     *
     * @param y 偏移量
     */
    private void offsetLidView(int y) {
        if (mLidView != null && mLidView.getBottom() >= getBottom()) {
            int offset = y - mLastY;
            //判断是否越界
            if (mLidView.getBottom() + offset < getBottom()) {
                offset = getBottom() - mLidView.getBottom();
            }
            //如果棺材盖未打开, 并且是向下滑动, 则加一个阻尼效果
            if (offset > 0 && mLidView.getTop() > mLidOffset) {
                offset /= 2;
            }
            //更新需要联动的view
            offsetChildView(offset);
            //更新状态
            int newState = mLidView.getTop() <= getTop() ? STATE_COVER : STATE_HALF;
            if (mCurrentStatus != newState) {
                mCurrentStatus = newState;
                notifyListener();
            }
        }
        mLastY = y;
    }


    /**
     * 更新棺材盖和其他需要联动的View的位置
     *
     * @param offset 偏移量
     */
    private void offsetChildView(int offset) {
        //不是正在打开或关闭状态,并且棺材盖当前位置高于默认的偏移量
        if (!isLidOpeningOrClosing() && mLidView.getTop() < mLidOffset) {
            int bottomViewOffset = offset / 2;//损失一半
            //判断越界
            if (mBottomView.getTop() > getTop() || mBottomView.getTop() + bottomViewOffset > getTop()) {
                bottomViewOffset = getTop() - mBottomView.getTop();
            }
            //更新BottomView和HeaderView的位置
            mBottomViewOffset += bottomViewOffset;
            mBottomView.offsetTopAndBottom(bottomViewOffset);
            mHeaderViewOffset += bottomViewOffset;
            mHeaderView.offsetTopAndBottom(bottomViewOffset);
            mTransitionView.offsetTopAndBottom(-bottomViewOffset);
        }
        //更新棺材盖的位置
        mLidViewOffset += offset;
        mLidView.offsetTopAndBottom(offset);
        //更新TopBar的透明度
        float percent = (float) mLidViewOffset / (getBottom() - mLidOffset);
        mTransitionView.setAlpha(1F - percent);
        percent = (float) (mLidView.getTop() - mTopBar.getHeight()) / (mLidOffset - mTopBar.getHeight());
        if (percent > 1F) {
            percent = 1F;
        }
        if (percent < 0) {
            percent = 0;
        }
        setTopBarBackgroundAlpha(percent);
    }
棺材底因为没有其他View的联动, 所以offsetBottomView方法就offset自己就行了.
哈哈, 现在还差棺材盖, TopBar, BottomBar, ResidualView, HeaderView他们的打开, 关闭动画, 就基本完成我们的CoffinLayout了, 其实那些动画也很简单, 都是用的ValueAnimator, 不过我们再想想: 这么多View的动画, 无非就是起点和终点不同, 最后都是调用ValueAnimator的start方法来开始的, 为了代码质量, 肯定要先封装一个方法了:
    /**
     * 执行动画
     *
     * @param target 要执行动画的view
     * @param startY 开始值
     * @param endY   结束值
     */
    private void startValueAnimation(View target, int startY, int endY) {
        ValueAnimator animator = ValueAnimator.ofInt(startY, endY).setDuration(ANIMATION_DURATION);
        animator.addUpdateListener(animation -> target.setTranslationY((int) animation.getAnimatedValue()));
        animator.start();
    }
哈哈, 这样一来, 我们需要播放动画的时候, 就舒服多了:
    public void showBottomBar() {
        startValueAnimation(mBottomBar, mBottomBar.getLayoutParams().height, 0);
    }

    public void hideBottomBar() {
        startValueAnimation(mBottomBar, 0, mBottomBar.getLayoutParams().height);
    }

    private void showResidualView() {
        startValueAnimation(mResidualView, 0, -mResidualView.getLayoutParams().height);
    }

    private void showHeaderView() {
        startValueAnimation(mHeaderView, Math.abs(mBottomView.getTop()) >
                mHeaderView.getHeight() ? -mHeaderView.getHeight() : mBottomView.getTop(), 0);
    }

    private void showTopBar() {
        startValueAnimation(mTopBar, -mTopBar.getLayoutParams().height, 0);
    }

    private void hideTopBar() {
        startValueAnimation(mTopBar, 0, -mTopBar.getLayoutParams().height);
    }


对了, 我们应该向外公开两个方法: 打开棺材盖, 合上棺材盖.
先看看怎么打开:

    /**
     * 打开棺材盖
     */
    public void openCoffin() {
        if (mCurrentStatus == STATE_OPENING) {
            return;
        }
        abortScrollerAnimation();
        isNewScroll = true;
        int offset = getBottom() - mLidView.getTop();
        mScroller.startScroll(0, 0, 0, offset, ANIMATION_DURATION);
        mCurrentStatus = STATE_OPENING;
        notifyListener();
        invalidate();
        if (mBottomBar != null) {
            hideBottomBar();
        }
        if (mTopBar != null) {
            if (mTopBar.getTranslationY() == 0) {
                hideTopBar();
            } else{
                postDelayed(() -> {
                    if (mTopBar.getTranslationY() == 0) {
                        hideTopBar();
                    }
                }, ANIMATION_DURATION);
            }
        }
    }
我们先是判断了当前状态, 如果正在打开就直接return. 接着我们还打断了当前未完成的惯性滚动, 开始了新的一轮滚动, 因为我们上面的computeScroll方法已经做了平滑滚动的处理, 所以这里调用了Scroller的startScroll方法之后直接invalidate就行了. 之后我们就隐藏了BottomBar和TopBar, 如果TopBar正在执行打开动画,那么我们等他打开完,再来隐藏他.
再看看怎么关闭:

    /**
     * 关闭棺材盖
     */
    public void closeCoffin() {
        if (mCurrentStatus == STATE_CLOSING) {
            return;
        }
        abortScrollerAnimation();
        isNewScroll = true;
        int offset = mLidOffset - mLidView.getTop();
        mScroller.startScroll(0, 0, 0, offset, ANIMATION_DURATION);
        mTransitionView.setVisibility(VISIBLE);
        mHeaderView.setVisibility(VISIBLE);
        if (mCurrentStatus == STATE_NAKED) {
            showHeaderView();
        }
        mCurrentStatus = STATE_CLOSING;
        notifyListener();
        invalidate();
        if (mBottomBar != null && mBottomBar.getTranslationY() > 0) {
            showBottomBar();
        }
        if (mTopBar != null && mTopBar.getTranslationY() < 0) {
            showTopBar();
        }
    }


emmm, 跟打开的逻辑差不多, 不过这里多了一个showHeaderView方法, 如果是已经打开了的棺材盖, 就要播放显示HeaderView的动画, 如果是没达到触发的距离, 回弹到原位的情况下, 就不用播放了.
哈哈, 现在基本上是完成了, 再来看看我们的劳动效果:

这里写图片描述这里写图片描述



哈哈哈, 本文到此结束,有错误的地方请指出,谢谢大家!

完整代码地址: https://github.com/wuyr/CoffinLayout

猜你喜欢

转载自blog.csdn.net/u011387817/article/details/79552699