首页 > 代码库 > 仿QQ侧滑删除ListView——2015第一博

仿QQ侧滑删除ListView——2015第一博

一直感觉QQ最近联系人那个侧滑删除功能挺高大上的,经过几经波折,终于在新的一年里实现了该功能。实现这个功能真是费了老劲了,好几次有了想法,兴奋的去写代码实现,结果让代码打了自己一个耳光,最终还是用margin的方式实现了这种效果,好吧, 先上效果!


技术分享


看完效果,就来说一下思路吧:


1、item的左右滑动效果我是用的magin实现的。


2、虽然item布局的时候文本TextView的宽度设置的是match_parent,但在点下去的时候就将这个值设置为了固定值:屏幕的宽度


3、通过提供一个方法来处理滑动和外部itemClick的冲突


主要代码:


public class QQListView extends ListView {
	private int mScreenWidth;	// 屏幕宽度
	private int mDownX;			// 按下点的x值
	private int mDownY;			// 按下点的y值
	private int mDeleteBtnWidth;// 删除按钮的宽度
	
	private boolean isDeleteShown;	// 删除按钮是否正在显示
	
	private ViewGroup mPointChild;	// 当前处理的item
	private LinearLayout.LayoutParams mLayoutParams;	// 当前处理的item的LayoutParams
	
	public QQListView(Context context, AttributeSet attrs) {
		this(context, attrs, 0);
	}

	public QQListView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		
		// 获取屏幕宽度
		WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
		DisplayMetrics dm = new DisplayMetrics();
		wm.getDefaultDisplay().getMetrics(dm);
		mScreenWidth = dm.widthPixels;
	}
	
	@Override
	public boolean onTouchEvent(MotionEvent ev) {
		switch (ev.getAction()) {
		case MotionEvent.ACTION_DOWN:
			performActionDown(ev);
			break;
		case MotionEvent.ACTION_MOVE:
			return performActionMove(ev);
		case MotionEvent.ACTION_UP:
			performActionUp();
			break;
		}
		
		return super.onTouchEvent(ev);
	}

	// 处理action_down事件
	private void performActionDown(MotionEvent ev) {
		if(isDeleteShown) {
			turnToNormal();
		}
		
		mDownX = (int) ev.getX();
		mDownY = (int) ev.getY();
		// 获取当前点的item
		mPointChild = (ViewGroup) getChildAt(pointToPosition(mDownX, mDownY)
				- getFirstVisiblePosition());
		// 获取删除按钮的宽度
		mDeleteBtnWidth = mPointChild.getChildAt(1).getLayoutParams().width;
		mLayoutParams = (LinearLayout.LayoutParams) mPointChild.getChildAt(0)
				.getLayoutParams();
		// 为什么要重新设置layout_width 等于屏幕宽度
		// 因为match_parent时,不管你怎么滑,都不会显示删除按钮
		// why? 因为match_parent时,ViewGroup就不去布局剩下的view
		mLayoutParams.width = mScreenWidth;
		mPointChild.getChildAt(0).setLayoutParams(mLayoutParams);
	}
	
	// 处理action_move事件
	private boolean performActionMove(MotionEvent ev) {
		int nowX = (int) ev.getX();
		int nowY = (int) ev.getY();
		if(Math.abs(nowX - mDownX) > Math.abs(nowY - mDownY)) {
			// 如果向左滑动
			if(nowX < mDownX) {
				// 计算要偏移的距离
				int scroll = (nowX - mDownX) / 2;
				// 如果大于了删除按钮的宽度, 则最大为删除按钮的宽度
				if(-scroll >= mDeleteBtnWidth) {
					scroll = -mDeleteBtnWidth;
				}
				// 重新设置leftMargin
				mLayoutParams.leftMargin = scroll;
				mPointChild.getChildAt(0).setLayoutParams(mLayoutParams);
			}
			
			return true;
		}
		return super.onTouchEvent(ev);
	}
	
	// 处理action_up事件
	private void performActionUp() {
		// 偏移量大于button的一半,则显示button
		// 否则恢复默认
		if(-mLayoutParams.leftMargin >= mDeleteBtnWidth / 2) {
			mLayoutParams.leftMargin = -mDeleteBtnWidth;
			isDeleteShown = true;
		}else {
			turnToNormal();
		}
		
		mPointChild.getChildAt(0).setLayoutParams(mLayoutParams);
	}

	/**
	 * 变为正常状态
	 */
	public void turnToNormal() {
		mLayoutParams.leftMargin = 0;
		mPointChild.getChildAt(0).setLayoutParams(mLayoutParams);
		isDeleteShown = false;
	}
	
	/**
	 * 当前是否可点击
	 * @return 是否可点击
	 */
	public boolean canClick() {
		return !isDeleteShown;
	}
}


很显然, 肯定要选择重写ListView来实现这种效果,并且重写onTouchEvent,通过判断move来达到侧滑效果。


先看看构造方法:


public QQListView(Context context, AttributeSet attrs, int defStyle) {  
    super(context, attrs, defStyle);  
          
    // 获取屏幕宽度  
    WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);  
    DisplayMetrics dm = new DisplayMetrics();  
    wm.getDefaultDisplay().getMetrics(dm);  
    mScreenWidth = dm.widthPixels;  
}  


在构造方法中就干了一件事:获取屏幕的宽度, 为什么要获取屏幕宽度呢? 上面已经说过了,这里要改变一下item里的第一个TextView的layout_width为固定值。 match_parent不是很好吗? 为什么要多次一举重新设置为屏幕宽度呢?答案是:当前一个View的layout_width为match_parent时,ViewGroup就不去理会剩下的View了,也就是删除的那个按钮根本没有绘制出来!


下面的onTouchEvent中,分别在ACTION_DOWN、ACTION_MOVE、ACTION_UP三个case中调用了三个方法来处理这三个事件。


首先看看在DOWN的时候做了什么。

// 处理action_down事件  
private void performActionDown(MotionEvent ev) {  
    if(isDeleteShown) {  
        turnToNormal();  
    }  
          
    mDownX = (int) ev.getX();  
    mDownY = (int) ev.getY();  
    // 获取当前点的item  
    mPointChild = (ViewGroup) getChildAt(pointToPosition(mDownX, mDownY)  
            - getFirstVisiblePosition());  
    // 获取删除按钮的宽度  
    mDeleteBtnWidth = mPointChild.getChildAt(1).getLayoutParams().width;  
    mLayoutParams = (LinearLayout.LayoutParams) mPointChild.getChildAt(0)  
            .getLayoutParams();  
    // 为什么要重新设置layout_width 等于屏幕宽度  
    // 因为match_parent时,不管你怎么滑,都不会显示删除按钮  
    // why? 因为match_parent时,ViewGroup就不去布局剩下的view  
    mLayoutParams.width = mScreenWidth;  
    mPointChild.getChildAt(0).setLayoutParams(mLayoutParams);  
} 


3~5行,如果某一个item的deleteButton正在显示,则调用turnToNormal方法去恢复现场,turnToNormal方法其实很简单,稍候说明一下。


7~11行的任务就是确定当前按下的点所在哪个item上,并获取这个item。这里需要注意的pointToPosition方法获取的是当前点在所有item中第几个,而getChildAt()获取的是距离第一个可见项的第几个,所以要减去getFirstVisiblePosition()才能得到正确的item。


13行,获取了deleteButton的宽度,这个宽度是在下面判断deleteButton是否要全部显示或隐藏用的。


最主要的14~25行,重新设置第一个TextView的layout_width,至于为什么,上面已经说过了。


处理move事件要稍微麻烦点。


// 处理action_move事件  
private boolean performActionMove(MotionEvent ev) {  
    int nowX = (int) ev.getX();  
    int nowY = (int) ev.getY();  
    if(Math.abs(nowX - mDownX) > Math.abs(nowY - mDownY)) {  
        // 如果向左滑动  
        if(nowX < mDownX) {  
            // 计算要偏移的距离  
            int scroll = (nowX - mDownX) / 2;  
            // 如果大于了删除按钮的宽度, 则最大为删除按钮的宽度  
            if(-scroll >= mDeleteBtnWidth) {  
                scroll = -mDeleteBtnWidth;  
            }  
            // 重新设置leftMargin  
            mLayoutParams.leftMargin = scroll;  
            mPointChild.getChildAt(0).setLayoutParams(mLayoutParams);  
        }  
              
        return true;  
    }  
    return super.onTouchEvent(ev);  
}  


performActionMove是有返回值的,而且我们在onTouchEvent中return了它的返回值,有返回值的目的就是让横向滑动的时候在我们的onTouchEvent中消费了事件,竖屏滑动的时候交由ListView的onTouchEvent处理事件,从而避免屏蔽掉了ListView的上下滑动机制。


3~4行,首先获取当前手指所在的点。


然后第5行去判断x轴方向的位移是否大于y轴方向的位移,如果不大于,move事件直接交由super处理。


再来看看if内部,接着又是一个if, 这里主要是判断是不是向左滑动,如果向左滑动, 在第9行计算需要的偏移量,这里取的是手指偏移量的一半。


10~13行主要是为了防止滑动过界,右边出现空白的情况。


15~16行,就是改变第一个TextView的leftMargin的值,从而达到向左移动的效果。


处理up就简单多了。


// 处理action_up事件  
private void performActionUp() {  
    // 偏移量大于button的一半,则显示button  
    // 否则恢复默认  
    if(-mLayoutParams.leftMargin >= mDeleteBtnWidth / 2) {  
        mLayoutParams.leftMargin = -mDeleteBtnWidth;  
        isDeleteShown = true;  
    }else {  
        turnToNormal();  
    }  
          
    mPointChild.getChildAt(0).setLayoutParams(mLayoutParams);  
}  


主要就是通过判断view的leftMargin来确定deletebutton是要进入显示状态还是不显示状态。


很多地方都用到了turnToNormal这个方法,那我们就来看看这个自定义的方法。

public void turnToNormal() {  
    mLayoutParams.leftMargin = 0;  
    mPointChild.getChildAt(0).setLayoutParams(mLayoutParams);  
    isDeleteShown = false;  
} 


这个自定义方法是一个public的,并不是我没有注意代码的封装性,而是这个方法在外部也会使用到, turnToNormal要做的事也很简单,就是“恢复现场”。


还一个简单的自定义方法,在外部需要itemClick的时候,通过该方法判断是否当前是否处于item可点击状态。

public boolean canClick() {  
    return !isDeleteShown;  
}  

好啦, 接下来是应用环节了,看两个布局文件,一个是main布局,一个是ListView的item布局


<RelativeLayout 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:context=".MainActivity" >  
      
    <org.loader.qqlist.QQListView  
        android:id="@+id/list"  
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content"  
        android:cacheColorHint="@android:color/transparent"  
        android:listSelector="@android:color/transparent"  
        android:divider="@android:color/darker_gray"  
        android:dividerHeight="2dp" />  
  
</RelativeLayout>  


这个没什么好说的了, 就是引用了自定义的ListView。


来看看item的布局

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    android:layout_width="match_parent"  
    android:layout_height="wrap_content"  
    android:orientation="horizontal" >  
  
    <TextView  
        android:id="@+id/tv"  
        android:layout_width="match_parent"  
        android:layout_height="wrap_content"  
        android:paddingBottom="20dp"  
        android:paddingLeft="10dp"  
        android:paddingTop="20dp"  
        android:background="@android:color/white"/>  
  
    <TextView  
        android:id="@+id/delete"  
        android:layout_width="80dp"  
        android:layout_height="match_parent"  
        android:background="#FFFF0000"  
        android:gravity="center"  
        android:paddingLeft="20dp"  
        android:textColor="@android:color/white"  
        android:paddingRight="20dp"  
        android:text="删除" />  
  
</LinearLayout> 


哎? 不是说第一个TextView的layout_width要动态设置成固定值:屏幕的宽度吗? 这里应该wrap_content也可以吧? 答案是不可以! 回想一下,去设置固定值是在处理到该item的事件时才干的活,那你从没有点击的item呢? 肯定还是需要match_parent的。


最后看看Activity

public class MainActivity extends Activity {  
    private QQListView mListView;  
    private ArrayList<String> mData = http://www.mamicode.com/new ArrayList() {  >


62行, 我们在deleteButton的onClick事件中调用了自定义方法turnToNormal,这样就保证了删除后,deleteButton不会继续存在下一个item上。


22~24行, 多了一个判断,通过canClick方法来判断当前item是否可点击。


最后是源码下载地址:http://git.oschina.net/qibin/horizontalScrollListView

仿QQ侧滑删除ListView——2015第一博