ListView拖拽互换 item 的实现(QQ 分组管理功能)
在写这篇文章前,碰巧看到有个哥们也做了这个功能,【Android】可拖拽排序的ListView。而且就在几个小时前发表的,本来想还是算了,我就不写这个功能,不过我大致浏览了他的实现原理跟我的实现原理还是有很大差别,所以还是决定写这样一篇文章,因为我相信大家看文章,更多是想了解其中的原理,而非单纯的为了实现某个功能。只有了解了原理,才能扩展,实现更多的功能。
好了,回到正题,先来看效果图
分析原理
1、长按获取所按 Item 的 View,并通过View 生成一个 Drawable;
2、 在 ListView 中根据手势滑动不断的绘制 Drawable;
3、Drawable每到达另一个 Item的位置,就替换这个 item,并设置动画效果;
4、当 Drawable 滑到屏幕的最下方或者最上方,且此 ListView 可以滚动,这 listView 执行滚动。
开篇就讲了,此功能的实现方式不止一种,所以有兴趣了解其他实现原理可以看 [【Android】可拖拽排序的ListView]这篇文章。
分步实现
这里就按照原理来一步一步实现此功能,先来看如何获取 Drawable
通过 View 获取 bitmapDrawable
通过 position 获取 View
/*获取Item的View*/
View mobileView = getViewForPosition(position);
因为我们一般在 ListView 的 adapter 中都会复用 View,所以这里需要做一个 position 的转化
/**
* 通过位置获取View
*
* @param position
* @return
*/
public View getViewForPosition(int position) {
int itemPosition = position - getFirstVisiblePosition();
View view = getChildAt(itemPosition);
return view;
}
通过 View,获取 Drawable
/**
* 创建一个
* BitmapDrawable
* @param view
* @return
*/
private BitmapDrawable createDrawable(View view) {
BitmapDrawable bitmapDrawable = null;
/*获取位置变量*/
int left = view.getLeft();
int top = view.getTop();
int right = left + view.getMeasuredWidth();
int bottom = top + view.getMeasuredHeight();
/*首先创建一个Bitmap,这个位图为空位图,里面啥也没有*/
Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
/*创建一个画布,把空位图放入画布中*/
Canvas canvas = new Canvas(bitmap);
/*把所选的item的View 绘制到画布上,其实也就是会知道了Bitmap上*/
view.draw(canvas);
/*绘制边矩形*/
Rect rect = new Rect(0,0,bitmap.getWidth(),bitmap.getHeight()) ;
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG) ;
paint.setStrokeWidth(12);
paint.setStyle(Paint.Style.STROKE);
paint.setColor(0xff1f8c03);
canvas.drawRect(rect, paint);
/*把bitmap转化成BitmapDrawable*/
bitmapDrawable = new BitmapDrawable(getResources(), bitmap);
/*设置drawable的原始位置*/
mCellOriginBounds = new Rect(left, top, right, bottom);
/*初始化drawable的当前位置*/
mCellCurrentBounds = new Rect(mCellOriginBounds);
bitmapDrawable.setBounds(mCellCurrentBounds);
/*设置drawable的透明度*/
bitmapDrawable.setAlpha(120);
return bitmapDrawable;
}
这里获取的是一个 BitmapDrawable,其实也就是一个 Drawable,BitmapDrawable继承之Drawable,所以很多方法可以共用;并且设置了它的位置,跟所选中的 item 的位置一样,
bitmapDrawable.setBounds(mCellCurrentBounds);
接着是把 Drawable 绘制在 ListView 上面
/*绘制Drawable*/
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (mMobileView != null) {
mMobileView.draw(canvas);
}
}
调用invalidate()重绘方法,这时的效果图已经可以看到了
监听手势滑动
手势滑动的时候不断改变 drawable 的 bounds,并invalidate(),这时我们所获取的 BitmapDrawable 就会不断的改变位置,具体怎么改变呢,看代码
@Override
public boolean onTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
/*这里是获取按下的时候点的坐标位置*/
int pointIndex = MotionEventCompat.getActionIndex(ev);
mActivityPointId = MotionEventCompat.getPointerId(ev, pointIndex);
if (mActivityPointId == INVALID) {
break;
}
mDownMotionY = (int) MotionEventCompat.getY(ev, pointIndex);
break;
case MotionEvent.ACTION_MOVE:
if (isMove) {
int pointMoveIndex = MotionEventCompat.getActionIndex(ev);
mActivityPointId = MotionEventCompat.getPointerId(ev, pointMoveIndex);
if (mActivityPointId == INVALID) {
break;
}
mCurrentMotionY = (int) MotionEventCompat.getY(ev, mActivityPointId);
int delay = mCurrentMotionY - mDownMotionY;
/*偏移Drawable的位置*/
mCellCurrentBounds.offsetTo(mCellCurrentBounds.left, mCellOriginBounds.top + delay + mScrollOffset);
mMobileView.setBounds(mCellCurrentBounds);
/*去判断是否需要交换数据*/
checkExchangeItem();
isScroll = false;
/*判断是否需要滚动ListView*/
isScroll = checkScroll();
/*然后重绘*/
invalidate();
return false;
}
break;
case MotionEvent.ACTION_UP:
/*松手执行结束拖拽操作*/
touchEventEnd();
break;
case MotionEvent.ACTION_POINTER_UP:
int pointUpIndex = MotionEventCompat.getActionIndex(ev);
int pointId = MotionEventCompat.getPointerId(ev, pointUpIndex);
if (pointId == mActivityPointId) {
/*松手执行结束拖拽操作*/
touchEventEnd();
}
break;
case MotionEvent.ACTION_CANCEL:
/*这里直接复原*/
touchEventCancel();
break;
}
return super.onTouchEvent(ev);
}
在 Drawable 改变位置的时候,我们是要不断的检测,是否达到交换 item 的条件,如果达到了,则执行交换,并设置相应的交换动画效果。
检测和执行 item 交换
/**
* 检测是否需要交换数据,如果需要则交换
*/
private void checkExchangeItem() {
/*这个如果是初始化的时候调用,则直接返回*/
if (mCellOriginBounds == null) {
return;
}
/*获取滑动两点之间的差值*/
final int deltaY = mCurrentMotionY - mDownMotionY;
/*获取滑动的总值*/
int deltaYTotal = mCellOriginBounds.top + mScrollOffset + deltaY;
/*获取相邻两个View 和自己的View*/
View belowView = getViewForID(mBelowItemId);
View mobileView = getViewForID(mMobileItemId);
View aboveView = getViewForID(mAboveItemId);
/*判断是向下还是向上滑动*/
boolean isBelow = (belowView != null) && (deltaYTotal > belowView.getTop());
boolean isAbove = (aboveView != null) && (deltaYTotal < aboveView.getTop());
if (isAbove || isBelow) {
final long switchItemID = isBelow ? mBelowItemId : mAboveItemId;
/*获取下一个 item 的View*/
View switchView = isBelow ? belowView : aboveView;
/*获取原始 item 的 position,用于数据交换*/
final int originalItem = getPositionForView(mobileView);
/*真正交换两个数据*/
SwapItem(originalItem, getPositionForView(switchView));
/*设置显示和隐藏View*/
switchView.setVisibility(INVISIBLE);
mobileView.setVisibility(VISIBLE);
/*更新Adapter 的数据*/
((BaseAdapter) getAdapter()).notifyDataSetChanged();
/*重新赋值*/
mDownMotionY = mCurrentMotionY;
/*更新相邻两个 item 的 id*/
updateNeighborViewsForID(mMobileItemId);
/*设置偏移量,这个的作用是用于ListView 的滚动*/
mScrollOffset = mScrollOffset + deltaY;
/*----------以下部分为动画效果部分---------------*/
View swappedView = getViewForID(switchItemID) ;
int switchViewNewTop = swappedView.getTop();
int switchTop = switchView.getTop() ;
int delta = switchTop - switchViewNewTop ;
swappedView.setTranslationY(delta);
ObjectAnimator animator = ObjectAnimator.ofFloat(swappedView,
"translationY",0);
animator.setDuration(MOVE_DURATION);
animator.start();
}
}
/**
* 根据位置 交换两个数据,
*
* @param indexOne
* @param indexTwo
*/
private void SwapItem(int indexOne, int indexTwo) {
Object temp = mList.get(indexOne);
mList.set(indexOne, mList.get(indexTwo));
mList.set(indexTwo, temp);
}
在 Drawable 改变位置的时候,还有一件事我们不能忘了,就是要判断 Drawable 所在的 item 是否已经到了 ListView 不可见的位置,比如我们这里的屏幕顶部或者底部,如果到了,则触发 ListView 滚动
判断和执行 ListView 滚动
/**
* 检测是否可以滚懂
* @return
*/
private boolean checkScroll() {
int height = getHeight();
/*如果当前 item 的顶部小于0,即到了 listView 的顶部不可见的位置。*/
if (mCellCurrentBounds.top < 0) {
smoothScrollBy(-4, 0);
return true;
}
/*当当前 item 的底部大于ListView 的高度,即到了 listView 的低部不可见的位置*/
if (mCellCurrentBounds.top + mCellCurrentBounds.height() > height ) {
smoothScrollBy(4, 0);
return true;
}
return false;
}
其实上面判断条件如果严谨一点还需要判断当前 ListVeiw 是否到了最顶端,或者最低端,也就是不能滑动的时候,如果到了,则不让滑动,当然,没有判断也没有关系,因为如果到了滑不动的时候,系统默认不能滑动。
在 ListView 滚动的时候还需要检测和交换 item
private class OnScrollerImpl implements OnScrollListener {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
/*停止滚动,继续判断是否符合滚动的条件*/
if (isMove && isScroll) {
/**/
isScroll = checkScroll();
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (isMove) {
/*检测和交换 item*/
checkExchangeItem();
/*更新相邻的两个 itemId*/
updateNeighborViewsForID(mMobileItemId);
}
}
}
最后就是放开手指的时候的操作了
/**
*
*/
private void touchEventEnd() {
final View mobileView = getViewForID(mMobileItemId);
if (isMove){
isMove = false;
isScroll = false;
mActivityPointId = INVALID;
/*------------放开手指时的结束动画-------------*/
mCellCurrentBounds.offsetTo(mCellOriginBounds.left, mobileView.getTop());
ObjectAnimator hoverViewAnimator = ObjectAnimator.ofObject(mMobileView, "bounds",
sBoundEvaluator, mCellCurrentBounds);
hoverViewAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
invalidate();
}
});
hoverViewAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
setEnabled(false);
}
@Override
public void onAnimationEnd(Animator animation) {
mAboveItemId = INVALID;
mMobileItemId = INVALID;
mBelowItemId = INVALID;
mobileView.setVisibility(VISIBLE);
mMobileView = null;
setEnabled(true);
invalidate();
}
});
hoverViewAnimator.start();
}else {
touchEventCancel();
}
}
ListView拖拽交换 item 的核心实现就是以上内容了,其他只要稍微注意一下;
总结:
发现很多篇文章没写总结了,以后的文章最好还是写一下,算是对整篇文章知识的一个回顾;
1、通过 View 创建 Drawable,这里主要用到 Bitmap、Canvas两个类,首先创建一个空的 Bitmap,其次创建一个 Canvas并把 Bitmap 传入,这个时候,我的理解是,用 Bitmap 来作为画布,再把 View 通过 onDraw(Canvas c)绘制 View 的内容到画布上。
2、手势监听,这个不多解释了!
3、属性动画的运用。这里主要是对 ObjectAnimator做了最基本的使用,也非常简单。
4、ListView 通过 position 来获取 item 的 View,不能直接通过 getChildAt(int i),因为 item 一般都是复用的。
点击下载源码
- 1楼cxl84998858911小时前
- 楼主你真的很厉害,我们的项目中就用到了listView 的拖拽功能,我当时是从网上down的!膜~~~