转载请注明本文出自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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
|
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;
/** * @blog http://blog.csdn.net/xiaanming
*
* @author xiaanming
*
*/
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上的多个同时进行的动画效果,例如我们可以将
1
2
3
4
5
6
7
8
9
10
11
|
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);
}
});
|
替换成
1
2
3
4
5
6
7
8
9
10
11
|
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就派上用场了,例如
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
2
3
4
5
|
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(); } }); } }
所有的代码都已经编写完毕了,接下来就是运行工程,看看具体的效果是不是我们想要的
好了,今天的讲解到这里结束了,有了NineOldAndroids我们可以在2.x的手机上面实现许多复杂的动画效果,文章也介绍了关于开源库NineOldAndroids使用的一些知识,文章有点长,希望读者还是先将文章看下,然后自己看看能不能自己实现出来,有什么不明白的地方请在下面留言,我会为大家解答的!
项目源码,点击下载
ps: 下载源码的时候运行出错,是因为我加入了NineOldAndroids的Jar包,然后又加入了NineOldAndroids工程库,主要是我写DEMO的时候为了方便看源码就导入了NineOldAndroids工程库,大家删除一个JAR包或者NineOldAndroids工程库 就能解决了