Android 使用NineOldAndroids实现绚丽的ListView左右滑动删除Item效果
转载请注明本文出自xiaanming的博客(http://blog.csdn.net/xiaanming/article/details/18311877),请尊重他人的辛勤劳动成果,谢谢!
今天还是给大家带来自定义控件的编写,自定义一个ListView的左右滑动删除Item的效果,这个效果之前已经实现过了,有兴趣的可以看下Android 使用Scroller实现绚丽的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,我们先看代码然后给大家讲解下具体的功能实现
1 package com.example.swipedismisslistview; 2 3 import static com.nineoldandroids.view.ViewHelper.setAlpha; 4 import static com.nineoldandroids.view.ViewHelper.setTranslationX; 5 import android.content.Context; 6 import android.util.AttributeSet; 7 import android.view.MotionEvent; 8 import android.view.VelocityTracker; 9 import android.view.View; 10 import android.view.ViewConfiguration; 11 import android.view.ViewGroup; 12 import android.widget.AdapterView; 13 import android.widget.ListView; 14 15 import com.nineoldandroids.animation.Animator; 16 import com.nineoldandroids.animation.AnimatorListenerAdapter; 17 import com.nineoldandroids.animation.ValueAnimator; 18 import com.nineoldandroids.view.ViewHelper; 19 import com.nineoldandroids.view.ViewPropertyAnimator; 20 /** 21 * @blog http://blog.csdn.net/xiaanming 22 * 23 * @author xiaanming 24 * 25 */ 26 public class SwipeDismissListView extends ListView { 27 /** 28 * 认为是用户滑动的最小距离 29 */ 30 private int mSlop; 31 /** 32 * 滑动的最小速度 33 */ 34 private int mMinFlingVelocity; 35 /** 36 * 滑动的最大速度 37 */ 38 private int mMaxFlingVelocity; 39 /** 40 * 执行动画的时间 41 */ 42 protected long mAnimationTime = 150; 43 /** 44 * 用来标记用户是否正在滑动中 45 */ 46 private boolean mSwiping; 47 /** 48 * 滑动速度检测类 49 */ 50 private VelocityTracker mVelocityTracker; 51 /** 52 * 手指按下的position 53 */ 54 private int mDownPosition; 55 /** 56 * 按下的item对应的View 57 */ 58 private View mDownView; 59 private float mDownX; 60 private float mDownY; 61 /** 62 * item的宽度 63 */ 64 private int mViewWidth; 65 /** 66 * 当ListView的Item滑出界面回调的接口 67 */ 68 private OnDismissCallback onDismissCallback; 69 70 /** 71 * 设置动画时间 72 * 73 * @param mAnimationTime 74 */ 75 public void setmAnimationTime(long mAnimationTime) { 76 this.mAnimationTime = mAnimationTime; 77 } 78 79 /** 80 * 设置删除回调接口 81 * 82 * @param onDismissCallback 83 */ 84 public void setOnDismissCallback(OnDismissCallback onDismissCallback) { 85 this.onDismissCallback = onDismissCallback; 86 } 87 88 public SwipeDismissListView(Context context) { 89 this(context, null); 90 } 91 92 public SwipeDismissListView(Context context, AttributeSet attrs) { 93 this(context, attrs, 0); 94 } 95 96 public SwipeDismissListView(Context context, AttributeSet attrs, 97 int defStyle) { 98 super(context, attrs, defStyle); 99 100 ViewConfiguration vc = ViewConfiguration.get(context); 101 mSlop = vc.getScaledTouchSlop(); 102 mMinFlingVelocity = vc.getScaledMinimumFlingVelocity() * 8; //获取滑动的最小速度 103 mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity(); //获取滑动的最大速度 104 } 105 106 107 @Override 108 public boolean onTouchEvent(MotionEvent ev) { 109 switch (ev.getAction()) { 110 case MotionEvent.ACTION_DOWN: 111 handleActionDown(ev); 112 break; 113 case MotionEvent.ACTION_MOVE: 114 return handleActionMove(ev); 115 case MotionEvent.ACTION_UP: 116 handleActionUp(ev); 117 break; 118 } 119 return super.onTouchEvent(ev); 120 } 121 122 /** 123 * 按下事件处理 124 * 125 * @param ev 126 * @return 127 */ 128 private void handleActionDown(MotionEvent ev) { 129 mDownX = ev.getX(); 130 mDownY = ev.getY(); 131 132 mDownPosition = pointToPosition((int) mDownX, (int) mDownY); 133 134 if (mDownPosition == AdapterView.INVALID_POSITION) { 135 return; 136 } 137 138 mDownView = getChildAt(mDownPosition - getFirstVisiblePosition()); 139 140 if (mDownView != null) { 141 mViewWidth = mDownView.getWidth(); 142 } 143 144 //加入速度检测 145 mVelocityTracker = VelocityTracker.obtain(); 146 mVelocityTracker.addMovement(ev); 147 } 148 149 150 /** 151 * 处理手指滑动的方法 152 * 153 * @param ev 154 * @return 155 */ 156 private boolean handleActionMove(MotionEvent ev) { 157 if (mVelocityTracker == null || mDownView == null) { 158 return super.onTouchEvent(ev); 159 } 160 161 // 获取X方向滑动的距离 162 float deltaX = ev.getX() - mDownX; 163 float deltaY = ev.getY() - mDownY; 164 165 // X方向滑动的距离大于mSlop并且Y方向滑动的距离小于mSlop,表示可以滑动 166 if (Math.abs(deltaX) > mSlop && Math.abs(deltaY) < mSlop) { 167 mSwiping = true; 168 169 //当手指滑动item,取消item的点击事件,不然我们滑动Item也伴随着item点击事件的发生 170 MotionEvent cancelEvent = MotionEvent.obtain(ev); 171 cancelEvent.setAction(MotionEvent.ACTION_CANCEL | 172 (ev.getActionIndex()<< MotionEvent.ACTION_POINTER_INDEX_SHIFT)); 173 onTouchEvent(cancelEvent); 174 } 175 176 if (mSwiping) { 177 // 跟谁手指移动item 178 ViewHelper.setTranslationX(mDownView, deltaX); 179 // 透明度渐变 180 ViewHelper.setAlpha(mDownView, Math.max(0f, Math.min(1f, 1f - 2f * Math.abs(deltaX)/ mViewWidth))); 181 182 // 手指滑动的时候,返回true,表示SwipeDismissListView自己处理onTouchEvent,其他的就交给父类来处理 183 return true; 184 } 185 186 return super.onTouchEvent(ev); 187 188 } 189 190 /** 191 * 手指抬起的事件处理 192 * @param ev 193 */ 194 private void handleActionUp(MotionEvent ev) { 195 if (mVelocityTracker == null || mDownView == null|| !mSwiping) { 196 return; 197 } 198 199 float deltaX = ev.getX() - mDownX; 200 201 //通过滑动的距离计算出X,Y方向的速度 202 mVelocityTracker.computeCurrentVelocity(1000); 203 float velocityX = Math.abs(mVelocityTracker.getXVelocity()); 204 float velocityY = Math.abs(mVelocityTracker.getYVelocity()); 205 206 boolean dismiss = false; //item是否要滑出屏幕 207 boolean dismissRight = false;//是否往右边删除 208 209 //当拖动item的距离大于item的一半,item滑出屏幕 210 if (Math.abs(deltaX) > mViewWidth / 2) { 211 dismiss = true; 212 dismissRight = deltaX > 0; 213 214 //手指在屏幕滑动的速度在某个范围内,也使得item滑出屏幕 215 } else if (mMinFlingVelocity <= velocityX 216 && velocityX <= mMaxFlingVelocity && velocityY < velocityX) { 217 dismiss = true; 218 dismissRight = mVelocityTracker.getXVelocity() > 0; 219 } 220 221 if (dismiss) { 222 ViewPropertyAnimator.animate(mDownView) 223 .translationX(dismissRight ? mViewWidth : -mViewWidth)//X轴方向的移动距离 224 .alpha(0) 225 .setDuration(mAnimationTime) 226 .setListener(new AnimatorListenerAdapter() { 227 @Override 228 public void onAnimationEnd(Animator animation) { 229 //Item滑出界面之后执行删除 230 performDismiss(mDownView, mDownPosition); 231 } 232 }); 233 } else { 234 //将item滑动至开始位置 235 ViewPropertyAnimator.animate(mDownView) 236 .translationX(0) 237 .alpha(1) 238 .setDuration(mAnimationTime).setListener(null); 239 } 240 241 //移除速度检测 242 if(mVelocityTracker != null){ 243 mVelocityTracker.recycle(); 244 mVelocityTracker = null; 245 } 246 247 mSwiping = false; 248 } 249 250 251 252 /** 253 * 在此方法中执行item删除之后,其他的item向上或者向下滚动的动画,并且将position回调到方法onDismiss()中 254 * @param dismissView 255 * @param dismissPosition 256 */ 257 private void performDismiss(final View dismissView, final int dismissPosition) { 258 final ViewGroup.LayoutParams lp = dismissView.getLayoutParams();//获取item的布局参数 259 final int originalHeight = dismissView.getHeight();//item的高度 260 261 ValueAnimator animator = ValueAnimator.ofInt(originalHeight, 0).setDuration(mAnimationTime); 262 animator.start(); 263 264 animator.addListener(new AnimatorListenerAdapter() { 265 @Override 266 public void onAnimationEnd(Animator animation) { 267 if (onDismissCallback != null) { 268 onDismissCallback.onDismiss(dismissPosition); 269 } 270 271 //这段代码很重要,因为我们并没有将item从ListView中移除,而是将item的高度设置为0 272 //所以我们在动画执行完毕之后将item设置回来 273 ViewHelper.setAlpha(dismissView, 1f); 274 ViewHelper.setTranslationX(dismissView, 0); 275 ViewGroup.LayoutParams lp = dismissView.getLayoutParams(); 276 lp.height = originalHeight; 277 dismissView.setLayoutParams(lp); 278 279 } 280 }); 281 282 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 283 @Override 284 public void onAnimationUpdate(ValueAnimator valueAnimator) { 285 //这段代码的效果是ListView删除某item之后,其他的item向上滑动的效果 286 lp.height = (Integer) valueAnimator.getAnimatedValue(); 287 dismissView.setLayoutParams(lp); 288 } 289 }); 290 291 } 292 293 /** 294 * 删除的回调接口 295 * 296 * @author xiaanming 297 * 298 */ 299 public interface OnDismissCallback { 300 public void onDismiss(int dismissPosition); 301 } 302 303 }
看过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上的多个同时进行的动画效果,例如我们可以将
1 ViewPropertyAnimator.animate(mDownView) 2 .translationX(dismissRight ? mViewWidth : -mViewWidth)//X轴方向的移动距离 3 .alpha(0) 4 .setDuration(mAnimationTime) 5 .setListener(new AnimatorListenerAdapter() { 6 @Override 7 public void onAnimationEnd(Animator animation) { 8 //Item滑出界面之后执行删除 9 performDismiss(mDownView, mDownPosition); 10 } 11 });
替换成
1 AnimatorSet set = new AnimatorSet(); 2 set.playTogether(ObjectAnimator.ofFloat(mDownView, "translationX", dismissRight ? mViewWidth : -mViewWidth), 3 ObjectAnimator.ofFloat(mDownView, "alpha", 0)); 4 set.setDuration(mAnimationTime).start(); 5 set.addListener(new AnimatorListenerAdapter() { 6 @Override 7 public void onAnimationEnd(Animator animation) { 8 //Item滑出界面之后执行删除 9 performDismiss(mDownView, mDownPosition); 10 } 11 });
在效果上面是一样的,但是ViewPropertyAnimator在性能上要比使用ObjectAnimator来实现多个同时进行的动画要高的多,举 个例子,假如要对View使用移动和透明度的动画,使用ViewPropertyAnimator的话,某个时间点上我们只需要调用一次 invalidate()方法刷新界面就行了,而使用ObjectAnimator的话,移动的动画需要调用invalidate(),透明度的动画也需 要调用invalidate()方法,在性能上使用AnimationSet比ViewPropertyAnimator要低,但是有的时候我们还是需要 使用ObjectAnimator,比如,在某个时间内,我们需要将View先变大在变小在变大等复杂情况,这时候ObjectAnimator就派上用 场了,例如
1 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,看看下面这段代码
1 ViewHelper.setAlpha(dismissView, 1f); 2 ViewHelper.setTranslationX(dismissView, 0); 3 ViewGroup.LayoutParams lp = dismissView.getLayoutParams(); 4 lp.height = originalHeight; 5 dismissView.setLayoutParams(lp);
我们使用动画只是将item移动出了屏幕,并且将item的高度设置为了0,并没有将item的View从ListView中Remove掉,况且 ListView也不能直接Remove掉Item的,只能将数据源删除,在调用notifyDataSetChanged()刷新,所以我们需要将刚刚 滑出屏幕高度设置为0的Item恢复回来
自定义控件的代码我们已经编写完了,接下来我们就要使用它了,先看界面的布局代码
1 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" 2 xmlns:tools="http://schemas.android.com/tools" 3 android:layout_width="match_parent" 4 android:layout_height="match_parent"> 5 6 <com.example.swipedismisslistview.SwipeDismissListView 7 android:id="@+id/swipeDismissListView" 8 android:layout_width="match_parent" 9 android:layout_height="match_parent" 10 android:listSelector="@android:color/transparent" 11 android:cacheColorHint="@android:color/transparent"> 12 </com.example.swipedismisslistview.SwipeDismissListView> 13 14 </RelativeLayout>
很简单,一个RelativeLayout包裹我们自定义的ListView控件,接下来就是主界面的代码编写,跟平常的ListView使用一样,但是我们需要设置OnDismissCallback()监听,在
onDismiss()中删除该位置对于的数据,刷新ListView
1 package com.example.swipedismisslistview; 2 3 import java.util.ArrayList; 4 import java.util.List; 5 6 import android.app.Activity; 7 import android.os.Bundle; 8 import android.view.View; 9 import android.widget.AdapterView; 10 import android.widget.AdapterView.OnItemClickListener; 11 import android.widget.ArrayAdapter; 12 import android.widget.Toast; 13 14 import com.example.swipedismisslistview.SwipeDismissListView.OnDismissCallback; 15 16 public class SwipeActivity extends Activity { 17 private SwipeDismissListView swipeDismissListView; 18 private ArrayAdapter<String> adapter; 19 private List<String> dataSourceList = new ArrayList<String>(); 20 21 @Override 22 protected void onCreate(Bundle savedInstanceState) { 23 super.onCreate(savedInstanceState); 24 setContentView(R.layout.activity_swipe); 25 init(); 26 } 27 28 private void init() { 29 swipeDismissListView = (SwipeDismissListView) findViewById(R.id.swipeDismissListView); 30 for (int i = 0; i < 20; i++) { 31 dataSourceList.add("滑动删除" + i); 32 } 33 34 adapter = new ArrayAdapter<String>(this, 35 android.R.layout.simple_list_item_1, 36 android.R.id.text1, dataSourceList); 37 38 swipeDismissListView.setAdapter(adapter); 39 40 swipeDismissListView.setOnDismissCallback(new OnDismissCallback() { 41 42 @Override 43 public void onDismiss(int dismissPosition) { 44 adapter.remove(adapter.getItem(dismissPosition)); 45 } 46 }); 47 48 49 swipeDismissListView.setOnItemClickListener(new OnItemClickListener() { 50 51 @Override 52 public void onItemClick(AdapterView<?> parent, View view, 53 int position, long id) { 54 Toast.makeText(SwipeActivity.this, adapter.getItem(position), Toast.LENGTH_SHORT).show(); 55 } 56 }); 57 58 } 59 60 }
所有的代码都已经编写完毕了,接下来就是运行工程,看看具体的效果是不是我们想要的
好 了,今天的讲解到这里结束了,有了NineOldAndroids我们可以在2.x的手机上面实现许多复杂的动画效果,文章也介绍了关于开源库 NineOldAndroids使用的一些知识,文章有点长,希望读者还是先将文章看下,然后自己看看能不能自己实现出来,有什么不明白的地方请在下面留 言,我会为大家解答的!