自定义IndicatorListView——带Group指示器的ListView

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.结语

源码等忙完稍作整理会放到GitHub上,有人说有OnScrollChangedListener监听可以监听scrollY和lastScrollY,但是这是API 23的新方法,我们需要兼容到API 14,所以并没法采用。文内如果有更好的替代方案,欢迎交流,尊重我的劳动成果,转载请告知并写明 大笑

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值