在网易新闻中有一个新闻栏目管理,其中GridView的item是可以拖拽的,效果十分炫酷。具体效果如下图:
是不是也想自己也想实现出相同的效果呢?那就一起来往下看吧。
首先我们来梳理一下思路:
- 当用户长按选择一个item时,将该item隐藏,然后用WindowManager添加一个新的window,该window与所选择item一模一样,并且跟随用户手指滑动而不断改变位置。
- 当window的位置坐标在GridView里面时,使用
pointToPosition (int x, int y)
方法来判断对应的应该是哪个item,在adapter中作出数据集相应的变化,然后做出平移的动画。
- 当用户手指抬起时,把window移除,使用
notifyDataSetChanged()
做出GridView更新。
讲完了思路后,我们就来实践一下吧,把这个控件取名为DragGridView。
1 2 3 4 5 6 7 8 9 10 11 12 13
| public DragGridView(Context context) { this(context, null); }
public DragGridView(Context context, AttributeSet attrs) { this(context, attrs, 0); }
public DragGridView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); setOnItemLongClickListener(this); }
|
手指在Item上长按时
首先在构造器中得到WindowManager对象以及设置长按监听器,所以只有长按item才能拖拽。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: mWindowX = ev.getRawX(); mWindowY = ev.getRawY(); break; case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_UP: break; } return super.onInterceptTouchEvent(ev); }
|
然后在onInterceptTouchEvent(MotionEvent ev)
中得到手指下落时的ev.getRawX()
和ev.getRawY()
,以备后面的计算使用。至于getRawX()
和getX()
的区别这里就不再讲述了,如果有不懂的可以自行百度。
下面就是onItemLongClick(AdapterView<?> parent, View view, int position, long id)
方法了,我们在DragGridView中定义了两种模式:MODE_DRAG
和MODE_NORMAL
,分别对应着item拖拽和item不拖拽:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Override public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { if (mode == MODE_DRAG) { return false; } this.view = view; this.position = position; this.tempPosition = position; mX = mWindowX - view.getLeft() - this.getLeft(); mY = mWindowY - view.getTop() - this.getTop(); initWindow(); return true; }
|
在onItemLongClick()中先判断了一下模式,只有在MODE_NORMAL
的情况下才会添加window。然后计算出mX和mY。如图所示:
其中红点是手指按下的坐标,也就是(mWindowX,mWindowY)这个点;绿边框为DragGridView,因为DragGridView有可能会有margin值;所以this.getLeft()就是绿边框到屏幕的距离,而view.getLeft()就是长按的Item的左边到绿边框的距离。这几个值相减就得到了mX。同理,mY也是这样得到的。
然后来看看initWindow();
这个方法:
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
|
private void initWindow() { if (dragView == null) { dragView = View.inflate(getContext(), R.layout.drag_item, null); TextView tv_text = (TextView) dragView.findViewById(R.id.tv_text); tv_text.setText(((TextView) view.findViewById(R.id.tv_text)).getText()); } if (layoutParams == null) { layoutParams = new WindowManager.LayoutParams(); layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE; layoutParams.format = PixelFormat.RGBA_8888; layoutParams.gravity = Gravity.TOP | Gravity.LEFT; layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; layoutParams.width = view.getWidth(); layoutParams.height = view.getHeight(); layoutParams.x = view.getLeft() + this.getLeft(); layoutParams.y = view.getTop() + this.getTop(); view.setVisibility(INVISIBLE); }
mWindowManager.addView(dragView, layoutParams); mode = MODE_DRAG; }
|
在initWindow()
中,我们先创建了一个dragView,而dragView里面的内容与长按的Item的内容完全一致。然后创建WindowManager.LayoutParams
的对象,把dragView添加到window上去。同时,也要把长按的Item隐藏了。在这里别忘了需要申请显示悬浮窗的权限:
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
手指滑动时
在initWindow()
之后,我们就要考虑当手指滑动时window也要跟着动了,我们重写onTouchEvent(MotionEvent ev)
来监听滑动事件,可以看到下面的updateWindow(ev)
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Override public boolean onTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: if (mode == MODE_DRAG) { updateWindow(ev); } break; case MotionEvent.ACTION_UP: if (mode == MODE_DRAG) { closeWindow(ev.getX(), ev.getY()); } break; } return super.onTouchEvent(ev); }
|
这里贴出updateWindow(ev)
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
private void updateWindow(MotionEvent ev) { if (mode == MODE_DRAG) { float x = ev.getRawX() - mX; float y = ev.getRawY() - mY; if (layoutParams != null) { layoutParams.x = (int) x; layoutParams.y = (int) y; mWindowManager.updateViewLayout(dragView, layoutParams); } float mx = ev.getX(); float my = ev.getY(); int dropPosition = pointToPosition((int) mx, (int) my); Log.i(TAG, "dropPosition : " + dropPosition + " , tempPosition : " + tempPosition); if (dropPosition == tempPosition || dropPosition == GridView.INVALID_POSITION) { return; } itemMove(dropPosition); } }
|
在这里,mX和mY就派上用场了。根据ev.getRawX()
和ev.getRawY()
分别减去mX
和mY
就得到了移动中layoutParams.x和layoutParams.y。再调用updateViewLayout (View view, ViewGroup.LayoutParams params)
就出现了window跟随手指滑动而滑动的效果。最后根据 pointToPosition(int x, int y)
返回的值来执行itemMove(dropPosition);
。
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
|
private void itemMove(int dropPosition) { TranslateAnimation translateAnimation; if (dropPosition < tempPosition) { for (int i = dropPosition; i < tempPosition; i++) { View view = getChildAt(i); View nextView = getChildAt(i + 1); float xValue = (nextView.getLeft() - view.getLeft()) * 1f / view.getWidth(); float yValue = (nextView.getTop() - view.getTop()) * 1f / view.getHeight(); translateAnimation = new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, xValue, Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, yValue); translateAnimation.setInterpolator(new LinearInterpolator()); translateAnimation.setFillAfter(true); translateAnimation.setDuration(300); if (i == tempPosition - 1) { translateAnimation.setAnimationListener(animationListener); } view.startAnimation(translateAnimation); } } else { for (int i = tempPosition + 1; i <= dropPosition; i++) { View view = getChildAt(i); View prevView = getChildAt(i - 1); float xValue = (prevView.getLeft() - view.getLeft()) * 1f / view.getWidth(); float yValue = (prevView.getTop() - view.getTop()) * 1f / view.getHeight(); translateAnimation = new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, xValue, Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, yValue); translateAnimation.setInterpolator(new LinearInterpolator()); translateAnimation.setFillAfter(true); translateAnimation.setDuration(300); if (i == dropPosition) { translateAnimation.setAnimationListener(animationListener); } view.startAnimation(translateAnimation); } } tempPosition = dropPosition; }
Animation.AnimationListener animationListener = new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) {
}
@Override public void onAnimationEnd(Animation animation) { ListAdapter adapter = getAdapter(); if (adapter != null && adapter instanceof DragGridAdapter) { ((DragGridAdapter) adapter).exchangePosition(position, tempPosition, true); } position = tempPosition; }
@Override public void onAnimationRepeat(Animation animation) {
} };
|
上面的代码主要是根据dropPosition使要改变位置的Item来做出平移动画,当最后一个要改变位置的Item平移动画完成之后,在adapter中完成数据集的交换。
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
|
public void exchangePosition(int originalPosition, int nowPosition, boolean isMove) { T t = list.get(originalPosition); list.remove(originalPosition); list.add(nowPosition, t); movePosition = nowPosition; this.isMove = isMove; notifyDataSetChanged(); }
@Override public View getView(int position, View convertView, ViewGroup parent) { Log.i(TAG, "-------------------------------"); for (T t : list){ Log.i(TAG, t.toString()); } View view = getItemView(position, convertView, parent); if (position == movePosition && isMove) { view.setVisibility(View.INVISIBLE); } return view; }
|
手指抬起时
在上面onTouchEvent(MotionEvent ev)
方法中,可以看到手指抬起时调用了closeWindow(ev.getX(), ev.getY());
,那就一起来看看:
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
|
private void closeWindow(float x, float y) { if (dragView != null) { mWindowManager.removeView(dragView); dragView = null; layoutParams = null; } itemDrop(); mode = MODE_NORMAL; }
private void itemDrop() { if (tempPosition == position || tempPosition == GridView.INVALID_POSITION) { getChildAt(position).setVisibility(VISIBLE); } else { ListAdapter adapter = getAdapter(); if (adapter != null && adapter instanceof DragGridAdapter) { ((DragGridAdapter) adapter).exchangePosition(position, tempPosition, false); } } }
|
可以看出主要做的事情就是移除了window,并且也是调用了exchangePosition(int originalPosition, int nowPosition, boolean isMove)
,不同的是第三个参数isMove传入了false,这样所有的Item都显示出来了。
讲了这么多,来看看最后的效果吧: