1. 程式人生 > >Android 使用Scroller實現絢麗的ListView左右滑動刪除Item效果

Android 使用Scroller實現絢麗的ListView左右滑動刪除Item效果

我在上一篇文章中Android 帶你從原始碼的角度解析Scroller的滾動實現原理從原始碼的角度介紹了Scroller的滾動實現原理,相信大家對Scroller的使用有一定的瞭解,這篇文章就給大家帶來使用Scroller的小例子,來幫助大家更加熟悉的掌握Scroller的使用,掌握好了Scroller的使用我們就能實現很多滑動的效果。例如側滑選單,launcher,ListView的下拉重新整理等等效果,我今天實現的是ListView的item的左右滑動刪除item的效果,現在很多朋友看到這個效果應該是在Android的通知欄下拉中看到這個滑動刪除的效果吧,我看到這個效果是在我之前的三星手機上左右滑動打電話發簡訊的效果,感覺很棒,不過現在很多手機聯絡人滑動都不是我之前那臺手機的效果啦,網上很多朋友也寫了關於滑動刪除ListView的item的例子,有些是滑動手指離開之後然後給item加向左或者向右的移動動畫,我覺得這樣子的使用者體驗不是很好,所以今天自己也寫了一個關於ListView左右滑動刪除Item的小例子,ListView的item會隨著手指在螢幕上的滑動而滑動,手指離開螢幕的時候item會根據判斷向左或者向右劃出螢幕,就是跟通知欄的效果差不多,接下來就帶大家來實現這個效果。

先說下實現該效果的主要思路

  1. 先根據手指觸控的點來獲取點選的是ListView的哪一個item
  2. 手指在螢幕中滑動我們利用scrollBy()來使該item跟隨手指一起滑動
  3. 手指放開的時候,我們判斷手指拖動的距離來判斷item到底是滑出螢幕還是回到開始位置
主要思路就是上面這三步,接下來我們就用程式碼來實現吧,首先我們新建一個專案,叫SlideCutListView

根據需求我們需要自己自定義一個ListView來實現該功能,接下來先貼出程式碼再講解具體的實現

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;

/**
 * @blog http://blog.csdn.net/xiaanming
 * 
 * @author xiaanming
 * 
 */
public class SlideCutListView extends ListView {
	/**
	 * 當前滑動的ListView position
	 */
	private int slidePosition;
	/**
	 * 手指按下X的座標
	 */
	private int downY;
	/**
	 * 手指按下Y的座標
	 */
	private int downX;
	/**
	 * 螢幕寬度
	 */
	private int screenWidth;
	/**
	 * ListView的item
	 */
	private View itemView;
	/**
	 * 滑動類
	 */
	private Scroller scroller;
	private static final int SNAP_VELOCITY = 600;
	/**
	 * 速度追蹤物件
	 */
	private VelocityTracker velocityTracker;
	/**
	 * 是否響應滑動,預設為不響應
	 */
	private boolean isSlide = false;
	/**
	 * 認為是使用者滑動的最小距離
	 */
	private int mTouchSlop;
	/**
	 *  移除item後的回撥介面
	 */
	private RemoveListener mRemoveListener;
	/**
	 * 用來指示item滑出螢幕的方向,向左或者向右,用一個列舉值來標記
	 */
	private RemoveDirection removeDirection;

	// 滑動刪除方向的列舉值
	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();
	}
	
	/**
	 * 設定滑動刪除的回撥介面
	 * @param removeListener
	 */
	public void setRemoveListener(RemoveListener removeListener) {
		this.mRemoveListener = removeListener;
	}

	/**
	 * 分發事件,主要做的是判斷點選的是那個item, 以及通過postDelayed來設定響應左右滑動事件
	 */
	@Override
	public boolean dispatchTouchEvent(MotionEvent event) {
		switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN: {
			addVelocityTracker(event);

			// 假如scroller滾動還沒有結束,我們直接返回
			if (!scroller.isFinished()) {
				return super.dispatchTouchEvent(event);
			}
			downX = (int) event.getX();
			downY = (int) event.getY();

			slidePosition = pointToPosition(downX, downY);

			// 無效的position, 不做任何處理
			if (slidePosition == AdapterView.INVALID_POSITION) {
				return super.dispatchTouchEvent(event);
			}

			// 獲取我們點選的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);
	}

	/**
	 * 往右滑動,getScrollX()返回的是左邊緣的距離,就是以View左邊緣為原點到開始滑動的距離,所以向右邊滑動為負值
	 */
	private void scrollRight() {
		removeDirection = RemoveDirection.RIGHT;
		final int delta = (screenWidth + itemView.getScrollX());
		// 呼叫startScroll方法來設定一些滾動的引數,我們在computeScroll()方法中呼叫scrollTo來滾動item
		scroller.startScroll(itemView.getScrollX(), 0, -delta, 0,
				Math.abs(delta));
		postInvalidate(); // 重新整理itemView
	}

	/**
	 * 向左滑動,根據上面我們知道向左滑動為正值
	 */
	private void scrollLeft() {
		removeDirection = RemoveDirection.LEFT;
		final int delta = (screenWidth - itemView.getScrollX());
		// 呼叫startScroll方法來設定一些滾動的引數,我們在computeScroll()方法中呼叫scrollTo來滾動item
		scroller.startScroll(itemView.getScrollX(), 0, delta, 0,
				Math.abs(delta));
		postInvalidate(); // 重新整理itemView
	}

	/**
	 * 根據手指滾動itemView的距離來判斷是滾動到開始位置還是向左或者向右滾動
	 */
	private void scrollByDistanceX() {
		// 如果向左滾動的距離大於螢幕的二分之一,就讓其刪除
		if (itemView.getScrollX() >= screenWidth / 2) {
			scrollLeft();
		} else if (itemView.getScrollX() <= -screenWidth / 2) {
			scrollRight();
		} else {
			// 滾回到原始位置,為了偷下懶這裡是直接呼叫scrollTo滾動
			itemView.scrollTo(0, 0);
		}

	}

	/**
	 * 處理我們拖動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;

				// 手指拖動itemView滾動, deltaX大於0向左滾動,小於0向右滾
				itemView.scrollBy(deltaX, 0);
				
				return true;  //拖動的時候ListView不滾動
			case MotionEvent.ACTION_UP:
				int velocityX = getScrollVelocity();
				if (velocityX > SNAP_VELOCITY) {
					scrollRight();
				} else if (velocityX < -SNAP_VELOCITY) {
					scrollLeft();
				} else {
					scrollByDistanceX();
				}
				
				recycleVelocityTracker();
				// 手指離開的時候就不響應左右滾動
				isSlide = false;
				break;
			}
		}

		//否則直接交給ListView來處理onTouchEvent事件
		return super.onTouchEvent(ev);
	}

	@Override
	public void computeScroll() {
		// 呼叫startScroll的時候scroller.computeScrollOffset()返回true,
		if (scroller.computeScrollOffset()) {
			// 讓ListView item根據當前的滾動偏移量進行滾動
			itemView.scrollTo(scroller.getCurrX(), scroller.getCurrY());
			
			postInvalidate();

			// 滾動動畫結束的時候呼叫回撥介面
			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方向的滑動速度,大於0向右滑動,反之向左
	 * 
	 * @return
	 */
	private int getScrollVelocity() {
		velocityTracker.computeCurrentVelocity(1000);
		int velocity = (int) velocityTracker.getXVelocity();
		return velocity;
	}

	/**
	 * 
	 * 當ListView item滑出螢幕,回撥這個介面
	 * 我們需要在回撥方法removeItem()中移除該Item,然後重新整理ListView
	 * 
	 * @author xiaanming
	 *
	 */
	public interface RemoveListener {
		public void removeItem(RemoveDirection direction, int position);
	}

}
  • 首先我們重寫dispatchTouchEvent()方法,該方法是事件的分發方法,我們在裡面只做了一些簡單的步驟,我們按下螢幕的時候,如果某個item正在進行滾動,我們直接交給SlideCutListView的父類處理分發事件,否則根據點選的X,Y座標利用pointToPosition(int x, int y)來獲取點選的是ListView的哪一個position,從而獲取到我們需要滑動的item的View,我們還在該方法加入了滑動速度的檢測,並且在ACTION_MOVE的時候來判斷是否響應item的左右移動,用isSlide來記錄是否響應左右滑動
  • 然後就是重寫onTouchEvent()方法,我們先判斷isSlide為true,並且我們點選的是ListView上面的有效的position,否則直接交給SlideCutListView的父類也就是ListView來處理,在ACTION_MOVE中呼叫scrollBy()來移動item,scrollBy()是相對item的上一個位置進行移動的,所以我們每次都要用現在移動的距離減去上一個位置的距離然後賦給scrollBy()方法,這樣子我們就實現了item的平滑移動,當我們將手指擡起的時候,我們先根據手指滑動的速度來確定是item是滑出螢幕還是滑動至原始位置,如果向右的速度大於我們設定的SNAP_VELOCITY,item就呼叫scrollRight()方法滾出螢幕,如果向左的速度小於-SNAP_VELOCITY,則呼叫scrollLeft()向左滾出螢幕,如果我們是緩慢的移動item,則呼叫scrollByDistanceX()方法來判斷是滾到那個位置
在scrollRight()和scrollLeft()方法中我們使用Scroller類的startScroll()方法先設定滾動的引數,然後呼叫postInvalidate()來重新整理介面,介面重新整理就會呼叫computeScroll()方法,我們在裡面處理滾動邏輯就行了,值得一提的是computeScroll()裡面的這段程式碼
itemView.scrollTo(0, 0);

我們需要將該item滾動在(0, 0 )這個點,因為我們只是將ListView的Item滾動出螢幕而已,並沒有將該item移除,而且我們不能手動呼叫removeView()來從ListView中移除該item,我們只能通過改變ListView的資料,然後通過notifyDataSetChanged()來重新整理ListView,所以我們需要將其滾動至(0, 0),這裡比較關鍵。

定義好了我們左右滑動的ListView,接下來就來使用它,佈局很簡單,一個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>

接下來我們來看看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>
還記得我在上一篇文章中提到過呼叫scrollTo()方法是對裡面的子View進行滾動的,而不是對整個佈局進行滾動的,所以我們用LinearLayout來套住我們的item的佈局,這點需要注意一下,不然滾動的只是TextView。

主頁面MainActivity裡面的程式碼比較簡單,裡面使用的也是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("滑動刪除" + 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();
			}
		});
	}

	
	//滑動刪除之後的回撥方法
	@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;
		}
		
	}	


}

這裡面需要對SlideCutListView設定RemoveListener,然後我們在回撥方法removeItem(RemoveDirection direction, int position)中刪除該position的資料,在呼叫notifyDataSetChanged()重新整理ListView,我這裡用的是ArrayAdatper,直接呼叫remove()就可以了。

所有的程式碼就編寫完了,我們來執行下程式看看效果吧


好了,今天的講解就到此結束了,有疑問的朋友可以在下面留言,我會幫大家解答的。今天是2013年的最後一天,希望大家開開心心度過2013,也開開心心的迎接2014,提前祝大家元旦快樂!