一般情况下,如果想要在ListView上面实现Listitem的滑动删除效果,或者仿QQ的滑动显示删除效果的时候,只需要继承ListView,自定义一个ListView就可以,不过由于之前用了开源库StickyListHeaders来实现ListView的分组,那么这一次在实现这个仿QQ的左右滑动显示隐藏删除按钮的时候,就要在StickyListHeaders的基础上实现。而StickyListHeaders要开放了一个setOnTouchListener的接口,允许调用者传进去一个OnTouchListener,来实现自定义的OnTouch效果,如下:
@Override public void setOnTouchListener(final OnTouchListener l) { if (l != null) { mList.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { return l.onTouch(StickyListHeadersListView.this, event); } }); } else { mList.setOnTouchListener(null); } }
那么解决问题的方法就在于实现一个OnTouchListener,然后将其传给StickyListHeaderListView。
lvTasks.setOnTouchListener(new ListitemSlopListener(this));
而ListitemSlopListener就是我们自定义的OnTouchListener了,其代码如下:
public class ListitemSlopListener implements OnTouchListener {
...
private void initConfiguration() {
ViewConfiguration vc = ViewConfiguration.get(mContext);
mSlop = vc.getScaledTouchSlop();
mMinFling = vc.getScaledMaximumFlingVelocity();
mMaxFling = mMinFling * 8;
}
...
@Override
public boolean onTouch(View v, MotionEvent event) {
mListView = ((StickyListHeadersListView) v).getWrappedList();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = event.getX();
mDownY = event.getY();
mPosition = mListView.pointToPosition((int)mDownX, (int)mDownY);
mSlopView = mListView.getChildAt(mPosition - mListView.getFirstVisiblePosition());
Log.v(Helper.TAG,"Action Down : onTouch Triggered");
if(mSlopView != null){
btnDelete = (Button) mSlopView.findViewById(R.id.btnDelete);
if(btnDelete.getVisibility() == View.INVISIBLE || btnDelete.getVisibility() == View.GONE){
btnDelete.setVisibility(View.VISIBLE);
ViewHelper.setAlpha(btnDelete, 0);
}
mBtnWidth = btnDelete.getWidth();
initVelocityTrackerIfNotExists(event);
return true;
}
break;
case MotionEvent.ACTION_MOVE:
if(mVelocityTracker != null && mSlopView != null && btnDelete != null){
float dx = event.getX() - mDownX;
float dy = event.getY() - mDownY;
if(Math.abs(dx) > mSlop && Math.abs(dy) < mSlop){
mMoving = true;
}
if(mMoving){
if (dx > 0) {
//Direction : right to hide the button if the button shows
if(ViewHelper.getAlpha(btnDelete) > 0){
ViewHelper.setAlpha(
btnDelete,Math.min(1f,Math.max(0f, 1f - Math.abs(dx) /mBtnWidth)));
}
} else {
//Direction : Left to show the button if the button exists
ViewHelper.setAlpha(
btnDelete,
Math.max(0f,Math.min(1f, Math.abs(dx) / mBtnWidth)));
}
return true;
}else{
return false;
}
}
case MotionEvent.ACTION_UP:
if (mVelocityTracker != null && mSlopView != null && mMoving) {
float dx = event.getX() - mDownX;
mVelocityTracker.computeCurrentVelocity(1000);
float vx = Math.abs(mVelocityTracker.getXVelocity());
float vy = Math.abs(mVelocityTracker.getYVelocity());
boolean isShow = false;
if(Math.abs(dx) > mBtnWidth / 2){
isShow = dx < 0;
}else if(vx > mMinFling && vx < mMaxFling && vx > vy){
isShow = vx < 0;
}
if(isShow){
ViewPropertyAnimator.animate(btnDelete).alpha(1f).setDuration(DURATION);
}else{
ViewPropertyAnimator.animate(btnDelete).alpha(0f).setDuration(DURATION);
}
recycleVelocityTracker();
mMoving = false;
}else{
//Only When the Delete Button is not visible, the the onItemClick action was fired.
if(btnDelete != null && ViewHelper.getAlpha(btnDelete) > 0){
ViewPropertyAnimator.animate(btnDelete).alpha(0f).setDuration(DURATION);
}else{
TodoTask todoTask = (TodoTask) mListView.getItemAtPosition(mPosition);
if(todoTask != null){
mListView.performItemClick(mSlopView, mPosition, todoTask.getId());
}
}
}
return true;
}
return false;
}
}
我们知道,在屏幕上的任何事件,包括点击,长按,滑动,还是其它手势,其实都是由一系列的Touch事件组成的,而这些Touch事件其实是下面三步组成的:
0)一个ACTION_DOWN事件
1)N个ACTION_MOVE事件
2)一个ACTION_UP事件组成的。
而根据Android的事件分发机制,在触发View本身的OnTouchEvent方法的时候,如果存在一个OnTouchListener,那么View会首先调用OnTouchListener来响应事件,只有当OnTouchListener返回false,表明其不处理该事件的时候,才会继续将事件传递给View的OnTouchEvent,那么在这里,我们就要在OnTouchListener的onTouch方法中,将这个事件给完全消费掉,来实现我们自己想要的效果。
OnTouch方法中,主要实现了下面的功能:1)当我们手指接触到屏幕上的时候,一个Down事件就会被触发,于是会来到Listener的Down分支下面。
在这里,我们会首先利用ListView的pointToPosition方法,根据触摸的点坐标来获取ListView中item的位置,并利用getChildAt方法找出手指触摸到的那个Listitem,并将其放置到变量mSlopView中。
mSlopView其实就是TaskItem,其结构如下:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="48dp" android:background="@drawable/item_selector" android:padding="5dip" > <ImageView android:id="@+id/ivComplete" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_alignParentLeft="true" android:layout_centerVertical="true" android:contentDescription="@string/imageview_contentdesc" android:padding="5dp" /> <TextView android:id="@+id/tvTitle" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_centerVertical="true" android:layout_toRightOf="@+id/ivComplete" android:gravity="left|center_vertical" android:padding="5dp" android:singleLine="true" android:textSize="18sp" /> <Button android:id="@+id/btnDelete" android:layout_width="wrap_content" android:layout_height="32dp" android:layout_alignParentRight="true" android:layout_centerVertical="true" android:background="@drawable/shape_button" android:padding="5dp" android:text="@string/action_delete" android:textSize="12sp" android:visibility="invisible" /> </RelativeLayout>
删除按钮一开始是invisible的,当Down事件发生的时候,我们要将其置为Visible的,但同时要将其alpha值置为0,因为虽然可见,但此时还不能显示出来。
而最后,要在这里返回一个true,表明在这里,Down事件已经被这个OnTouchListener给消费了,这样,后续的Move事件, Up事件才会继续被这个OnTouchListener来处理。
2)当Donw事件被这个OnTouchListener消费后,后续的Move事件也会来到这里进行处理。
在Move分支中, 我们要根据移动的距离来判断这是不是一次移动,这是通过跟mSlop变量比较实现的。mSlop值是Android定义一个滑动事件的最短距离,当移动超过这个距离,表明这是一个滑动事件,那么这个时候,我们就要根据移动的距离去动态地改变删除按钮的alpha值,来显示或者隐藏删除按钮。在这里,定义
2.1)当手指向左移动的时候,则表明这是要显示按钮,那么会将按钮的alpha值由 0 向 1 变化,最终完全显示按钮。
2.2)当手指向右移动的时候,则表明是要删除按钮,那么会将按钮的alpha值由 1 向 0 变化,最终完全隐藏按钮。
最后,这个方法也要返回 true ,表明Move事件也在这里被消费了。
3)最后就是来到Up事件了。每一个Listitem的OnTouch事件其实分两种情况:
3.1)当OnTouch事件被定义为滑动事件时,即滑动距离超过mSlop的时候,我们要根据最终滑动的距离来决定是否要显示按钮。只有当滑动距离超过按钮的一半宽度的时候,才显示按钮,如果按钮还没有完全显示,则添加一个动画效果,来显示按钮。而如果没有超过按钮的一半宽度的时候,也要为该按钮添加一个动画效果,但是这个效果则是慢慢慢将按钮的alpha值变为0,最终隐藏按钮。
3.1)当滑动的距离小于mSlop的时候,该事件只被定义为一个OnItemClick事件,那么在这里要显示地调用performClick方法来触发ListView的OnItemClick事件,这是因为不想去改变开源库的源码,所以我们必须将所有的OnTouch事件(当然,只是我们想处理的)都在OnTouchListener中处理。
最后,在Up分支中,我们处理了事件之后,同样返回一个true,表明所有的OnTouch事件都在这里被处理了,不需要再将事件传递下去。
下面是实现后的效果图:
结束。