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

轉帖請註明本文出自xiaanming的博客(http://blog.csdn.net/xiaanming/article/details/17539199),請尊重他人的辛勤勞動成果,謝謝!java

我在上一篇文章中Android 帶你從源碼的角度解析Scroller的滾動實現原理從源碼的角度介紹了Scroller的滾動實現原理,相信你們對Scroller的使用有必定的瞭解,這篇文章就給你們帶來使用Scroller的小例子,來幫助你們更加熟悉的掌握Scroller的使用,掌握好了Scroller的使用咱們就能實現不少滑動的效果。例如側滑菜單,launcher,ListView的下拉刷新等等效果,我今天實現的是ListView的item的左右滑動刪除item的效果,如今不少朋友看到這個效果應該是在Android的通知欄下拉中看到這個滑動刪除的效果吧,我看到這個效果是在我以前的三星手機上左右滑動打電話發短信的效果,感受很棒,不過如今不少手機聯繫人滑動都不是我以前那臺手機的效果啦,網上不少朋友也寫了關於滑動刪除ListView的item的例子,有些是滑動手指離開以後而後給item加向左或者向右的移動動畫,我以爲這樣子的用戶體驗不是很好,因此今天本身也寫了一個關於ListView左右滑動刪除Item的小例子,ListView的item會隨着手指在屏幕上的滑動而滑動,手指離開屏幕的時候item會根據判斷向左或者向右劃出屏幕,就是跟通知欄的效果差很少,接下來就帶你們來實現這個效果。android

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

  1. 先根據手指觸摸的點來獲取點擊的是ListView的哪個item
  2. 手指在屏幕中滑動咱們利用scrollBy()來使該item跟隨手指一塊兒滑動
  3. 手指放開的時候,咱們判斷手指拖動的距離來判斷item究竟是滑出屏幕仍是回到開始位置

主要思路就是上面這三步,接下來咱們就用代碼來實現吧,首先咱們新建一個項目,叫SlideCutListViewide

 

根據需求咱們須要本身自定義一個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()裏面的這段代碼post

itemView.scrollTo(0, 0); 

咱們須要將該item滾動在(0, 0 )這個點,由於咱們只是將ListView的Item滾動出屏幕而已,並無將該item移除,並且咱們不能手動調用removeView()來從ListView中移除該item,咱們只能經過改變ListView的數據,而後經過notifyDataSetChanged()來刷新ListView,因此咱們須要將其滾動至(0, 0),這裏比較關鍵。動畫

定義好了咱們左右滑動的ListView,接下來就來使用它,佈局很簡單,一個RelativeLayout包裹咱們自定義的ListViewthis

<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的佈局spa

<?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。.net

 

主頁面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()就能夠了。

全部的代碼就編寫完了,咱們來運行下程序看看效果吧

相關文章
相關標籤/搜索