使用少量代碼實現(xiàn)自己的RecyclerView側(cè)滑菜單

沒有找到自己想要的效果的側(cè)滑菜單主到,花了些時間研究了一下能完成項目需求就行了慰技。效果如下:

因為邏輯比較簡單构诚,總代碼量500行左右,所以各種各樣的定制都通過修改源碼能實現(xiàn)呵俏,而且不需要繼承特定的Adapter堆缘,使用方式和普通的RecyclerView沒有區(qū)別。

一. 實現(xiàn)一個側(cè)滑菜單

這里我使用DragHelper實現(xiàn)普碎,支持左劃和右劃菜單吼肥,并且可以同時存在兩個菜單。

通過判斷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判斷當前菜單是否開啟或是關(guān)閉啤斗。

* 因為在調(diào)用 {@link #open()} 或者 {@link #close()} 的時候表箭,mIsOpen的值已經(jīng)被改變,但是

* 此時ContentView還沒有到達應該的位置钮莲。亦或者ContentView已經(jīng)到拖拽達指定位置免钻,但是此時并沒有

* 松開手指,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) {

// 關(guān)閉菜單過程中禁止接收down事件

if (isCloseAnimating()) {

return false;

}

// 菜單打開的時候,按下Content關(guān)閉菜單

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);

}

// 開始拖動后叛赚,發(fā)送一個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);

// 拖拽后手指抬起澡绩,或者已經(jīng)開啟菜單,不應該響應到點擊事件

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) {

// 開啟狀態(tài)下俺附,點擊在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 {

// 關(guān)閉狀態(tài),獲取當前即將要開啟的菜單事镣。

if (isRightDrag) {

mCurrentMenu = mMenus.get(Gravity.LEFT);

mIsDragged = mCurrentMenu != null;

} else if (isLeftDrag) {

mCurrentMenu = mMenus.get(Gravity.RIGHT);

mIsDragged = mCurrentMenu != null;

}

}

if (mIsDragged) {

// 開始拖動后步鉴,分發(fā)down事件給DragHelper,并且發(fā)送一個cancel取消點擊事件

MotionEvent obtain = MotionEvent.obtain(ev);

obtain.setAction(MotionEvent.ACTION_DOWN);

mDragHelper.processTouchEvent(obtain);

if (getParent() != null) {

// 解決和父控件的滑動沖突璃哟。

getParent().requestDisallowInterceptTouchEvent(true);

}

}

}

// 最后一個是內(nèi)容氛琢,倒數(shù)第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);

}

/**

* 關(guān)閉菜單

*/

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;

}

/**

* 是否正在做關(guān)閉動畫

*/

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是內(nèi)容当宴, 那么可以左劃或右劃畜吊,開啟或關(guān)閉菜單

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

這樣青自,一個體驗還不錯的側(cè)滑菜單就設計好了株依。

當然你也可以直接使用代碼家的AndroidSwipeLayout

二、自定義RecylcerView管理SwipeItemLayout交互體驗

交互方式我參考了IOS系統(tǒng)的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好友列表的交互體驗

* 當有菜單打開的時候获高,只要不是點擊在菜單上哈肖,關(guān)閉該菜單。

* 只能同時打開一個菜單念秧,防止多點觸控打開菜單

*/

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上摊趾,則關(guān)閉菜單, 并且不分發(fā)事件币狠。

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;

}

}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末砾层,一起剝皮案震驚了整個濱河市漩绵,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌肛炮,老刑警劉巖止吐,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異侨糟,居然都是意外死亡碍扔,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門粟害,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蕴忆,“玉大人,你說我怎么就攤上這事悲幅√锥欤” “怎么了?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵汰具,是天一觀的道長卓鹿。 經(jīng)常有香客問我,道長留荔,這世上最難降的妖魔是什么吟孙? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任澜倦,我火速辦了婚禮,結(jié)果婚禮上杰妓,老公的妹妹穿的比我還像新娘藻治。我一直安慰自己,他們只是感情好巷挥,可當我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布桩卵。 她就那樣靜靜地躺著,像睡著了一般倍宾。 火紅的嫁衣襯著肌膚如雪雏节。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天高职,我揣著相機與錄音钩乍,去河邊找鬼。 笑死怔锌,一個胖子當著我的面吹牛寥粹,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播埃元,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼排作,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了亚情?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤哈雏,失蹤者是張志新(化名)和其女友劉穎楞件,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體裳瘪,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡土浸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了彭羹。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片黄伊。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖派殷,靈堂內(nèi)的尸體忽然破棺而出还最,到底是詐尸還是另有隱情,我是刑警寧澤毡惜,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布拓轻,位于F島的核電站,受9級特大地震影響经伙,放射性物質(zhì)發(fā)生泄漏扶叉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望枣氧。 院中可真熱鬧溢十,春花似錦、人聲如沸达吞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宗挥。三九已至乌庶,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間契耿,已是汗流浹背瞒大。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留搪桂,地道東北人透敌。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像踢械,于是被迫代替她去往敵國和親酗电。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容