Android 可拖拽的GridView效果实现, 长按可拖拽和item实时交换
在Android开发中,我们常常用到ListView和GridView,而有的时候系统的ListView,GridView并不能满足我们的需求,所以我们需要自己定义一个ListView或者GridView,我的上一篇文章中就是自定义的一个左右滑动删除item的例子,大家有兴趣的可以去看看 Android 使用Scroller实现绚丽的ListView左右滑动删除Item效果,今天这篇文章就给大家来自定义GridView的控件,GridView主要是来显示网格的控件,在Android的开发中使用很普通,相对于TextView,Button这些控件来说要来的复杂些,今天给大家带来长按GridView的item,然后将其拖拽其他item上面,使得GridView的item发生交换,比较典型的就是我们的Launcher,网上有很多关于GridView的拖动的Demo,但是大部分都是相同的,而且存在一些Bug,而且大部分都是点击GridView的item然后进行拖动,或者item之间不进行实时交换,今天给大家更加详细的介绍GridView拖拽,并且将Demo做的更完美,大家更容易接受,也许很多人听到这个感觉实现起来很复杂,就关掉的这篇文章,其实告诉大家,只要知道了思路就感觉一点都不复杂了,不信大家可以接着往下看看,首先还是跟大家说说实现的思路
- 根据手指按下的X,Y坐标来获取我们在GridView上面点击的item
- 手指按下的时候使用Handler和Runnable来实现一个定时器,假如定时时间为1000毫秒,在1000毫秒内,如果手指抬起了移除定时器,没有抬起并且手指点击在GridView的item所在的区域,则表示我们长按了GridView的item
- 如果我们长按了item则隐藏item,然后使用WindowManager来添加一个item的镜像在屏幕用来代替刚刚隐藏的item
- 当我们手指在屏幕移动的时候,更新item镜像的位置,然后在根据我们移动的X,Y的坐标来获取移动到GridView的哪一个位置
- 到GridView的item过多的时候,可能一屏幕显示不完,我们手指拖动item镜像到屏幕下方,要触发GridView想上滚动,同理,当我们手指拖动item镜像到屏幕上面,触发GridView向下滚动
- GridView交换数据,刷新界面,移除item的镜像
看完上面的这些思路你是不是找到了些感觉了呢,心里痒痒的想动手试试吧,好吧,接下来就带大家根据思路来实现可拖拽的GridView,新建一个项目就叫DragGridView
新建一个类DragGridView继承GridView,先来看看DragGridView的代码,然后在根据代码进行相关的讲解
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
|
package
com.example.draggridview;
import
android.app.Activity;
import
android.content.Context;
import
android.graphics.Bitmap;
import
android.graphics.PixelFormat;
import
android.graphics.Rect;
import
android.os.Handler;
import
android.os.Vibrator;
import
android.util.AttributeSet;
import
android.view.Gravity;
import
android.view.MotionEvent;
import
android.view.View;
import
android.view.WindowManager;
import
android.widget.AdapterView;
import
android.widget.GridView;
import
android.widget.ImageView;
/**
*
* @author xiaanming
*
*/
public
class
DragGridView
extends
GridView{
/**
* DragGridView的item长按响应的时间, 默认是1000毫秒,也可以自行设置
*/
private
long
dragResponseMS =
1000
;
/**
* 是否可以拖拽,默认不可以
*/
private
boolean
isDrag =
false
;
private
int
mDownX;
private
int
mDownY;
private
int
moveX;
private
int
moveY;
/**
* 正在拖拽的position
*/
private
int
mDragPosition;
/**
* 刚开始拖拽的item对应的View
*/
private
View mStartDragItemView =
null
;
/**
* 用于拖拽的镜像,这里直接用一个ImageView
*/
private
ImageView mDragImageView;
/**
* 震动器
*/
private
Vibrator mVibrator;
private
WindowManager mWindowManager;
/**
* item镜像的布局参数
*/
private
WindowManager.LayoutParams mWindowLayoutParams;
/**
* 我们拖拽的item对应的Bitmap
*/
private
Bitmap mDragBitmap;
/**
* 按下的点到所在item的上边缘的距离
*/
private
int
mPoint2ItemTop ;
/**
* 按下的点到所在item的左边缘的距离
*/
private
int
mPoint2ItemLeft;
/**
* DragGridView距离屏幕顶部的偏移量
*/
private
int
mOffset2Top;
/**
* DragGridView距离屏幕左边的偏移量
*/
private
int
mOffset2Left;
/**
* 状态栏的高度
*/
private
int
mStatusHeight;
/**
* DragGridView自动向下滚动的边界值
*/
private
int
mDownScrollBorder;
/**
* DragGridView自动向上滚动的边界值
*/
private
int
mUpScrollBorder;
/**
* DragGridView自动滚动的速度
*/
private
static
final
int
speed =
20
;
/**
* item发生变化回调的接口
*/
private
OnChanageListener onChanageListener;
public
DragGridView(Context context) {
this
(context,
null
);
}
public
DragGridView(Context context, AttributeSet attrs) {
this
(context, attrs,
0
);
}
public
DragGridView(Context context, AttributeSet attrs,
int
defStyle) {
super
(context, attrs, defStyle);
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(
50
);
//震动一下
mStartDragItemView.setVisibility(View.INVISIBLE);
//隐藏该item
//根据我们按下的点显示item镜像
createDragImage(mDragBitmap, mDownX, mDownY);
}
};
/**
* 设置回调接口
* @param onChanageListener
*/
public
void
setOnChangeListener(OnChanageListener onChanageListener){
this
.onChanageListener = onChanageListener;
}
/**
* 设置响应拖拽的毫秒数,默认是1000毫秒
* @param dragResponseMS
*/
public
void
setDragResponseMS(
long
dragResponseMS) {
this
.dragResponseMS = dragResponseMS;
}
@Override
public
boolean
dispatchTouchEvent(MotionEvent ev) {
switch
(ev.getAction()){
case
MotionEvent.ACTION_DOWN:
mDownX = (
int
) ev.getX();
mDownY = (
int
) ev.getY();
//根据按下的X,Y坐标获取所点击item的position
mDragPosition = pointToPosition(mDownX, mDownY);
if
(mDragPosition == AdapterView.INVALID_POSITION){
return
super
.dispatchTouchEvent(ev);
}
//使用Handler延迟dragResponseMS执行mLongClickRunnable
mHandler.postDelayed(mLongClickRunnable, dragResponseMS);
//根据position获取该item所对应的View
mStartDragItemView = getChildAt(mDragPosition - getFirstVisiblePosition());
//下面这几个距离大家可以参考我的博客上面的图来理解下
mPoint2ItemTop = mDownY - mStartDragItemView.getTop();
mPoint2ItemLeft = mDownX - mStartDragItemView.getLeft();
mOffset2Top = (
int
) (ev.getRawY() - mDownY);
mOffset2Left = (
int
) (ev.getRawX() - mDownX);
//获取DragGridView自动向上滚动的偏移量,小于这个值,DragGridView向下滚动
mDownScrollBorder = getHeight() /
4
;
//获取DragGridView自动向下滚动的偏移量,大于这个值,DragGridView向上滚动
mUpScrollBorder = getHeight() *
3
/
4
;
//开启mDragItemView绘图缓存
mStartDragItemView.setDrawingCacheEnabled(
true
);
//获取mDragItemView在缓存中的Bitmap对象
mDragBitmap = Bitmap.createBitmap(mStartDragItemView.getDrawingCache());
//这一步很关键,释放绘图缓存,避免出现重复的镜像
mStartDragItemView.destroyDrawingCache();
break
;
case
MotionEvent.ACTION_MOVE:
int
moveX = (
int
)ev.getX();
int
moveY = (
int
) ev.getY();
//如果我们在按下的item上面移动,只要不超过item的边界我们就不移除mRunnable
if
(!isTouchInItem(mStartDragItemView, moveX, moveY)){
mHandler.removeCallbacks(mLongClickRunnable);
}
break
;
case
MotionEvent.ACTION_UP:
mHandler.removeCallbacks(mLongClickRunnable);
mHandler.removeCallbacks(mScrollRunnable);
break
;
}
return
super
.dispatchTouchEvent(ev);
}
/**
* 是否点击在GridView的item上面
* @param itemView
* @param x
* @param y
* @return
*/
private
boolean
isTouchInItem(View dragView,
int
x,
int
y){
if
(dragView ==
null
){
return
false
;
}
int
leftOffset = dragView.getLeft();
int
topOffset = dragView.getTop();
if
(x < leftOffset || x > leftOffset + dragView.getWidth()){
return
false
;
}
if
(y < topOffset || y > topOffset + dragView.getHeight()){
return
false
;
}
return
true
;
}
@Override
public
boolean
onTouchEvent(MotionEvent ev) {
if
(isDrag && mDragImageView !=
null
){
switch
(ev.getAction()){
case
MotionEvent.ACTION_MOVE:
moveX = (
int
) ev.getX();
moveY = (
int
) ev.getY();
//拖动item
onDragItem(moveX, moveY);
break
;
case
MotionEvent.ACTION_UP:
onStopDrag();
isDrag =
false
;
break
;
}
return
true
;
}
return
super
.onTouchEvent(ev);
}
/**
* 创建拖动的镜像
* @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 ;
mDragImageView =
new
ImageView(getContext());
mDragImageView.setImageBitmap(bitmap);
mWindowManager.addView(mDragImageView, mWindowLayoutParams);
}
/**
* 从界面上面移动拖动镜像
*/
private
void
removeDragImage(){
if
(mDragImageView !=
null
){
mWindowManager.removeView(mDragImageView);
mDragImageView =
null
;
}
}
/**
* 拖动item,在里面实现了item镜像的位置更新,item的相互交换以及GridView的自行滚动
* @param x
* @param y
*/
private
void
onDragItem(
int
moveX,
int
moveY){
mWindowLayoutParams.x = moveX - mPoint2ItemLeft + mOffset2Left;
mWindowLayoutParams.y = moveY - mPoint2ItemTop + mOffset2Top - mStatusHeight;
mWindowManager.updateViewLayout(mDragImageView, mWindowLayoutParams);
//更新镜像的位置
onSwapItem(moveX, moveY);
//GridView自动滚动
mHandler.post(mScrollRunnable);
}
/**
* 当moveY的值大于向上滚动的边界值,触发GridView自动向上滚动
* 当moveY的值小于向下滚动的边界值,触犯GridView自动向下滚动
* 否则不进行滚动
*/
private
Runnable mScrollRunnable =
new
Runnable() {
@Override
public
void
run() {
int
scrollY;
if
(moveY > mUpScrollBorder){
scrollY = speed;
mHandler.postDelayed(mScrollRunnable,
25
);
}
else
if
(moveY < mDownScrollBorder){
scrollY = -speed;
mHandler.postDelayed(mScrollRunnable,
25
);
}
else
{
scrollY =
0
;
mHandler.removeCallbacks(mScrollRunnable);
}
//当我们的手指到达GridView向上或者向下滚动的偏移量的时候,可能我们手指没有移动,但是DragGridView在自动的滚动
//所以我们在这里调用下onSwapItem()方法来交换item
onSwapItem(moveX, moveY);
smoothScrollBy(scrollY,
10
);
}
};
/**
* 交换item,并且控制item之间的显示与隐藏效果
* @param moveX
* @param moveY
*/
private
void
onSwapItem(
int
moveX,
int
moveY){
//获取我们手指移动到的那个item的position
int
tempPosition = pointToPosition(moveX, moveY);
//假如tempPosition 改变了并且tempPosition不等于-1,则进行交换
if
(tempPosition != mDragPosition && tempPosition != AdapterView.INVALID_POSITION){
if
(onChanageListener !=
null
){
onChanageListener.onChange(mDragPosition, tempPosition);
}
getChildAt(tempPosition - getFirstVisiblePosition()).setVisibility(View.INVISIBLE);
//拖动到了新的item,新的item隐藏掉
getChildAt(mDragPosition - getFirstVisiblePosition()).setVisibility(View.VISIBLE);
//之前的item显示出来
mDragPosition = tempPosition;
}
}
/**
* 停止拖拽我们将之前隐藏的item显示出来,并将镜像移除
*/
private
void
onStopDrag(){
View view = getChildAt(mDragPosition - 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;
}
/**
*
* @author xiaanming
*
*/
public
interface
OnChanageListener{
/**
* 当item交换位置的时候回调的方法,我们只需要在该方法中实现数据的交换即可
* @param form
* 开始的position
* @param to
* 拖拽到的position
*/
public
void
onChange(
int
form,
int
to);
}
}
|
首先看DragGridView的事件分发方法,不了解Android事件分发的可以先去了解下,Android事件分发对于自定义控件很重要,简单说下,当我们点击DragGridView的Item,先会去执行dispatchTouchEvent()方法将事件分发下去,所以我们要重写dispatchTouchEvent()方法在手指按下的时候根据pointToPosition()方法来获取我们按下的item的position,根据getChildAt()方法来获取该position上面所对应的View, 并且开启长按的定时器,默认时间为1000毫秒,如果在1000毫秒内手指抬起或者手指在屏幕上滑动出了该item,则取消长按定时器,否则就表示可以进行拖拽,手机友好的震动一下,隐藏我们长按的Item,屏幕调用createDragImage()方法来创建我们长按的item的镜像,创建Item的镜像使用的是WindowManager类,该类可以创建一个窗体显示在Activity之上,
再此之前大家先要理解这几个距离,理解这几个距离之前要首先知道getRawX(),getRawY()和getX(),getY()的区别,getRawX(),getRawY()是相对于屏幕的原点的距离,而getX(),getY()是相对于控件左上方的点的距离,为了方便大家理解我用Word简单的画了下图,画得不好,大家将就的看下,红色框框为我们的GridView
- mPoint2ItemTop 手指按下的点到该Item的上边缘的距离,如上图的1号线
- mPoint2ItemLeft 手指按下的点到该Item的左边缘的距离,如上图的2号线
- mOffset2Top DragGridView的上边缘到屏幕上边缘的距离,如上图的3号线,这个距离包裹状态栏,标题栏,或者一些在DragGridView上面的布局的高度,这个很重要我们现实Item镜像需要用到
- mOffset2Left DragGridView的左边缘到屏幕左边缘的距离,如上图的4号线,我这个Demo的这个距离为0,因为我设置DragGridView的宽度为充满屏幕,但是我们要考虑假如DragGridView与屏幕左边缘设置了间隙或者左边有其他的布局的情形
- mDownScrollBorder 这个距离表示当DragGridView的item过多的时候,手机一屏显示不完全,我们拖动Item镜像到这个高度的时候,DragGridView自动向下滚动,如上图的5号线
- .mUpScrollBorder 这个和mDownScrollBorder相反,当我们大于这个高度的时候,DragGridView自动向上滚动,如上图的6号线
手指离开界面,将item的镜像移除,并将拖拽到的item显示出来,这样子就实现了GirdView的拖拽效果啦,接下来我们来使用下我们自定义可拖拽的GridView吧,先看主界面布局,只有我们自定义的一个DragGridView
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
android:layout_width
=
"match_parent"
android:layout_height
=
"match_parent"
>
<
com.example.draggridview.DragGridView
android:id
=
"@+id/dragGridView"
android:listSelector
=
"@android:color/transparent"
android:layout_width
=
"match_parent"
android:layout_height
=
"match_parent"
android:cacheColorHint
=
"@android:color/transparent"
android:verticalSpacing
=
"10dip"
android:horizontalSpacing
=
"10dip"
android:stretchMode
=
"columnWidth"
android:gravity
=
"center"
android:numColumns
=
"3"
>
</
com.example.draggridview.DragGridView
>
</
RelativeLayout
>
|
接下来我们看看DragGridView的item的布局,上面一个ImageView下面一个TextView
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
<?
xml
version
=
"1.0"
encoding
=
"utf-8"
?>
android:layout_width
=
"fill_parent"
android:layout_height
=
"wrap_content"
android:background
=
"@android:color/transparent"
>
<
ImageView
android:id
=
"@+id/item_image"
android:scaleType
=
"centerCrop"
android:layout_width
=
"wrap_content"
android:layout_height
=
"wrap_content"
android:layout_centerHorizontal
=
"true"
>
</
ImageView
>
<
TextView
android:id
=
"@+id/item_text"
android:layout_width
=
"wrap_content"
android:layout_height
=
"wrap_content"
android:layout_below
=
"@+id/item_image"
android:layout_centerHorizontal
=
"true"
>
</
TextView
>
</
RelativeLayout
>
|
布局搞定了我们就来看看主页面MainActivity的代码吧
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
|
package
com.example.draggridview;
import
java.util.ArrayList;
import
java.util.Collections;
import
java.util.HashMap;
import
java.util.List;
import
android.app.Activity;
import
android.os.Bundle;
import
android.widget.SimpleAdapter;
import
com.example.draggridview.DragGridView.OnChanageListener;
/**
*
* @author xiaanming
*
*/
public
class
MainActivity
extends
Activity {
private
List<HashMap<String, Object>> dataSourceList =
new
ArrayList<HashMap<String, Object>>();
@Override
protected
void
onCreate(Bundle savedInstanceState) {
super
.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
DragGridView mDragGridView = (DragGridView) findViewById(R.id.dragGridView);
for
(
int
i =
0
; i <
30
; i++) {
HashMap<String, Object> itemHashMap =
new
HashMap<String, Object>();
itemHashMap.put(
"item_image"
,R.drawable.com_tencent_open_notice_msg_icon_big);
itemHashMap.put(
"item_text"
,
"拖拽 "
+ Integer.toString(i));
dataSourceList.add(itemHashMap);
}
final
SimpleAdapter mSimpleAdapter =
new
SimpleAdapter(
this
, dataSourceList,
R.layout.grid_item,
new
String[] {
"item_image"
,
"item_text"
},
new
int
[] { R.id.item_image, R.id.item_text });
mDragGridView.setAdapter(mSimpleAdapter);
mDragGridView.setOnChangeListener(
new
OnChanageListener() {
@Override
public
void
onChange(
int
from,
int
to) {
HashMap<String, Object> temp = dataSourceList.get(from);
//直接交互item
// dataSourceList.set(from, dataSourceList.get(to));
// dataSourceList.set(to, temp);
// dataSourceList.set(to, temp);
//这里的处理需要注意下
if
(from < to){
for
(
int
i=from; i<to; i++){
Collections.swap(dataSourceList, i, i+
1
);
}
}
else
if
(from > to){
for
(
int
i=from; i>to; i--){
Collections.swap(dataSourceList, i, i-
1
);
}
}
dataSourceList.set(to, temp);
mSimpleAdapter.notifyDataSetChanged();
}
});
}
}
|
这里面的代码还是比较简单,主要讲下onChange()方法,我们要为mDragGridView设置一个OnChanageListener的回调接口,在onChange()方法里面实现数据的交换逻辑,第一个参数from为item开始的位置,第二个参数to为item拖拽到的位置,刚开始我使用的交换逻辑是
1
2
3
4
|
HashMap<String, Object> temp = dataSourceList.get(from);
//直接交互item
// dataSourceList.set(from, dataSourceList.get(to));
// dataSourceList.set(to, temp);
|
直接交换的item的数据,然后看了下网易新闻的拖拽的GridView,他不是直接实现两个item直接的数据交换,所以将数据交换逻辑改成了下面的方式
简单说下,数据的交换逻辑,比如我们将position从5拖拽到7这个位置,我注释掉的逻辑是直接将5和7的数据交换,而后面的那种逻辑是将6的位置数据移动到5,将7的位置移动到6,然后再7显示5 6->5, 7->6, 5->7不知道大家理解了没有。