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_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
我还在不断的改进,比如两边都可以滑之类的。
我是天王盖地虎的分割线
Github:https://github.com/yydcdut/SlideAndDragListView
开源中国:http://git.oschina.net/yydcdut/SlideAndDragListView
SlideAndDragListView,一个可排序可滑动item的ListView的更多相关文章
- Android SlideAndDragListView,一个可排序可滑动item的ListView
SlideAndDragListView简介 SlideAndDragListView,可排序.可滑动item显示”菜单”的ListView. SlideAndDragListView(SDLV)继承 ...
- RecyclerView拖拽排序和滑动删除实现
效果图 如何实现 那么是如何实现的呢?主要就要使用到ItemTouchHelper ,ItemTouchHelper 一个帮助开发人员处理拖拽和滑动删除的实现类,它能够让你非常容易实现侧滑删除.拖拽的 ...
- ItemTouchHelper(实现RecyclerView上添加拖动排序与滑动删除的所有事情)
简单介绍: ItemTouchHelper是一个强大的工具,它处理好了关于在RecyclerView上添加拖动排序与滑动删除的所有事情.它是RecyclerView.ItemDecoration的子类 ...
- RecyclerView实现拖动排序和滑动删除功能
RecyclerView 的拖动排序需要借助一下 ItemTouchHelper 这个类,ItemTouchHelper 类是 Google 提供的一个支持 RecyclerView 滑动和拖动的一个 ...
- WP8.1 侧边滑动Item
效果图 我看ios 和安卓上有好多类似的Item的效果,UWP上有微软官方的库,其中也有类似得效果,看样子WP8.1没有啊,顺便我的程序也是需要,我也就仿了一个. 具体思路是: 触摸控制GRId在CA ...
- 一个H5的3D滑动组件实现(兼容2D模式)
起由 原始需求来源于一个项目的某个功能,要求实现3D图片轮播效果,而已有的组件大多是普通的2D图片轮播,于是重新造了一个轮子,实现了一个既支持2D,又支持3D的滑动.轮播组件. 实现思路 刚一开始肯定 ...
- 写一个js向左滑动删除 交互特效的插件——Html5 touchmove
需求描述 需要实现类似QQ中对联系人的操作:向左滑动,滑出删除按钮.滑动超过一半时松开则自动滑到底,不到一半时松开则返回原处. 纯js实现 使用了h5的touchmove等事件,以及用js动态改变cs ...
- JavaScript写一个表格排序类
依稀记得那是上个星期六的下午,我参加了网易暑期实习生招聘笔试.考得相当糟糕,编程题3个题通过了2个,简答题没做对,选择题貌似是20个题猜了6-7个,99%是挂了,唉唉唉!生活不只眼前的苟且,学习的脚步 ...
- 42.输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S, 如果有多对数字的和等于S,输出两个数的乘积最小的。
输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S, 如果有多对数字的和等于S,输出两个数的乘积最小的. 这道题有很多烟雾弹: 首先如果有多对,最前面的两个数就是乘积最小的, ...
随机推荐
- JAVA 8 方法引用 - Method References
什么是方法引用 简单地说,就是一个Lambda表达式.在Java 8中,我们会使用Lambda表达式创建匿名方法,但是有时候,我们的Lambda表达式可能仅仅调用一个已存在的方法,而不做任何其它事,对 ...
- servlet 学习(一)
一.Servlet简介 Servlet是sun公司提供的一门用于开发动态web资源的技术. Sun公司在其API中提供了一个servlet接口,用户若想用发一个动态web资源(即开发一个Java程序向 ...
- PHP 类型判断和NULL,空值检查
PHP是一种宽松类型的编程语言,在函数中对传入的参数值的“类型”以及”值是否为空或者NULL“进行检查是不可缺少的步骤. 类型检查 从PHP5开始,PHP允许对函数的参数进行类型约束,即可以约束参数的 ...
- 手写一个json格式化 api
最近写的一个东西需要对json字符串进行格式化然后显示在网页上面. 我就想去网上找找有没有这样的api可以直接调用.百度 json api ,搜索结果都是那种只能在网页上进行校验的工具,没有api. ...
- Linux IPC Pipe
mkfifo() //创建有名管道(FIFO special file),创建完了就像普通文件一样open(),再读写,成功返回0,失败返回-1设errno.VS$man 3 mkfifo #incl ...
- PlaceHolder的两种实现方式
placeholder属性是HTML5 中为input添加的.在input上提供一个占位符,文字形式展示输入字段预期值的提示信息(hint),该字段会在输入为空时显示. 如 <input typ ...
- 解决Python2.7的UnicodeEncodeError: 'ascii' codec can’t encode异常错误
import sys reload(sys) sys.setdefaultencoding('utf-8') 好了,通过上面短短的三行,我们算是很好的解决了这个问题了,同样的方式也可以应用到Unico ...
- ELF Format 笔记(一)—— 概述
ilocker:关注 Android 安全(新手) QQ: 2597294287 ELF Object files 参与程序的链接和执行,从这两个角度分别有两种视图: ELF header 位于文件的 ...
- jquery常用方法
一.多个按钮绑定同一事件 $("#index_svip,#index_svip_renew").click(function() { seajs.use(['svipLayer'] ...
- [转]新型智慧城市总体架构 华为 新ICT 一云二网三平台
本文转自:http://www.jpsycn.com/hangyexinwen/20160801142354.html “十三五”规划提出,充分运用现代信息技术和大数据,建设一批新型示范性智慧城市.日 ...