RecyclerView's ItemDecoration explanation and advanced feature practice

There is no doubt that RecyclerView is one of the most important system components in the Android world now, and its appearance is to replace ListView and GridView efficiently. Its appearance at that time solved a big demand of mine, which was to horizontally load the application list on the TV box interface. Since ListView does not have the function of horizontal loading, and those HorizontalListViews that are open source on the Internet do not meet the demand, so we can only do it ourselves. Define ViewGroup to meet the requirements, but the recycling mechanism is not very perfect, so the performance is not good, so when RecyclerView was born, I embraced it for the first time, and recommended Android development team members to understand it.

But later, I found that RecyclerView is not only easier to use than ListView, but it is more complicated in some places, and it gives more power to developers themselves, such as layout, such as the dividing line of ITEM, such as click monitoring and so on. But in a word, it is a good thing, so we need to spend more time to learn it. In normal development, we can generally meet most of the needs according to the basic usage of RecyclerView, but it is far from enough in some scenarios. For example, we don’t want to be limited to LinearLayoutManager. If we want to define LayoutManager by ourselves, we need to define the effect of the time axis, we want to achieve wonderful add and delete animations, etc. In these cases, we need to have enough understanding of RecyclerView itself to solve the problem.

Today, this article does not talk about the basic knowledge and usage of RecyclerView, but an interesting knowledge point ItemDecoration.

ItemDecoration

The English meaning of Decoration is the meaning of decorations. When it is extended here, it must also be related to the interface decoration of RecyclerView. What we often see is the dividing line.
When we use ListView, we only android:dividerneed to use in the xml file, but unfortunately RecyclerView has no corresponding control.

We create a new project, and then add a RecyclerView to a page. Create the relevant Adapter, load the layout file, the layout file here is very simple, it is a TextView, and then initialize it in the Activity.

public class DividerActivity extends AppCompatActivity {
    
    
    RecyclerView mRecyclerView;
    List<String> data;
    TestAdapter mAdapter;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_divider);
        mRecyclerView = (RecyclerView) findViewById(R.id.divider_recyclerview);
        initDatas();
        mAdapter = new TestAdapter(data);
        mRecyclerView.setAdapter(mAdapter);
        LinearLayoutManager layoutmanager = new LinearLayoutManager(this);
        layoutmanager.setOrientation(LinearLayoutManager.VERTICAL);
        mRecyclerView.setLayoutManager(layoutmanager);
    }

    private void initDatas() {
    
    
        data = new ArrayList<>();
        for (int i = 0; i < 56;i++) {
    
    
            data.add(i+" test ");
        }
    }
}

Write picture description here

It can be seen that all the options are mixed together. For the sake of appearance, a dividing line of 1 px should be needed. Before, I usually set its topMargin or bottomMargin in the Item layout file, so we can modify it in the relevant Adapter.

public TestHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    
    
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_item,parent,false);
        RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) view.getLayoutParams();
        layoutParams.topMargin = 1;
        view.setLayoutParams(layoutParams);
        TestHolder holder = new TestHolder(view);
        return holder;
}

The effect is as follows:

Write picture description here

Now we can also achieve it by adding ItemDecoration to RecyclerView.

First, we need to customize an ItemDecoration. According to the current requirements, we only need to implement one of its methods.

public class TestDividerItemDecoration extends RecyclerView.ItemDecoration {
    
    

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
    
    
        super.getItemOffsets(outRect, view, parent, state);

//        //如果不是第一个,则设置top的值。
        if (parent.getChildAdapterPosition(view) != 0){
    
    
            //这里直接硬编码为1px
            outRect.top = 1;
        }
    }
}

Then add it to RecyclerView in Activity.

mRecyclerView = (RecyclerView) findViewById(R.id.divider_recyclerview);
initDatas();
mAdapter = new TestAdapter(data);
mRecyclerView.setAdapter(mAdapter);
LinearLayoutManager layoutmanager = new LinearLayoutManager(this);
layoutmanager.setOrientation(LinearLayoutManager.VERTICAL);
mRecyclerView.setLayoutManager(layoutmanager);
mRecyclerView.addItemDecoration(new TestDividerItemDecoration());

The effect is as shown in the figure:

Write picture description here

getItemOffsets()

We can see that the custom TestDividerItemDeoration only implements one method getItemOffsets(). There are four parameters in the method.

  • Rect outRect
  • View view
  • RecyclerView parent
  • RecyclerView.State state

What do these four parameters do? We might as well press the Ctrl key and click the method name in AndroidStudio to get to the place where it is called.

Rect getItemDecorInsetsForChild(View child) {
    
    
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    if (!lp.mInsetsDirty) {
    
    
        return lp.mDecorInsets;
    }

    if (mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) {
    
    
        // changed/invalid items should not be updated until they are rebound.
        return lp.mDecorInsets;
    }
    final Rect insets = lp.mDecorInsets;
    insets.set(0, 0, 0, 0);
    final int decorCount = mItemDecorations.size();
    for (int i = 0; i < decorCount; i++) {
    
    
        mTempRect.set(0, 0, 0, 0);
        //在这里被调用
        mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
        insets.left += mTempRect.left;
        insets.top += mTempRect.top;
        insets.right += mTempRect.right;
        insets.bottom += mTempRect.bottom;
    }
    lp.mInsetsDirty = false;
    return insets;
}

We pay attention to this line of code java mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);. It is easy to know that outRect is a Rect with all 0s. view refers to the Item in RecyclerView. parent is RecyclerView itself, and state is a state.

We can see this picture below.

Write picture description here

The green area represents an ItemView in RecyclerView, and the outer orange area is the corresponding outRect, that is, the offset area between ItemView and other components, which is equivalent to the margin attribute, by overriding the getItemOffsets() method, and then specifying top and left in outRect , right, bottom can control the interval in each direction. Note that these properties are offsets, which refer to the values ​​that offset the various directions of the ItemView. In the above example, I set up outRect.top = 1;so that there is a 1 px gap between each ItemView, and this 1 px gap reveals the background color below, so it looks like a divider, which achieves a simple divider effect, but The effect of the dividing line in this method can only depend on the background color. What if I want to customize the color of the dividing line? At this time, we will talk about a new method named onDraw().

onDraw()

In every View in Android, onDraw() is a very important method, which is used to draw the UI effect of the component, so it is naturally used to draw the appearance in ItemDecocration. Let's look at its method declaration.
java public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state);

You can see that it passed a Canvas parameter object, so it has the ability to draw. But how to draw it?

In fact, it is used in conjunction with the previous getItemOffsets method. getItemOffsets opens up the top, bottom, left, and right intervals of the ItemView, and the onDraw method determines the range of the content to be drawn by calculating the coordinate position of each ItemView and its outRect value.

Suppose we want to design a red dividing line with a height of 2 px, then we need to draw a rectangle with a height of 2 px above each ItemView top position, and then fill it with red color.
One thing to note is that getItemOffsets is for each ItemView, while the onDraw method is for the RecyclerView itself, so in the onDraw method, it is necessary to traverse the ItemViews visible on the screen, obtain their position information, and then draw the corresponding dividing lines respectively.

Let's look at the diagram below
Write picture description here

In order to facilitate observation, I made the color of the first dividing line transparent. We can see that the area drawn by each dividing line is actually the area between outRect.top and ItemView.top, so we need to do it in the original getOffsets method. When the position is offset, record the upward interval distance of each itemView, and the subsequent logic is to traverse the View on the screen, and then draw the dividing line.

public class ColorDividerItemDecoration extends RecyclerView.ItemDecoration {
    
    

    private float mDividerHeight;

    private Paint mPaint;

    public ColorDividerItemDecoration() {
    
    
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setColor(Color.RED);
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
    
    
        super.getItemOffsets(outRect, view, parent, state);

//        //第一个ItemView不需要在上面绘制分割线
        if (parent.getChildAdapterPosition(view) != 0){
    
    
            //这里直接硬编码为1px
            outRect.top = 1;
            mDividerHeight = 1;
        }
    }

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

        int childCount = parent.getChildCount();

        for ( int i = 0; i < childCount; i++ ) {
    
    
            View view = parent.getChildAt(i);

            int index = parent.getChildAdapterPosition(view);
            //第一个ItemView不需要绘制
            if ( index == 0 ) {
    
    
                continue;
            }

            float dividerTop = view.getTop() - mDividerHeight;
            float dividerLeft = parent.getPaddingLeft();
            float dividerBottom = view.getTop();
            float dividerRight = parent.getWidth() - parent.getPaddingRight();

            c.drawRect(dividerLeft,dividerTop,dividerRight,dividerBottom,mPaint);
        }
    }
}

Then we add ColorDividerItemDecoration to the corresponding RecyclerView in the Activity.

mRecyclerView.addItemDecoration(new ColorDividerItemDecoration());

The effect is as follows:
Write picture description here

At this point, the red dividing line is done.

But it must be noted that the onDraw method can not only draw simple lines, but it has a Canvas, so drawing circles, rectangles, arcs, and pictures are all easy. In order to improve the technical content of this code, below we use ItemDecoration to realize the effect of a time axis.

Realize the effect of time axis through ItemDecoration

At the beginning of coding, do design first, or think first. Think about what we are going to do, or how we are going to do it.
Write picture description here

We can see that the white pattern on the left is probably the graph we want to draw on the time axis. We use the getItemOffsets method to set the left and top spacing of the ItemView. Then determine the starting coordinates of the axis, the graph or pattern of the middle axis node. We can decompose the corresponding time axis fragments through ItemView, as shown in the figure below.
Write picture description here

It is mainly the determination of some parameters, such as DividerHeight. Note that this DividerHeight does not refer to the upward interval value of ItemView, but the height of the corresponding ItemDecoration. Center coordinates (centerX, centerY), and the starting coordinates of the upper and lower axes. With these parameters in place, we can easily code.

public class TimelineItemDecoration extends RecyclerView.ItemDecoration {
    
    


    private Paint mPaint;
    //ItemView左边的间距
    private float mOffsetLeft;
    //ItemView右边的间距
    private float mOffsetTop;
    //时间轴结点的半径
    private float mNodeRadius;

    public TimelineItemDecoration(Context context) {
    
    
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setColor(Color.RED);

        mOffsetLeft = context.getResources().getDimension(R.dimen.timeline_item_offset_left);
        mNodeRadius = context.getResources().getDimension(R.dimen.timeline_item_node_radius);
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
    
    
        super.getItemOffsets(outRect, view, parent, state);

//        //第一个ItemView不需要在上面绘制分割线
        if (parent.getChildAdapterPosition(view) != 0){
    
    
            //这里直接硬编码为1px
            outRect.top = 1;
            mOffsetTop = 1;
        }

        outRect.left = (int) mOffsetLeft;
    }

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

        int childCount = parent.getChildCount();

        for ( int i = 0; i < childCount; i++ ) {
    
    
            View view = parent.getChildAt(i);

            int index = parent.getChildAdapterPosition(view);

            float dividerTop = view.getTop() - mOffsetTop;
            //第一个ItemView 没有向上方向的间隔
            if ( index == 0 ) {
    
    
                dividerTop = view.getTop();
            }

            float dividerLeft = parent.getPaddingLeft();
            float dividerBottom = view.getBottom();
            float dividerRight = parent.getWidth() - parent.getPaddingRight();

            float centerX = dividerLeft + mOffsetLeft / 2;
            float centerY = dividerTop + (dividerBottom - dividerTop) / 2;

            float upLineTopX = centerX;
            float upLineTopY = dividerTop;
            float upLineBottomX = centerX;
            float upLineBottomY = centerY - mNodeRadius;

            //绘制上半部轴线
            c.drawLine(upLineTopX,upLineTopY,upLineBottomX,upLineBottomY,mPaint);

            //绘制时间轴结点
            c.drawCircle(centerX,centerY,mNodeRadius,mPaint);

            float downLineTopX = centerX;
            float downLineTopY = centerY + mNodeRadius;
            float downLineBottomX = centerX;
            float downLineBottomY = dividerBottom;

            //绘制上半部轴线
            c.drawLine(downLineTopX,downLineTopY,downLineBottomX,downLineBottomY,mPaint);
        }
    }
}

Then the effect is as follows:

Write picture description here
It doesn't feel very beautiful, so we try to change the solid circle of the node into a hollow circle.

//绘制时间轴结点
mPaint.setStyle(Paint.Style.STROKE);
c.drawCircle(centerX,centerY,mNodeRadius,mPaint);
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);

The effect is as follows:
Write picture description here

It feels much more beautiful.

At the same time, we can use icons instead of circles or circles for axis nodes.
Write picture description here

The icon above looks better.

It should be noted that in the onDraw method, ItemDecoration is drawn under ItemView, that is, ItemView may cover the content of ItemDecoration. We can verify it by drawing a complete circle on the boundary between the time axis and ItemView to observe its effect.

     mPaint.setStyle(Paint.Style.STROKE);
    c.drawCircle(view.getLeft(),centerY,mNodeRadius,mPaint);

    mPaint.setStyle(Paint.Style.FILL_AND_STROKE);

Write picture description here
It can be seen that the circle is indeed covered by the content of the ItemView where it overlaps.

You may think, can the content of ItemDecoration be overlaid on the content of ItemView?

The answer is yes, but not in the onDraw() method, but another method onDrawOver().

onDrawOver and corner mark.

Apps or websites in reality often have some rankings such as the following:

Write picture description here
or so.

Write picture description here

These subscripts are all drawn on top of ItemView, and now with ItemDecoration we can implement it easily and elegantly.

For example, we want to implement a book sales ranking list. We have rough sketches.

Write picture description here

Then we can code.
layout file:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="wrap_content"
    android:background="@android:color/white">

    <TextView
        android:id="@+id/tv_rank_oder"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_alignParentLeft="true"
        android:layout_marginLeft="24dp"
        android:gravity="center"
        android:textColor="#6c6c6c"/>

    <ImageView
        android:id="@+id/iv_cover"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:layout_centerVertical="true"
        android:layout_marginLeft="60dp"/>

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/iv_cover"
        android:layout_marginLeft="12dp"
        android:layout_marginTop="12dp"
        android:textColor="@android:color/black"/>
    <TextView
        android:id="@+id/tv_price"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/iv_cover"
        android:layout_below="@id/tv_title"
        android:layout_marginLeft="12dp"
        android:layout_marginTop="12dp"
        android:textColor="@android:color/holo_red_dark"/>
</RelativeLayout>

Corresponding Adapter code:

public class BookRankAdapter extends RecyclerView.Adapter<BookRankAdapter.TestHolder> {
    
    

    List<String> data;
    int[] mIconResouces;
    public BookRankAdapter(List<String> data,int[] ids) {
    
    
        this.data = data;
        this.mIconResouces = ids;
    }

    public void setData(List<String> data,int[] ids) {
    
    
        this.data = data;
        mIconResouces = ids;
        notifyDataSetChanged();
    }

    @Override
    public TestHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    
    
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_ranklist_item,parent,false);
        TestHolder holder = new TestHolder(view);
        return holder;
    }

    @Override
    public void onBindViewHolder(TestHolder holder, int position) {
    
    
        if (data != null && data.size() > 0 ) {
    
    
            String text = data.get(position);

            String[] infos = text.split("-");
            holder.tvOrder.setText(position+"");
            holder.tvTitle.setText(infos[0]);
            holder.tvPrice.setText(infos[1]);

            holder.ivCover.setImageResource(mIconResouces[position]);
        }
    }


    @Override
    public int getItemCount() {
    
    
        return data == null ? 0 : data.size();
    }

    static class TestHolder extends  RecyclerView.ViewHolder{
    
    
        public TextView tvOrder;
        public TextView tvTitle;
        public TextView tvPrice;
        public ImageView ivCover;
        public TestHolder(View itemView) {
    
    
            super(itemView);

            tvOrder = (TextView) itemView.findViewById(R.id.tv_rank_oder);
            tvTitle = (TextView) itemView.findViewById(R.id.tv_title);
            tvPrice = (TextView) itemView.findViewById(R.id.tv_price);
            ivCover = (ImageView) itemView.findViewById(R.id.iv_cover);

        }

    }
}


Custom FlagItemDecoration

public class FlagItemDecoration extends RecyclerView.ItemDecoration {
    
    
    private Paint mPaint;
    private Bitmap mIcon;
    private float mFlagLeft;

    public FlagItemDecoration(Context context) {
    
    
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setColor(Color.RED);


        mIcon = BitmapFactory.decodeResource(context.getResources(),R.drawable.hotsale);
        mFlagLeft = context.getResources().getDimension(R.dimen.flag_left);
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
    
    
      //  super.getItemOffsets(outRect, view, parent, state);

        //第一个ItemView不需要在上面绘制分割线
        if (parent.getChildAdapterPosition(view) == 0){
    
    
            outRect.top = 0;
        } else {
    
    
            outRect.top = 2;
        }

    }

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

        int childCount = parent.getChildCount();

        for ( int i = 0; i < childCount; i++ ) {
    
    
            View view = parent.getChildAt(i);
            int index = parent.getChildAdapterPosition(view);
            float top = view.getTop();
            if ( index < 3 ) {
    
    
                c.drawBitmap(mIcon,mFlagLeft,top,mPaint);
            }

        }
    }
}

Then perform corresponding data processing in the Activity. The data here is for testing, so it is more random.

public class BookRankActivity extends AppCompatActivity {
    
    
    RecyclerView mRecyclerView;
    List<String> data;
    BookRankAdapter mAdapter;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_bookrank);
        mRecyclerView = (RecyclerView) findViewById(R.id.bookrank_recyclerview);


        initDatas();
        int resouces[] = new int[] {
    
    R.drawable.book_renmin,R.drawable.book_huochetou,
            R.drawable.book_jieyouzahuodian,R.drawable.book_tensoflow,R.drawable.book_wangyangming
            ,R.drawable.book_renmin,R.drawable.book_huochetou,
                R.drawable.book_jieyouzahuodian,R.drawable.book_tensoflow,R.drawable.book_wangyangming
            ,R.drawable.book_renmin,R.drawable.book_huochetou,
                R.drawable.book_jieyouzahuodian,R.drawable.book_tensoflow,R.drawable.book_wangyangming
        };
        mAdapter = new BookRankAdapter(data,resouces);
        mRecyclerView.setAdapter(mAdapter);
        LinearLayoutManager layoutmanager = new LinearLayoutManager(this);
        layoutmanager.setOrientation(LinearLayoutManager.VERTICAL);
        mRecyclerView.setLayoutManager(layoutmanager);
        mRecyclerView.addItemDecoration(new FlagItemDecoration(this));
    }

    private void initDatas() {
    
    
        data = new ArrayList<>();
        data.add("人民的名义- ¥ 33.5");
        data.add("火车头 - ¥ 27.5");
        data.add("解忧杂货店- ¥ 19.9");
        data.add("TensorFlow - ¥ 102.5");
        data.add("王阳明心学 - ¥ 60");

        data.add("人民的名义1- ¥ 33.5");
        data.add("火车头1 - ¥ 27.5");
        data.add("解忧杂货店1- ¥ 19.9");
        data.add("TensorFlow1 - ¥ 102.5");
        data.add("王阳明心学1 - ¥ 60");

        data.add("人民的名义2 - ¥ 33.5");
        data.add("火车头2 - ¥ 27.5");
        data.add("解忧杂货店2- ¥ 19.9");
        data.add("TensorFlow2 - ¥ 102.5");
        data.add("王阳明心学2 - ¥ 60");
    }
}

The final effect is as follows:

Write picture description here

Some people are thinking, can such an operation be done through the layout file in ItemView? Yes, it is indeed possible. Define the Flag logo in each ItemView layout file, and then decide whether to load the logo in the Adapter's onBindViewHolder method according to the value of the postion.

But here is to illustrate the onDrawOver method in ItemDecoration, in order to illustrate that it does allow the ItemDecoration image to be drawn on top of the ItemView content. In fact, there are many more benefits of ItemDecoration.

Summarize

Customizing an ItemDecoration usually requires overriding its three methods as needed.
* getItemOffsets expand the space in the four directions of ItemView up, down, left and right
* onDraw draws graphics under ItemView content
* onDrawOver draws graphics above ItemView content.

github address

Reprinted: https://blog.csdn.net/briblue/article/details/70161917

Guess you like

Origin blog.csdn.net/gqg_guan/article/details/132205673