編輯:關於Android編程
今天還是給大家帶來自定義控件的編寫,自定義一個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,我們先看代碼然後給大家講解下具體的功能實現
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包裹我們自定義的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所有的代碼都已經編寫完畢了,接下來就是運行工程,看看具體的效果是不是我們想要的adapter; private List dataSourceList = new ArrayList (); @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 (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使用的一些知識,文章有點長,希望讀者還是先將文章看下,然後自己看看能不能自己實現出來,有什麼不明白的地方請在下面留言,我會為大家解答的!
項目源碼,點擊下載
Android Studio的代碼自動檢測的錯誤提示方式感覺有點奇葩,和Eclipse差別很大,Eclipse檢測到某個資源文件找不到或者錯誤,都會在Project中對應
Android程序中,Tab標簽窗口是一種常用的UI界面元素。它的實現主要是利用了TabHost類。TabHost說明TabHost是一個標簽窗口的容器。一個TabHos
Android 5.0 Lollipop 推出了 Material Design 安卓界面風格的設計理念並且給出了一個 Material Design 的兼容庫,庫中包含
這篇文章是繼自定義EditText樣式之後的功能強化,對於實際應用項目有很大的參考意見,感興趣的朋友可以移步上一篇,”Android Studion自定義Ed