仿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 = new ArrayList<String>() {
        {
            for(int i=0;i<50;i++) {
                add("hello world, hello android  " + i);
            }
        }
    };  

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);  

        mListView = (QQListView) findViewById(R.id.list);
        mListView.setAdapter(new MyAdapter());
        mListView.setOnItemClickListener(new OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view,
                    int position, long id) {
                if(mListView.canClick()) {
                    Toast.makeText(MainActivity.this, mData.get(position), Toast.LENGTH_SHORT).show();
                }
            }
        });
    }  

    class MyAdapter extends BaseAdapter {  

        @Override
        public int getCount() {
            return mData.size();
        }  

        @Override
        public Object getItem(int position) {
            return mData.get(position);
        }  

        @Override
        public long getItemId(int position) {
            return position;
        }  

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            if(null == convertView) {
                convertView = View.inflate(MainActivity.this, R.layout.item, null);
            }
            TextView tv = (TextView) convertView.findViewById(R.id.tv);
            TextView delete = (TextView) convertView.findViewById(R.id.delete);  

            tv.setText(mData.get(position));  

            final int pos = position;
            delete.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    mData.remove(pos);
                    notifyDataSetChanged();
                    mListView.turnToNormal();
                }
            });  

            return convertView;
        }
    }
}  

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

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

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

时间: 2024-10-01 03:35:42

仿QQ侧滑删除ListView——2015第一博的相关文章

仿QQ侧滑删除

模仿QQ侧滑删除,需要的可以学习下 下载地址:http://www.devstore.cn/code/info/788.html 运行截图:

仿QQ侧滑菜单

仿QQ侧滑菜单 1.仿QQ侧滑(淡入淡出) 2.点击侧滑菜单相应地方响应事件 3.可以自定义侧滑菜单哦 下载地址:http://www.devstore.cn/code/info/846.html  运行截图:

自定义view系列(5)--99.99%实现QQ侧滑删除效果

首先声明本文是基于GitHub上"baoyongzhang"的SwipeMenuListView修改而来, 该项目地址:https://github.com/baoyongzhang/SwipeMenuListView 可以说这个侧滑删除效果是我见过效果最好且比较灵活的项目,没有之一!!! 但是在使用它之前需要给大家提两点注意事项: 1,该项目支持Gradle dependence,但是目前作者提供的依赖地址对应的项目不是最新的项目,依赖过后的代码与demo中使用的不一致,会提示没有B

安卓listView实现下拉刷新上拉加载滑动仿QQ的删除功能

大家对这些功能都是看的多了,然后对上拉刷新和下拉加载的原理都是非常清楚的,所以实现这功能其实也就是为了让大家能够从众多的同行们来进行比较学习而已,虽然即使是这样,但是面试的时候面试官还是会问你上拉和下拉是怎么实现的,滑动删除功能是怎么实现,其实要实现这些功能又不是唯一的方法,但是基本上思想都是一致的.然后gitup上的这些例子是非常的多,然后实现的也是大同小异但是也不能不让我们去球童存异.作为天朝的程序员即使是一个伸手党也不必太觉得羞耻,能把别人的东西来改一改或者沿用别人的思想来模仿也是不错的.

Android自定义View之仿QQ侧滑菜单实现

最近,由于正在做的一个应用中要用到侧滑菜单,所以通过查资料看视频,学习了一下自定义View,实现一个类似于QQ的侧滑菜单,顺便还将其封装为自定义组件,可以实现类似QQ的侧滑菜单和抽屉式侧滑菜单两种菜单. 下面先放上效果图: 我们这里的侧滑菜单主要是利用HorizontalScrollView来实现的,基本的思路是,一个布局中左边是菜单布局,右边是内容布局,默认情况下,菜单布局隐藏,内容布局显示,当我们向右侧滑,就会将菜单拉出来,而将内容布局的一部分隐藏,如下图所示: 下面我们就一步步开始实现一个

鹅厂系列一 : 仿QQ侧滑菜单

--不会的东西你不尝试的去做,你永远都不会做 好了,跟随潮流,还是先看下效果,不然可能都没人想看下去了(不会看到效果后不想看了吧O(∩_∩)O~) 额,图片资源来自QQ_374.APK,里面四五千个图片,找这几个没把我累死,当然感谢QQ的资源,额,.先来看看初始布局,我不知道腾讯是怎么布局的,我自己为了做的像他们一点,我的布局暂时是像下面这样的,到了自定义控件的时候,还会进行重新测量和布局. 嗯,就是让左面板在主面板的下面,所以我们自定义的控件SlideLayout继承FrameLayout.一

OC仿QQ侧滑

之前做侧滑用的控件的DDMenu,总感觉好像差了点什么,自己尝试写了一个,三层叠加,感觉效果不理想,偶然间看到了一篇博客,与大家分享,再次申明,该代码不是我写的,只是为了给自己留一个查找资料的机会 下载地址:https://github.com/WiitterSimithYU/CWLateralSlide 博客地址:https://mp.weixin.qq.com/s/N9zPQ4NweEHRh768wPk1qA 原文地址:https://www.cnblogs.com/hualuoshuiji

小程序仿QQ侧滑例子

缩放:wxml <!--page/one/index.wxml--> <view class="page"> <view class="page-bottom"> <view class="page-content"> <view class="wc"> <navigator url="../one/index" hover-class=&q

2015第一博,Oracle时间段查询,TO_DATE和TO_CHAR使用对比

写在前面: 在时间段查询的时候,在使用TO_DATE的时候,包括开始日期,不包括结束日期: 在使用TO_CHAR的时候可以包括开始日期,也可以包括结束日期.具体效果如图: 1. TO_DATE 使用,没有查询到结束日期. 2. TO_CHAR使用,查询到结束日期. 3.sql 对比 -- 包括开始日期,不包括结束日期 SELECT * FROM SYST_LOG WHERE ( OPERATE_TIME > TO_DATE ('2015-02-03', 'yyyy-mm-dd') AND OPE