1. 程式人生 > >ListView滑動刪除實現之三——建立可滑動刪除的ListView

ListView滑動刪除實現之三——建立可滑動刪除的ListView

前言:這幾天房子真是把我搞得頭大,天天也睡不好,奶奶個熊,不知道決策對不對,聽天由命吧。

相關文章:

今天就要來實現我們開頭所說的那個滑動刪除的效果了,首先來看看如何讓檢視跟隨手指移動而移動。然後再進一步看看在ListView中又該如何做。
知識補充
這裡先補充一個知識。這裡會用到一個函式:

View.getScrollX()
它返回當前的view視角橫向移動的座標,即我們呼叫View.scrollX(x,y),getScrollX()返回的就是這裡的x
View.getScrollY()同理。

一、如何讓VIEW跟隨手指反向移動

先看下面效果圖:


這個圖大家估計很容易看懂,就是讓裡面的View(藍色框和黃色框)跟著我們的手指移動。但大家注意了,我們呼叫scrollTo移動的它們父容器(LinearLayout)的視角。所以我們LinearLayout的移動方向與手指移動方向正好反過來。你比如,開始時,我們手指向左滑動。這時候我們的LinearLayout的視角要向右走,才能看到刪除的黃框!後來,當手指向右滑動時,我們的LinearLayout的視角只有向左走才能回到藍色框檢視!所以,從效果圖裡可以看到的最重要一點是:父視窗(LinearLayout)的視角移動向方與手指的移動方向正好是相反的!!!!這一點大家一定要理解出來,不然程式碼上就會出錯。這裡看不懂也沒關係,下面會結合程式碼和檢視具體講解。



然後我們看一下具體實現:

1、首先,我們看看佈局(acitivty_main.xml)

先列出程式碼:

<LinearLayout 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:orientation="vertical"
    tools:context=".MainActivity">

    <TextView
        android:text="@string/hello_world"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <LinearLayout
        android:id="@+id/lin_root"
        android:layout_width="fill_parent"
        android:layout_height="200dp"
        android:orientation="horizontal">

        <TextView
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:background="#0000ff"
            android:text="ITEM"
            android:gravity="center"
            android:textSize="25dp" />

        <TextView
            android:layout_width="200dp"
            android:layout_height="fill_parent"
            android:background="#ffff00"
            android:text="刪除"
            android:textSize="25dp"
            android:textColor="#ffffff"
            android:gravity="center" />

    </LinearLayout>

</LinearLayout>
從效果圖中,我們也可以看到,主佈局是一個LinearLayout的垂直佈局,我們要滑動的佈局頁面是其中的第二個LinearLayout,它是一個水平佈局,我們在前一節講過它的佈局方式:
    <LinearLayout
        android:id="@+id/lin_root"
        android:layout_width="fill_parent"
        android:layout_height="200dp"
        android:orientation="horizontal">

        <TextView
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:background="#0000ff"
            android:text="ITEM"
            android:gravity="center"
            android:textSize="25dp" />

        <TextView
            android:layout_width="200dp"
            android:layout_height="fill_parent"
            android:background="#ffff00"
            android:text="刪除"
            android:textSize="25dp"
            android:textColor="#ffffff"
            android:gravity="center" />

    </LinearLayout>
首先,將第一個TextView(藍色塊)width_layout設定為fill_parent或者match_parent來填滿整個視窗。然後旁邊再附一個黃色塊的TextView;由於父容器LinearLayout是水平佈局,所以藍色框會佔滿整個父容器,黃色框會被擠到右邊看不到的位置。下面我們看看操作程式碼.

2、MainActivity程式碼

操作程式碼如下:

public class MainActivity extends Activity {
    private LinearLayout itemRoot;
    private int mlastX = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        itemRoot = (LinearLayout)findViewById(R.id.lin_root);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int scrollX = itemRoot.getScrollX();
        int x = (int)event.getX();

        if (event.getAction() == MotionEvent.ACTION_MOVE){
            int newScrollX = scrollX + mlastX - x;
            itemRoot.scrollTo(newScrollX,0);
        }
        mlastX = x;

        return super.onTouchEvent(event);
    }
}
這裡最難的就是如何計算移動距離了。我們下面用一個圖來講解移動距離的計算方法。
先看下面這個圖,表示的是根部的LinearLayout(lin_root)的視角跟隨手指的移動方向。下面為了講解方便起見,就用根佈局的ID:lin_root來代表根佈局的檢視。

圖(1),表示程式的初始化情況:黑色框表示螢幕,從這張圖中可以看到,藍色塊顯示在螢幕中,但黃色塊在螢幕外。
圖(2),這個圖模擬此時lin_root.scrollTo到了一個點,其中紅色框表示移動後的lin_root視角。它對應的顯示效果圖就是它下面的那個圖。
圖(3),在圖(2)的基礎上,使用者手指又滑動的一段距離,此時lin_root視角繼續往後移動,在圖(3)中的綠色框就表示這時候移動後的lin_root視角。它對應的顯示效果是它下面的那個圖。

但這裡有個問題必須注意:我們的手指是向左移動的,而lin_root的視角是向右移動的。也就是說,手指的移動方向與lin_root視角的移動方向是反過來的。
理解了上面的視角的移動方向與手指的移動方向是反過來的之後,然後我們再看下面這個圖:
在這個圖中,顯示瞭如何計算出手指的移動距離


要計算出手指的移動距離,必須利用lin_root的父容器,即MainActivity的根佈局原點來算。這是因為lin_root的視角是實時移動的,也就是說lin_root的原點是一直變的,即隨著手動的位置在變。如果我們仍然以lin_root的左上角為原點來計算手指的位置那必然是不準確的。所以我們必須找一個不動的座標點來準確的定位手指的位置。進而計算出手指的移動的距離。這個可以用螢幕座標,也可以利用它的父輩或者祖輩的座標系來算。而在父類中攔截OnTouchEvent()事件是非常容易的,所以,我們就利用它的父容器來做。這也就是為什麼在MainActivity中重寫onTouchEvent()的原因。因為lin_root自身的OnTouchEvent是無法準確獲得手指位置的。
有了上面的鋪墊,下面就是動真格的了,下面來看如何來計算新一輪的scrollX

從上面圖中,我們可以很容易看出來:
newScrollX = scrollX + 手動移動距離
其中手指移動距離 = lastX - getX();
所以newScrollX = scrollX + lastX - getX();

然後再看下面的程式碼:

public boolean onTouchEvent(MotionEvent event) {
    int scrollX = itemRoot.getScrollX();
    int x = (int)event.getX();

    if (event.getAction() == MotionEvent.ACTION_MOVE){
        int newScrollX = scrollX + mlastX - x;
        itemRoot.scrollTo(newScrollX,0);
    }
    mlastX = x;

    return super.onTouchEvent(event);
}
這裡首先通過itemRoot.getScrollX();獲得lin_root上次的視角移動X座標,然後利用int newScrollX = scrollX + mlastX - x;計算出這次應該移動的座標。最後利用itemRoot.scrollTo(newScrollX,0);將lin_root的視角移動到這個位置去。
到這裡,大家應該對這個公式的計算就比較理解了,下面我們進一步優化。

3、優化

(1)滑動距離優化

在上面的程式碼中,我們讓lin_root的視角跟隨手指移動,從效果圖中也可以看到,移動距離左右可以移意移動。這明顯不是我們想要的,我們想讓lin_root的視角只能向右移動,顯示出右邊的刪除框,而且不能超過刪除框,所以總結下來就是下面兩點:

  • 1、lin_root視角只能向右移動,即newScrollX必須大於0
  • 2、lin_root視角向右移動距離不能超過黃色刪除框的寬度 
所以我們把程式碼加以修改:
private final int MAX_WIDTH = 200;
private int dipToPx(Context context, int dip) {
    return (int) (dip * context.getResources().getDisplayMetrics().density + 0.5f);
}
首先,定義一個向右移動的最大距離,因為在acitivity_main.xml中定義了黃色刪除框的寬度是200dip,所以我們這裡MAX_WIDTH也設為200。在程式碼中我們利用dipToPx()函式將DIP轉換成PX;下面是具體的操作程式碼:
public boolean onTouchEvent(MotionEvent event) {
    int maxLength = dipToPx(this,MAX_WIDTH);

    int scrollX = itemRoot.getScrollX();
    int x = (int) event.getX();

    if (event.getAction() == MotionEvent.ACTION_MOVE) {
        int newScrollX = scrollX + mlastX - x;
        if (newScrollX<0){
            newScrollX = 0;
        }else if (newScrollX >maxLength){
            newScrollX = maxLength;
        }
        itemRoot.scrollTo(newScrollX, 0);
    }
    mlastX = x;

    return super.onTouchEvent(event);
}
先摘出核心的改動程式碼:
if (newScrollX<0){
    newScrollX = 0;
}else if (newScrollX >maxLength){
    newScrollX = maxLength;
}
itemRoot.scrollTo(newScrollX, 0);
從上面的程式碼中可以看到,當newScrollX小於0時,即lin_root向左移動時,這是我們不允許的,所以直接將它復位。當newScrollX >maxLength時,即向右移動的距離超過了黃色框的總寬度,就不應允許他繼續滑動了,所以這時候就應該將它的最大移動距離定死在maxLength的位置。
效果圖如下:

(2)、自動收縮、彈出優化

從上面的效果圖中,大家有沒有發現一個問題,滑動太死板了好嗎……想把它停哪就停哪,這也太隨意了點了吧。想想QQ,當用戶滑出一段距離後,它會自動接著往外全部滑出,而不是停那不動!!!反過來,當用戶往裡縮一定距離之後,它會繼續往裡縮,直到回到初始化狀態。就是下面的這個效果:


這裡的改進主要是在當用戶手指擡起的時候,判斷lin_root當前視角的位置,如果已經移動超過黃色塊的1/2,那麼就將黃色塊全部顯示出來,如果沒有超過黃色塊的1/2,那就不移動;看看這裡的改進程式碼:

public boolean onTouchEvent(MotionEvent event) {
    int maxLength = dipToPx(this, MAX_WIDTH);

    int scrollX = itemRoot.getScrollX();
    int x = (int) event.getX();
    int newScrollX = scrollX + mlastX - x;

    if (event.getAction() == MotionEvent.ACTION_MOVE) {
        if (newScrollX < 0) {
            newScrollX = 0;
        } else if (newScrollX > maxLength) {
            newScrollX = maxLength;
        }
        itemRoot.scrollTo(newScrollX, 0);
    }else if(event.getAction() == MotionEvent.ACTION_UP){
        if (scrollX > maxLength/2){
            newScrollX = maxLength;
        }else {
            newScrollX = 0;
        }
        itemRoot.scrollTo(newScrollX, 0);
    }
    mlastX = x;

    return super.onTouchEvent(event);
}
比較以前,這裡新增的程式碼就是下面這幾行:
}else if(event.getAction() == MotionEvent.ACTION_UP){
    if (scrollX > maxLength/2){
        newScrollX = maxLength;
    }else {
        newScrollX = 0;
    }
    itemRoot.scrollTo(newScrollX, 0);
}
即當用戶手指提起來的時候,判斷lin_root移動的當前位置,如果大於黃色塊長度的1/2,就直接將移動距離設定為最大移動距離:
newScrollX = maxLength;
如果小於黃色塊長度的1/2,就將狀態還原:
newScrollX = 0;
好了,到這裡有關移動的問題就結束了,下面就看看在listView中,如何滑動刪除ITEM。
原始碼在文章底部給出

二、滑動刪除的ListView

先看看效果圖:
下圖顯示的是,可以把listview中每一個ITEM都可以滑動顯示出刪除框;然後在載入請的ITEM的時候,把新載入的ITEM全部還原到初始化狀態。


1、搭框架

我們先拋開滑動的問題不管,先把listview的顯示框架搭起來。

(1)、重寫ListView:(MyListView)

由於我們後面會重寫ListView,所以我們在搭框架的時候就先重寫一個ListView,命名為:MyListView
public class MyListView extends ListView {
    public MyListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
}
我們在這裡只是重寫,不做任何處理,至於為什麼要重寫,下面會講到

(2)、MainActivity的佈局:(activity_main.xml)

然後是MainActivity的佈局:
<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"
    tools:context=".MainActivity">

    <com.harvic.com.blog3_1.MyListView
        android:id="@+id/listview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"></com.harvic.com.blog3_1.MyListView>

</RelativeLayout>
這個依然沒什麼難度,就是把我們重寫的listView放進來顯示

(3)Item的佈局:(item_layout.xml)

這裡的佈局採用我們上面的藍色圖塊和黃色圖塊的佈局,由於我們使用的初始化顯示的,所以只會藍色塊會佔滿整個ITEM,黃色塊會被擠到右邊看不到的位置裡。當後面我們滑動的時候,lin_root視角移動,才能看得到黃色塊。這裡只搭框架,先不提視角移動。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/lin_root"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:minHeight="120dp">

    <TextView
        android:id="@+id/title"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:background="#0000ff"
        android:gravity="center"
        android:textSize="25dp" />

    <TextView
        android:id="@+id/del"
        android:layout_width="200dp"
        android:layout_height="fill_parent"
        android:background="#ffff00"
        android:text="刪除"
        android:textSize="25dp"
        android:textColor="#ffffff"
        android:gravity="center" />

</LinearLayout>
這裡的佈局採用第一部分:《一、如何讓VIEW跟隨手指反向移動》的佈局,便於大家理解。

(4)、ListView的Adapter:MergeListAdapter

下面就是對應Adapter的程式碼了;
首先,定一個ViewHolder來承載ITEM中的各個VIEW,由於我們這裡只需要對第一個textview賦值,所以這裡的ViewHolder也只有一個成員變數TextView title;
private static class ViewHolder {
    public TextView title;
}
有了ViewHolder來承載Item中的VIEW檢視,那就需要一個類來承載檢視中應該顯示的資料,所以還會有一個類DataHolder:
public static class DataHolder {
    public String title;
}
ViewHolder與DataHolder是完全對應的,一個儲存的是ITEM的檢視,一個儲存的是ITEM檢視中的值;
然後就是MergeListAdapter的構造函數了:
private Context mContext;
private LayoutInflater mInflater;
private List<DataHolder> mDataList = new ArrayList<DataHolder>();
public MergeListAdapter(Context context, List<DataHolder> dataList){
    mContext = context;
    mInflater = LayoutInflater.from(context);
    if (dataList!=null && dataList.size()>0){
        mDataList.addAll(dataList);
    }
}
可以看到,這裡有一個專門的陣列dataList來儲存傳過來的每一個ITEM對應的值
然後是重寫BaseAdapter都要重寫的向個函式:
@Override
public int getCount() {
    return mDataList.size();
}

@Override
public Object getItem(int position) {
    return mDataList.get(position);
}

@Override
public long getItemId(int position) {
    return position;
}
有關 getCount()和getItemId()沒什麼好講的,關鍵問題在於:getItem(int position)!當我們使用listview::getItemAtPosition(position),根據在listview中點選的位置來獲取當前點選的ITEM的時候,就會呼叫我們這裡重寫的 getItem(int position)!所以我們想通過listview::getItemAtPosition(position)得到什麼,我們這裡就可以通過重寫 getItem(int position)返回過去。這一點非常重要,因為,我們要移動某個ITEM的視角,所以必須得到這個ITEM的LinearLayout根佈局lin_root對應的變數,所以我們就可以在getItem(int position)中返回。看不懂也沒關係,下面會再次講一遍。
最後,重頭戲來了,下面看看getView()的實現:
public View getView(int position, View convertView, ViewGroup parent) {
    ViewHolder holder;
    if (convertView == null || convertView.getTag() == null) {
        convertView = mInflater.inflate(R.layout.item_layout,parent,false);
        holder = new ViewHolder();
        holder.title = (TextView)convertView.findViewById(R.id.title);
        convertView.setTag(holder);
    } else {
        holder = (ViewHolder) convertView.getTag();
    }
    DataHolder item = mDataList.get(position);

    holder.title.setText(item.title);
    return convertView;
}
其實,對於重寫BaseAdapter而言,這裡應該沒什麼難度,下面簡單講一下:
首先,獲取convertView檢視:
ViewHolder holder;
if (convertView == null || convertView.getTag() == null) {
    convertView = mInflater.inflate(R.layout.item_layout,parent,false);
    holder = new ViewHolder();
    holder.title = (TextView)convertView.findViewById(R.id.title);
    convertView.setTag(holder);
} else {
    holder = (ViewHolder) convertView.getTag();
}
由於listView的每個ITEM佈局都是完全一樣的,只是裡面的資料可能不同而已,所以為了充分利用資源,google將listview中每個item的view在不再使用時都會重新分配給下一個ITEM使用,有關convertView的回收利用機制參考以前的一篇文章:《BaseAdapter——convertView回收機制與動態控制元件響應》 ,相信大家看完這篇文章以後,應該就明白了為什麼會使用convertView.setTag()和convertView.getTag()了
所以,這時候MergeListAdapter的完整程式碼應該是:
public class MergeListAdapter extends BaseAdapter {
    private Context mContext;
    private LayoutInflater mInflater;
    private List<DataHolder> mDataList = new ArrayList<DataHolder>();

    public MergeListAdapter(Context context, List<DataHolder> dataList) {
        mContext = context;
        mInflater = LayoutInflater.from(context);
        if (dataList != null && dataList.size() > 0) {
            mDataList.addAll(dataList);
        }
    }

    @Override
    public int getCount() {
        return mDataList.size();
    }

    @Override
    public Object getItem(int position) {
        return mDataList.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder;
        if (convertView == null || convertView.getTag() == null) {
            convertView = mInflater.inflate(R.layout.item_layout, parent, false);
            holder = new ViewHolder();
            holder.title = (TextView) convertView.findViewById(R.id.title);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }
        DataHolder item = mDataList.get(position);

        holder.title.setText(item.title);
        return convertView;
    }

    private static class ViewHolder {
        public TextView title;
    }

    public static class DataHolder {
        public String title;
    }

}

(5)、MainActivity中設定資料:

public class MainActivity extends Activity {

    private MyListView listView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        listView = (MyListView)findViewById(R.id.listview);
        final List<DataHolder> items = new ArrayList<DataHolder>();
        for(int i=0;i<20;i++){
            DataHolder item = new DataHolder();
            item.title = "第"+i+"項";
            items.add(item);
        }
        MergeListAdapter adapter = new MergeListAdapter(this,items);
        listView.setAdapter(adapter);
    }

}
在這裡就是構造資料建立MergeListAdapter例項,然後設定到listview中顯示。
到這裡的效果圖如下:

2、建立可滑動的ITEM

到這裡我們就需要滑動listView的Item了;先想一下方案:當用戶點選listView的時候,找到當前點選的ITEM,讓它像我們第一部分講的一樣,跟著使用者手指移動就好了。
但這裡涉及到兩個最重要的問題:

  •  如何知道當前使用者點選的哪個項
  •  即便知道了當前點選的哪一項,但在listview中怎麼移動指定的ITEM呢?
首先,第一個問題:如何知道當前使用者點選的哪個項
我們主要是在onTouchEvent(MotionEvent event)事件中,利用getItemAtPosition(position),根據當前的點選位置來獲取指定位置的值。基本程式碼如下:
int x = (int) event.getX();
int y = (int) event.getY();
int position = pointToPosition(x, y);
getItemAtPosition(position)
我們前面提到過getItemAtPosition(position)得到的結果就是我們在MergeListAdapter中,重寫getItem()函式時的返回值:
public Object getItem(int position) {
    return mDataList.get(position);
}
到這裡有沒有靈機一現?既然我們通過getItemAtPosition(position)能夠得到MergeListAdapter中getItem()函式的返回值,那我讓它直接返回指定ITEM的根佈局:lin_root的例項多好!!!!事實上,我們就是這樣獲得指定ITEM的lin_root例項的,只是將它加以變種:由於我們沒辦法直接在我們getItem()中返回指定position的例項,所以我們把lin_root的例項,放在DataHolder中:
public static class DataHolder {
    public String title;
    public LinearLayout rootView;
}
這樣,我們在getItem()中,就可以根據position返回當前ITEM的DataHolder的物件了:
public Object getItem(int position) {
    return mDataList.get(position);
}
然後在listView中,當用戶下按時,根據位置找到當前的ITEM:
private LinearLayout itemRoot;

public class MyListView extends ListView {
     …………
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                //我們想知道當前點選了哪一行
                int position = pointToPosition(x, y);
                if (position != INVALID_POSITION) {
                    DataHolder data = (DataHolder) getItemAtPosition(position);
                    itemRoot = data.rootView;
                }
            }
            break;
            …………
        }
    }
這裡的關鍵點在於:根據getItemAtPosition(position)獲得指定位置的DataHolder物件,由於我們把這個ITEM的LinearLayout根結點存在了DataHolder的rootView中,所以在這裡就可以直接得到使用者要滑動的這個ITEM的lin_root的例項了。
private LinearLayout itemRoot;
DataHolder data = (DataHolder) getItemAtPosition(position);
itemRoot = data.rootView;
然後就是在使用者移動和擡起手指時的判斷操作了,也就是把第一部分跟隨手指移動檢視的程式碼移過來:
public boolean onTouchEvent(MotionEvent event) {
    int maxLength = dipToPx(mContext, MAX_WIDTH);

    int x = (int) event.getX();
    int y = (int) event.getY();

    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            //我們想知道當前點選了哪一行
            int position = pointToPosition(x, y);
            if (position != INVALID_POSITION) {
                DataHolder data = (DataHolder) getItemAtPosition(position);
                itemRoot = data.rootView;
            }
        }
        case MotionEvent.ACTION_MOVE: {
            int scrollX = itemRoot.getScrollX();
            int newScrollX = scrollX + mlastX - x;
            if (newScrollX < 0) {
                newScrollX = 0;
            } else if (newScrollX > maxLength) {
                newScrollX = maxLength;
            }
            itemRoot.scrollTo(newScrollX, 0);
        }
        break;
        case MotionEvent.ACTION_UP: {
            int scrollX = itemRoot.getScrollX();
            int newScrollX = scrollX + mlastX - x;
            if (scrollX > maxLength / 2) {
                newScrollX = maxLength;
            } else {
                newScrollX = 0;
            }
            itemRoot.scrollTo(newScrollX, 0);
        }
        break;
    }

    mlastX = x;
    return super.onTouchEvent(event);
}
這段程式碼在第一部分已經講了,這裡就不再重複了,到這裡好像是結束了,但,但,但,我們忽略了一個重要的問題:DataHolder裡的rootView什麼時候賦值的呢?當然是在MergeListAdapter中的getView中,因為,MergeListAdapter中唯一一個能根據ID獲得對應VIEW的地方就只有getView()中了:修改後的getView()程式碼如下:
public View getView(int position, View convertView, ViewGroup parent) {
    ViewHolder holder;
    if (convertView == null || convertView.getTag() == null) {
        convertView = mInflater.inflate(R.layout.item_layout, parent, false);
        holder = new ViewHolder();
        holder.title = (TextView) convertView.findViewById(R.id.title);
        convertView.setTag(holder);
    } else {
        holder = (ViewHolder) convertView.getTag();
    }
    DataHolder item = mDataList.get(position);
    holder.title.setText(item.title);

    item.rootView = (LinearLayout)convertView.findViewById(R.id.lin_root);
    item.rootView.scrollTo(0,0);
    return convertView;
}
這裡相比較框架裡的不同在在於最後四句:
DataHolder item = mDataList.get(position);
holder.title.setText(item.title);

item.rootView = (LinearLayout)convertView.findViewById(R.id.lin_root);
item.rootView.scrollTo(0,0);
因為我們的DataHolder最基本的任務就是初始化對應ITEM的資料,所以holder.title.setText(item.title);就句才是它的根本任務。而又由於,我們為了獲取lin_root的例項,因而我們不得以在DataHolder類中又新增一個他額外的活:儲存對應ITEM的lin_root例項,所以這裡在初始化完ITEM的值後,就利用下面這句獲取lin_root的例項儲存在DataHolder的rootView中:
item.rootView = (LinearLayout)convertView.findViewById(R.id.lin_root);
而又由於,我們使用了convertView的回收機制,所以當用戶滑出來,而沒有還原的情況下,我們如果使用了這個回收的檢視,顯示出來的肯定也是滑出來的ITEM,這就不對了,所以我們要將lin_root的視角設為初始化的(0,0)位置。
到這裡就實現了開頭檢視的效果。有關滑動的問題都講完了,大家要跟著工程看講解,不然可能會覺得很迷茫
原始碼在文章底部給出

3、響應ITEM刪除事件

這部分,我們就需要想辦法響應ITEM的刪除事件了,當用戶點選ITEM的刪除的時候,我們要把指定的ITEM刪除掉,就下面的這個效果:


方法一:

大家肯定很清楚的一點是,刪除的意義就是從MergeListAdapter的資料集mDataList中將它刪除即可,所以最簡單的辦法就是在getView()中直接響應點選事件:

public View getView(final int position, View convertView, ViewGroup parent) {
    …………
    TextView delTv = (TextView) convertView.findViewById(R.id.del);
    delTv.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            mDataList.remove(position);
            notifyDataSetChanged();
        }
    });
    return convertView;
}

方法二:

而上面的處理方式是直接在MergeListAdapter中處理資料,這有點感覺不太好,因為在內部直接處理顯示資料,而外部使用Adapter的地方卻不知道,所以我們一般會將資料的處理工作全部放到外面去。而MergeListAdapter只提供處理資料的介面。
所以我們在MergeListAdapter中新增一個刪除指定ITEM資料的介面:
public void removeItem(int position){
    mDataList.remove(position);
    notifyDataSetChanged();
}
然後在getView()中響應:
public View getView(final int position, View convertView, ViewGroup parent) {
    …………
    TextView delTv = (TextView) convertView.findViewById(R.id.del);
    delTv.setOnClickListener(mDelClickListener);
    return convertView;
}
其中,mDelClickListener是在構造時傳過來的:
 public MergeListAdapter(Context context, List<DataHolder> dataList, View.OnClickListener delClickListener) {
	 …………
 }
這樣,外部由於實現了delClickListener,所以就可以在外面處理del的點選事件了。
public class MainActivity extends Activity implements View.OnClickListener {

    private MergeListAdapter adapter;
    …………

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.del){
           int position = listView.getPositionForView(v);
           adapter.removeItem(position);
        }
    }
}
在構造MergeListAdapter傳進去OnClickListener,然後在使用者點選ITEM的時候,我們外部進行處理。我們這裡是刪除ITEM,所以我們能在內部處理mDataList,但如果我不是刪除ITEM呢,而是做其它工作呢?那這裡的實現方式無疑是最好的。

4、存在問題——引出下文

好了,到這裡有關利用ScrollTo來做滑動刪除的問題已經解決了,但我們這裡有沒有發現一個問題:
大家還記不記得下面的程式碼:

public boolean onTouchEvent(MotionEvent event) {
    
    …………
    switch (event.:getAction()) {
        …………
        case MotionEvent.ACTION_UP: {
            int scrollX = itemRoot.getScrollX();
            int newScrollX = scrollX + mlastX - x;
            if (scrollX > maxLength / 2) {
                newScrollX = maxLength;
            } else {
                newScrollX = 0;
            }
            itemRoot.scrollTo(newScrollX, 0);
        }
        break;
    }
    …………
}
當我們手指擡起的時候,我們會根據當前所處位置直接將lin_root的視角移動到指定的位置——還原或完全展開。但scrollTo(x,y)是直接將lin_root的視角移到指定的位置,而沒有任何的緩衝過程。所以就會顯得非常生硬,試想一下,如果它能有一個移動時間,比如我們設定從當前位置移動目標位置使用10秒,那它在十秒內緩緩移動過去,這樣的感覺是不是很好。而scrollTo(x,y)是根本無法完成這個緩衝效果的,因為它只會一次性顯示在指定位置。所以為了解決這個視角緩緩移動的問題,google又補充了一個類:Scroller,它的出現就是為了解決視角緩慢移動的問題的!!這就是我們下節要講的內容。

好了,這篇文章到這裡就結束了,內容比較長,涉及問題也比較多,大家可能要耐著性子看了。

原始碼內容包括:

1、《TryScrollToMotion》:對應第一部分:如何讓VIEW跟隨手指反向移動

2、《ScrollItem_1》:對應第二部分的PART2:建立可滑動的ITEM

3、《ScrollItem_2》:對應第二部分的PART3:響應ITEM刪除事件

如果本文有幫到你,記得加關注哦

參考文章: