您的位置:首页 > 移动开发 > Android开发

ViewDragHelper的源码分析及概述学习

2017-03-27 19:44 411 查看

一、概述

使用到ViewDragHelper是自己在做仿微信的侧滑删除操作时用到的,需要对Recycleview内的Item的滑动事件做处理,并将隐藏在屏幕右侧的view滑出来做出删除操作。自己的demo删除了,这里就借用一下ITluochen大神的demo,主要是能为自己以后用到ViewDragHelper提供方便与指引,ITluochen大神的demo讲解的其实非常详细了,大家也可以去看看。

这里先简要介绍ViewDragHelper,后面再结合实例讲解。

ViewDragHelper解决了Android中手势处理过于复杂的问题,在DrawerLayout出现之前,侧滑菜单都是由第三方开源代码实现的,其中著名的当属MenuDrawer
,MenuDrawer重写onTouchEvent方法来实现侧滑效果,代码量很大,实现逻辑也需要很大的耐心才能看懂。如果每个开发人员都从这么原始的步奏开始做起,那对于安卓生态是相当不利的。所以说ViewDragHelper等的出现反映了安卓开发框架已经开始向成熟的方向迈进。

首先我们通过一个简单的例子来看看其快捷的用法,分为以下几个步骤:

(1)创建实例

(2)触摸相关的方法的调用

(3)ViewDragHelper.Callback实例的编写

第一步,

利用ViewDragHelper.create(ViewGroup forParent, Callback cb)会创建一个ViewDragHelper的实例。

第二步,弄一个类继承自ViewDragHelper.Callback,作为第一步中create方法的参数,复写一下这么几个方法并且一些逻辑操作

tryCaptureView()

clampViewPositionHorizontal()

clampViewPositionVertical()

onViewPositionChanged()

onViewReleased()

第三步实现动画效果,

第二步中的复写的那几个方法很重要。这几个方法到底分别的有什么用呢?可以大概这么理解:

我们知道TouchEvent大概可以分为三个状态,Down(按下)、Move(移动)和Up(抬起)。

那么在这三个不同的状态里面,与之关联的就是上面的几个方法:

Touch的down事件:

回调tryCaptureView()

Touch的move事件

回调

clampViewPositionHorizontal()

clampViewPositionVertical()

onViewPositionChanged()

Touch的up事件

回调:onViewReleased()


二、demo实例

这个demo主要实现仿微信侧滑删除item,用一般的手势处理机制也能实现,但是考虑到事件分发、手势位移处理等比较复杂,代码量大,所以还是使用了V4包下的ViewDragHelper比较方便。且viewgroup一般是用户在viewgroup中控制其子view滑动事件比较方便。

布局文件:这里的布局分为两个部分,然后将这两个布局include到主布局中,在主布局中就可以方便的使用ViewDragHelper对两个子布局进行滑动控制。


(1)布局文件

内容部分布局如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="#97d8da"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/mTvContent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="文本"
android:gravity="center"
android:textSize="28dp"
/>
</LinearLayout>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[/code]

删除部分布局如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="130dp"
android:layout_height="60dp"
android:background="#ff0000"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="删除"
android:textSize="22dp"
android:textColor="#ffffff"
/>
</LinearLayout>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[/code]

adapter里面的item(将该Item做为List的item)
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.amqr.slidedelete.MainActivity">
<!--自定义ViewGroup继承者Layout-->
<com.amqr.slidedelete.view.SlideDelete
android:layout_width="match_parent"
android:layout_height="60dp">
<!--文本部分-->
<include layout="@layout/slide_content"/>
<!--删除部分-->
<include layout="@layout/slide_delete"/>
</com.amqr.slidedelete.view.SlideDelete>
</RelativeLayout>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[/code]

主布局如下:

activity_main
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.amqr.slidedelete.MainActivity">
<ListView
android:id="@+id/mLv"
android:layout_width="match_parent"
android:layout_height="match_parent"></ListView>
</RelativeLayout>
1
2
3
4
5
6
7
8
9
10
11
1
2
3
4
5
6
7
8
9
10
11
[/code]


(2)自定义控件SlideDelete

public class SlideDelete extends ViewGroup{
private View mContent; // 内容部分
private View mDelete;  // 删除部分
public SlideDelete(Context context) {
super(context);
}
public SlideDelete(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SlideDelete(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mContent = getChildAt(0);
mDelete = getChildAt(1);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 这跟mContent的父亲的大小有关,父亲是宽填充父窗体,高度是和孩子一样是60dp
mContent.measure(widthMeasureSpec,heightMeasureSpec); // 测量内容部分的大小
LayoutParams layoutParams = mDelete.getLayoutParams();
int deleteWidth = MeasureSpec.makeMeasureSpec(layoutParams.width,MeasureSpec.EXACTLY);
int deleteHeight = MeasureSpec.makeMeasureSpec(layoutParams.height,MeasureSpec.EXACTLY);
// 这个参数就需要指定为精确大小
mDelete.measure(deleteWidth,deleteHeight); // 测量删除部分的大小
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int mContentWidth = mContent.getMeasuredWidth();
int mContentHeight = mContent.getMeasuredHeight();
mContent.layout(0,0,mContentWidth,mContentHeight); // 摆放内容部分的位置
int mDeleteWidth = mDelete.getMeasuredWidth();
mDelete.layout(mContentWidth,0,
mContentWidth + mDeleteWidth, mContentHeight); // 摆放删除部分的位置
}
}
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
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
[/code]

此时效果图如下:



这里需要解释一下: 删除部分其实已经绘制显示在手机的右侧了,内容部分已经match_parent了,所以屏幕上现在暂时还看不见(需要后期通过ViewDragHelper将他滑动出来)。


(3)MyDrawHelper类

class MyDrawHelper extends ViewDragHelper.Callback {
/**
* Touch的down事件会回调这个方法 tryCaptureView
* @Child:指定要动的孩子  (哪个孩子需要动起来)
* @pointerId: 点的标记
* @return : ViewDragHelper是否继续分析处理 child的相关touch事件
*/
@Override
public boolean tryCaptureView(View child, int pointerId) {
System.out.println("调用tryCaptureView");
//表示mcontent和nDelete都能响应滑动事件
return mContent == child || mDelete == child;
}
// Touch的move事件会回调这面这几个方法
// clampViewPositionHorizontal
// clampViewPositionVertical
// onViewPositionChanged
/**
* 捕获了水平方向移动的位移数据
* 可以在该方法中对child移动的边界进行控制
* @param child 移动的孩子View
* @param left 父容器的左上角到孩子View的距离
* @param dx 增量值,其实就是移动的孩子View的左上角距离控件(父亲)的距离,包含正负
* @return 如何动
* 调用完此方法,在android2.3以上就会动起来了,2.3以及以下是海动不了的
* 2.3不兼容怎么办?没事,我们复写onViewPositionChanged就是为了解决这个问题的
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
Log.d("Slide","增量值:   "+left);
return left;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return super.clampViewPositionVertical(child, top, dy);
}
/**
* 当View的位置改变时的回调
* @param changedView  哪个View的位置改变了
* @param left  changedView的left
* @param top  changedView的top
* @param dx x方向的上的增量值
* @param dy y方向上的增量值
*/
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
invalidate();
super.onViewPositionChanged(changedView, left, top, dx, dy);
}
/**
* 相当于Touch的up的事件会回调onViewReleased这个方法
*
* @param releasedChild
* @param xvel x方向的速率
* @param yvel y方向的速率
*/
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
}
}
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
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
[/code]

这里对上面的clampViewPositionHorizontal(View child, int left, int dx)稍作解释一下,这个函数比较关键。



在上图中,我标出了left的值,下面以一个限定在灰色区域内滑动的实例代码加深理解。
public int clampViewPositionHorizontal(View child, int left, int dx) {
int leftBound = getPaddingLeft();
int rightBound = getWidth() - child.getWidth() - leftBound;
//限定了view的在灰色趋于中(也就是l指大于leftBound小于rightBound)
int newLeft = Math.min(Math.max(left, leftBound), rightBound);
return newLeft;
}
1
2
3
4
5
6
7
1
2
3
4
5
6
7
[/code]


(4)、当一个孩子动起来另外一个孩子也可以跟随着动起来

/**
* 当View的位置改变时的回调  这个方法的价值是结合clampViewPositionHorizontal或者clampViewPositionVertical
* @param changedView  哪个View的位置改变了
* @param left  changedView的left
* @param top  changedView的top
* @param dx x方向的上的增量值
* @param dy y方向上的增量值
*/
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
//super.onViewPositionChanged(changedView, left, top, dx, dy);
invalidate();
if(changedView == mContent){ // 如果移动的是mContent
//我们移动mContent的实惠要相应的联动改变mDelete的位置
// 怎么改变mDelete的位置,当然是mDelete的layput方法啦
int tempDeleteLeft = mContentWidth+left;
int tempDeleteRight = mContentWidth+left + mDeleteWidth;
mDelete.layout(tempDeleteLeft,0,tempDeleteRight,mDeleteHeight);
}else{ // touch的是mDelete
int tempContentLeft = left - mContentWidth;
int tempContentRight = left;
mContent.layout(tempContentLeft,0,tempContentRight,mContentHeight);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[/code]

当前效果如下:




(5)解决越界问题

这个越界的问题为什么会产生,是因为现在在clampViewPositionHorizontal方法里面我们简单粗暴地返回了left。这样肯定是不行的。所以我们需要在这个方法上做一些处理
/**
*
* 捕获了水平方向移动的位移数据
* @param child 移动的孩子View
* @param left 父容器的左上角到孩子View的距离
* @param dx 增量值,其实就是移动的孩子View的左上角距离控件(父亲)的距离,包含正负
* @return 如何动
*
* 调用完此方法,在android2.3以上就会动起来了,2.3以及以下是海动不了的
* 2.3不兼容怎么办?没事,我们复写onViewPositionChanged就是为了解决这个问题的
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
Log.d("Slide","增量值:   "+left);
if(child == mContent){ // 解决内容部分左右拖动的越界问题
if(left>0){
return 0;
}else if(-left>mDeleteWidth){
return -mDeleteWidth;
}
}
if(child == mDelete){ // 解决删除部分左右拖动的越界问题
if(left<mContentWidth - mDeleteWidth){
return mContentWidth - mDeleteWidth;
}else if(left > mContentWidth){
return mContentWidth;
}
}
return left;
}
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
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
[/code]


(6)释放时位置的归正

/**
* 相当于Touch的up的事件会回调onViewReleased这个方法
*
* @param releasedChild
* @param xvel  x方向的速率
* @param yvel  y方向的速率
*/
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
//super.onViewReleased(releasedChild, xvel, yvel);
// 方法的参数里面没有left,那么我们就采用 getLeft()这个方法
int mConLeft = mContent.getLeft();
// 这里没必要分来两个孩子判断
if(-mConLeft>mDeleteWidth/2){
mContent.layout(-mDeleteWidth,0,mContentWidth-mDeleteWidth,mContentHeight);
mDelete.layout(mContentWidth-mDeleteWidth,0,mContentWidth,mDeleteHeight);
}else{
mContent.layout(0,0,mContentWidth,mContentHeight);
mDelete.layout(mContentWidth,0,mContentWidth+mDeleteWidth,mDeleteHeight);
}
super.onViewReleased(releasedChild, xvel, yvel);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[/code]


(7)位置归正的过渡动画

ViewDragHelper里面给我们提供了一个方法,smoothSlideViewTo(View child, int finalLeft, int finalTo), smooth是平滑的意思,这个方法就是帮助我们做平滑滑动的。
public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) {
mCapturedView = child;
mActivePointerId = INVALID_POINTER;
boolean continueSliding = forceSettleCapturedViewAt(finalLeft, finalTop, 0, 0);
if (!continueSliding && mDragState == STATE_IDLE && mCapturedView != null) {
// If we're in an IDLE state to begin with and aren't moving anywhere, we
// end up having a non-null capturedView with an IDLE dragState
mCapturedView = null;
}
return continueSliding;
}
1
2
3
4
5
6
7
8
9
10
11
1
2
3
4
5
6
7
8
9
10
11
[/code]

三个参数

child,被滑动的那个child

finalLeft 、finalTop: 这两个点构成了孩子运动到最后的左上角的坐标点

通过孩子最后左上角的点就可以确定最后的应该到达的位置
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
//super.onViewReleased(releasedChild, xvel, yvel);
// 方法的参数里面没有left,那么我们就采用 getLeft()这个方法
int mConLeft = mContent.getLeft();
// 这里没必要分来两个孩子判断
//内容部分向右滑动距离超过删除部分宽度的一半则删除部分全部显示否则不显示
if(-mConLeft>mDeleteWidth/2){
//mContent.layout(-mDeleteWidth,0,mContentWidth-mDeleteWidth,mContentHeight);
//mDelete.layout(mContentWidth-mDeleteWidth,0,mContentWidth,mDeleteHeight);
//采用ViewDragHelper的 smoothSlideViewTo 方法让移动变得顺滑自然,不会太生硬
//smoothSlideViewTo只是模拟了数据,但是不会真正的动起来,动起来需要调用 invalidate
// 而 invalidate 通过调用draw()等方法之后最后还是还是会调用 computeScroll 这个方法
// 所以,使用 smoothSlideViewTo 做过渡动画需要结合  invalidate方法 和 computeScroll方法
// smoothSlideViewTo的动画执行时间没有暴露的参数可以设置,但是这个时间是google给我们经过大量计算给出合理时间
//mcontent最后左上角坐标点为(-mDeleteWidth,0)
viewDragHelper.smoothSlideViewTo(mContent,-mDeleteWidth,0);
// mDelete最后左上角坐标点为(mContentWidth-mDeleteWidth,0)
viewDragHelper.smoothSlideViewTo(mDelete,mContentWidth-mDeleteWidth,0);
}else{
//mContent.layout(0,0,mContentWidth,mContentHeight);
//mDelete.layout(mContentWidth, 0, mContentWidth + mDeleteWidth, mDeleteHeight);
//mcontent最后左上角坐标点为(0,0)
viewDragHelper.smoothSlideViewTo(mContent, 0, 0);
// mDelete最后左上角坐标点为(mContentWidth, 0)
viewDragHelper.smoothSlideViewTo(mDelete,mContentWidth, 0);
}
invalidate();
super.onViewReleased(releasedChild, xvel, yvel);
}
}
@Override
public void computeScroll() {
//super.computeScroll();
// 把捕获的View适当的时间移动,其实也可以理解为 smoothSlideViewTo 的模拟过程还没完成
if(viewDragHelper.continueSettling(true)){
invalidate();
}
// 其实这个动画过渡的过程大概在怎么走呢?
// 1、smoothSlideViewTo方法进行模拟数据,模拟后就就调用invalidate();
// 2、invalidate()最终调用computeScroll,computeScroll做一次细微动画,
//    computeScroll判断模拟数据是否彻底完成,还没完成会再次调用invalidate
// 3、递归调用,知道数据noni完成。
}
}
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
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
[/code]


(8)完整的自定义ViewGroup操作子控件类

三、在ListView里面嵌入我们的自定义控件

新建一个Activity,假设名为MyActivity,并且把这个Activity设置为启动页。
MyActivity
public class MyActivity extends Activity{
private ListView mLv;
private ArrayList<String> mData;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my);
mLv = (ListView) findViewById(R.id.mLv);
mData=new ArrayList<>();
for(int i=0;i<200;i++){
mData.add("文本"+i);
}
mLv.setAdapter(new MyAdapter());
}
class MyAdapter extends BaseAdapter{
@Override
public int getCount() {
return mData.size();
}
@Override
public Object getItem(int position) {
return mData.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
if(convertView == null){
viewHolder = new ViewHolder();
convertView = View.inflate(MyActivity.this,R.layout.item,null);
viewHolder.mSlideDelete = (SlideDelete) convertView.findViewById(R.id.mSlideDelete);
viewHolder.mLlContent = (LinearLayout) convertView.findViewById(R.id.mLlContent);
viewHolder.mLlDelete = (LinearLayout) convertView.findViewById(R.id.mLlDelete);
convertView.setTag(viewHolder);
}else{
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.mSlideDelete.setOnSlideDeleteListener(new SlideDelete.OnSlideDeleteListener() {
@Override
public void onOpen(SlideDelete slideDelete) {
}
@Override
public void onClose(SlideDelete slideDelete) {
}
});
return convertView;
}
}
class ViewHolder{
SlideDelete mSlideDelete;
LinearLayout mLlContent;
LinearLayout mLlDelete;
}
}
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
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
[/code]



但是仍然有缺陷,实际使用中,我们通常是只能一个item处于打开状态,即A打开,B必须处于关闭,不能A和B都处于打开的状态,所以接下来我们还需要使用回调使得只有一个Item被打开。


(9)只有一个Item被打开

1、我们给SlideDelete添加接口和回调,接口里面有onOpen(SlideDelete slideDelete)和onClose(SlideDelete slideDelete)两个方法。
// SlideDlete的接口
public interface OnSlideDeleteListener {
void onOpen(SlideDelete slideDelete);
void onClose(SlideDelete slideDelete);
}
1
2
3
4
5
1
2
3
4
5
[/code]

2、暴露一个setOnSlideDeleteListener方法给外部调用,把SlideDelete的onViewReleased里面的打开和关闭抽取暴露出来,通过参数boolean决定是否显示delete部分。
private OnSlideDeleteListener onSlideDeleteListener;
public void setOnSlideDeleteListener(OnSlideDeleteListener onSlideDeleteListener){
this.onSlideDeleteListener = onSlideDeleteListener;
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
//super.onViewReleased(releasedChild, xvel, yvel);
// 方法的参数里面没有left,那么我们就采用 getLeft()这个方法
int mConLeft = mContent.getLeft();
// 这里没必要分来两个孩子判断
if(-mConLeft>mDeleteWidth/2){  // mDelete展示起来
isShowDelete(true);
if(onSlideDeleteListener != null){
onSlideDeleteListener.onOpen(SlideDelete.this); // 调用接口打开的方法
}
}else{    // mDetele隐藏起来
isShowDelete(false);
if(onSlideDeleteListener != null){
onSlideDeleteListener.onClose(SlideDelete.this); // 调用接口的关闭的方法
}
}
super.onViewReleased(releasedChild, xvel, yvel);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[/code]

3、在MyActivity的Adapter里面调用SlideDelete暴露出来的实现接口的方法。

弄一个集合记录起来已经打开的item,每次getView的执行都先关闭已经打开的item
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
if(convertView == null){
viewHolder = new ViewHolder();
convertView = View.inflate(MyActivity.this,R.layout.item,null);
viewHolder.mSlideDelete = (SlideDelete) convertView.findViewById(R.id.mSlideDelete);
viewHolder.mTvContent = (TextView) convertView.findViewById(R.id.mTvContent);
viewHolder.mLlDelete = (LinearLayout) convertView.findViewById(R.id.mLlDelete);
convertView.setTag(viewHolder);
}else{
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.mTvContent.setText(mData.get(position));
viewHolder.mSlideDelete.setOnSlideDeleteListener(new SlideDelete.OnSlideDeleteListener() {
@Override
public void onOpen(SlideDelete slideDelete) {
closeOtherItem();
slideDeleteArrayList.add(slideDelete);
Log.d("Slide", "slideDeleteArrayList当前数量:" + slideDeleteArrayList.size());
}
@Override
public void onClose(SlideDelete slideDelete) {
slideDeleteArrayList.remove(slideDelete);
Log.d("Slide", "slideDeleteArrayList当前数量:" + slideDeleteArrayList.size());
}
});
return convertView;
}
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
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
[/code]

关闭所有已经打开的item的方法
private void closeOtherItem(){
// 采用Iterator的原因是for是线程不安全的,迭代器是线程安全的
ListIterator<SlideDelete> slideDeleteListIterator = slideDeleteArrayList.listIterator();
while(slideDeleteListIterator.hasNext()){
SlideDelete slideDelete = slideDeleteListIterator.next();
slideDelete.isShowDelete(false);
}
slideDeleteArrayList.clear();
}
1
2
3
4
5
6
7
8
9
1
2
3
4
5
6
7
8
9
[/code]

至此完成。

附上当前完整的SlideDelete的代码
public class SlideDelete extends ViewGroup{
private View mContent; // 内容部分
private View mDelete; // 删除部分
private ViewDragHelper viewDragHelper;
private int mContentWidth;
private int mContentHeight;
private int mDeleteWidth;
private int mDeleteHeight;
public SlideDelete(Context context) {
super(context);
}
public SlideDelete(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SlideDelete(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
private OnSlideDeleteListener onSlideDeleteListener;
public void setOnSlideDeleteListener(OnSlideDeleteListener onSlideDeleteListener){
this.onSlideDeleteListener = onSlideDeleteListener;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mContent = getChildAt(0);
mDelete = getChildAt(1);
//public static ViewDragHelper create(ViewGroup forParent, Callback cb)
viewDragHelper = ViewDragHelper.create(this,new MyDrawHelper());
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 这跟mContent的父亲的大小有关,父亲是宽填充父窗体,高度是和孩子一样是60dp
mContent.measure(widthMeasureSpec,heightMeasureSpec); // 测量内容部分的大小
LayoutParams layoutParams = mDelete.getLayoutParams();
int deleteWidth = MeasureSpec.makeMeasureSpec(layoutParams.width, MeasureSpec.EXACTLY);
int deleteHeight = MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY);
// 这个参数就需要指定为精确大小
mDelete.measure(deleteWidth, deleteHeight); // 测量删除部分的大小
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mContentWidth = mContent.getMeasuredWidth();
mContentHeight = mContent.getMeasuredHeight();
mContent.layout(0, 0, mContentWidth, mContentHeight); // 摆放内容部分的位置
mDeleteWidth = mDelete.getMeasuredWidth();
mDeleteHeight = mDelete.getMeasuredHeight();
mDelete.layout(mContentWidth, 0,
mContentWidth + mDeleteWidth, mContentHeight); // 摆放删除部分的位置
}
class MyDrawHelper extends ViewDragHelper.Callback {
/**
* Touch的down事件会回调这个方法 tryCaptureView
*
* @Child:指定要动的孩子 (哪个孩子需要动起来)
* @pointerId: 点的标记
* @return : ViewDragHelper是否继续分析处理 child的相关touch事件
*/
@Override
public boolean tryCaptureView(View child, int pointerId) {
System.out.println("调用tryCaptureView");
System.out.println("contentView : " + (mContent == child));
return mContent == child || mDelete == child;
}
// Touch的move事件会回调这面这几个方法
// clampViewPositionHorizontal
// clampViewPositionVertical
// onViewPositionChanged
/**
*
* 捕获了水平方向移动的位移数据
* @param child 移动的孩子View
* @param left 父容器的左上角到孩子View的距离
* @param dx 增量值,其实就是移动的孩子View的左上角距离控件(父亲)的距离,包含正负
* @return 如何动
*
* 调用完此方法,在android2.3以上就会动起来了,2.3以及以下是海动不了的
* 2.3不兼容怎么办?没事,我们复写onViewPositionChanged就是为了解决这个问题的
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
//Log.d("Slide", "增量值: " + left);
if(child == mContent){ // 解决内容部分左右拖动的越界问题
if(left>0){
return 0;
}else if(-left>mDeleteWidth){
return -mDeleteWidth;
}
}
if(child == mDelete){ // 解决删除部分左右拖动的越界问题
if(left<mContentWidth - mDeleteWidth){
return mContentWidth - mDeleteWidth;
}else if(left > mContentWidth){
return mContentWidth;
}
}
return left;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return super.clampViewPositionVertical(child, top, dy);
}
/**
* 当View的位置改变时的回调 这个方法的价值是结合clampViewPositionHorizontal或者clampViewPositionVertical
* @param changedView 哪个View的位置改变了
* @param left changedView的left
* @param top changedView的top
* @param dx x方向的上的增量值
* @param dy y方向上的增量值
*/
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
//super.onViewPositionChanged(changedView, left, top, dx, dy);
invalidate();
if(changedView == mContent){ // 如果移动的是mContent
//我们移动mContent的实惠要相应的联动改变mDelete的位置
// 怎么改变mDelete的位置,当然是mDelete的layput方法啦
int tempDeleteLeft = mContentWidth+left;
int tempDeleteRight = mContentWidth+left + mDeleteWidth;
mDelete.layout(tempDeleteLeft,0,tempDeleteRight,mDeleteHeight);
}else{ // touch的是mDelete
int tempContentLeft = left - mContentWidth;
int tempContentRight = left;
mContent.layout(tempContentLeft,0,tempContentRight,mContentHeight);
}
}
/**
* 相当于Touch的up的事件会回调onViewReleased这个方法
*
* @param releasedChild
* @param xvel x方向的速率
* @param yvel y方向的速率
*/
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
//super.onViewReleased(releasedChild, xvel, yvel);
// 方法的参数里面没有left,那么我们就采用 getLeft()这个方法
int mConLeft = mContent.getLeft();
// 这里没必要分来两个孩子判断
if(-mConLeft>mDeleteWidth/2){ // mDelete展示起来
isShowDelete(true);
if(onSlideDeleteListener != null){
onSlideDeleteListener.onOpen(SlideDelete.this); // 调用接口打开的方法
}
}else{ // mDetele隐藏起来
isShowDelete(false);
if(onSlideDeleteListener != null){
onSlideDeleteListener.onClose(SlideDelete.this); // 调用接口的关闭的方法
}
}
super.onViewReleased(releasedChild, xvel, yvel);
}
}
/**
* 是否展示delete部分
* @param isShowDelete
*/
public void isShowDelete(boolean isShowDelete){
if(isShowDelete){
//mContent.layout(-mDeleteWidth,0,mContentWidth-mDeleteWidth,mContentHeight);
//mDelete.layout(mContentWidth-mDeleteWidth,0,mContentWidth,mDeleteHeight);
//采用ViewDragHelper的 smoothSlideViewTo 方法让移动变得顺滑自然,不会太生硬
//smoothSlideViewTo只是模拟了数据,但是不会真正的动起来,动起来需要调用 invalidate
// 而 invalidate 通过调用draw()等方法之后最后还是还是会调用 computeScroll 这个方法
// 所以,使用 smoothSlideViewTo 做过渡动画需要结合 invalidate方法 和 computeScroll方法
// smoothSlideViewTo的动画执行时间没有暴露的参数可以设置,但是这个时间是google给我们经过大量计算给出合理时间
viewDragHelper.smoothSlideViewTo(mContent,-mDeleteWidth,0);
viewDragHelper.smoothSlideViewTo(mDelete,mContentWidth-mDeleteWidth,0);
}else{
//mContent.layout(0,0,mContentWidth,mContentHeight);
//mDelete.layout(mContentWidth, 0, mContentWidth + mDeleteWidth, mDeleteHeight);
viewDragHelper.smoothSlideViewTo(mContent, 0, 0);
viewDragHelper.smoothSlideViewTo(mDelete, mContentWidth, 0);
}
invalidate();
}
@Override
public void computeScroll() {
//super.computeScroll();
// 把捕获的View适当的时间移动,其实也可以理解为 smoothSlideViewTo 的模拟过程还没完成
if(viewDragHelper.continueSettling(true)){
invalidate();
}
// 其实这个动画过渡的过程大概在怎么走呢?
// 1、smoothSlideViewTo方法进行模拟数据,模拟后就就调用invalidate();
// 2、invalidate()最终调用computeScroll,computeScroll做一次细微动画,
// computeScroll判断模拟数据是否彻底完成,还没完成会再次调用invalidate
// 3、递归调用,知道数据noni完成。
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//return super.onTouchEvent(event);
/**Process a touch event received by the parent view. This method will dispatch callback events
as needed before returning. The parent view's onTouchEvent implementation should call this. */
viewDragHelper.processTouchEvent(event); // 使用ViewDragHelper必须复写onTouchEvent并调用这个方法
return true; //消费这个touch
}
// SlideDlete的接口 public interface OnSlideDeleteListener { void onOpen(SlideDelete slideDelete); void onClose(SlideDelete slideDelete); }
}
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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
[/code]

附上当前完整的MyActivity代码
public class MyActivity extends Activity{
private ListView mLv;
private ArrayList<String> mData;
// 记录有多少个条目的delete被展示出来的集合
private List<SlideDelete> slideDeleteArrayList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my);
mLv = (ListView) findViewById(R.id.mLv);
mData=new ArrayList<>();
for(int i=0;i<200;i++){
mData.add("文本"+i);
}
mLv.setAdapter(new MyAdapter());
}
class MyAdapter extends BaseAdapter{
@Override
public int getCount() {
if(mData!=null){
return mData.size();
}
return 0;
}
@Override
public Object getItem(int position) {
if(mData!=null){
return mData.get(position);
}
return null;
}
@Override
public long getItemId(int position) {
return position;
}
@Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder viewHolder; if(convertView == null){ viewHolder = new ViewHolder(); convertView = View.inflate(MyActivity.this,R.layout.item,null); viewHolder.mSlideDelete = (SlideDelete) convertView.findViewById(R.id.mSlideDelete); viewHolder.mTvContent = (TextView) convertView.findViewById(R.id.mTvContent); viewHolder.mLlDelete = (LinearLayout) convertView.findViewById(R.id.mLlDelete); convertView.setTag(viewHolder); }else{ viewHolder = (ViewHolder) convertView.getTag(); } viewHolder.mTvContent.setText(mData.get(position)); viewHolder.mSlideDelete.setOnSlideDeleteListener(new SlideDelete.OnSlideDeleteListener() { @Override public void onOpen(SlideDelete slideDelete) { closeOtherItem(); slideDeleteArrayList.add(slideDelete); Log.d("Slide", "slideDeleteArrayList当前数量:" + slideDeleteArrayList.size()); } @Override public void onClose(SlideDelete slideDelete) { slideDeleteArrayList.remove(slideDelete); Log.d("Slide", "slideDeleteArrayList当前数量:" + slideDeleteArrayList.size()); } }); return convertView; }
}
class ViewHolder{
SlideDelete mSlideDelete;
TextView mTvContent;
LinearLayout mLlDelete;
}
private void closeOtherItem(){ // 采用Iterator的原因是for是线程不安全的,迭代器是线程安全的 ListIterator<SlideDelete> slideDeleteListIterator = slideDeleteArrayList.listIterator(); while(slideDeleteListIterator.hasNext()){ SlideDelete slideDelete = slideDeleteListIterator.next(); slideDelete.isShowDelete(false); } slideDeleteArrayList.clear(); }
}
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
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
[/code]

4、滑动屏幕就关闭打开的条目

完善一下,如果当前有item的删除部分是展开的,当这个情况下我们去滑动竖直方向滑动屏幕,那么删除部分就会被隐藏回去。其实就是做一下ListView的滑动监听而已
mLv.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if(scrollState == SCROLL_STATE_FLING || scrollState == SCROLL_STATE_TOUCH_SCROLL){
closeOtherItem();
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
}
});
1
2
3
4
5
6
7
8
9
10
11
1
2
3
4
5
6
7
8
9
10
11
[/code]

5、删除按钮按下删除item
viewHolder.mLlDelete.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mData.remove(position);
notifyDataSetChanged();
}
});
1
2
3
4
5
6
7
1
2
3
4
5
6
7
[/code]

最终效果



ViewDragHelper对象的构建通常在viewGroup的内部,用于实现一个自定义的布局的时候,对布局内部的View进行拖动,可用的构造方法有两种。
/**
* 两个工厂方法,通常使用第一个
* forParent 表示所在的ViewGroup
* sensitivity 表示拖动的灵敏度
* cb 表示我们需要实现的拖动的各种监听
* */
public static ViewDragHelper create(ViewGroup forParent, Callback cb) {
return new ViewDragHelper(forParent.getContext(), forParent, cb);
}

public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) {
final ViewDragHelper helper = create(forParent, cb);
helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));
return helper;
}
//这个是在父View的构造方法中进行实例化。
public MyLinearLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initView();
}

private void initView() {
mHelper=ViewDragHelper.create(this, new myCallBack());
}


其中mTouchSlop为能够被系统识别为滑动的移动距离。两个构建方法并不难,我们继续,看下在ViewGroup中还需要做些什么。
/**
* Check if this event as provided to the parent view's onInterceptTouchEvent should
* cause the parent to intercept the touch event stream.
*检测这个作为被提供给父view的onInterceptTouchEvent的事件是否令父view拦截到当前的触摸事件流.
* @param ev MotionEvent provided to onInterceptTouchEvent
* 如果父View在onInterceptTouchEvent方法中应该返回true的话, 则返回true
* 意思就是如果父控件决定拦截,就返回true。
* @return true if the parent view should return true from onInterceptTouchEvent
*/
public boolean shouldInterceptTouchEvent(MotionEvent ev) {}


后面的代码实在是太多就没有全贴,我们只需要知道这个方法是使用就一个了,当然还是推荐大家去看一下里面都做了什么,便于更好的理解和使用这个类。shouldInterceptTouchEvent()方法在ViewGroup的onInterceptTouchEvent()方法中调用,直接把事件传过去,跟GestureDetector用法差不多。接下来还有。
/**
* Process a touch event received by the parent view. This method will dispatch callback events
* as needed before returning. The parent view's onTouchEvent implementation should call this.
*  处理从父view中获取的触摸事件.这个方法将分发callback回调事件.父view的onTouchEvent方法中应该调用该方法.
* @param ev The touch event received by the parent view
*/
public void processTouchEvent(MotionEvent ev) {}


processTouchEvent则在父View的onTouchEvent方法中被调用,用于分发CallBack的回调事件。

onInterceptTouchEvent中通过使用mHelper.shouldInterceptTouchEvent(event)来决定我们是否应该拦截当前的事件。onTouchEvent中通过mHelper.processTouchEvent(event)处理事件。

/**
* Move the captured settling view by the appropriate amount for the current time.
* If <code>continueSettling</code> returns true, the caller should call it again
* on the next frame to continue.
*
* @param deferCallbacks true if state callbacks should be deferred via posted message.
*                       Set this to true if you are calling this method from
*                       {@link android.view.View#computeScroll()} or similar methods
*                       invoked as part of layout or drawing.
* @return true if settle is still in progress
*/
public boolean continueSettling(boolean deferCallbacks) {}
//在父View中的使用
@Override
public void computeScroll() {
if(mHelper.continueSettling(true)){
ViewCompat.postInvalidateOnAnimation(this);
}
}


这里就是如果要延迟刷新的话就传入true,返回值如果为True则会重新调用这个方法,直到结束。通常的使用方式就是如上文在父View中使用。至此在父View中我们需要做的就基本结束了。

接下来就是常用方法
* <p>If this method returns true, a call to {@link #onViewCaptured(android.view.View, int)}
* will follow if the capture is successful.</p>
*  决定是否捕获传进来的View
* @param child Child the user is attempting to capture
* @param pointerId ID of the pointer attempting the capture
* @return true if capture should be allowed, false otherwise
*/
public abstract boolean tryCaptureView(View child, int pointerId);
/**
*  决定横向能够拖动的距离
* @param child Child view being dragged
* @param left Attempted motion along the X axis
* @param dx Proposed change in position for left
* @return The new clamped position for left
*/
public int clampViewPositionHorizontal(View child, int left, int dx) {
return 0;
}
/**
*  决定纵向能够拖动的距离
* @param child Child view being dragged
* @param top Attempted motion along the Y axis
* @param dy Proposed change in position for top
* @return The new clamped position for top
*/
public int clampViewPositionVertical(View child, int top, int dy) {
return 0;
}


三个最常用的方法,在实现拖动的时候基本上都要重写的(第一个是实现),注释已经很明确了,通常在第二,三个方法中直接返回 left,top就可以。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android