Copyright notice: This article is the blogger’s original article, shall not be reproduced without the permission of the blogger

Android Development from Scratch series

Source code: AnliaLee/ Android-Recyclerviews, welcome star

If you see any mistakes or have any good suggestions, please leave a comment

preface

In order to achieve the effect of list grouping and sticky head in the recent project, I searched a lot of materials and open source libraries on the Internet, but felt that none of them were very easy to use, some were not strong in scalability and some were too complicated to use, so I decided to build the wheel by myself. Before action, many predecessors studied the source code, decided a few development directions

  • Make it as user-friendly as possible, reducing the amount of code called and coupling with other classes
  • Use RecyclerView to achieve the list function
  • Custom RecyclerView. ItemDecoration draw grouped Item and viscous head
  • View the layout in the layout as a layoutinflater.inflate and pass it in as an ItemDecoration to draw.
  • Provide interfaces in ItemDecoration for users to group list data and set the display content of group items

At present, the first stage of GroupItemDecoration has been developed (it will continue to update and expand functions). The source code and examples have been uploaded to Github, and the specific effect is shown in the figure below


Introduction to GroupItemDecoration

GroupItemDecoration currently only supports LinearLayoutManager. VERTICAL type, using the process is as follows

  • Copy the files from package com.anlia.library.group into your project
  • Inflate your layout
LayoutInflater layoutInflater = LayoutInflater.from(this);
View groupView = layoutInflater.inflate(R.layout.item_group,null);
Copy the code
  • Call recyclerView. Add GroupItemDecoration addItemDecoration
recyclerView.addItemDecoration(new GroupItemDecoration(this,groupView,new GroupItemDecoration.DecorationCallback() {
	@Override
	public void setGroup(List<GroupItem> groupList) {
		// Set GroupItem(int startPosition), for example:
		GroupItem groupItem = new GroupItem(0);
		groupItem.setData("name"."Group 1");
		groupList.add(groupItem);

		groupItem = new GroupItem(5);
		groupItem.setData("name"."Group 2");
		groupList.add(groupItem);
	}

	@Override
	public void buildGroupView(View groupView, GroupItem groupItem) {
		FindViewById = groupView.findViewById = groupView.findViewById = groupView.findViewById = groupView.findViewById
		TextView textName = (TextView) groupView.findViewById(R.id.text_name);
		textName.setText(groupItem.getData("name").toString()); }}));Copy the code

If you still don’t know, you can go to the demo


Implementation approach

Before we customize ItemDecoration, we should first understand the use of ItemDecoration. If you are not clear, please check these two blogs

RecyclerView ItemDecoration from simple to deep

Deep understanding of RecyclerView series one: ItemDecoration

In short, we achieved grouping and sticky head effects in three steps

  1. Rewrite ItemDecoration. GetItemOffsets in RecyclerView GroupView placeholder
  2. Rewrite ItemDecoration.onDraw to draw the GroupView in the position reserved in the previous step
  3. Rewrite ItemDecoration. OnDrawOver drawing top hover GroupView (viscous head)

First, create a GroupItemDecoration inherited from ItemDecoration, obtain the GroupView set by the user in the initialization method, and provide an interface for the user to set group correlation

public class GroupItemDecoration extends RecyclerView.ItemDecoration {
	private Context context;
    private View groupView;
    private DecorationCallback decorationCallback;

    public GroupItemDecoration(Context context,View groupView,DecorationCallback decorationCallback) {
        this.context = context;
        this.groupView = groupView;
        this.decorationCallback = decorationCallback;
    }

    public interface DecorationCallback {
        /** * Set group *@param groupList
         */
        void setGroup(List<GroupItem> groupList);

        /** * Build GroupView *@param groupView
         * @param groupItem
         */
        void buildGroupView(View groupView, GroupItem groupItem); }}Copy the code

Then rewrite the getItemOffsets method to reserve positions for the GroupView according to the groups set by the user. The most important thing is to measure the width, height and position of the GroupView. MeasureView calls View.measure and View.layout in the order in which a View is drawn. For information on how to measure a View, check out this blog post on how Android gets the width and height of the loaded layout during initialization. Next is the concrete implementation code

public class GroupItemDecoration extends RecyclerView.ItemDecoration {
    // omit some code...
    private List<GroupItem> groupList = new ArrayList<>();// List of groups set by the user
    private Map<Object,GroupItem> groups = new HashMap<>();// Save the relationship between startPosition and group objects
    private int[] groupPositions;// Saves an array of group startPosition
    private int positionIndex;// Group the index of startPosition in groupPositions
	
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        if(! isLinearAndVertical(parent)){/ / if not LinearLayoutManager RecyclerView type VERTICAL, jump out of the (the same below)
            return;
        }

        if(isFirst){
            measureView(groupView,parent);// To draw a View, measure the size and position of the View
            decorationCallback.setGroup(groupList);// Get the list of groups set by the user
            if(groupList.size()==0) {// If the user does not set the group, jump (same below)
                return;
            }
            groupPositions = new int[groupList.size()];
            positionIndex = 0;

            int a = 0;
            for(int i=0; i<groupList.size(); i++){// Save the mapping between groupItem and startPosition
                int p = groupList.get(i).getStartPosition();
                if(groups.get(p)==null){
                    groups.put(p,groupList.get(i));
                    groupPositions[a] = p;
                    a++;
                }
            }
            isFirst = false;
        }

        int position = parent.getChildAdapterPosition(view);
        if(groups.get(position)! =null) {// If groupView needs to be drawn before childView corresponding to this position in RecyclerView, reserve corresponding height space for itoutRect.top = groupViewHeight; }}/** * Measure the size and position of the View *@param view
     * @param parent
     */
    private void measureView(View view,View parent){
        if (view.getLayoutParams() == null) {
            view.setLayoutParams(new ViewGroup.LayoutParams(
                    ViewGroup.LayoutParams.WRAP_CONTENT, 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);/ / is not specified
        }

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

        groupViewHeight = view.getMeasuredHeight();
    }

    / * * * LayoutManager type, at present GroupItemDecoration only supports LinearLayoutManager. VERTICAL *@param parent
     * @return* /
    private boolean isLinearAndVertical(RecyclerView parent){
        RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
        if(! (layoutManagerinstanceof LinearLayoutManager)) {
            return false;
        }else {
            if(((LinearLayoutManager) layoutManager).getOrientation() ! = LinearLayoutManager.VERTICAL){return false; }}return true; }}Copy the code

After RecyclerView reserves space for GroupView, we need to rewrite the onDraw method to draw it. In order to ensure that all groups set by users are drawn, we need to iterate over all childViews of RecyclerView. When the corresponding GroupItem can be found in the position of the childView, Draw the GroupView above the childView (where the space was previously reserved) as shown below

@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
	super.onDraw(c, parent, state);
	if(groupList.size()==0| |! isLinearAndVertical(parent)){return;
	}

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

		int position = parent.getChildAdapterPosition(child);
		if(groups.get(position)! =null){
			c.save();
			c.translate(left,top - groupViewHeight);// Move the starting point of the canvas to the upper left corner of the previously reserved space
			decorationCallback.buildGroupView(groupView,groups.get(position));// Get the GroupView internal control data through the interface callback
			measureView(groupView,parent);// The View needs to be remeasured because the internal control has data setgroupView.draw(c); c.restore(); }}}Copy the code

The next step is to draw the sticky head, which consists of two special effects

  • Keep the GroupView of childView at the top of RecyclerView
  • When the user slides RecyclerView so that the GroupView of the previous group or the next group “collides” with the GroupView of the top, it will be pushed in the direction of the user’s slide

To better understand the relationship between adjacent groups and the following code logic, the blogger briefly introduces the grouping logic:

RecyclerView Groups within the RecyclerView visual range (displayed on the current screen) are divided into “previous group (PRE)”, “Current group (CUR)” and “Next Group (Next)” based on the following

  • The next group is determined by the cur group, and the group that follows the cur group is the Next group
  • The childView is a cur group if it is the first child of the RecyclerView group. If the childView is completely off the screen, the group is a Cur group

The specific code is as follows:

@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
	super.onDrawOver(c, parent, state);
	if(groupList.size()==0| |! isStickyHeader || ! isLinearAndVertical(parent)){return;
	}
	int childCount = parent.getChildCount();
	Map<Object,Object> map = new HashMap<>();

	// Iterate through the currently visible childView to find the current and next group and save their position index and the top position of the GroupView
	for (int i = 0; i < childCount; i++) {
		View child = parent.getChildAt(i);
		float top = child.getTop();
		int position = parent.getChildAdapterPosition(child);
		if(groups.get(position)! =null){
			positionIndex = searchGroupIndex(groupPositions,position);
			if(map.get("cur") = =null){
				map.put("cur", positionIndex);
				map.put("curTop",top);
			}else {
				if(map.get("next") = =null){
					map.put("next", positionIndex);
					map.put("nextTop",top);
				}
			}
		}
	}

	c.save();
	if(map.get("cur")! =null) {// If the current group is not empty, RecyclerView has at least one GroupView visible part
		indexCache = (int)map.get("cur");
		float curTop = (float)map.get("curTop");
		if(curTop-groupViewHeight<=0) {// Keep the current group GroupView at the top
			curTop = 0;
		}else {
			map.put("pre", (int)map.get("cur") -1);
			if(curTop - groupViewHeight < groupViewHeight){// Determine the collision with the previous group and push the current top GroupView
				curTop = curTop - groupViewHeight*2;
			}else {
				curTop = 0;
			}
			indexCache = (int)map.get("pre");
		}

		if(map.get("next")! =null) {float nextTop = (float)map.get("nextTop");
			if(nextTop - groupViewHeight < groupViewHeight){// Determine the collision with the next group and push the current top GroupView
				curTop = nextTop - groupViewHeight*2;
			}
		}

		c.translate(0,curTop);
		if(map.get("pre")! =null) {// Determine the group belonging to the top childView and draw the corresponding GroupView
			drawGroupView(c,parent,(int)map.get("pre"));
		}else {
			drawGroupView(c,parent,(int)map.get("cur")); }}else {// If the current group is empty, find the last GroupView through the previously cached index and draw it to the top
		c.translate(0.0);
		drawGroupView(c,parent,indexCache);
	}
	c.restore();
}

/** * Draw GroupView *@param canvas
 * @param parent
 * @param index
 */
private void drawGroupView(Canvas canvas,RecyclerView parent,int index){
	if(index<0) {return;
	}
	decorationCallback.buildGroupView(groupView,groups.get(groupPositions[index]));
	measureView(groupView,parent);
	groupView.draw(canvas);
}

/** * Query the index * of startPosition@param groupArrays
 * @param startPosition
 * @return* /
private int searchGroupIndex(int[] groupArrays, int startPosition){
	Arrays.sort(groupArrays);
	int result = Arrays.binarySearch(groupArrays,startPosition);
	return result;
}
Copy the code

The 1.0.0 version of GroupItemDecoration is now fully implemented, and the following updates will address the internal control of GroupView to respond to various events and expand more functions. If you feel good about it, please give a thumb-up. Your support is my biggest power


Update (GroupItem click and hold events)

This update adds a new interface to listen for GroupItem click and hold events (currently not supported for GroupItem child controls).

recyclerView.addOnItemTouchListener(new GroupItemClickListener(groupItemDecoration,new GroupItemClickListener.OnGroupItemClickListener() {
	@Override
	public void onGroupItemClick(GroupItem groupItem) {}@Override
	public void onGroupItemLongClick(GroupItem groupItem) {}}));Copy the code

The effect is shown in figure