1.简介
由于工作中有这个需求,搜了好多类似的带group indicator的控件,但总是不尽人意,最终决定自定义一个,如果有业务需求变更或者Bug也好修改。
这个是实现的效果,为了能比较明显的看出效果IndicatorView给加了背景半透明。Android App中也有这个效果的实现例子,就是QQ的联系人。
2.思路
最先考虑的是对ExpandableListView进行扩展,给上面附上一层ViewGroup,将groupView的内容通过mAdapter.getGroupView再生成一份View填充到上层ViewGroup中。
但是,ListView的getScrollY()始终返回0;如果使用OnScrollListener中的 onScroll回调获取到的也只是firstVisibleItem、visibleItemCount等值,如果要做到IndicatorView被下一个GroupView推上去的效果,还是需要Y值进行对IndicatorView位移的。
然后考虑到了可以通过获取GroupView.getTop() 并结合ChildView.getTop() GroupView.height ChildView.height 还有position 来综合计算当前的位置,每当View发生变化时,重写onLayout()方法,进行更新这些数据。唉~想想就麻烦死了,何苦自己难为自己。另外多讲一点,如果ExpandableListView使用了ViewHolder,getTop()获取的Y值会很乱,因为有布局的复用,特别是逆向滑动时,获取上一个GroupView的Top值时会得到下方将要出现的GroupView的值。
还是决定自己完全自定义一份。至少对scrollY值,以及各个GroupView的Top值更容易掌握,需求中也是要求点击该Group中最后一条展开或者收缩,收缩也并非所有的childView都收缩,保留几个childView还是显示状态,这更坚定了自定义的想法。
3.实现
基本布局的样子是
首先 IndicatorView extends FrameLayout ,然后在里面铺上ScrollView+竖向的LinearLayout作为GroupView和ChildView的容器,利用FrameLayout的特性,最后添加上一个ViewGroup作为IndicatorView指示器的容器。
写一套IAdapter的接口方法作为将数据导入该空间的适配器:
/**
* IndicatorListViewAdapter 接口
*/
public interface IndicatorListViewExpandAdapter {
View getGroupView(int groupPosition);
View getChildView(int groupPosition, int childPosition);
int getGroupCount();
int getChildCount(int groupPosition);
Object getGroup(int groupPosition);
Object getChild(int groupPosition, int childPosition);
}
然后再定义两个接口,分别是OnGroupViewClick和OnChildViewClick,注意方法参数包含view、groupPosition、childPosition这些,很简单,不再累述
接下来写生成GroupView和ChildView的方法:
private View generateChildView(int groupPosition, int childPosition) {
View ret = mAdapter.getChildView(groupPosition, childPosition);
ret.setTag(Group_Position, groupPosition);
ret.setTag(View_Type, Child_View);
ret.setTag(Child_Position,childPosition);
switch (currentShowType) {
case Only_Group:
ret.setVisibility(View.GONE);
break;
case Show_All:
break;
case Show_More:
if (childPosition >= moreNumber) {
ret.setVisibility(View.GONE);
}
break;
}
if(mChildClickListener!=null){
ret.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
int gPosition = (int) v.getTag(Group_Position);
int cPosition = (int) v.getTag(Child_Position);
mChildClickListener.onChildClick(v,gPosition,cPosition);
}
});
}
return ret;
}
其中switch内是我的特殊业务需求,忽略掉;Group_Position和Child_Position是放到Tag中的常量key,用来取该View相应position信息用的。GroupView的生成方法基本类似。
然后就是实现一个根据数据填充布局的方法,实现效果想类似于Adapter的notifyDataSetChanged(),由于时间比较紧,先简单实现一下,removeAllViews()清掉所有的View再重新填充,不过好点的实现方案应该使用观察者模式DataSetObserver的onChanged()和onInvalidated()方法实现的好,之后有时间再优化。
填充View的方法:
public void notifyDataSetChanged() {
mGroupViews.clear();
mContainer.removeAllViews();
for (int i = 0; i < mAdapter.getGroupCount(); i++) {
View groupView = generateGroupView(i);
mGroupViews.add(groupView);
mContainer.addView(groupView);
for (int j = 0; j < mAdapter.getChildCount(i); j++) {
View childView = generateChildView(i, j);
mContainer.addView(childView);
}
}
}
给ScrollView添加一下OnScrollListener监听,随时获取scrollY并更新Indicator的位置,
核心点来了:
if (containerY > lastScrollY) {// 向下
for (int i = 0; i < mGroupViews.size(); i++) {
if ((containerY >= mGroupViews.get(i).getTop() && (containerY <= mGroupViews.get(i).getBottom() - 10))) {
View v = mIndicatorView.getChildAt(0);
if (v != null && (i == (int) v.getTag(Group_Position))) {
break;
} else {
mIndicatorView.removeAllViews();
View indicatorView = generateGroupView(i);
indicatorView.setTag(Group_Position, i);
mIndicatorView.addView(indicatorView);
indicatorHeight = mGroupViews.get(i).getMeasuredHeight();
indicatorRight = mGroupViews.get(i).getMeasuredWidth();
indicatorBottom = indicatorHeight;
}
break;
}
}
for (int i = 0; i < mGroupViews.size(); i++) {
if (mGroupViews.get(i).getTop() - containerY > 0 && containerY - mGroupViews.get(i).getTop() + indicatorHeight > 0) {
mIndicatorView.layout(0, - (indicatorHeight - (mGroupViews.get(i).getTop() - containerY)), indicatorRight, indicatorBottom - (indicatorHeight - (mGroupViews.get(i).getTop() - containerY)));
break;
}
}
}
判断一下containerY(也就是scrollY),和前一次的lastScrollY比较一下,如果大于方向是向下,如果小于则是向上,一定要把等于的情况滤掉,不然会多出好多不必要的调用会导致indicator反复绘制闪现。
向上的方法基本一致,注意一下position 是 i-1,因为向上Indicator要展示的是前一个group的布局;不过此处有一个坑,向上时一定要先layout再addView到IndicatorView容器里,否则将没有视图,造成的原因应该是刚addview没draw完成就重新执行layout给新值,导致draw不执行,特别是在上下groupView切换的临界点尤为明显。另外可能也和View的绘制方式是由左上向右下绘制的,向下滑动时显示没问题,向上滑动时显示就有问题。View实际上是一帧一帧不断重绘实现的流畅的画面。
4.结语
![大笑](http://static.blog.csdn.net/xheditor/xheditor_emot/default/laugh.gif)