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_LOCATION
和DragEvent.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
我還在不斷的改進,比如兩邊都可以滑之類的。