前言
之前项目中有需求,需要对Gridview长按拖拽换位,总体实现难度不大,主要问题是不改变拖拽轨迹item顺序以及好多bug的修复过程,因此在此记录
效果图
大纲
1.自定义MyGridView
1.1成员变量介绍
1.2点击事件处理
1.3拖拽逻辑处理
2.承载Activity:MyGridActivity
3.数据bean类:MyGridBean
4.适配器:MyGridAdapter
5.Bug总结以及改进
6.代码地址
正文
1.1成员变量介绍
//坐标
private int mDownX, mDownY;
private int mPointTop, mPointLeft;
private int mOffsetX, mOffsetY;
private int mUpBorder, mDownBorder;
private int mTouchX, mTouchY;
private int mStatusHeight;
//当前view
private int mSelectIndex;
private int mCurrentPosition;
private int mDragStartPosition;
private View mSelctView;
//事件
private Runnable mLongRunnable;
private Runnable mScrollRunnable;
private Handler mHandler;
private boolean isDrag;
private onChangeListener mOnChangeListener;
//镜像
private WindowManager mWindowManager;
private WindowManager.LayoutParams mWindowLayoutParams;
private ImageView mDragImageView;
private Bitmap mDragView;
//其他常量
private static final long LONG_TOUCH_TIME = 800;
private static final int SCROLL_SPEED = 20;
坐标:
int mDownX, mDownY;
获取的是dispatchTouchEvent中ev.getX/Y,手指点下的相对坐标,具体区别参考:http://blog.youkuaiyun.com/dmk877/article/details/51550031
int mPointTop, mPointLeft;
获取的是手指点下的点和当前view边界距离,为之后计算镜像位置
int mOffsetX, mOffsetY;
获取MyGridview距离屏幕距离,也为之后计算镜像位置
int mUpBorder, mDownBorder;
计算MyGridview开始滚动的区域,因为拖拽过程中Gridview滑动事件失效,需要自定义实现,因此需要获取边界值
int mTouchX, mTouchY;
获取onTouchEvent中获取的ev.getX/Y,为获取手指移动到的itemView
int mStatusHeight
计算当前设备状态栏高度,为之后计算镜像位置
当前view
int mSelectIndex
手指点下的item position,后面是交换item时的start position
int mCurrentPosition
手指移动到的item position,同时也是交换item时的end position
int mDragStartPosition
长按事件点下的那个item position,为了实现一次拖拽只交换抬起和最后放下的item,需要保存这个item position
View mSelctView
长按事件点下的那个item view,获取bitmap构建镜像
事件
Runnable mLongRunnable;
长按事件,创建镜像等逻辑
Runnable mScrollRunnable;
滚动事件,开始拖动后需要自定义实现滚动
Handler mHandler;
Handler
boolean isDrag;
滚动开始标记
onChangeListener mOnChangeListener;
对外提供交换接口,用于数据源内信息交换
镜像
WindowManager mWindowManager;
WindowManager.LayoutParams mWindowLayoutParams;
ImageView mDragImageView;
Bitmap mDragView;
实现镜像创建
其他常量
long LONG_TOUCH_TIME = 800
长按事件触发延迟,单位毫秒
int SCROLL_SPEED = 20;
滑动速度
private void init() {
mHandler = new Handler();
mLongRunnable = new Runnable() {
@Override
public void run() {
isDrag = true;
mSelctView.setVisibility(View.INVISIBLE);
creatDragImage(mDragView, mDownX, mDownY);
}
};
mScrollRunnable = new Runnable() {
@Override
public void run() {
mHandler.removeCallbacks(mLongRunnable);
int scrollY;
if (mTouchY > mUpBorder) {
scrollY = SCROLL_SPEED;
mHandler.postDelayed(mScrollRunnable, 25);
} else if (mTouchY < mDownBorder) {
scrollY = -SCROLL_SPEED;
mHandler.postDelayed(mScrollRunnable, 25);
} else {
scrollY = 0;
mHandler.removeCallbacks(mScrollRunnable);
}
onSwapItem(mTouchX, mTouchY);
smoothScrollBy(scrollY, 10);
}
};
mStatusHeight = getStatusHeight(getContext());
mWindowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
//为区别position = 0情况,设置缺省值为-1
mCurrentPosition = -1;
}
1.2点击事件处理
主要流程:
dispatchTouchEvent
1.down获取按下点坐标,计算上文的几个坐标,以及获取需要当前view bitmap,同时绑定长按事件
2.move获取移动坐标,判定是否手指移出view区域或者开始滑动,若是则取消长按事件
3.up和cancel取消长按事件和滚动事件
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = (int) ev.getX();
mDownY = (int) ev.getY();
mSelectIndex = pointToPosition(mDownX, mDownY);
if (mSelectIndex != AdapterView.INVALID_POSITION) {
mHandler.postDelayed(mLongRunnable, LONG_TOUCH_TIME);
mSelctView = getChildAt(mSelectIndex - getFirstVisiblePosition());
//记录一次拖拽流程中真正交换的起始item position
mDragStartPosition = mSelectIndex;
mPointTop = mDownY - mSelctView.getTop();
mPointLeft = mDownX - mSelctView.getLeft();
mOffsetX = (int) (ev.getRawX() - mDownX);
mOffsetY = (int) (ev.getRawY() - mDownY);
mUpBorder = getHeight() * 3 / 4;
mDownBorder = getHeight() / 4;
mSelctView.setDrawingCacheEnabled(true);
mDragView = Bitmap.createBitmap(mSelctView.getDrawingCache());
mSelctView.destroyDrawingCache();
}
break;
case MotionEvent.ACTION_MOVE:
int moveX = (int) ev.getX();
int moveY = (int) ev.getY();
//若手指移除当前view区域或gridview滚动则取消长按事件
if (!isTouchInItem(mSelctView, moveX, moveY) || Math.abs(moveY - mDownY) > 100) {
mHandler.removeCallbacks(mLongRunnable);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mHandler.removeCallbacks(mLongRunnable);
mHandler.removeCallbacks(mScrollRunnable);
break;
}
return super.dispatchTouchEvent(ev);
}
onTouchEvent
1.move根据移动坐标获取当前移动到的item,进行交换逻辑
2.up停止拖拽
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (isDrag && mDragImageView != null) {
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
mTouchX = (int) ev.getX();
mTouchY = (int) ev.getY();
//拖动item
onDragItem(mTouchX, mTouchY);
break;
case MotionEvent.ACTION_UP:
onStopDrag();
isDrag = false;
mCurrentPosition = AdapterView.INVALID_POSITION;
break;
}
return true;
}
return super.onTouchEvent(ev);
}
1.3拖拽逻辑处理
onDragItem
跟随手指移动更新镜像位置
/**
* 拖拽更新
*
* @param moveX
* @param moveY
*/
private void onDragItem(int moveX, int moveY) {
mWindowLayoutParams.x = moveX - mPointLeft + mOffsetX;
mWindowLayoutParams.y = moveY - mPointTop + mOffsetY - mStatusHeight;
mWindowManager.updateViewLayout(mDragImageView, mWindowLayoutParams); //更新镜像的位置
onSwapItem(moveX, moveY);
//拖拽时到边界自动滚动
mHandler.post(mScrollRunnable);
}
onSwapItem
调用对外接口实现数据源数据更新
/**
* item换位
*
* @param moveX
* @param moveY
*/
private void onSwapItem(int moveX, int moveY) {
mCurrentPosition = pointToPosition(moveX, moveY);
if (mCurrentPosition != mSelectIndex && mCurrentPosition != AdapterView.INVALID_POSITION) {
if (mOnChangeListener != null) {
mOnChangeListener.onChange(mSelectIndex, mCurrentPosition, mDragStartPosition);
}
mSelectIndex = mCurrentPosition;
}
}
onStopDrag
清除镜像
/**
* 停止拖拽
*/
private void onStopDrag() {
View view = getChildAt(mSelectIndex - getFirstVisiblePosition());
if (view != null) {
view.setVisibility(View.VISIBLE);
}
if (mDragImageView != null) {
mWindowManager.removeView(mDragImageView);
mDragImageView = null;
}
}
2.承载Activity:MyGridActivity
因为是demo没有特别复杂逻辑,主要生成假数据,然后实现MyGridview提供的交换接口,内嵌的adapter单独拿出后面讲
public class MyGridActivity extends Activity {
private MyGridView mMyGridView;
private MyGridAdapter mAdapter;
private List<MyGridBean> mBeans;
private static final int MAX_ITEM = 100;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my_grid);
mMyGridView = (MyGridView) findViewById(R.id.mgv);
mMyGridView.setSelector(new ColorDrawable(Color.TRANSPARENT));
mBeans = new ArrayList<>();
//假数据填充
for (int i = 0; i < MAX_ITEM; i++) {
MyGridBean bean = new MyGridBean();
if (i % 4 == 0) {
bean.setDrableRes(R.mipmap.icon_one);
} else if (i % 4 == 1) {
bean.setDrableRes(R.mipmap.icon_two);
} else if (i % 4 == 2) {
bean.setDrableRes(R.mipmap.icon_three);
} else if (i % 4 == 3) {
bean.setDrableRes(R.mipmap.icon_four);
}
bean.setTitle("按钮" + i);
mBeans.add(bean);
}
mAdapter = new MyGridAdapter(this);
mMyGridView.setAdapter(mAdapter);
mMyGridView.setOnChangeListenner(new MyGridView.onChangeListener() {
@Override
public void onChange(int start, int end, int drag) {
mAdapter.changeItem(start, end, drag);
}
});
}
}
3.数据bean类:MyGridBean
测试demo仅需要两个属性即可,图片资源和title,实际可以再拓展一个属性,三种type不同的gridview:空白view、添加view、显示的view;具体根据业务实现吧,在此简单略过了
public class MyGridBean {
private int drableRes;
private String title;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public int getDrableRes() {
return drableRes;
}
public void setDrableRes(int drableRes) {
this.drableRes = drableRes;
}
}
4.适配器:MyGridAdapter
因为就适配个图片和文字,getview就不详细描述,主要讲下changeItem,最初开始是交换完数据源内容直接notifyDataSetChanged,后面发现输入item过多会有少许卡顿 ,所以优化了下,然后出了个问题,就是如果滑屏换位的话,有可能getChildAt会取到null,因为换位item已经不可见了,所以这时候就不要对不可见view操作,数据源替换之后,再次可见这个view时还会调用getview
class MyGridAdapter extends BaseAdapter {
Context context;
public MyGridAdapter(Context context) {
this.context = context;
}
@Override
public int getCount() {
return mBeans.size();
}
@Override
public Object getItem(int position) {
return mBeans.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
convertView = View.inflate(context, R.layout.layout_my_grid, null);
viewHolder = new ViewHolder();
viewHolder.iv = (ImageView) convertView.findViewById(R.id.iv_layout_mg);
viewHolder.tv = (TextView) convertView.findViewById(R.id.tv_layout_mg);
MyGridBean bean = mBeans.get(position);
viewHolder.iv.setImageResource(bean.getDrableRes());
viewHolder.tv.setText(bean.getTitle());
convertView.setTag(viewHolder);
return convertView;
}
/**
* 刷新交换item数据,代替notifyDataSetChanged
* @param start
* @param end
* @param drag
*/
public void changeItem(int start, int end, int drag) {
int fristVisible = mMyGridView.getFirstVisiblePosition();
ViewHolder viewHolderStart = null;
ViewHolder viewHolderEnd = null;
MyGridBean tempBean = mBeans.get(start);
mBeans.set(start, mBeans.get(end));
mBeans.set(end, tempBean);
MyGridBean beanStart = mBeans.get(start);
MyGridBean beanEnd = mBeans.get(end);
View viewStart = mMyGridView.getChildAt(start - fristVisible);
View viewEnd = mMyGridView.getChildAt(end - fristVisible);
/**
* 因为拖拽过程可能伴随着滑屏,导致需要换位的item不可见,则getChildAt获取为null,因此以下有关viewStart、viewEnd处理均需加入非null判断
* 但只要在数据源mBeans中交换了,再次显示会重新调用getView,显示还是交换后的数据
*/
if (viewStart != null) {
viewHolderStart = (ViewHolder) viewStart.getTag();
viewHolderStart.iv.setImageResource(beanStart.getDrableRes());
viewHolderStart.tv.setText(beanStart.getTitle());
}
if (viewEnd != null) {
viewHolderEnd = (ViewHolder) viewEnd.getTag();
viewHolderEnd.iv.setImageResource(beanEnd.getDrableRes());
viewHolderEnd.tv.setText(beanEnd.getTitle());
}
if (mMyGridView.getCurrentPosition() == start) {
viewStart.setVisibility(View.INVISIBLE);
if (viewEnd != null) {
viewEnd.setVisibility(View.VISIBLE);
}
} else if (mMyGridView.getCurrentPosition() == end) {
if (viewStart != null) {
viewStart.setVisibility(View.VISIBLE);
}
viewEnd.setVisibility(View.INVISIBLE);
}
//二次交换
if (start == drag || end == drag)
return;
tempBean = mBeans.get(start);
mBeans.set(start, mBeans.get(drag));
mBeans.set(drag, tempBean);
beanStart = mBeans.get(start);
beanEnd = mBeans.get(drag);
viewStart = mMyGridView.getChildAt(start - fristVisible);
viewEnd = mMyGridView.getChildAt(drag - fristVisible);
if (viewStart != null) {
viewHolderStart = (ViewHolder) viewStart.getTag();
viewHolderStart.iv.setImageResource(beanStart.getDrableRes());
viewHolderStart.tv.setText(beanStart.getTitle());
}
if (viewEnd != null) {
viewHolderEnd = (ViewHolder) viewEnd.getTag();
viewHolderEnd.iv.setImageResource(beanEnd.getDrableRes());
viewHolderEnd.tv.setText(beanEnd.getTitle());
}
if (mMyGridView.getCurrentPosition() == start) {
if (viewStart != null) {
viewStart.setVisibility(View.INVISIBLE);
}
if (viewEnd != null) {
viewEnd.setVisibility(View.VISIBLE);
}
} else if (mMyGridView.getCurrentPosition() == drag) {
if (viewStart != null) {
viewStart.setVisibility(View.VISIBLE);
}
if (viewEnd != null) {
viewEnd.setVisibility(View.INVISIBLE);
}
}
}
class ViewHolder {
ImageView iv;
TextView tv;
}
}
5.Bug总结以及改进
可能还存在其他未发现bug,若有网友朋友发现希望不吝赐教,在此简单罗列目前发现的几个Bug:
1.滑屏过程触发长按事件:手指在任意item滑屏,延时时间过后会触发拖拽事件;在dispatchTouchEvent中move对Y偏移量进行判断,若超过一定值即认定为滑动,随后取消长按事件
2.换位可见与不可见问题:拖拽时应当移动时隐藏手指所在item,本来是使用getChildAt获取换位的两个view写在onDragItem中,但是因为个什么原因出现了异常,因此这部分逻辑换到changeItem中
改进:
1.在geiview中没有复用convertView,因为换位没有notifyDataSetChanged,因此效果还是可以的,但是如果增加几种不同type的item,就需要考虑这个问题;
2.换位动画还没有增加,可以让换位更圆润丝滑些
3.当前activity退出后再次进入换位效果不能保存,因此需要数据持久化处理
4.再有什么建议希望大家踊跃提供
6.代码地址
demo:GitHub链接