Search code examples
androidandroid-recyclerviewlayout-animation

How do I use a GridLayoutAnimation in a RecyclerView?


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);
        }
    }
}

Solution

  • 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.