ViewDragHelper使用笔记及侧滑菜单实践

ViewDragHelper

一个拖拽实现的帮助类,存在于v4包中,对于实现简单的拖拽简直不要太简单;再也不用去重写onTouch()了;

官网API https://developer.android.com/reference/android/support/v4/widget/ViewDragHelper.html

该类主要用于拖拽view的实现,例如侧滑菜单时候的左右拖拽或者上下拖拽

使用方法

创建 ViewDragHelper实例

1
2
3
ViewDragHelper create (ViewGroup forParent, 
float sensitivity,
ViewDragHelper.Callback cb)
  • 参数1 要使用DragHelper的布局
  • 参数2 灵敏度,值越大越灵敏,1.0属于正常
  • 参数3 回调,这里是主要阵地

事件拦截于处理

重写布局的 onInterceptTouchEvent()方法 ,ViewDragHelper会自行判断是否需要拦截事件

1
2
3
4
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mDragHelper.shouldInterceptTouchEvent(ev);
}

重写布局的 onTouchEvent() 处理拦截的事件

1
2
3
4
5
@Override
public boolean onTouchEvent(MotionEvent event) {
mDragHelper.processTouchEvent(event);
return true;
}

如果需要滑动动画 重写 computeScroll()

1
2
3
4
5
6
7
8
9
10
/**
* 因为要在 DragHelper的中使用动画
*/
@Override
public void computeScroll() {
super.computeScroll();
if (mDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}

前面在创建ViewDragHelper的时候用到了一个Callback,其实主要是在这里添加自己的逻辑,主要介绍一下这个类

这是一个抽象类,我们必须要实现的方法只有一个tryCaptureView()

1
public abstract boolean tryCaptureView(View child, int pointerId);
  • 参数1 准备捕获的子view
  • 参数2 准备捕获的指针ID

该方法的返回值决定了ViewDragHelper是否要捕获这个view;如果返回false就不捕获;

说的一下我的思路吧,我在父布局中持有了需要拖拽处理的子view的引用,如果捕获的view是我持有的view就返回true,捕获这次事件

1
2
3
4
5
6
7
8
9
10
11
/**
* 决定是否捕获此view
* 这里自由决定
* @param child 待捕获的子元素
* @param pointerId
* @return 是否捕获
*/
@Override
public boolean tryCaptureView(View child, int pointerId) {
return child==mContentView;
}

如果你需要处理水平拖拽,重写 clampViewPositionHorizontal() 即可,该方法返回值就是view拖拽后的坐标值;默认是不处理的;

下面是我的实现,为了防止拖拽出屏幕做了简单处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 水平 拖动
* @param child 拖动的元素
* @param left 将要去往的位置
* @param dx 拖动了的距离
* @return 新位置
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
//限制在容器内
int leftBound = getPaddingLeft();
int rightBound = getWidth() - mContentView.getWidth();
int newLeft = Math.min(Math.max(left,leftBound),rightBound);
return newLeft;
}

如果需要处理垂直拖拽,重写 clampViewPositionVertical() ;该方法和上面的那个方法一样,返回值就是view拖拽后的坐标值;默认不处理;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 垂直拖动
* @param child
* @param top
* @param dy
* @return
*/
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
int topBound = getPaddingTop();
int bottomBound = getHeight() - mContentView.getHeight();
int newTop = Math.min(Math.max(top,topBound),bottomBound);
return newTop;
}

可以从onViewDragStateChanged()方法中得到ViewDragHelper的状态变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public void onViewDragStateChanged(int state) {
switch (state){
case ViewDragHelper.STATE_IDLE:
Log.e("onViewDragStateChanged","state-->STATE_IDLE"+state);
break;
case ViewDragHelper.STATE_DRAGGING:
Log.e("onViewDragStateChanged","state-->STATE_DRAGGING"+state);
break;
case ViewDragHelper.STATE_SETTLING:
Log.e("onViewDragStateChanged","state-->STATE_SETTLING"+state);
break;
}
}

当子view的位置发生变化会触发 onViewPositionChanged() 方法

1
2
3
4
5
6
7
8
9
10
11
/**
* 当 view 的 position发生改变时触发
* @param changedView 拖动的view
* @param left 新位置 X轴
* @param top 新位置 Y轴
* @param dx 从上次位置 到这次位置移动的距离 X轴
* @param dy 从上次位置 到这次位置移动的距离 Y轴
*/
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
}

拖动动作停止,可以从 onViewReleased() 中得到速度信息

1
2
3
4
5
6
7
8
9
10
/**
*
* @param releasedChild
* @param xvel x 轴速度 每秒移动的像素值
* @param yvel Y 轴速度 每秒移动的像素值
*/
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
Log.e("onViewReleased","xvel-->"+xvel+";yvel-->"+yvel);
}

我用到的就这些,就介绍这些吧 ,
实现了一个可以自由拖动的layout 看Demo中的DragLayout ; https://github.com/sky-mxc/AndroidDemo/tree/master/drag

侧滑菜单实现

以前写过一个侧滑菜单,思路是重写 ListView或者RecycleView 的onTouch事件,判断根据坐标点判断找到子view,然后让子view滑动,从而实现的侧滑。感觉比较麻烦。今天说一下另外一个思路,

写一个通用的布局,例如一个LineaLayout,里面定义两个Group,一个是item内容,另一个是Item 菜单;在LineaLayout内部定义一个ViewDragHelper来处理拖动事件。ViewDragHelper会将拖动事件处理好,我们只需要在callback中处理简单的逻辑就好。

写一个SwipeLayout 继承自 LineaLayout; 在构造时就创建好 DragHelper

1
2
3
4
public SwipeItemLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mDragHelper = ViewDragHelper.create(this, 1.0f, new SwipeItemDragHelper());
}

加载完毕布局之后,拿到两个item,一个内容,一个菜单

1
2
3
4
5
6
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mContentView = getChildAt(0);
mActionView = getChildAt(1);
}

事件交由 DragHelper处理

1
2
3
4
5
6
7
8
9
10
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mDragHelper.shouldInterceptTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
mDragHelper.processTouchEvent(event);
return true;
}

要在callback中使用动画

1
2
3
4
5
6
7
8
9
10
/**
* 因为要在 DragHelper的中使用动画
*/
@Override
public void computeScroll() {
super.computeScroll();
if (mDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}

主要逻辑就在callback中处理

tryCaptureView() 如果拖动的时内容或者菜单就捕获此次多动

1
2
3
4
@Override
public boolean tryCaptureView(View child, int pointerId) {
return child == mContentView || child == mActionView;
}

因为实现的是侧滑菜单,这里只处理 水平拖动就好,注释写的很清楚了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
dragDx+=dx;
if (child == mContentView) {
/**
* 这个位置 的范围应该是在 0和 -dragDistance之间;最大是0;最小是 -dragDistance
*/
int leftBound = getPaddingLeft();
int minLeft = -leftBound - mDragDistance;
int newLeft = Math.min(Math.max(minLeft, left), 0);
return newLeft;
} else {
/**
* 这个view的位置范围应该是在 父布局的宽度-actionView的宽和父布局的宽度之间;
*/
int leftBound = getPaddingLeft();
int minLeft = getWidth() - leftBound - mActionView.getWidth();
int newLeft = Math.min(Math.max(minLeft, left), getWidth());
return newLeft;
}
}

当view 被拖动的时候,另一个view跟随被拖动的view一起移动

1
2
3
4
5
6
7
8
9
10
11
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
//同时移动
if (changedView == mContentView) {
mActionView.offsetLeftAndRight(dx);
} else {
mContentView.offsetLeftAndRight(dx);
}
invalidate();

}

当滑动结束后,可以根据滑动的速度或者滑动的距离来决定是否要打开或者关闭菜单;具体思路 注释已经很清楚了

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
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
/**
* 这里的速度 是这样计算的 每秒的拖动的像素 值
* 速度判断
* 如果向→滑动 速度肯定是 正数;
* 如果向←滑动 速度肯定是 负数
* 如果 拖动距离 是 actionView的 ¼ 就允许打开或关闭
*/
//根据速度决定是否打开
boolean settleToOpen = false;
float realVel = Math.abs(xvel);
int realDragX = Math.abs(dragDx);
if (realVel > AUTO_OPEN_SPEED_LIMIT) { //根据速度判断
if (xvel > 0) { //右滑
settleToOpen = false;
} else { //左滑
settleToOpen = true;
}
}else if(realDragX> mDragDistance/4){ //根据拖动距离判断
if (dragDx>0){ //右滑
settleToOpen = false;
}else{
settleToOpen = true;
}
}
isOpen = settleToOpen;
int settleDestX = isOpen ? -mDragDistance : 0;
Log.e("onViewReleased", "settleToOpen->" + settleToOpen + ";destX->" + settleDestX + ";xvel->" + xvel + ";dragDx-->" + dragDx);
mDragHelper.smoothSlideViewTo(mContentView, settleDestX, 0);
ViewCompat.postInvalidateOnAnimation(SwipeItemLayout.this);
dragDx = 0;
}
}

为了滑动更加灵敏,在左右滑动item时,禁止父布局的上下滑动

在onTouch中 判断滑动距离,超过一定范围就不让父布局处理;getParent().requestDisallowInterceptTouchEvent(true);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
public boolean onTouchEvent(MotionEvent event) {
mDragHelper.processTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
x = event.getRawX();
break;
case MotionEvent.ACTION_MOVE:
float gap = event.getRawX() - x;
int sl = ViewConfiguration.get(getContext()).getScaledTouchSlop();
if (Math.abs(gap) > sl) {
getParent().requestDisallowInterceptTouchEvent(true);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
x = 0;
break;
}
return true;
}

贴一下代码,完整Demo看GitHub https://github.com/sky-mxc/AndroidDemo/tree/master/drag

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
152
153
154
155
156
157
158
159
160
161
162
/**
* Created by mxc on 2017/7/23.
* description:
*/

public class SwipeItemLayout extends LinearLayout {
private final double AUTO_OPEN_SPEED_LIMIT = 500.0;
private View mActionView;
private View mContentView;
private int mDragDistance;
private ViewDragHelper mDragHelper;
private boolean isOpen;


public SwipeItemLayout(Context context) {
this(context, null);
}

public SwipeItemLayout(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

public SwipeItemLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mDragHelper = ViewDragHelper.create(this, 1.0f, new SwipeItemDragHelper());
}

@Override
protected void onFinishInflate() {
super.onFinishInflate();
mContentView = getChildAt(0);
mActionView = getChildAt(1);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mDragDistance = mActionView.getMeasuredWidth();
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mDragHelper.shouldInterceptTouchEvent(ev);
}


float x = 0;

@Override
public boolean onTouchEvent(MotionEvent event) {
mDragHelper.processTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
x = event.getRawX();
break;
case MotionEvent.ACTION_MOVE:
float gap = event.getRawX() - x;
int sl = ViewConfiguration.get(getContext()).getScaledTouchSlop();
if (Math.abs(gap) > sl) {
getParent().requestDisallowInterceptTouchEvent(true);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
x = 0;
break;
}
return true;
}


/**
* 因为要在 DragHelper的中使用动画
*/
@Override
public void computeScroll() {
super.computeScroll();
if (mDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}

class SwipeItemDragHelper extends ViewDragHelper.Callback {

private int dragDx;

@Override
public boolean tryCaptureView(View child, int pointerId) {
return child == mContentView || child == mActionView;
}

@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
dragDx+=dx;
if (child == mContentView) {
/**
* 这个位置 的范围应该是在 0和 -dragDistance之间;最大是0;最小是 -dragDistance
*/
int leftBound = getPaddingLeft();
int minLeft = -leftBound - mDragDistance;
int newLeft = Math.min(Math.max(minLeft, left), 0);
return newLeft;
} else {
/**
* 这个view的位置范围应该是在 父布局的宽度-actionView的宽和父布局的宽度之间;
*/
int leftBound = getPaddingLeft();
int minLeft = getWidth() - leftBound - mActionView.getWidth();
int newLeft = Math.min(Math.max(minLeft, left), getWidth());
return newLeft;
}
}

@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
//同时移动
if (changedView == mContentView) {
mActionView.offsetLeftAndRight(dx);
} else {
mContentView.offsetLeftAndRight(dx);
}
invalidate();
// Log.e("onViewPosition", "dx-->" + dx);
}

@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
/**
* 这里的速度 是这样计算的 每秒的拖动的像素 值
* 速度判断
* 如果向→滑动 速度肯定是 正数;
* 如果向←滑动 速度肯定是 负数
* 如果 拖动距离 是 actionView的 ¼ 就允许打开或关闭
*/
//根据速度决定是否打开
boolean settleToOpen = false;
float realVel = Math.abs(xvel);
int realDragX = Math.abs(dragDx);
if (realVel > AUTO_OPEN_SPEED_LIMIT) { //根据速度判断
if (xvel > 0) { //右滑
settleToOpen = false;
} else { //左滑
settleToOpen = true;
}
}else if(realDragX> mDragDistance/4){ //根据拖动距离判断
if (dragDx>0){ //右滑
settleToOpen = false;
}else{
settleToOpen = true;
}
}
isOpen = settleToOpen;
int settleDestX = isOpen ? -mDragDistance : 0;
Log.e("onViewReleased", "settleToOpen->" + settleToOpen + ";destX->" + settleDestX + ";xvel->" + xvel + ";dragDx-->" + dragDx);
mDragHelper.smoothSlideViewTo(mContentView, settleDestX, 0);
ViewCompat.postInvalidateOnAnimation(SwipeItemLayout.this);
dragDx = 0;
}
}


}
坚持原创技术分享,您的支持将鼓励我继续创作!