詳解Android中實(shí)現(xiàn)ListView左右滑動(dòng)刪除條目的方法
使用Scroller實(shí)現(xiàn)絢麗的ListView左右滑動(dòng)刪除Item效果
這里來(lái)給大家?guī)?lái)使用Scroller的小例子,同時(shí)也能用來(lái)幫助初步解除的讀者更加熟悉的掌握Scroller的使用,掌握好了Scroller的使用我們就能實(shí)現(xiàn)很多滑動(dòng)的效果。例如側(cè)滑菜單,launcher,ListView的下拉刷新等等效果,我今天實(shí)現(xiàn)的是ListView的item的左右滑動(dòng)刪除item的效果,現(xiàn)在很多朋友看到這個(gè)效果應(yīng)該是在Android的通知欄下拉中看到這個(gè)滑動(dòng)刪除的效果吧,我看到這個(gè)效果是在我之前的三星手機(jī)上左右滑動(dòng)打電話發(fā)短信的效果,感覺(jué)很棒,不過(guò)現(xiàn)在很多手機(jī)聯(lián)系人滑動(dòng)都不是我之前那臺(tái)手機(jī)的效果啦,網(wǎng)上很多朋友也寫了關(guān)于滑動(dòng)刪除ListView的item的例子,有些是滑動(dòng)手指離開(kāi)之后然后給item加向左或者向右的移動(dòng)動(dòng)畫,我覺(jué)得這樣子的用戶體驗(yàn)不是很好,所以今天自己也寫了一個(gè)關(guān)于ListView左右滑動(dòng)刪除Item的小例子,ListView的item會(huì)隨著手指在屏幕上的滑動(dòng)而滑動(dòng),手指離開(kāi)屏幕的時(shí)候item會(huì)根據(jù)判斷向左或者向右劃出屏幕,就是跟通知欄的效果差不多,接下來(lái)就帶大家來(lái)實(shí)現(xiàn)這個(gè)效果。
先說(shuō)下實(shí)現(xiàn)該效果的主要思路
先根據(jù)手指觸摸的點(diǎn)來(lái)獲取點(diǎn)擊的是ListView的哪一個(gè)item
手指在屏幕中滑動(dòng)我們利用scrollBy()來(lái)使該item跟隨手指一起滑動(dòng)
手指放開(kāi)的時(shí)候,我們判斷手指拖動(dòng)的距離來(lái)判斷item到底是滑出屏幕還是回到開(kāi)始位置
主要思路就是上面這三步,接下來(lái)我們就用代碼來(lái)實(shí)現(xiàn)吧,首先我們新建一個(gè)項(xiàng)目,叫SlideCutListView
根據(jù)需求我們需要自己自定義一個(gè)ListView來(lái)實(shí)現(xiàn)該功能,接下來(lái)先貼出代碼再講解具體的實(shí)現(xiàn)
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 { /** * 當(dāng)前滑動(dòng)的ListView position */ private int slidePosition; /** * 手指按下X的坐標(biāo) */ private int downY; /** * 手指按下Y的坐標(biāo) */ private int downX; /** * 屏幕寬度 */ private int screenWidth; /** * ListView的item */ private View itemView; /** * 滑動(dòng)類 */ private Scroller scroller; private static final int SNAP_VELOCITY = 600; /** * 速度追蹤對(duì)象 */ private VelocityTracker velocityTracker; /** * 是否響應(yīng)滑動(dòng),默認(rèn)為不響應(yīng) */ private boolean isSlide = false; /** * 認(rèn)為是用戶滑動(dòng)的最小距離 */ private int mTouchSlop; /** * 移除item后的回調(diào)接口 */ private RemoveListener mRemoveListener; /** * 用來(lái)指示item滑出屏幕的方向,向左或者向右,用一個(gè)枚舉值來(lái)標(biāo)記 */ private RemoveDirection removeDirection; // 滑動(dòng)刪除方向的枚舉值 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(); } /** * 設(shè)置滑動(dòng)刪除的回調(diào)接口 * @param removeListener */ public void setRemoveListener(RemoveListener removeListener) { this.mRemoveListener = removeListener; } /** * 分發(fā)事件,主要做的是判斷點(diǎn)擊的是那個(gè)item, 以及通過(guò)postDelayed來(lái)設(shè)置響應(yīng)左右滑動(dòng)事件 */ @Override public boolean dispatchTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { addVelocityTracker(event); // 假如scroller滾動(dòng)還沒(méi)有結(jié)束,我們直接返回 if (!scroller.isFinished()) { return super.dispatchTouchEvent(event); } downX = (int) event.getX(); downY = (int) event.getY(); slidePosition = pointToPosition(downX, downY); // 無(wú)效的position, 不做任何處理 if (slidePosition == AdapterView.INVALID_POSITION) { return super.dispatchTouchEvent(event); } // 獲取我們點(diǎn)擊的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); } /** * 往右滑動(dòng),getScrollX()返回的是左邊緣的距離,就是以View左邊緣為原點(diǎn)到開(kāi)始滑動(dòng)的距離,所以向右邊滑動(dòng)為負(fù)值 */ private void scrollRight() { removeDirection = RemoveDirection.RIGHT; final int delta = (screenWidth + itemView.getScrollX()); // 調(diào)用startScroll方法來(lái)設(shè)置一些滾動(dòng)的參數(shù),我們?cè)赾omputeScroll()方法中調(diào)用scrollTo來(lái)滾動(dòng)item scroller.startScroll(itemView.getScrollX(), 0, -delta, 0, Math.abs(delta)); postInvalidate(); // 刷新itemView } /** * 向左滑動(dòng),根據(jù)上面我們知道向左滑動(dòng)為正值 */ private void scrollLeft() { removeDirection = RemoveDirection.LEFT; final int delta = (screenWidth - itemView.getScrollX()); // 調(diào)用startScroll方法來(lái)設(shè)置一些滾動(dòng)的參數(shù),我們?cè)赾omputeScroll()方法中調(diào)用scrollTo來(lái)滾動(dòng)item scroller.startScroll(itemView.getScrollX(), 0, delta, 0, Math.abs(delta)); postInvalidate(); // 刷新itemView } /** * 根據(jù)手指滾動(dòng)itemView的距離來(lái)判斷是滾動(dòng)到開(kāi)始位置還是向左或者向右滾動(dòng) */ private void scrollByDistanceX() { // 如果向左滾動(dòng)的距離大于屏幕的二分之一,就讓其刪除 if (itemView.getScrollX() >= screenWidth / 2) { scrollLeft(); } else if (itemView.getScrollX() <= -screenWidth / 2) { scrollRight(); } else { // 滾回到原始位置,為了偷下懶這里是直接調(diào)用scrollTo滾動(dòng) itemView.scrollTo(0, 0); } } /** * 處理我們拖動(dòng)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; // 手指拖動(dòng)itemView滾動(dòng), deltaX大于0向左滾動(dòng),小于0向右滾 itemView.scrollBy(deltaX, 0); return true; //拖動(dòng)的時(shí)候ListView不滾動(dòng) case MotionEvent.ACTION_UP: int velocityX = getScrollVelocity(); if (velocityX > SNAP_VELOCITY) { scrollRight(); } else if (velocityX < -SNAP_VELOCITY) { scrollLeft(); } else { scrollByDistanceX(); } recycleVelocityTracker(); // 手指離開(kāi)的時(shí)候就不響應(yīng)左右滾動(dòng) isSlide = false; break; } } //否則直接交給ListView來(lái)處理onTouchEvent事件 return super.onTouchEvent(ev); } @Override public void computeScroll() { // 調(diào)用startScroll的時(shí)候scroller.computeScrollOffset()返回true, if (scroller.computeScrollOffset()) { // 讓ListView item根據(jù)當(dāng)前的滾動(dòng)偏移量進(jìn)行滾動(dòng) itemView.scrollTo(scroller.getCurrX(), scroller.getCurrY()); postInvalidate(); // 滾動(dòng)動(dòng)畫結(jié)束的時(shí)候調(diào)用回調(diào)接口 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方向的滑動(dòng)速度,大于0向右滑動(dòng),反之向左 * * @return */ private int getScrollVelocity() { velocityTracker.computeCurrentVelocity(1000); int velocity = (int) velocityTracker.getXVelocity(); return velocity; } /** * * 當(dāng)ListView item滑出屏幕,回調(diào)這個(gè)接口 * 我們需要在回調(diào)方法removeItem()中移除該Item,然后刷新ListView * * @author xiaanming * */ public interface RemoveListener { public void removeItem(RemoveDirection direction, int position); } }
首先我們重寫dispatchTouchEvent()方法,該方法是事件的分發(fā)方法,我們?cè)诶锩嬷蛔隽艘恍┖?jiǎn)單的步驟,我們按下屏幕的時(shí)候,如果某個(gè)item正在進(jìn)行滾動(dòng),我們直接交給SlideCutListView的父類處理分發(fā)事件,否則根據(jù)點(diǎn)擊的X,Y坐標(biāo)利用pointToPosition(int x, int y)來(lái)獲取點(diǎn)擊的是ListView的哪一個(gè)position,從而獲取到我們需要滑動(dòng)的item的View,我們還在該方法加入了滑動(dòng)速度的檢測(cè),并且在ACTION_MOVE的時(shí)候來(lái)判斷是否響應(yīng)item的左右移動(dòng),用isSlide來(lái)記錄是否響應(yīng)左右滑動(dòng)
然后就是重寫onTouchEvent()方法,我們先判斷isSlide為true,并且我們點(diǎn)擊的是ListView上面的有效的position,否則直接交給SlideCutListView的父類也就是ListView來(lái)處理,在ACTION_MOVE中調(diào)用scrollBy()來(lái)移動(dòng)item,scrollBy()是相對(duì)item的上一個(gè)位置進(jìn)行移動(dòng)的,所以我們每次都要用現(xiàn)在移動(dòng)的距離減去上一個(gè)位置的距離然后賦給scrollBy()方法,這樣子我們就實(shí)現(xiàn)了item的平滑移動(dòng),當(dāng)我們將手指抬起的時(shí)候,我們先根據(jù)手指滑動(dòng)的速度來(lái)確定是item是滑出屏幕還是滑動(dòng)至原始位置,如果向右的速度大于我們?cè)O(shè)置的SNAP_VELOCITY,item就調(diào)用scrollRight()方法滾出屏幕,如果向左的速度小于-SNAP_VELOCITY,則調(diào)用scrollLeft()向左滾出屏幕,如果我們是緩慢的移動(dòng)item,則調(diào)用scrollByDistanceX()方法來(lái)判斷是滾到那個(gè)位置
在scrollRight()和scrollLeft()方法中我們使用Scroller類的startScroll()方法先設(shè)置滾動(dòng)的參數(shù),然后調(diào)用postInvalidate()來(lái)刷新界面,界面刷新就會(huì)調(diào)用computeScroll()方法,我們?cè)诶锩嫣幚頋L動(dòng)邏輯就行了,值得一提的是computeScroll()里面的這段代碼
itemView.scrollTo(0, 0);
我們需要將該item滾動(dòng)在(0, 0 )這個(gè)點(diǎn),因?yàn)槲覀冎皇菍istView的Item滾動(dòng)出屏幕而已,并沒(méi)有將該item移除,而且我們不能手動(dòng)調(diào)用removeView()來(lái)從ListView中移除該item,我們只能通過(guò)改變ListView的數(shù)據(jù),然后通過(guò)notifyDataSetChanged()來(lái)刷新ListView,所以我們需要將其滾動(dòng)至(0, 0),這里比較關(guān)鍵。
定義好了我們左右滑動(dòng)的ListView,接下來(lái)就來(lái)使用它,布局很簡(jiǎn)單,一個(gè)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>
接下來(lái)我們來(lái)看看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>
還記得我在上一篇文章中提到過(guò)調(diào)用scrollTo()方法是對(duì)里面的子View進(jìn)行滾動(dòng)的,而不是對(duì)整個(gè)布局進(jìn)行滾動(dòng)的,所以我們用LinearLayout來(lái)套住我們的item的布局,這點(diǎn)需要注意一下,不然滾動(dòng)的只是TextView。
主頁(yè)面MainActivity里面的代碼比較簡(jiǎn)單,里面使用的也是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("滑動(dòng)刪除" + 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(); } }); } //滑動(dòng)刪除之后的回調(diào)方法 @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; } } }
這里面需要對(duì)SlideCutListView設(shè)置RemoveListener,然后我們?cè)诨卣{(diào)方法removeItem(RemoveDirection direction, int position)中刪除該position的數(shù)據(jù),在調(diào)用notifyDataSetChanged()刷新ListView,我這里用的是ArrayAdatper,直接調(diào)用remove()就可以了。
所有的代碼就編寫完了,我們來(lái)運(yùn)行下程序看看效果吧
使用NineOldAndroids實(shí)現(xiàn)絢麗的ListView左右滑動(dòng)刪除Item效果
再給大家來(lái)一個(gè)ListView左右滑動(dòng)刪除Item效果的例子,上面使用的是滑動(dòng)類Scroller來(lái)實(shí)現(xiàn)的,但是看了下通知欄的左右滑動(dòng)刪除效果,確實(shí)很棒,當(dāng)我們滑動(dòng)Item超過(guò)一半的時(shí)候,item的透明度就變成了0,我們就知道抬起手指的時(shí)候item就被刪除了,當(dāng)item的透明度不為0的時(shí)候,我們抬起手指Item會(huì)回到起始位置,這樣我們就知道拖動(dòng)到什么位置item會(huì)刪除,什么位置Item不刪除,用戶體驗(yàn)更好了,還有一個(gè)效果,就是我們滑動(dòng)刪除了item的時(shí)候,ListView的其他item會(huì)出現(xiàn)向上或者向下滾動(dòng)的效果,感覺(jué)效果很棒,所以在GitHub上面搜索了下,發(fā)現(xiàn)很多開(kāi)源庫(kù)都有這個(gè)效果,比如ListViewAnimations, android-swipelistview等等,我看了下實(shí)現(xiàn)原理,使用的是Jake Wharton的動(dòng)畫開(kāi)源庫(kù)NineOldAndroids,這個(gè)庫(kù)究竟是干嘛的呢?在API3.0(Honeycomb), SDK新增了一個(gè)android.animation包,里面的類是實(shí)現(xiàn)動(dòng)畫效果相關(guān)的類,通過(guò)Honeycomb API,能夠?qū)崿F(xiàn)非常復(fù)雜的動(dòng)畫效果,但是如果開(kāi)發(fā)者想在3.0以下使用這一套API, 則需要使用開(kāi)源框架Nine Old Androids,在這個(gè)庫(kù)中會(huì)根據(jù)我們運(yùn)行的機(jī)器判斷其SDK版本,如果是API3.0以上則使用Android自帶的動(dòng)畫類,否則就使用Nine Old Androids庫(kù)中,這是一個(gè)兼容庫(kù),接下來(lái)我們就來(lái)看看這個(gè)效果的具體實(shí)現(xiàn)吧
實(shí)現(xiàn)該效果的主要思路
先根據(jù)手指觸摸的點(diǎn)來(lái)獲取點(diǎn)擊的是ListView的哪一個(gè)Item
當(dāng)手指在屏幕上面滑動(dòng)的時(shí)候,我們要使得Item跟隨手指的滑動(dòng)而滑動(dòng)
當(dāng)我們抬起手指的時(shí)候,我們根據(jù)滑動(dòng)的距離或者手指在屏幕上面的速度來(lái)判斷Item是滑出屏幕還是滑動(dòng)至其實(shí)位置
Item滑出屏幕時(shí),使ListView的其他item產(chǎn)生向上擠壓或者向下擠壓的效果
大致的思路這是這四步,其中的一些細(xì)節(jié)接下來(lái)我會(huì)一一為大家解答的,接下來(lái)我們就用代碼來(lái)實(shí)現(xiàn)這種效果吧
首先我們新建一個(gè)工程,叫Swipedismisslistview,我們需要將Nine Old Androids這個(gè)庫(kù)引入到工程,大家可以去https://github.com/JakeWharton/NineOldAndroids下載,可以使用Jar包,也可以使用工程庫(kù)的形式引入到我們自己的工程,我們還需要自定義一個(gè)ListView,我們先看代碼然后給大家講解下具體的功能實(shí)現(xiàn)
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 { /** * 認(rèn)為是用戶滑動(dòng)的最小距離 */ private int mSlop; /** * 滑動(dòng)的最小速度 */ private int mMinFlingVelocity; /** * 滑動(dòng)的最大速度 */ private int mMaxFlingVelocity; /** * 執(zhí)行動(dòng)畫的時(shí)間 */ protected long mAnimationTime = 150; /** * 用來(lái)標(biāo)記用戶是否正在滑動(dòng)中 */ private boolean mSwiping; /** * 滑動(dòng)速度檢測(cè)類 */ private VelocityTracker mVelocityTracker; /** * 手指按下的position */ private int mDownPosition; /** * 按下的item對(duì)應(yīng)的View */ private View mDownView; private float mDownX; private float mDownY; /** * item的寬度 */ private int mViewWidth; /** * 當(dāng)ListView的Item滑出界面回調(diào)的接口 */ private OnDismissCallback onDismissCallback; /** * 設(shè)置動(dòng)畫時(shí)間 * * @param mAnimationTime */ public void setmAnimationTime(long mAnimationTime) { this.mAnimationTime = mAnimationTime; } /** * 設(shè)置刪除回調(diào)接口 * * @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; //獲取滑動(dòng)的最小速度 mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity(); //獲取滑動(dòng)的最大速度 } @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(); } //加入速度檢測(cè) mVelocityTracker = VelocityTracker.obtain(); mVelocityTracker.addMovement(ev); } /** * 處理手指滑動(dòng)的方法 * * @param ev * @return */ private boolean handleActionMove(MotionEvent ev) { if (mVelocityTracker == null || mDownView == null) { return super.onTouchEvent(ev); } // 獲取X方向滑動(dòng)的距離 float deltaX = ev.getX() - mDownX; float deltaY = ev.getY() - mDownY; // X方向滑動(dòng)的距離大于mSlop并且Y方向滑動(dòng)的距離小于mSlop,表示可以滑動(dòng) if (Math.abs(deltaX) > mSlop && Math.abs(deltaY) < mSlop) { mSwiping = true; //當(dāng)手指滑動(dòng)item,取消item的點(diǎn)擊事件,不然我們滑動(dòng)Item也伴隨著item點(diǎn)擊事件的發(fā)生 MotionEvent cancelEvent = MotionEvent.obtain(ev); cancelEvent.setAction(MotionEvent.ACTION_CANCEL | (ev.getActionIndex()<< MotionEvent.ACTION_POINTER_INDEX_SHIFT)); onTouchEvent(cancelEvent); } if (mSwiping) { // 跟誰(shuí)手指移動(dòng)item ViewHelper.setTranslationX(mDownView, deltaX); // 透明度漸變 ViewHelper.setAlpha(mDownView, Math.max(0f, Math.min(1f, 1f - 2f * Math.abs(deltaX)/ mViewWidth))); // 手指滑動(dòng)的時(shí)候,返回true,表示SwipeDismissListView自己處理onTouchEvent,其他的就交給父類來(lái)處理 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; //通過(guò)滑動(dòng)的距離計(jì)算出X,Y方向的速度 mVelocityTracker.computeCurrentVelocity(1000); float velocityX = Math.abs(mVelocityTracker.getXVelocity()); float velocityY = Math.abs(mVelocityTracker.getYVelocity()); boolean dismiss = false; //item是否要滑出屏幕 boolean dismissRight = false;//是否往右邊刪除 //當(dāng)拖動(dòng)item的距離大于item的一半,item滑出屏幕 if (Math.abs(deltaX) > mViewWidth / 2) { dismiss = true; dismissRight = deltaX > 0; //手指在屏幕滑動(dòng)的速度在某個(gè)范圍內(nèi),也使得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軸方向的移動(dòng)距離 .alpha(0) .setDuration(mAnimationTime) .setListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { //Item滑出界面之后執(zhí)行刪除 performDismiss(mDownView, mDownPosition); } }); } else { //將item滑動(dòng)至開(kāi)始位置 ViewPropertyAnimator.animate(mDownView) .translationX(0) .alpha(1) .setDuration(mAnimationTime).setListener(null); } //移除速度檢測(cè) if(mVelocityTracker != null){ mVelocityTracker.recycle(); mVelocityTracker = null; } mSwiping = false; } /** * 在此方法中執(zhí)行item刪除之后,其他的item向上或者向下滾動(dòng)的動(dòng)畫,并且將position回調(diào)到方法onDismiss()中 * @param dismissView * @param dismissPosition */ private void performDismiss(final View dismissView, final int dismissPosition) { final ViewGroup.LayoutParams lp = dismissView.getLayoutParams();//獲取item的布局參數(shù) 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); } //這段代碼很重要,因?yàn)槲覀儾](méi)有將item從ListView中移除,而是將item的高度設(shè)置為0 //所以我們?cè)趧?dòng)畫執(zhí)行完畢之后將item設(shè)置回來(lái) 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向上滑動(dòng)的效果 lp.height = (Integer) valueAnimator.getAnimatedValue(); dismissView.setLayoutParams(lp); } }); } /** * 刪除的回調(diào)接口 * * @author xiaanming * */ public interface OnDismissCallback { public void onDismiss(int dismissPosition); } }
看過(guò)Android 使用Scroller實(shí)現(xiàn)絢麗的ListView左右滑動(dòng)刪除Item效果你會(huì)發(fā)現(xiàn),這個(gè)自定義的SwipeDismissListView只重寫了onTouchEvent()方法,其實(shí)我們重寫這一個(gè)方法就能實(shí)現(xiàn)我們需要的效果
1. 我們先看手指按下屏幕的處理方法handleActionDown();該方法里面根據(jù)我們手指按下的點(diǎn)根據(jù)pointToPosition()方法來(lái)獲取我們點(diǎn)擊的position,然后利用getChildAt()來(lái)獲取我們按下的item的View對(duì)象,并且加入手指在屏幕滑動(dòng)的速度檢查,這一步相對(duì)來(lái)說(shuō)還是比較簡(jiǎn)單
2. 接下來(lái)就是手指在屏幕上面滑動(dòng)的處理方法handleActionMove(),這個(gè)方法就稍微的復(fù)雜些,我們需要根據(jù)手指在X軸的滑動(dòng)距離和Y軸的滑動(dòng)距離來(lái)判斷是ListView item的水平滑動(dòng)還是ListView的上下滑動(dòng),當(dāng)滿足Math.abs(deltaX) > mSlop && Math.abs(deltaY) < mSlop這個(gè)條件時(shí)候,我們用一個(gè)布爾值mSwiping來(lái)標(biāo)記Item現(xiàn)在處于水平滑動(dòng)的狀態(tài),這時(shí)候我們需要處理Item跟隨手指的滑動(dòng)而滑動(dòng)的邏輯,我們使用ViewHelper來(lái)處理Item的滑動(dòng)邏輯,這個(gè)類會(huì)根據(jù)機(jī)器的SDK版本來(lái)判斷使用Android系統(tǒng)的API還是NineOldandroids中自己實(shí)現(xiàn)的API使得View滑動(dòng)的效果,NineOldandroids中主要使用Camera(可以實(shí)現(xiàn)各種復(fù)雜動(dòng)畫效果的類),我們直接使用ViewHelper的setTranslationX()和setAlpha()就實(shí)現(xiàn)了item滑動(dòng)和透明度漸變的效果,為了使得我們?cè)诨瑒?dòng)item的時(shí)候,ListView不上下滾動(dòng),我們必須返回true來(lái)屏蔽ListView的上下滾動(dòng),這里需要我們要非常熟悉Android的事件分發(fā)機(jī)制,這里我就不說(shuō)明了,大家不了解的去網(wǎng)上找找相關(guān)的文章看看
還有一個(gè)問(wèn)題,就是當(dāng)我們滑動(dòng)ListView的item的時(shí)候,會(huì)伴隨著item的點(diǎn)擊事件,這不是我們想要的效果,所以當(dāng)Item滑動(dòng)的時(shí)候我們需要取消ListView Item的點(diǎn)擊事件
3. 在看手指抬起的時(shí)候的處理方法handleActionUp(),這里面需要根據(jù)手指的滑動(dòng)速度或者Item移動(dòng)的距離來(lái)判斷Item是滑出屏幕還是滑動(dòng)至起始位置,并且要判斷item向左還是向右滑出屏幕等等邏輯,具體的邏輯可以看代碼,相信大家都看得懂.
我這里要說(shuō)說(shuō)ViewPropertyAnimator類,這個(gè)類能更好的實(shí)現(xiàn)一個(gè)View同時(shí)進(jìn)行多個(gè)動(dòng)畫的功能,當(dāng)然我們也可以使用ObjectAnimator利用AnimatorSet來(lái)實(shí)現(xiàn)一個(gè)View上的多個(gè)同時(shí)進(jìn)行的動(dòng)畫效果,例如我們可以將
ViewPropertyAnimator.animate(mDownView) .translationX(dismissRight ? mViewWidth : -mViewWidth)//X軸方向的移動(dòng)距離 .alpha(0) .setDuration(mAnimationTime) .setListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { //Item滑出界面之后執(zhí)行刪除 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滑出界面之后執(zhí)行刪除 performDismiss(mDownView, mDownPosition); } });
在效果上面是一樣的,但是ViewPropertyAnimator在性能上要比使用ObjectAnimator來(lái)實(shí)現(xiàn)多個(gè)同時(shí)進(jìn)行的動(dòng)畫要高的多,舉個(gè)例子,假如要對(duì)View使用移動(dòng)和透明度的動(dòng)畫,使用ViewPropertyAnimator的話,某個(gè)時(shí)間點(diǎn)上我們只需要調(diào)用一次invalidate()方法刷新界面就行了,而使用ObjectAnimator的話,移動(dòng)的動(dòng)畫需要調(diào)用invalidate(),透明度的動(dòng)畫也需要調(diào)用invalidate()方法,在性能上使用AnimationSet比ViewPropertyAnimator要低,但是有的時(shí)候我們還是需要使用ObjectAnimator,比如,在某個(gè)時(shí)間內(nèi),我們需要將View先變大在變小在變大等復(fù)雜情況,這時(shí)候ObjectAnimator就派上用場(chǎng)了,例如
ObjectAnimator.ofInt(mDownView, "scaleX", 0 ,100 ,0, 100).setDuration(100).start()
通過(guò)上面的幾步我們就實(shí)現(xiàn)了ListView的左右滑動(dòng)刪除item的效果啦,但是還有一個(gè)效果,item刪除之后,ListView的其他item向上或者向下緩緩滑動(dòng)的效果,實(shí)現(xiàn)這個(gè)也很容易,就是動(dòng)態(tài)設(shè)置item的高度,item高度逐漸變小,這樣其他的item就會(huì)出現(xiàn)向上或者向下擠壓的效果啦!
4. 這里我們使用的是ValueAnimator這個(gè)類,這個(gè)類并不是針對(duì)View作用的動(dòng)畫,而是對(duì)某個(gè)值作用的動(dòng)畫,他默認(rèn)使用的Interpolator(插補(bǔ)器)是AccelerateDecelerateInterpolator(開(kāi)始和結(jié)束的時(shí)候慢,中間快) , 舉個(gè)很簡(jiǎn)單的例子,我們?cè)?0秒內(nèi)使用ValueAnimator將某個(gè)值從0變化到100,如果使用LinearInterpolator(線性插補(bǔ)器,勻速變化)在第2秒的時(shí)候,這個(gè)值變成了20,而是用AccelerateDecelerateInterpolator,可能在第二秒的時(shí)候這個(gè)值為15或者13,所以我們?cè)赩alueAnimator變化的時(shí)候設(shè)置值動(dòng)畫變化的監(jiān)聽(tīng)器AnimatorUpdateListener就知道某個(gè)時(shí)間這個(gè)值變成了多少,從而對(duì)View的某個(gè)屬性進(jìn)行設(shè)置(例如大?。?,所以ValueAnimator是間接的對(duì)View設(shè)置動(dòng)畫的
了解了ValueAnimator的使用原理,我們就可以現(xiàn)實(shí)上面的動(dòng)畫效果了,我們使用ValueAnimator將item的高度變成0,設(shè)置ValueAnimator變化的監(jiān)聽(tīng),我們?cè)诨卣{(diào)函數(shù)onAnimationUpdate()中動(dòng)態(tài)的設(shè)置item的高度, 然后添加AnimatorListener監(jiān)聽(tīng)動(dòng)畫的狀態(tài)(例如動(dòng)畫開(kāi)始,結(jié)束,重復(fù)等)監(jiān)聽(tīng),在動(dòng)畫結(jié)束的回調(diào)函數(shù)onAnimationEnd()中刪除該item的數(shù)據(jù),調(diào)用notifyDataSetChanged刷新ListView,看看下面這段代碼
ViewHelper.setAlpha(dismissView, 1f); ViewHelper.setTranslationX(dismissView, 0); ViewGroup.LayoutParams lp = dismissView.getLayoutParams(); lp.height = originalHeight; dismissView.setLayoutParams(lp);
我們使用動(dòng)畫只是將item移動(dòng)出了屏幕,并且將item的高度設(shè)置為了0,并沒(méi)有將item的View從ListView中Remove掉,況且ListView也不能直接Remove掉Item的,只能將數(shù)據(jù)源刪除,在調(diào)用notifyDataSetChanged()刷新,所以我們需要將剛剛滑出屏幕高度設(shè)置為0的Item恢復(fù)回來(lái)
自定義控件的代碼我們已經(jīng)編寫完了,接下來(lái)我們就要使用它了,先看界面的布局代碼
<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>
很簡(jiǎn)單,一個(gè)RelativeLayout包裹我們自定義的ListView控件,接下來(lái)就是主界面的代碼編寫,跟平常的ListView使用一樣,但是我們需要設(shè)置OnDismissCallback()監(jiān)聽(tīng),在
onDismiss()中刪除該位置對(duì)于的數(shù)據(jù),刷新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("滑動(dòng)刪除" + 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(); } }); } }
所有的代碼都已經(jīng)編寫完畢了,接下來(lái)就是運(yùn)行工程,看看具體的效果是不是我們想要的
- Android高仿QQ6.0側(cè)滑刪除實(shí)例代碼
- android RecyclerView側(cè)滑菜單,滑動(dòng)刪除,長(zhǎng)按拖拽,下拉刷新上拉加載
- Android仿微信滑動(dòng)彈出編輯、刪除菜單效果、增加下拉刷新功能
- Android實(shí)現(xiàn)左滑刪除列表功能
- Android 實(shí)現(xiàn)左滑出現(xiàn)刪除選項(xiàng)
- Android仿QQ列表左滑刪除操作
- Android使用CardView作為RecyclerView的Item并實(shí)現(xiàn)拖拽和左滑刪除
- Android 滑動(dòng)監(jiān)聽(tīng)RecyclerView線性流+左右劃刪除+上下移動(dòng)
- Android開(kāi)發(fā)中模仿qq列表信息滑動(dòng)刪除功能
- Android?Recyclerview實(shí)現(xiàn)左滑刪除功能
相關(guān)文章
android客戶端從服務(wù)器端獲取json數(shù)據(jù)并解析的實(shí)現(xiàn)代碼
今天總結(jié)一下android客戶端從服務(wù)器端獲取json數(shù)據(jù)的實(shí)現(xiàn)代碼,需要的朋友可以參考下2013-06-06Android實(shí)現(xiàn)登陸界面的記住密碼功能
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)登陸界面的記住密碼功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04android中用xml文件實(shí)現(xiàn)帶邊框背景效果的方法
這篇文章主要給大家介紹了在android中xml文件實(shí)現(xiàn)帶邊框背景效果的方法,其實(shí)實(shí)現(xiàn)的功能不是很難,僅作記錄,幫助需要的朋友們做個(gè)參考,需要的朋友們下面來(lái)一起看看吧。2017-06-06懸浮對(duì)話框Android代碼實(shí)現(xiàn)
這篇文章主要為大家詳細(xì)介紹了懸浮對(duì)話框Android代碼實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-08-08Android自定義View實(shí)現(xiàn)相機(jī)對(duì)焦框
這篇文章主要為大家詳細(xì)介紹了Android自定義View實(shí)現(xiàn)相機(jī)對(duì)焦框,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05Android基于Intent實(shí)現(xiàn)Activity之間數(shù)據(jù)傳遞的方法
這篇文章主要介紹了Android基于Intent實(shí)現(xiàn)Activity之間數(shù)據(jù)傳遞的方法,結(jié)合實(shí)例形式分析了Activity之間數(shù)據(jù)傳遞操作的相關(guān)技巧,代碼備有較為詳盡的注釋,需要的朋友可以參考下2016-11-11Android實(shí)現(xiàn)Service下載文件,Notification顯示下載進(jìn)度的示例
本篇文章主要介紹了Android實(shí)現(xiàn)Service下載文件,Notification顯示下載進(jìn)度,具有一定的參考價(jià)值,有興趣的可以了解一下。2017-01-01Android基礎(chǔ)控件RadioGroup使用方法詳解
這篇文章主要為大家詳細(xì)介紹了Android基礎(chǔ)控件RadioGroup的使用方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-11-11Android 圖片保存到相冊(cè)不顯示的解決方案(兼容Android 10及更高版本)
這篇文章主要介紹了Android 圖片保存到系統(tǒng)相冊(cè)不顯示的解決方案,幫助大家更好的理解和學(xué)習(xí)使用Android開(kāi)發(fā),感興趣的朋友可以了解下2021-04-04