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

来源:互联网 发布:剑桥 知乎 编辑:程序博客网 时间:2024/05/21 09:10

一、背景

在做项目的时候,有一个需求,在两级列表中,实现类似于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;}@Overridepublic String toString() {return "(" + groupPos + ", " + childPos + ")";}}

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

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

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

@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {// 取得group/child positionif (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了!

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

// 上次划动的Xprivate float mLastX;// 本次划动开始的Xprivate float mStartX;// 本次划动开始的时间private long mStartTime;@Overridepublic boolean onTouch(View v, MotionEvent event) {// get touch event for upper layerswitch (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) {// 判定为flingif (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

0 1
原创粉丝点击