分组悬停列表-1:简单的Text

Tags
#Android

一、效果图

notion image
网上关于实现带悬停分组头部的列表的方法有很多:
  1. 有人用自定义ExpandListView实现
  1. 有人用一个额外的父布局里面套 RecyclerView/ListView+一个头部View(位置固定在父布局上方)实现的。
对于以上解决方案,有以下几点个人觉得不好的地方:
  1. 现在RecyclerView是主流,ExpandListView很少有人用了
  1. 在RecyclerView外套一个父布局总归是增加布局层级,容易overdraw,显得不够优雅。
  1. item布局实现带这种分类头部的方法有两种,
一种是把分类头部当做一种itemViewtype(麻烦), 另一种是每个Item布局都包含了分类头部的布局,代码里根据postion等信息动态Visible,Gone头部(布局冗余,item效率降低)。
况且Google为我们提供了ItemDecoration,它本身就是用来修饰RecyclerView里的Item的,它的getItemOffsets() 方法用于为Item分类头部留出空间和绘制(解决缺点3),它的onDraw()、onDrawOver()方法用于绘制滚动以及悬停的头部View(解决缺点2)。
本文就利用ItemDecoration 打造分组悬停列表,你会发现,使用ItemDecoration之后,分组悬停列表是如此简单的一件事了。

二、实现代码:

思路分析:
  1. 继承ItemDecoration,重写三个方法:getItemOffsets、onDraw、onDrawOver
  1. getItemOffsets是给Item添加额外的Padding,以便有地方显示Divider
  1. onDraw是绘制在Item的下面的,可能会被Item覆盖
  1. onDrawOver是绘制在Item的上面的,可能会覆盖掉Item
  1. 实现分组悬停效果,就是在onDraw中绘制跟随滚动的Tag,在onDrawOver中绘制悬停在第一项那里的Tag
public class PinnedDivider extends RecyclerView.ItemDecoration {

    private Paint paint;//画笔
    private Rect rect = new Rect();//用于存放测量文字Rect
    private Drawable divider;//分割线颜色

    private Builder builder;

    private PinnedDivider(Builder builder) {
        this.builder = builder;
        this.paint = new Paint();
        this.paint.setAntiAlias(true);
        this.paint.setTextSize(builder.tagTextSize);
        this.divider = new ColorDrawable(builder.dividerColor);
    }

    /**
     * 设置分组悬停视图的显示区域
     */
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
        int position = params.getViewLayoutPosition() - builder.headerCount;

        //防止越界
        if (position > builder.data.size() - 1 || position < 0) {
            return;
        }
        //第1项肯定要有tag
        if (position == 0) {
            outRect.set(0, builder.tagHeight, 0, 0);
        }
        //其余项,不为空且跟前一个tag不一样了,说明是新的分类,也要tag
        else if (!builder.data.get(position).getPinnedTag().equals(builder.data.get(position - 1).getPinnedTag())) {
            outRect.set(0, builder.tagHeight, 0, 0);
        }
        //和下一项一样的,都需要分割线
        for (int i = 0; i < builder.data.size() - 1; i++) {
            String tag1 = builder.data.get(i).getPinnedTag();
            String tag2 = builder.data.get(i + 1).getPinnedTag();
            if (tag1.equals(tag2)) {
                int top = outRect.top;
                outRect.set(0, top, 0, builder.dividerHeight);
            }
        }
    }

    /**
     * 绘制最底层
     *
     * @param c
     * @param parent
     * @param state
     */
    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        int childCount = parent.getChildCount();

        //绘制随着滚动的分组视图
        for (int i = 0; i < childCount; i++) {
            View child = parent.getChildAt(i)

三、使用方式

public class MainActivity extends AppCompatActivity {

    private SingleAdapter<Bean> adapter;
    private RecyclerView rv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
        initData();
    }

    private void initView() {
        rv = (RecyclerView) findViewById(R.id.rv);
        rv.setLayoutManager(new LinearLayoutManager(this));
        adapter = new SingleAdapter<Bean>(this, R.layout.item_tv) {
            @Override
            protected void bindData(BaseViewHolder holder, Bean item, int position) {
                TextView tv = holder.getView(R.id.tv);
                tv.setText(item.getCity());
            }
        };
        rv.setAdapter(adapter);
    }

    private void initData() {
        List<Bean> list = new ArrayList<>();
        list.add(new Bean("A", "安达"));
        list.add(new Bean("A", "安化"));
        list.add(new Bean("A", "安康"));
        list.add(new Bean("A", "安陆"));

        list.add(new Bean("B", "包头"));
        list.add(new Bean("B", "保山"));
        list.add(new Bean("B", "宝兴"));
        list.add(new Bean("B", "北京"));
        list.add(new Bean("B", "本溪"));
        list.add(new Bean("B", "宾阳"));

        list.add(new Bean("C", "茶陵"));
        list.add(new Bean("C", "朝阳"));
        list.add(new Bean("C", "昌黎"));
        list.add(new Bean("C", "常德"));
        list.add(new Bean("C", "常州"));
        list.add(new Bean("C", "郴州"));
        list.add(new Bean("C", "成都"));
        list.add(new Bean("C", "承德"));
        list.add(new Bean("C", "赤壁"));
        list.add(new Bean("C", "崇阳"));
        list.add(new Bean("C", "滁州"));
        list.add(new Bean("C", "长春"));
        list.add(new Bean("C", "长春"));
        list.add(new Bean("C", "长春"));
        list.add(new Bean("C", "长春"));
        list.add(new Bean("C", "长春"));
        list.add(new Bean("C", "长春"));
        //使用建造者模式,创建悬停的Divider
        PinnedDivider p
这里的Bean需要实现一个接口:
public class Bean implements Pinnable {

    private String tag;
    private String city;

    public Bean(String tag, String city) {
        this.tag = tag;
        this.city = city;
    }

    public String getCity() {
        return city;
    }

    @Override
    public boolean isPanned() {
        return true;
    }

    @Override
    public String getPinnedTag() {
        return tag;
    }
}
接口如下:
public interface Pinnable {

    //是否需要显示悬停title
    boolean isPanned();

    //悬停的tag
    String getPinnedTag();
}
最后奉上源码:Github

参考目录

  1. Android 仿微信通讯录 导航分组列表
  1. 深入理解 RecyclerView 系列之一:ItemDecoration
  1. 深入浅出 RecyclerView – 张涛

© fishyer 2022