Start Activity from Fragment using Transition (API 21 support)

Damian Wieczorek picture Damian Wieczorek · Nov 18, 2014 · Viewed 7k times · Source

I'm trying to port an Android app to the new support library (support-v4:21.0.0) and I'm having trouble starting Activities from Fragments with a transition.

In my Activities, I've been doing something like:

Bundle options = ActivityOptionsCompat.makeSceneTransitionAnimation(this).toBundle();
ActivityCompat.startActivityForResult(this, intent, REQUEST_SOMETHING, options);

which works fine for Activities. However, if I try to do something similar with Fragments, like:

Activity activity = getActivity();
Bundle options = ActivityOptionsCompat.makeSceneTransitionAnimation(activity).toBundle();
ActivityCompat.startActivityForResult(activity, intent, REQUEST_SOMETHING, options);

it turns out that onActivityResult() is not called for the Fragment, but only the enclosing Activity. I haven't found anything in the support library to pass the options Bundle as a parameter to startActivityForResult() on an actual Fragment and have it call back to onActivityResult() in that Fragment. Is this possible?

The simplest solution would be to handle all onActivityResult() calls in the Activity itself, but I'd rather not do that because I have a ton of possible Fragments that may be receiving that callback.

Help is appreciated. Thanks!

Answer

0101100101 picture 0101100101 · Feb 26, 2015

Sadly, ActivityCompat.startActivityForResult() doesn't work quite right in Fragments (see Alex Lockwood's answer). For several weeks I marvelled at how Google never gave us an ActivityCompat method equivalent to Fragment's implementation of startActivityForResult(). What were they thinking?! But then I had an idea: Let's take a look at how the method is actually implemented.

As a matter of fact, startActivityForResult() in Fragment is different from the one in Activity (see here):

public void startActivityForResult(Intent intent, int requestCode) {
    if (mActivity == null) {
        throw new IllegalStateException("Fragment " + this + " not attached to Activity");
    }
    mActivity.startActivityFromFragment(this, intent, requestCode);
}

Now startActivityFromFragment() looks like this (see here):

public void startActivityFromFragment(Fragment fragment, Intent intent, 
        int requestCode) {
    if (requestCode == -1) {
        super.startActivityForResult(intent, -1);
        return;
    }
    if ((requestCode&0xffff0000) != 0) {
        throw new IllegalArgumentException("Can only use lower 16 bits for requestCode");
    }
    super.startActivityForResult(intent,
                                 ((fragment.mIndex + 1) << 16) + (requestCode & 0xffff));
}

Google uses some odd byte shifting on the request code to make sure only the calling Fragment's onActivityResult() is called afterwards. Now since ActivityCompat doesn't provide any startActivityFromFragment(), the only option left is to implement it yourself. Reflection is required to access the package private field mIndex.

public static void startActivityForResult(Fragment fragment, Intent intent,
                                          int requestCode, Bundle options) {
    if (Build.VERSION.SDK_INT >= 16) {
        if ((requestCode & 0xffff0000) != 0) {
            throw new IllegalArgumentException("Can only use lower 16 bits" +
                                               " for requestCode");
        }
        if (requestCode != -1) {
            try {
                Field mIndex = Fragment.class.getDeclaredField("mIndex");
                mIndex.setAccessible(true);
                requestCode = ((mIndex.getInt(this) + 1) << 16) + (requestCode & 0xffff);
            } catch (NoSuchFieldException | IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        }
        ActivityCompat.startActivityForResult(fragment.getActivity(), intent,
                                              requestCode, options);
    } else {
        fragment.getActivity().startActivityFromFragment(this, intent, requestCode);
    }
}

Copy that method anywhere you like and use it from your Fragment. Its onActivityResult() will be called as it should.

UPDATE: Support library v23.2 was released and it seems startActivityFromFragment(Fragment fragment, Intent intent, int requestCode, Bundle options) does the job now :)