SlideAndDragListView,一個可排序可滑動item的ListView

SlideAndDragListView簡介

SlideAndDragListView,可排序、可滑動item顯示」菜單」的ListView。java

SlideAndDragListView(SDLV)繼承於Android的ListView,SDLV能夠拖動item到SDLV的任意位置,其中包括了拖動item往上滑和往下滑;SDLV能夠向右滑動item,像Android的QQ那樣(QQ是向左滑),而後顯現出來"菜單」之類的按鈕。android

github地址:https://github.com/yydcdut/SlideAndDragListView
開源中國:http://git.oschina.net/yydcdut/SlideAndDragListViewgit

怎麼使用

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滑開那部分的背景。github

item_btn1_background - "菜單"中第一個button的背景。ide

item_btn1_text - "菜單"中第一個button的text。佈局

item_btn1_text_color - "菜單"中第一個button的字體顏色。post

item_btn2_background - "菜單"中第二個button的背景。字體

item_btn2_text - "菜單"中第二個button的text。動畫

item_btn2_text_color - "菜單"中第二個button的字體顏色。ui

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

我還在不斷的改進,好比兩邊均可以滑之類的。

我是天王蓋地虎的分割線

Github:https://github.com/yydcdut/SlideAndDragListView 開源中國:http://git.oschina.net/yydcdut/SlideAndDragListView

相關文章
相關標籤/搜索