preface

“Contentment”, a lot of people are not satisfied with the status quo, all kinds of toss and toss, often abandon their roots to the last, chang Le can be less impetuous, more quiet. Recently, a lot of things have happened in xiaobian, and the mentality has changed a lot. I feel helpless in reality, lonely in the distant city away from my hometown, and impetuous in pursuit of fame and wealth. Maybe life is like this, each age has its own troubles.

Speaking of thrashing, I’ve seen all sorts of custom LayoutManagers making all sorts of cool animations a long time ago and thought I’d do it myself. But every time because the system comes with the LinearLayoutManager source code to make a face confused. Just this time not busy, toss about a day, wrote a simple Demo, the effect is as follows:

Results the preview

“Multiple Types of RecyclerView”

use

	mRecyclerView.setLayoutManager(stackLayoutManager = new StackLayoutManager(this));
Copy the code

The text is just a simple Demo with a single function. It mainly explains the process and steps. Please modify it according to specific requirements.

The significance of each attribute is shown in the figure:

Customize LayoutManager basics

For the basics of custom LayoutManager, check out the following excellent article:

1, Chen Xiaoyuan’s custom LayoutManager 11 type flying Dragon in the sky (xiaoyuan boss custom article logic is clear, can be called a textbook, very classic)

Blog.csdn.net/u011387817/…

2, Zhang Xutong master custom LayoutManager(a) series of common mistakes, problems, notes, common API

Blog.csdn.net/zxt0601/art…

3. Zhang Xutong’s mastery of custom LayoutManager(2) to achieve flow layout

Blog.csdn.net/zxt0601/art…

4, Yong Chao Chen Android imitation douban book video channel recommendation form stack list recyclerView-LayoutManager

Blog.csdn.net/ccy0122/art…

These articles are very accurate in their analysis of the misunderstandings and precautions of custom LayoutManager. I have read several articles back and forth, hoping to help you.

Customize the basic LayoutManager flow

Make Items appear

In a custom ViewGroup, we want to display child views in three ways:

  1. Add Add a child View to a ViewGroup by using the addView method or directly in XML;
  2. Measure overrides the onMeasure method and determines its own size and the size of each subview;
  3. Layout overrides the onLayout method, which calls the layout method of the child View to determine its position and size.

In the custom LayoutManager, the process is similar. We need to override the onLayoutChildren method, which calls back during initialization or Adapter data set update. In this method, we need to do the following:

  1. Layout before, we need to call detachAndScrapAttachedViews method to separate the Items of screen, adjust position and internal data, and then add it back (if required);
  2. So once we’ve separated them, we have to figure out how to add them back, so we need to add them with the addView method, so where do we get those views? We need to call the Recycler’s getViewForPosition method to get it.
  3. Once the Item is fetched and repopulated, measure it by calling measureChild or measureChildWithMargins.
  4. What else do I need to do after I measure it? Yes, the layout, we also decided to use layoutDecorated or layoutDecoratedWithMargins method according to the requirements;
  5. In the custom ViewGroup, the layout can be run to see the effect, but in LayoutManager there is a very important thing, is the recycling, we in the layout, but also some Items are no longer needed to recycle, to ensure the smoothness of sliding;

The above content comes from Chen Xiaoyuan’s custom LayoutManager type 11 Flying Dragon in the sky.

Layout of the implementation

Take a look at the relevant parameters:

The index value of 0 view a completely mobile distance, slip out of the screen need to firstChildCompleteScrollLength; The index value of 0 slip out of screen needed to move the view distance is: firstChildCompleteScrollLength + onceCompleteScrollLength; The spacing between items is normalViewGap

We record the offset dx in the scrollHorizontallyBy method, save a cumulative offset mHorizontalOffset, and then for both cases where the index is 0 and non-0, In less than firstChildCompleteScrollLength mHorizontalOffset case, use the offset divided by the percentage firstChildCompleteScrollLength access to already rolling fraction; Under the condition of the same index value of 0, minus the firstChildCompleteScrollLength offset needed to get the percentage of the scroll. Depending on the percentage, it’s easy to lay out childView.

I’m going to start writing code. I’m going to call it StackLayoutManager.

StackLayoutManager inheritance RecyclerView LayoutManager, needs to be rewritten generateDefaultLayoutParams method:

    @Override
    public RecyclerView.LayoutParams generateDefaultLayoutParams(a) {
        return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT, RecyclerView.LayoutParams.WRAP_CONTENT);
    }
Copy the code

Let’s start with member variables:

    /** * The distance required for a complete focus slide */
    private float onceCompleteScrollLength = -1;

    /** * The offset of the first child view */
    private float firstChildCompleteScrollLength = -1;

    /** * The first view's position */ is visible on the screen
    private int mFirstVisiPos;

    /** * Position of the last view visible on the screen */
    private int mLastVisiPos;

    /** * accumulated offset in horizontal direction */
    private long mHorizontalOffset;

    /** * margin between views */
    private float normalViewGap = 30;

    private int childWidth = 0;

    /** * Indicates whether */ is automatically selected
    private boolean isAutoSelect = true;
    // Select the animation
    private ValueAnimator selectAnimator;
Copy the code

Then look at the scrollHorizontallyBy method:

    @Override
    public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
        // Swipe from right to left, dx > 0; Slide your finger from left to right, dx < 0;
        // The View will not move without its child
        if (dx == 0 || getChildCount() == 0) {
            return 0;
        }

        // Error handling
        float realDx = dx / 1.0 f;
        if (Math.abs(realDx) < 0.00000001 f) {
            return 0;
        }

        mHorizontalOffset += dx;

        dx = fill(recycler, state, dx);

        return dx;
    }

    private int fill(RecyclerView.Recycler recycler, RecyclerView.State state, int dx) {
        int resultDelta = dx;
        resultDelta = fillHorizontalLeft(recycler, state, dx);
        recycleChildren(recycler);
        return resultDelta;
    }

    private int fillHorizontalLeft(RecyclerView.Recycler recycler, RecyclerView.State state, int dx) {
        //----------------1. Boundary detection -----------------
        if (dx < 0) {
            // Left boundary has been reached
            if (mHorizontalOffset < 0) {
                mHorizontalOffset = dx = 0; }}if (dx > 0) {
            if (mHorizontalOffset >= getMaxOffset()) {
                // Slide to the rightmost edge according to the maximum offset
                mHorizontalOffset = (long) getMaxOffset();
                dx = 0; }}// Separate all views and add them to the temporary cache
        detachAndScrapAttachedViews(recycler);

        float startX = 0;
        float fraction = 0f;
        boolean isChildLayoutLeft = true;

        View tempView = null;
        int tempPosition = -1;

        if (onceCompleteScrollLength == -1) {
            // Since mFirstVisiPos may change below, use tempPosition to store it temporarily
            tempPosition = mFirstVisiPos;
            tempView = recycler.getViewForPosition(tempPosition);
            measureChildWithMargins(tempView, 0.0);
            childWidth = getDecoratedMeasurementHorizontal(tempView);
        }

        // Fix the first visible view mFirstVisiPos sliding how many full onceCompleteScrollLength represents sliding how many items
        firstChildCompleteScrollLength = getWidth() / 2 + childWidth / 2;
        if (mHorizontalOffset >= firstChildCompleteScrollLength) {
            startX = normalViewGap;
            onceCompleteScrollLength = childWidth + normalViewGap;
            mFirstVisiPos = (int) Math.floor(Math.abs(mHorizontalOffset - firstChildCompleteScrollLength) / onceCompleteScrollLength) + 1;
            fraction = (Math.abs(mHorizontalOffset - firstChildCompleteScrollLength) % onceCompleteScrollLength) / (onceCompleteScrollLength * 1.0 f);
        } else {
            mFirstVisiPos = 0;
            startX = getMinOffset();
            onceCompleteScrollLength = firstChildCompleteScrollLength;
            fraction = (Math.abs(mHorizontalOffset) % onceCompleteScrollLength) / (onceCompleteScrollLength * 1.0 f);
        }

        // Temporarily assign mLastVisiPos to getItemCount() -1, rest easy, the next run will determine whether the view has overflowed the screen, and correct the value in time to finish the layout
        mLastVisiPos = getItemCount() - 1;

        float normalViewOffset = onceCompleteScrollLength * fraction;
        boolean isNormalViewOffsetSetted = false;

        //----------------3. Start layout -----------------
        for (int i = mFirstVisiPos; i <= mLastVisiPos; i++) {
            View item;
            if(i == tempPosition && tempView ! =null) {
                // If a temporary view is already taken when initializing data
                item = tempView;
            } else {
                item = recycler.getViewForPosition(i);
            }

            addView(item);
            measureChildWithMargins(item, 0.0);

            if(! isNormalViewOffsetSetted) { startX -= normalViewOffset; isNormalViewOffsetSetted =true;
            }

            int l, t, r, b;
            l = (int) startX;
            t = getPaddingTop();
            r = l + getDecoratedMeasurementHorizontal(item);
            b = t + getDecoratedMeasurementVertical(item);

            layoutDecoratedWithMargins(item, l, t, r, b);

            startX += (childWidth + normalViewGap);

            if (startX > getWidth() - getPaddingRight()) {
                mLastVisiPos = i;
                break; }}return dx;
    }
Copy the code

Methods involved:

    /** * Maximum offset **@return* /
    private float getMaxOffset(a) {
        if (childWidth == 0 || getItemCount() == 0) return 0;
        return (childWidth + normalViewGap) * (getItemCount() - 1);
    }
 
    /** * Get the horizontal space of a childView, taking margin into account **@param view
     * @return* /
    public int getDecoratedMeasurementHorizontal(View view) {
        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
                view.getLayoutParams();
        return getDecoratedMeasuredWidth(view) + params.leftMargin
                + params.rightMargin;
    }

    /** * Get the vertical space of a childView, taking margin into account **@param view
     * @return* /
    public int getDecoratedMeasurementVertical(View view) {
        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
                view.getLayoutParams();
        return getDecoratedMeasuredHeight(view) + params.topMargin
                + params.bottomMargin;
    }
Copy the code

Recycling reuse

Recyclerview-layoutmanager RecyclerView-LayoutManager RecyclerView-LayoutManager

 / * * *@param recycler
     * @param state
     * @param delta
     */
    private int fill(RecyclerView.Recycler recycler, RecyclerView.State state, int delta) {
        int resultDelta = delta;
        / /... omit
        
        recycleChildren(recycler);
       log("childCount= [" + getChildCount() + "]" + ",[recycler.getScrapList().size():" + recycler.getScrapList().size());
        return resultDelta;
    }
    
	/** * Reclaim the Item to be reclaimed. * /
    private void recycleChildren(RecyclerView.Recycler recycler) {
        List<RecyclerView.ViewHolder> scrapList = recycler.getScrapList();
        for (int i = 0; i < scrapList.size(); i++) { RecyclerView.ViewHolder holder = scrapList.get(i); removeAndRecycleView(holder.itemView, recycler); }}Copy the code

Recycling here will not verify, interested partners can verify themselves.

Animation effects

    private int fillHorizontalLeft(RecyclerView.Recycler recycler, RecyclerView.State state, int dx) {
        / / omit...
        //----------------3. Start layout -----------------
        for (int i = mFirstVisiPos; i <= mLastVisiPos; i++) {
            / / omit...
            
            // Zoom the subview
            final float minScale = 0.6 f;
            float currentScale = 0f;
            final int childCenterX = (r + l) / 2;
            final int parentCenterX = getWidth() / 2;
            isChildLayoutLeft = childCenterX <= parentCenterX;
            if (isChildLayoutLeft) {
                final float fractionScale = (parentCenterX - childCenterX) / (parentCenterX * 1.0 f);
                currentScale = 1.0 f - (1.0 f - minScale) * fractionScale;
            } else {
                final float fractionScale = (childCenterX - parentCenterX) / (parentCenterX * 1.0 f);
                currentScale = 1.0 f - (1.0 f - minScale) * fractionScale;
            }
            item.setScaleX(currentScale);
            item.setScaleY(currentScale);
            item.setAlpha(currentScale);
            
            layoutDecoratedWithMargins(item, l, t, r, b);
           / / omit...
        }
        return dx;
    }
Copy the code

The more you move childView toward the middle of the screen, the larger the zoom ratio, and the more you move toward the sides, the smaller the zoom ratio.

Automatically selected

1. Automatically selected after scrolling stops

Listen onScrollStateChanged, calculate the position that should stay while scrolling stops, and then calculate the mHorizontalOffset value when stopping. Play the property animation to update the current mHorizontalOffset to the final value. The relevant codes are as follows:

    @Override
    public void onScrollStateChanged(int state) {
        super.onScrollStateChanged(state);
        switch (state) {
            case RecyclerView.SCROLL_STATE_DRAGGING:
                // Stops the currently playing animation when the finger is pressed
                cancelAnimator();
                break;
            case RecyclerView.SCROLL_STATE_IDLE:
                // When the list scrolling stops, check to see if automatic selection is turned on
                if (isAutoSelect) {
                    // Find the closest item index to the target drop point
                    smoothScrollToPosition(findShouldSelectPosition());
                }
                break;
            default:
                break; }}/** * smooth scroll to a position **@paramPosition Index of the target Item */
    public void smoothScrollToPosition(int position) {
        if (position > -1&& position < getItemCount()) { startValueAnimator(position); }}private int findShouldSelectPosition(a) {
        if (onceCompleteScrollLength == -1 || mFirstVisiPos == -1) {
            return -1;
        }
        int position = (int) (Math.abs(mHorizontalOffset) / (childWidth + normalViewGap));
        int remainder = (int) (Math.abs(mHorizontalOffset) % (childWidth + normalViewGap));
        // More than half, the next item should be selected
        if (remainder >= (childWidth + normalViewGap) / 2.0 f) {
            if (position + 1 <= getItemCount() - 1) {
                return position + 1; }}return position;
    }

    private void startValueAnimator(int position) {
        cancelAnimator();

        final float distance = getScrollToPositionOffset(position);

        long minDuration = 100;
        long maxDuration = 300;
        long duration;

        float distanceFraction = (Math.abs(distance) / (childWidth + normalViewGap));

        if (distance <= (childWidth + normalViewGap)) {
            duration = (long) (minDuration + (maxDuration - minDuration) * distanceFraction);
        } else {
            duration = (long) (maxDuration * distanceFraction);
        }
        selectAnimator = ValueAnimator.ofFloat(0.0 f, distance);
        selectAnimator.setDuration(duration);
        selectAnimator.setInterpolator(new LinearInterpolator());
        final float startedOffset = mHorizontalOffset;
        selectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float value = (float) animation.getAnimatedValue();
                mHorizontalOffset = (long) (startedOffset + value); requestLayout(); }}); selectAnimator.start(); }Copy the code
2. Click the non-focus view to automatically select it as the focus view

SmoothScrollToPosition method to automatically select focus.

The middle view overlays the views on both sides

It works like this:

RecyclerView inherits from ViewGroup, so the larger the index value of index in addView(View Child, int index), the more it will be displayed in the upper layer. Therefore, it can be concluded that the added green card of 2 is the largest index, and the analysis can draw the following conclusions:

Index size:

0 < 1 < 2 > 3 > 4

The principle of maximum in the middle and gradual diminution on both sides.

If the index value is less than or equal to the index value, call addView(item), otherwise call addView(item, 0); The relevant codes are as follows:

    private int fillHorizontalLeft(RecyclerView.Recycler recycler, RecyclerView.State state, int dx) {
        //省略 ......
        //----------------3. Start layout -----------------
        for (int i = mFirstVisiPos; i <= mLastVisiPos; i++) {
             //省略 ......
            int focusPosition = (int) (Math.abs(mHorizontalOffset) / (childWidth + normalViewGap));
            if (i <= focusPosition) {
                addView(item);
            } else {
                addView(item, 0);
            }
             //省略 ...... 
        }
        return dx;
    }
Copy the code

That’s about the end of the article.

Source code address:

Github.com/HpWens/MeiW…

Give me a star

conclusion

People who love to laugh, luck is generally not too bad. And give yourself a pat on the back, and we’ll see you next time.