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

一、背景

在做项目的时候,有一个需求,在两级列表中,实现类似于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

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值