Recipes for Android: How to Make LayoutManager Deliciously

  • Tutorial
Hi Habr!

We love to develop mobile applications that differ from their counterparts in both functions and the user interface. Last time we talked about client-server interaction in one of our applications, but this time we will share the implementation of its UI features using the LayoutManager written from scratch. We think that the article will be useful not only to novice android developers, but also to more advanced specialists.




Let's start with


If you are an android developer, then you probably already used RecyclerView, a powerful and incredibly customizable replacement for ListView and GridView. One of the degrees of customization of RecyclerView is that it does not know anything about the location of elements within itself. This work is delegated to his LayoutManager. Google provided us with 3 standard managers: LinearLayoutManager for lists as in ListView, GridLayoutManager for tiles, grids or tables, and StaggeredGridLayoutManager for layout like on Google+. For our application, it was necessary to implement a layout that did not fit into the framework of the available layout managers, so we decided to try writing our own. It turned out to create your LayoutManager like a drug. Having tried it once, it’s already difficult to stop - it turned out to be so useful in solving non-standard layout problems.

image

So the challenge. In our training application there will be articles in a very simple format: picture, title and text. We want to have a vertical list of articles, each card in which will occupy 75% of the screen height. In addition to the vertical, there will be a horizontal list in which each article will be open in full screen. The transition from vertical to horizontal mode will be animated by clicking on a card and using the back button - back to vertical. And also, for beauty, in vertical mode, the bottom card will scroll out with scaling effect when scrolling. By the way, you can see our training project here: https://github.com/forceLain/AwesomeRecyclerView , it already has a fake DataProvider that returns 5 fake articles, all layouts and, in fact, LayoutManager itself :)
Imagine that an Activity with a RecyclerView in it, as well as a RecyclerView.Adapter that creates and fills in card articles, we have already written (or copied from a training project) and it's time to create your LayoutManager.

We write the basis



The first thing that needs to be done is to implement the generateDefaultLayoutParams () method, which will return the desired LayoutParams for views whose LayoutParams do not suit us

publicclassAwesomeLayoutManagerextendsRecyclerView.LayoutManager{
    @Overridepublic RecyclerView.LayoutParams generateDefaultLayoutParams(){
        returnnew RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.MATCH_PARENT);
    }
}

The main magic happens in the onLayoutChildren (...) method, which is the starting point for adding and positioning our views. First, learn how to have at least one article.

@OverridepublicvoidonLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state){
    View view = recycler.getViewForPosition(0);
    addView(view);
    measureChildWithMargins(view, 0, 0);
    layoutDecorated(view, 0, 0, getWidth(), getHeight());
}

In the first line, we ask recycler to give us a view for the first position. Then, Recycler decides whether to return it from the internal cache or create a new one. On the second line we will stay longer.

If you have already created your own views, you probably know how to add another child inside your view. To do this, add the child view to your layout (as in the second line), then measure by calling the measure (...) method on it and, finally, position it in the right place by calling the layout (...) method on it with the right size. If you have never done anything like this before, now you have some idea how this happens :) As for RecyclerView, here it takes a different turn. For almost all standard View-class methods related to size and layout, RecyclerView has alternative methods that should be used. First of all, they are needed because in RecyclerView there is the ItemDecoration class, with which you can change the size of the views,
Here are some examples of alternative methods:

view.layout(left, top, right, bottom) -> layoutDecorated(view, left, top, right, bottom)
view.getLeft() -> getDecoratedLeft(view)
view.getWidth() -> getDecoratedWidth(view)

etc.

So, in the third line, we allow the view to calculate its sizes, and in the fourth line we place it in the layout from the upper left corner (0, 0) to the lower right (getWidth (), getHeight ()).
To measure the size of the view, we used the ready-made method measureChildWithMargins (...). In fact, it doesn’t quite suit us, because it takes measurements taking into account the width and height specified in the LayoutParams of the child view. And there can be anything: wrap_content, match_parent, or even set in dp. But we agreed that all the cards we have will be a fixed size! So we have to write our measure, without forgetting about the existence of decorators:

privatevoidmeasureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec){
    Rect decorRect = new Rect();
    calculateItemDecorationsForChild(child, decorRect);
    RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams();
    widthSpec = updateSpecWithExtra(widthSpec, lp.leftMargin + decorRect.left,
            lp.rightMargin + decorRect.right);
    heightSpec = updateSpecWithExtra(heightSpec, lp.topMargin + decorRect.top,
                lp.bottomMargin + decorRect.bottom);
    child.measure(widthSpec, heightSpec);
}
privateintupdateSpecWithExtra(int spec, int startInset, int endInset){
    if (startInset == 0 && endInset == 0) {
        return spec;
    }
    finalint mode = View.MeasureSpec.getMode(spec);
    if (mode == View.MeasureSpec.AT_MOST || mode == View.MeasureSpec.EXACTLY) {
        return View.MeasureSpec.makeMeasureSpec(
                View.MeasureSpec.getSize(spec) - startInset - endInset, mode);
    }
    return spec;
}

Now our onLayoutChildren () looks like this:

@OverridepublicvoidonLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state){
    View view = recycler.getViewForPosition(0);
    addView(view);
    finalint widthSpec = View.MeasureSpec.makeMeasureSpec(getWidth(), View.MeasureSpec.EXACTLY);
    finalint heightSpec = View.MeasureSpec.makeMeasureSpec(getHeight(), View.MeasureSpec.EXACTLY);
    measureChildWithDecorationsAndMargin(view, widthSpec, heightSpec);
    layoutDecorated(view, 0, 0, getWidth(), getHeight());
}

With MeasureSpec, we tell our view that its height and width should and will be equal to the height and width of the RecyclerView. Of course, to draw an article with a height of 75% of the screen height, you need to pass this same height to layoutDecorated ():

privatestaticfinalfloat VIEW_HEIGHT_PERCENT = 0.75f;
@OverridepublicvoidonLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state){
    View view = recycler.getViewForPosition(0);
    addView(view);
    int viewHeight = (int) (getHeight() * VIEW_HEIGHT_PERCENT);
    finalint widthSpec = View.MeasureSpec.makeMeasureSpec(getWidth(), View.MeasureSpec.EXACTLY);
    finalint heightSpec = View.MeasureSpec.makeMeasureSpec(getHeight(), View.MeasureSpec.EXACTLY);
    measureChildWithDecorationsAndMargin(view, widthSpec, heightSpec);
    layoutDecorated(view, 0, 0, getWidth(), viewHeight);
}

Now, if we install our LayoutManager in RecyclerView and start the project, we will see one article on three quarters of the screen.


Now we will try to draw the article articles, starting from the first (zero) and placing them under each other until the screen runs out vertically or do not run out of elements in the adapter.

@OverridepublicvoidonLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state){
    fillDown(recycler);
}
privatevoidfillDown(RecyclerView.Recycler recycler){
    int pos = 0;
    boolean fillDown = true;
    int height = getHeight();
    int viewTop = 0;
    int itemCount = getItemCount();
    int viewHeight = (int) (getHeight() * VIEW_HEIGHT_PERCENT);
    finalint widthSpec = View.MeasureSpec.makeMeasureSpec(getWidth(), View.MeasureSpec.EXACTLY);
    finalint heightSpec = View.MeasureSpec.makeMeasureSpec(getHeight(), View.MeasureSpec.EXACTLY);
    while (fillDown && pos < itemCount){
        View view = recycler.getViewForPosition(pos);
        addView(view);
        measureChildWithDecorationsAndMargin(view, widthSpec, heightSpec);
        int decoratedMeasuredWidth = getDecoratedMeasuredWidth(view);
        layoutDecorated(view, 0, viewTop, decoratedMeasuredWidth, viewTop + viewHeight);
        viewTop = getDecoratedBottom(view);
        fillDown = viewTop <= height;
        pos++;
    }
}



It looks ready, but so far we have not done one very important thing. Earlier, we said that recycler determines whether to take views from the cache or create new ones, but in reality it still has empty cache, since we haven’t put anything into it yet. Add a call to detachAndScrapAttachedViews (recycler) in onLayoutChildren () in the first place before fillDown ().

@OverridepublicvoidonLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state){
    detachAndScrapAttachedViews(recycler);
   fillDown(recycler);
}

This method removes all views from our layout and puts them in its special scrap cache. If necessary, you can return the view using the recycler.getViewForPosition (pos) method.

They See Me Rollin '


Now it’s good to teach our LayoutManager to scroll.
First, let’s tell our LayoutManager that we want to scroll vertically:

@OverridepublicbooleancanScrollVertically(){
    returntrue;
}

Then we implement the vertical scroll itself

@OverridepublicintscrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state){
    offsetChildrenVertical(-dy);
    return dy;
}

At the input of this method, we get dy - the distance by which you need to scroll. We must return the distance to which we really scrolled our views. This is necessary in order to prevent the content from leaving the screen. Let's immediately write an algorithm that determines whether we can still scroll and how far:

Scrolling
@OverridepublicintscrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state){
    int delta = scrollVerticallyInternal(dy);
    offsetChildrenVertical(-delta);
    return delta;
}
privateintscrollVerticallyInternal(int dy){
    int childCount = getChildCount();
    int itemCount = getItemCount();
    if (childCount == 0){
        return0;
    }
    final View topView = getChildAt(0);
    final View bottomView = getChildAt(childCount - 1);
    //Случай, когда все вьюшки поместились на экранеint viewSpan = getDecoratedBottom(bottomView) - getDecoratedTop(topView);
    if (viewSpan <= getHeight()) {
        return0;
    }
    int delta = 0;
    //если контент уезжает внизif (dy < 0){
        View firstView = getChildAt(0);
        int firstViewAdapterPos = getPosition(firstView);
        if (firstViewAdapterPos > 0){ //если верхняя вюшка не самая первая в адаптере
            delta = dy;
        } else { //если верхняя вьюшка самая первая в адаптере и выше вьюшек больше быть не можетint viewTop = getDecoratedTop(firstView);
            delta = Math.max(viewTop, dy);
        }
    } elseif (dy > 0){ //если контент уезжает вверх
        View lastView = getChildAt(childCount - 1);
        int lastViewAdapterPos = getPosition(lastView);
        if (lastViewAdapterPos < itemCount - 1){ //если нижняя вюшка не самая последняя в адаптере
            delta = dy;
        } else { //если нижняя вьюшка самая последняя в адаптере и ниже вьюшек больше быть не можетint viewBottom = getDecoratedBottom(lastView);
            int parentBottom = getHeight();
            delta = Math.min(viewBottom - parentBottom, dy);
        }
    }
    return delta;
}


Now we can scroll through our 2 added articles, but when scrolling, new articles are not added to the screen. The algorithm for adding new views during scrolling may seem intricate, but this is only at first glance. First, try to describe it with the words:
  1. First, we shift all available views to dy using offsetChildrenVertical (-dy)
  2. We select one of the views available in the layout as “anchor” and remember it and its position. In our case, we will choose the one that is fully visible on the screen as an anchor view. If there is none, then choose the one whose visible area is maximum. This way of determining the anchor view will help us in the future, when implementing a change in orientation of our layout manager
  3. We remove all the views available in the layout, placing them in our own cache and remembering at what positions they were
  4. Add views above the position that was taken as anchor in the layout. Then add the anchor and everything that should be below it. First of all, we take the views from our cache and, if we do not find it, we ask recycler


NOTE: the implementation of the scroll and the addition of view to the layout is an individual matter. With the same success, one could take the topmost one by anchor view and fill the screen down from it. And if you wanted to make such a LayoutManager, which behaves like a ViewPager, you would not have to add views at all during scrolling, but only in between swipes.

Scrolling + Recycling
private SparseArray<View> viewCache = new SparseArray<>(); 
    @OverridepublicvoidonLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state){
        detachAndScrapAttachedViews(recycler);
        fill(recycler);
    }
    privatevoidfill(RecyclerView.Recycler recycler){
        View anchorView = getAnchorView();
        viewCache.clear();
        //Помещаем вьюшки в кэш и...for (int i = 0, cnt = getChildCount(); i < cnt; i++) {
            View view = getChildAt(i);
            int pos = getPosition(view);
            viewCache.put(pos, view);
        }
        //... и удалям из лэйаутаfor (int i = 0; i < viewCache.size(); i++) {
            detachView(viewCache.valueAt(i));
        }
        fillUp(anchorView, recycler);
        fillDown(anchorView, recycler);
        //отправляем в корзину всё, что не потребовалось в этом цикле лэйаута//эти вьюшки или ушли за экран или не понадобились, потому что соответствующие элементы //удалились из адаптераfor (int i=0; i < viewCache.size(); i++) {
            recycler.recycleView(viewCache.valueAt(i));
        }
    }
    privatevoidfillUp(@Nullable View anchorView, RecyclerView.Recycler recycler){
        int anchorPos = 0;
        int anchorTop = 0;
        if (anchorView != null){
            anchorPos = getPosition(anchorView);
            anchorTop = getDecoratedTop(anchorView);
        }
        boolean fillUp = true;
        int pos = anchorPos - 1;
        int viewBottom = anchorTop; //нижняя граница следующей вьюшки будет начитаться от верхней границы предыдущейint viewHeight = (int) (getHeight() * VIEW_HEIGHT_PERCENT);
        finalint widthSpec = View.MeasureSpec.makeMeasureSpec(getWidth(), View.MeasureSpec.EXACTLY);
        finalint heightSpec = View.MeasureSpec.makeMeasureSpec(viewHeight, View.MeasureSpec.EXACTLY);
        while (fillUp && pos >= 0){
            View view = viewCache.get(pos); //проверяем кэшif (view == null){ 
                //если вьюшки нет в кэше - просим у recycler новую, измеряем и лэйаутим её
                view = recycler.getViewForPosition(pos);
                addView(view, 0);
                measureChildWithDecorationsAndMargin(view, widthSpec, heightSpec);
                int decoratedMeasuredWidth = getDecoratedMeasuredWidth(view);
                layoutDecorated(view, 0, viewBottom - viewHeight, decoratedMeasuredWidth, viewBottom);
            } else {
                //если вьюшка есть в кэше - просто аттачим её обратно//нет необходимости проводить measure/layout цикл.
                attachView(view);
                viewCache.remove(pos);
            }
            viewBottom = getDecoratedTop(view);
            fillUp = (viewBottom > 0);
            pos--;
        }
    }
    privatevoidfillDown(@Nullable View anchorView, RecyclerView.Recycler recycler){
        int anchorPos = 0;
        int anchorTop = 0;
        if (anchorView != null){
            anchorPos = getPosition(anchorView);
            anchorTop = getDecoratedTop(anchorView);
        }
        int pos = anchorPos;
        boolean fillDown = true;
        int height = getHeight();
        int viewTop = anchorTop;
        int itemCount = getItemCount();
        int viewHeight = (int) (getHeight() * VIEW_HEIGHT_PERCENT);
        finalint widthSpec = View.MeasureSpec.makeMeasureSpec(getWidth(), View.MeasureSpec.EXACTLY);
        finalint heightSpec = View.MeasureSpec.makeMeasureSpec(viewHeight, View.MeasureSpec.EXACTLY);
        while (fillDown && pos < itemCount){
            View view = viewCache.get(pos);
            if (view == null){
                view = recycler.getViewForPosition(pos);
                addView(view);
                measureChildWithDecorationsAndMargin(view, widthSpec, heightSpec);
                int decoratedMeasuredWidth = getDecoratedMeasuredWidth(view);
                layoutDecorated(view, 0, viewTop, decoratedMeasuredWidth, viewTop + viewHeight);
            } else {
                attachView(view);
                viewCache.remove(pos);
            }
            viewTop = getDecoratedBottom(view);
            fillDown = viewTop <= height;
            pos++;
        }
    }
    //метод вернет вьюшку с максимальной видимой площадьюprivate View getAnchorView(){
        int childCount = getChildCount();
        HashMap<Integer, View> viewsOnScreen = new HashMap<>();
        Rect mainRect = new Rect(0, 0, getWidth(), getHeight());
        for (int i = 0; i < childCount; i++) {
            View view = getChildAt(i);
            int top = getDecoratedTop(view);
            int bottom = getDecoratedBottom(view);
            int left = getDecoratedLeft(view);
            int right = getDecoratedRight(view);
            Rect viewRect = new Rect(left, top, right, bottom);
            boolean intersect = viewRect.intersect(mainRect);
            if (intersect){
                int square = viewRect.width() * viewRect.height();
                viewsOnScreen.put(square, view);
            }
        }
        if (viewsOnScreen.isEmpty()){
            returnnull;
        }
        Integer maxSquare = null;
        for (Integer square : viewsOnScreen.keySet()) {
            if (maxSquare == null){
                maxSquare = square;
            } else {
                maxSquare = Math.max(maxSquare, square);
            }
        }
        return viewsOnScreen.get(maxSquare);
    }
    @OverridepublicintscrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state){
        int delta = scrollVerticallyInternal(dy);
        offsetChildrenVertical(-delta);
        fill(recycler);
        return delta;
    }


Please note that inside fillUp (), views are added using the addView (view, 0) method, and not addView (view), as before. This was done in order to preserve the order of the elements inside the layout - the higher the view, the less its serial number should be.

Wow effect


At this point, we have a fully working LayoutManager, which behaves like a ListView. Now add the effect of scaling the bottom card. Just one method is enough for this!

privatevoidupdateViewScale(){
        int childCount = getChildCount();
        int height = getHeight();
        int thresholdPx = (int) (height * SCALE_THRESHOLD_PERCENT); // SCALE_THRESHOLD_PERCENT = 0.66f or 2/3for (int i = 0; i < childCount; i++) {
            float scale = 1f;
            View view = getChildAt(i);
            int viewTop = getDecoratedTop(view);
            if (viewTop >= thresholdPx){
                int delta = viewTop - thresholdPx;
                scale = (height - delta) / (float)height;
                scale = Math.max(scale, 0);
            }
            view.setPivotX(view.getHeight()/2);
            view.setPivotY(view.getHeight() / -2);
            view.setScaleX(scale);
            view.setScaleY(scale);
        }
    }

Place this method inside fill () last. It sets scale <1 for those views whose top border is below 2/3 of the screen. At the same time, the smaller the scale, the lower this boundary. Additionally, we shift the zoom focus (setPivotX and setPivotY) so that it becomes higher than the view itself. This allows you to create the effect that the bottom card is floating out from under the top.

If we start our project now, we will see that everything does not work exactly as expected: the bottom card is drawn on top of the top, although it was necessary on the contrary.


This is because the drawing order of the views in Android is determined by the order in which they are added. Fortunately, inverting the drawing order in RecyclerView is not at all difficult:

    recyclerView.setChildDrawingOrderCallback(new RecyclerView.ChildDrawingOrderCallback() {
            @OverridepublicintonGetChildDrawingOrder(int childCount, int i){
                return childCount - i - 1;
            }
        });

Well, now everything is in order.



The horizon is not littered!


Now that we know how to make a vertical LayoutManager, it will not be difficult for us to make a horizontal mode in its likeness. We need a class field where the current mode (orientation) will be stored, getter- and setter- for it. And also, you will need to implement similar methods fillLeft (), fillRight (), canScrollHorizontally (), scrollHorizontallyBy (), etc.

publicenum Orientation {VERTICAL, HORIZONTAL}
    private Orientation orientation = Orientation.VERTICAL;
    privateint mAnchorPos;
    publicvoidsetOrientation(Orientation orientation){
        View anchorView = getAnchorView();
        mAnchorPos = anchorView != null ? getPosition(anchorView) : 0;
        if (orientation != null){
            this.orientation = orientation;
        }
        requestLayout();
    }
    privatevoidfill(RecyclerView.Recycler recycler){
        View anchorView = getAnchorView();
        viewCache.clear();
        for (int i = 0, cnt = getChildCount(); i < cnt; i++) {
            View view = getChildAt(i);
            int pos = getPosition(view);
            viewCache.put(pos, view);
        }
        for (int i = 0; i < viewCache.size(); i++) {
            detachView(viewCache.valueAt(i));
        }
        switch (orientation) {
            case VERTICAL:
                fillUp(anchorView, recycler);
                fillDown(anchorView, recycler);
                break;
            case HORIZONTAL:
                fillLeft(anchorView, recycler);
                fillRight(anchorView, recycler);
                break;
        }
        //отправляем в корзину всё, что не потребовалось в этом цикле лэйаута//эти вьюшки или ушли за экран или не понадобились, потому что соответствующие элементы //удалились из адаптераfor (int i=0; i < viewCache.size(); i++) {
            recycler.recycleView(viewCache.valueAt(i));
        }
        updateViewScale();
    }
    @OverridepublicbooleancanScrollVertically(){
        return orientation == Orientation.VERTICAL;
    }
    @OverridepublicbooleancanScrollHorizontally(){
        return orientation == Orientation.HORIZONTAL;
    }


We will not give implementations of the fillLeft (), fillRight () methods and defining the scroll borders, because they are very similar to their “vertical” counterparts. Just change top to left and bottom to right and make layout full screen :). You can peek at the code in our github training project, which we mentioned at the beginning of the article. Also, we draw your attention to the fact that the position mAnchorPos is determined and stored inside setOrientation (), which is then used inside fill * () - methods to restore the current article when changing orientation.

Finally, you need to deal with the animation of the transition from vertical to horizontal mode. We will respond to clicks on the card and open the one on which we clicked. Since the left and right borders of the view in the vertical and horizontal modes always coincide, we will only need to animate its top and bottom. And the top and bottom of all its neighbors :)

Let's write a public method openView (int pos), the call of which will start the animation

publicvoidopenItem(int pos){
        if (orientation == Orientation.VERTICAL){
            View viewToOpen = null;
            int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                View view = getChildAt(i);
                int position = getPosition(view);
                if (position == pos){
                    viewToOpen = view;
                }
            }
            if (viewToOpen != null){
                openView(viewToOpen);
            }
        }
    }

And the animation itself inside a private openView (View view):

Animation
privatevoidopenView(final View viewToAnimate){
        final ArrayList<ViewAnimationInfo> animationInfos = new ArrayList<>();
        int childCount = getChildCount();
        int animatedPos = getPosition(viewToAnimate);
        for (int i = 0; i < childCount; i++) {
            View view = getChildAt(i);
            int pos = getPosition(view);
            int posDelta = pos - animatedPos;
            final ViewAnimationInfo viewAnimationInfo = new ViewAnimationInfo();
            viewAnimationInfo.startTop = getDecoratedTop(view);
            viewAnimationInfo.startBottom = getDecoratedBottom(view);
            viewAnimationInfo.finishTop = getHeight() * posDelta;
            viewAnimationInfo.finishBottom = getHeight() * posDelta + getHeight();
            viewAnimationInfo.view = view;
            animationInfos.add(viewAnimationInfo);
        }
        ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
        animator.setDuration(TRANSITION_DURATION_MS);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @OverridepublicvoidonAnimationUpdate(ValueAnimator animation){
                float animationProgress = (float) animation.getAnimatedValue();
                for (ViewAnimationInfo animationInfo : animationInfos) {
                    int top = (int) (animationInfo.startTop + animationProgress * (animationInfo.finishTop - animationInfo.startTop));
                    int bottom = (int) (animationInfo.startBottom + animationProgress * (animationInfo.finishBottom - animationInfo.startBottom));
                    layoutDecorated(animationInfo.view, 0, top, getWidth(), bottom);
                }
                updateViewScale();
            }
        });
        animator.addListener(new Animator.AnimatorListener() {
            @OverridepublicvoidonAnimationStart(Animator animation){}
            @OverridepublicvoidonAnimationEnd(Animator animation){
                setOrientation(Orientation.HORIZONTAL);
            }
            @OverridepublicvoidonAnimationCancel(Animator animation){}
            @OverridepublicvoidonAnimationRepeat(Animator animation){}
        });
        animator.start();
    }

ViewAnimationInfo is just a structure class for conveniently storing different values:

privatestaticclassViewAnimationInfo{
        int startTop;
        int startBottom;
        int finishTop;
        int finishBottom;
        View view;
    }


Here's what happens inside openView: for each view on the screen, we remember its top and bottom, as well as count its top and bottom, which this view should “leave” for. Then we create and run the ValueAnimator, which gives us progress from 0 to 1, based on which we count the top and bottom for each view during the animation and execute layoutDecorated (...) with the desired values ​​in each animation cycle. At that moment, when the animation is over, call setOrientation (Orientation.HORIZONTAL) to finally switch to horizontal mode. Seamlessly and imperceptibly.

Take a sample


It is a pity that it will not be possible to place all the useful information about the LayoutManager in just one article. If necessary, you can peek at something in our training project (for example, the implementation of smoothScrollToPosition ()), and you will have to look for something yourself.

In conclusion, I want to say that LayoutManager is an extremely powerful and flexible tool. RecyclerView + CustomLayoutManager has come to our aid more than once in solving very non-standard design tasks. It opens up spaces for animation of both the views themselves and the content in them. In addition, it greatly extends the possibilities of optimization. For example, if a user wants to execute smoothScroll () from the 1st element to the 100th element, it is not necessary to honestly scroll through all 99 elements. You can cheat and add the 100th element to the layout before scrolling, and then scroll to it, saving a bunch of resources!
However, LayoutManager is far from easy to learn from scratch. To use it effectively, you need to have a good idea of ​​how custom views are created, how measure / layout cycles work, how to use MeasureSpec, and so on in the same vein.

Related links:


A training project with an example LayoutManager: https://github.com/forceLain/AwesomeRecyclerView
3-part article about creating your LayoutManager, similar to the GridLayoutManager in English: http://wiresareobsolete.com/2014/09/building-a- recyclerview-layoutmanager-part-1 /

Also popular now: