美文网首页ITBOXandnroidAndroid知识
使用少量代码实现自己的RecyclerView侧滑菜单

使用少量代码实现自己的RecyclerView侧滑菜单

作者: AItsuki | 来源:发表于2016-10-28 15:40 被阅读7839次

    没有找到自己想要的效果的侧滑菜单,花了些时间研究了一下能完成项目需求就行了。效果如下:


    因为逻辑比较简单,总代码量500行左右,所以各种各样的定制都通过修改源码能实现,而且不需要继承特定的Adapter,使用方式和普通的RecyclerView没有区别。

    一. 实现一个侧滑菜单

    这里我使用DragHelper实现,支持左划和右划菜单,并且可以同时存在两个菜单。
    通过判断xml中的layout_gravity属性决定菜单是左划还是右划。

    注释应该写的都比较清楚, 部分逻辑参考了代码家的SwipeLayout

    package com.aitsuki.swipe;
    
    import android.content.Context;
    import android.graphics.Rect;
    import android.support.v4.view.GravityCompat;
    import android.support.v4.view.ViewCompat;
    import android.support.v4.widget.ViewDragHelper;
    import android.util.AttributeSet;
    import android.util.Log;
    import android.view.Gravity;
    import android.view.MotionEvent;
    import android.view.View;
    import android.view.ViewConfiguration;
    import android.view.ViewGroup;
    import android.widget.FrameLayout;
    
    import java.util.LinkedHashMap;
    
    /**
     * Created by AItsuki on 2017/2/23.
     * 1. 最多同时设置两个菜单
     * 2. 菜单必须设置layoutGravity属性. start left end right
     */
    public class SwipeItemLayout extends FrameLayout {
    
        public static final String TAG = "SwipeItemLayout";
    
        private ViewDragHelper mDragHelper;
        private int mTouchSlop;
        private int mVelocity;
    
        private float mDownX;
        private float mDownY;
        private boolean mIsDragged;
        private boolean mSwipeEnable = true;
    
        /**
         * 通过判断手势进行赋值 {@link #checkCanDragged(MotionEvent)}
         */
        private View mCurrentMenu;
    
        /**
         * 某些情况下,不能通过mIsOpen判断当前菜单是否开启或是关闭。
         * 因为在调用 {@link #open()} 或者 {@link #close()} 的时候,mIsOpen的值已经被改变,但是
         * 此时ContentView还没有到达应该的位置。亦或者ContentView已经到拖拽达指定位置,但是此时并没有
         * 松开手指,mIsOpen并不会重新赋值。
         */
        private boolean mIsOpen;
    
        /**
         * Menu的集合,以{@link android.view.Gravity#LEFT}和{@link android.view.Gravity#LEFT}作为key,
         * 菜单View作为value保存。
         */
        private LinkedHashMap<Integer, View> mMenus = new LinkedHashMap<>();
    
        public SwipeItemLayout(Context context) {
            this(context, null);
        }
    
        public SwipeItemLayout(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public SwipeItemLayout(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
            mVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
            mDragHelper = ViewDragHelper.create(this, new DragCallBack());
        }
    
        @Override
        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
            super.onLayout(changed, left, top, right, bottom);
            updateMenu();
        }
    
        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                // 关闭菜单过程中禁止接收down事件
                if (isCloseAnimating()) {
                    return false;
                }
    
                // 菜单打开的时候,按下Content关闭菜单
                if (mIsOpen && isTouchContent(((int) ev.getX()), ((int) ev.getY()))) {
                    close();
                    return false;
                }
            }
            return super.dispatchTouchEvent(ev);
        }
    
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            if (!mSwipeEnable) {
                return false;
            }
    
            int action = ev.getAction();
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    mIsDragged = false;
                    mDownX = ev.getX();
                    mDownY = ev.getY();
                    break;
                case MotionEvent.ACTION_MOVE:
                    checkCanDragged(ev);
                    break;
                case MotionEvent.ACTION_CANCEL:
                case MotionEvent.ACTION_UP:
                    if (mIsDragged) {
                        mDragHelper.processTouchEvent(ev);
                        mIsDragged = false;
                    }
                    break;
                default:
                    if (mIsDragged) {
                        mDragHelper.processTouchEvent(ev);
                    }
                    break;
            }
            return mIsDragged || super.onInterceptTouchEvent(ev);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent ev) {
            if (!mSwipeEnable) {
                return super.onTouchEvent(ev);
            }
    
            int action = ev.getAction();
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    mIsDragged = false;
                    mDownX = ev.getX();
                    mDownY = ev.getY();
                    break;
                case MotionEvent.ACTION_MOVE:
                    boolean beforeCheckDrag = mIsDragged;
                    checkCanDragged(ev);
                    if (mIsDragged) {
                        mDragHelper.processTouchEvent(ev);
                    }
    
                    // 开始拖动后,发送一个cancel事件用来取消点击效果
                    if (!beforeCheckDrag && mIsDragged) {
                        MotionEvent obtain = MotionEvent.obtain(ev);
                        obtain.setAction(MotionEvent.ACTION_CANCEL);
                        super.onTouchEvent(obtain);
                    }
    
                    break;
                case MotionEvent.ACTION_CANCEL:
                case MotionEvent.ACTION_UP:
                    if (mIsDragged || mIsOpen) {
                        mDragHelper.processTouchEvent(ev);
                        // 拖拽后手指抬起,或者已经开启菜单,不应该响应到点击事件
                        ev.setAction(MotionEvent.ACTION_CANCEL);
                        mIsDragged = false;
                    }
                    break;
            }
            return mIsDragged || super.onTouchEvent(ev);
        }
    
        /**
         * 判断是否可以拖拽View
         */
        private void checkCanDragged(MotionEvent ev) {
            if (mIsDragged) {
                return;
            }
    
            float dx = ev.getX() - mDownX;
            float dy = ev.getY() - mDownY;
            boolean isRightDrag = dx > mTouchSlop && dx > Math.abs(dy);
            boolean isLeftDrag = dx < -mTouchSlop && Math.abs(dx) > Math.abs(dy);
    
            if (mIsOpen) {
                // 开启状态下,点击在content上就捕获事件,点击在菜单上则判断touchSlop
                int downX = (int) mDownX;
                int downY = (int) mDownY;
                if (isTouchContent(downX, downY)) {
                    mIsDragged = true;
                } else if (isTouchMenu(downX, downY)) {
                    mIsDragged = (isLeftMenu() && isLeftDrag) || (isRightMenu() && isRightDrag);
                }
    
            } else {
                // 关闭状态,获取当前即将要开启的菜单。
                if (isRightDrag) {
                    mCurrentMenu = mMenus.get(Gravity.LEFT);
                    mIsDragged = mCurrentMenu != null;
                } else if (isLeftDrag) {
                    mCurrentMenu = mMenus.get(Gravity.RIGHT);
                    mIsDragged = mCurrentMenu != null;
                }
            }
    
            if (mIsDragged) {
                // 开始拖动后,分发down事件给DragHelper,并且发送一个cancel取消点击事件
                MotionEvent obtain = MotionEvent.obtain(ev);
                obtain.setAction(MotionEvent.ACTION_DOWN);
                mDragHelper.processTouchEvent(obtain);
                if (getParent() != null) {
                    // 解决和父控件的滑动冲突。
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
            }
        }
    
        // 最后一个是内容,倒数第1第2个设置了layout_gravity = right or left的是菜单,其余的忽略
        @Override
        public void addView(View child, int index, ViewGroup.LayoutParams params) {
            super.addView(child, index, params);
            LayoutParams lp = (LayoutParams) child.getLayoutParams();
            int gravity = GravityCompat.getAbsoluteGravity(lp.gravity, ViewCompat.getLayoutDirection(child));
            switch (gravity) {
                case Gravity.RIGHT:
                    mMenus.put(Gravity.RIGHT, child);
                    break;
                case Gravity.LEFT:
                    mMenus.put(Gravity.LEFT, child);
                    break;
            }
        }
    
        /**
         * 获取ContentView,最上层显示的View即为ContentView
         */
        public View getContentView() {
            return getChildAt(getChildCount() - 1);
        }
    
        /**
         * 判断down是否点击在Content上
         */
        public boolean isTouchContent(int x, int y) {
            View contentView = getContentView();
            if (contentView == null) {
                return false;
            }
            Rect rect = new Rect();
            contentView.getHitRect(rect);
            return rect.contains(x, y);
        }
    
        private boolean isLeftMenu() {
            return mCurrentMenu != null && mCurrentMenu == mMenus.get(Gravity.LEFT);
        }
    
        private boolean isRightMenu() {
            return mCurrentMenu != null && mCurrentMenu == mMenus.get(Gravity.RIGHT);
        }
    
        public boolean isTouchMenu(int x, int y) {
            if (mCurrentMenu == null) {
                return false;
            }
    
            Rect rect = new Rect();
            mCurrentMenu.getHitRect(rect);
            return rect.contains(x, y);
        }
    
        /**
         * 关闭菜单
         */
        public void close() {
            if (mCurrentMenu == null) {
                mIsOpen = false;
                return;
            }
            mDragHelper.smoothSlideViewTo(getContentView(), getPaddingLeft(), getPaddingTop());
            mIsOpen = false;
            invalidate();
        }
    
        /**
         * 开启菜单
         */
        public void open() {
            if (mCurrentMenu == null) {
                mIsOpen = false;
                return;
            }
    
            if (isLeftMenu()) {
                mDragHelper.smoothSlideViewTo(getContentView(), mCurrentMenu.getWidth(), getPaddingTop());
            } else if (isRightMenu()) {
                mDragHelper.smoothSlideViewTo(getContentView(), -mCurrentMenu.getWidth(), getPaddingTop());
            }
            mIsOpen = true;
            invalidate();
        }
    
        /**
         * 菜单是否开始拖动
         */
        public boolean isOpen() {
            return mIsOpen;
        }
    
        /**
         * 是否正在做开启动画
         */
        private boolean isOpenAnimating() {
            if (mCurrentMenu != null) {
                int contentLeft = getContentView().getLeft();
                int menuWidth = mCurrentMenu.getWidth();
                if (mIsOpen && ((isLeftMenu() && contentLeft < menuWidth)
                        || (isRightMenu() && -contentLeft < menuWidth))) {
                    return true;
                }
            }
            return false;
        }
    
        /**
         * 是否正在做关闭动画
         */
        private boolean isCloseAnimating() {
            if (mCurrentMenu != null) {
                int contentLeft = getContentView().getLeft();
                if (!mIsOpen && ((isLeftMenu() && contentLeft > 0) || (isRightMenu() && contentLeft < 0))) {
                    return true;
                }
            }
            return false;
        }
    
        /**
         * 当菜单被ContentView遮住的时候,要设置菜单为Invisible,防止已隐藏的菜单接收到点击事件。
         */
        private void updateMenu() {
            View contentView = getContentView();
            if (contentView != null) {
                int contentLeft = contentView.getLeft();
                if (contentLeft == 0) {
                    for (View view : mMenus.values()) {
                        if (view.getVisibility() != INVISIBLE) {
                            view.setVisibility(INVISIBLE);
                        }
                    }
                } else {
                    if (mCurrentMenu != null && mCurrentMenu.getVisibility() != VISIBLE) {
                        mCurrentMenu.setVisibility(VISIBLE);
                    }
                }
            }
        }
    
        @Override
        public void computeScroll() {
            super.computeScroll();
            if (mDragHelper.continueSettling(true)) {
                ViewCompat.postInvalidateOnAnimation(this);
            }
        }
    
        private class DragCallBack extends ViewDragHelper.Callback {
    
            @Override
            public boolean tryCaptureView(View child, int pointerId) {
                // menu和content都可以抓取,因为在menu的宽度为MatchParent的时候,是无法点击到content的
                return child == getContentView() || mMenus.containsValue(child);
            }
    
            @Override
            public int clampViewPositionHorizontal(View child, int left, int dx) {
    
                // 如果child是内容, 那么可以左划或右划,开启或关闭菜单
                if (child == getContentView()) {
                    if (isRightMenu()) {
                        return left > 0 ? 0 : left < -mCurrentMenu.getWidth() ?
                                -mCurrentMenu.getWidth() : left;
                    } else if (isLeftMenu()) {
                        return left > mCurrentMenu.getWidth() ? mCurrentMenu.getWidth() : left < 0 ?
                                0 : left;
                    }
                }
    
                // 如果抓取到的child是菜单,那么不移动child,而是移动contentView
                else if (isRightMenu()) {
                    View contentView = getContentView();
                    int newLeft = contentView.getLeft() + dx;
                    if (newLeft > 0) {
                        newLeft = 0;
                    } else if (newLeft < -child.getWidth()) {
                        newLeft = -child.getWidth();
                    }
                    contentView.layout(newLeft, contentView.getTop(), newLeft + contentView.getWidth(),
                            contentView.getBottom());
                    return child.getLeft();
                } else if (isLeftMenu()) {
                    View contentView = getContentView();
                    int newLeft = contentView.getLeft() + dx;
                    if (newLeft < 0) {
                        newLeft = 0;
                    } else if (newLeft > child.getWidth()) {
                        newLeft = child.getWidth();
                    }
                    contentView.layout(newLeft, contentView.getTop(), newLeft + contentView.getWidth(),
                            contentView.getBottom());
                    return child.getLeft();
                }
                return 0;
            }
    
            @Override
            public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
                super.onViewPositionChanged(changedView, left, top, dx, dy);
                updateMenu();
            }
    
            @Override
            public void onViewReleased(View releasedChild, float xvel, float yvel) {
                Log.e(TAG, "onViewReleased: " + xvel + " ,releasedChild = " + releasedChild);
                if (isLeftMenu()) {
                    if (xvel > mVelocity) {
                        open();
                    } else if (xvel < -mVelocity) {
                        close();
                    } else {
                        if (getContentView().getLeft() > mCurrentMenu.getWidth() / 3 * 2) {
                            open();
                        } else {
                            close();
                        }
                    }
                } else if (isRightMenu()) {
                    if (xvel < -mVelocity) {
                        open();
                    } else if (xvel > mVelocity) {
                        close();
                    } else {
                        if (getContentView().getLeft() < -mCurrentMenu.getWidth() / 3 * 2) {
                            open();
                        } else {
                            close();
                        }
                    }
                }
            }
    
        }
    }
    
    

    xml中的用法如下,需要通过layout_gravity指定左右菜单,最顶层的标签则是Content。

    <?xml version="1.0" encoding="utf-8"?>
    <com.aitsuki.swipe.SwipeItemLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/swipe_layout"
        android:layout_width="match_parent"
        android:layout_height="@dimen/swipe_item_height">
    
        <TextView
            android:id="@+id/left_menu"
            android:layout_width="@dimen/swipe_item_menu_width"
            android:layout_height="match_parent"
            android:layout_gravity="left"
            android:background="@color/red500"
            android:gravity="center"
            android:text="left"
            android:textColor="@color/white"/>
    
        <TextView
            android:id="@+id/right_menu"
            android:layout_width="@dimen/swipe_item_menu_width"
            android:layout_height="match_parent"
            android:layout_gravity="right"
            android:background="@color/blue500"
            android:gravity="center"
            android:text="right"
            android:textColor="@color/white"/>
    
        <include layout="@layout/swipe_content"/>
    
    </com.aitsuki.swipe.SwipeItemLayout>
    
    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="@dimen/swipe_item_height"
        android:background="?android:colorBackground"
        android:foreground="?listChoiceBackgroundIndicator">
    
        <TextView
            android:id="@+id/tv_content"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:textColor="@color/primaryText"
            tools:text="Content"/>
    
    </FrameLayout>
    

    这样,一个体验还不错的侧滑菜单就设计好了。
    当然你也可以直接使用代码家的AndroidSwipeLayout

    二、自定义RecylcerView管理SwipeItemLayout交互体验

    交互方式我参考了IOS系统的message列表,和QQ的好友列表。
    只有短短的100行代码,注释也不较多,应该看得明白。

    package com.aitsuki.swipe;
    
    import android.content.Context;
    import android.graphics.Rect;
    import android.support.annotation.Nullable;
    import android.support.v7.widget.RecyclerView;
    import android.util.AttributeSet;
    import android.view.MotionEvent;
    import android.view.View;
    import android.view.ViewGroup;
    
    /**
     * Created by AItsuki on 2017/2/23.
     * 仿IOS message列表,QQ好友列表的交互体验
     * 当有菜单打开的时候,只要不是点击在菜单上,关闭该菜单。
     * 只能同时打开一个菜单,防止多点触控打开菜单
     */
    public class SwipeMenuRecyclerView extends RecyclerView {
    
        public SwipeMenuRecyclerView(Context context) {
            super(context);
        }
    
        public SwipeMenuRecyclerView(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
        }
    
        public SwipeMenuRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
        }
    
        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            int action = ev.getActionMasked();
            // 手指按下的时候,如果有开启的菜单,只要手指不是落在该Item上,则关闭菜单, 并且不分发事件。
            if (action == MotionEvent.ACTION_DOWN) {
                int x = (int) ev.getX();
                int y = (int) ev.getY();
                View openItem = findOpenItem();
                if (openItem != null && openItem != getTouchItem(x, y)) {
                    SwipeItemLayout swipeItemLayout = findSwipeItemLayout(openItem);
                    if (swipeItemLayout != null) {
                        swipeItemLayout.close();
                        return false;
                    }
                }
            } else if (action == MotionEvent.ACTION_POINTER_DOWN) {
                // FIXME: 2017/3/22 不知道怎么解决多点触控导致可以同时打开多个菜单的bug,先暂时禁止多点触控
                return false;
            }
            return super.dispatchTouchEvent(ev);
        }
    
        /**
         * 获取按下位置的Item
         */
        @Nullable
        private View getTouchItem(int x, int y) {
            Rect frame = new Rect();
            for (int i = 0; i < getChildCount(); i++) {
                View child = getChildAt(i);
                if (child.getVisibility() == VISIBLE) {
                    child.getHitRect(frame);
                    if (frame.contains(x, y)) {
                        return child;
                    }
                }
            }
            return null;
        }
    
        /**
         * 找到当前屏幕中开启的的Item
         */
        @Nullable
        private View findOpenItem() {
            int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                SwipeItemLayout swipeItemLayout = findSwipeItemLayout(getChildAt(i));
                if (swipeItemLayout != null && swipeItemLayout.isOpen()) {
                    return getChildAt(i);
                }
            }
            return null;
        }
    
        /**
         * 获取该View
         */
        @Nullable
        private SwipeItemLayout findSwipeItemLayout(View view) {
            if (view instanceof SwipeItemLayout) {
                return (SwipeItemLayout) view;
            } else if (view instanceof ViewGroup) {
                ViewGroup group = (ViewGroup) view;
                int count = group.getChildCount();
                for (int i = 0; i < count; i++) {
                    SwipeItemLayout swipeLayout = findSwipeItemLayout(group.getChildAt(i));
                    if (swipeLayout != null) {
                        return swipeLayout;
                    }
                }
            }
            return null;
        }
    
    }
    

    三、GIF上演示的Demo

    不需要实现或继承特定的Adapter,使用方式和普通RecyclerView没有区别。
    这个项目我可能会慢慢完善下去,但是这篇文章不会跟着更新了,毕竟只是一种实现思路,具体的还得看github上的。
    GitHub:SwipeMenuRecyclerView

    相关文章

      网友评论

        本文标题:使用少量代码实现自己的RecyclerView侧滑菜单

        本文链接:https://www.haomeiwen.com/subject/swasuttx.html