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

3

12

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?

Horwitz answered 5/11, 2014 at 3:36 Comment(1)
I gave you my point of view explanation on this kind of buggy behaviour on G+!Crucible
C
10

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:

Charlottcharlotta answered 7/11, 2014 at 1:35 Comment(1)
Thanks a lot for the help! This seems to fix the problem for me. I edited your answer to provide a bit more context for the link.Horwitz
B
3

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.

Bankroll answered 5/11, 2014 at 12:37 Comment(3)
Both approaches definitely feel hacky. If I had to choose between the two I would probably prefer the first over the second since the reflection seems kind of scary. Who knows what the side effects of clearing the transition's state might be...Horwitz
Perhaps there is another way... if all you want to do is prevent Animator#pause() from being called, maybe you could just wrap the circular reveal animator in a simple Animator wrapper subclass that forwards all of its method calls except pause() to the circular reveal animator. Seems a little safer than using reflection, at least... but still doesn't seem like the ideal solution.Horwitz
@AlexLockwood Yeah, that seems much nicer, but still not ideal. Either way, pretty sure you're throwing the Exception because the Animator eventually makes its way back to RenderNodeAnimator.pause.Bankroll
W
0

You can add listener to enter transition that sets flag transitionInProgress in methods onTransitionStart() / onTransitionEnd(). Then, you can override method finishAfterTransition() and then check transitionInProgress flag, and call super only if transition finished. Otherwise you can just finish() your Activity or do nothing.

override fun finishAfterTransition() {
    if (!transitionInProgress){
        super.finishAfterTransition()
    } else {
        finish()
    }
}
Wow answered 10/10, 2017 at 9:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.