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事件。