The "Radial reaction" Material Design pattern can be found here.

Right now, I am using a Subclassed implementation of RecyclerView to automatically provide the right number of rows and columns of Cards depending on the size of the device. In that implementation of RecyclerView's onBindViewHolder, I am adding the Card element of my layout to a custom AnimationHelper class which in turn should animate that Card when it is that Card's turn. My problem right now is determining when it is everyone's turn...

I've got the animation effect kind of working, but I need some help in figuring out how to more accurately represent the pattern. Right now, I've got a rapid "domino" effect, and it just doesn't look right. Upon further examination of the clip from the Material Documentation, it is obvious that the first cell is colored first, and then the cells below and to the right, continuing on with that pattern until all six cells have been darkened. This is where I need some help coming up with a methodology to, no matter how many rows and columns of cards there are, animate my cards to achieve this ever elusive visual design pattern.

Here is an imgur album with a few stills from the clip on the site to better understand how it is they achieved the effect.

Here is the link to the code on Github: github /halfjew22/AutoFitGridRecyclerView/tree/master/app/src/main/java/com/lustig/autofitgridrecyclerview (Sorry, without at least 10 rep, I can't post links)

Any help, assistance, constructive criticism of my code, and suggestions for a better way of implementing this animation are very welcome.

Below are the relevant code snippets:

~~~~~~Animation Helper~~~~~

package com.lustig.autofitgridrecyclerview.animations; import android.animation.Animator; import android.animation.ObjectAnimator; import android.os.Handler; import android.view.View; import java.util.ArrayList; /** * With this class, I am going to achieve the radial reaction effect described on the * Material Guidelines docs here: */ /** * Questions / Concerns * * How many animations can happen at one time? * How should I deal with that? * Will a simple boolean flag work for 2, 3 or more animations occurring in parallel? */ /** * I'm not worried about efficiency so much as actually doing what I want to do. * After I get something working, I can work on finding better ways to do things, * but if I bog myself down with trying to pre-optimize, ain't shit gon' get did... */ public class AnimationHelper { public static final long DEFAULT_DURATION_MS = 200; private static final String DEFAULT_PROPERTY_TO_ANIMATE = "alpha"; /* Type of property of View being animated */ private String mViewPropertyToAnimate = DEFAULT_PROPERTY_TO_ANIMATE; /* Duration of animations, set to default value, but can be changed */ private long mAnimationDuration = DEFAULT_DURATION_MS; /* Value to determine if an animation is currently occurring */ private boolean isCurrentlyAnimating = false; /* First, I need an animation queue. I should use a generic view, I'll try that first */ private ArrayList<View> mViewsToAnimate = new ArrayList<View>(); /* Next I'll need a method to add to the animation queue */ public void addViewToQueue(View viewToAnimate) { /** * If I've already animated the view, don't do it again */ if (viewToAnimate.getVisibility() == View.VISIBLE) { return; } mViewsToAnimate.add(viewToAnimate); /* This method is the meat and potatoes of this class */ startAnimationChain(); } /* This method will be in charge of starting the domino effect */ private void startAnimationChain() { /* If there is currently not an animation happening, start one */ if (mViewsToAnimate.size() >= 2 && !isCurrentlyAnimating) { animateSingleView(mViewsToAnimate.get(0)); /** * If we are currently animating, just wait. The next view animation should * automatically be spawned */ } else if (isCurrentlyAnimating) { // Just wait, animations should continue until the list is empty } } private void animateSingleView(final View v) { /* Right now, using a single animation, will change in the future */ ObjectAnimator animator = ObjectAnimator.ofFloat(v, mViewPropertyToAnimate, 0f, 1f); animator.setDuration(mAnimationDuration); animator.addListener( new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { v.setVisibility(View.VISIBLE); /* Remove the currently animating View from the list */ mViewsToAnimate.remove(v); /* Notify the class Object that an animation is happening */ isCurrentlyAnimating = true; /** * If, after removing the currently animating view, the list is not empty, * start the next animation after the current animation is halfway done. */ if (!mViewsToAnimate.isEmpty()) { /* Set a timer for (mAnimationDuration / 2) ms to start next animation */ final Handler handler = new Handler(); handler.postDelayed( new Runnable() { @Override public void run() { // Animate the first item in the list because each time // an animation starts, it is removed from the list if (!mViewsToAnimate.isEmpty()) { animateSingleView(mViewsToAnimate.get(0)); } } }, mAnimationDuration / 6); } } @Override public void onAnimationEnd(Animator animation) { /** * Setting this boolean flag could potentially cause issues as I'm going * to have to use a Runnable to wait some time before starting the next * animation. If there are any bugs, come back here, debug, and make sure * that this flag is behaving as expected */ /* Notify the class Object that the current animation has finished */ isCurrentlyAnimating = false; } @Override public void onAnimationCancel(Animator animation) { // Ignored intentionally } @Override public void onAnimationRepeat(Animator animation) { // Ignored intentionally } }); animator.start(); } }

~~~~~AutofitRecyclerView~~~~~

package com.lustig.autofitgridrecyclerview.recyclers; import android.content.Context; import android.content.res.TypedArray; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.AttributeSet; public class AutofitRecyclerView extends RecyclerView { private GridLayoutManager manager; private int columnWidth = -1; public AutofitRecyclerView(Context context) { super(context); init(context, null); } public AutofitRecyclerView(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public AutofitRecyclerView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context, attrs); } private void init(Context context, AttributeSet attrs) { if (attrs != null) { int[] attrsArray = { android.R.attr.columnWidth }; TypedArray array = context.obtainStyledAttributes(attrs, attrsArray); columnWidth = array.getDimensionPixelSize(0, -1); array.recycle(); } manager = new GridLayoutManager(getContext(), 1); setLayoutManager(manager); } @Override protected void onMeasure(int widthSpec, int heightSpec) { super.onMeasure(widthSpec, heightSpec); if (columnWidth > 0) { int spanCount = Math.max(1, getMeasuredWidth() / columnWidth); manager.setSpanCount(spanCount); } } }

~~~~~Adapter and ViewHolder implementation~~~~~