preview

Many apps have a sliding banner on the front page. It looks something like this:

There are many ways to achieve, introduce a RecyclerView to achieve

Implementation approach

The first tiled banner is actually very easy to implement, is a RecycerView + PagerSnapHelper. However, in order to accommodate multiple display effects, such as the second display effect, we need to customize LayoutMmanager and SnapHelper.

LayoutManager part

Customizing a LayoutManager is usually done in two steps, layout and sliding. Think about what attributes LayoutManager requires.

attribute

A Boolean value is required to indicate whether the layout is circular. Two float values are required to identify the width and height of the zoom when sliding.

public class BannerLayoutManager{
    private floatHeightScale = 0.9 f; privatefloatWidthScale = 0.9 f; private boolean infinite =true; // Default infinite loop... }Copy the code

layout

1, calculate the first View start position: int offsetX = (parent layout width – child View width) / 2

 int offsetX = (mOrientationHelper.getTotalSpace() - mOrientationHelper.getDecoratedMeasurement(scrap)) / 2;
Copy the code

2, calculate whether to add a view as the first child view to show the effect of the circular layout.

View lastChild = getChildAt(getChildCount() - 1); // If the layout is circular and the last view is out of the parent layout, add the leftmost viewif( infinite && lastChild ! = null && getDecoratedRight(lastChild) > mOrientationHelper.getTotalSpace()) { layoutLeftItem(recycler); }Copy the code

3, zoom all view zoom rules: based on the parent layout of the center line (center line), if the child view of the center line and center line coincide, the zoom ratio is 1.0F; If not, calculate the distance between the center line and the center line of the child view. The larger the distance, the smaller the scale ratio.

 private void scaleItem() {
        if (heightScale >= 1 || widthScale >= 1) {
            return;
        }

        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            floatItemMiddle = (getDecoratedRight(Child) + getDecoratedLeft(Child)) / 2.0f;floatScreenMiddle = mOrientationHelper. GetTotalSpace () / 2.0 f;floatInterval = Math. Abs (screenmiddle-itemMiddle) * 1.0f;float ratio = 1 - (1 - heightScale) * (interval / itemWidth);
            floatratioWidth = 1 - (1 - widthScale) * (interval / itemWidth); child.setScaleX(ratioWidth); child.setScaleY(ratio); }}Copy the code

4. Overall layout method

private void layoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (getItemCount() == 0 || state.isPreLayout()) {
            removeAndRecycleAllViews(recycler);
            return;
        }
        detachAndScrapAttachedViews(recycler);

        View scrap = recycler.getViewForPosition(0);
        measureChildWithMargins(scrap, 0, 0);
        itemWidth = getDecoratedMeasuredWidth(scrap);
        int offsetX = (mOrientationHelper.getTotalSpace() - mOrientationHelper.getDecoratedMeasurement(scrap)) / 2;
        for (int i = 0; i < getItemCount(); i++) {
            if (offsetX > mOrientationHelper.getTotalSpace()) {
                break; } View viewForPosition = recycler.getViewForPosition(i); addView(viewForPosition); measureChildWithMargins(viewForPosition, 0, 0); offsetX += layoutItem(viewForPosition, offsetX); } View lastChild = getChildAt(getChildCount() - 1); // If the layout is circular and the last view is out of the parent layout, add the leftmost viewif( infinite && lastChild ! = null && getDecoratedRight(lastChild) > mOrientationHelper.getTotalSpace()) { layoutLeftItem(recycler); } scaleItem(); }Copy the code

sliding

The processing of sliding is to recycle the view to reduce consumption and improve efficiency. The way to do this is to add and remove views based on the sliding distance.

It was also my first time to customize LayoutManager and I felt it was a bit cumbersome to write. Divided the left slide right slide two cases to write.

    @Override
    public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
        returnoffsetDx(dx, recycler); } private int offsetDx(int dx, RecyclerView.Recycler recycler) { int realScroll = dx; / / to the leftif(dx > 0) { realScroll = scrollToLeft(dx, recycler, realScroll); } / / to the rightif (dx < 0) {
            realScroll = scrollToRight(dx, recycler, realScroll);
        }
        scaleItem();

        return realScroll;
    }
Copy the code

ScrollToLeft or scrollToRight does just three things, add the view, calculate the actual slide distance and slide, and reclaim the view

    private int scrollToLeft(int dx, RecyclerView.Recycler recycler, int realScroll) {
        while (trueRightView = getChildAt(getChildCount() -1); int decoratedRight = getDecoratedRight(rightView);if (decoratedRight - dx < mOrientationHelper.getTotalSpace()) {
                int position = getPosition(rightView);
                if(! infinite && position == getItemCount() - 1) {break;
                }

                int addPosition = infinite ? (position + 1) % getItemCount() : position + 1;
                View lastViewAdd = recycler.getViewForPosition(addPosition);
                addView(lastViewAdd);
                measureChildWithMargins(lastViewAdd, 0, 0);
                int left = decoratedRight;
                layoutDecoratedWithMargins(lastViewAdd, left, getItemTop(lastViewAdd), left + getDecoratedMeasuredWidth(lastViewAdd), getItemTop(lastViewAdd) + getDecoratedMeasuredHeight(lastViewAdd));
            } else {
                break; GetChildCount () -1); getChildCount() -1); int left = getDecoratedLeft(lastChild);if(getPosition(lastChild) == getItemCount() -1) {// The last view has reached the bottom, calculate the actual sliding distanceif(left - dx < 0) { realScroll = left; } } offsetChildrenHorizontal(-realScroll); // Retrieve the view that slides out of the parent layoutfor (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            int decoratedRight = getDecoratedRight(child);
            if(decoratedRight < 0) { removeAndRecycleView(child, recycler); }}return realScroll;
    }
Copy the code

At this point, the custom LayoutManager is almost complete.

SnapHelper part

Custom LayoutManager can be used to create GIF effects efficiently, but there is a problem with sliding. RecyclerView supports inertia sliding by default. You can’t swipe one page at a time and center it (similar to the ViewPager slide). To achieve this, Google provides an abstract SnapHelper class that you can inherit to implement your own sliding logic. The SDK provides two implementations: PagerSnapHelper and LinearSnapHelper. PagerSnapHelper can do the ViewPager effect of swiping one page at a time, but when it comes to the last view, it’s obviously stuck. PagerSnapHelper does not support circular layouts by default. So I inherited PagerSnaperHelper, modified the logic a little bit, and implemented a looping slide effect.

public class BannerPageSnapHelper extends PagerSnapHelper {

    private boolean infinite = false;
    private OrientationHelper horizontalHelper;

    @Override
    public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
                                      int velocityY) {
        final int itemCount = layoutManager.getItemCount();
        if (itemCount == 0) {
            return RecyclerView.NO_POSITION;
        }

        View mStartMostChildView = findStartView(layoutManager, getHorizontalHelper(layoutManager));

        if (mStartMostChildView == null) {
            return RecyclerView.NO_POSITION;
        }
        final int centerPosition = layoutManager.getPosition(mStartMostChildView);
        if (centerPosition == RecyclerView.NO_POSITION) {
            return RecyclerView.NO_POSITION;
        }

        final boolean forwardDirection;
        if (layoutManager.canScrollHorizontally()) {
            forwardDirection = velocityX > 0;
        } else {
            forwardDirection = velocityY > 0;
        }

        if (forwardDirection) {
            if (centerPosition == layoutManager.getItemCount() - 1) {
                return infinite ? 0 : layoutManager.getItemCount() - 1;
            } else {
                returncenterPosition + 1; }}else {
            return centerPosition;
        }
    }

    private View findStartView(RecyclerView.LayoutManager layoutManager,
                               OrientationHelper helper) {
        int childCount = layoutManager.getChildCount();
        if (childCount == 0) {
            return null;
        }

        View closestChild = null;
        int start = Integer.MAX_VALUE;

        for(int i = 0; i < childCount; i++) { final View child = layoutManager.getChildAt(i); int childStart = helper.getDecoratedStart(child); / * *if child is more to start than previous closest, set it as closest  **/
            if(childStart < start) { start = childStart; closestChild = child; }}return closestChild;
    }

    @NonNull
    private OrientationHelper getHorizontalHelper(
            @NonNull RecyclerView.LayoutManager layoutManager) {
        if (horizontalHelper == null) {
            horizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
        }
        return horizontalHelper;
    }

    public boolean isInfinite() {
        return infinite;
    }

    public void setInfinite(boolean infinite) { this.infinite = infinite; }}Copy the code

extension

For the banner section, a general project will have the following parameters.

1, style display style: such as rounded corners or tiling. You can put a CardView around each child view to set the rounded corners, and then set the width and height of the view in the adapter as required.

2, loop display: BannerLayoutManager and PagerHelper both have a property, infinite, which is true, loop display.

3, automatic playback: in the Activity or fragments with Rxjava or Handler to add a timer, call recyclerView. SmoothScrollToPosition (position).

4, Slide animation display time: BannerLayoutManager has a smoothScrollTime property, call the set method to set it.

That should satisfy most of your needs.

The source code

To learn more, go to the code github.com/ZhangHao555…