Github: SwipeMenuListView 分析版本:d1cb862

SwipeMenuListView 是一个像 Android QQ 那样在 ListView 中拉出菜单的开源库。

SwipeMenuListView

SwipeMenuListView

SwipeMenuListView 是一个很棒的 ListView 控件,但是现在作者已经没有维护了。

使用

添加依赖

1
2
3
dependencies {
compile 'com.baoyz.swipemenulistview:library:1.3.0'
}

Step 1

添加 SwipeMenuListView 到 layout 布局中

1
2
3
4
<com.baoyz.swipemenulistview.SwipeMenuListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent" />

Step 2

创建 SwipeMenuCreator 并添加 items

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
SwipeMenuCreator creator = new SwipeMenuCreator() {

@Override
public void create(SwipeMenu menu) {
// create "open" item
SwipeMenuItem openItem = new SwipeMenuItem(
getApplicationContext());
// set item background
openItem.setBackground(new ColorDrawable(Color.rgb(0xC9, 0xC9,
0xCE)));
// set item width
openItem.setWidth(dp2px(90));
// set item title
openItem.setTitle("Open");
// set item title fontsize
openItem.setTitleSize(18);
// set item title font color
openItem.setTitleColor(Color.WHITE);
// add to menu
menu.addMenuItem(openItem);

// create "delete" item
SwipeMenuItem deleteItem = new SwipeMenuItem(
getApplicationContext());
// set item background
deleteItem.setBackground(new ColorDrawable(Color.rgb(0xF9,
0x3F, 0x25)));
// set item width
deleteItem.setWidth(dp2px(90));
// set a icon
deleteItem.setIcon(R.drawable.ic_delete);
// add to menu
menu.addMenuItem(deleteItem);
}
};

// set creator
listView.setMenuCreator(creator);

Step 3

Menu 的 Click 监听器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
listView.setOnMenuItemClickListener(new OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(int position, SwipeMenu menu, int index) {
switch (index) {
case 0:
// open
break;
case 1:
// delete
break;
}
// false : close the menu; true : not close the menu
return false;
}
});

滑动方向

1
2
3
4
5
// Right
mListView.setSwipeDirection(SwipeMenuListView.DIRECTION_RIGHT);

// Left
mListView.setSwipeDirection(SwipeMenuListView.DIRECTION_LEFT);

创建不同的 Menu

利用 Adapter 中的 ViewType

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class AppAdapter extends BaseAdapter {

...

@Override
public int getViewTypeCount() {
// menu type count
return 2;
}

@Override
public int getItemViewType(int position) {
// current menu type
return type;
}

...
}

通过 view type 来创建不同的 menus

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SwipeMenuCreator creator = new SwipeMenuCreator() {

@Override
public void create(SwipeMenu menu) {
// Create different menus depending on the view type
switch (menu.getViewType()) {
case 0:
// create menu of type 0
break;
case 1:
// create menu of type 1
break;
...
}
}

};

其他

OnSwipeListener

1
2
3
4
5
6
7
8
9
10
11
12
listView.setOnSwipeListener(new OnSwipeListener() {

@Override
public void onSwipeStart(int position) {
// swipe start
}

@Override
public void onSwipeEnd(int position) {
// swipe end
}
});

平滑打开 menu

1
listView.smoothOpenMenu(position);

打开或者关闭 menu 的动画插值器

1
2
3
4
// Close Interpolator
listView.setCloseInterpolator(new BounceInterpolator());
// Open Interpolator
listView.setOpenInterpolator(...);

源码

SwipeMenuListView 0x00

先从 SwipeMenuListView 开始看,从构造函数和常用 API 开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class SwipeMenuListView extends ListView {
private static final int TOUCH_STATE_NONE = 0;
private static final int TOUCH_STATE_X = 1;
private static final int TOUCH_STATE_Y = 2;

private int MAX_Y = 5;
private int MAX_X = 3;
private int mTouchState;

public SwipeMenuListView(Context context) {
super(context);
init();
}

public SwipeMenuListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}

public SwipeMenuListView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}

private void init() {
MAX_X = dp2px(MAX_X);
MAX_Y = dp2px(MAX_Y);
mTouchState = TOUCH_STATE_NONE;
}

@Override
public void setAdapter(ListAdapter adapter) {
super.setAdapter(new SwipeMenuAdapter(getContext(), adapter) {
@Override
public void createMenu(SwipeMenu menu) {
if (mMenuCreator != null) {
mMenuCreator.create(menu);
}
}

@Override
public void onItemClick(SwipeMenuView view, SwipeMenu menu,
int index)
{

boolean flag = false;
if (mOnMenuItemClickListener != null) {
flag = mOnMenuItemClickListener.onMenuItemClick(
view.getPosition(), menu, index);
}
if (mTouchView != null && !flag) {
mTouchView.smoothCloseMenu();
}
}
});
}
}

从构造器中看不出来什么,只是进行了初始化操作,以及有一个手势的状态机。当看到 ListView#setAdapter(ListAdapter) 的时候,发现实际设置进去的 Adapter 外面还包了一层 SwipeMenuAdapter ,那么来看看 SwipeMenuAdapter

SwipeMenuAdapter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
public class SwipeMenuAdapter implements WrapperListAdapter,
SwipeMenuView.OnSwipeItemClickListener {


private ListAdapter mAdapter;
private Context mContext;
private SwipeMenuListView.OnMenuItemClickListener onMenuItemClickListener;

public SwipeMenuAdapter(Context context, ListAdapter adapter) {
mAdapter = adapter;
mContext = context;
}

//WrapperListAdapter中方法
@Override
public int getCount() {
return mAdapter.getCount();
}

//WrapperListAdapter中方法
@Override
public Object getItem(int position) {
return mAdapter.getItem(position);
}

//WrapperListAdapter中方法
@Override
public long getItemId(int position) {
return mAdapter.getItemId(position);
}

//WrapperListAdapter中方法
@Override
public View getView(int position, View convertView, ViewGroup parent) {
SwipeMenuLayout layout = null;
if (convertView == null) {//刚初始化的时候
//调用adapter的getView得到用户返回的View
View contentView = mAdapter.getView(position, convertView, parent);
//new一个SwipeMenu
SwipeMenu menu = new SwipeMenu(mContext);
//设置当前的ViewType
menu.setViewType(mAdapter.getItemViewType(position));
//向SwipeMenu中添加SwipeMenuItem
createMenu(menu);
//menu的View
SwipeMenuView menuView = new SwipeMenuView(menu,
(SwipeMenuListView) parent);
//设置监听器
menuView.setOnSwipeItemClickListener(this);
//SwipeMenuListView
SwipeMenuListView listView = (SwipeMenuListView) parent;
//整个item,SwipeMenuLayout
layout = new SwipeMenuLayout(contentView, menuView,
listView.getCloseInterpolator(),
listView.getOpenInterpolator());
//设置position位置
layout.setPosition(position);
} else {
layout = (SwipeMenuLayout) convertView;
//关闭menu
layout.closeMenu();
//设置position位置
layout.setPosition(position);
//调用adapter的getView,将用户的View传递出去
View view = mAdapter.getView(position, layout.getContentView(),
parent);
}
return layout;
}

public void createMenu(SwipeMenu menu) {
// Test Code
SwipeMenuItem item = new SwipeMenuItem(mContext);
item.setTitle("Item 1");
item.setBackground(new ColorDrawable(Color.GRAY));
item.setWidth(300);
menu.addMenuItem(item);

item = new SwipeMenuItem(mContext);
item.setTitle("Item 2");
item.setBackground(new ColorDrawable(Color.RED));
item.setWidth(300);
menu.addMenuItem(item);
}

@Override
public void onItemClick(SwipeMenuView view, SwipeMenu menu, int index) {
if (onMenuItemClickListener != null) {
onMenuItemClickListener.onMenuItemClick(view.getPosition(), menu,
index);
}
}

public void setOnMenuItemClickListener(
SwipeMenuListView.OnMenuItemClickListener onMenuItemClickListener)
{

this.onMenuItemClickListener = onMenuItemClickListener;
}

//WrapperListAdapter中方法
@Override
public void registerDataSetObserver(DataSetObserver observer) {
mAdapter.registerDataSetObserver(observer);
}

//WrapperListAdapter中方法
@Override
public void unregisterDataSetObserver(DataSetObserver observer) {
mAdapter.unregisterDataSetObserver(observer);
}

//WrapperListAdapter中方法
@Override
public boolean areAllItemsEnabled() {
return mAdapter.areAllItemsEnabled();
}

//WrapperListAdapter中方法
@Override
public boolean isEnabled(int position) {
return mAdapter.isEnabled(position);
}

//WrapperListAdapter中方法
@Override
public boolean hasStableIds() {
return mAdapter.hasStableIds();
}

//WrapperListAdapter中方法
@Override
public int getItemViewType(int position) {
return mAdapter.getItemViewType(position);
}

//WrapperListAdapter中方法
@Override
public int getViewTypeCount() {
return mAdapter.getViewTypeCount();
}

//WrapperListAdapter中方法
@Override
public boolean isEmpty() {
return mAdapter.isEmpty();
}

//WrapperListAdapter中方法
@Override
public ListAdapter getWrappedAdapter() {
return mAdapter;
}
}

SwipeMenuAdapter 实现了 WrapperListAdapter ,而 WrapperListAdapter 的父类是 ListAdapter ,也就是说 WrapperListAdapter 是一个 ListAdapter 包装类。那么我们这里可以将这个类看简单点,可以通过看一个 BaseAdapter 来看这个类,也就是我们需要关心的是 getView()getItemId()getItem()getCounts() ,而在 SwipeMenuAdapter 中可以看出在 getView() 中的操作比较多。

getView() 中首先判断参数 convertView 是不是为 null ,如果是,那么 new 出一个自己 SwipeMenuLayout 出来,包括用户的 item view 和 menu view,然后返回;如果不为 null ,那么参数 convertView 应该是 SwipeMenuLayout ,通过调用 adapter.getView() 将 用户的 item view 传递给用户。

那么我们先来看看 Menu 的 View :

SwipeMenuView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
public class SwipeMenuView extends LinearLayout implements OnClickListener {

private SwipeMenuListView mListView;//代码中没有使用到
private SwipeMenuLayout mLayout;
private SwipeMenu mMenu;
private OnSwipeItemClickListener onItemClickListener;
private int position;

public int getPosition() {
return position;
}

public void setPosition(int position) {
this.position = position;
}

public SwipeMenuView(SwipeMenu menu, SwipeMenuListView listView) {
super(menu.getContext());
mListView = listView;
mMenu = menu;
//获得传入的Menu中的MenuItem
List<SwipeMenuItem> items = menu.getMenuItems();
int id = 0;
//通过item构造出View添加到SwipeMenuView中
for (SwipeMenuItem item : items) {
addItem(item, id++);
}
}

/**
* 将 MenuItem 转换成 UI控件
*/

private void addItem(SwipeMenuItem item, int id) {
LayoutParams params = new LayoutParams(item.getWidth(),
LayoutParams.MATCH_PARENT);
LinearLayout parent = new LinearLayout(getContext());
parent.setId(id);
parent.setGravity(Gravity.CENTER);
parent.setOrientation(LinearLayout.VERTICAL);
parent.setLayoutParams(params);
parent.setBackgroundDrawable(item.getBackground());
parent.setOnClickListener(this);
addView(parent);

if (item.getIcon() != null) {
parent.addView(createIcon(item));
}
if (!TextUtils.isEmpty(item.getTitle())) {
parent.addView(createTitle(item));
}

}

/**
* 创建图片
*/

private ImageView createIcon(SwipeMenuItem item) {
ImageView iv = new ImageView(getContext());
iv.setImageDrawable(item.getIcon());
return iv;
}

/**
* 创建文字
*/

private TextView createTitle(SwipeMenuItem item) {
TextView tv = new TextView(getContext());
tv.setText(item.getTitle());
tv.setGravity(Gravity.CENTER);
tv.setTextSize(item.getTitleSize());
tv.setTextColor(item.getTitleColor());
return tv;
}

//点击事件
@Override
public void onClick(View v) {
//menu滑开的时候才算点击
if (onItemClickListener != null && mLayout.isOpen()) {
onItemClickListener.onItemClick(this, mMenu, v.getId());
}
}

public OnSwipeItemClickListener getOnSwipeItemClickListener() {
return onItemClickListener;
}

public void setOnSwipeItemClickListener(OnSwipeItemClickListener onItemClickListener) {
this.onItemClickListener = onItemClickListener;
}

/**
* 设置SwipeMenuLayout
*/

public void setLayout(SwipeMenuLayout mLayout) {
this.mLayout = mLayout;
}

public static interface OnSwipeItemClickListener {
void onItemClick(SwipeMenuView view, SwipeMenu menu, int index);
}
}

SwipeMenuView 继承 LinearLayout ,然后在子 View 是很多个 LinearLayout ,而这每个子 View 中的子子 View 就是 各个 Menu Item 所转换出来的 UI 控件。需要注意的是 LayoutParams params = new LayoutParams(item.getWidth(), LayoutParams.MATCH_PARENT); ,设置的宽度是通过 Menu Item 定了的。

SwipeMenuLayout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
public class SwipeMenuLayout extends FrameLayout {
private View mContentView;
private SwipeMenuView mMenuView;

private Interpolator mCloseInterpolator;
private Interpolator mOpenInterpolator;

//手势
private OnGestureListener mGestureListener;
private GestureDetectorCompat mGestureDetector;

private ScrollerCompat mOpenScroller;//滑开的scroller
private ScrollerCompat mCloseScroller;//关闭的scroller

private boolean isFling;//手指满足是否滑动的标志位
private int MIN_FLING = dp2px(15);//手指最小移动距离,大于这个距离可能会算作滑动
private int MAX_VELOCITYX = -dp2px(500);//X轴方向手指滑动速度

public SwipeMenuLayout(View contentView, SwipeMenuView menuView) {
this(contentView, menuView, null, null);
}

public SwipeMenuLayout(View contentView, SwipeMenuView menuView,
Interpolator closeInterpolator, Interpolator openInterpolator)
{

super(contentView.getContext());
mCloseInterpolator = closeInterpolator;
mOpenInterpolator = openInterpolator;
mContentView = contentView;
mMenuView = menuView;
//将SwipeMenuLayout设置给SwipeMenuView
mMenuView.setLayout(this);
init();
}

private SwipeMenuLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}

private SwipeMenuLayout(Context context) {
super(context);
}

private void init() {
//设置改控件的宽度match_parent,高度wrap_content
setLayoutParams(new AbsListView.LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.WRAP_CONTENT));
//Simple Gesture
mGestureListener = new SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
isFling = false;
return true;
}

//滑动
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2,
float velocityX, float velocityY)
{

//判断滑动的距离是否大于MIN_FLING 以及X轴方向的速度是否小于MAX_VELOCITYX(MAX_VELOCITYX是负值)
if (Math.abs(e1.getX() - e2.getX()) > MIN_FLING
&& velocityX < MAX_VELOCITYX) {
isFling = true;
}
return super.onFling(e1, e2, velocityX, velocityY);
}
};
mGestureDetector = new GestureDetectorCompat(getContext(),
mGestureListener);
//new Scroller
if (mCloseInterpolator != null) {
mCloseScroller = ScrollerCompat.create(getContext(),
mCloseInterpolator);
} else {
mCloseScroller = ScrollerCompat.create(getContext());
}
if (mOpenInterpolator != null) {
mOpenScroller = ScrollerCompat.create(getContext(),
mOpenInterpolator);
} else {
mOpenScroller = ScrollerCompat.create(getContext());
}

//设置layoutParams
LayoutParams contentParams = new LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
mContentView.setLayoutParams(contentParams);
//设置ID
if (mContentView.getId() < 1) {
mContentView.setId(CONTENT_VIEW_ID);
}
//设置layoutParams和id
mMenuView.setId(MENU_VIEW_ID);
mMenuView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT));
//添加到SwipeMenuLayout中
addView(mContentView);
addView(mMenuView);
}

private int dp2px(int dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
getContext().getResources().getDisplayMetrics());
}
}

SwipeMenuLayout 是一个 FrameLayout ,两个子 View ,分别是用户的 item View 和 menu View 。手指的时候滑动的操作是通过 SimpleOnGestureListener 来完成的。

继续看这个 SwipeMenuLayout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class SwipeMenuLayout extends FrameLayout {
private int mSwipeDirection;

public void setSwipeDirection(int swipeDirection) {
mSwipeDirection = swipeDirection;
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//measure SwipeMenuView 高传的是getMeasuredHeight()且MeasureSpec.EXACTLY
mMenuView.measure(MeasureSpec.makeMeasureSpec(0,
MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(
getMeasuredHeight(), MeasureSpec.EXACTLY));
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mContentView.layout(0, 0, getMeasuredWidth(),
mContentView.getMeasuredHeight());
//通过方向来判断将SwipeMenuView放在哪个位置,而且这些位置都是在屏幕外边的位置
if (mSwipeDirection == SwipeMenuListView.DIRECTION_LEFT) {
mMenuView.layout(getMeasuredWidth(), 0,
getMeasuredWidth() + mMenuView.getMeasuredWidth(),
mContentView.getMeasuredHeight());
} else {
mMenuView.layout(-mMenuView.getMeasuredWidth(), 0,
0, mContentView.getMeasuredHeight());
}
}

}

通过 SwipeMenuLayoutonMeasure()SwipeMenuView 传递一个确切的高度,然后在 onLayout() 中将 SwipeMenuView 通过方向放在方向对应的屏幕外边的位置。

接下来看看 SwipeMenuLayout 是怎么滑动的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
public class SwipeMenuLayout extends FrameLayout {

private static final int STATE_CLOSE = 0;
private static final int STATE_OPEN = 1;

private int state = STATE_CLOSE;

private int mDownX;
private int mBaseX;

//这是一个对外暴露的API,而调用这个API的是SwipeMenuListView,那么MotionEvent是SwipeMenuListView的MotionEvent
public boolean onSwipe(MotionEvent event) {
//Gesture的判断
mGestureDetector.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//记录down的X坐标
mDownX = (int) event.getX();
//滑动标志位置为false
isFling = false;
break;
case MotionEvent.ACTION_MOVE:
//计算手指滑动距离
int dis = (int) (mDownX - event.getX());
if (state == STATE_OPEN) {
//DIRECTION_LEFT = 1 || DIRECTION_RIGHT = -1
dis += mMenuView.getWidth()*mSwipeDirection;;
}
swipe(dis);
break;
case MotionEvent.ACTION_UP:
//滑动状态 && 滑开距离 > SwipeMenuView / 2 && 滑动方向 == mSwipeDirection
if ((isFling || Math.abs(mDownX - event.getX()) > (mMenuView.getWidth() / 2)) && Math.signum(mDownX - event.getX()) == mSwipeDirection) {
//滑开这个menu
smoothOpenMenu();
} else {
//关闭这个menu
smoothCloseMenu();
return false;
}
break;
}
return true;
}

private void swipe(int dis) {
//通过dis的正负 与 mSwipeDirection 对比相同与否
if (Math.signum(dis) != mSwipeDirection) {
//不相同的话dis归0,表示不滑动
dis = 0;
} else if (Math.abs(dis) > mMenuView.getWidth()) {//此时说明相同
//如果dis大于了SwipeMenuView的宽度的话,将dis设置为SwipeMenuView的宽度
dis = mMenuView.getWidth()*mSwipeDirection;
}
//用户的View移动,腾出位置
mContentView.layout(-dis, mContentView.getTop(),
mContentView.getWidth() -dis, getMeasuredHeight());
//通过方向移动SwipeMenuView
if (mSwipeDirection == SwipeMenuListView.DIRECTION_LEFT) {

mMenuView.layout(mContentView.getWidth() - dis, mMenuView.getTop(),
mContentView.getWidth() + mMenuView.getWidth() - dis,
mMenuView.getBottom());
} else {
mMenuView.layout(-mMenuView.getWidth() - dis, mMenuView.getTop(),
- dis, mMenuView.getBottom());
}
}

//顺滑的滑开menu
public void smoothOpenMenu() {
//将状态设置为STATE_OPEN
state = STATE_OPEN;
//通过方向来判断需要往那个方向滑
if (mSwipeDirection == SwipeMenuListView.DIRECTION_LEFT) {
mOpenScroller.startScroll(-mContentView.getLeft(), 0, mMenuView.getWidth(), 0, 350);
} else {
mOpenScroller.startScroll(mContentView.getLeft(), 0, mMenuView.getWidth(), 0, 350);
}
//刷新
postInvalidate();
}

//顺滑的关闭menu
public void smoothCloseMenu() {
//将状态设置为STATE_CLOSE
state = STATE_CLOSE;
if (mSwipeDirection == SwipeMenuListView.DIRECTION_LEFT) {
//设置baseX
mBaseX = -mContentView.getLeft();
mCloseScroller.startScroll(0, 0, mMenuView.getWidth(), 0, 350);
} else {
mBaseX = mMenuView.getRight();
//设置baseX
mCloseScroller.startScroll(0, 0, mMenuView.getWidth(), 0, 350);
}
postInvalidate();
}

//直接关闭menu
public void closeMenu() {
//如果Scroller还没有滑完,就阻断滑动动画
if (mCloseScroller.computeScrollOffset()) {
mCloseScroller.abortAnimation();
}
//如果状态为STATE_OPEN
if (state == STATE_OPEN) {
//将状态设置为STATE_CLOSE
state = STATE_CLOSE;
//调用swipe()-->调用layout()
swipe(0);
}
}

//scroll开始滑动的时候就进到了这里
@Override
public void computeScroll() {
if (state == STATE_OPEN) {
if (mOpenScroller.computeScrollOffset()) {
//调用swipe()-->调用layout()
swipe(mOpenScroller.getCurrX()*mSwipeDirection);
postInvalidate();
}
} else {
if (mCloseScroller.computeScrollOffset()) {
//通过mBaseX的值来计算滑动
swipe((mBaseX - mCloseScroller.getCurrX())*mSwipeDirection);
postInvalidate();
}
}
}
}

SwipeMenuLayout 通过 Scroller 达到顺滑的打开和关闭,同时 Scroller 每次计算出滑动的值的时候传递给 swipe(int) 方法,该方法通过 View#layout(...) 方法实现位置的变换。

好,那么看完了 SwipeMenuLayout ,回过头来再看看 SwipeMenuListView

SwipeMenuListView 0x01

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
public class SwipeMenuListView extends ListView {
private static final int TOUCH_STATE_NONE = 0;
private static final int TOUCH_STATE_X = 1;
private static final int TOUCH_STATE_Y = 2;

public static final int DIRECTION_LEFT = 1;
public static final int DIRECTION_RIGHT = -1;
private int mDirection = 1;//swipe from right to left by default

private int mTouchState;
private int mTouchPosition;
private SwipeMenuLayout mTouchView;

private float mDownX;
private float mDownY;

@Override
public boolean onTouchEvent(MotionEvent ev) {
//处理一些有时候不希望用户有操作,或者关闭menu的时候的手势操作等
if (ev.getAction() != MotionEvent.ACTION_DOWN && mTouchView == null)
return super.onTouchEvent(ev);
//得到action
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
//之前的点击位置
int oldPos = mTouchPosition;
mDownX = ev.getX();//记录X
mDownY = ev.getY();//记录Y
mTouchState = TOUCH_STATE_NONE;
//得到新的手指的位置
mTouchPosition = pointToPosition((int) ev.getX(), (int) ev.getY());
//如果是同一位置 && mTouchView != null && mTouchView 滑开
if (mTouchPosition == oldPos && mTouchView != null
&& mTouchView.isOpen()) {
//将mTouchState置为TOUCH_STATE_X
mTouchState = TOUCH_STATE_X;
//剩下的交给SwipeMenuLayout来处理
mTouchView.onSwipe(ev);
return true;
}
//得到当前手指的 item View
View view = getChildAt(mTouchPosition - getFirstVisiblePosition());
//这里判断是指当前手指对应的view与mTouchView不是同一个,且mTouchView是滑开的状态,那么就去处理mTouchView,将menu关闭,然后结束这次手势操作
if (mTouchView != null && mTouchView.isOpen()) {
//关闭menu
mTouchView.smoothCloseMenu();
//置为null
mTouchView = null;
// return super.onTouchEvent(ev);
// try to cancel the touch event
MotionEvent cancelEvent = MotionEvent.obtain(ev);
cancelEvent.setAction(MotionEvent.ACTION_CANCEL);
onTouchEvent(cancelEvent);
//进行close回调
if (mOnMenuStateChangeListener != null) {
mOnMenuStateChangeListener.onMenuClose(oldPos);
}
return true;
}
if (view instanceof SwipeMenuLayout) {
//将最新的view传递给mTouchView
mTouchView = (SwipeMenuLayout) view;
//设置方向
mTouchView.setSwipeDirection(mDirection);
}
if (mTouchView != null) {
//剩下的交给SwipeMenuLayout来处理
mTouchView.onSwipe(ev);
}
break;
case MotionEvent.ACTION_MOVE:
//计算x和y滑动了多少距离
float dy = Math.abs((ev.getY() - mDownY));
float dx = Math.abs((ev.getX() - mDownX));
//当mTouchState为TOUCH_STATE_X时候
if (mTouchState == TOUCH_STATE_X) {
//交给SwipeMenuLayout处理移动的操作
if (mTouchView != null) {
mTouchView.onSwipe(ev);
}
//将item的drawable设置为normal的
getSelector().setState(new int[]{0});
//让ListView对此次操作不做任何处理
ev.setAction(MotionEvent.ACTION_CANCEL);
super.onTouchEvent(ev);
return true;
} else if (mTouchState == TOUCH_STATE_NONE) {//当mTouchState为TOUCH_STATE_NONE时候
if (Math.abs(dy) > MAX_Y) {//计算Y上的距离,Y上是否有动作
mTouchState = TOUCH_STATE_Y;
} else if (dx > MAX_X) {//计算X上的距离,X上是否有动作
mTouchState = TOUCH_STATE_X;
//回调
if (mOnSwipeListener != null) {
mOnSwipeListener.onSwipeStart(mTouchPosition);
}
}
}
break;
case MotionEvent.ACTION_UP:
//X上有动作
if (mTouchState == TOUCH_STATE_X) {
if (mTouchView != null) {
//是否滑开
boolean isBeforeOpen = mTouchView.isOpen();
//让SwipeMenuLayout处理
mTouchView.onSwipe(ev);
//是否滑开,因为手指抬起的时候SwipeMenuLayout回去判断是否满足调教而开启和关闭
boolean isAfterOpen = mTouchView.isOpen();
//根据之前和之后的滑开状态来进行回调
if (isBeforeOpen != isAfterOpen && mOnMenuStateChangeListener != null) {
if (isAfterOpen) {
mOnMenuStateChangeListener.onMenuOpen(mTouchPosition);
} else {
mOnMenuStateChangeListener.onMenuClose(mTouchPosition);
}
}
if (!isAfterOpen) {
mTouchPosition = -1;
mTouchView = null;
}
}
if (mOnSwipeListener != null) {
mOnSwipeListener.onSwipeEnd(mTouchPosition);
}
//希望ListView不处理接下来的手势操作
ev.setAction(MotionEvent.ACTION_CANCEL);
super.onTouchEvent(ev);
return true;
}
break;
}
return super.onTouchEvent(ev);
}
}

MotionEvent.ACTION_DOWN 中有一段是 if (mTouchView != null && mTouchView.isOpen()) {...} ,这里的判断是指当前手指对应的 view 与 mTouchView 不是同一个的话,且 mTouchView 是滑开的状态,那么就去处理 mTouchView ,将 menu 关闭,然后结束这次手势操作,这里可以发现结束手势操作之后,手指不离开屏幕的话也是无法滑动的,因为就是一开始的 if (ev.getAction() != MotionEvent.ACTION_DOWN && mTouchView == null) ,满足条件就会调用 return super.onTouchEvent(ev); ,而 ev 的 action 一直是 CANCEL 。

MotionEvent.ACTION_MOVEmTouchView.onSwipe(ev) 之后 ev.setAction(MotionEvent.ACTION_CANCEL); super.onTouchEvent(ev); 是因为 SwipeMenuLayout 移动位置的时候,手指可能在 Y 轴上的动作也比较大,此时让 ListView 忽略,直接穿的 action 为 CANCEL ,这样 ListView 就不会因为在 Y 轴上有动作而滑动。

SwipeListView 核心部分就分析完了。

SwipeMenu

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class SwipeMenu {

private Context mContext;
private List<SwipeMenuItem> mItems;
private int mViewType;

public SwipeMenu(Context context) {
mContext = context;
mItems = new ArrayList<SwipeMenuItem>();
}

public Context getContext() {
return mContext;
}

public void addMenuItem(SwipeMenuItem item) {
mItems.add(item);
}

public void removeMenuItem(SwipeMenuItem item) {
mItems.remove(item);
}

public List<SwipeMenuItem> getMenuItems() {
return mItems;
}

public SwipeMenuItem getMenuItem(int index) {
return mItems.get(index);
}

public int getViewType() {
return mViewType;
}

public void setViewType(int viewType) {
this.mViewType = viewType;
}

}

之前代码里面也出现过,这里把这部分数据结构拿出来。

SwipeMenuItem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
public class SwipeMenuItem {

private int id;
private Context mContext;
private String title;
private Drawable icon;
private Drawable background;
private int titleColor;
private int titleSize;
private int width;

public SwipeMenuItem(Context context) {
mContext = context;
}

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public int getTitleColor() {
return titleColor;
}

public int getTitleSize() {
return titleSize;
}

public void setTitleSize(int titleSize) {
this.titleSize = titleSize;
}

public void setTitleColor(int titleColor) {
this.titleColor = titleColor;
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public void setTitle(int resId) {
setTitle(mContext.getString(resId));
}

public Drawable getIcon() {
return icon;
}

public void setIcon(Drawable icon) {
this.icon = icon;
}

public void setIcon(int resId) {
this.icon = mContext.getResources().getDrawable(resId);
}

public Drawable getBackground() {
return background;
}

public void setBackground(Drawable background) {
this.background = background;
}

public void setBackground(int resId) {
this.background = mContext.getResources().getDrawable(resId);
}

public int getWidth() {
return width;
}

public void setWidth(int width) {
this.width = width;
}

}

之前代码里面也出现过,这里把这部分数据结构拿出来。

总结

  • SwipeListView 手势那部分的 ACTION_CANCEL 很经典!
  • ListView 的手势操作太复杂,没有通过 onInterceptTouchEvent 来拦截事件,转而是将 Event 传递给 item View ,让 item View 去操作
  • View 的移动是通过 View.layout(...) 来实现的
  • 平滑的移动是通过 Scroller 来实现的