Android實現可拖拽的ListView
通過繼承ListView實現可拖拽的ListView,先說說實現拖拽的原理吧,實現拖拽需要考慮三個問題:第一怎么確定你在拖拽listview里面的item的時候就是你手指當前選中的item;第二實現拖拽的效果,就是有一個浮動的層跟隨你的手指在移動;第三你放開手指時怎么把你拖拽的這個item放到當前listView的位置(也就是說改變item的位置)。明白了這三個問題就比較好實現了。
里面會涉及到一些比較重要的方法調用,首先是pointToPosition(int x, int y)這方方法Android 官方的解釋是” Maps a point to a position in the list”,我把它理解為通過x和y的位置來確定這個listView里面這個item的位置。有了這個方法就解決了第一和第三個問題了。接下來我們可以通過WindowManager來解決第二個問題,然后通過pointToPosition方法就可以獲取你手指按下時的item,這個item其實就是你listview里面的item了,這樣的就可以把這個item設置為是WindowManager的view,這樣的話拖動的層的效果就模擬出來了,接下來是怎么讓這個WindowManager跟隨你的手指在移動。這個時候會涉及到WindowManager里面的updateViewLayout(view, layoutparams)來刷新WindowManager的位置,這樣就實現了WindowManager會跟隨你的手指在移動。最后就剩下你放下手指的時候怎么讓你拖拽的item插入到listview里面,這個插入的動作其實包含了移除和插入這兩個動作。這個時候你可能會問在某個位置插入這個item需要”position”和”item”兩個參數,position我們可以通過pointToPosition方法來獲取,然后要插入的“item”其實是你adapter里面數據。因為我們上面的一系列動作都是在listview里面完成的,但是在我們重寫listview的時候是還沒有給listview設置adapter是吧,這個問題的我們通過在重寫listview的類中自定義一個接口,然后你在activity里面初始化listview數據的時候實現這個接口。接口里面只有一個方法,方法里面的兩個參數一個是你開始拖拽的的item的位置,另一個是你拖拽移動之后之后的item的位置。下面我們看看效果吧:
下面我們看看代碼:
private final float mAlpha = 0.9f; //拖動的view private ImageView mDragView; private Context mContext; private WindowManager mWindowManager; private WindowManager.LayoutParams mLayoutParams; //開始拖動時的位置 private int mDragStartPosition; //當前的位置 private int mDragCurrentPostion; //在滑動的時候,手的移動要大于這個返回的距離值才開始移動控件 private int mScaledTouchSlop; //當前位置距離邊界的位置 private int mDragOffsetX; private int mDragOffSetY; //移動的位置 private int mDragPointX; private int mDragPointY; //邊界 private int mUpperBound; private int mLowerBound; private DropViewListener mDropViewListener; public DragListView(Context context) { super(context); mContext = context; mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); mScaledTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop(); } public DragListView(Context context, AttributeSet attr) { super(context, attr); mContext = context; mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); mScaledTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop(); }
我們在onInterceptTouchEvent方法里面作初始化動作:
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { //ev.getX()相對于控件本身左上角,ev.getRawX()相對于容器圓點位置 switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: final int x = (int) ev.getX();//相對于空間本身 final int y = (int) ev.getY(); final int itemNum = pointToPosition(x, y); if(itemNum == AdapterView.INVALID_POSITION){ break; } final ViewGroup item = (ViewGroup) getChildAt(itemNum - getFirstVisiblePosition()); mDragPointX = x - item.getLeft(); mDragPointY = y - item.getTop(); mDragOffsetX = ((int) ev.getRawX()) - x; mDragOffSetY = ((int) ev.getRawY()) - y; //長按 item.setOnLongClickListener(new OnLongClickListener() { @Override public boolean onLongClick(View v) { //計算邊界 final int height = getHeight(); mUpperBound = Math.min(y - mScaledTouchSlop, height / 3); mLowerBound = Math.max(y + mScaledTouchSlop, height * 2 / 3); mDragCurrentPostion = mDragStartPosition = itemNum; item.setDrawingCacheEnabled(true); Bitmap bitmap = Bitmap.createBitmap(item.getDrawingCache()); startDragging(bitmap, x, y); return true; } }); break; } return super.onInterceptTouchEvent(ev); }這里面我們就記錄了拖拽的當前和開始時位置mDragCurrentPostion和 mDragStartPosition,并通過startDragging來初始化mWindowManager,在startDragging方法里面調用的stopDragging方法其實是一個內存釋放的過程,這個方法做的事情就是釋放內存空間。
private void startDragging(Bitmap bitm, int x, int y){ stopDragging(); mLayoutParams = new WindowManager.LayoutParams(); mLayoutParams.gravity = Gravity.TOP | Gravity.LEFT; mLayoutParams.x = x - mDragPointX + mDragOffsetX; mLayoutParams.y = y - mDragPointY + mDragOffSetY; mLayoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT; mLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT; mLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; mLayoutParams.format = PixelFormat.TRANSLUCENT; mLayoutParams.windowAnimations = 0; ImageView imageView = new ImageView(mContext); imageView.setImageBitmap(bitm); imageView.setBackgroundResource(R.drawable.tab_item_bg); imageView.setPadding(0, 0, 0, 0); mWindowManager.addView(imageView, mLayoutParams); mDragView = imageView; } private void stopDragging(){ if(mDragView != null){ mWindowManager.removeView(mDragView); mDragView.setImageDrawable(null); mDragView = null; } }我們在onInterceptTouchEvent方法記錄了拖拽的開始位置和當前位置,并且初始化了windowmanager,接下來我們通過onTouchEvent方法來實現讓windowmanager跟隨你的手指移動
@Override public boolean onTouchEvent(MotionEvent ev) { if(mDragView != null && mDragCurrentPostion != INVALID_POSITION && mDropViewListener != null){ switch (ev.getAction()) { case MotionEvent.ACTION_UP: //int y = (int) ev.getY(); stopDragging(); //數據交換 if(mDragCurrentPostion >= 0 && mDragCurrentPostion < getCount()){ mDropViewListener.drop(mDragStartPosition, mDragCurrentPostion); } break; case MotionEvent.ACTION_MOVE: int x = (int) ev.getX(); int y = (int) ev.getY(); dragView(x, y); if (y >= getHeight() / 3) { mUpperBound = getHeight() / 3; } if (y <= getHeight() * 2 / 3) { mLowerBound = getHeight() * 2 / 3; } int speed = 0; if (y > mLowerBound) { if (getLastVisiblePosition() < getCount() - 1) { speed = y > (getHeight() + mLowerBound) / 2 ? 16 : 4; } else { speed = 1; } } else if (y < mUpperBound) { speed = y < mUpperBound / 2 ? -16 : -4; if (getFirstVisiblePosition() == 0 && getChildAt(0).getTop() >= getPaddingTop()) { speed = 0; } } if (speed != 0) { smoothScrollBy(speed, 30); } break; } return true; } return super.onTouchEvent(ev); }其中dragView方法就是根據你手指移動來改變windowmanager的位置,在移動的時候我們需要考慮的另外一個問題是你的listview滾動條的時候,這個時候我們需要考慮邊界,不然會拋出空指針異常。
private void dragView(int x, int y){ if(mDragView != null){ mLayoutParams.alpha = mAlpha; mLayoutParams.y = y - mDragPointY + mDragOffSetY; mLayoutParams.x = x - mDragPointX + mDragOffsetX; mWindowManager.updateViewLayout(mDragView, mLayoutParams); } int tempPosition = pointToPosition(0, y); if(tempPosition != INVALID_POSITION){ mDragCurrentPostion = tempPosition; } //滾動 int scrollY = 0; if(y < mUpperBound){ scrollY = 8; }else if(y > mLowerBound){ scrollY = -8; } if(scrollY != 0){ int top = getChildAt(mDragCurrentPostion - getFirstVisiblePosition()).getTop(); setSelectionFromTop(mDragCurrentPostion, top + scrollY); } }這樣的話就完成了拖拽的效果,剩下的是你放開手指,然后把你拖拽的item插入到listview的列表里面,我們定義了一個接口
public void setDropViewListener(DropViewListener listener){ this.mDropViewListener = listener; } public interface DropViewListener { void drop(int from, int to); }這樣的話,你在初始化listview給listView設置adapter的時候需要實現DropViewListener接口,from和to兩個參數分別是你拖拽時的最初位置和你移動后的位置,這樣的話你在activity里面就可以實現數據插入了。
注意:
但是如果你的這listview的item包含了復選框的話,這個時候listview的onitemclicklistener事件就會失效,也就是說你不能通過你平時處理listview的onitemclicklistener的方法一樣處理,你也可以通過自定義接口來模擬onitemclicklistener事件。