您的位置:首页 > 其它

ListView拖拽交换 item 的实现(QQ 分组管理功能)

2015-03-18 23:17 721 查看
在写这篇文章前,碰巧看到有个哥们也做了这个功能,【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 一般都是复用的。

点击下载源码
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐