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
我还在不断的改进,比如两边都可以滑之类的。