Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom circular reveal transition results in "java.lang.UnsupportedOperationException" when paused?

I created a custom circular reveal transition to use as part of an Activity's enter transition (specifically, I am setting the transition as the window's enter transition by calling Window#setEnterTransition()):

public class CircularRevealTransition extends Visibility {
    private final Rect mStartBounds = new Rect();

    /**
     * Use the view's location as the circular reveal's starting position.
     */
    public CircularRevealTransition(View v) {
        int[] loc = new int[2];
        v.getLocationInWindow(loc);
        mStartBounds.set(loc[0], loc[1], loc[0] + v.getWidth(), loc[1] + v.getHeight());
    }

    @Override
    public Animator onAppear(ViewGroup sceneRoot, final View v, TransitionValues startValues, TransitionValues endValues) {
        if (endValues == null) {
            return null;
        }
        int halfWidth = v.getWidth() / 2;
        int halfHeight = v.getHeight() / 2;
        float startX = mStartBounds.left + mStartBounds.width() / 2 - halfWidth;
        float startY = mStartBounds.top + mStartBounds.height() / 2 - halfHeight;
        float endX = v.getTranslationX();
        float endY = v.getTranslationY();
        v.setTranslationX(startX);
        v.setTranslationY(startY);

        // Create a circular reveal animator to play behind a shared
        // element during the Activity Transition.
        Animator revealAnimator = ViewAnimationUtils.createCircularReveal(v, halfWidth, halfHeight, 0f,
                FloatMath.sqrt(halfWidth * halfHeight + halfHeight * halfHeight));
        revealAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                // Set the view's visibility to VISIBLE to prevent the
                // reveal from "blinking" at the end of the animation.
                v.setVisibility(View.VISIBLE);
            }
        });

        // Translate the circular reveal into place as it animates.
        PropertyValuesHolder pvhX = PropertyValuesHolder.ofFloat("translationX", startX, endX);
        PropertyValuesHolder pvhY = PropertyValuesHolder.ofFloat("translationY", startY, endY);
        Animator translationAnimator = ObjectAnimator.ofPropertyValuesHolder(v, pvhX, pvhY);

        AnimatorSet anim = new AnimatorSet();
        anim.setInterpolator(getInterpolator());
        anim.playTogether(revealAnimator, translationAnimator);
        return anim;
    }
}

This works OK normally. However, when I click the "back button" in the middle of the transition, I get the following exception:

Process: com.adp.activity.transitions, PID: 13800
java.lang.UnsupportedOperationException
        at android.view.RenderNodeAnimator.pause(RenderNodeAnimator.java:251)
        at android.animation.AnimatorSet.pause(AnimatorSet.java:472)
        at android.transition.Transition.pause(Transition.java:1671)
        at android.transition.TransitionSet.pause(TransitionSet.java:483)
        at android.app.ActivityTransitionState.startExitBackTransition(ActivityTransitionState.java:269)
        at android.app.Activity.finishAfterTransition(Activity.java:4672)
        at com.adp.activity.transitions.DetailsActivity.finishAfterTransition(DetailsActivity.java:167)
        at android.app.Activity.onBackPressed(Activity.java:2480)

Is there any specific reason why I am getting this error? How should it be avoided?

like image 435
Alex Lockwood Avatar asked Nov 05 '14 03:11

Alex Lockwood


2 Answers

You will need to create a subclass of Animator that ignores calls to pause() and resume() in order to avoid this exception.

For more details, I just finished a post about this topic below:

  • Part 1: http://halfthought.wordpress.com/2014/11/07/reveal-transition/
  • Part 2: https://halfthought.wordpress.com/2014/12/02/reveal-activity-transitions/
like image 104
George Mount Avatar answered Nov 04 '22 17:11

George Mount


Is there any specific reason why I am getting this error?

ViewAnimationUtils.createCircularReveal is a shortcut for creating a new RevealAnimator, which is a subclass of RenderNodeAnimator. By default, RenderNodeAnimator.pause throws an UnsupportedOperationException. You see this occur here in your stack trace:

java.lang.UnsupportedOperationException
        at android.view.RenderNodeAnimator.pause(RenderNodeAnimator.java:251)

When Activity.onBackPressed is called in Lollipop, it makes a new call to Activity.finishAfterTransition, which eventually makes a call back to Animator.pause in Transition.pause(android.view.View), which is when your UnsupportedOperationException is finally thrown.

The reason it isn't thrown when using the "back" button after the transition is complete, is due to how the EnterTransitionCoordinator handles the entering Transition once it's completed.

How should it be avoided?

I suppose you have a couple of options, but neither are really ideal:

Option 1

Attach a TransitionListener when you call Window.setEnterTransition so you can monitor when to invoke the "back" button. So, something like:

public class YourActivity extends Activity {

    /** True if the current window transition is animating, false otherwise */
    private boolean mIsAnimating = true;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // Get the Window and enable Activity transitions
        final Window window = getWindow();
        window.requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);
        // Call through to super
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_child);

        // Set the window transition and attach our listener
        final Transition circularReveal = new CircularRevealTransition(yourView);
        window.setEnterTransition(circularReveal.addListener(new TransitionListenerAdapter() {

            @Override
            public void onTransitionEnd(Transition transition) {
                super.onTransitionEnd(transition);
                mIsAnimating = false;
            }

        }));

        // Restore the transition state if available
        if (savedInstanceState != null) {
            mIsAnimating = savedInstanceState.getBoolean("key");
        }
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        // Save the current transition state
        outState.putBoolean("key", mIsAnimating);
    }

    @Override
    public void onBackPressed() {
        if (!mIsAnimating) {
            super.onBackPressed();
        }
    }

}

Option 2

Use reflection to call ActivityTransitionState.clear, which will stop Transition.pause(android.view.View) from being called in ActivityTransitionState.startExitBackTransition.

@Override
public void onBackPressed() {
    if (!mIsAnimating) {
        super.onBackPressed();
    } else {
        clearTransitionState();
        super.onBackPressed();
    }
}

private void clearTransitionState() {
    try {
        // Get the ActivityTransitionState Field
        final Field atsf = Activity.class.getDeclaredField("mActivityTransitionState");
        atsf.setAccessible(true);
        // Get the ActivityTransitionState
        final Object ats = atsf.get(this);
        // Invoke the ActivityTransitionState.clear Method
        final Method clear = ats.getClass().getDeclaredMethod("clear", (Class[]) null);
        clear.invoke(ats);
    } catch (final Exception ignored) {
        // Nothing to do
    }
}

Obviously each has drawbacks. Option 1 basically disables the "back" button until the transition is complete. Option 2 allows you to interrupt using the "back" button, but clears the transition state and uses reflection.

Here's a gfy of the results. You can see how it completely transitions from "A" to "M" and back again, then the "back" button interrupts the transition and goes back to "A". That'll make more sense if you watch it.

At any rate, I hope that helps you out some.

like image 31
adneal Avatar answered Nov 04 '22 17:11

adneal