Android SlideAndDragListView,一个可排序可滑动item的ListView

SlideAndDragListView简介

SlideAndDragListView,可排序、可滑动item显示”菜单”的ListView。

SlideAndDragListView(SDLV)继承于Android的ListView,SDLV可以拖动item到SDLV的任意位置,其中包括了拖动item往上滑和往下滑;SDLV可以向右滑动item,像Android的QQ那样(QQ是向左滑),然后显现出来"菜单”之类的按钮。

github地址:https://github.com/yydcdut/SlideAndDragListView
开源中国:http://git.oschina.net/yydcdut/SlideAndDragListView

怎么使用

XML

 <com.yydcdut.sdlv.SlideAndDragListView
        xmlns:sdlv="http://schemas.android.com/apk/res-auto"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:divider="@android:color/black"
        android:dividerHeight="0.5dip"
        android:paddingLeft="8dip"
        android:paddingRight="8dip"
        sdlv:item_background="@android:color/white"
        sdlv:item_btn1_background="@drawable/btn1_drawable"
        sdlv:item_btn1_text="Delete1"
        sdlv:item_btn1_text_color="#00ff00"
        sdlv:item_btn2_background="@drawable/btn2_drawable"
        sdlv:item_btn2_text="Rename1"
        sdlv:item_btn2_text_color="#ff0000"
        sdlv:item_btn_number="2"
        sdlv:item_btn_width="70dip"
        sdlv:item_height="80dip">
    </com.yydcdut.sdlv.SlideAndDragListView>

attributes

item_background - item滑开那部分的背景。

item_btn1_background - "菜单"中第一个button的背景。

item_btn1_text - "菜单"中第一个button的text。

item_btn1_text_color - "菜单"中第一个button的字体颜色。

item_btn2_background - "菜单"中第二个button的背景。

item_btn2_text - "菜单"中第二个button的text。

item_btn2_text_color - "菜单"中第二个button的字体颜色。

item_btn_number - 要显示出来的”菜单”中的button的个数,在0~2之间。

item_btn_width - “菜单”中button的宽度。

item_height - item的高度。

监听器

SlideAndDragListView.OnListItemLongClickListener
sdlv.setOnListItemLongClickListener(new SlideAndDragListView.OnListItemLongClickListener() {
            @Override
            public void onListItemLongClick(View view, int position) {

            }
        });

public void onListItemLongClick(View view, int position) . 参数 view 是被长点击的item, 参数 position 是item在SDLV中的位置。

SlideAndDragListView.OnListItemClickListener
sdlv.setOnListItemClickListener(new SlideAndDragListView.OnListItemClickListener() {
            @Override
            public void onListItemClick(View v, int position) {

            }
        });

public void onListItemClick(View view, int position) . 参数 view 是被点击的item, 参数 position 是item在SDLV中的位置。

SlideAndDragListView.OnDragListener
sdlv.setOnDragListener(new SlideAndDragListView.OnDragListener() {
            @Override
            public void onDragViewMoving(int position) {

            }

            @Override
            public void onDragViewDown(int position) {

            }
        });

public void onDragViewMoving(int position) .参数 position 是被拖动的item的现在所在的位置,同时onDragViewMoving这个方法会被不停的调用,因为一直在拖动,同时position也会改变。

public void onDragViewDown(int position) . 参数 position 是被拖动的item被放下的时候在SDLV中的位置。

SlideAndDragListView.OnSlideListener
sdlv.setOnSlideListener(new SlideAndDragListView.OnSlideListener() {
            @Override
            public void onSlideOpen(View view, int position) {

            }

            @Override
            public void onSlideClose(View view, int position) {

            }
        });

public void onSlideOpen(View view, int position). 参数 view 是滑动开的那个item, 同时 position 是那个item在SDLV中的位置。

public void onSlideClose(View view, int position).参数 view 是滑动关闭的那个item, 同时 position 是那个item在SDLV中的位置。

SlideAndDragListView.OnButtonClickListenerProxy
sdlv.setOnButtonClickListenerProxy(new SlideAndDragListView.OnButtonClickListenerProxy() {
            @Override
            public void onClick(View view, int position, int number) {

            }
        });

public void onClick(View view, int position, int number) . 参数 view 是”菜单”中被点击的button,参数 position 这个button所在的item在SDLV中的位置,参数, number 代表哪一个被点击了,因为可能会有2个。

权限

<uses-permission android:name="android.permission.VIBRATE"/>

简单的实现

SDLV用的是最基本的Android API来实现的,所以很好理解。其中各个功能的实现分别是:

  • 拖动item:Android的View.OnDragListener接口。
  • 向右滑动item显示”菜单”:Android的Scroller类和View的scrollTo方法。
  • 拖动item往上或往下:ListView的smoothScrollToPosition方法。
  • 适配器:BaseAdapter类和ViewHolder。
  • item的长点击事件:因为系统的onItemLongClick事件与View.OnDragListener接口中的事件有冲突,所以我SDLV中通过Handler在手势事件中发送Message模拟onItemLongClick事件。
  • 模拟onItemLongClick中的振动:Context.VIBRATOR_SERVICE。
  • 手势事件:系统的dispatchTouchEvent。

结构

各个击破

里面有几个SDItemXXXX的控件,主要是应对于item的高度而重写了onMeasure

方法。这里就不说了哈。

从layout布局开始说吧:

item_sdlv.xml

<?xml version="1.0" encoding="utf-8"?>
<com.yydcdut.sdlv.SDItemLayout
    android:id="@+id/layout_item_main"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:ignore="MissingPrefix">

    <com.yydcdut.sdlv.SDItemLayout
        android:id="@+id/layout_item_bg"
        android:layout_width="fill_parent"
        android:layout_height="@dimen/slv_item_height"
        android:background="@android:color/transparent">

        <com.yydcdut.sdlv.SDItemBGImage
            android:id="@+id/img_item_bg"
            android:layout_width="fill_parent"
            android:layout_height="@dimen/slv_item_height"
            android:background="@android:color/white"/>

        <com.yydcdut.sdlv.SDItemText
            android:id="@+id/txt_item_edit_btn1"
            android:layout_width="@dimen/slv_item_bg_btn_width"
            android:layout_height="@dimen/slv_item_height"
            android:layout_alignParentLeft="true"
            android:background="@android:color/holo_red_light"
            android:gravity="center"
            android:lines="1"
            android:text="@string/btn1"
            android:textColor="@android:color/white"
            android:textSize="@dimen/txt_size"/>

        <com.yydcdut.sdlv.SDItemText
            android:id="@+id/txt_item_edit_btn2"
            android:layout_width="@dimen/slv_item_bg_btn_width"
            android:layout_height="@dimen/slv_item_height"
            android:layout_toRightOf="@+id/txt_item_edit_btn1"
            android:background="@android:color/darker_gray"
            android:gravity="center"
            android:lines="1"
            android:text="@string/btn2"
            android:textColor="@android:color/white"
            android:textSize="@dimen/txt_size"/>

    </com.yydcdut.sdlv.SDItemLayout>

    <com.yydcdut.sdlv.SDItemLayout
        android:id="@+id/layout_item_scroll"
        android:layout_width="match_parent"
        android:layout_height="@dimen/slv_item_height"
        android:background="@android:color/transparent">

        <com.yydcdut.sdlv.SDItemBGImage
            android:id="@+id/img_item_scroll_bg"
            android:layout_width="fill_parent"
            android:layout_height="@dimen/slv_item_height"
            android:background="@android:color/white"/>

        <FrameLayout
            android:id="@+id/layout_custom"
            android:layout_width="fill_parent"
            android:layout_height="@dimen/slv_item_height">
        </FrameLayout>

    </com.yydcdut.sdlv.SDItemLayout>

</com.yydcdut.sdlv.SDItemLayout>

根是一个RelativeLayout,里面有两个大的RelativeLayout子跟。底层那个RelativeLayout是有三个控件,分别是一个长度宽度都和父Layout一样的ImageView,这个是就前面讲的item_background的背景设置的地方,另外两个是TextView,就是前面讲到的”菜单”中的button。上面那层也有个ImageView,主要是覆盖住下面那层Layout,因为什么不直接用Layout的background呢,因为当时发现scrollTo之后下面那层是没有显示出来的,还是被挡住了的。另外一个是一个FrameLayout,这里是用户自定义的item显示的地方。

看完了item的布局,那么来看看Adapter吧。

public abstract class SDAdapter<T> extends BaseAdapter implements View.OnClickListener {
    /* 上下文 */
    private final Context mContext;
    /* 数据 */
    private List<T> mDataList;
    /* Drag的位置 */
    private int mDragPosition = -1;
    /* 点击button的位置 */
    private int mBtnPosition = -1;
    /* button的单击监听器 */
    private OnButtonClickListener mOnButtonClickListener;
    /* 当前滑开的item的位置 */
    private int mSlideOpenItemPosition;
    /* ---------- attrs ----------- */
    private float mItemHeight;
    private int mItemBtnNumber;
    private String mItemBtn1Text;
    private String mItemBtn2Text;
    private float mItemBtnWidth;
    private Drawable mItemBGDrawable;
    private int mItemBtn1TextColor;
    private int mItemBtn2TextColor;
    private Drawable mItemBtn1Drawable;
    private Drawable mItemBtn2Drawable;
    /* ---------- attrs ----------- */

    public SDAdapter(Context context, List<T> dataList) {
        mContext = context;
        mDataList = dataList;
    }

    @Override
    public int getCount() {
        return mDataList.size();
    }

    @Override
    public Object getItem(int position) {
        return mDataList.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder;
        if (convertView == null) {
            holder = new ViewHolder();
            convertView = LayoutInflater.from(mContext).inflate(R.layout.item_sdlv, null);
            holder.layoutMain = (SDItemLayout) convertView.findViewById(R.id.layout_item_main);
            holder.layoutMain.setItemHeight((int) mItemHeight);
            holder.layoutScroll = (SDItemLayout) convertView.findViewById(R.id.layout_item_scroll);
            holder.layoutScroll.setItemHeight((int) mItemHeight);
            holder.layoutBG = (SDItemLayout) convertView.findViewById(R.id.layout_item_bg);
            holder.layoutBG.setItemHeight((int) mItemHeight);
            holder.imgBGScroll = (SDItemBGImage) convertView.findViewById(R.id.img_item_scroll_bg);
            holder.imgBGScroll.setItemHeight((int) mItemHeight);
            holder.imgBG = (SDItemBGImage) convertView.findViewById(R.id.img_item_bg);
            holder.imgBG.setItemHeight((int) mItemHeight);
            holder.layoutCustom = (FrameLayout) convertView.findViewById(R.id.layout_custom);
            holder.btn1 = (SDItemText) convertView.findViewById(R.id.txt_item_edit_btn1);
            holder.btn2 = (SDItemText) convertView.findViewById(R.id.txt_item_edit_btn2);
            holder.btn1.setBtnWidth((int) mItemBtnWidth);
            holder.btn1.setBtnHeight((int) mItemHeight);
            holder.btn2.setBtnWidth((int) mItemBtnWidth);
            holder.btn2.setBtnHeight((int) mItemHeight);
            //如果用户设置了背景的话就用用户的背景
            if (mItemBGDrawable != null) {
                holder.imgBG.setBackgroundDrawable(mItemBGDrawable);
                holder.imgBGScroll.setBackgroundDrawable(mItemBGDrawable);
            }
            //判断哪些隐藏哪些显示
            checkVisible(holder);
            //设置text
            holder.btn1.setText(mItemBtn1Text);//setText有容错处理
            holder.btn2.setText(mItemBtn2Text);//setText有容错处理
            //设置监听器
            holder.btn1.setOnClickListener(this);
            holder.btn2.setOnClickListener(this);
            //一开始加载的时候都不可点击
            holder.btn1.setClickable(false);
            holder.btn2.setClickable(false);
            //背景和字体颜色
            holder.btn1.setBackgroundDrawable(mItemBtn1Drawable);
            holder.btn2.setBackgroundDrawable(mItemBtn2Drawable);
            holder.btn1.setTextColor(mItemBtn1TextColor);
            holder.btn2.setTextColor(mItemBtn2TextColor);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }

        //没有展开的item里面的btn是不可点击的
        if (mSlideOpenItemPosition == position) {
            holder.btn1.setClickable(true);
            holder.btn2.setClickable(true);
        } else {
            holder.btn1.setClickable(false);
            holder.btn2.setClickable(false);
        }

        //用户的view
        View customView = getView(mContext, holder.layoutCustom.getChildAt(0), position, mDragPosition);
        if (holder.layoutCustom.getChildAt(0) == null) {
            holder.layoutCustom.addView(customView);
        } else {
            holder.layoutCustom.removeViewAt(0);
            holder.layoutCustom.addView(customView);
        }

        //所有的都归位
        holder.layoutScroll.scrollTo(0, 0);

        //把背景显示出来(因为在drag的时候会将背景透明,因为好看)
        holder.imgBGScroll.setVisibility(View.VISIBLE);
        holder.layoutBG.setVisibility(View.VISIBLE);
        return convertView;
    }

    /**
     * 与BaseAdapter类似
     *
     * @param context
     * @param convertView
     * @param position
     * @param dragPosition 当前拖动的item的位置,如果没有拖动item的话值是-1
     * @return
     */
    public abstract View getView(Context context, View convertView, int position, int dragPosition);

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.txt_item_edit_btn1) {
            if (mOnButtonClickListener != null && mBtnPosition != -1) {
                mOnButtonClickListener.onClick(v, mBtnPosition, 0);
            }
        } else if (v.getId() == R.id.txt_item_edit_btn2) {
            if (mOnButtonClickListener != null && mBtnPosition != -1) {
                mOnButtonClickListener.onClick(v, mBtnPosition, 1);
            }
        }
    }

    class ViewHolder {
        public SDItemLayout layoutMain;
        public SDItemLayout layoutScroll;
        public SDItemLayout layoutBG;
        public SDItemBGImage imgBGScroll;
        public SDItemBGImage imgBG;
        public SDItemText btn1;
        public SDItemText btn2;
        public FrameLayout layoutCustom;
    }

    /**
     * 判断用户要几个button
     *
     * @param vh
     */
    private void checkVisible(ViewHolder vh) {
        switch (mItemBtnNumber) {
            case 0:
                vh.btn1.setVisibility(View.GONE);
                vh.btn2.setVisibility(View.GONE);
                break;
            case 1:
                vh.btn1.setVisibility(View.VISIBLE);
                vh.btn2.setVisibility(View.GONE);
                break;
            case 2:
                vh.btn1.setVisibility(View.VISIBLE);
                vh.btn2.setVisibility(View.VISIBLE);
                break;
            default:
                throw new IllegalArgumentException("");
        }
        vh.btn1.setClickable(false);
        vh.btn2.setClickable(false);
    }

    //...............................
}

Adapter里面的作用就是把item的layout显示出来,然后设置高度,某些控件需要设置宽度,然后设置一些其他参数,比如背景啊等等。其中要注意的是holder.btn1.setClickable(false); 和 holder.btn2.setClickable(false);,因为不设置clickable为false的话就出当看不见的时间点击那个位置也会触发onClick事件。第二个就是:holder.layoutScroll.scrollTo(0, 0); 这个地方,当ListView滑走的时候就把这个归位回到0,0的位置,不然回出现顺序错乱。第三个地方是:

//用户的view
        View customView = getView(mContext, holder.layoutCustom.getChildAt(0), position, mDragPosition);
        if (holder.layoutCustom.getChildAt(0) == null) {
            holder.layoutCustom.addView(customView);
        } else {
            holder.layoutCustom.removeViewAt(0);
            holder.layoutCustom.addView(customView);
        }

这里的customView是通过一个abstract方法,用户只需要实现这个Adapter中的这个方法就行了。其次就是getChildAt、addView和removeViewAt这三个方法,主要是不同的position有显示不同的用户的信息。

在onClick事件中要去判断当前点击的是不是已经在item中显现出来的,是的话才回掉出去。

接下来讲讲SDLV吧,我把重要部分的代码贴出来。

public class SlideAndDragListView<T> extends ListView implements Handler.Callback, View.OnDragListener,
        SDAdapter.OnButtonClickListener, AdapterView.OnItemClickListener {
    //....................
    /* onTouch里面的状态 */
    private static final int STATE_NOTHING = -1;//抬起状态
    private static final int STATE_DOWN = 0;//按下状态
    private static final int STATE_LONG_CLICK = 1;//长点击状态
    private static final int STATE_SCROLL = 2;//SCROLL状态
    private static final int STATE_LONG_CLICK_FINISH = 3;//长点击已经触发完成
    private int mState = STATE_NOTHING;
    //.....................
    @Override
    public boolean handleMessage(Message msg) {
        switch (msg.what) {
            case MSG_WHAT_LONG_CLICK:
                if (mState == STATE_LONG_CLICK) {//如果得到msg的时候state状态是Long Click的话
                    //改为long click触发完成
                    mState = STATE_LONG_CLICK_FINISH;
                    //得到长点击的位置
                    int position = msg.arg1;
                    //找到那个位置的view
                    View view = getChildAt(mSlideTargetPosition - getFirstVisiblePosition());
                    //通知adapter
                    mSDAdapter.setDragPosition(position);
                    //如果设置了监听器的话,就触发
                    if (mOnListItemLongClickListener != null) {
                        scrollBack();
                        mVibrator.vibrate(100);
                        mOnListItemLongClickListener.onListItemLongClick(view, position);
                    }
                    mCurrentPosition = position;
                    mBeforeCurrentPosition = position;
                    mBeforeBeforePosition = position;
                    //把背景给弄透明,这样drag的时候要好看些
                    view.findViewById(R.id.layout_item_bg).setVisibility(INVISIBLE);
                    view.findViewById(R.id.img_item_scroll_bg).setVisibility(INVISIBLE);
                    //drag
                    ClipData.Item item = new ClipData.Item("1");
                    ClipData data = new ClipData("1", new String[]{ClipDescription.MIMETYPE_TEXT_PLAIN}, item);
                    view.startDrag(data, new View.DragShadowBuilder(view), null, 0);
                    //通知adapter变颜色
                    mSDAdapter.notifyDataSetChanged();
                }
                break;
        }
        return true;
    }
   //.....................
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (mIsScrollerScrolling) {//scroll正在滑动的话就不要做其他处理了
                    return false;
                }
                //获取出坐标来
                mXDown = (int) ev.getX();
                mYDown = (int) ev.getY();

                //通过坐标找到在ListView中的位置
                mSlideTargetPosition = pointToPosition(mXDown, mYDown);
                if (mSlideTargetPosition == AdapterView.INVALID_POSITION) {
                    return super.dispatchTouchEvent(ev);
                }

                //通过位置找到要slide的view
                View view = getChildAt(mSlideTargetPosition - getFirstVisiblePosition());
                if (view == null) {
                    return super.dispatchTouchEvent(ev);
                }
                mSlideTargetView = view.findViewById(R.id.layout_item_scroll);
                if (mSlideTargetView != null) {
                    //如果已经是滑开了的或者没有滑开的
                    mXScrollDistance = mSlideTargetView.getScrollX();
                } else {
                    mXScrollDistance = 0;
                }
                //当前state状态味按下
                mState = STATE_DOWN;
                break;
            case MotionEvent.ACTION_MOVE:
                if (mIsScrollerScrolling) {//scroll正在滑动的话就不要做其他处理了
                    return false;
                }
                if (fingerNotMove(ev)) {//手指的范围在50以内
                    if (mState != STATE_SCROLL && mState != STATE_LONG_CLICK_FINISH && mState != STATE_LONG_CLICK) {//状态不为滑动状态且不为已经触发完成
                        sendLongClickMessage();
                        mState = STATE_LONG_CLICK;
                    } else if (mState == STATE_SCROLL) {//当为滑动状态的时候
                        //有滑动,那么不再触发长点击
                        removeLongClickMessage();
                    }
                } else if (fingerLeftAndRightMove(ev) && mSlideTargetView != null) {//上下范围在50,主要检测左右滑动
                    boolean bool = false;
                    //这次位置与上一次的不一样,那么要滑这个之前把之前的归位
                    if (mLastPosition != mSlideTargetPosition) {
                        mLastPosition = mSlideTargetPosition;
                        bool = scrollBack();
                    }
                    //如果有scroll归位的话的话先跳过这次move
                    if (bool) {
                        return super.dispatchTouchEvent(ev);
                    }
                    //scroll当前的View
                    int moveDistance = (int) ev.getX() - mXDown;//这个往右是正,往左是负
                    int distance = mXScrollDistance - moveDistance < 0 ? mXScrollDistance - moveDistance : 0;
                    mSlideTargetView.scrollTo(distance, 0);
                    mState = STATE_SCROLL;
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                if (mIsScrollerScrolling) {//scroll正在滑动的话就不要做其他处理了
                    return false;
                }
                if (mSlideTargetView != null && mState == STATE_SCROLL) {
                    //如果滑出的话,那么就滑到固定位置(只要滑出了 mBGWidth / 2 ,就算滑出去了)
                    if (Math.abs(mSlideTargetView.getScrollX()) > mBGWidth / 2) {
                        //通知adapter
                        mSDAdapter.setBtnPosition(mSlideTargetPosition);
                        //不触发onListItemClick事件
                        mOnListItemClickListener = null;
                        mSDAdapter.setSlideOpenItemPosition(mSlideTargetPosition);
                        if (mOnSlideListener != null) {
                            mOnSlideListener.onSlideOpen(mSlideTargetView, mSlideTargetPosition);
                        }
                        //滑出
                        int delta = mBGWidth - Math.abs(mSlideTargetView.getScrollX());
                        if (Math.abs(mSlideTargetView.getScrollX()) < mBGWidth) {
                            mScroller.startScroll(mSlideTargetView.getScrollX(), 0, -delta, 0, SCROLL_QUICK_TIME);
                        } else {
                            mScroller.startScroll(mSlideTargetView.getScrollX(), 0, -delta, 0, SCROLL_TIME);
                        }
                        postInvalidate();
                    } else {
                        //通知adapter
                        mSDAdapter.setBtnPosition(-1);
                        mSDAdapter.setSlideOpenItemPosition(-1);
                        //如果有onListItemClick事件的话,就赋值过去,代表可以触发了
                        if (mTempListItemClickListener != null && mOnListItemClickListener == null) {
                            mOnListItemClickListener = mTempListItemClickListener;
                        }
                        //滑回去,归位
                        if (mOnSlideListener != null) {
                            mOnSlideListener.onSlideClose(mSlideTargetView, mSlideTargetPosition);
                        }
                        mScroller.startScroll(mSlideTargetView.getScrollX(), 0, -mSlideTargetView.getScrollX(), 0, SCROLL_QUICK_TIME);
                        postInvalidate();
                    }
                    mState = STATE_NOTHING;
                    removeLongClickMessage();
                    //更新last的值
                    mLastPosition = mSlideTargetPosition;
                    //设置为无效的
                    mSlideTargetPosition = AdapterView.INVALID_POSITION;
                    return false;
                }
                mState = STATE_NOTHING;
                removeLongClickMessage();
                //更新last的值
                mLastPosition = mSlideTargetPosition;
                //设置为无效的
                mSlideTargetPosition = AdapterView.INVALID_POSITION;
                break;
            default:
                removeLongClickMessage();
                mState = STATE_NOTHING;
                break;
        }
        return super.dispatchTouchEvent(ev);
    }
    //.....................
    @Override
    public boolean onDrag(View v, DragEvent event) {
        final int action = event.getAction();
        switch (action) {
            case DragEvent.ACTION_DRAG_STARTED:
                return true;
            case DragEvent.ACTION_DRAG_ENTERED:
                return true;
            case DragEvent.ACTION_DRAG_LOCATION:
                //当前移动的item在ListView中的position
                int position = pointToPosition((int) event.getX(), (int) event.getY());
                //如果位置发生了改变
                if (mBeforeCurrentPosition != position) {
                    //有时候得到的position是-1(AdapterView.INVALID_POSITION),忽略掉
                    if (position >= 0) {
                        //判断是往上了还是往下了
                        mUp = position - mBeforeCurrentPosition <= 0;
                        //记录移动之后上一次的位置
                        mBeforeBeforePosition = mBeforeCurrentPosition;
                        //记录当前位置
                        mBeforeCurrentPosition = position;
                    }
                }
                moveListViewUpOrDown(position);
                //有时候为-1(AdapterView.INVALID_POSITION)的情况,忽略掉
                if (position >= 0) {
                    //判断是不是已经换过位置了,如果没有换过,则进去换
                    if (position != mCurrentPosition) {
                        if (mUp) {//往上
                            //只是移动了一格
                            if (position - mBeforeBeforePosition == -1) {
                                T t = mDataList.get(position);
                                mDataList.set(position, mDataList.get(position + 1));
                                mDataList.set(position + 1, t);
                            } else {//一下子移动了好几个位置,其实可以和上面那个方法合并起来的
                                T t = mDataList.get(mBeforeBeforePosition);
                                for (int i = mBeforeBeforePosition; i > position; i--) {
                                    mDataList.set(i, mDataList.get(i - 1));
                                }
                                mDataList.set(position, t);
                            }
                        } else {
                            if (position - mBeforeBeforePosition == 1) {
                                T t = mDataList.get(position);
                                mDataList.set(position, mDataList.get(position - 1));
                                mDataList.set(position - 1, t);
                            } else {
                                T t = mDataList.get(mBeforeBeforePosition);
                                for (int i = mBeforeBeforePosition; i < position; i++) {
                                    mDataList.set(i, mDataList.get(i + 1));
                                }
                                mDataList.set(position, t);
                            }
                        }
                        mSDAdapter.notifyDataSetChanged();
                        //更新位置
                        mCurrentPosition = position;
                    }
                }
                //通知adapter
                mSDAdapter.setDragPosition(position);
                if (mOnDragListener != null) {
                    mOnDragListener.onDragViewMoving(mCurrentPosition);
                }
                return true;
            case DragEvent.ACTION_DRAG_EXITED:
                return true;
            case DragEvent.ACTION_DROP:
                mSDAdapter.notifyDataSetChanged();
                //通知adapter
                mSDAdapter.setDragPosition(-1);
                if (mOnDragListener != null) {
                    mOnDragListener.onDragViewDown(mCurrentPosition);
                }
                return true;
            case DragEvent.ACTION_DRAG_ENDED:
                return true;
            default:
                break;
        }
        return false;
    }
    //.....................
    /**
     * 如果到了两端,判断ListView是往上滑动还是ListView往下滑动
     *
     * @param position
     */
    private void moveListViewUpOrDown(int position) {
        //ListView中最上面的显示的位置
        int firstPosition = getFirstVisiblePosition();
        //ListView中最下面的显示的位置
        int lastPosition = getLastVisiblePosition();
        //能够往上的话往上
        if ((position == firstPosition || position == firstPosition + 1) && firstPosition != 0) {
            smoothScrollToPosition(firstPosition - 1);
        }
        //能够往下的话往下
        if ((position == lastPosition || position == lastPosition - 1) && lastPosition != getCount() - 1) {
            smoothScrollToPosition(lastPosition + 1);
        }
    }
    //.....................
}

首先看到的前面一堆声明的STATE状态,这是我给dispatchTouchEvent设置的状态机,理解了设定的状态之后,了解了不同的状态下能做什么不能做什么之后,在dispatchTouchEvent代码里面就可以看起来很简单了。

首先,当手指按下的时候,回去取出X,Y坐标保存下来,通过X,Y坐标和pointToPosition()方法来确定当前这个左边是哪个item,得到item的位置,有些情况下返回的是-1,所以这里进行判断如果是-1(AdapterView.INVALID_POSITION)的话就先跳过。如果不是,那么得到这个item的View,判断这个item的View有没有scroll过,scroll的距离是多少。此时将state的状态变为DOWN

到MOVE的情况了。首先判断scroller的computeScroll方法是不是正在被调用,是的话返回false,代表事件不再往下传递,不是的话继续往下走,判断MOVE情况下手指偏移量有哆嗦,如果上下左右都是在50以内的话,并且state不为SCROLL和LONG_CLICK_FINISH,判定为用户有长点击的趋势,那么发送一个长点击的Message出去,此事state状态变为LONG_CLICK,如果后面一直是这样的话,Handler取出消息进行处理,如果是LONG_CLICK的话就进行长点击的事件处理,此时状态变为LONG_CLICK_FINISH;如果之前是有那个趋势,但是长点击的触发时间没到,就滑动的了,状态变为了SCROLL了,就把那条长点击的Message的时间从MessageQueue中取消掉。现在说如果变成SCROLL状态,如果手指上下偏移唉50以内,并且左右偏移超过50,那么可以定义为SCROLL状态。在此状态中需要判断是否已经有View被Slide Open了,有的话将其归位,回到0,0处,然后跳过,如果没有的话,则进行View的scrollTo操作,此时state的状态变为SCROLL

到了手指抬起的情况了。首先判断scroller的computeScroll方法是不是正在被调用。之后去判断当前的这个View的Scroll了的距离,如果超出了我们所规定了,通过Scroller滚到指定地方。在这里,规定了”菜单”中的距离的一半不到,滚到0,0处,超过一半或者远远超过距离,则滚到”菜单宽度的距离处”。之后将state状态变为NOTHING。返回false不向下传递事件了。

dispatchTouchEvent简单的分析完了,回过头来说为什么要用dispatchTouchEvent而不是onTouchEvent,我是这样想的:dispatchTouchEvent和onTouchEvent差不多,但是onTouchEvent做了很多其他的处理,比如系统的单击和长点击事件等等,我在dispatchTouchEvent做出来,返回true或者false还可以控制去不去触发onTouchEvent中的系统事件。所以选择了dispatchTouchEvent。至少我是这么理解的,对Touch这块还不是特别熟悉,有不对的地方请指出。

好,现在分析拖动。拖动的开始是在这里:

//drag
                    ClipData.Item item = new ClipData.Item("1");
                    ClipData data = new ClipData("1", new String[]{ClipDescription.MIMETYPE_TEXT_PLAIN}, item);
                    view.startDrag(data, new View.DragShadowBuilder(view), null, 0);
                    //通知adapter变颜色
                    mSDAdapter.notifyDataSetChanged();

响应事件是在这里:

public boolean onDrag(View v, DragEvent event) {
    return false;
}

其中DragEvent中有许多ACTION,而我们只需要用到DragEvent.ACTION_DRAG_LOCATIONDragEvent.ACTION_DROP

在DRAG_LOCATION当中,首先是确定位置。然后记录位置,通过这个位置与之前记录的位置判断现在的操作是要往上拖动还是往下拖动,如果位置发生变化那么就在存放数据恩List里面调换位置,然后notify一下dataChange了。在这个过程中还要一个判断,就是在moveListViewUpOrDown(position);这个方法里面,这里面主要是判断这个position是不是到了顶端或者底端,是的话就让listview往上滑或者往下滑。在ACTION_DROP中就是释放了拖放的item。

总结

其实整个控件并不是那么复杂,只是有些地方脑子绕不过弯来,但是这样的地方也不多。往上也有很多这样类似的控件,有一个动画做的超级好,我还没有去读过他们的代码。有人问我最近在做什么,我就说最近自己在搞一个APP,然后把一些控件抽出来开源,就比如这个,他说这个往上有很多,干嘛自己写,当时我简单的回答说写着好玩。但是现在发现很多东西实践了才真正理解了。

谢谢大家,控件地址在:https://github.com/yydcdut/SlideAndDragListView
开源中国:http://git.oschina.net/yydcdut/SlideAndDragListView

我还在不断的改进,比如两边都可以滑之类的。

时间: 2024-10-16 13:49:35

Android SlideAndDragListView,一个可排序可滑动item的ListView的相关文章

【Android】一个activity中垂直排列两个listview

为了实现这种效果,一开始我想到的办法是ilistVew中加上一个footerview,footerview里面嵌套一个listview,但是实际操作之后发现footerview里的listview只显示一项,这个问题困扰了半天,一直没有找到合适的解决办法,直到昨天晚上偶然看到一篇博文介绍说,scrollview里嵌套listview也出现了同样的问题,解决办法是动态设置listview的高度,于是我用这种方法也试了试,结果还真解决了. MainActivity.java package com.

RecyclerView实现条目Item拖拽排序与滑动删除

RecyclerView实现条目Item拖拽排序与滑动删除 版权声明:转载请注明本文转自严振杰的博客: http://blog.csdn.net/yanzhenjie1003 效果演示 直播视频讲解:[http://pan.baidu.com/s/1miEOtwG1 推荐大家结合我直播的视频看效果更好. 本博客源码传送门. 需求和技术分析 RecyclerView Item拖拽排序::长按RecyclerView的Item或者触摸Item的某个按钮. RecyclerView Item滑动删除:

Android开发学习之路-RecyclerView滑动删除和拖动排序

Android开发学习之路-RecyclerView使用初探 Android开发学习之路-RecyclerView的Item自定义动画及DefaultItemAnimator源码分析 Android开发学习之路-下拉刷新怎么做? 本篇是接着上面三篇之后的一个对RecyclerView的介绍,这里多说两句,如果你还在使用ListView的话,可以放弃掉ListView了.RecyclerView自动帮我们缓存Item视图(ViewHolder),允许我们自定义各种动作的动画和分割线,允许我们对It

Android 实例讲解HorizontalScrollView实现左右滑动

本博文主要讲解怎么使用HorizontalScrollView实现左右滑动的效果. HorizontalScrollView实际上是一个FrameLayout ,一般通过只放置一个LinearLayout子控件.如果要使其添加其他的控件,就使用LinearLayout子控件来添加其他的控件,最后达到丰富其内容的效果.其中,LinearLayout设置的orientation布局为Horizontal.HorizontalScrollView不可以和ListView同时用,因为ListView有自

Android Studio 一个完整的APP实例(附源码和数据库)

前言: 这是我独立做的第一个APP,是一个记账本APP. This is the first APP, I've ever done on my own. It's a accountbook APP. 源码: https://github.com/AnneHan/accountBook 欢迎satr or fork 备注: APP中所涉及到的图标请勿商用 效果图 (备注:在把图片制作成gif时,图片的质量受损,所以最终呈现出来的gif图片,背景变得有些模糊) 开发环境 IDE:Android

Android 加入一个动作按钮

在XML中声明一个动作按钮 所有的动作按钮和其他的可以利用的items都定义在menu资源文件夹中的XML文件中.为了增加一个动作按钮到工具栏,需要在工程 /res/menu/ 目录下面创建一个新的XML文件. 对每个需要添加的item增加一个<item>元素包含到工具栏中,例如 res/menu/main_activity_actions.xml <menu xmlns:android="http://schemas.android.com/apk/res/android&q

Android自定义组件——四个方向滑动的菜单实现

今天无意中实现了一个四个方向滑动的菜单,感觉挺好玩,滑动起来很顺手,既然已经做出来了就贴出来让大家也玩弄一下. 一.效果演示 (说明:目前没有安装Android模拟器,制作的动态图片太卡了,就贴一下静态图片吧,实际效果可以下载源代码查看) (向上滑动) (向下滑动) (向左滑动) (向右滑动) 二.实现过程介绍 1.放置5个View (分别是上下左右中) @Override protected void onLayout(boolean changed, int l, int t, int r,

Android:一个高效的UI才是一个拉风的UI(二)

趁今晚老大不在偷偷早下班,所以有时间继续跟大伙扯扯UI设计之痛,也算一个是对上篇<Android:一个高效的UI才是一个拉风的UI(一)>的完整补充吧.写得不好的话大家尽管拍砖~(来!砸死我把~) 前言 前篇博客翻箱倒柜的介绍了优化UI设计的两个方法,第一个就是使用尽量少的组件来实现布局功能,第二个就是使用<meger>标签来减少不必要的根节点,这两个方法都可以提高应用UI的运行效率,但是够了吗?远远是不够的,方法就像money一样永远不嫌多,所以不再介绍多一些UI设计优化的方法说

Android——用FragmentPagerAdapter实现Fragment的滑动效果

效果: ViewPage来源于android -support.v4 什么是viewPage?ViewPage 类似于ListView 用于显示多个View集合. 支持页面左右滑动. 如何使用viewPage以及需要注意点?ViewPage 需要Adapter:PagerAdapter 有四个重要方法:(1) void destroyItem(ViewGroup container, int position, Object object): 销毁(2)Object instantiateIte