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

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

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

}

}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容