10

I'm trying to replace my GridView with the new RecyclerView (using GridLayoutManager) but it seems like it doesn't cope well with gridLayoutAnimation (ClassCastException: LayoutAnimationController$AnimationParameters cannot be cast to GridLayoutAnimationController$AnimationParameters). It works with a regular layout animation, but because it's a grid, it takes too long to complete on tablets.

What I'm trying to accomplish is similar to Hierarchical Timing. If you look at the example video, it shows the layout animation go from top-left to down-right diagonally. A regular layout animation would execute the animation row after row, hence taking too much time to complete on bigger grids (e.g. tablets). I've also tried exploring ItemAnimator, but that would only run the animation on all cells simultaneously like it does in the "Don't" example.

Is there a way to accomplish this grid layout animation in a RecyclerView?

This is the gridview_layout_animation.xml:

<!-- replace gridLayoutAnimation with layoutAnimation and -->
<!-- replace column- and rowDelay with delay for RecyclerView -->

<gridLayoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
    android:columnDelay="15%"
    android:rowDelay="15%"
    android:animation="@anim/grow_in"
    android:animationOrder="normal"
    android:direction="top_to_bottom|left_to_right"
    android:interpolator="@android:interpolator/linear"
/>

And this is the animation grow_in.xml:

<set android:shareInterpolator="false"
 xmlns:android="http://schemas.android.com/apk/res/android">
    <scale
        android:interpolator="@android:interpolator/decelerate_quint"
        android:fromXScale="0.0"
        android:toXScale="1.0"
        android:fromYScale="0.0"
        android:toYScale="1.0"
        android:pivotX="50%"
        android:pivotY="50%"
        android:fillAfter="true"
        android:duration="400"
        android:startOffset="200"
    />
</set>

EDIT: Based on Galaxas0's answer, here is a solution which only requires you to use a custom view that extends RecyclerView. Basically only overriding the attachLayoutAnimationParameters() method. With this <gridLayoutAnimation> works as it did with GridView.

public class GridRecyclerView extends RecyclerView {

    public GridRecyclerView(Context context) {
        super(context);
    }

    public GridRecyclerView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public GridRecyclerView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    public void setLayoutManager(LayoutManager layout) {
        if (layout instanceof GridLayoutManager){
            super.setLayoutManager(layout);
        } else {
            throw new ClassCastException("You should only use a GridLayoutManager with GridRecyclerView.");
            }
        }

    @Override
    protected void attachLayoutAnimationParameters(View child, ViewGroup.LayoutParams params, int index, int count) {

        if (getAdapter() != null && getLayoutManager() instanceof GridLayoutManager){

            GridLayoutAnimationController.AnimationParameters animationParams =
                (GridLayoutAnimationController.AnimationParameters) params.layoutAnimationParameters;

            if (animationParams == null) {
                animationParams = new GridLayoutAnimationController.AnimationParameters();
                params.layoutAnimationParameters = animationParams;
            }

            int columns = ((GridLayoutManager) getLayoutManager()).getSpanCount();

            animationParams.count = count;
            animationParams.index = index;
            animationParams.columnsCount = columns;
            animationParams.rowsCount = count / columns;

            final int invertedIndex = count - 1 - index;
            animationParams.column = columns - 1 - (invertedIndex % columns);
            animationParams.row = animationParams.rowsCount - 1 - invertedIndex / columns;

        } else {
            super.attachLayoutAnimationParameters(child, params, index, count);
        }
    }
}
Musenkishi
  • 128
  • 1
  • 6

3 Answers3

6

LayoutAnimationController is coupled into ViewGroup and both ListView and GridView extend the method below to provide the child's animationParams. The issue is that GridLayoutAnimationController requires its own AnimationParameters that cannot be class-casted.

    /**
     * Subclasses should override this method to set layout animation
     * parameters on the supplied child.
     *
     * @param child the child to associate with animation parameters
     * @param params the child's layout parameters which hold the animation
     *        parameters
     * @param index the index of the child in the view group
     * @param count the number of children in the view group
     */
    protected void attachLayoutAnimationParameters(View child,
            LayoutParams params, int index, int count) {
        LayoutAnimationController.AnimationParameters animationParams =
                    params.layoutAnimationParameters;
        if (animationParams == null) {
            animationParams = new LayoutAnimationController.AnimationParameters();
            params.layoutAnimationParameters = animationParams;
        }

        animationParams.count = count;
        animationParams.index = index;
    }

Since this method by default adds a LayoutAnimationController.AnimationParameters instead of GridLayoutAnimationController.AnimationParameters, the fix should be to create and attach one beforehand. What we need to implement is what GridView already does:

@Override
protected void attachLayoutAnimationParameters(View child,
        ViewGroup.LayoutParams params, int index, int count) {

    GridLayoutAnimationController.AnimationParameters animationParams =
            (GridLayoutAnimationController.AnimationParameters) params.layoutAnimationParameters;

    if (animationParams == null) {
        animationParams = new GridLayoutAnimationController.AnimationParameters();
        params.layoutAnimationParameters = animationParams;
    }

    animationParams.count = count;
    animationParams.index = index;
    animationParams.columnsCount = mNumColumns;
    animationParams.rowsCount = count / mNumColumns;

    if (!mStackFromBottom) {
        animationParams.column = index % mNumColumns;
        animationParams.row = index / mNumColumns;
    } else {
        final int invertedIndex = count - 1 - index;

        animationParams.column = mNumColumns - 1 - (invertedIndex % mNumColumns);
        animationParams.row = animationParams.rowsCount - 1 - invertedIndex / mNumColumns;
    }
}

To replicate GridView, the closest thing we can do is shoehorn the modifications into onBindViewHolder() which allows them to run before dispatchDraw, the call that triggers animations.

ViewGroup.LayoutParams params = holder.itemView.getLayoutParams();
        GridLayoutAnimationController.AnimationParameters animationParams = new GridLayoutAnimationController.AnimationParameters();
        params.layoutAnimationParameters = animationParams;

        animationParams.count = 9;
        animationParams.columnsCount = 3;
        animationParams.rowsCount = 3;
        animationParams.index = position;
        animationParams.column = position / animationParams.columnsCount;
        animationParams.row = position % animationParams.columnsCount;

If using RecyclerView's new GridLayoutManager, try getting parameters from that. The sample above is a proof of concept to show that it works. I've hardcoded values that don't exactly work for my application as well.

Since this is an API that's been around since API 1 with no real documentation or samples, I would highly suggest against using it, considering there are many ways to replicate its functionality.

Aditya Vaidyam
  • 6,259
  • 3
  • 24
  • 26
  • Could you please explain how? I've tried doing it with the default ItemAnimator as well as downloading examples of different [ItemAnimators](https://github.com/gabrielemariotti/RecyclerViewItemAnimators/tree/master/library/src/main/java/it/gmariotti/recyclerview/itemanimator) but I can't seem to the animation I'm looking for. – Musenkishi Oct 29 '14 at 08:29
  • Have you made sure not to use `notifyDataSetChanged()`, and instead use `notifyItemRangeAdded()` and `notifyItemRangeRemoved()`? This should always trigger the first-load animation. – Aditya Vaidyam Oct 29 '14 at 14:44
  • Yes, I've tried using `notifyItemRangeAdded()` but as I described above, it's not giving me the desired behavior. `notifyItemRangeAdded()` will run the animation on the first row and **not** start on the second row until all cells in the first has begun its animation. The animation I could get from a `` could go diagonally across the rows, meaning it could start animating several rows at the same time. Essentially making all cells in the grid appear in a wave from top left to right bottom. – Musenkishi Oct 31 '14 at 08:14
  • Hmm, this is true. Have you tried creating the layout animation in code instead of xml? – Aditya Vaidyam Oct 31 '14 at 13:39
  • I've updated my answer with findings, and I'll update it again when I've come up with a solid solution. – Aditya Vaidyam Oct 31 '14 at 14:21
  • Good finding! I'm marking this answer as the correct one. I noticed I didn't have to touch `onBindViewHolder()`, I just extended `RecyclerView` and overrode `attachLayoutAnimationParameters()` as described in your answer. – Musenkishi Nov 04 '14 at 16:01
4

Simpler solution

TransitionManager.beginDelayedTransition(moviesGridRecycler);
gridLayoutManager.setSpanCount(gridColumns);
adapter.notifyDataSetChanged();

But don't forget to make your RecyclerAdapter setHasStableIds(true); and implement getItemID()

@Override
public long getItemId(int position) {
   return yourItemSpecificLongID;
}
Simon K. Gerges
  • 3,097
  • 36
  • 34
0

Quoting @Musenkishi at https://gist.github.com/Musenkishi/8df1ab549857756098ba

No clue. Are you calling recyclerView.scheduleLayoutAnimation(); after setting the adapter? And have you set android:layoutAnimation="@anim/your_layout_animation" to your <GridRecyclerView> in the layout?

This solved my issue.

Rafael Ruiz Muñoz
  • 5,333
  • 6
  • 46
  • 92