详解Android中实现ListView左右滑动删除条目的方法
作者:xiaanming
使用Scroller实现绚丽的ListView左右滑动删除Item效果
这里来给大家带来使用Scroller的小例子,同时也能用来帮助初步解除的读者更加熟悉的掌握Scroller的使用,掌握好了Scroller的使用我们就能实现很多滑动的效果。例如侧滑菜单,launcher,ListView的下拉刷新等等效果,我今天实现的是ListView的item的左右滑动删除item的效果,现在很多朋友看到这个效果应该是在Android的通知栏下拉中看到这个滑动删除的效果吧,我看到这个效果是在我之前的三星手机上左右滑动打电话发短信的效果,感觉很棒,不过现在很多手机联系人滑动都不是我之前那台手机的效果啦,网上很多朋友也写了关于滑动删除ListView的item的例子,有些是滑动手指离开之后然后给item加向左或者向右的移动动画,我觉得这样子的用户体验不是很好,所以今天自己也写了一个关于ListView左右滑动删除Item的小例子,ListView的item会随着手指在屏幕上的滑动而滑动,手指离开屏幕的时候item会根据判断向左或者向右划出屏幕,就是跟通知栏的效果差不多,接下来就带大家来实现这个效果。
先说下实现该效果的主要思路
先根据手指触摸的点来获取点击的是ListView的哪一个item
手指在屏幕中滑动我们利用scrollBy()来使该item跟随手指一起滑动
手指放开的时候,我们判断手指拖动的距离来判断item到底是滑出屏幕还是回到开始位置
主要思路就是上面这三步,接下来我们就用代码来实现吧,首先我们新建一个项目,叫SlideCutListView
根据需求我们需要自己自定义一个ListView来实现该功能,接下来先贴出代码再讲解具体的实现
package com.example.slidecutlistview; import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.WindowManager; import android.widget.AdapterView; import android.widget.ListView; import android.widget.Scroller; public class SlideCutListView extends ListView { /** * 当前滑动的ListView position */ private int slidePosition; /** * 手指按下X的坐标 */ private int downY; /** * 手指按下Y的坐标 */ private int downX; /** * 屏幕宽度 */ private int screenWidth; /** * ListView的item */ private View itemView; /** * 滑动类 */ private Scroller scroller; private static final int SNAP_VELOCITY = 600; /** * 速度追踪对象 */ private VelocityTracker velocityTracker; /** * 是否响应滑动,默认为不响应 */ private boolean isSlide = false; /** * 认为是用户滑动的最小距离 */ private int mTouchSlop; /** * 移除item后的回调接口 */ private RemoveListener mRemoveListener; /** * 用来指示item滑出屏幕的方向,向左或者向右,用一个枚举值来标记 */ private RemoveDirection removeDirection; // 滑动删除方向的枚举值 public enum RemoveDirection { RIGHT, LEFT; } public SlideCutListView(Context context) { this(context, null); } public SlideCutListView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public SlideCutListView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); screenWidth = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getWidth(); scroller = new Scroller(context); mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); } /** * 设置滑动删除的回调接口 * @param removeListener */ public void setRemoveListener(RemoveListener removeListener) { this.mRemoveListener = removeListener; } /** * 分发事件,主要做的是判断点击的是那个item, 以及通过postDelayed来设置响应左右滑动事件 */ @Override public boolean dispatchTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { addVelocityTracker(event); // 假如scroller滚动还没有结束,我们直接返回 if (!scroller.isFinished()) { return super.dispatchTouchEvent(event); } downX = (int) event.getX(); downY = (int) event.getY(); slidePosition = pointToPosition(downX, downY); // 无效的position, 不做任何处理 if (slidePosition == AdapterView.INVALID_POSITION) { return super.dispatchTouchEvent(event); } // 获取我们点击的item view itemView = getChildAt(slidePosition - getFirstVisiblePosition()); break; } case MotionEvent.ACTION_MOVE: { if (Math.abs(getScrollVelocity()) > SNAP_VELOCITY || (Math.abs(event.getX() - downX) > mTouchSlop && Math .abs(event.getY() - downY) < mTouchSlop)) { isSlide = true; } break; } case MotionEvent.ACTION_UP: recycleVelocityTracker(); break; } return super.dispatchTouchEvent(event); } /** * 往右滑动,getScrollX()返回的是左边缘的距离,就是以View左边缘为原点到开始滑动的距离,所以向右边滑动为负值 */ private void scrollRight() { removeDirection = RemoveDirection.RIGHT; final int delta = (screenWidth + itemView.getScrollX()); // 调用startScroll方法来设置一些滚动的参数,我们在computeScroll()方法中调用scrollTo来滚动item scroller.startScroll(itemView.getScrollX(), 0, -delta, 0, Math.abs(delta)); postInvalidate(); // 刷新itemView } /** * 向左滑动,根据上面我们知道向左滑动为正值 */ private void scrollLeft() { removeDirection = RemoveDirection.LEFT; final int delta = (screenWidth - itemView.getScrollX()); // 调用startScroll方法来设置一些滚动的参数,我们在computeScroll()方法中调用scrollTo来滚动item scroller.startScroll(itemView.getScrollX(), 0, delta, 0, Math.abs(delta)); postInvalidate(); // 刷新itemView } /** * 根据手指滚动itemView的距离来判断是滚动到开始位置还是向左或者向右滚动 */ private void scrollByDistanceX() { // 如果向左滚动的距离大于屏幕的二分之一,就让其删除 if (itemView.getScrollX() >= screenWidth / 2) { scrollLeft(); } else if (itemView.getScrollX() <= -screenWidth / 2) { scrollRight(); } else { // 滚回到原始位置,为了偷下懒这里是直接调用scrollTo滚动 itemView.scrollTo(0, 0); } } /** * 处理我们拖动ListView item的逻辑 */ @Override public boolean onTouchEvent(MotionEvent ev) { if (isSlide && slidePosition != AdapterView.INVALID_POSITION) { requestDisallowInterceptTouchEvent(true); addVelocityTracker(ev); final int action = ev.getAction(); int x = (int) ev.getX(); switch (action) { case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: MotionEvent cancelEvent = MotionEvent.obtain(ev); cancelEvent.setAction(MotionEvent.ACTION_CANCEL | (ev.getActionIndex()<< MotionEvent.ACTION_POINTER_INDEX_SHIFT)); onTouchEvent(cancelEvent); int deltaX = downX - x; downX = x; // 手指拖动itemView滚动, deltaX大于0向左滚动,小于0向右滚 itemView.scrollBy(deltaX, 0); return true; //拖动的时候ListView不滚动 case MotionEvent.ACTION_UP: int velocityX = getScrollVelocity(); if (velocityX > SNAP_VELOCITY) { scrollRight(); } else if (velocityX < -SNAP_VELOCITY) { scrollLeft(); } else { scrollByDistanceX(); } recycleVelocityTracker(); // 手指离开的时候就不响应左右滚动 isSlide = false; break; } } //否则直接交给ListView来处理onTouchEvent事件 return super.onTouchEvent(ev); } @Override public void computeScroll() { // 调用startScroll的时候scroller.computeScrollOffset()返回true, if (scroller.computeScrollOffset()) { // 让ListView item根据当前的滚动偏移量进行滚动 itemView.scrollTo(scroller.getCurrX(), scroller.getCurrY()); postInvalidate(); // 滚动动画结束的时候调用回调接口 if (scroller.isFinished()) { if (mRemoveListener == null) { throw new NullPointerException("RemoveListener is null, we should called setRemoveListener()"); } itemView.scrollTo(0, 0); mRemoveListener.removeItem(removeDirection, slidePosition); } } } /** * 添加用户的速度跟踪器 * * @param event */ private void addVelocityTracker(MotionEvent event) { if (velocityTracker == null) { velocityTracker = VelocityTracker.obtain(); } velocityTracker.addMovement(event); } /** * 移除用户速度跟踪器 */ private void recycleVelocityTracker() { if (velocityTracker != null) { velocityTracker.recycle(); velocityTracker = null; } } /** * 获取X方向的滑动速度,大于0向右滑动,反之向左 * * @return */ private int getScrollVelocity() { velocityTracker.computeCurrentVelocity(1000); int velocity = (int) velocityTracker.getXVelocity(); return velocity; } /** * * 当ListView item滑出屏幕,回调这个接口 * 我们需要在回调方法removeItem()中移除该Item,然后刷新ListView * * @author xiaanming * */ public interface RemoveListener { public void removeItem(RemoveDirection direction, int position); } }
首先我们重写dispatchTouchEvent()方法,该方法是事件的分发方法,我们在里面只做了一些简单的步骤,我们按下屏幕的时候,如果某个item正在进行滚动,我们直接交给SlideCutListView的父类处理分发事件,否则根据点击的X,Y坐标利用pointToPosition(int x, int y)来获取点击的是ListView的哪一个position,从而获取到我们需要滑动的item的View,我们还在该方法加入了滑动速度的检测,并且在ACTION_MOVE的时候来判断是否响应item的左右移动,用isSlide来记录是否响应左右滑动
然后就是重写onTouchEvent()方法,我们先判断isSlide为true,并且我们点击的是ListView上面的有效的position,否则直接交给SlideCutListView的父类也就是ListView来处理,在ACTION_MOVE中调用scrollBy()来移动item,scrollBy()是相对item的上一个位置进行移动的,所以我们每次都要用现在移动的距离减去上一个位置的距离然后赋给scrollBy()方法,这样子我们就实现了item的平滑移动,当我们将手指抬起的时候,我们先根据手指滑动的速度来确定是item是滑出屏幕还是滑动至原始位置,如果向右的速度大于我们设置的SNAP_VELOCITY,item就调用scrollRight()方法滚出屏幕,如果向左的速度小于-SNAP_VELOCITY,则调用scrollLeft()向左滚出屏幕,如果我们是缓慢的移动item,则调用scrollByDistanceX()方法来判断是滚到那个位置
在scrollRight()和scrollLeft()方法中我们使用Scroller类的startScroll()方法先设置滚动的参数,然后调用postInvalidate()来刷新界面,界面刷新就会调用computeScroll()方法,我们在里面处理滚动逻辑就行了,值得一提的是computeScroll()里面的这段代码
itemView.scrollTo(0, 0);
我们需要将该item滚动在(0, 0 )这个点,因为我们只是将ListView的Item滚动出屏幕而已,并没有将该item移除,而且我们不能手动调用removeView()来从ListView中移除该item,我们只能通过改变ListView的数据,然后通过notifyDataSetChanged()来刷新ListView,所以我们需要将其滚动至(0, 0),这里比较关键。
定义好了我们左右滑动的ListView,接下来就来使用它,布局很简单,一个RelativeLayout包裹我们自定义的ListView
<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" android:background="@android:color/darker_gray"> <com.example.slidecutlistview.SlideCutListView android:id="@+id/slideCutListView" android:layout_width="match_parent" android:layout_height="match_parent" android:listSelector="@android:color/transparent" android:divider="@drawable/reader_item_divider" android:cacheColorHint="@android:color/transparent"> </com.example.slidecutlistview.SlideCutListView> </RelativeLayout>
接下来我们来看看ListView的item的布局
<?xml version="1.0" encoding="UTF-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="wrap_content" > <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:background="@drawable/friendactivity_comment_detail_list2" > <TextView android:id="@+id/list_item" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="15dip" /> </LinearLayout> </LinearLayout>
还记得我在上一篇文章中提到过调用scrollTo()方法是对里面的子View进行滚动的,而不是对整个布局进行滚动的,所以我们用LinearLayout来套住我们的item的布局,这点需要注意一下,不然滚动的只是TextView。
主页面MainActivity里面的代码比较简单,里面使用的也是ArrayAdapter,相信大家都能看懂
package com.example.slidecutlistview; import java.util.ArrayList; import java.util.List; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.ArrayAdapter; import android.widget.Toast; import com.example.slidecutlistview.SlideCutListView.RemoveDirection; import com.example.slidecutlistview.SlideCutListView.RemoveListener; public class MainActivity extends Activity implements RemoveListener{ private SlideCutListView slideCutListView ; private ArrayAdapter<String> adapter; private List<String> dataSourceList = new ArrayList<String>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); init(); } private void init() { slideCutListView = (SlideCutListView) findViewById(R.id.slideCutListView); slideCutListView.setRemoveListener(this); for(int i=0; i<20; i++){ dataSourceList.add("滑动删除" + i); } adapter = new ArrayAdapter<String>(this, R.layout.listview_item, R.id.list_item, dataSourceList); slideCutListView.setAdapter(adapter); slideCutListView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Toast.makeText(MainActivity.this, dataSourceList.get(position), Toast.LENGTH_SHORT).show(); } }); } //滑动删除之后的回调方法 @Override public void removeItem(RemoveDirection direction, int position) { adapter.remove(adapter.getItem(position)); switch (direction) { case RIGHT: Toast.makeText(this, "向右删除 "+ position, Toast.LENGTH_SHORT).show(); break; case LEFT: Toast.makeText(this, "向左删除 "+ position, Toast.LENGTH_SHORT).show(); break; default: break; } } }
这里面需要对SlideCutListView设置RemoveListener,然后我们在回调方法removeItem(RemoveDirection direction, int position)中删除该position的数据,在调用notifyDataSetChanged()刷新ListView,我这里用的是ArrayAdatper,直接调用remove()就可以了。
所有的代码就编写完了,我们来运行下程序看看效果吧
使用NineOldAndroids实现绚丽的ListView左右滑动删除Item效果
再给大家来一个ListView左右滑动删除Item效果的例子,上面使用的是滑动类Scroller来实现的,但是看了下通知栏的左右滑动删除效果,确实很棒,当我们滑动Item超过一半的时候,item的透明度就变成了0,我们就知道抬起手指的时候item就被删除了,当item的透明度不为0的时候,我们抬起手指Item会回到起始位置,这样我们就知道拖动到什么位置item会删除,什么位置Item不删除,用户体验更好了,还有一个效果,就是我们滑动删除了item的时候,ListView的其他item会出现向上或者向下滚动的效果,感觉效果很棒,所以在GitHub上面搜索了下,发现很多开源库都有这个效果,比如ListViewAnimations, android-swipelistview等等,我看了下实现原理,使用的是Jake Wharton的动画开源库NineOldAndroids,这个库究竟是干嘛的呢?在API3.0(Honeycomb), SDK新增了一个android.animation包,里面的类是实现动画效果相关的类,通过Honeycomb API,能够实现非常复杂的动画效果,但是如果开发者想在3.0以下使用这一套API, 则需要使用开源框架Nine Old Androids,在这个库中会根据我们运行的机器判断其SDK版本,如果是API3.0以上则使用Android自带的动画类,否则就使用Nine Old Androids库中,这是一个兼容库,接下来我们就来看看这个效果的具体实现吧
实现该效果的主要思路
先根据手指触摸的点来获取点击的是ListView的哪一个Item
当手指在屏幕上面滑动的时候,我们要使得Item跟随手指的滑动而滑动
当我们抬起手指的时候,我们根据滑动的距离或者手指在屏幕上面的速度来判断Item是滑出屏幕还是滑动至其实位置
Item滑出屏幕时,使ListView的其他item产生向上挤压或者向下挤压的效果
大致的思路这是这四步,其中的一些细节接下来我会一一为大家解答的,接下来我们就用代码来实现这种效果吧
首先我们新建一个工程,叫Swipedismisslistview,我们需要将Nine Old Androids这个库引入到工程,大家可以去https://github.com/JakeWharton/NineOldAndroids下载,可以使用Jar包,也可以使用工程库的形式引入到我们自己的工程,我们还需要自定义一个ListView,我们先看代码然后给大家讲解下具体的功能实现
package com.example.swipedismisslistview; import static com.nineoldandroids.view.ViewHelper.setAlpha; import static com.nineoldandroids.view.ViewHelper.setTranslationX; import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ListView; import com.nineoldandroids.animation.Animator; import com.nineoldandroids.animation.AnimatorListenerAdapter; import com.nineoldandroids.animation.ValueAnimator; import com.nineoldandroids.view.ViewHelper; import com.nineoldandroids.view.ViewPropertyAnimator; public class SwipeDismissListView extends ListView { /** * 认为是用户滑动的最小距离 */ private int mSlop; /** * 滑动的最小速度 */ private int mMinFlingVelocity; /** * 滑动的最大速度 */ private int mMaxFlingVelocity; /** * 执行动画的时间 */ protected long mAnimationTime = 150; /** * 用来标记用户是否正在滑动中 */ private boolean mSwiping; /** * 滑动速度检测类 */ private VelocityTracker mVelocityTracker; /** * 手指按下的position */ private int mDownPosition; /** * 按下的item对应的View */ private View mDownView; private float mDownX; private float mDownY; /** * item的宽度 */ private int mViewWidth; /** * 当ListView的Item滑出界面回调的接口 */ private OnDismissCallback onDismissCallback; /** * 设置动画时间 * * @param mAnimationTime */ public void setmAnimationTime(long mAnimationTime) { this.mAnimationTime = mAnimationTime; } /** * 设置删除回调接口 * * @param onDismissCallback */ public void setOnDismissCallback(OnDismissCallback onDismissCallback) { this.onDismissCallback = onDismissCallback; } public SwipeDismissListView(Context context) { this(context, null); } public SwipeDismissListView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public SwipeDismissListView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); ViewConfiguration vc = ViewConfiguration.get(context); mSlop = vc.getScaledTouchSlop(); mMinFlingVelocity = vc.getScaledMinimumFlingVelocity() * 8; //获取滑动的最小速度 mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity(); //获取滑动的最大速度 } @Override public boolean onTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: handleActionDown(ev); break; case MotionEvent.ACTION_MOVE: return handleActionMove(ev); case MotionEvent.ACTION_UP: handleActionUp(ev); break; } return super.onTouchEvent(ev); } /** * 按下事件处理 * * @param ev * @return */ private void handleActionDown(MotionEvent ev) { mDownX = ev.getX(); mDownY = ev.getY(); mDownPosition = pointToPosition((int) mDownX, (int) mDownY); if (mDownPosition == AdapterView.INVALID_POSITION) { return; } mDownView = getChildAt(mDownPosition - getFirstVisiblePosition()); if (mDownView != null) { mViewWidth = mDownView.getWidth(); } //加入速度检测 mVelocityTracker = VelocityTracker.obtain(); mVelocityTracker.addMovement(ev); } /** * 处理手指滑动的方法 * * @param ev * @return */ private boolean handleActionMove(MotionEvent ev) { if (mVelocityTracker == null || mDownView == null) { return super.onTouchEvent(ev); } // 获取X方向滑动的距离 float deltaX = ev.getX() - mDownX; float deltaY = ev.getY() - mDownY; // X方向滑动的距离大于mSlop并且Y方向滑动的距离小于mSlop,表示可以滑动 if (Math.abs(deltaX) > mSlop && Math.abs(deltaY) < mSlop) { mSwiping = true; //当手指滑动item,取消item的点击事件,不然我们滑动Item也伴随着item点击事件的发生 MotionEvent cancelEvent = MotionEvent.obtain(ev); cancelEvent.setAction(MotionEvent.ACTION_CANCEL | (ev.getActionIndex()<< MotionEvent.ACTION_POINTER_INDEX_SHIFT)); onTouchEvent(cancelEvent); } if (mSwiping) { // 跟谁手指移动item ViewHelper.setTranslationX(mDownView, deltaX); // 透明度渐变 ViewHelper.setAlpha(mDownView, Math.max(0f, Math.min(1f, 1f - 2f * Math.abs(deltaX)/ mViewWidth))); // 手指滑动的时候,返回true,表示SwipeDismissListView自己处理onTouchEvent,其他的就交给父类来处理 return true; } return super.onTouchEvent(ev); } /** * 手指抬起的事件处理 * @param ev */ private void handleActionUp(MotionEvent ev) { if (mVelocityTracker == null || mDownView == null|| !mSwiping) { return; } float deltaX = ev.getX() - mDownX; //通过滑动的距离计算出X,Y方向的速度 mVelocityTracker.computeCurrentVelocity(1000); float velocityX = Math.abs(mVelocityTracker.getXVelocity()); float velocityY = Math.abs(mVelocityTracker.getYVelocity()); boolean dismiss = false; //item是否要滑出屏幕 boolean dismissRight = false;//是否往右边删除 //当拖动item的距离大于item的一半,item滑出屏幕 if (Math.abs(deltaX) > mViewWidth / 2) { dismiss = true; dismissRight = deltaX > 0; //手指在屏幕滑动的速度在某个范围内,也使得item滑出屏幕 } else if (mMinFlingVelocity <= velocityX && velocityX <= mMaxFlingVelocity && velocityY < velocityX) { dismiss = true; dismissRight = mVelocityTracker.getXVelocity() > 0; } if (dismiss) { ViewPropertyAnimator.animate(mDownView) .translationX(dismissRight ? mViewWidth : -mViewWidth)//X轴方向的移动距离 .alpha(0) .setDuration(mAnimationTime) .setListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { //Item滑出界面之后执行删除 performDismiss(mDownView, mDownPosition); } }); } else { //将item滑动至开始位置 ViewPropertyAnimator.animate(mDownView) .translationX(0) .alpha(1) .setDuration(mAnimationTime).setListener(null); } //移除速度检测 if(mVelocityTracker != null){ mVelocityTracker.recycle(); mVelocityTracker = null; } mSwiping = false; } /** * 在此方法中执行item删除之后,其他的item向上或者向下滚动的动画,并且将position回调到方法onDismiss()中 * @param dismissView * @param dismissPosition */ private void performDismiss(final View dismissView, final int dismissPosition) { final ViewGroup.LayoutParams lp = dismissView.getLayoutParams();//获取item的布局参数 final int originalHeight = dismissView.getHeight();//item的高度 ValueAnimator animator = ValueAnimator.ofInt(originalHeight, 0).setDuration(mAnimationTime); animator.start(); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (onDismissCallback != null) { onDismissCallback.onDismiss(dismissPosition); } //这段代码很重要,因为我们并没有将item从ListView中移除,而是将item的高度设置为0 //所以我们在动画执行完毕之后将item设置回来 ViewHelper.setAlpha(dismissView, 1f); ViewHelper.setTranslationX(dismissView, 0); ViewGroup.LayoutParams lp = dismissView.getLayoutParams(); lp.height = originalHeight; dismissView.setLayoutParams(lp); } }); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { //这段代码的效果是ListView删除某item之后,其他的item向上滑动的效果 lp.height = (Integer) valueAnimator.getAnimatedValue(); dismissView.setLayoutParams(lp); } }); } /** * 删除的回调接口 * * @author xiaanming * */ public interface OnDismissCallback { public void onDismiss(int dismissPosition); } }
看过Android 使用Scroller实现绚丽的ListView左右滑动删除Item效果你会发现,这个自定义的SwipeDismissListView只重写了onTouchEvent()方法,其实我们重写这一个方法就能实现我们需要的效果
1. 我们先看手指按下屏幕的处理方法handleActionDown();该方法里面根据我们手指按下的点根据pointToPosition()方法来获取我们点击的position,然后利用getChildAt()来获取我们按下的item的View对象,并且加入手指在屏幕滑动的速度检查,这一步相对来说还是比较简单
2. 接下来就是手指在屏幕上面滑动的处理方法handleActionMove(),这个方法就稍微的复杂些,我们需要根据手指在X轴的滑动距离和Y轴的滑动距离来判断是ListView item的水平滑动还是ListView的上下滑动,当满足Math.abs(deltaX) > mSlop && Math.abs(deltaY) < mSlop这个条件时候,我们用一个布尔值mSwiping来标记Item现在处于水平滑动的状态,这时候我们需要处理Item跟随手指的滑动而滑动的逻辑,我们使用ViewHelper来处理Item的滑动逻辑,这个类会根据机器的SDK版本来判断使用Android系统的API还是NineOldandroids中自己实现的API使得View滑动的效果,NineOldandroids中主要使用Camera(可以实现各种复杂动画效果的类),我们直接使用ViewHelper的setTranslationX()和setAlpha()就实现了item滑动和透明度渐变的效果,为了使得我们在滑动item的时候,ListView不上下滚动,我们必须返回true来屏蔽ListView的上下滚动,这里需要我们要非常熟悉Android的事件分发机制,这里我就不说明了,大家不了解的去网上找找相关的文章看看
还有一个问题,就是当我们滑动ListView的item的时候,会伴随着item的点击事件,这不是我们想要的效果,所以当Item滑动的时候我们需要取消ListView Item的点击事件
3. 在看手指抬起的时候的处理方法handleActionUp(),这里面需要根据手指的滑动速度或者Item移动的距离来判断Item是滑出屏幕还是滑动至起始位置,并且要判断item向左还是向右滑出屏幕等等逻辑,具体的逻辑可以看代码,相信大家都看得懂.
我这里要说说ViewPropertyAnimator类,这个类能更好的实现一个View同时进行多个动画的功能,当然我们也可以使用ObjectAnimator利用AnimatorSet来实现一个View上的多个同时进行的动画效果,例如我们可以将
ViewPropertyAnimator.animate(mDownView) .translationX(dismissRight ? mViewWidth : -mViewWidth)//X轴方向的移动距离 .alpha(0) .setDuration(mAnimationTime) .setListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { //Item滑出界面之后执行删除 performDismiss(mDownView, mDownPosition); } });
替换成
AnimatorSet set = new AnimatorSet(); set.playTogether(ObjectAnimator.ofFloat(mDownView, "translationX", dismissRight ? mViewWidth : -mViewWidth), ObjectAnimator.ofFloat(mDownView, "alpha", 0)); set.setDuration(mAnimationTime).start(); set.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { //Item滑出界面之后执行删除 performDismiss(mDownView, mDownPosition); } });
在效果上面是一样的,但是ViewPropertyAnimator在性能上要比使用ObjectAnimator来实现多个同时进行的动画要高的多,举个例子,假如要对View使用移动和透明度的动画,使用ViewPropertyAnimator的话,某个时间点上我们只需要调用一次invalidate()方法刷新界面就行了,而使用ObjectAnimator的话,移动的动画需要调用invalidate(),透明度的动画也需要调用invalidate()方法,在性能上使用AnimationSet比ViewPropertyAnimator要低,但是有的时候我们还是需要使用ObjectAnimator,比如,在某个时间内,我们需要将View先变大在变小在变大等复杂情况,这时候ObjectAnimator就派上用场了,例如
ObjectAnimator.ofInt(mDownView, "scaleX", 0 ,100 ,0, 100).setDuration(100).start()
通过上面的几步我们就实现了ListView的左右滑动删除item的效果啦,但是还有一个效果,item删除之后,ListView的其他item向上或者向下缓缓滑动的效果,实现这个也很容易,就是动态设置item的高度,item高度逐渐变小,这样其他的item就会出现向上或者向下挤压的效果啦!
4. 这里我们使用的是ValueAnimator这个类,这个类并不是针对View作用的动画,而是对某个值作用的动画,他默认使用的Interpolator(插补器)是AccelerateDecelerateInterpolator(开始和结束的时候慢,中间快) , 举个很简单的例子,我们在10秒内使用ValueAnimator将某个值从0变化到100,如果使用LinearInterpolator(线性插补器,匀速变化)在第2秒的时候,这个值变成了20,而是用AccelerateDecelerateInterpolator,可能在第二秒的时候这个值为15或者13,所以我们在ValueAnimator变化的时候设置值动画变化的监听器AnimatorUpdateListener就知道某个时间这个值变成了多少,从而对View的某个属性进行设置(例如大小),所以ValueAnimator是间接的对View设置动画的
了解了ValueAnimator的使用原理,我们就可以现实上面的动画效果了,我们使用ValueAnimator将item的高度变成0,设置ValueAnimator变化的监听,我们在回调函数onAnimationUpdate()中动态的设置item的高度, 然后添加AnimatorListener监听动画的状态(例如动画开始,结束,重复等)监听,在动画结束的回调函数onAnimationEnd()中删除该item的数据,调用notifyDataSetChanged刷新ListView,看看下面这段代码
ViewHelper.setAlpha(dismissView, 1f); ViewHelper.setTranslationX(dismissView, 0); ViewGroup.LayoutParams lp = dismissView.getLayoutParams(); lp.height = originalHeight; dismissView.setLayoutParams(lp);
我们使用动画只是将item移动出了屏幕,并且将item的高度设置为了0,并没有将item的View从ListView中Remove掉,况且ListView也不能直接Remove掉Item的,只能将数据源删除,在调用notifyDataSetChanged()刷新,所以我们需要将刚刚滑出屏幕高度设置为0的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"> <com.example.swipedismisslistview.SwipeDismissListView android:id="@+id/swipeDismissListView" android:layout_width="match_parent" android:layout_height="match_parent" android:listSelector="@android:color/transparent" android:cacheColorHint="@android:color/transparent"> </com.example.swipedismisslistview.SwipeDismissListView> </RelativeLayout>
很简单,一个RelativeLayout包裹我们自定义的ListView控件,接下来就是主界面的代码编写,跟平常的ListView使用一样,但是我们需要设置OnDismissCallback()监听,在
onDismiss()中删除该位置对于的数据,刷新ListView
package com.example.swipedismisslistview; import java.util.ArrayList; import java.util.List; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.ArrayAdapter; import android.widget.Toast; import com.example.swipedismisslistview.SwipeDismissListView.OnDismissCallback; public class SwipeActivity extends Activity { private SwipeDismissListView swipeDismissListView; private ArrayAdapter<String> adapter; private List<String> dataSourceList = new ArrayList<String>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_swipe); init(); } private void init() { swipeDismissListView = (SwipeDismissListView) findViewById(R.id.swipeDismissListView); for (int i = 0; i < 20; i++) { dataSourceList.add("滑动删除" + i); } adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, android.R.id.text1, dataSourceList); swipeDismissListView.setAdapter(adapter); swipeDismissListView.setOnDismissCallback(new OnDismissCallback() { @Override public void onDismiss(int dismissPosition) { adapter.remove(adapter.getItem(dismissPosition)); } }); swipeDismissListView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Toast.makeText(SwipeActivity.this, adapter.getItem(position), Toast.LENGTH_SHORT).show(); } }); } }
所有的代码都已经编写完毕了,接下来就是运行工程,看看具体的效果是不是我们想要的