1. 程式人生 > >Android 寫一個可以橫向滑動條目的列表

Android 寫一個可以橫向滑動條目的列表

在開發中,會發現很多列表希望條目能夠側滑,側滑出來一兩個按鈕什麼的,例如QQ就可以側滑出刪除按鈕。這邊文章就是教大家寫一個可以側滑的自定義控制元件。另外,本文的內容不是屬於Android中比較高深的內容,高手可以略過。通過閱讀本文,你可能學習到的知識有:

  1. 自定義側滑控制元件的實現
  2. Android事件傳遞簡要內容
  3. 屬性動畫ValueAnimator的使用

先來看一下要實現的側滑是什麼樣的效果:

sample.gif
因為這個例子很簡單所以程式碼就不單獨拿出來了,想看程式碼的可以去這個倉庫:https://github.com/Lee-swifter/UToolBox 。 這個一個工具箱的APP,裡面包含有一些查詢的工具,選擇“電視節目表”,然後隨便進入一個頻道,就可以看到這個例子了。程式碼的話請搜尋類 TvChannelItem

這個例子其實就是類似於QQ訊息介面的側滑,只是QQ是滑出來兩個按鈕,而我的只是一個按鈕,另外在滑動出來後再進行上下滑動時我沒有做處理。下面看一下這個控制元件是怎麼做出來的吧。

功能分析

首先可以看到,這個側滑是列表中的條目的功能,那麼需不需要對ListViewRecyclerView做一做手腳呢?這個是不需要的,因為這個側滑只是屬於條目的功能,雖然是放在列表裡面的,但是並非需要列表特別支援。另外如果要修改列表控制元件的話,勢必會降低這個功能的可移植性。所以我們僅僅是針對每一個條目寫一個可側滑的自定義控制元件。雖然很多人會說上下滑動的東西里面再加上左右滑動,肯定會出現事件衝突的情況。沒錯,這個問題是有的,但是也很好解決。

說句題外話,ListView已經有些過時,現在應該轉到RecyclerView上了,這個例子中的程式碼使用的都是RecyclerView

建立自定義控制元件

既然已經確定只需要建立自定義控制元件,那麼就開始考慮這個自定義控制元件要怎麼去設計。

控制元件的設計

  1. 首先,能看到這個控制元件是由一些其他基本控制元件組成,因此我們需要寫的只是一個組合控制元件,而非繼承自View類;
  2. 再次,控制元件中所有的基本控制元件整體是橫向排列的,使用LinearLayout可以方便的做出這種佈局。因此我們繼承LinearLayout
  3. 關於滑動:因為控制元件的內容是要大於控制元件的寬度的,因此再滑動的時候應該移動的是控制元件的內容,而不是控制元件的位置。這個說是好說,但是這裡有一些函式和變數如果弄混了,就很容易卡在這裡;
  4. 上下滑動與左右滑動的衝突:首先,我們必須要判斷使用者當前是要進行上下滑動還是左右滑動,如果是上下滑動,可以交由RecyclerView來處理;如果是左右滑動,那麼我們必須遮蔽列表的事件攔截,至於怎麼滑動,就是我們自己說的算了。

控制元件的實現

整體就是這些問題了,下面就開始編碼,如果在寫程式碼過程中出現了什麼問題,那就再去解決什麼問題。

  1. 自定義控制元件的佈局:
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

   <!-- 注意下面這個佈局的寬度是match_parent的,也就是佔用整個控制元件寬度-->
    <LinearLayout
        android:orientation="vertical"
        android:padding="5dip"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_vertical">

        <TextView
            android:id="@+id/widget_channel_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:lines="1"
            android:textSize="16sp"/>

        <TextView
            android:id="@+id/widget_channel_rel"
            android:layout_marginTop="5dip"
            android:layout_width="wrap_content"
            android:lines="1"
            android:layout_height="wrap_content"/>

    </LinearLayout>

    <!-- 這個Button是放在上面的佈局的右邊,也就是超出了控制元件顯示部分-->
    <Button
        android:id="@+id/widget_channel_live_button"
        android:layout_width="100dip"
        android:layout_height="match_parent"
        android:text="@string/live"
        android:textSize="16sp"
        android:background="@android:color/holo_green_light"/>

</merge>

這裡需要注意的第一個LinearLayout的寬度和下面的Button的寬度,這兩個寬度是這個佈局的重點。

  1. 建立自定義控制元件:
public class TvChannelItem extends LinearLayout {

    private int touchSlop;

    public TvChannelItem(Context context) {
        super(context, null);
    }

    public TvChannelItem(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TvChannelItem(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        setOrientation(HORIZONTAL);

        LayoutInflater.from(context).inflate(R.layout.widget_tv_channel, this);
        name = ButterKnife.findById(this, R.id.widget_channel_name);
        url = ButterKnife.findById(this, R.id.widget_channel_rel);
        button = ButterKnife.findById(this, R.id.widget_channel_live_button);

        touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }   
}

在建立自定義控制元件的時候,不需要什麼特別的操作,只是將佈局通過LayoutInflater引入進來,並找到響應的控制元件。注意在構造時初始化了一個變數touchSlop,這個變數指的是可以考慮為使用者進行滑動操作的最小畫素距離。也就是說如果滑動超過了這個值,那麼認為是滑動操作;如果是小於這個值,則認為是點選操作。

  1. 滑動處理

滑動的處理是這個控制元件的關鍵部分,內容移動、衝突處理都在這裡做。下面貼出程式碼,並在程式碼中給出註釋:

@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            //記錄按下的位置
            downX = event.getRawX();
            downY = event.getRawY();
            break;
        case MotionEvent.ACTION_MOVE:
            float nowX = event.getRawX();
            float nowY = event.getRawY();

            //判斷使用者是上下滑動還是左右滑動
            if (!touchMode && (Math.abs(nowX - downX) > touchSlop || Math.abs(nowY - downY) > touchSlop)) {
                touchMode = true;   //一旦該變數被置為true,則滑動方向確定
                if (Math.abs(nowX - downX) > touchSlop && Math.abs(nowY - downY) <= touchSlop) {
                    slide = true;   //此時認為是左右滑動
                    getParent().requestDisallowInterceptTouchEvent(true);   //請求父控制元件不要攔截觸控事件

                    //以下程式碼避免出發點擊事件
                    MotionEvent cancelEvent = MotionEvent.obtain(event);
                    cancelEvent.setAction(MotionEvent.ACTION_CANCEL | (event.getActionIndex() << MotionEvent.ACTION_POINTER_INDEX_SHIFT));
                    onTouchEvent(cancelEvent);
                }
            }

            if (slide) {
                float diffX = downX - nowX + lastScrollX;
                if (diffX < 0)  //設定阻尼
                    diffX /= 3;
                else if (diffX > button.getWidth())
                    diffX = (diffX - button.getWidth()) / 3 + button.getWidth();

                scrollTo((int) diffX, 0);   //滑動到手指位置
            }

            break;
        case MotionEvent.ACTION_UP:
            if (slide) {    //如果是左右滑動,那麼鬆手時需要自動滑到指定位置
                ValueAnimator animator;     //使用的是ValueAnimator,而非Scroller
                if (getScrollX() > button.getWidth() / 2) {
                    animator = ValueAnimator.ofInt(getScrollX(), button.getWidth());
                } else {
                    animator = ValueAnimator.ofInt(getScrollX(), 0);
                }
                animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        scrollTo((Integer) animation.getAnimatedValue(), 0);
                    }
                });
                animator.start();
                slide = false;
            }
            touchMode = false;  //重置變數
            break;
    }

    return super.onTouchEvent(event);
}

以上就是本控制元件的關鍵程式碼。由幾個地方需要講解一下:

  1. getParent().requestDisallowInterceptTouchEvent(true); 此程式碼用於避免父控制元件攔截事件。因為在Android的事件傳遞過程中,如果一個控制元件的onTouchEvent函式返回true,那麼後續的事件都會傳遞到這個控制元件中處理,但是此時父控制元件仍然可以攔截事件,而子控制元件會接收到一個ACTION_CANCEL的事件。在本例中,此行程式碼可以避免在橫向滑動時觸發上下滑動。
  2. 此處移動是移動的控制元件中的內容,而控制元件本身沒有移動,scrollX就是隻內容距控制元件左邊的相對距離。如果你使用了translationX,那麼你會發現控制元件位置在移動,而右邊的按鈕並沒有被移動出來。如果對這個問題有疑問,可以看看這篇文章http://blog.csdn.net/whsdu929/article/details/52152520
  3. 在手指擡起來的時候,滑出來的控制元件將滑回指定位置,此時可以用Scroller來實現,但本例中使用的是ValueAnimator,也僅僅是這個要比用Scroller方便一些。至於ValueAnimator的用法,本例子只是最簡單的,其詳細用法可以自行搜尋。

使用

控制元件已經寫好了,那麼就看看怎麼使用。因為這個控制元件僅僅是一個LinearLayout,因此其使用也沒有需要額外注意的地方,只要會使用RecyclerView,就會使用這個。只不過把佈局換成了單個自定義控制元件而已。

下面是佈局程式碼:

<?xml version="1.0" encoding="utf-8"?>
<lic.swifter.box.widget.TvChannelItem 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/item_tv_channel"
    android:layout_width="match_parent"
    android:layout_height="70dip" />

Adapter程式碼:

public class TvChannelAdapter extends RecyclerView.Adapter<TvChannelHolder> {

    public class TvChannelHolder extends RecyclerView.ViewHolder {
        private TvChannelItem channelItem;

        public TvChannelHolder(View itemView) {
            super(itemView);
            channelItem = ButterKnife.findById(itemView, R.id.item_tv_channel);
        }

        public void setChannel(TvChannel channel) {
            channelItem.setChannel(channel);
        }
    }


    private List<TvChannel> list;

    public TvChannelAdapter(List<TvChannel> list) {
        this.list = list;
    }


    @Override
    public TvChannelHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View rootView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_tv_channel, parent, false);
        return new TvChannelHolder(rootView);
    }

    @Override
    public void onBindViewHolder(TvChannelHolder holder, int position) {
        holder.setChannel(list.get(position));
    }

    @Override
    public int getItemCount() {
        return list.size();
    }
}

RecyclerView佈局程式碼:

recycler.setLayoutManager(new LinearLayoutManager(this));
recycler.setAdapter(new TvChannelAdapter(response.result));

以上程式碼中包含了我專案中的一些內容,但是那些只是資料的封裝。總體使用方式就是這樣,就是RecyclerView正常使用而已。

程式碼

本文章中的程式碼都可以在https://github.com/Lee-swifter/UToolBox 中找到,搜尋TvChannelItem就可以找到本文中描述的自定義控制元件。
也可以從這裡直接下載應用http://fir.im/tobox ,在應用中檢視效果(選擇“電視節目表”,然後隨便進入一個頻道,就可以看到本例子)。