欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

页面折叠效果实现

程序员文章站 2022-05-15 17:13:36
...

前言

Android手机屏幕相对较小,可能无法一次性将所有的元素都展示给用户,但有些情况下又确实需要将多个元素同时展示给用户。比如在外卖下单之后用户即希望能够查看当前外卖的状态也希望能够直接通过地图查看到外卖骑士的位置,地图控件和订单状态界面都很大,这时后可以通过折叠订单状态界面来展示更多的地图界面。除了上面描述的场景还有很多比如展示某些特殊菜单也可以使用这种交互方式,这里就来简单实现页面折叠效果。

实现效果

页面折叠效果实现

滚动判断

通常需要折叠的界面不是简单的布局对象,有可能是像ScrollView、ListView或者RecyclerView之类比较复杂的控件,这些控件由于需要实现内部的滚动操作会导致用户从外部下拉时出现无法折叠的效果。这就需要在合适的实际拦截这些复杂控件的事件派发,如果复杂控件无法通过下拉展示更多的内容时做拦截将事件派发给父控件,可以参考SwipRefreshLayout的实现来判断当前的控件无法再做下拉操作。
页面折叠效果实现
View自带了判断能否竖向滚动的方法canScrollVertically(direction),其中direction为负数代表滚动条能否向上滚动,为正数代表滚动条能够向下滚动。根据图中range代表整体内容的高度,extent代表展示界面的高度,offset表示上部移动出去的高度。如果range为0代表根本无法滚动;如果direct为负数代表滚动条向上滚动,当offset > 0那么滚动条可以向上滚动;如果direct为正数代表滚动条向下滚动,当offset < remain -1那么滚动条就可以向下移动。

public boolean canScrollVertically(int direction) {
    final int offset = computeVerticalScrollOffset();
    final int remain = computeVerticalScrollRange() - computeVerticalScrollExtent();
    if (remain == 0) return false;
    if (direction < 0) {
        return offset > 0;
    } else {
        return offset < remain - 1;
    }
}

不过ListView暂时还不能对这个方法有很好的支持,需要对它做特殊判断,在ListViewCompat类中有专门负责判断ListView属性能否滚动的方法。

public static boolean canScrollList(@NonNull ListView listView, int direction) {
    if (Build.VERSION.SDK_INT >= 19) {
        // 实现和else里的代码是一样的
        return listView.canScrollList(direction);
    } else {
        // provide backport on earlier versions
        final int childCount = listView.getChildCount();
        if (childCount == 0) {
            return false;
        }

        final int firstPosition = listView.getFirstVisiblePosition();
        if (direction > 0) {
            final int lastBottom = listView.getChildAt(childCount - 1).getBottom();
            final int lastPosition = firstPosition + childCount;
            return lastPosition < listView.getCount()
                    || (lastBottom > listView.getHeight() - listView.getListPaddingBottom());
        } else {
            final int firstTop = listView.getChildAt(0).getTop();
            return firstPosition > 0 || firstTop < listView.getListPaddingTop();
        }
    }
}

折叠实现

为了能够在ViewGroup中移动它的子控件,可以使用ViewDragHelper工具类辅助拖动操作,需要注意的是当上方的展示空间被拖到固定位置再继续拖动就无法再拖动了,只需要判断clampViewPositionVertical超过固定的高度就不再改变,用户也就无法拖动了。

public class CollapsePageView extends FrameLayout {
    private View menuView;
    private View listView;
    private ViewDragHelper viewDragHelper;
    private int mDownY;
    private boolean mIsMenuOpen = false;

    public CollapsePageView(@NonNull Context context) {
        this(context, null);
    }

    public CollapsePageView(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CollapsePageView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        viewDragHelper = ViewDragHelper.create(this, new ViewDragHelper.Callback() {
            @Override
            public boolean tryCaptureView(@NonNull View child, int pointerId) {
                // 只能拖动上面的View
                return listView == child;
            }

            @Override
            public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
                return 0;
            }

            @Override
            public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
                // 如果拖动超过固定值,不再允许拖动操作
                if (top < menuView.getHeight()) {
                    return top;
                }
                return menuView.getHeight();
            }

            @Override
            public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
                super.onViewReleased(releasedChild, xvel, yvel);
                if (listView.getTop() < menuView.getHeight() / 2) {
                    viewDragHelper.settleCapturedViewAt(0, 0);
                    mIsMenuOpen = false;
                } else {
                    viewDragHelper.settleCapturedViewAt(0, menuView.getHeight());
                    mIsMenuOpen = true;
                }
                invalidate();
            }
        });
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        menuView = getChildAt(0);
        listView = getChildAt(1);
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (viewDragHelper.continueSettling(true)) {
            invalidate();
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                viewDragHelper.processTouchEvent(ev);
                mDownY = (int) ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                int y = (int) ev.getY();
                // 如果后方的View被打开或者用户向下拖动,
                if ((mIsMenuOpen || y - mDownY > 0) && !canChildScrollUp()) {
                    return true;
                }
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        viewDragHelper.processTouchEvent(event);
        return true;
    }

    // 判断当前的展示控件滚动条能否继续向上移动
    public boolean canChildScrollUp() {
        if (listView instanceof ListView) {
            return ListViewCompat.canScrollList((ListView) listView, -1);
        }
        return listView.canScrollVertically(-1);
    }
}

当用户放开手指的时候需要判断是否已经拖动超出固定高度的一半,超出就需要将后方的View整体展示出来,未超出则需要将前方的View重新放到默认的位置,这些都是ViewDragHelper的基础操作,在ViewDragHelper已经介绍地很详细了,这里不再赘述。主要看时间拦截操作,只要在ACTION_MOVE里判断上方控件滚动条无法继续向上滚动而且用户向下拖动就拦截当前事件派发操作,之后所有的MotionEvent都会发送给CollapsePageView来做处理。