自定义ItemDecoration实现分组粘性效果(类似通讯录)

在上一篇文章RecyclerView自定义ItemDecoration绘制分割线,简单的实现了通用的分割线,同样,我们可以利用ItemDecoration来实现类似通讯录的分组粘性效果。

一、实现类似通讯录的分组粘性布局,分组布局中只有文字,直接上代码。

/**
 * 类似通讯录的分组粘性布局,分组布局中只有文字
 */
public class SectionDecoration extends RecyclerView.ItemDecoration {
    private DecorationCallback callback;
    private TextPaint textPaint;
    private Paint paint;
    private float stickyHeight;
    private Paint.FontMetrics fontMetrics;

    public SectionDecoration(Context context, DecorationCallback callback){
        this.callback = callback;

        paint = new Paint();
        paint.setColor(context.getResources().getColor(R.color.colorAccent));
        stickyHeight = (int) context.getResources().getDimension(R.dimen.setion_height);

        textPaint = new TextPaint();
        textPaint.setAntiAlias(true);
        textPaint.setTextSize(80);
        textPaint.setTypeface(Typeface.DEFAULT_BOLD);
        textPaint.getFontMetrics(fontMetrics);
        fontMetrics = new Paint.FontMetrics();
        textPaint.setTextAlign(Paint.Align.LEFT);
        textPaint.setColor(Color.WHITE);
    }
    @Override
    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        if (!isLinearAndVertical(parent)){
            return;
        }

        int position = parent.getChildAdapterPosition(view);
        if (isFirstInGroup(position)){
            outRect.top = (int) stickyHeight;
        }
    }

    @Override
    public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDraw(c, parent, state);
        if (!isLinearAndVertical(parent)){
            return;
        }

        float left = parent.getPaddingLeft();
        float right = parent.getWidth() - parent.getPaddingRight();
        int itemCount = parent.getChildCount();
        for(int i=0;i < itemCount;i++){
            View view = parent.getChildAt(i);
            int position = parent.getChildAdapterPosition(view);
            if (isFirstInGroup(position)){
                float top = view.getTop() - stickyHeight;
                float bottom = view.getTop();
                c.drawRect(left,top,right,bottom,paint);
                c.drawText(callback.getGroupFirstChar(position).toUpperCase(),left,bottom,textPaint);
            }
        }
    }

    @Override
    public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
        if (!isLinearAndVertical(parent)){
            return;
        }

        int itemCount = state.getItemCount();//recycle列表的总item数
        int childCount = parent.getChildCount();//recycle控件包好的第一层控件数
        float left = parent.getPaddingLeft();
        float right = parent.getWidth() - parent.getPaddingRight();

        long preGroupId, groupId = -1;
        for (int i = 0; i < childCount; i++) {
            View view = parent.getChildAt(i);
            int position = parent.getChildAdapterPosition(view);

            preGroupId = groupId;
            groupId = callback.getGroupId(position);
            if (groupId < 0 || groupId == preGroupId) continue;//上一个id和当前id相同,跳出循环

            float viewBottom = view.getBottom();
            float textY = Math.max(stickyHeight, view.getTop());
            if (position + 1 < itemCount) {
                long nextGroupId = callback.getGroupId(position + 1);
                if (nextGroupId != groupId && viewBottom < textY ) {//下一个和当前不一样,移动当前
                    textY = viewBottom;//组内最后一个view进入了header
                }
            }
            c.drawRect(left, textY - stickyHeight, right, textY, paint);
            c.drawText(callback.getGroupFirstChar(position).toUpperCase(), left, textY, textPaint);
        }
    }

    /**
     * 判断LayoutManager类型,目前GroupItemDecoration仅支持LinearLayoutManager.VERTICAL
     */
    public boolean isLinearAndVertical(RecyclerView parent){
        RecyclerView.LayoutManager manager = parent.getLayoutManager();
        if (!(manager instanceof LinearLayoutManager)){
            return false;
        }else {
            if (((LinearLayoutManager)manager).getOrientation() != LinearLayoutManager.VERTICAL){
                return false;
            }
        }
        return true;
    }

    /**
     * 是否为每个分组的第一个item
     * @param pos
     * @return
     */
    private boolean isFirstInGroup(int pos){
        if (callback.getGroupId(pos) < 0)return false;

        if (pos == 0){
            return true;
        }else {
            long preGroupId = callback.getGroupId(pos - 1);
            long groupId = callback.getGroupId(pos);
            return groupId != preGroupId;
        }
    }

    public interface DecorationCallback {
        long getGroupId(int position);//得到]组的id
        String getGroupFirstChar(int position);//得到通讯录的首字母
    }
}

二、自定义分组粘性布局,布局中的图片暂时只支持本地图片

     1.自定义布局需要计算分组控件的高度

 if (view.getLayoutParams() == null) {
            view.setLayoutParams(new ViewGroup.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
        }

        int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY);
        int childWidth = ViewGroup.getChildMeasureSpec(widthSpec,
                parent.getPaddingLeft() + parent.getPaddingRight(), view.getLayoutParams().width);

        int childHeight;
        if(view.getLayoutParams().height > 0){
            childHeight = View.MeasureSpec.makeMeasureSpec(view.getLayoutParams().height, View.MeasureSpec.EXACTLY);
        } else {
            childHeight = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);//未指定
        }

        view.measure(childWidth, childHeight);
        view.layout(0,0,view.getMeasuredWidth(),view.getMeasuredHeight());

        groupViewHeight = view.getMeasuredHeight();

2.DecorationCallback接口中的方法,改成

public interface DecorationCallback {
   long getGroupId(int position);//得到分组的id
   void buildGroupView(View groupView, GroupItem groupItem);//构建GroupView,设置组控件的值
  }

3.在getItemOffsets预留相应的高度空间

 int position = parent.getChildAdapterPosition(view);
 if(isFirstInGroup(position) && groupList.get(position) != null){
       measureView(groupView,parent);//绘制View需要先测量View的大小及相应的位置
       //为其预留相应的高度空间
       outRect.top = (int) groupViewHeight;
   }

4.在onDraw中,绘制每个组控件,关键代码如下

int childCount = parent.getChildCount();
for (int i=0;i<childCount;i++){
     View childView = parent.getChildAt(i);
     float left = childView.getLeft();
     float top = childView.getTop();

     int position = parent.getChildAdapterPosition(childView);
     if (groupList.get(position) != null){
           c.save();
           c.translate(left,top - groupViewHeight);//将画布起点移动到之前预留空间的左上角
           drawGroupView(c,parent,position);
          }
    }

5.在onDrawOver中绘悬浮在最上面的头布局,核心代码

int itemCount = state.getItemCount();//recycle列表的总item数
        int childCount = parent.getChildCount();//recycle控件包好的第一层控件数
        float left = parent.getPaddingLeft();

        long preGroupId, groupId = -1;
        for (int i = 0; i < childCount; i++) {
            View view = parent.getChildAt(i);
            int position = parent.getChildAdapterPosition(view);

            preGroupId = groupId;
            groupId = callback.getGroupId(position);
            if (groupId < 0 || groupId == preGroupId) continue;//上一个id和当前id相同,跳出循环

            float viewBottom = view.getBottom();
            float textY = Math.max(groupViewHeight, view.getTop());
            if (position + 1 < itemCount) {
                long nextGroupId = callback.getGroupId(position + 1);
                if (nextGroupId != groupId && viewBottom < textY ) {//下一个和当前不一样,移动当前
                    textY = viewBottom;//组内最后一个view进入了header
                }
            }
            c.save();
            c.translate(left,textY - groupViewHeight);//将画布起点移动到之前预留空间的左上角
            drawGroupView(c,parent,position);
    private void drawGroupView(Canvas c, RecyclerView parent, int position) {
        callback.buildGroupView(groupView,groupList.get(position));
        measureView(groupView,parent);//因为内部控件设置了数据,所以需要重新测量View
        groupView.draw(c);
        c.restore();//重新绘制
    }

效果图晚点再放

写本篇博客的目的在于加深学习记忆,写的不明白的地方请参考RecyclerView之ItemDecoration由浅入深

猜你喜欢

转载自blog.csdn.net/weixin_38617084/article/details/89155883