Android自定义可拖拽ListView

如下图所示,下面开始自定义一个可拖拽的ListView

自定义可拖拽ListView

下面先罗列出此自定义view中所用到的主要方法,在这些基础上我们进行如题所说的控件,会变得简单起来

映射一个点到列表的位置。就是说:参数是手指按在屏幕的坐标,返回值是这个坐标所存在的item的position值。

1
2
3
4
5
6
7
8
9
/** 
* Maps a point to a position in the list.
*
* @param x X in local coordinate
* @param y Y in local coordinate
* @return The position of the item which contains the specified point, or
* {@link #INVALID_POSITION} if the point does not intersect an item.
*/
public int pointToPosition(int x, int y)

此方法是根据获得的position值来获取对应position的item的View对象。

1
2
3
4
5
6
7
8
/** 
* Returns the view at the specified position in the group.
*
* @param index the position at which to get the view from
* @return the view at the specified position or null if the position
* does not exist within the group
*/
public View getChildAt(int index)

使用以上方法需要注意,在我们日常的ListView开发中,经常需要对View对象进行复用优化。那么着就引起了接下来的问题,对象被复用后,在传入position这个位置值时,那么我们获取的对象就有可能是之前复用的。换句话说,我们的ListView优化后,屏幕上显示多少个item,也就有多少个View对象,如果有10个View对象的话,当我点击第18个item的时候,通过此方法,返回的对象实际上就是第八个item的实例,那么如何解决这个问题,Google提供了下面这个方法:

1
2
3
4
5
6
7
/** 
* Returns the position within the adapter's data set for the first item
* displayed on screen.
*
* @return The position within the adapter's data set
*/
public int getFirstVisiblePosition()

这里返回的值,是当前屏幕上显示的所有item中得第一个item的position值。所以我们的用法如下,这样我们得到的对象就是当前点击的item正在使用的View对象(如果复用,就是复用后的)。

1
getChildAt(position - getFirstVisiblePosition())

当我们拖动item的时候,为了知道此时是正在拖动的状态和拖动的item是哪个,就需要让这个item的整体布局跟着手指来移动。那么我们获取到item的布局,然后让他跟着手指移动?很抱歉,布局已经绘制了,状态是locked。重新new一个布局来跟着手指拖动?先不说这样很麻烦,对于应用中得各种各样的item布局,难道还要绘制各种各样的View来对应?google同样提供了如下方法:

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
/** 
* <p>Enables or disables the drawing cache. When the drawing cache is enabled, the next call
* to {@link #getDrawingCache()} or {@link #buildDrawingCache()} will draw the view in a
* bitmap. Calling {@link #draw(android.graphics.Canvas)} will not draw from the cache when
* the cache is enabled. To benefit from the cache, you must request the drawing cache by
* calling {@link #getDrawingCache()} and draw it on screen if the returned bitmap is not
* null.</p>
*
* <p>Enabling the drawing cache is similar to
* {@link #setLayerType(int, android.graphics.Paint) setting a layer} when hardware
* acceleration is turned off. When hardware acceleration is turned on, enabling the
* drawing cache has no effect on rendering because the system uses a different mechanism
* for acceleration which ignores the flag. If you want to use a Bitmap for the view, even
* when hardware acceleration is enabled, see {@link #setLayerType(int, android.graphics.Paint)}
* for information on how to enable software and hardware layers.</p>
*
* <p>This API can be used to manually generate
* a bitmap copy of this view, by setting the flag to <code>true</code> and calling
* {@link #getDrawingCache()}.</p>
*
* @param enabled true to enable the drawing cache, false otherwise
*
* @see #isDrawingCacheEnabled()
* @see #getDrawingCache()
* @see #buildDrawingCache()
* @see #setLayerType(int, android.graphics.Paint)
*/
public void setDrawingCacheEnabled(boolean enabled)

将View的绘制进行缓存成Bitmap,那这回简单了,拖动哪个item,将它变成皂片就可以了。

1
2
3
4
5
6
7
8
/** 
* <p>Calling this method is equivalent to calling <code>getDrawingCache(false)</code>.</p>
*
* @return A non-scaled bitmap representing this view or null if cache is disabled.
*
* @see #getDrawingCache(boolean)
*/
public Bitmap getDrawingCache()

了解了以上所有方法,那么我们可以开始我们的正式开发了,首先我们理清思路:在按下一定时间后,显示出对应item的皂片,咱后根据手指拖动,实时跟新皂片的位置,并进行item的更新,以达到替换效果。然后在停止拖动的时候释放资源。

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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
package com.example.view;  

import android.R.integer;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Vibrator;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.ImageView;
import android.widget.ListAdapter;
import android.widget.ListView;

public class CanDragListView extends ListView {

private ListAdapter mAdapter;
private WindowManager mWindowManager;
/**
* item镜像的布局参数
*/
private WindowManager.LayoutParams mWindowLayoutParams;
private WindowManager.LayoutParams mNewWindowLayoutParams;
/**
* 振动器
*/
private Vibrator mVibrator;
/**
* 选中的item的position
*/
private int mSelectedPosition;
/**
* 选中的item的View对象
*/
private View mItemView;
/**
* 用于拖拽的镜像,这里直接用一个ImageView装载Bitmap
*/
private ImageView mDragIV;
private ImageView mNewDragIv;
/**
* 选中的item的镜像Bitmap
*/
private Bitmap mBitmap;
/**
* 按下的点到所在item的上边缘的距离
*/
private int mPoint2ItemTop;

/**
* 按下的点到所在item的左边缘的距离
*/
private int mPoint2ItemLeft;

/**
* CanDragListView距离屏幕顶部的偏移量
*/
private int mOffset2Top;
/**
* CanDragListView自动向下滚动的边界值
*/
private int mDownScrollBorder;

/**
* CanDragListView自动向上滚动的边界值
*/
private int mUpScrollBorder;
/**
* CanDragListView自动滚动的速度
*/
private static final int speed = 20;

/**
* CanDragListView距离屏幕左边的偏移量
*/
private int mOffset2Left;
/**
* 状态栏的高度
*/
private int mStatusHeight;
/**
* 按下的系统时间
*/
private long mActionDownTime = 0;
/**
* 移动的系统时间
*/
private long mActionMoveTime = 0;
/**
* 默认长按事件时间是1000毫秒
*/
private long mLongClickTime = 1000;
/**
* 是否可拖拽,默认为false
*/
private boolean isDrag = false;
/**
* 按下是的x坐标
*/
private int mDownX;
/**
* 按下是的y坐标
*/
private int mDownY;

/**
* item发生变化回调的接口
*/
private OnChanageListener onChanageListener;

/**
* 设置回调接口
*
* @param onChanageListener
*/
public void setOnChangeListener(OnChanageListener onChanageListener) {
this.onChanageListener = onChanageListener;
}

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

public CanDragListView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public CanDragListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mAdapter = getAdapter();
mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
mStatusHeight = getStatusHeight(context); // 获取状态栏的高度
}

private Handler mHandler = new Handler();

// 用来处理长按的Runnable
private Runnable mLongClickRunnable = new Runnable() {

@Override
public void run() {
isDrag = true; // 设置可以拖拽
mVibrator.vibrate(100); // 震动100毫秒
if (mItemView != null) {
mItemView.setVisibility(View.INVISIBLE);// 隐藏该item
}
Log.i("CanDragListView", "**mLongClickRunnable**");
// 根据我们按下的点显示item镜像
createDragImage(mBitmap, mDownX, mDownY);
}
};

/**
* 当mDownY的值大于向上滚动的边界值,触发自动向上滚动 当mDownY的值小于向下滚动的边界值,触犯自动向下滚动 否则不进行滚动
*/
private Runnable mScrollRunnable = new Runnable() {

@Override
public void run() {
int scrollY;
if (mDownY > mUpScrollBorder) {
scrollY = speed;
mHandler.postDelayed(mScrollRunnable, 25);
} else if (mDownY < mDownScrollBorder) {
scrollY = -speed;
mHandler.postDelayed(mScrollRunnable, 25);
} else {
scrollY = 0;
mHandler.removeCallbacks(mScrollRunnable);
}

// 所以我们在这里调用下onSwapItem()方法来交换item
onSwapItem(mDownY, mDownY);

smoothScrollBy(scrollY, 10);
}
};

@Override
public boolean onTouchEvent(MotionEvent event) {
// Log.i("CanDragListView", mSelectedPosition+"****"+mItemView);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mActionDownTime = event.getDownTime();
mDownX = (int) event.getX();
mDownY = (int) event.getY();

// 根据按下的坐标获取item对应的position
mSelectedPosition = pointToPosition(mDownX, mDownY);
// 如果是无效的position,即值为-1
if (mSelectedPosition == AdapterView.INVALID_POSITION) {
return super.onTouchEvent(event);
}
// 根据position获取对应的item
mItemView = getChildAt(mSelectedPosition - getFirstVisiblePosition());
// 使用Handler延迟mLongClickTime执行mLongClickRunnable
mHandler.postDelayed(mLongClickRunnable, mLongClickTime);
if (mItemView != null) {
// 下面这几个距离大家可以参考我的博客上面的图来理解下
mPoint2ItemTop = mDownY - mItemView.getTop();
mPoint2ItemLeft = mDownX - mItemView.getLeft();

mOffset2Top = (int) (event.getRawY() - mDownY);
mOffset2Left = (int) (event.getRawX() - mDownX);

// 获取CanDragListView自动向上滚动的偏移量,小于这个值,CanDragListView向下滚动
mDownScrollBorder = getHeight() / 4;
// 获取CanDragListView自动向下滚动的偏移量,大于这个值,CanDragListView向上滚动
mUpScrollBorder = getHeight() * 3 / 4;

// 将该item进行绘图缓存
mItemView.setDrawingCacheEnabled(true);
// 从缓存中获取bitmap
mBitmap = Bitmap.createBitmap(mItemView.getDrawingCache());
// 释放绘图缓存,避免出现重复的缓存对象
mItemView.destroyDrawingCache();
}

// Log.i("CanDragListView", "****"+isDrag);
break;
case MotionEvent.ACTION_MOVE:
// TODO
if (isDrag) {
int moveX = (int) event.getX();
int moveY = (int) event.getY();
if (!isOnTouchInItem(mItemView, moveX, moveY)) {
mHandler.removeCallbacks(mLongClickRunnable);
}
mDownX = moveX;
mDownY = moveY;
onDragItem(moveX, moveY);
}
break;
case MotionEvent.ACTION_UP:
onStopDrag();
mHandler.removeCallbacks(mLongClickRunnable);
mHandler.removeCallbacks(mScrollRunnable);

isDrag = false;
break;
default:
break;
}

return super.onTouchEvent(event);
}

/**
* 判断手指按下的坐标是否在item范围内
*
* @param view
* @param downX
* @param downY
* @return
*/
private boolean isOnTouchInItem(View view, int downX, int downY) {
if (view == null) {
return false;
}
int leftX = view.getLeft();
int topY = view.getTop();
if (downX < leftX || downX > leftX + view.getWidth()) {
return false;
}
if (downY < topY || downY > topY + view.getHeight()) {
return false;
}
return true;
}

/**
* 创建拖动的镜像
*
* @param bitmap
* @param downX
* 按下的点相对父控件的X坐标
* @param downY
* 按下的点相对父控件的X坐标
*/
private void createDragImage(Bitmap bitmap, int downX, int downY) {
mWindowLayoutParams = new WindowManager.LayoutParams();
mWindowLayoutParams.format = PixelFormat.TRANSLUCENT; // 图片之外的其他地方透明
mWindowLayoutParams.gravity = Gravity.TOP | Gravity.LEFT;
mWindowLayoutParams.x = downX - mPoint2ItemLeft + mOffset2Left;
mWindowLayoutParams.y = downY - mPoint2ItemTop + mOffset2Top - mStatusHeight;
mWindowLayoutParams.alpha = 0.55f; // 透明度
mWindowLayoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
mWindowLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
mWindowLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;

mNewWindowLayoutParams = new WindowManager.LayoutParams();
mNewWindowLayoutParams.format = PixelFormat.TRANSLUCENT; // 图片之外的其他地方透明
mNewWindowLayoutParams.gravity = Gravity.TOP | Gravity.LEFT;
mNewWindowLayoutParams.x = downX - mPoint2ItemLeft + mOffset2Left;
mNewWindowLayoutParams.y = downY - mPoint2ItemTop + mOffset2Top - mStatusHeight;
// mNewWindowLayoutParams.alpha = 0.55f; // 透明度
mNewWindowLayoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
mNewWindowLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
mNewWindowLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;

mDragIV = new ImageView(getContext());
mNewDragIv = new ImageView(getContext());
mDragIV.setImageBitmap(bitmap);
mWindowManager.addView(mDragIV, mWindowLayoutParams);
mWindowManager.addView(mNewDragIv, mNewWindowLayoutParams);
}

/**
* 移除镜像
*/
private void removeDragImage() {
if (mDragIV != null) {
mWindowManager.removeView(mDragIV);
mDragIV = null;
}
if (mNewDragIv != null) {
mWindowManager.removeView(mNewDragIv);
mNewDragIv = null;
}
}

/**
* 拖动item,在里面实现了item镜像的位置更新,item的相互交换以及ListView的自行滚动
*
* @param x
* @param y
*/
private void onDragItem(int moveX, int moveY) {
if (mWindowLayoutParams != null && mDragIV != null) {
mWindowLayoutParams.x = moveX - mPoint2ItemLeft + mOffset2Left;
mWindowLayoutParams.y = moveY - mPoint2ItemTop + mOffset2Top - mStatusHeight;
mWindowManager.updateViewLayout(mDragIV, mWindowLayoutParams); // 更新镜像的位置
}
onSwapItem(moveX, moveY);
// ListView自动滚动
mHandler.post(mScrollRunnable);

}

/**
* 交换item,并且控制item之间的显示与隐藏效果
*
* @param moveX
* @param moveY
*/
private void onSwapItem(int moveX, int moveY) {
// 获取我们手指移动到的那个item的position
int position = pointToPosition(moveX, moveY);

// 假如tempPosition 改变了并且tempPosition不等于-1,则进行交换
if (position != mSelectedPosition && position != AdapterView.INVALID_POSITION) {

// mAdapter.getItem(mSelectedPosition);
View newItem = getChildAt(position - getFirstVisiblePosition());
View oldItem = getChildAt(mSelectedPosition - getFirstVisiblePosition());

mNewWindowLayoutParams.x = moveX - (moveX - oldItem.getLeft()) + mOffset2Left;
mNewWindowLayoutParams.y = moveY - (moveY - oldItem.getTop()) + mOffset2Top - mStatusHeight;

newItem.setDrawingCacheEnabled(true);
Bitmap bitmap = Bitmap.createBitmap(newItem.getDrawingCache());
newItem.destroyDrawingCache();
mNewDragIv.setImageBitmap(bitmap);
if (newItem != null && oldItem != null) {
newItem.setVisibility(INVISIBLE);// 隐藏拖动到的位置的item
oldItem.setVisibility(VISIBLE);//显示之前的
mWindowManager.updateViewLayout(mNewDragIv, mNewWindowLayoutParams); // 更新镜像的位置
if (onChanageListener != null) {
Log.i("CanDragListView", "**onSwapItem**");
onChanageListener.onChange(mSelectedPosition, position);
}
}

mSelectedPosition = position;
}
}

/**
* 停止拖拽我们将之前隐藏的item显示出来,并将镜像移除
*/
private void onStopDrag() {
View view = getChildAt(mSelectedPosition - getFirstVisiblePosition());
if (view != null) {
view.setVisibility(View.VISIBLE);
}
// ((DragAdapter)this.getAdapter()).setItemHide(-1);
removeDragImage();
}

/**
* 获取状态栏的高度
*
* @param context
* @return
*/
private static int getStatusHeight(Context context) {
int statusHeight = 0;
Rect localRect = new Rect();
((Activity) context).getWindow().getDecorView().getWindowVisibleDisplayFrame(localRect);
statusHeight = localRect.top;
if (0 == statusHeight) {
Class<?> localClass;
try {
localClass = Class.forName("com.android.internal.R$dimen");
Object localObject = localClass.newInstance();
int i5 = Integer.parseInt(localClass.getField("status_bar_height").get(localObject).toString());
statusHeight = context.getResources().getDimensionPixelSize(i5);
} catch (Exception e) {
e.printStackTrace();
}
}
return statusHeight;
}

/**
* 监听数据拖拽的接口,用来更新数据显示
*/
public interface OnChanageListener {

/**
* 当item交换位置的时候回调的方法,在此处实现数据的交换
*
* @param start
* 开始的position
* @param to
* 拖拽到的position
*/
public void onChange(int start, int to);
}

}

在代码里定义了改变的回调接口,目的是对item进行交换并且更新。开始我是想将item的更新都固定在自定义的ListView中,这样拿来就能直接用。

对于item的更新,一定要在Activity中实现OnChanageListener后,在方法里对数据集合进行元素位置交换。这里用的方法是:

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
	/** 
* Swaps the elements of list {@code list} at indices {@code index1} and
* {@code index2}.
*
* @param list
* the list to manipulate.
* @param index1
* position of the first element to swap with the element in
* index2.
* @param index2
* position of the other element.
*
* @throws IndexOutOfBoundsException
* if index1 or index2 is out of range of this list.
* @since 1.4
*/
@SuppressWarnings("unchecked")
public static void swap(List<?> list, int index1, int index2)

参数是要交换数据的集合,和要交换的两个元素的位置。用法是:

mListView.setOnChangeListener(new OnChanageListener() {

@Override
public void onChange(int start, int to) {
//数据交换
if(start < to){
for(int i=start; i<to; i++){
Collections.swap(mList, i, i+1);
}
}else if(start > to){
for(int i=start; i>to; i--){
Collections.swap(mList, i, i-1);
}
}
mAdapter.notifyDataSetChanged();
}
});
```

到此,可拖拽的ListView完成。

xml布局文件示例如下:

```xml
<?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="match_parent"
android:orientation="vertical" >

<include
android:layout_width="match_parent"
android:layout_height="wrap_content"
layout="@layout/include_title" />

<com.qiyuan.activity.view.CanDragListView
android:id="@+id/main_activity_lv"
android:dividerHeight="0.2dp"
android:divider="@color/blueviolet"
android:footerDividersEnabled="false"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />

</LinearLayout>