最近在尝试做一个多层嵌套ListView,先根据日期划分成各个子Item,子Item内再嵌套一个ListView展示该日期下的详细列表,最后列表的具体项能够通过点击来展开、收起以聚焦用户的注意力。先上效果图:
接下来我们开始逐步实现:
- 首先是布局文件,从内到外共有三个布局:交易记录、单日交易列表、整体交易列表。
可扩展布局用的是GitHub上大神的ExpandableLayout,Android
Studio 直接Gradle引入编译会报错,建议直接下载工程引用。
<!--交易记录-->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:expandable="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<xu.shallow.multi_listview.Expandable.ExpandableLayoutItem
android:id="@+id/eli_row"
android:layout_width="match_parent"
android:layout_height="wrap_content"
expandable:el_contentLayout="@layout/view_data_item_content"
expandable:el_headerLayout="@layout/view_data_item_header"/>
</LinearLayout>
<!--单日交易列表-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/data_list_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_marginTop="10dp"
android:background="@android:color/holo_red_light"
android:gravity="center"
android:paddingBottom="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_horizontal_margin"
android:textColor="@android:color/white"/>
<xu.shallow.multi_listview.Expandable.ExpandableLayoutListView
android:id="@+id/elv_data"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/data_list_title"/>
</RelativeLayout>
<!--整体交易列表-->
<ListView
android:id="@+id/lv_main"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ListView>
- 布局文件完成之后我们开始生成测试数据并绑定到ListView上去
//手动生成测试用数据
List<TransInfo> list=makeDebugData();
ListView lv_main = (ListView)findViewById(R.id.lv_main);
Main_Adapter main_adapter = new Main_Adapter(MainActivity.this, list);
lv_main.setAdapter(main_adapter);
- 在主列表的Adapter中将列表按日期进行分类,并放入Map中
private Context context;
private List<TransInfo> list;
private Map<String, List<TransInfo>> listMap;
private String[] date_keys;
public Main_Adapter(Context context, List<TransInfo> list) {
this.context = context;
this.list = list;
date_keys = getList(list);
}
…
public void getList(List<TransInfo> list) {
if (list == null || list.size() == 0) {
date_keys = new String[0];
return;
}
listMap = new HashMap<>();
try {
for (TransInfo transInfo : list) {
String time = transInfo.getOrder_time();
SimpleDateFormat spf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.getDefault());
Date format_date = spf.parse(time);
spf = new SimpleDateFormat("yyyy年MM月dd日", Locale.getDefault());
String date = spf.format(format_date);
if (listMap.containsKey(date)) {
listMap.get(date).add(transInfo);
} else {
List<TransInfo> tmpList = new ArrayList<>();
tmpList.add(transInfo);
listMap.put(date, tmpList);
}
}
date_keys = listMap.keySet().toArray(new String[listMap.size()]);
Arrays.sort(date_keys, Collections.reverseOrder());
} catch (Exception e) {
e.printStackTrace();
}
}
…
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(R.layout.view_data_list_item, parent, false);
}
TextView data_list_title = BaseViewHolder.get(convertView, R.id.data_list_title);
final ExpandableLayoutListView elv_data = BaseViewHolder.get(convertView, R.id.elv_data);
data_list_title.setText(date_keys[position]);
Data_List_Adapter adapter = new Data_List_Adapter(context, listMap.get(date_keys[position]));
elv_data.setAdapter(adapter);
return convertView;
}
- 子列表中展示具体的记录
@Override
public View getView(final int position, View convertView, final ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(
R.layout.view_data_item, parent, false);
}
final ExpandableLayoutItem eli_row = BaseViewHolder.get(convertView, R.id.eli_row);
TextView goods_name = BaseViewHolder.get(convertView, R.id.goods_name);
TextView goods_amount = BaseViewHolder.get(convertView, R.id.goods_amount);
TextView order_id = BaseViewHolder.get(convertView, R.id.order_id);
TextView order_time = BaseViewHolder.get(convertView, R.id.order_time);
goods_name.setText(list.get(position).getGoods_name());
String amount = list.get(position).getGoods_amount();
String fmt_amount = String.valueOf(Float.valueOf(amount) / 100);
goods_amount.setText("¥ " + fmt_amount);
order_id.setText(context.getString(R.string.order_id) + ":" + list.get(position).getOrder_id());
order_time.setText(context.getString(R.string.order_time) + ":" + list.get(position).getOrder_time());
return convertView;
}
好布局和代码都写好了,让我们策马崩腾跑起来~
我们生成的测试数据是3组每组10个,运行下来却发现每组只显示了一个,查了下资料发现默认情况下Android是禁止在ScrollView中放入另外的ScrollView的,它的高度是无法计算的。为了解决这个问题,我们需要根据ListView的子项目重新计算主ListView的高度,然后把高度再作为LayoutParams设置给主ListView,这样它的高度就正确了。
public static void setListViewHeightBasedOnChildren(ListView listView) {
try {
ListAdapter listAdapter = listView.getAdapter();
if (listAdapter == null) {
// pre-condition
return;
}
int totalHeight = 0;
for (int i = 0; i < listAdapter.getCount(); i++) {
View listItem = listAdapter.getView(i, null, listView);
listItem.measure(0, 0);
totalHeight += listItem.getMeasuredHeight();
}
ViewGroup.LayoutParams params = listView.getLayoutParams();
params.height = totalHeight + (listView.getDividerHeight() * (listAdapter.getCount() - 1));
listView.setLayoutParams(params);
} catch (Exception e) {
e.printStackTrace();
}
}
只要在设置ListView的Adapter后调用此静态方法即可让子ListView正确的显示在其父ListView的ListItem中。但是要注意的是,子ListView的每个Item必须是LinearLayout,不能是其他的,因为其他的Layout(如RelativeLayout)没有重写onMeasure(),所以会在onMeasure()时抛出异常。
配置好后我们再来看下效果:
我们发现子ListView已经可以正常展现出来并可以点击展开具体的项,但有一个问题,就是当我们子项展开的时候,ListView的高度并没有变化,需要下拉才能实现,会导致整个界面会有两个下拉列表,对用户体验并不是很好。那么我们需要重新处理下ListView的高度计算,对于屏幕之外的Item直接通过getView来计算高度,对于展示在屏幕内的Item我们实时计算目前的高度。
if (i >= listView.getFirstVisiblePosition() && i <= listView.getLastVisiblePosition()) {
totalHeight += listView.getChildAt(i).getMeasuredHeight();
} else {
View listItem = listAdapter.getView(i, null, listView);
listItem.measure(0, 0);
totalHeight += listItem.getMeasuredHeight();
}
同时,我们需要在Item内添加一个监听器,在展开及收起的时候通知ListView高度已经改变了。
eli_row.setOnExpendItemChangeListener(new ExpandableLayoutItem.onExpendItemChangeListener() {
@Override
public void onChange(boolean b) {
Util.setListViewHeightBasedOnChildren((ListView) parent);
}
});
至此整个ListView的基本做好了,后续还需要完善的是在子Listview边界进行切换时,界面会重新计算高度,滚动条和界面会有一个短暂的卡顿。
最后附上项目地址:Multi_ListView