Android 使用NineOldAndroids實現絢麗的ListView左右滑動刪除Item效果
今天還是給大家帶來自定義控制元件的編寫,自定義一個ListView的左右滑動刪除Item的效果,這個效果之前已經實現過了,有興趣的可以看下Android 使用Scroller實現絢麗的ListView左右滑動刪除Item效果,之前使用的是滑動類Scroller來實現的,但是看了下通知欄的左右滑動刪除效果,確實很棒,當我們滑動Item超過一半的時候,item的透明度就變成了0,我們就知道擡起手指的時候item就被刪除了,當item的透明度不為0的時候,我們擡起手指Item會回到起始位置,這樣我們就知道拖動到什麼位置item會刪除,什麼位置Item不刪除,使用者體驗更好了,還有一個效果,就是我們滑動刪除了item的時候,ListView的其他item會出現向上或者向下滾動的效果,感覺效果很棒,所以在GitHub上面搜尋了下,發現很多開源庫都有這個效果,比如
實現該效果的主要思路
- 先根據手指觸控的點來獲取點選的是ListView的哪一個Item
- 當手指在螢幕上面滑動的時候,我們要使得Item跟隨手指的滑動而滑動
- 當我們擡起手指的時候,我們根據滑動的距離或者手指在螢幕上面的速度來判斷Item是滑出螢幕還是滑動至其實位置
- Item滑出螢幕時,使ListView的其他item產生向上擠壓或者向下擠壓的效果
大致的思路這是這四步,其中的一些細節接下來我會一一為大家解答的,接下來我們就用程式碼來實現這種效果吧
首先我們新建一個工程,叫Swipedismisslistview,我們需要將Nine Old Androids這個庫引入到工程,大家可以去https://github.com/JakeWharton/NineOldAndroids
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上的多個同時進行的動畫效果,例如我們可以將
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()中刪除該位置對於的資料,重新整理ListViewpackage 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工程庫 就能解決了