您的位置:首页 > 移动开发 > IOS开发

[笨笨的方法] 实现IOS列表的滑动删除效果

2014-04-04 10:17 731 查看
一、背景

在做项目的时候,有一个需求,在两级列表中,实现类似于IOS的滑动删除效果,大体如下图:



但有两点不太一样的地方:上层界面,是随手势滑动的;下层界面在上层被滑走后露出来。

老大让我实现这个功能时,我想这个功能应该很简单啊,我就准备这样来做了:

1.写一个对应每行的View类,本身支持滑动,这个应该不难写。

2.让ExpandableListVIew的使用上述的View作为childView。

这样很简单就实现了嘛。

万万没想到的是,这个类写好了,我把它放在一个ListView中先试了一下,每行左右滑动没有问题,但是每一行点不了!即使设置了onItemClickListener,也监听不到事件!

我回想了一下,发现我把这个问题想简单了:一个支持滑动的类,必然会重写onTouch()来处理对它的触摸事件,但是把这个View放到List里,就会带来这样的问题,onTouch都被View消耗(consume)了,ListView的点击事件就无法触发了。

我们这儿用到的触摸动作分为:DOWN, MOVE和UP,Android中触摸动作是层层传递的,并在某一层被消耗掉。

ListView的点击需要接收一个DOWN和一个UP,这样构成单击;而我自己的View,需要先接收一个DOWN,后续才会接收到MOVE和UP,这就形成了一个矛盾:把DOWN给List,我的View就滑不动;把DOWN给View,List就点不了,听上去真的有点蛋蛋的忧伤...

二、怎么解决这个问题呢?

通过看原来我们代码的实现和自己在网上查找方法,找到两种解决办法:

1. 参照SwipeListView(https://github.com/47deg/android-swipelistview)这一开源工程,它为ListView实现了滑动功能,解决方案是,对ListView和Item的onInterceptTouchEvent和onTouch事件进行了很详细地动作判定和操作(把笔者给看晕了),但笔者需要的是ExpandableListView(啊魂淡),而且看起代码来也有点力不从心,所以干脆就放弃了,有兴趣的同学可以直接使用,或参照修改后使用。

2. 受到一段代码的启发(这段代码的功能是,通过点击在ListView上的x,y,获得ListView对应的position),楼主就想,能不能通过点击在ExpandableListView上的x,y,获得ExpandableListView对应的groupPosition和childPosition呢,能获得这两个position,点击事件不就很easy了么?

三、动起手来骚年们

1. 通过x,y获得groupPosition和childPosition,新建一个类ExpandableListView2继承于ExpandableListView,并在其中添加方法:

/**
* 通过position,找到对应的groupPosition和childPosition
*/
private Positions getPositionsByPosition(ExpandableListAdapter adapter,
int position) {
Positions result = new Positions();
if (position >= 0) {
int p = position + 1;
for (int group = 0; group < adapter.getGroupCount(); group++) {
// 减去组
if (p - 1 <= 0) {
result.groupPos = group;
result.childPos = -1;
break;
} else {
p = p - 1;
}
// 减去组成员
int childrenCount = isGroupExpanded(group) ? adapter
.getChildrenCount(group) : 0;
if (p - childrenCount <= 0) {
result.groupPos = group;
result.childPos = p - 1;
break;
} else {
p = p - childrenCount;
}
}
}
return result;
}

/**
* 用于保存group和child的position的容器类
*/
public class Positions {
private int groupPos = -1;
private int childPos = -1;

public boolean isGroup() {
return groupPos != -1 && childPos == -1;
}

public boolean isChild() {
return groupPos != -1 && childPos != -1;
}

@Override
public String toString() {
return "(" + groupPos + ", " + childPos + ")";
}
}


2.什么时候获得x和y呢,当然要在ExpandableListView2的onInterceptTouchEvent中了

onInterceptTouchEvent的作用是intercept touchEvent,就是让父布局来决定,是否截断touchEvent向下传递。

以我们这个问题来说,父布局是不需要截断事件的,我们只在里面记录事件的x和y就可以了,所以代码是这样的:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 取得group/child position
if (ev.getAction() == MotionEvent.ACTION_DOWN
&& getExpandableListAdapter() != null) {
int x = (int) ev.getX();
int y = (int) ev.getY();
// 位置保存下来
mLastPosition = pointToPosition(x, y);
mAdapter = (ExpandableListAdapter) getExpandableListAdapter();
mLastGroupAndChildPosition = getPositionsByPosition(mAdapter,
mLastPosition);
Log.d("UERY", "mLastPosition=" + mLastPosition + " (group,child)="
+ mLastGroupAndChildPosition);
}
return super.onInterceptTouchEvent(ev);
}
每对ExpandableListView2进行点击,一个Positions对象就会被记录下来(保存于mLastGroupAndChildPosition),这个Positions对象中的group/child position,将作为我们处理Item点击事件的依据!

3.真正的点击事件

/**
* 执行一次点击事件,position取上次ACTION_DOWN所点到的位置
*/
public void performLastClick() {
if (mLastPosition != -1 && mLastGroupAndChildPosition.isChild()) {
if (mOnChildClickListener != null) {
mOnChildClickListener.onChildClick(this,
getChildAt(mLastPosition),
mLastGroupAndChildPosition.groupPos,
mLastGroupAndChildPosition.childPos, 0); // TODO id is
// invalid
}
}
}
mOnChildClickListener就是我们通过ExpandableListView.setOnChildClickListener()设置进来的监听器。

/***************************************** 至此ExpandableListView2准备就绪,只欠东风 *****************************************/

4.东风在哪?谁来调用ExpandableListView2的performLastClick()? 当然是我们可滑动的View了!

滑动功能我们就不过多关注了,核心代码如下:

// 上次划动的X
private float mLastX;
// 本次划动开始的X
private float mStartX;
// 本次划动开始的时间
private long mStartTime;

@Override
public boolean onTouch(View v, MotionEvent event) {
// get touch event for upper layer
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
// 开始手动划动
mManualSliding = true;
mAutoSliding = false;
mLastX = event.getRawX();
mStartX = event.getRawX();
mStartTime = System.currentTimeMillis();
return true;
}

case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP: {
final int action = event.getAction();
// 完成划动
float endX = event.getRawX();
long costTime = System.currentTimeMillis() - mStartTime;
// 划动距离
int flingDistance = (int) (endX - mStartX);
// 划动速度
float velocityX = flingDistance / 0.001f / costTime;
mManualSliding = false;
finishSlide(action, flingDistance, costTime, velocityX);
return true;
}

case MotionEvent.ACTION_MOVE: {
if (mManualSliding) {
float nowRawX = event.getRawX();
float xDiff = nowRawX - mLastX;
mLastX = nowRawX;
LayoutParams lp = (LayoutParams) v.getLayoutParams();
int newRightMargin = (int) (lp.rightMargin - xDiff);
if (newRightMargin < 0) {
newRightMargin = 0;
} else if (newRightMargin > mMaxSlideDistance) {
newRightMargin = mMaxSlideDistance;
}
lp.setMargins((int) -newRightMargin, 0, (int) newRightMargin, 0);
v.setLayoutParams(lp);
}
return true;
}

default:
break;
}
return super.onTouchEvent(event);
}


其中,ACTION_DOWN时,记录点击事件的x,y,起始时间等;ACTION_MOVE时,滑动上层界面。

重点在于ACTION_UP/ACTION_CANCEL,在这儿,记录了滑动距离、速度和时间,交给了finishSlide()方法去处理,finishSlide()如下:

/**
* 处理划动动作事件完成
*
* @param startX
* @param endX
*/
private void finishSlide(int action, int flingDistance, long costTime, float velocityX) {
/*
* action System.out.println("action: " + action);
* System.out.println("flingDistance: " + flingDistance);
* System.out.println("velocityX: " + velocityX);
* System.out.println("costTime: " + costTime);
* System.out.println(" ");
*/
if (action == MotionEvent.ACTION_UP) {
if (Math.abs(flingDistance) <= mTouchSlop && costTime < DOUBLE_TAP_TIMEOUT) {
// 判定为单击
mParent.performLastClick();
} else if (Math.abs(velocityX) >= mMinimumFlingVelocity && Math.abs(velocityX) <= mMaximumFlingVelocity) {
// 判定为fling
if (velocityX < 0) {
autoSlide2Left();
} else {
autoSlide2Right();
}
}
}
// 手动拖动&其它情况
LayoutParams lp = (LayoutParams) mUpperLayer.getLayoutParams();
int rightMargin = lp.rightMargin;
if ((flingDistance < 0 && rightMargin >= mMaxSlideDistance / 3)
|| (flingDistance > 0 && rightMargin > mMaxSlideDistance / 3 * 2)) {
// 1.意图向左划,且已划出超过下层视图宽度1/3;
// 2.意图向右划,但未超出下层视图宽度1/3;
// 做左划处理
autoSlide2Left();
} else {
// 其它情况做右划处理
autoSlide2Right();
}
}


finishSlide()主要对三种情况做判断:单击、快速的滑动(fling)和其它情况。

单击我们调用父(ExpandableListView2)的performLastClick()。

滑动,我们根据方向和速度,决定是向左划还是向右划开上层界面。

其它情况下,我们根据上层界面距离哪边更近,让它自己完成划动。

其中一些临街值的确定(都是很科学的)

// 取得触摸事件判定临界值
final ViewConfiguration configuration = ViewConfiguration.get(getContext());
mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity();


至此,这问题算是得到了解决,大体总结一下:

我们定制了两个View :

1. ExpandableListView2,可以自己记录最后一次点击的groupPosition/childPosition,并提供一个点击功能。

2. 可滑动的ItemView,可以处理滑动、点击事件,如果判定为点击事件,则交给ExpandableListView2处理。

这种方法算是一种比较笨的方法,ExpandableListView2和ItemView之间耦合比较大,必须要配合使用,但也是无奈。

如果大家有更好更优雅的解决方案,不妨提出来共享,谢谢!

最后上一张效果图



相关代码下载链接:http://download.csdn.net/detail/ueryueryuery/7144855

没分的同学可以发邮件至ueryueryuery@163.com,说明需要哪份代码,LOL
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: