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

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

作者: Kerry202 | 来源:发表于2017-11-21 16:25 被阅读147次

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

    因为逻辑比较简单,总代码量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 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。

    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">

    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"/>

    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"/>

    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">

    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"/>

    这样,一个体验还不错的侧滑菜单就设计好了。

    当然你也可以直接使用代码家的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;

    }

    }

    相关文章

      网友评论

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

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