1. 程式人生 > >仿QQ對話列表滑動刪除與置頂的原理及實現

仿QQ對話列表滑動刪除與置頂的原理及實現

接下來,我們將完成QQ聊天介面的ListView滑動效果,大家可能都用過ListView,知道ListView是上下滑動的,並不會產生左右滑動的效果,如果想讓ListView變成左右滑動的效果,必須對安卓原始碼有所瞭解,如果你想了解原始碼,請到http://blog.csdn.net/column/details/core-services.html 該專欄下了解詳情。

我的思路就是:

所有的螢幕操作事件由ListView作做攔截,同時把事件傳遞給SlideView做滑動,這種實現的確可以達到效果,而且程式碼很簡單,根本不需要處理什麼複雜的滑動衝突。

QQ效果圖與自己實現的效果圖對比:

                

1.思路流程

首先我們需要實現自己的ListView來處理截獲螢幕的事件,但不是由ListView處理,而是轉發給自定義item控制元件處理,也就是實現的SlideView控制元件。根據處理手勢設定item的狀態,也就是說當已經滑動了,這個時候如果不獲取item的狀態,下次在滑動這個item的時候是不知道這個控制元件已經滑動了,不然就會有二次滑動,所以必須儲存滑動狀態。

下面用箭頭詳細說明今天程式碼流程:

當手指滑動某個item的時候——>ListView截獲滑動事件——>在自定義ListView中實現onTouchEvent()方法,判斷當前是哪個item——>然後將事件派發給item自己的SlideView去處理滑動——>處理完成後設定當前item的狀態(防止二次滑動,也防止其他item滑動後,該item沒有恢復到原來的模樣)

2.定義Item容器

程式碼如下:

<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_height="match_parent">
    <LinearLayout
android:id="@+id/view_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation=
"horizontal"> </LinearLayout> <LinearLayout android:id="@+id/holder" android:layout_width="120dp" android:layout_height="match_parent" android:background="#C5C1AA" android:orientation="horizontal"> <TextView android:id="@+id/top" android:layout_width="wrap_content" android:layout_height="50dp" android:gravity="center" android:background="#FFD700" android:text="@string/slide_holder_top" android:textSize="20sp" android:layout_weight="1"/> <TextView android:id="@+id/delete" android:layout_width="wrap_content" android:layout_height="50dp" android:gravity="center" android:text="@string/slide_holder_delete" android:textSize="20sp" android:layout_weight="1"/> </LinearLayout> </merge>

這裡需要說明的就是,merge節點在載入的時候會忽略掉檢視的層級,也就是說載入是時候會自動忽略掉merge節點,直接載入內部的內容,這裡就是兩個LinearLayout了。

這裡為什麼要用這個,因為等會的SlideView會繼承LinearLayout,如果用LinearLayout包裹這兩個LinearLayout,就會多出一個本身沒用LinearLayout,除了增加視力層級,沒有任何效果。

第一個LinearLayout寬高都設定為match_parent,目的就是將後一個LinearLayout擠出螢幕之外。好達到有滑動出控制元件的效果。

3.實現SlideView

我們SlideView會繼承自LinearLayout,當然你也可以繼承其他的佈局,只是LinearLayout考慮的相對簡單點。

㈠定義item狀態的介面

public interface OnSlideViewOnListener {
    //滑動的三個狀態
public static final int SLIDE_STATUS_OFF = 0;
    public static final int SLIDE_STATUS_START_SCROLL = 1;
    public static final int SLIDE_STATUS_ON = 2;
/**
     * 更新滑動的狀態
     *
     * @param view
* @param status
*/
public void Slide(View view, int status);
}

這個就不用多作解釋了,一目瞭然。

㈡初始化SlideView

public void initView() {
    //獲取上下文
this.mContext = getContext();
//初始化滑動物件
this.mScroller = new Scroller(this.mContext);
//設定該LinearLayout為橫向座標
setOrientation(LinearLayout.HORIZONTAL);
//載入佈局
View.inflate(this.mContext,R.layout.slide_holder,this);
//獲取容器
this.mLinearLayout = (LinearLayout) findViewById(R.id.view_content);
//將隱藏容器的寬度根據螢幕調節
this.mHolderWidth = Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this.mHolderWidth, getResources().getDisplayMetrics()));
}

㈢處理ListView傳送過來的事件

public void onRequiredToEvent(MotionEvent event) {
    //獲取當前X,Y座標
int x = (int) event.getX();
    int y = (int) event.getY();
Log.d(TAG, "x=" + x + " ,y=" + y);
//getScrollX()獲取的值則發生了變化:指呼叫的控制元件的水平移動的距離,當未移動的時候,獲取的值為0. 當向右移動20,則獲取值為 -20,再向右移動10,則獲取-30
int scrollX = getScrollX();
//根據按鍵的方式處理相應的事件
switch (event.getAction()) {
        //按下時候處理
case MotionEvent.ACTION_DOWN: {
            if (!this.mScroller.isFinished()) {
                this.mScroller.abortAnimation();
}
            if (this.mOnSlideViewOnListener != null) {
                this.mOnSlideViewOnListener.Slide(this, OnSlideViewOnListener.SLIDE_STATUS_START_SCROLL);
}
            break;
}
        //移動時候處理
case MotionEvent.ACTION_MOVE: {
            //x,y各滑動了多少距離
int deltaX = x - this.mLastX;
            int deltaY = y - this.mLastY;
Log.d(TAG, "deltaX=" + deltaX + " ,deltaY=" + deltaY);
            if (Math.abs(deltaX) < Math.abs(deltaY) * 2) {
                //角度大於60度,不符合滑動的條件
break;
}
            int newScrollX = scrollX - deltaX;
            if (deltaX != 0) {
                if (newScrollX < 0) {
                    newScrollX = 0;
} else if (newScrollX > this.mHolderWidth) {
                    newScrollX = this.mHolderWidth;
}
                this.scrollTo(newScrollX, 0);
}
            break;
}
        //擡起時候處理
case MotionEvent.ACTION_UP: {
            int newScrollX = 0;
            if (scrollX - this.mHolderWidth * 0.75 > 0) {
                newScrollX = this.mHolderWidth;
}
            this.smoothScrollTo(newScrollX, 0);
            if (this.mOnSlideViewOnListener != null) {
                this.mOnSlideViewOnListener.Slide(this, newScrollX == 0 ? OnSlideViewOnListener.SLIDE_STATUS_OFF : OnSlideViewOnListener.SLIDE_STATUS_ON);
}
            break;
}
        default:
            break;
}

    //將歷史座標更新
this.mLastX = x;
    this.mLastY = y;
}

在開始的時候定義了一個Scroller物件,該物件是彈性滑動物件,滑動效果由他實現。

Math.abs(deltaX) < Math.abs(deltaY) * 2的解釋:

deltaX為滑動的座標變數,X向左座標越小,X向右座標越大,Y向下座標越大,Y向上座標越小,所以有可能相對於上一個座標變數相減會是負值,所以用Math.abs()取他們的絕對值,因為我們只要保證橫向滑動的時候,角度在60度以內就算是橫向滑動,大於60度,那就算縱向滑動所以不處理滑動。而tan(63)約等於2,故對邊除鄰邊要<2,所以把變更Y移過去就得到這個值。

當然影象更容易理解,如下所示:

大的直線箭頭為X,Y軸,120度的箭頭為滑動方向,我們在該圖中為左,在矩形ListView的Item這個120度角度內都判斷為向左滑動,如果手指超過這個角度,就不是橫向滑動了。

getScrollX()指呼叫的控制元件的水平移動的距離,當未移動的時候,獲取的值為0. 當向右移動20,則獲取值為 -20,再向右移動10,則獲取-30

this.mScroller.isFinished():

返回scroller是否已完成滾動。

返回值:停止滾動返回true,否則返回false

this.mScroller.abortAnimation():停止動畫。Scroller滾動到最終xy位置時中止動畫。

這兩句應該這樣理解,當我按下某個item的時候,如果我在滑動這個item,擡起手後,他的動畫還沒有結束,這個時候,我又按下了剛才的item,就必須讓他停止動畫。又因為正在滑動,所以必須設定這個item的狀態為滑動。

newScrollX = scrollX - deltaX;的目的是為了防止滑動越界,因為我可能手指一直向某個方向滑動,如果滑動超過了120dip,那麼如果不處理越界,就會有多的空白部分,會導致滑動無止境,有可能滑動的螢幕上沒有這個item了。所以當超過120dip的時候,就停止滑動if:newScrollX,else:newScrollX = this.mHolderWidth;當小於0後,也停止滑動:if:newScrollX < 0,else:newScrollX = 0;然後自執行滑動this.scrollTo(newScrollX, 0);

this.scrollTo(newScrollX, 0);是直接從某個座標在X軸滑動newScrollX的距離,Y軸滑動0,X右為負,左為正,Y下為負,上為正。與座標系相反。

最後解釋一下,當手指擡起後,滿足scrollX - this.mHolderWidth * 0.75 > 0,則滑動出隱藏控制元件。然後滑動。

滑動程式碼如下:

public void smoothScrollTo(int destX, int destY) {
    int scrollX = getScrollX();
    int delta = destX - scrollX;
    this.mScroller.startScroll(scrollX, 0, delta, 0, Math.abs(delta) * 3);
invalidate();//重繪
}

因為為橫向滑動destY是沒有意義的,這裡這樣是可能很容易擴充套件程式碼,首先獲得手指滑動的距離,然後獲得最終item需要滑動的距離delta,然後執行滑動。

public void startScroll (int startX, int startY, int dx, int dy) :以提供的起始點和將要滑動的距離開始滾動。滾動會使用預設值250ms作為持續時間。

當呼叫startScroll方法後,scroller並不是直接包辦一切,它只是幫助你計算每次移動的偏移量,你需要重寫computeScroll方法,並在裡面處理移動。程式碼如下:

//呼叫startScroll()是不會有滾動效果的,只有在computeScroll()獲取滾動情況,做出滾動的響應
@Override
public void computeScroll() {
    if(this.mScroller.computeScrollOffset()){
        scrollTo(this.mScroller.getCurrX(),this.mScroller.getCurrY());
postInvalidate();
}
    super.computeScroll();
}

如果滑動出隱藏控制元件設定狀態為ON,否則為OFF。

玩QQ的也應該知道,所有的item只有一個,也只能有一個滑動出現,如果要滑動出另一個控制元件必須將上個控制元件關閉。所以還需要將控制元件關閉的方法,程式碼如下 :

/**
 * 將當前狀態置為關閉
 */
public void shrink(){
    if(getScrollX()!=0){
        this.smoothScrollTo(0,0);
}
}

當滑動另一個控制元件的時候就呼叫上一個item控制元件的shrink()方法。

當然還需要將View載入進來,程式碼如下:

/**
 * 將View載入進來
 * @param view
*/
public void setContentView(View view){
    this.mLinearLayout.addView(view);
}

當然還要有設定回撥函式的方法:

/**
 * 設定回撥函式
 * @param onSlideViewOnListener
*/
public void setmOnSlideViewOnListener(OnSlideViewOnListener onSlideViewOnListener){
    this.mOnSlideViewOnListener=onSlideViewOnListener;
}

這樣SlideView就基本實現了。

4.實現自定義的ListView

如果用系統自己的ListView,將不會將滑動事件傳送到SlideView,故需要實現自己的ListView,然而我們並不 是直接繼承ListView,而是ListViewCompat,該類也繼承自ListView,不過有些擴充套件的方法可以使用,更加全能。

程式碼如下:

public class MyListView extends ListViewCompat {

    private SlideView chooseSlideView;
    public MyListView(Context context, AttributeSet attrs) {
        super(context, attrs);
}

    @Override
public boolean onTouchEvent(MotionEvent event) {
        switch(event.getAction()){
            case MotionEvent.ACTION_DOWN:{
                int x=(int)event.getX();
                int y=(int)event.getY();
                int position=pointToPosition(x,y);
                if(position!= ListView.INVALID_POSITION){
                    MessageItem item=(MessageItem)getItemAtPosition(position);
                    this.chooseSlideView=item.mSlideView;
}
                break;
}
            default:
                break;
}
        //將操作提交給SlideView處理
if(this.chooseSlideView!=null){
            this.chooseSlideView.onRequiredToEvent(event);
}
        return super.onTouchEvent(event);
}
}

當接收到觸控事件後,根據X,Y座標,獲得item的ID,提供給我們的方法為pointToPosition(x,y),一看就知道該方法的:point點到Position。每個ListView都有一個無效的位置,比如第一行的前一行,最後一行的後一行,需要作一個判斷:position!= ListView.INVALID_POSITION。如果是有效的位置,就獲得該item的資訊,並設定chooseSlideView。然後將觸控事件提交給item的包裝View處理(也就是SlideView)。

MessageItem程式碼如下:

public class MessageItem {
    public int iconRes;
    public String title;
    public String msg;
    public String time;
    public SlideView mSlideView;
}

5.最後實現Activity程式碼

啟動的Activity需要實現兩個介面,一個是SlideView.OnSlideViewOnListener,一個是View.OnClickListener點選事件。

對於ListView需要一個介面卡adapter,程式碼如下:

private class SlideAdapter extends BaseAdapter {

    private LayoutInflater mInflater;
SlideAdapter() {
        super();
        this.mInflater = getLayoutInflater();
}

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

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

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

    @Override
public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder;
SlideView slideView = (SlideView) convertView;
        if (slideView == null) {
            View itemView = this.mInflater.inflate(R.layout.list_item, null);
slideView = new SlideView(MainActivity.this);
slideView.setContentView(itemView);
holder = new ViewHolder(slideView);
slideView.setmOnSlideViewOnListener(MainActivity.this);
slideView.setTag(holder);
} else {
            holder = (ViewHolder) slideView.getTag();
}
        MessageItem item = mMessageItem.get(position);
item.mSlideView = slideView;
item.mSlideView.shrink();
holder.icon.setImageResource(item.iconRes);
holder.title.setText(item.title);
holder.msg.setText(item.msg);
holder.time.setText(item.time);
holder.delete.setOnClickListener(MainActivity.this);
holder.top.setOnClickListener(MainActivity.this);
        return slideView;
}
}

定義LayoutInflater都是用來載入佈局檔案的,不管是Activity裡的setContentView()還是剛才的SlideView的Viiew.inflate其內部實現都是這樣做的。

其中list_item程式碼如下:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
    <ImageView
android:id="@+id/icon"
android:layout_width="50dp"
android:layout_height="50dp"/>
    <LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/icon"
android:layout_toRightOf="@+id/icon"
android:orientation="vertical">
        <TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
        <TextView
android:id="@+id/msg"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
    </LinearLayout>
    <TextView
android:id="@+id/time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"/>
</RelativeLayout>

這裡定義了一個ViewHolder,為的就是防止二次載入。其程式碼如下:

public class ViewHolder {
    public ImageView icon;
    public TextView title;
    public TextView msg;
    public TextView time;
    public TextView delete;
    public TextView top;
ViewHolder(View view) {
        this.icon = (ImageView) view.findViewById(R.id.icon);
        this.title = (TextView) view.findViewById(R.id.title);
        this.msg = (TextView) view.findViewById(R.id.msg);
        this.time = (TextView) view.findViewById(R.id.time);
        this.delete = (TextView) view.findViewById(R.id.delete);
        this.top = (TextView) view.findViewById(R.id.top);
}
}

每次獲得MessageItem都呼叫了item.mSlideView.shrink();將隱藏控制元件隱藏。其他的程式碼不用多作解釋,ListView基本都是這麼用的。

下面我們來看一個介面SlideView.OnSlideViewOnListener實現,程式碼如下:

@Override
public void Slide(View view, int status) {
    if (this.mLastSlideViewWithStatusOn != null && this.mLastSlideViewWithStatusOn != view) {
        this.mLastSlideViewWithStatusOn.shrink();
}
    if (status == SlideView.OnSlideViewOnListener.SLIDE_STATUS_ON) {
        this.mLastSlideViewWithStatusOn = (SlideView) view;
}
}

我們定義了一個記錄開啟隱藏控制元件的SlideView:

private SlideView mLastSlideViewWithStatusOn;

當滑動某個控制元件的時候,就將上一個開啟隱藏控制元件的item關閉。如果這個控制元件滑動開了,也就記錄這個控制元件status ==SlideView.OnSlideViewOnListener.SLIDE_STATUS_ON:

this.mLastSlideViewWithStatusOn = (SlideView) view;

在來看看實現點選事件的程式碼:

@Override
public void onClick(View v) {
    switch (v.getId()) {
        case R.id.delete: {
            int position = this.mListView.getPositionForView(v);
            if (position != ListView.INVALID_POSITION) {
                this.mMessageItem.remove(position);
                this.adapter.notifyDataSetChanged();
}
            break;
}
        case R.id.top: {
            int position = this.mListView.getPositionForView(v);
            if (position != ListView.INVALID_POSITION) {
                MessageItem item = this.mMessageItem.get(position);
                this.mMessageItem.remove(position);
                this.mMessageItem.add(0, item);
                this.adapter.notifyDataSetChanged();
}
            break;
}
        default:
            break;
}
}

怎麼刪除item,因為這個程式碼沒有任何解釋,有必要說明一下。

為了獲取操作螢幕的時值,點選的是那個item的刪除按鈕,就必須獲取他的位置position,我們使用getPositionForView(),position for view,也就是位置根據View獲取。接著也是判斷獲取的position是不是有效的。然後將根據位置刪除這個item。mMessageItem:

private List<MessageItem> mMessageItem = new ArrayList<MessageItem>();

是資訊的集合。

然後更新ListView:

this.adapter.notifyDataSetChanged();

而置頂思考的思路如下:

獲取該item的位置,如上面程式碼所示,然後獲得具體的MessageItem,將集合該位置的MessageItem刪除,將具體的MessageItem插入到第一個位置。然後更新ListView如上所示。

置頂後的效果圖如下所示:

下面為初始化Activity:

public void initView() {
    this.mListView = (ListViewCompat) findViewById(R.id.myList);
    for (int i = 0; i < 20; i++) {
        MessageItem item = new MessageItem();
        if (i < 10) {
            item.iconRes =imgRes[i];
item.title = name[i];
item.msg = author[i];
item.time = time[i];
} else {
            item.iconRes = imgRes[19-i];
item.title = name[19 - i];
item.msg = author[19 - i];
item.time = time[19 - i];
}
        this.mMessageItem.add(item);
}
    this.adapter = new SlideAdapter();
    this.mListView.setAdapter(this.adapter);
    this.mListView.setOnItemClickListener(this);
}

成員變更如下:

private String[] name = {"吾國之教育病理", "穀物大腦", "中國曆代經濟變革得失", "了凡四訓", "耶路撒冷三千年", "如果這是宋史", "中國近代史", "羅馬人的故事", "光榮與夢想", "大秦帝國"};
private String[] author = {"鄭也夫", "戴維·珀爾馬特", "吳曉波", "袁了凡", "西蒙·蒙蒂菲奧裡", "高天流雲 ", "王奇生", "鹽野七生 ", "威廉·曼徹斯特 ", "孫皓暉"};
private String[] time = {"2013-10-1 ", " 2015-5-20", "2013-8-1 ", "2007", "2013年3月", "2008", "2013年7月22日", "2011 年12月", "2006年", "2012年5月"};
private int[] imgRes = {R.drawable.book1, R.drawable.book2, R.drawable.book3, R.drawable.book4, R.drawable.book5, R.drawable.book6, R.drawable.book7, R.drawable.book8, R.drawable.book9, R.drawable.book10};
private ListViewCompat mListView;
private List<MessageItem> mMessageItem = new ArrayList<MessageItem>();
private SlideAdapter adapter;
// 上次處於開啟狀態的SlideView
private SlideView mLastSlideViewWithStatusOn;

activity_main.xml程式碼如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
    <com.example.liyuanjing.slidelistview.MyListView
android:id="@+id/myList"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>

本文章程式碼下載地址如下: