Building a RecyclerView LayoutManager – Part 3

This article is Part 3 in our series. Here are links to Part 1 and Part 2 as well.

In the previous post, we discussed adding proper support for data set changes and targeted scrolling. In this installment of the series, we will focus on properly supporting animations in your fancy new LayoutManager.

In case you’ve forgotten, the code samples are on GitHub.

The Problem With Free

We talked about notifyDataSetChanged() the last time, but you may have noticed that changing the data in this way doesn’t animate the change.[1] RecyclerView includes a new API for making animated changes, which requires you to notify the adapter which positions in the adapter have changed, and what the action was:

  • notifyItemInserted() and notifyItemRangeInserted(): Insertion of new item(s) at the given position(s).

  • notifyItemChanged() and notifyItemRangeChanged(): Invalidate he item(s) at the given position(s), nothing structural has changed in the data set.

  • notifyItemRemoved() and notifyItemRangeRemoved(): Removal of the item(s) at the given position(s).

  • notifyItemMoved(): An item has relocated to a new position in the data set.

By default, your LayoutManager will get “simple item animations” for free when these methods are used. These animations are simply based on whether each current view position is still present in the layout after a change. New views are faded in, removed views are faded out, and other views are moved to their new location. Here’s what our grid layout looks like with the free animations:

resize?url=http%3A%2F%2Fwiresareobsolete.com%2Fwp content%2Fuploads%2F2015%2F02%2FDefaultRecyclerAnimationsSmall
Figure 1. Default Simple Item Animations

The problem here is that several items fade out that weren’t removed. This is because they are no longer visible inside the parent RecyclerView bounds. We would like the views to slide out of view towards where the user would expect them to go, but at this stage the framework only knows that our code didn’t lay them out again after the data set change took place. In addition, new views are fading in as if they were added. It would be better if these views slid into place from their expected locations as well.

The framework needs our help–we have to add a bit more to the LayoutManager…​

Predictive Item Animations

The following animation represents what conceptually ought to happen when an item is removed:

resize?url=http%3A%2F%2Fwiresareobsolete.com%2Fwp content%2Fuploads%2F2015%2F02%2FRecycleConceptSmall
Figure 2. Removal Animation Concept

Notice particularly how the items on the left have to slide up and to the right to fill the gap on the previous row. You can imagine the reverse happening for an item added in this location.

As we discussed in the first post of the series, onLayoutChildren() is typically only called once by the parent RecyclerView during the initial layout or when the data set size (i.e. item count) changes. The predictive item animations feature allows us to provide a more meaningful description of how the views should transition based on changes in the data. We need to start by indicating to the framework that our LayoutManager is able to provide this additional data:

@Override
public boolean supportsPredictiveItemAnimations() {
    return true;
}

With this one change, onLayoutChildren() will now be called twice for each batch of data set changes–first as a “pre-layout” phase, and again for the real layout.

What Should I Do During Pre-Layout?

During the pre-layout phase of onLayoutChildren(), you should run your layout logic to set up the initial conditions for the change animation. This means that you need to lay out all the views that were currently visible before the change AND any additional views that you know will be visible after the animation runs (these are termed APPEARING views). These extra appearing views should be laid out in the off-screen positions where the user would expect them to be coming from. The framework will capture these positions and use them to animate the new views into place instead of doing a simple fade-in.

We can check which layout phase we are in via RecyclerView.State.isPreLayout()

In the FixedGridLayoutManager example, we use pre-layout to determine how many visible views are being removed as a result of the data set change. Removed views are still returned from the Recycler in pre-layout, so you can lay them out in their original location and not have to worry about accounting for an empty space. To indicate future removal to you, LayoutParams.isViewRemoved() will return true for the given view. Our example counts the number of removed views so we have a rough idea of how much space will get filled by appearing views.

@Override
public void onLayoutChildren(RecyclerView.Recycler recycler,
                             RecyclerView.State state) {
    ...

    SparseIntArray removedCache = null;
    /*
     * During pre-layout, we need to take note of any views that are
     * being removed in order to handle predictive animations
     */
    if (state.isPreLayout()) {
        removedCache = new SparseIntArray(getChildCount());
        for (int i=0; i < getChildCount(); i++) {
            final View view = getChildAt(i);
            LayoutParams lp = (LayoutParams) view.getLayoutParams();

            if (lp.isItemRemoved()) {
                //Track these view removals as visible
                removedCache.put(lp.getViewPosition(), REMOVE_VISIBLE);
            }
        }
        ...
    }

    ...

    //Fill the grid for the initial layout of views
    fillGrid(DIRECTION_NONE, childLeft, childTop,
             recycler, state.isPreLayout(), removedCache);

    ...
}

During pre-layout, RecyclerView attempts to map the adapter positions of your views to their “old” locations (meaning before the data set change). When you ask for a view by position, expect that position to be the initial position of that item view. Beware of trying to transform them yourself between pre-layout and “real” layout.

The final change in the example comes as a modification to fillGrid() in which we will attempt to lay out “N” additional views (per row) as appearing views, where N is the number of visible views being removed. These views will always be filled in from the right on a removal, so they are computed as the positions following the last visible column:

private void fillGrid(int direction,
                      int emptyLeft,
                      int emptyTop,
                      RecyclerView.Recycler recycler,
                      boolean preLayout,
                      SparseIntArray removedPositions) {
    ...

    for (int i = 0; i < getVisibleChildCount(); i++) {
        int nextPosition = positionOfIndex(i);

        ...

        if (i % mVisibleColumnCount == (mVisibleColumnCount - 1)) {
            leftOffset = startLeftOffset;
            topOffset += mDecoratedChildHeight;

            //During pre-layout, on each column end, apply extra appearing views
            if (preLayout) {
                layoutAppearingViews(recycler, view, nextPosition,
                        removedPositions.size(), ...);
            }
        } else {
            leftOffset += mDecoratedChildWidth;
        }
    }

    ...
}

private void layoutAppearingViews(RecyclerView.Recycler recycler,
                                  View referenceView,
                                  int referencePosition,
                                  int extraCount,
                                  int offset) {
    //Nothing to do...
    if (extraCount < 1) return;

    for (int extra = 1; extra <= extraCount; extra++) {
        //Grab the next position after the reference
        final int extraPosition = referencePosition + extra;
        if (extraPosition < 0 || extraPosition >= getItemCount()) {
            //Can't do anything with this
            continue;
        }

        /*
         * Obtain additional position views that we expect to appear
         * as part of the animation.
         */
        View appearing = recycler.getViewForPosition(extraPosition);
        addView(appearing);

        //Find layout delta from reference position
        final int newRow = getGlobalRowOfPosition(extraPosition + offset);
        final int rowDelta =
                newRow - getGlobalRowOfPosition(referencePosition + offset);
        final int newCol = getGlobalColumnOfPosition(extraPosition + offset);
        final int colDelta =
                newCol - getGlobalColumnOfPosition(referencePosition + offset);

        layoutTempChildView(appearing, rowDelta, colDelta, referenceView);
    }
}

Inside the layoutAppearingViews() helper, each additional appearing view is laid out at it’s “global” position (i.e. the row/column position it would occupy in the grid). This location is off-screen, but gives the framework the data it needs to produce a starting point for the animation to slide these views in.

Changes for the “Real” Layout

We’ve already discussed the basics of what to do during your layout in Part 1, but we’ll have to tweak the formula a bit with our animation support added. The one additional step will be to determine if we have any disappearing views. In our example, this is done by running a normal layout pass, and then determining if there are any views left in the Recycler’s scrap heap.

We can use the scrap heap in this way because our layout logic always calls detachAndScrapAttachedViews() before starting each layout pass. As discussed previously, this is the best practice to adhere to in your layouts.

Views still in scrap that aren’t considered removed are disappearing views. We need to lay these views out in their off-screen positions so the animation system can slide them out of view (instead of just fading them out).

@Override
public void onLayoutChildren(RecyclerView.Recycler recycler,
                             RecyclerView.State state) {
    ...

    if (!state.isPreLayout() && !recycler.getScrapList().isEmpty()) {
        final List<RecyclerView.ViewHolder> scrapList = recycler.getScrapList();
        final HashSet<View> disappearingViews = new HashSet<View>(scrapList.size());

        for (RecyclerView.ViewHolder holder : scrapList) {
            final View child = holder.itemView;
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            if (!lp.isItemRemoved()) {
                disappearingViews.add(child);
            }
        }

        for (View child : disappearingViews) {
            layoutDisappearingView(child);
        }
    }
}

private void layoutDisappearingView(View disappearingChild) {
    /*
     * LayoutManager has a special method for attaching views that
     * will only be around long enough to animate.
     */
    addDisappearingView(disappearingChild);

    //Adjust each disappearing view to its proper place
    final LayoutParams lp = (LayoutParams) disappearingChild.getLayoutParams();

    final int newRow = getGlobalRowOfPosition(lp.getViewPosition());
    final int rowDelta = newRow - lp.row;
    final int newCol = getGlobalColumnOfPosition(lp.getViewPosition());
    final int colDelta = newCol - lp.column;

    layoutTempChildView(disappearingChild, rowDelta, colDelta, disappearingChild);
}
Laying out views (and, thus, adding them to the container) removes them from the scrap list. Be careful to note the views you need from scrap before you start making changes, or you will end up with concurrent modification issues on the collection.

Similar to our code for the appearing views, layoutDisappearingView() places each remaining view at it’s “global” position as the final layout location. This gives the framework the information that it needs to slide these views out in the proper direction during the animation.

The following image should help to visualize the FixedGridLayoutManager example:

  • The black box represents the RecyclerView visible bounds.

  • Red View: Item removed from the data set.

  • Green Views (Appearing views): Not initially present, but laid out off-screen during pre-layout.

  • Purple Views (Disappearing views): Initially placed in their original locations during pre-layout, then laid out off-screen during the “real” layout phase.

resize?url=http%3A%2F%2Fwiresareobsolete.com%2Fwp content%2Fuploads%2F2015%2F02%2FRecycleRemove
Figure 3. Simple Remove Animation

Reacting to Off-Screen Changes

You may have noticed that our ability to determine a removal change in the last section hinged on the visible views. What if the change occurs outside the visible bounds? Depending on your layout structure, a change like this may still require you to adjust the layout for a better animation experience.

Luckily, the adapter posts these changes to your LayoutManager as well. You can override onItemsRemoved(), onItemsMoved(), onItemsAdded(), or onItemsChanged() to react to these events even if they occur in a view range that isn’t reflected in the current layout. These methods will give you the position and range of the change.

When the removed range occurs outside the visible area, onItemRemoved() is called before pre-layout. This allows us to collect data about the change that we may need in order to best support any appearing view changes that might be caused by this event.

In our example, we collect these removals in the same way as before, but mark them with a different type.

@Override
public void onItemsRemoved(RecyclerView recyclerView,
                           int positionStart,
                           int itemCount) {
    mFirstChangedPosition = positionStart;
    mChangedPositionCount = itemCount;
}

@Override
public void onLayoutChildren(RecyclerView.Recycler recycler,
                             RecyclerView.State state) {
    ...

    SparseIntArray removedCache = null;
    /*
     * During pre-layout, we need to take note of any views that are
     * being removed in order to handle predictive animations
     */
    if (state.isPreLayout()) {
        ...

        //Track view removals that happened out of bounds (i.e. off-screen)
        if (removedCache.size() == 0 && mChangedPositionCount > 0) {
            for (int i = mFirstChangedPosition;
                    i < (mFirstChangedPosition + mChangedPositionCount); i++) {
                removedCache.put(i, REMOVE_INVISIBLE);
            }
        }
    }

    ...

    //Fill the grid for the initial layout of views
    fillGrid(DIRECTION_NONE, childLeft, childTop,
             recycler, state.isPreLayout(), removedCache);

    ...
}
This method is sill called when the removed items are visible. In that case, however, it is called after pre-layout. This is why our example still gathers data from the visible removed views when they are present.

With all this in place, we can run the sample application again. We can see the disappearing items on the left sliding off to rejoin the end of their previous rows. The new appearing items on the right slide properly into place alongside the existing grid. Now, the only view fading out in our new animation is the view that was actually removed!

resize?url=http%3A%2F%2Fwiresareobsolete.com%2Fwp content%2Fuploads%2F2015%2F02%2FPredictiveRecyclerAnimationsSmall
Figure 4. Predictive Removal Animation

More To Come…

This was supposed to be the end of this series, I swear! However, there were some interesting issues that came up in building the animations that are specific to the FixedGridLayoutManager use case, and not necessarily all custom implementations. So in the next (and final…​I promise this time) post, I’ll address what those challenges were.

A special thanks to Yiğit Boyar for providing much of the technical input that made this post possible!


1. The framework will attempt to animate views if your adapter uses stable IDs, which provides enough data to guess which views are removed/added/etc.
 

Dave

Dave Smith is an embedded software developer based in Denver, CO and head geek Wireless Designs, LLC. He has been focused on the Android platform since 2009. If you would like to hear more from Dave, you can follow him on Twitter @devunwired. You can also find him on Google+.

 
  • mzgreen

    Great post, thanks! 🙂
    There is one thing that I would like to ask – you are removing only one item but three items below it are also moved to the end of the list. Shouldn’t these three items just slide up and the rest of the views fix their positions accordingly? Is there a reason for such behavior or maybe you just designed your component to work this way?

    Looking forward to the next part.

  • In this case, the layout design is such that adapter positions are laid out left to right in row (0-5 in Row #1, 6-10 in Row #2, 11-15 in Row #3, etc.) so the “previous” item position for elements in the left-hand row is to slide them up to the end of the previous rows. If you were to decide to lay out the positions top to bottom, then what you’re saying would be true and items from the top row would slide to the bottom row to fill in the gaps.

  • Ivan Gaglioti

    Hi Dave, great post thanks!
    I have a question about a “not desired” behavior…i’ll try to explain my issue: my view item used with the recycler has a (bottom) part hidden as default that user can see by toggling on the head area (normally is a button “info”) of item itself, the problem is that when the item shows this hidden area it scrolls up…what can i do to make “fixed” the item when a part of its are change the visibility?
    Thanks in advance.

  • What layout manager are you using? Does your adapter use stable ids? Is the problem that the view animates to a new position when it changes size, or does it jump?

  • Ivan Gaglioti

    i use a default instance of LinearLayoutManager.

    No i don’t use stable ids as i can’t because I use a (custom) CursorAdapter for Recycler.Adapter with observers (https://github.com/ivangag/SMCheck/blob/Material/app/src/main/java/org/symptomcheck/capstone/adapters/CursorRecyclerAdapter.java).
    Here my view layout used for the item in the RecyclerView
    (https://github.com/ivangag/SMCheck/blob/Material/app/src/main/res/layout/fragment_google_cardview_checkins.xml)
    In short, when the user click the ImageButton i set the visibility of bottom portion of layout (basically a listview) to GONE.
    The problem is that when the portion of item view become visible the item itself is scrolled slightly upward, i.e. it moves (slightly) from its original position and list scrolls.

    Thanks.

  • Hmm, I can’t say for sure. If the view change you are seeing is an animation, you could try clearing the default item animator. RecyclerView may be trying to resolve the layout change it sees (changing to GONE forces a layout pass) with a smooth animation. If it’s not an animated change, I’m surprised that the default layout managers would not attempt to preserve the item’s initial position during a new layout pass. I don’t see an actual RecyclerView in the code you posted, but that’s my best guess.

  • Ivan Gaglioti

    Actually i use a RecyclerView and i disabled all my custom animation (question: when you speak about default animation do you mean the RecyclerView one?)

    This is the xml definition of recycler used in a layout inflated
    ….

    At the code side:

    mRecyclerView = (RecyclerView) getActivity().findViewById(R.id.checkin_recycler_view);

    // use this setting to improve performance if you know that changes
    // in content do not change the layout size of the RecyclerView
    mRecyclerView.setHasFixedSize(true); // i tried also with the ‘false’ value but it doesn’t change behavior

    // use a linear layout manager
    mLayoutManager = new LinearLayoutManager(getActivity());
    mRecyclerView.setLayoutManager(mLayoutManager);
    // specify an adapter
    //mAdapter = new MyAdapter(myDataset);

    mRecyclerView.setAdapter(mAdapter);
    where the Adapter is an implementation of ViewHolder with the plus of Cursor handling.
    https://github.com/ivangag/SMCheck/blob/Material/app/src/main/java/org/symptomcheck/capstone/adapters/CheckInRecyclerCursorAdapter.java

    Thanks again for your hints…

  • Doing my best to run your code as-written, I am not seeing the problem I believe you are describing. However, one thing I notice in your code is an attempt to manually trigger requestLayout() in the adapter, rather than just toggling the view visibility. There be dragons when invoking the layout system manually that may be resetting some of the state in RecyclerView.

  • Ivan Gaglioti

    hi Dave, thanks for answering.. the truth is that I really toggled visibility.. but meanwhile I changed the code by trying to handle and modify dimensions of layout.. it does work better, i.e the view item doesn’t flip up, even though i can see that status of an item sometime is lost, i.e if I change the the number of items the view in the position N lose the previous layout (of that I modified dimensions ) and it is assigned to another one… but here I think is a my problem with the handling of how recycler works..

  • Дмитрий Филюстин

    Hi. Thanks for the articles (yet to read). Dave, I want to build a coverflow(http://cdn.cultofmac.com/wp-content/uploads/2010/10/Screen-shot-2010-10-05-at-3.58.34-PM.jpg) list with the RecyclerView. Do you think it’s possible?

  • Stephen Gutknecht

    Dave or others: I kind of have the opposite problem. Using your game scores example for the Playground sample. What if a score changes due to a Live Game in progress? What I want to know – is that particular score currently visible in the RecyclerView? From outside the View – regardless of position – I want to be able to access the ViewHolder objects of ONLY the subset that are visible. Thanks for your great tutorial.

  • Stephen Gutknecht

    The LayoutManager does know the visible positions – so – could the SimpleAdapter in the sample be expanded to have the object of it’s own LayoutManager available?

  • The LayoutManager is really designed to be isolated from the Adapter through the Recycler, so I’m not sure that force linking them together would be the best idea. Why would you be worried about updating adapter data for views that aren’t visible? In the example you gave (game scores), if the score has changed, update it. The adapter will reflect the change to the view.

    Are you worried about potential additional layout passes when notifyDataSetChanged() triggers? I’d have to say in most cases, that is not something you should worry about. Especially if you do a layout pass like this example, where existing views are simply detached/attached when nothing significant has changed…this is very fast.

    If you truly need to optimize it, the adapter could use the notifyItemChanged() methods instead. The LayoutManager can learn of what the change was (what position had the game score update) and act accordingly. If the position was not visible, it doesn’t necessarily need to do another layout right this second.

  • 言者歌途

    It’s really a great post~But some places confused me。
    As your figure “Simple remove animation” described, When we remove one visible item, in the pre-layout turn, we must layout one item which is off-screen now after the last visible column item per row. And in the real-layout turn, the APPEARING items will be slided into screen.
    Let me try to simulate removing 2 items.
    For example: here is a small 4 * 4 grid, only 3 * 3 area could be seen, and the first and the second items will be deleted.
    So the real index will be like this:
    1 2 3 4
    5 6 7 8
    9 10 11 12
    13 14 15 16
    but we can only see
    1 2 3
    5 6 7
    9 10 11
    When we remove 1 and 2, In the pre-layout turn.
    the real index will be like this:
    1 2 3 4 5 6
    7 8 9 10 11 12
    13 14 15 16
    and then in the real-layout turn
    the index will be like this:
    3 4 5 6
    7 8 9 10
    11 12 13 14
    15 16
    and we can only see
    3 4 5
    7 8 9
    11 12 13
    So the animation start from
    1 2 3 &nbsp &nbsp&nbsp &nbsp&nbsp&nbsp &nbsp&nbsp 3 4 5
    5 6 7 &nbsp&nbsp&nbsp to&nbsp&nbsp&nbsp 7 8 9
    9 10 11 &nbsp&nbsp&nbsp &nbsp&nbsp&nbsp 11 12 13
    Am I right?

  • You are correct about the initial and final states, but the transition states are a bit off. During pre-layout, you would need to lay out views in their initial locations PLUS any views that WILL BE APPEARING (off-screen, but in the place where they are expected to be off-screen). That would look like this for your example:

    01 02 03 04
    05 06 07 08
    09 10 11 12
    13
    …the APPEARING views are (04, 08, 12, 13). In the example you gave, you put more columns into pre-layout than actually exist in the real layout. Views such as 05 or 09 are not in their proper INITIAL locations in your pre-layout diagram.

    Then during real layout, you need to lay views out in their final locations PLUS any PREVIOUSLY LAID OUT views that have DISAPPEARED (no longer on-screen):
    03 04 05 06
    07 08 09 10
    11 12 13
    …the DISAPPEARING views are (06, 10). Here your diagram was more correct, you’re just laying out too many views.

  • 言者歌途

    Thanks for your reply~You’re right, I made a mistake. I forgot the visible area was only 3 * 3. But there is still one problem I don’t understand.

    In your passage, you mentioned that “The final change in the example comes as a modification to fillGrid() in which we will attempt to lay out “N” additional views (per row) as appearing views, where N is the number of visible views being removed. ”

    And the code is accordant to it.

    //During pre-layout, on each column end, apply any additional appearing views
    if (preLayout) {
    layoutAppearingViews(recycler, view, nextPosition, removedPositions.size(), …);

    So when we remove two items, should it not to be two APPEARING views per row?
    If it’s right, the martix should be like below in the pre-layout turn.
    01 02 03 04 05
    06 07 08 09 10
    11 12 13 14 15
    the APPEARING views are (04, 05, 09, 10, 14, 15)
    then during real layout, the matrix should be like this:
    03 04 05 06
    07 08 09 10
    11 12 13 14
    the DISAPPEARING views are (06, 10, 14)
    I feel more confused now…

  • You are correct that the code attempts to lay out N additional views on each row, where N is equal to the removed count. But the code will not break the structure of the grid to do so. Inside that method, the “global” position is always computed so we know which row/column to place the views.

    In other words, layoutAppearingViews() will indeed add the positions (04, 05, 08, 09, 12, 13)…but it will add them in their proper place based on their positions in the grid. The won’t get arbitrarily placed at the end of a row, they get placed at their correct “global” row/column.

    If we look at just view 05 for a minute, think about what would happen if it was appended to the end of Row 1 during pre-layout (instead of laid out in it’s initial location…which is in Row 2), then moved into it’s correct position during real layout. The animation would not slide smoothly from one place to the other. It would jump offscreen and then slide back into place.

    Having said that, there is a small bug in this current code that will lay out views twice if they are part of that “width + N” and also already visible (like views 05 and 09 in this example). This is causing extra work, but doesn’t affect the final animation. I’ll fix that.

  • 言者歌途

    Thanks very much~I get it now~
    One more question, can we define the animation ourselves?

  • You don’t need to customize the layout manager to change the animation behavior, so all of this discussion doesn’t apply. That’s what the ItemAnimator is for. You just set the ItemAnimator on the RecyclerView that implements the behavior you want.

  • Alan

    I cannot see the pictures in this post,The pictures are broken.

  • Alan, thank you for pointing this out. The problem should now be fixed.

  • Marek Hlava

    Hi Dave, I know it’s a pretty old article but it helped me a lot with building my own table layout manager. So thank you.
    One question though, do you know how to enable scrolling in only one direction at a time? I tried to set time constraints, e.g. you cannot scroll horizontally if vertical scroll occurred within last 100ms. The problem is that canScrollHorizontally() is always called before canScrollVertically() even though the horizontal scroll is only by one pixel. Any idea how to solve this?

  • Deli Riom

    Hi, I have been trying to make this exact issue work on a simple vertical list (only animate new views, instead of all appearing ones).
    I tried using your FixedGridLayoutManager with just one column, but there is one issue, and I’m clearly not skilled enough to fix it.
    Actually my items appear in a certain order, so when a new one is added it can be offscreen, so I scroll to its position. And there it goes all weird:
    Still using FixedGridLayoutManager, if i call scrollToPosition(), all appearing items are animated (like with the regular LinearLayoutManager. Btw, do you know why?).
    And if I call smoothScrollToPosition(), only one item is animated, BUT not the right one. The one that gets animated is the first view that was hidden in the direction of the scroll. Example: views 0, 1, 2 are hidden, 3 and on are visible. I add a view to pos 0, everything scrolls fine and 0, 1, 2, 3 (the hidden ones + the new one) become visible, but instead of animating the new view at 0, the animation is run on holder 3.
    Do you have any idea why this could happen?
    And also, why the behaviour changes so much when using one or the other scroll functions? The documentations says to override onLayoutChildren, nothing about scrolling…
    Thanks in advance for your help.

  • I’m not sure I have a good solution for you, other than to caution that scrolling at the same time as an item change is probably not smart. Both of those operations trigger a layout change, and the animation logic attempts to take snapshots of the view both before and after an item change to determine animation types and paths. If you scroll at the same time, you will likely throw off the snapshot of the animator when it determines which positions and item views should remain. At a minimum, may be try posting the animation to the next frame so it starts after the scroll change is complete.

  • Deli Riom

    Hi, Thank you for your answer!
    I removed the scrolling, but I still had the same problem (based on logs, since I didn’t see it on the screen anymore). So I wen back to your project and added the following class, for purely logging purposes:

    public class MyDefaultItemAnimator extends DefaultItemAnimator {

    @Override

    public void onAddFinished(RecyclerView.ViewHolder item) {

    super.onAddFinished(item);

    Log.d(“EELOG”, “add finished for item “+item.getAdapterPosition());

    }

    @Override

    public boolean animateAdd(RecyclerView.ViewHolder holder) {

    Log.d(“EELOG”, “starting animateAdd() for holder ” + holder.getLayoutPosition());

    return super.animateAdd(holder);

    }

    @Override

    public boolean animateRemove(RecyclerView.ViewHolder holder) {

    Log.d(“EELOG”, “starting animateRemove() for holder ” + holder.getLayoutPosition());

    return super.animateRemove(holder);

    }

    }

    and then in your RecyclerFragment, line 46 (onCreateView()):

    mList.setItemAnimator(new MyDefaultItemAnimator());

    Then I launched the app, and went to the Fixed Two-Way list, with a single row of 5 items initially (I think it was the default setup).
    I scrolled manually to position 3 (so that 0 and 1 are hidden), then added a view to position 0, and the log said the view that was animated was 2 (so again, the first hidden one)

    Is this the intended behaviour?

    Thanks in advance for your time

  • Yes, that is expected. In a traditional vertical list (e.g. LinearLayoutManager), adding an item off-screen has no animation effect at all (because the structure of the visible items don’t change). In the grid manager from the sample, all item changes (visible or not) affect the structure of the grid (it’s less visible why with only a single row) as items flow across onto different rows.

    Regarding the logs you see, the animator is reporting the views to be animated, not the adapter item position. In the case you created (Item #3 is the first visible), adding a new adapter item at Position #0 shifts all the views after it (in that row) to the right. Which means Item #3 shifts to the right and Item #2 slides into it’s place. Item #2 is not currently in the layout (it was recycled when you scrolled it off-screen), so it has to be added back to the layout as part of the item change animation—thus the logs you are seeing.

  • Anand Vardhan

    Hello Dave, I am stuck on a issue regarding RecyclerView. The issue is stated in the link : http://stackoverflow.com/questions/36632091/having-trouble-with-onitemtouchlistener-of-recyclerview-in-android. Please have a look at it.

  • I think I would start by simplifying your click listener logic. A much more direct approach is to avoid touch listeners and use the ViewHolder as a click listener for the item view and/or child views. The RV Playground app used in this post does this as well in SimpleAdapter (link). I think part of your problem is you are getting bit by the fact that touch listeners are additive, and your code is doubling them up in some cases.

    After that, you might look into how your edits are affecting the data set. When you make a change to an item, you should only need to update that item in the adapter and notify of a change. You should not be re-initializing the entire RecyclerView every time.

    Perhaps try those two things and then see where you end up.

  • Anand Vardhan

    Really Thank You Dave, I will implement as said and let you know about the outcome.
    Thank You.

  • Sachin Rajput

    hello i am creating one simple example of recycler view , each row in recycler view has one image and one textview and one button , everything is coming fine till here on clicking of the button i am incrementing the value of that text view just like count , textview value is updating perfectly , but when i am scrolling it and coming back so textview values are misplaced 🙁 , can u help plz

  • Олег (Beloo)

    Hi, Dave.Is it possible to force animations of recycler view from scrollVerticallyBy? I have to make corrections there, but in that case, if i called detachAndScrapAttachedViews() and then fill() those views would be immediate being replaced in a right place, without animation. onLayout() there occasionally not force onLayoutChildren() as well

  • Oleg (Beloo)

    I answered on own question here http://stackoverflow.com/a/40020911/3379437. Maybe it will help someone

  • Yasir Ali

    Hi Dave. Is it possible to force animation on notifyDataSetChanged? actually I would like to perform insert and remove animations same time. I have list of items and I add/remove some items and would like to perform animation by notifyDataSetChanged. is it possible ?

  • Calling notifyDataSetChanged() will trigger animations if your adapter has stable ids enabled (see RecyclerView.Adapter.setHasStableIds()).

    You are probably looking for methods like notifyItemInserted() and notifyItemRemoved(), which will trigger animations automatically in any case. Animations always trigger on the beginning of the next draw frame, so you can call these methods multiple times in the same block and the animations will all run together.

  • Yasir Ali

    Hi Dave,
    Thanks for your reply and Adapter.setHasStableIds() resolve my issue. Now I have general question it is possible to get RecyclerView.State false even I return true from supportsPredictiveItemAnimations ?