摺疊列表 ExpandableListView
1. ExpandableListView 的特性
ExpandableListView 繼承自 ListView,這意味著它擁有 ListView 的所有屬性,是 ListView 的升級版。它在 ListView 的基礎上增加了子列表,當我們點選某個列表項的時候,它會展開顯示所有的子 item;當我們再次點選該列表項的時候,它會收縮隱藏所有的子 item,其中子 item 相當於是一個 ListView,我們可以給它設定不同的列表樣式及點選事件,通常適用於有兩級分類並且子類比較多的列表場景。
2. ExpandableListView 的基本使用方法
2.1 常用屬性
- android:childDivider:
設定子列表項的分割線樣式,可以通過 drawable 或者 color 資源的方式進行配置 - android:childIndicator:
設定顯示在子列表項旁邊的 View,一般用作該列表項的指示標註 - android:childIndicatorEnd:
設定子列表指示View的終止位置邊界 - android:childIndicatorLeft:
設定子列表指示View的左邊界 - android:childIndicatorRight:
設定子列表指示View的右邊界 - android:childIndicatorStart:
設定子列表指示View的起始位置邊界 - android:groupIndicator:
當前分類組旁邊的指示 View - android:indicatorEnd:
指示 View 的終止位置邊界 - android:indicatorLeft:
指示 View 的左邊界 - android:indicatorRight:
指示 View 的右邊界 - android:indicatorStart:
指示 View 的起始位置邊界
2.2 常用 API
- setChildIndicator(Drawable):
設定展示在子列表項旁邊的指示 View 的樣式資源 - setGroupIndicator(Drawable) :
設定展示在主列表項旁邊的指示 View 的樣式資源,這個不會因為主列表項的伸展或者收縮而改變 - getGroupView():
返回這一組列表的頭 View - getChildView():
返回列表的子列表項
2.3 事件監聽器
-
ExpandableListView.OnChildClickListener:
該介面當中只有一個回撥方法:public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id)
當我們點選一個子列表項的時候會回撥此方法,引數解析
- ExpandableListView parent:被點選的 ExpandableListView 物件
- View v:被點選的具體 item 物件
- int groupPosition:被點選的 item 所在組在主列表的位置
- int childPosition:被點選的 item 在當前組內的位置
-
ExpandableListView.OnGroupClickListener:
介面中只有一個回撥方法:public boolean onGroupClick(ExpandableListView parent, View v, int groupPosition, long id)
該方法監聽某個組的點選事件,當該組內有任意 item 被點選是回撥,引數詳情參見onGroupClick
方法的解析
- ExpandableListView.OnGroupCollapseListener:
只需要實現一個方法:
public void onGroupCollapse(int groupPosition)
當某個組被摺疊收縮的時候會回撥此方法,引數表示被收縮的組在整個主列表中的位置
- ExpandableListView.OnGroupExpandListener:
該介面同樣是需要實現一個方法:public void onGroupExpand(int groupPosition)
當某個組被展開的時候回撥此方法
3. ExpandableListView 示例
ExpandableListView 主要是在 ListView 的基礎之上加上了摺疊的分類效果,所以本節就通過 ExpandableListView 實現對資料的二級分類列表效果,大類就用大家比較熟悉的某競技遊戲裡面的英雄分類,而子類就是該類別裡面的幾個英雄。
PS:英雄分類仁者見仁智者見智,青銅選手求各位骨灰玩家輕拍
3.1 編寫 Activity 的佈局檔案
和前幾節的例子一樣,我們僅需要在根佈局中防止一個 ExpandableListView 即可,然後設定上相應的屬性,如下:
<ExpandableListView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/expandableListView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="@android:color/darker_gray"
android:dividerHeight="0.5dp"
android:indicatorLeft="?android:attr/expandableListPreferredItemIndicatorLeft"
android:padding="30dp" />
3.2 編寫列表佈局
列表佈局類似 ListView 裡面的 item 佈局,但是由於 ExpandableListView 有主類和子類區分,所以這裡需要提供兩套佈局以適應主列表和展開後的子列表:
- 主列表佈局 list_group.xml :
<TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/listTitle" android:layout_width="fill_parent" android:layout_height="wrap_content" android:paddingLeft="?android:attr/expandableListPreferredItemPaddingLeft" android:paddingTop="10dp" android:paddingBottom="10dp" android:textColor="@android:color/black" />
為了突出大分類,字型設定為黑體。
- 子列表佈局 list_item.xml :
<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/expandedListItem" android:layout_width="fill_parent" android:layout_height="wrap_content" android:paddingLeft="?android:attr/expandableListPreferredChildPaddingLeft" android:paddingTop="10dp" android:paddingBottom="10dp" />
3.3 編寫資料集合
本節資料會相對較多,並且有兩級分類,為了程式碼結構清晰這裡將資料單獨抽離出來,與 Activity 的業務程式碼隔離開,新建一個數據集類 DataCollection.java:
package com.emercy.myapplication;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
public class DataCollection {
// 通過map存放每一個大類,key是大類類別名,value是子類List
private static HashMap<String, List<String>> mExpandableListData = new HashMap<>();
private static final String MASTER = "法師";
private static final String ASSASSINATOR = "刺客";
private static final String SHOOTER = "射手";
private static final String TANK = "對抗";
private static final String ASSIST = "輔助";
// 類載入的時候初始化資料
static {
// 建立子類列表,存放在List當中
List<String> master = new ArrayList<>();
master.add("安琪拉");
master.add("西施");
master.add("沈夢溪");
master.add("嫦娥");
master.add("上官婉兒");
master.add("不知火舞");
List<String> assassinator = new ArrayList<>();
assassinator.add("馬超");
assassinator.add("鏡");
assassinator.add("蘭陵王");
assassinator.add("孫悟空");
assassinator.add("娜可露露");
assassinator.add("元歌");
List<String> shooter = new ArrayList<>();
shooter.add("狄仁傑");
shooter.add("伽羅");
shooter.add("蒙犽");
shooter.add("魯班七號");
shooter.add("孫尚香");
shooter.add("后羿");
List<String> tank = new ArrayList<>();
// 咦?為什麼馬超出現了兩次?
// 因為作者就叫馬超
tank.add("馬超");
tank.add("蓋倫");
tank.add("羋月");
tank.add("鎧");
tank.add("典韋");
List<String> assist = new ArrayList<>();
assist.add("蔡文姬");
assist.add("小明");
assist.add("莊周");
assist.add("魯班");
assist.add("東皇太一");
// 將所有的子類List作為Value存放到大類中
mExpandableListData.put(MASTER, master);
mExpandableListData.put(ASSASSINATOR, assassinator);
mExpandableListData.put(SHOOTER, shooter);
mExpandableListData.put(TANK, tank);
mExpandableListData.put(ASSIST, assist);
}
static HashMap<String, List<String>> getData() {
return mExpandableListData;
}
}
該類是一個靜態工具類,裡面只有一個靜態成員變數,用一個 map 來儲存所有的列表項。map 的 key 是大類的類別名稱,value 是子類的 List;子類通過一個 List 來儲存所有的子類 item,最後通過getData()
介面對外暴露資料集合。
3.4 編寫 Adapter
ExpandableListView 的 Adapter 有些不一樣,因為它需要區分主類別和子類別,會多一個 group 的概念,這裡採用的是 BaseExpandableListAdapter。相比前幾節使用的 baseAdapter 大體上的回撥方法都類似,只是多了一些對 group 的處理。
比如 baseAdapter 的getView
在 BaseExpandableListAdapter 裡面分成了getGroupView
和getChildView
分別用來設定主類別的 item 和子類別的 item。結合 BaseAdapter 的回撥方法不難理解 BaseExpandableListAdapter,程式碼如下:
package com.emercy.myapplication;
import android.content.Context;
import android.graphics.Typeface;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseExpandableListAdapter;
import android.widget.TextView;
import java.util.HashMap;
import java.util.List;
public class MyExpandableListAdapter extends BaseExpandableListAdapter {
private Context mContext;
private List<String> mHeroCategory;
private HashMap<String, List<String>> mHeroName;
public MyExpandableListAdapter(Context context, List<String> expandableListTitle,
HashMap<String, List<String>> expandableListDetail) {
mContext = context;
mHeroCategory = expandableListTitle;
mHeroName = expandableListDetail;
}
@Override
public Object getChild(int groupPosition, int childPosition) {
return mHeroName.get(mHeroCategory.get(groupPosition)).get(childPosition);
}
@Override
public long getChildId(int groupPosition, int childPosition) {
return childPosition;
}
@Override
public View getChildView(int groupPosition, final int childPosition,
boolean isLastChild, View convertView, ViewGroup parent) {
final String expandedListText = (String) getChild(groupPosition, childPosition);
if (convertView == null) {
LayoutInflater layoutInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = layoutInflater.inflate(R.layout.list_item, null);
}
TextView expandedListTextView = convertView.findViewById(R.id.expandedListItem);
expandedListTextView.setText(expandedListText);
return convertView;
}
@Override
public int getChildrenCount(int groupPosition) {
return mHeroName.get(mHeroCategory.get(groupPosition)).size();
}
@Override
public Object getGroup(int groupPosition) {
return mHeroCategory.get(groupPosition);
}
@Override
public int getGroupCount() {
return mHeroCategory.size();
}
@Override
public long getGroupId(int listPosition) {
return listPosition;
}
@Override
public View getGroupView(int groupPosition, boolean isExpanded,
View convertView, ViewGroup parent) {
String listTitle = (String) getGroup(groupPosition);
if (convertView == null) {
LayoutInflater layoutInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = layoutInflater.inflate(R.layout.list_group, null);
}
TextView listTitleTextView = convertView
.findViewById(R.id.listTitle);
listTitleTextView.setTypeface(null, Typeface.BOLD);
listTitleTextView.setText(listTitle);
return convertView;
}
@Override
public boolean hasStableIds() {
return false;
}
@Override
public boolean isChildSelectable(int groupPosition, int childPosition) {
return true;
}
}
如果有對這些回撥介面的實現不太理解的,可以回顧一下第24節中講 ListView 的時候對 BaseAdapter 做的詳細講解。
3.5 編寫 MainActivity
前面已經實現了佈局、資料、介面卡等模組的編寫,整個 ExpandableListView 的框架就已經搭建完畢了。雖然本節的示例比較簡單,程式碼量也比較少,但是也希望大家在學習過程中能夠注重模組的編寫順序,循序漸進的培養自己搭建一個更完整的更大型架構的能力。
框架搭建完畢就可以進入業務程式碼的編寫了,在MainActivity中我們主要做以下4件事:
- 設定佈局檔案並從佈局檔案中拿到 ExpandableListView 例項;
- 獲取資料集(實際使用中可能是從網路獲取或者本地讀取);
- 建立介面卡,併為 ExpandableListView 例項設定介面卡;
- 為 ExpandableListView 新增相應的事件監聽器,並實現監聽器介面中的回撥方法。
按照以上 4 步來做即可,程式碼如下:
package com.emercy.myapplication;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.ExpandableListAdapter;
import android.widget.ExpandableListView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
public class MainActivity extends Activity {
HashMap<String, List<String>> expandableListDetail;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 1.設定佈局檔案並從佈局檔案中拿到 ExpandableListView 例項;
setContentView(R.layout.activity_main);
ExpandableListView listView = findViewById(R.id.expandableListView);
// 2. 獲取資料集(實際使用中可能是從網路獲取或者本地讀取)
expandableListDetail = DataCollection.getData();
final List<String> heroCategory = new ArrayList<>(expandableListDetail.keySet());
// 3. 建立介面卡,併為 ExpandableListView 例項設定介面卡
ExpandableListAdapter adapter = new MyExpandableListAdapter(this, heroCategory, expandableListDetail);
listView.setAdapter(adapter);
// 4. 為 ExpandableListView 新增相應的事件監聽器,並實現監聽器介面中的回撥方法
listView.setOnGroupExpandListener(new ExpandableListView.OnGroupExpandListener() {
@Override
public void onGroupExpand(int groupPosition) {
Toast.makeText(getApplicationContext(), heroCategory.get(groupPosition)
+ " 列表展開", Toast.LENGTH_SHORT).show();
}
});
listView.setOnGroupCollapseListener(new ExpandableListView.OnGroupCollapseListener() {
@Override
public void onGroupCollapse(int groupPosition) {
Toast.makeText(getApplicationContext(), heroCategory.get(groupPosition)
+ " 列表摺疊", Toast.LENGTH_SHORT).show();
}
});
listView.setOnChildClickListener(new ExpandableListView.OnChildClickListener() {
@Override
public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) {
Toast.makeText(getApplicationContext(), heroCategory.get(groupPosition)
+ " -> " + expandableListDetail.get(heroCategory.get(groupPosition))
.get(childPosition), Toast.LENGTH_SHORT
).show();
return false;
}
});
}
}
編譯執行之後,介面上會展示一個 5 大英雄類別的 ListView,點選每個類別系統會回撥onGroupExpand
方法,我們在當中打印出當前被展開的類別名;然後會彈出該類下的英雄名稱,點選英雄名稱系統會回撥onChildClick
方法,我們在方法中打印出被點選的英雄名稱;最後我們可以點選已經展開的英雄類別,系統會將點選的類別恢復摺疊狀態同時回撥onGroupCollapse
方法,在其中我們打印出被摺疊的類別名稱,最終效果如下:
4. 小結
本節學習了 ListView 的升級版,ExpandableListView 繼承自 ListView,在 ListView 的基礎之上加上了二級分類,所以引入了 group 的概念。在佈局檔案中除了正常的列表 item 外還需要有一個 group 的佈局;
ExpandableListAdapter 也多了一些針對 group 的處理;資料也需要分主類別和子類別,我們先將英雄分為 5 大類,接著在 5 個大類下分別列舉了一些該類的英雄名稱,最終通過 ExpandableListAdapter 實現了一個英雄分類的示例 App。