RecyclerView初探索(一)、ItemDecoration使用

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/wanggang514260663/article/details/85261668


相信大家对于 RecyclerView现在使用应该相当普遍了吧,它作为 ListView的有力替代品,有着很好的扩展性。

我们在ListView中增加分割线的时候,会只用android:divider,而在RecyclerView中是没有这个属性的。代替品是ItemDecoration,当然ItemDecoration并不是只可以作为分割线使用的。网上已经有很多优秀的使用了,这里我列出来几个github上的。

制作角标: https://blog.csdn.net/yu_duan_hun/article/details/78961155
时间线效果: https://github.com/vivian8725118/TimeLine
在这里插入图片描述
各种样式的分割线: https://github.com/yqritc/RecyclerView-FlexibleDivider
在这里插入图片描述

1、ItemDecoration的基本使用

mMainAdapter = new MainAdapter(this, entities);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.addItemDecoration(new StickyItemDecoration.Builder(this).create(entities));
recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
recyclerView.setAdapter(mMainAdapter);

可以看到在设置addItemDecoration时候,是可以设置多个ItemDecoration.的,绘制顺序是从按照插入顺序绘制。

2、如何自定义ItemDecoration

public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) ;
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) ;
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) ;

在自定义ItemDecoration的时候,主要可以使用到下面三个方法。

  • getItemOffsets
    该方法中又一个outRect表示当前View的边界值,当我们设置他的上下左右边界时,会加入一个类似margin的操作,将布局撑开。下面是一个从别人博客那过来的图。
    在这里插入图片描述
    默认情况下outRect的上下左右值都是0,当outRect值变化时,会相对应的撑开布局。

  • onDraw
    这个方法实际上就是真实的绘制操作了,可以用canvas进行绘制,在getItemOffsets中,对布局撑开处理了之后,就可以在撑开的部分进行绘制。该操作的绘制是和itemview在同一层。

  • onDrawOver
    这个方法也是用来绘制的,但是它的绘制是在itemview的上层,体现效果就是会覆盖在itemview的上层。

4、系统自带的DividerItemDecoration源码分析

public class DividerItemDecoration extends ItemDecoration {
    public static final int HORIZONTAL = 0;
    public static final int VERTICAL = 1;
    private static final String TAG = "DividerItem";
    private static final int[] ATTRS = new int[]{16843284};
    private Drawable mDivider;
    private int mOrientation;
    private final Rect mBounds = new Rect();

    public DividerItemDecoration(Context context, int orientation) {
    	//获取的是主题中的@android:attr/listDivider属性
        TypedArray a = context.obtainStyledAttributes(ATTRS);
        this.mDivider = a.getDrawable(0);
        if (this.mDivider == null) {
            Log.w("DividerItem", "@android:attr/listDivider was not set in the theme used for this DividerItemDecoration. Please set that attribute all call setDrawable()");
        }
        a.recycle();
        //设置传入的绘制方向
        this.setOrientation(orientation);
    }

    public void setOrientation(int orientation) {
        if (orientation != 0 && orientation != 1) {
            throw new IllegalArgumentException("Invalid orientation. It should be either HORIZONTAL or VERTICAL");
        } else {
            this.mOrientation = orientation;
        }
    }

    public void setDrawable(@NonNull Drawable drawable) {
        if (drawable == null) {
            throw new IllegalArgumentException("Drawable cannot be null.");
        } else {
            this.mDivider = drawable;
        }
    }

    public void onDraw(Canvas c, RecyclerView parent, State state) {
        if (parent.getLayoutManager() != null && this.mDivider != null) {
            //根据传入的方向进行区分绘制
            if (this.mOrientation == 1) {
                this.drawVertical(c, parent);
            } else {
                this.drawHorizontal(c, parent);
            }
        }
    }

    private void drawVertical(Canvas canvas, RecyclerView parent) {
        canvas.save();
        int left;
        int right;
        if (parent.getClipToPadding()) {
            left = parent.getPaddingLeft();
            right = parent.getWidth() - parent.getPaddingRight();
            canvas.clipRect(left, parent.getPaddingTop(), right, parent.getHeight() - parent.getPaddingBottom());
        } else {
            left = 0;
            right = parent.getWidth();
        }
        int childCount = parent.getChildCount();
        for(int i = 0; i < childCount; ++i) {
            View child = parent.getChildAt(i);
            parent.getDecoratedBoundsWithMargins(child, this.mBounds);
            int bottom = this.mBounds.bottom + Math.round(child.getTranslationY());
            int top = bottom - this.mDivider.getIntrinsicHeight();
            //绘制操作,由于是使用的Drawable所以可以使用shape,也可以使用图片
            this.mDivider.setBounds(left, top, right, bottom);
            this.mDivider.draw(canvas);
        }
        canvas.restore();
    }

    private void drawHorizontal(Canvas canvas, RecyclerView parent) {
        canvas.save();
        int top;
        int bottom;
        if (parent.getClipToPadding()) {
            top = parent.getPaddingTop();
            bottom = parent.getHeight() - parent.getPaddingBottom();
            canvas.clipRect(parent.getPaddingLeft(), top, parent.getWidth() - parent.getPaddingRight(), bottom);
        } else {
            top = 0;
            bottom = parent.getHeight();
        }
        int childCount = parent.getChildCount();
        for(int i = 0; i < childCount; ++i) {
            View child = parent.getChildAt(i);
            parent.getLayoutManager().getDecoratedBoundsWithMargins(child, this.mBounds);
            int right = this.mBounds.right + Math.round(child.getTranslationX());
            int left = right - this.mDivider.getIntrinsicWidth();
            this.mDivider.setBounds(left, top, right, bottom);
            this.mDivider.draw(canvas);
        }
        canvas.restore();
    }

    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
        if (this.mDivider == null) {
            outRect.set(0, 0, 0, 0);
        } else {
            if (this.mOrientation == 1) {
                outRect.set(0, 0, 0, this.mDivider.getIntrinsicHeight());
            } else {
                outRect.set(0, 0, this.mDivider.getIntrinsicWidth(), 0);
            }
        }
    }
}

可以参考洋神的文章。https://blog.csdn.net/lmj623565791/article/details/45059587

3、实现一个带有顶部悬停效果的ItemDecoration

在这里插入图片描述

3-1、为RecyclerView插入字母分割条

插入分割线,从上面的分析需要做两部操作,撑开布局(getItemOffset)和在撑开的布局中绘制(onDraw)

  • getItemOffset
@Override
    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
    //获取当前view在整个列表中的位置
   int position = parent.getChildAdapterPosition(view);
    //如果没有填充数据,直接跳过
    if (groupEntities == null || groupEntities.size() == 0) return;
    //第一个元素,直接需要绘制,给一个高度撑开
    if (position == 0) {
        outRect.set(0, mStickyHeight, 0, 0);
    } else {
        //如果两个view的实体类key值不同,则表示两个实体是不同组的,则添加分组的标签上去
        if (null != groupEntities.get(position).getKey()
                && !groupEntities.get(position).getKey().equals(groupEntities.get(position - 1).getKey())) {
            outRect.set(0, mStickyHeight, 0, 0);
        }
    }
}
  • onDraw
@Override
public void onDraw(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
    super.onDraw(canvas, parent, state);
    //如果没有数据,直接跳过绘制
    if (groupEntities == null || groupEntities.size() == 0) return;
    //获取当前屏幕展示总的item数量
    int childCount = parent.getChildCount();
    for (int i = 0; i < childCount; i++) {
        View child = parent.getChildAt(i);
        //根据当前的itemview,获取这个item在整个布局列表中的位置
        RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams();
        int position = layoutParams.getViewLayoutPosition();
	//计算出来绘制的悬停标签的文字大小
        mTextPaint.getTextBounds(groupEntities.get(position).getKey(), 0, groupEntities.get(position).getKey().length(), textRect);
        if (position == 0) {
           //如果是第一个元素,无论如何都绘制一个标签上去
            canvas.drawRect(new Rect(0, 0, child.getRight(), mStickyHeight), mPaint);
            canvas.drawText(groupEntities.get(position).getKey(), mStickyLeftPadding, (mStickyHeight + textRect.height()) / 2, mTextPaint);
        } else {
            //如果元素是不同的分组,就在他们之间绘制一个标签
            if (groupEntities.get(position).getKey() != null
                    && !groupEntities.get(position).getKey().equals(groupEntities.get(position - 1).getKey())) {
                canvas.drawRect(new Rect(0, child.getTop() - mStickyHeight, child.getRight(), child.getTop()), mPaint);
                canvas.drawText(groupEntities.get(position).getKey(), 
                mStickyLeftPadding, child.getTop() - mStickyHeight / 2 + textRect.height() / 2, mTextPaint);
            }
        }
    }
}

到这里,就实现了如下操作,当前是不能悬停的。

3-2、实现悬停效果

在看代码之前,先用图来分析下,悬停效果的过程。

可以看到在滚动的时候,组内的最后一个元素的getTop是一个不断缩小的过程,当getTop变为0的时候,最后一个布局元素就刚好是屏幕展示的第一个元素了,再缩小的话,getTop就变为小于0的值,而当布局的高度-getTop=悬停布局高度的时候,刚好这里的B和C布局应该就是紧挨着的了。在缩小的话,就会C占据了B的位置。为了产生一个挤压的效果,在展示上面,B被C挤上去,所以使用canvas的平移,产生这个效果。好了分析完了,代码如下,很简单。

@Override
public void onDrawOver(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
    super.onDrawOver(canvas, parent, state);
    //没有数据填充,跳过绘制
    if (groupEntities == null || groupEntities.size() == 0) return;
    //获取屏幕上第一个元素在整个布局中的位置
    int firstVisiblePosition = ((LinearLayoutManager) parent.getLayoutManager()).findFirstVisibleItemPosition();
    //根据位置获取到这个元素对象
    View firstVisibleChild = parent.findViewHolderForAdapterPosition(firstVisiblePosition).itemView;
     //如果当前布局的两个实体不是同一个组,并且第一个布局展示出来的高度小于悬停view的高度,将canvas平移,实现出来挤压移动效果	 
    if (groupEntities.get(firstVisiblePosition).getKey() != null
            && !groupEntities.get(firstVisiblePosition).getKey().equals(groupEntities.get(firstVisiblePosition + 1).getKey())
            && firstVisibleChild.getTop() + firstVisibleChild.getHeight() < mStickyHeight) {
	//getTop在移动的时候,是一个从正---->负的过程	    
        canvas.translate(0, firstVisibleChild.getTop() + firstVisibleChild.getHeight() - mStickyHeight);
    }
   //获取文字大小
    mTextPaint.getTextBounds(groupEntities.get(firstVisiblePosition).getKey(), 0, groupEntities.get(firstVisiblePosition).getKey().length(), textRect);
   //绘制标签布局
    canvas.drawRect(new Rect(0, 0, firstVisibleChild.getRight(), mStickyHeight), mPaint);
    canvas.drawText(groupEntities.get(firstVisiblePosition).getKey(), mStickyLeftPadding, mStickyHeight / 2 + textRect.height() / 2, mTextPaint);
}
3-3、完整代码
public class StickyItemDecoration extends RecyclerView.ItemDecoration {

    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
    private Paint mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);

    private int mStickyLeftPadding;
    private int mStickyHeight;
    private List<GroupEntity> groupEntities;
    private Rect textRect = new Rect();

    public static class Builder {

        private int stickyLeftPadding = -1;
        private int stickyHeight;
        private int stickyFontSize;
        private int stickyFontColor;
        private int stickyBackgroundColor;

        private Context mContext;

        public Builder(Context context) {
            this.mContext = context;
        }

        public Builder setStickyLeftPadding(int stickyLeftPadding) {
            this.stickyLeftPadding = stickyLeftPadding;
            return this;
        }

        public Builder setSstickyHeight(int stickyHeight) {
            this.stickyHeight = stickyHeight;
            return this;
        }

        public Builder setSstickyFontSize(int stickyFontSize) {
            this.stickyFontSize = stickyFontSize;
            return this;
        }

        public Builder setSstickyFontColor(int stickyFontColor) {
            this.stickyFontColor = stickyFontColor;
            return this;
        }

        public Builder setSstickyBackgroundColor(int stickyBackgroundColor) {
            this.stickyBackgroundColor = stickyBackgroundColor;
            return this;
        }

        public StickyItemDecoration create(@NonNull List<GroupEntity> groupEntities) {
            if (stickyLeftPadding == -1) {
                stickyLeftPadding = dip2px(12);
            }
            if (stickyHeight == 0) {
                stickyHeight = dip2px(44);
            }
            if (stickyFontSize == 0) {
                stickyFontSize = sp2px(18);
            }
            if (stickyFontColor == 0) {
                stickyFontColor = Color.parseColor("#333333");
            }
            if (stickyBackgroundColor == 0) {
                stickyBackgroundColor = Color.parseColor("#e6dedd");
            }
            return new StickyItemDecoration(stickyLeftPadding, stickyHeight, 
            stickyFontSize, stickyFontColor, stickyBackgroundColor, groupEntities);
        }

        private int dip2px(int dipValue) {
            return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                    dipValue, mContext.getResources().getDisplayMetrics());
        }

        private int sp2px(int spValue) {
            return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
                    spValue, mContext.getResources().getDisplayMetrics());
        }
    }

    protected StickyItemDecoration(int stickyLeftPadding, int stickyHeight,
     int stickyFontSize, int stickyFontColor,
                                   int stickyBackgroundColor, List<GroupEntity> groupEntityList) {

        mPaint.setColor(stickyBackgroundColor);
        mPaint.setStyle(Paint.Style.FILL);

        this.mStickyHeight = stickyHeight;
        this.mStickyLeftPadding = stickyLeftPadding;
        this.groupEntities = groupEntityList;

        mTextPaint.setColor(stickyFontColor);
        mTextPaint.setStyle(Paint.Style.STROKE);
        mTextPaint.setTextSize(stickyFontSize);
    }

    @Override
    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, 
    @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        int position = parent.getChildAdapterPosition(view);

        if (groupEntities == null || groupEntities.size() == 0) return;

        if (position == 0) {
            outRect.set(0, mStickyHeight, 0, 0);
        } else {
            if (null != groupEntities.get(position).getKey()
                    && !groupEntities.get(position).getKey().
                    equals(groupEntities.get(position - 1).getKey())) {
                outRect.set(0, mStickyHeight, 0, 0);
            }
        }
    }

    @Override
    public void onDraw(@NonNull Canvas canvas, @NonNull RecyclerView parent, 
    @NonNull RecyclerView.State state) {
        super.onDraw(canvas, parent, state);

        if (groupEntities == null || groupEntities.size() == 0) return;

        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = parent.getChildAt(i);
            RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams();
            int position = layoutParams.getViewLayoutPosition();

            mTextPaint.getTextBounds(groupEntities.get(position).getKey(), 0, 
            groupEntities.get(position).getKey().length(), textRect);

            if (position == 0) {
                canvas.drawRect(new Rect(0, 0, child.getRight(), mStickyHeight), mPaint);
                canvas.drawText(groupEntities.get(position).getKey(), 
                mStickyLeftPadding, (mStickyHeight + textRect.height()) / 2, mTextPaint);
            } else {
                if (groupEntities.get(position).getKey() != null
                        && !groupEntities.get(position).getKey()
                        .equals(groupEntities.get(position - 1).getKey())) {

                    canvas.drawRect(new Rect(0, child.getTop() - mStickyHeight, 
                    child.getRight(), child.getTop()), mPaint);
                    canvas.drawText(groupEntities.get(position).getKey(), 
                    mStickyLeftPadding, child.getTop() - mStickyHeight / 2 + textRect.height() / 2, mTextPaint);
                }
            }
        }
    }

    @Override
    public void onDrawOver(@NonNull Canvas canvas, 
    @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDrawOver(canvas, parent, state);

        if (groupEntities == null || groupEntities.size() == 0) return;

        int firstVisiblePosition = ((LinearLayoutManager) parent.getLayoutManager())
        .findFirstVisibleItemPosition();

        View firstVisibleChild = parent.findViewHolderForAdapterPosition(firstVisiblePosition).itemView;

        if (groupEntities.get(firstVisiblePosition).getKey() != null
                && !groupEntities.get(firstVisiblePosition).getKey()
                .equals(groupEntities.get(firstVisiblePosition + 1).getKey())
                && firstVisibleChild.getTop() + firstVisibleChild.getHeight() < mStickyHeight) {
            canvas.translate(0, firstVisibleChild.getTop() 
            + firstVisibleChild.getHeight() - mStickyHeight);
        }

        mTextPaint.getTextBounds(groupEntities.get(firstVisiblePosition).getKey(),
         0, groupEntities.get(firstVisiblePosition).getKey().length(), textRect);

        canvas.drawRect(new Rect(0, 0, firstVisibleChild.getRight(), mStickyHeight), mPaint);
        canvas.drawText(groupEntities.get(firstVisiblePosition).getKey(), 
        mStickyLeftPadding, mStickyHeight / 2 + textRect.height() / 2, mTextPaint);
    }

    public static class GroupEntity {
        private String key;
        private String value;

        public GroupEntity(String key, String value) {
            this.key = key;
            this.value = value;
        }

        public String getKey() {
            return key;
        }

        public void setKey(String key) {
            this.key = key;
        }

        public String getValue() {
            return value;
        }

        public void setValue(String value) {
            this.value = value;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            GroupEntity that = (GroupEntity) o;

            return key != null ? key.equals(that.key) : that.key == null;
        }

        @Override
        public int hashCode() {
            return key != null ? key.hashCode() : 0;
        }
    }
}

使用很简答,可以直接使用链式操作

new StickyItemDecoration.Builder(this).create(entities)

可以使用的方法有

//设置文字距离左边距离
public Builder setStickyLeftPadding(int stickyLeftPadding) {
            this.stickyLeftPadding = stickyLeftPadding;
            return this;
}

//设置悬停布局高度
public Builder setSstickyHeight(int stickyHeight) {
    this.stickyHeight = stickyHeight;
    return this;
}

//设置文字大小
public Builder setSstickyFontSize(int stickyFontSize) {
    this.stickyFontSize = stickyFontSize;
    return this;
}

//设置文字颜色
public Builder setSstickyFontColor(int stickyFontColor) {
    this.stickyFontColor = stickyFontColor;
    return this;
}

//设置悬停布局背景颜色
public Builder setSstickyBackgroundColor(int stickyBackgroundColor) {
    this.stickyBackgroundColor = stickyBackgroundColor;
    return this;
}

猜你喜欢

转载自blog.csdn.net/wanggang514260663/article/details/85261668