列表中的列表
安卓平台上的列表组件是ListView,它的功能强大,性能也非常优秀,在应用编写过程中是很重要的数据展示组件。
在有些需求里会要求实现一个嵌套型的列表,也就是列表之中的元素依然是列表,对于特定的情况可以使用ExpandableListView来实现嵌套列表,但这也只能涵盖一部分需求,更多的时候是需要真正意义上的嵌套列表。
嵌套列表的实现
比如做一个资料页,页面上包含四到五个展示不同数据的模块,其中一个模块里有一个不定长度的列表展示获取到的数据;针对这个需求,资料页本身很好做,不管是用ScrollView然后按需放入模块布局还是使用ListView来实现,让BaseAdapter去负责展示都可以。
那么针对模块中要展示的列表怎么办呢?直观的想法是再加一个ListView,也就是ScrollView和ListView嵌套或者ListView和ListView相互嵌套这两种方案。但实际做一做就会知道,这样的结构会遇到列表高度和滑动事件冲突等问题。
列表高度问题是指当在一个可以滑动的组件中嵌套ListView时,这个ListView的高度必须手动指定,否则展示出来时列表高度会非常小,基本无法满足要求。
针对这个问题,解决方案就是提前设置高度,如果想让列表直接全部展示出来则需要自行计算全部展示所需的高度然后设置到ListView上,可以通过重写ListView类来实现自动计算和设置;否则只需要设置固定高度,列表自然会按照该高度展示。
要想自动设置列表高度方方便展示全部项目,有两种方法
第一种方案,重写ListView
public class ExpandListView extends ListView {
public ExpandListView(Context context) {
super(context);
}
public ExpandListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ExpandListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,
MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSpec);
}
}
重点就在于重载onMeasure方法,让高度的测量参考变为尽可能大
第二种方案,手动计算
public static void setListViewHeightBasedOnChildren(ListView listView) {
ListAdapter listAdapter = listView.getAdapter();
if (listAdapter == null) {
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);
}
两种方案均可达到预定效果
这样解决这个问题并非不可行,或者说这个方案是很不错的解决方案,既没有破坏ListView嵌套的本意又解决了列表展示不对的问题;但如果嵌套在内部的列表本身不展示所有项目则会遇到另外一个问题,那就是事件冲突。
安卓的ListView可以响应手指拖动事件来让列表滑动,但如果内部嵌套了另一个ListView并且两者都可以滑动,那么实际使用中就会发现滑动出现问题。
解决方案也不难,让子组件把事件拦截掉即可,这样一来虽然在子列表滑动时父列表无法响应事件,但就解决问题这一点而言是达到了效果。
public boolean onInterceptHoverEvent(MotionEvent event) {
return true;
}
看起来似乎这个话题就可以到此打住了,但实际上这样的嵌套依然会存在一些隐蔽的问题,毕竟谷歌官方是强调过不能在一个可以滑动的View中嵌套另一个可以滑动的View,以上的解决方案都是逆流而上的做法。
可能出现的问题包括子列表的项目布局根部必须是LinearLayout,否则无法计算高度;在某些情况下可能出现计算不准确导致的列表高度误差;还有子列表的getView可能会大量调用等。
而且上文提到的方案要么需要改写ListView来创建自定义的类,要么需要额外的代码来设置ListView的高度,再加上这些方案本身都等于在用一个功能相对比较复杂和强大的组件来实现最简单的需求,感官上觉得不是那么合适。
内嵌列表—LinearLayout实现
回顾一下那些需求,列表中嵌套的列表很多时候并不需要滑动的功能,它在被放到嵌套这一层上后就退化成了一般的列表,不管是重写ListView还是手动计算并设置高度,本质上都是在清除掉ListView的滑动能力,这样才能满足嵌套时的交互要求。
这么一个简单的列表为何一定要使用ListView实现呢?安卓平台上有着简单得多的方案可以做到这样的简单列表,只需要从头开始换个角度思考一遍就行了。
列表,其实就是包含竖向排列的多个子视图的一个东西,如果忽视列表本身要有滑动功能这个点,那么列表本质上就是一个LinearLayout,并且其orientation属性为vertical。
而LinearLayout的特性很清晰,它需要外部给它添加子项,不然就是一片空白。大部分时候使用LinearLayout都是在XML文件中就写好布局的情况,在这里需要稍微转变一下编程思维,既然列表在退化掉滑动功能后和LinearLayout很相似,那么只要能根据需求往LinearLayout中添加子项不就可以实现列表了吗?
这个思路是个简单想法,它能实现的也只是一个简单的列表,但针对嵌套列表使用的场景,这个思路能解决大部分问题,而且还简单,不需要重写ListView也不需要计算高度,因为它就是简简单单的一个LinearLayout罢了。
现在一步步来,首先明确目标,那就是自定义一个LinearLayout,然后让它可以按需添加子项来模拟出一个列表。这个按需添加该怎么实现,其实ListView本身就已经提供了一个很方便的工具,那就是Adapter。
因此,这个自定义组件需要做到的是接受一个Adapter并且仿照ListView那样获取到子项并挨个添加到LinearLayout中。
public class InnerListView extends LinearLayout {
private BaseAdapter adapter;
private int childCount;
public InnerListView(Context context) {
super(context);
init();
}
public InnerListView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
setOrientation(VERTICAL);
}
public BaseAdapter getAdapter() {
return adapter;
}
public void setAdapter(BaseAdapter praAdapter) {
adapter = praAdapter;
if (adapter != null) {
childCount = adapter.getCount();
notifyChanges();
} else {
Log.i("INNER_LIST_ERROR", "The adapter in InnerListView is null pointer!");
}
}
public void notifyChanges() {
removeAllViews();
for (int i = 0; i < childCount; i++) {
addView(adapter.getView(i, null, null));
}
}
}
非常明显,这个方案能适应小规模的列表,大规模以及复杂的列表在刷新时会有一定的性能问题,毕竟这个简单列表方案没有重用的功能。当然了,这个最初的版本也确实非常粗糙,针对其中的一些缺点可以进行不少优化。
这些缺点包括无法设置Header和Footer,每次刷新时都销毁掉所有的子项再重头创建一遍非常消耗性能等等。优化之后可以得到如下的完整代码。
public class InnerListView extends LinearLayout {
private BaseAdapter adapter;
private boolean hasFooterView = false;
private boolean isFooterViewAttached = false;
private boolean hasHeaderView = false;
private boolean isHeaderViewAttached = false;
private View footerView; // 表尾
private View headerView; // 表头
private int prevChildCount;
private int childCount;
private int maxChildCount;
public InnerListView(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
initAttr(attrs);
}
public InnerListView(Context context) {
super(context);
initAttr(null);
}
private void initAttr(AttributeSet attrs) {
setOrientation(VERTICAL);
childCount = 0;
maxChildCount = 0;
prevChildCount = -1;
}
public void setHeaderView(View praHeader) {
headerView = praHeader;
hasHeaderView = true;
}
public void setFooterView(View praFooter) {
footerView = praFooter;
hasFooterView = true;
}
public BaseAdapter getAdapter() {
return adapter;
}
public void setAdapter(BaseAdapter praAdapter) {
adapter = praAdapter;
if (!isHeaderViewAttached && hasHeaderView) {
addView(headerView);
isHeaderViewAttached = true;
}
if (!isFooterViewAttached && hasFooterView) {
addView(footerView);
isFooterViewAttached = true;
}
if (adapter != null) {
childCount = adapter.getCount();
notifyChanges();
prevChildCount = childCount;
if (childCount > maxChildCount) {
maxChildCount = childCount;
}
} else {
Log.i("INNER_LIST_ERROR",
"The adapter in InnerListView is null pointer!");
}
}
public void notifyChanges() {
int count = 0;
if (isHeaderViewAttached) {
count++;
}
if (prevChildCount < childCount) {
for (int i = 0; i < prevChildCount; i++) {
adapter.getView(i, getChildAt(i + count), null);
getChildAt(i + count).setVisibility(View.VISIBLE);
}
for (int i = (prevChildCount == -1 ? 0 : prevChildCount); i < childCount; i++) {
if (prevChildCount != -1 && i < maxChildCount) {
adapter.getView(i, getChildAt(i + count), null);
getChildAt(i + count).setVisibility(View.VISIBLE);
} else {
View v = adapter.getView(i, null, null);
addView(v, i + count);
}
}
} else {
for (int i = 0; i < childCount; i++) {
adapter.getView(i, getChildAt(i + count), null);
getChildAt(i + count).setVisibility(View.VISIBLE);
}
for (int i = childCount; i < prevChildCount; i++) {
getChildAt(i + count).setVisibility(View.GONE);
}
}
}
}
这个“列表”只能通过setAdapter方法来刷新,因为BaseAdapter的观察者模式是针对ListView等组件的,如果不重新定义BaseAdapter无法将notifyDataSetChanged接驳到自定义的LinearLayout上。
至此一个简单的内嵌列表组件就自定义实现了,在实际使用中它对于小型内嵌列表的需求工作得非常好,而且还能通过组合玩出一些花样,比如多层嵌套,列表分组展示等。