Android 寫一個可以橫向滑動條目的列表
在開發中,會發現很多列表希望條目能夠側滑,側滑出來一兩個按鈕什麼的,例如QQ就可以側滑出刪除按鈕。這邊文章就是教大家寫一個可以側滑的自定義控制元件。另外,本文的內容不是屬於Android中比較高深的內容,高手可以略過。通過閱讀本文,你可能學習到的知識有:
- 自定義側滑控制元件的實現
- Android事件傳遞簡要內容
- 屬性動畫
ValueAnimator
的使用
先來看一下要實現的側滑是什麼樣的效果:
因為這個例子很簡單所以程式碼就不單獨拿出來了,想看程式碼的可以去這個倉庫:https://github.com/Lee-swifter/UToolBox 。 這個一個工具箱的APP,裡面包含有一些查詢的工具,選擇“電視節目表”,然後隨便進入一個頻道,就可以看到這個例子了。程式碼的話請搜尋類 TvChannelItem
這個例子其實就是類似於QQ訊息介面的側滑,只是QQ是滑出來兩個按鈕,而我的只是一個按鈕,另外在滑動出來後再進行上下滑動時我沒有做處理。下面看一下這個控制元件是怎麼做出來的吧。
功能分析
首先可以看到,這個側滑是列表中的條目的功能,那麼需不需要對ListView
或RecyclerView
做一做手腳呢?這個是不需要的,因為這個側滑只是屬於條目的功能,雖然是放在列表裡面的,但是並非需要列表特別支援。另外如果要修改列表控制元件的話,勢必會降低這個功能的可移植性。所以我們僅僅是針對每一個條目寫一個可側滑的自定義控制元件。雖然很多人會說上下滑動的東西里面再加上左右滑動,肯定會出現事件衝突的情況。沒錯,這個問題是有的,但是也很好解決。
說句題外話,ListView
已經有些過時,現在應該轉到RecyclerView
上了,這個例子中的程式碼使用的都是RecyclerView
。
建立自定義控制元件
既然已經確定只需要建立自定義控制元件,那麼就開始考慮這個自定義控制元件要怎麼去設計。
控制元件的設計
- 首先,能看到這個控制元件是由一些其他基本控制元件組成,因此我們需要寫的只是一個組合控制元件,而非繼承自
View
類; - 再次,控制元件中所有的基本控制元件整體是橫向排列的,使用
LinearLayout
可以方便的做出這種佈局。因此我們繼承LinearLayout
; - 關於滑動:因為控制元件的內容是要大於控制元件的寬度的,因此再滑動的時候應該移動的是控制元件的內容,而不是控制元件的位置。這個說是好說,但是這裡有一些函式和變數如果弄混了,就很容易卡在這裡;
- 上下滑動與左右滑動的衝突:首先,我們必須要判斷使用者當前是要進行上下滑動還是左右滑動,如果是上下滑動,可以交由
RecyclerView
來處理;如果是左右滑動,那麼我們必須遮蔽列表的事件攔截,至於怎麼滑動,就是我們自己說的算了。
控制元件的實現
整體就是這些問題了,下面就開始編碼,如果在寫程式碼過程中出現了什麼問題,那就再去解決什麼問題。
- 自定義控制元件的佈局:
<?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的寬度,這兩個寬度是這個佈局的重點。
- 建立自定義控制元件:
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
,這個變數指的是可以考慮為使用者進行滑動操作的最小畫素距離。也就是說如果滑動超過了這個值,那麼認為是滑動操作;如果是小於這個值,則認為是點選操作。
- 滑動處理
滑動的處理是這個控制元件的關鍵部分,內容移動、衝突處理都在這裡做。下面貼出程式碼,並在程式碼中給出註釋:
@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);
}
以上就是本控制元件的關鍵程式碼。由幾個地方需要講解一下:
getParent().requestDisallowInterceptTouchEvent(true)
; 此程式碼用於避免父控制元件攔截事件。因為在Android的事件傳遞過程中,如果一個控制元件的onTouchEvent
函式返回true
,那麼後續的事件都會傳遞到這個控制元件中處理,但是此時父控制元件仍然可以攔截事件,而子控制元件會接收到一個ACTION_CANCEL
的事件。在本例中,此行程式碼可以避免在橫向滑動時觸發上下滑動。- 此處移動是移動的控制元件中的內容,而控制元件本身沒有移動,
scrollX
就是隻內容距控制元件左邊的相對距離。如果你使用了translationX
,那麼你會發現控制元件位置在移動,而右邊的按鈕並沒有被移動出來。如果對這個問題有疑問,可以看看這篇文章http://blog.csdn.net/whsdu929/article/details/52152520。- 在手指擡起來的時候,滑出來的控制元件將滑回指定位置,此時可以用
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 ,在應用中檢視效果(選擇“電視節目表”,然後隨便進入一個頻道,就可以看到本例子)。