如果大家不知道GridView基本使用,可以先参见:Android(java)学习笔记154:使用GridView以及重写BaseAdapter
1. 首先我们明白GridView拖拽的思路:
(1)根据手指按下的X,Y坐标来获取我们在GridView上面点击的item。
(2)手指按下的时候使用Handler和Runnable来实现一个定时器,假如定时时间为1000毫秒,在1000毫秒内,如果手指抬起了移除定时器,没有抬起并且手指点击在GridView的item所在的区域,则表示我们长按了GridView的item。
(3)如果我们长按了item则隐藏item,然后使用WindowManager来添加一个item的镜像在屏幕用来代替刚刚隐藏的item。
(4)当我们手指在屏幕移动的时候,更新item镜像的位置,然后在根据我们移动的X,Y的坐标来获取移动到GridView的那一个位置。
(5)到GridView的item过多的时候,可能一屏幕显示不完,我们手指拖动item镜像到屏幕下方,要触发GridView想上滚动,同理,当我们手指拖动item镜像到屏幕上面,触发GridView向下滚动。
(6)GridView交换数据,刷新界面,移除item的镜像。
2. 大家根据以上的思路来实现可拖拽的GridView:
(1)新建一个类DragGridView继承GridView:
1 package com.example.draggridview; 2 3 import android.annotation.SuppressLint; 4 import android.app.Activity; 5 import android.content.Context; 6 import android.graphics.Bitmap; 7 import android.graphics.PixelFormat; 8 import android.graphics.Rect; 9 import android.os.Handler; 10 import android.os.Vibrator; 11 import android.util.AttributeSet; 12 import android.view.Gravity; 13 import android.view.MotionEvent; 14 import android.view.View; 15 import android.view.WindowManager; 16 import android.widget.AdapterView; 17 import android.widget.GridView; 18 import android.widget.ImageView; 19 20 /** 21 * 22 * 23 * @author hebao 24 * 25 */ 26 @SuppressLint("NewApi") 27 public class DragGridView extends GridView{ 28 /** 29 * DragGridView的item长按响应的时间, 默认是1000毫秒,也可以自行设置 30 */ 31 private long dragResponseMS = 1000; 32 33 /** 34 * 是否可以拖拽,默认不可以 35 */ 36 private boolean isDrag = false; 37 38 private int mDownX; 39 private int mDownY; 40 private int moveX; 41 private int moveY; 42 /** 43 * 正在拖拽的position 44 */ 45 private int mDragPosition; 46 47 /** 48 * 刚开始拖拽的item对应的View 49 */ 50 private View mStartDragItemView = null; 51 52 /** 53 * 用于拖拽的镜像,这里直接用一个ImageView 54 */ 55 private ImageView mDragImageView; 56 57 /** 58 * 震动器 59 */ 60 private Vibrator mVibrator; 61 62 private WindowManager mWindowManager; 63 /** 64 * item镜像的布局参数 65 */ 66 private WindowManager.LayoutParams mWindowLayoutParams; 67 68 /** 69 * 我们拖拽的item对应的Bitmap 70 */ 71 private Bitmap mDragBitmap; 72 73 /** 74 * 按下的点到所在item的上边缘的距离 75 */ 76 private int mPoint2ItemTop ; 77 78 /** 79 * 按下的点到所在item的左边缘的距离 80 */ 81 private int mPoint2ItemLeft; 82 83 /** 84 * DragGridView距离屏幕顶部的偏移量 85 */ 86 private int mOffset2Top; 87 88 /** 89 * DragGridView距离屏幕左边的偏移量 90 */ 91 private int mOffset2Left; 92 93 /** 94 * 状态栏的高度 95 */ 96 private int mStatusHeight; 97 98 /** 99 * DragGridView自动向下滚动的边界值 100 */ 101 private int mDownScrollBorder; 102 103 /** 104 * DragGridView自动向上滚动的边界值 105 */ 106 private int mUpScrollBorder; 107 108 /** 109 * DragGridView自动滚动的速度 110 */ 111 private static final int speed = 80; 112 113 /** 114 * item发生变化回调的接口 115 */ 116 private OnChanageListener onChanageListener; 117 118 119 120 public DragGridView(Context context) { 121 this(context, null); 122 } 123 124 public DragGridView(Context context, AttributeSet attrs) { 125 this(context, attrs, 0); 126 } 127 128 public DragGridView(Context context, AttributeSet attrs, int defStyle) { 129 super(context, attrs, defStyle); 130 mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); 131 mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 132 mStatusHeight = getStatusHeight(context); //获取状态栏的高度 133 } 134 135 private Handler mHandler = new Handler(); 136 137 //用来处理是否为长按的Runnable 138 private Runnable mLongClickRunnable = new Runnable() { 139 140 @Override 141 public void run() { 142 isDrag = true; //设置可以拖拽 143 mVibrator.vibrate(50); //震动一下 144 mStartDragItemView.setVisibility(View.INVISIBLE);//隐藏该item 145 146 //根据我们按下的点显示item镜像 147 createDragImage(mDragBitmap, mDownX, mDownY); 148 } 149 }; 150 151 /** 152 * 设置回调接口 153 * @param onChanageListener 154 */ 155 public void setOnChangeListener(OnChanageListener onChanageListener){ 156 this.onChanageListener = onChanageListener; 157 } 158 159 /** 160 * 设置响应拖拽的毫秒数,默认是1000毫秒 161 * @param dragResponseMS 162 */ 163 public void setDragResponseMS(long dragResponseMS) { 164 this.dragResponseMS = dragResponseMS; 165 } 166 167 @Override 168 public boolean dispatchTouchEvent(MotionEvent ev) { 169 switch(ev.getAction()){ 170 case MotionEvent.ACTION_DOWN: 171 //使用Handler延迟dragResponseMS执行mLongClickRunnable 172 /** 173 * postDelayed(Runnable r, long delayMillis) 174 * 参数1: Runnable接口,要执行的内容 175 * 参数2: 延迟时间delayMillis(单位:ms)后执行Runnable接口的内容 176 */ 177 mHandler.postDelayed(mLongClickRunnable, dragResponseMS); 178 179 //getX(),getY()相对于容器的位置坐标,这里的容器是GridView 180 mDownX = (int) ev.getX(); 181 mDownY = (int) ev.getY(); 182 183 //根据按下的X,Y坐标获取所点击item的position,即依据触摸点的X,Y坐标计算出点击的是GridVIew的哪个Item 184 mDragPosition = pointToPosition(mDownX, mDownY); 185 /** 186 * INVALID_POSITION:代表无效的位置。 187 * AdapterView: 188 * 1.作用:以列表的形式显示数据。 189 * 2.内容:AdapterView的内容一般是包含多项相同格式资源的列表。 190 */ 191 if(mDragPosition == AdapterView.INVALID_POSITION){ 192 return super.dispatchTouchEvent(ev); 193 } 194 195 196 //public int getFirstVisiblePosition(): 197 // 作用: 来获取当前可见的第1个item的position 198 // return: 返回的是当前可见的第1个item的position 199 //根据position获取该item所对应的View 200 mStartDragItemView = getChildAt(mDragPosition - getFirstVisiblePosition()); 201 202 //下面这几个距离根据下面绘制图理解 203 mPoint2ItemTop = mDownY - mStartDragItemView.getTop(); 204 mPoint2ItemLeft = mDownX - mStartDragItemView.getLeft(); 205 206 //getRawX(),getRawY() 相对于屏幕位置坐标 207 mOffset2Top = (int) (ev.getRawY() - mDownY); 208 mOffset2Left = (int) (ev.getRawX() - mDownX); 209 210 //获取DragGridView自动向上滚动的偏移量,小于这个值,DragGridView向下滚动 211 mDownScrollBorder = getHeight() /4; 212 //获取DragGridView自动向下滚动的偏移量,大于这个值,DragGridView向上滚动 213 mUpScrollBorder = getHeight() * 3/4; 214 215 216 217 //开启mDragItemView绘图缓存 218 mStartDragItemView.setDrawingCacheEnabled(true); 219 //获取mDragItemView在缓存中的Bitmap对象 220 mDragBitmap = Bitmap.createBitmap(mStartDragItemView.getDrawingCache()); 221 //这一步很关键,释放绘图缓存,避免出现重复的镜像 222 mStartDragItemView.destroyDrawingCache(); 223 224 225 break; 226 case MotionEvent.ACTION_MOVE: 227 int moveX = (int)ev.getX(); 228 int moveY = (int) ev.getY(); 229 230 //如果我们在按下的item上面移动,只要不超过item的边界我们就不移除mRunnable 231 if(!isTouchInItem(mStartDragItemView, moveX, moveY)){ 232 mHandler.removeCallbacks(mLongClickRunnable); 233 } 234 break; 235 case MotionEvent.ACTION_UP: 236 mHandler.removeCallbacks(mLongClickRunnable); 237 mHandler.removeCallbacks(mScrollRunnable); 238 break; 239 } 240 return super.dispatchTouchEvent(ev); 241 } 242 243 244 /** 245 * 是否点击在GridView的item上面 246 * @param itemView 247 * @param x 248 * @param y 249 * @return 250 */ 251 private boolean isTouchInItem(View dragView, int x, int y){ 252 int leftOffset = dragView.getLeft(); 253 int topOffset = dragView.getTop(); 254 if(x < leftOffset || x > leftOffset + dragView.getWidth()){ 255 return false; 256 } 257 258 if(y < topOffset || y > topOffset + dragView.getHeight()){ 259 return false; 260 } 261 262 return true; 263 } 264 265 266 267 @Override 268 public boolean onTouchEvent(MotionEvent ev) { 269 if(isDrag && mDragImageView != null){ 270 switch(ev.getAction()){ 271 case MotionEvent.ACTION_MOVE: 272 moveX = (int) ev.getX(); 273 moveY = (int) ev.getY(); 274 //拖动item 275 onDragItem(moveX, moveY); 276 break; 277 case MotionEvent.ACTION_UP: 278 onStopDrag(); 279 isDrag = false; 280 break; 281 } 282 return true; 283 } 284 return super.onTouchEvent(ev); 285 } 286 287 288 /** 289 * 创建拖动的镜像 290 * @param bitmap 291 * @param downX 292 * 按下的点相对父控件的X坐标 293 * @param downY 294 * 按下的点相对父控件的X坐标 295 */ 296 private void createDragImage(Bitmap bitmap, int downX , int downY){ 297 mWindowLayoutParams = new WindowManager.LayoutParams(); 298 mWindowLayoutParams.format = PixelFormat.TRANSLUCENT; //图片之外的其他地方透明 299 mWindowLayoutParams.gravity = Gravity.TOP | Gravity.LEFT; 300 mWindowLayoutParams.x = downX - mPoint2ItemLeft + mOffset2Left; 301 mWindowLayoutParams.y = downY - mPoint2ItemTop + mOffset2Top - mStatusHeight; 302 mWindowLayoutParams.alpha = 0.55f; //透明度 303 mWindowLayoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT; 304 mWindowLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT; 305 mWindowLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 306 | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE ; 307 308 mDragImageView = new ImageView(getContext()); 309 mDragImageView.setImageBitmap(bitmap); 310 mWindowManager.addView(mDragImageView, mWindowLayoutParams); 311 } 312 313 /** 314 * 从界面上面移动拖动镜像 315 */ 316 private void removeDragImage(){ 317 if(mDragImageView != null){ 318 mWindowManager.removeView(mDragImageView); 319 mDragImageView = null; 320 } 321 } 322 323 /** 324 * 拖动item,在里面实现了item镜像的位置更新,item的相互交换以及GridView的自行滚动 325 * @param x 326 * @param y 327 */ 328 private void onDragItem(int moveX, int moveY){ 329 mWindowLayoutParams.x = moveX - mPoint2ItemLeft + mOffset2Left; 330 mWindowLayoutParams.y = moveY - mPoint2ItemTop + mOffset2Top - mStatusHeight; 331 mWindowManager.updateViewLayout(mDragImageView, mWindowLayoutParams); //更新镜像的位置 332 onSwapItem(moveX, moveY); 333 334 //GridView自动滚动 335 mHandler.post(mScrollRunnable); 336 } 337 338 339 /** 340 * 当moveY的值大于向上滚动的边界值,触发GridView自动向上滚动 341 * 当moveY的值小于向下滚动的边界值,触犯GridView自动向下滚动 342 * 否则不进行滚动 343 */ 344 private Runnable mScrollRunnable = new Runnable() { 345 346 @Override 347 public void run() { 348 int scrollY; 349 if(moveY > mUpScrollBorder){ 350 scrollY = -speed; 351 mHandler.postDelayed(mScrollRunnable, 25); 352 }else if(moveY < mDownScrollBorder){ 353 scrollY = speed; 354 mHandler.postDelayed(mScrollRunnable, 25); 355 }else{ 356 scrollY = 0; 357 mHandler.removeCallbacks(mScrollRunnable); 358 } 359 360 //当我们的手指到达GridView向上或者向下滚动的偏移量的时候,可能我们手指没有移动,但是DragGridView在自动的滚动 361 //所以我们在这里调用下onSwapItem()方法来交换item 362 onSwapItem(moveX, moveY); 363 364 View view = getChildAt(mDragPosition - getFirstVisiblePosition()); 365 //实现GridView的自动滚动 366 smoothScrollToPositionFromTop(mDragPosition, view.getTop() + scrollY); 367 } 368 }; 369 370 371 /** 372 * 交换item,并且控制item之间的显示与隐藏效果 373 * @param moveX 374 * @param moveY 375 */ 376 private void onSwapItem(int moveX, int moveY){ 377 //获取我们手指移动到的那个item的position 378 int tempPosition = pointToPosition(moveX, moveY); 379 380 //假如tempPosition 改变了并且tempPosition不等于-1,则进行交换 381 if(tempPosition != mDragPosition && tempPosition != AdapterView.INVALID_POSITION){ 382 getChildAt(tempPosition - getFirstVisiblePosition()).setVisibility(View.INVISIBLE);//拖动到了新的item,新的item隐藏掉 383 getChildAt(mDragPosition - getFirstVisiblePosition()).setVisibility(View.VISIBLE);//之前的item显示出来 384 385 if(onChanageListener != null){ 386 onChanageListener.onChange(mDragPosition, tempPosition); 387 } 388 389 mDragPosition = tempPosition; 390 } 391 } 392 393 394 /** 395 * 停止拖拽我们将之前隐藏的item显示出来,并将镜像移除 396 */ 397 private void onStopDrag(){ 398 getChildAt(mDragPosition - getFirstVisiblePosition()).setVisibility(View.VISIBLE); 399 removeDragImage(); 400 } 401 402 /** 403 * 获取状态栏的高度 404 * @param context 405 * @return 406 */ 407 private static int getStatusHeight(Context context){ 408 int statusHeight = 0; 409 Rect localRect = new Rect(); 410 ((Activity) context).getWindow().getDecorView().getWindowVisibleDisplayFrame(localRect); 411 statusHeight = localRect.top; 412 if (0 == statusHeight){ 413 Class<?> localClass; 414 try { 415 localClass = Class.forName("com.android.internal.R$dimen"); 416 Object localObject = localClass.newInstance(); 417 int i5 = Integer.parseInt(localClass.getField("status_bar_height").get(localObject).toString()); 418 statusHeight = context.getResources().getDimensionPixelSize(i5); 419 } catch (Exception e) { 420 e.printStackTrace(); 421 } 422 } 423 return statusHeight; 424 } 425 426 427 /** 428 * 429 * @author hebao 430 * 431 */ 432 public interface OnChanageListener{ 433 434 /** 435 * 当item交换位置的时候回调的方法,我们只需要在该方法中实现数据的交换即可 436 * @param form 437 * 开始的position 438 * @param to 439 * 拖拽到的position 440 */ 441 public void onChange(int form, int to); 442 } 443 }
首先看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()是相对于控件左上方的点的距离,相对View的触摸位置坐标,这里的View就是GridView。
为了方便大家理解我用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 号线
上面已经完成了开始拖拽的准备工作,要想拖动镜像我们还需要重写onTouchEvent()方法,获取移动的X,Y的坐标,利用WindowManager的updateViewLayout方法就能对镜像进行拖动,拖动的镜像的时候为了有更好的用户体验,我们还要做item的实时交换效果,我们利用手指移动的X,Y坐标,利用pointToPosition()来获取拖拽到的position,然后将之前的item显示出来,将拖拽到的item进行隐藏,这样子就完成了item在界面上面的交换,但是数据交换我这里没有做,所以我提供了回调接口OnChanageListener,我们只需要自己实现数据的交换逻辑然后刷新DragGridView即可,我们还需要实现DragGridView的自动向上滚动或者向下滚动,使用Handler和mScrollRunnable利用smoothScrollToPositionFromTop()来实现DragGridView滚动,具体的实现大家可以看代码。
手指离开界面,将item的镜像移除,并将拖拽到的item显示出来,这样子就实现了GirdView的拖拽效果啦。
1 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" 2 xmlns:tools="http://schemas.android.com/tools" 3 android:layout_width="match_parent" 4 android:layout_height="match_parent" > 5 6 <com.example.draggridview.DragGridView 7 android:id="@+id/dragGridView" 8 android:listSelector="@android:color/transparent" 9 android:layout_width="match_parent" 10 android:layout_height="match_parent" 11 android:cacheColorHint="@android:color/transparent" 12 android:verticalSpacing="10dip" 13 android:horizontalSpacing="10dip" 14 android:stretchMode="columnWidth" 15 android:gravity="center" 16 android:numColumns="3" > 17 </com.example.draggridview.DragGridView> 18 19 </RelativeLayout>
(3)接下来我们看看DragGridView的item的布局,上面一个ImageView下面一个TextView:
1 <?xml version="1.0" encoding="utf-8"?> 2 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 android:layout_width="fill_parent" 4 android:layout_height="wrap_content" 5 android:background="@android:color/transparent" > 6 7 <ImageView 8 android:id="@+id/item_image" 9 android:scaleType="centerCrop" 10 android:layout_width="wrap_content" 11 android:layout_height="wrap_content" 12 android:layout_centerHorizontal="true" > 13 </ImageView> 14 15 <TextView 16 android:id="@+id/item_text" 17 android:layout_width="wrap_content" 18 android:layout_height="wrap_content" 19 android:layout_below="@+id/item_image" 20 android:layout_centerHorizontal="true" > 21 </TextView> 22 23 </RelativeLayout>
(4)布局搞定了我们就来看看主页面MainActivity的代码吧:
1 package com.example.draggridview; 2 3 import java.util.ArrayList; 4 import java.util.Collections; 5 import java.util.HashMap; 6 import java.util.List; 7 8 import android.app.Activity; 9 import android.os.Bundle; 10 import android.widget.SimpleAdapter; 11 12 import com.example.draggridview.DragGridView.OnChanageListener; 13 14 15 public class MainActivity extends Activity { 16 private List<HashMap<String, Object>> dataSourceList = new ArrayList<HashMap<String, Object>>(); 17 18 @Override 19 protected void onCreate(Bundle savedInstanceState) { 20 super.onCreate(savedInstanceState); 21 setContentView(R.layout.activity_main); 22 23 DragGridView mDragGridView = (DragGridView) findViewById(R.id.dragGridView); 24 for (int i = 0; i < 30; i++) { 25 HashMap<String, Object> itemHashMap = new HashMap<String, Object>(); 26 itemHashMap.put("item_image",R.drawable.com_tencent_open_notice_msg_icon_big); 27 itemHashMap.put("item_text", "拖拽 " + Integer.toString(i)); 28 dataSourceList.add(itemHashMap); 29 } 30 31 32 final SimpleAdapter mSimpleAdapter = new SimpleAdapter(this, dataSourceList, 33 R.layout.grid_item, new String[] { "item_image", "item_text" }, 34 new int[] { R.id.item_image, R.id.item_text }); 35 36 mDragGridView.setAdapter(mSimpleAdapter); 37 38 mDragGridView.setOnChangeListener(new OnChanageListener() { 39 40 @Override 41 public void onChange(int from, int to) { 42 HashMap<String, Object> temp = dataSourceList.get(from); 43 //直接交互item 44 // dataSourceList.set(from, dataSourceList.get(to)); 45 // dataSourceList.set(to, temp); 46 // dataSourceList.set(to, temp); 47 48 49 //这里的处理需要注意下 50 if(from < to){ 51 for(int i=from; i<to; i++){ 52 Collections.swap(dataSourceList, i, i+1); 53 } 54 }else if(from > to){ 55 for(int i=from; i>to; i--){ 56 Collections.swap(dataSourceList, i, i-1); 57 } 58 } 59 60 dataSourceList.set(to, temp); 61 62 mSimpleAdapter.notifyDataSetChanged(); 63 64 65 } 66 }); 67 68 } 69 70 }
这里面的代码还是比较简单,主要讲下onChange()方法,我们要为mDragGridView设置一个OnChanageListener的回调接口,在onChange()方法里面实现数据的交换逻辑,第一个参数from为item开始的位置,第二个参数to为item拖拽到的位置,刚开始我使用的交换逻辑是:
1 HashMap<String, Object> temp = dataSourceList.get(from); 2 //直接交互item 3 // dataSourceList.set(from, dataSourceList.get(to)); 4 // dataSourceList.set(to, temp);
简单说下,数据的交换逻辑,比如我们将position从5拖拽到7这个位置,我注释掉的逻辑是直接将5和7的数据交换,而后面的那种逻辑是将6的位置数据移动到5,将7的位置移动到6,然后再7显示5 6->5, 7->6, 5->7不知道大家理解了没有。
1 <uses-permission android:name="android.permission.VIBRATE"/>
(6)运行效果,如下: