Intercepting ActionBar Home button in Fragment

2020-07-16 08:52发布

问题:

I can successfully intercept the ActionBar home button from my NavigationDrawerFragment, which is added to my MainActivity, like so:

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    if (!loggedIn() && item.getItemId() == android.R.id.home) {
        login();
        return true;
    }
    return super.onOptionsItemSelected(item);
}

However, in my ComposeActivity with ComposeFragment this does not work. onOptionsItemSelected is not called on the fragment.

I have debugged the code and the issue seems to come down to the design of the Android support library. It appears that both FragmentActivity and Activity have their own references to a FragmentManager.

FragmentActivity first checks if Activity can handle the event before checking any of its fragments, which is consistent with the docs:

/**
 * Dispatch context and options menu to fragments.
 */
@Override
public boolean onMenuItemSelected(int featureId, MenuItem item) {
    if (super.onMenuItemSelected(featureId, item)) {
        return true;
    }

    switch (featureId) {
        case Window.FEATURE_OPTIONS_PANEL:
            return mFragments.dispatchOptionsItemSelected(item);

        case Window.FEATURE_CONTEXT_MENU:
            return mFragments.dispatchContextItemSelected(item);

        default:
            return false;
    }
}

As seen in the snippet below, Activity handles the home button as a last resort, after checking if either it or any of its fragments can handle the event. But this reference to FragmentManager does not contain any fragments, the fragments are in the FragmentActivity's manager. Therefore the event will get swallowed by the Activity class if ActionBar.DISPLAY_HOME_AS_UP is set.

public boolean onMenuItemSelected(int featureId, MenuItem item) {
    /* ... */
    if (onOptionsItemSelected(item)) {
        return true;
    }
    if (mFragments.dispatchOptionsItemSelected(item)) {
        return true;
    }
    if (item.getItemId() == android.R.id.home && mActionBar != null &&
            (mActionBar.getDisplayOptions() & ActionBar.DISPLAY_HOME_AS_UP) != 0) {
        if (mParent == null) {
            return onNavigateUp();
        } else {
            return mParent.onNavigateUpFromChild(this);
        }
    }
    return false;
    /* ... */
}

It turns out that my MainActivity, being the root of my application and using a navigation drawer, did not have this flag set and so did not swallow the event. But my ComposeActivity has a parent activity and I need this flag to be set so it is not possible to intercept the action bar home button.

To sum up the issue: It is not possible to intercept a click on the Action Bar home button from a fragment in an activity with with DISPLAY_HOME_AS_UP set.

So is this a bug in the support library? It doesn't look like it would occur if I targeted a later Android version and dropped the support library.

Regarding workarounds for this, I guess I could:

  • In my ComposeActivity's onOptionsItemSelected I can manually pass the event to each of my fragments, seeing if they can handle it before calling super.
  • Override onMenuItemSelected in ComposeActivity and do the same thing.

Anybody encountered this before? Should I log a bug somewhere? Any other ideas of ways around this issue?

回答1:

As you have explained, due to the flow of how the event is dispatched in Android, it seems the child fragment will never intercept the event because it's consumed by the Activity first.

It's a workaround, but what I'm doing is passing the event to child fragments before is handled in the Activity.

In the Activity:

    @Override
public boolean onOptionsItemSelected(MenuItem item) {
    // Due to a problem of not being able to intercept android.R.id.home in fragments,
    // we start passing the event to the currently displayed fragment.
    // REF: http://stackoverflow.com/questions/21938419/intercepting-actionbar-home-button-in-fragment
    final Fragment currentFragment = getSupportFragmentManager().findFragmentById(R.id.XXXXXXX);
    if (currentFragment != null && currentFragment.onOptionsItemSelected(item)) {
        return true;
    }

    switch (item.getItemId()) {
        case XXX:
            ...
            return true;
        case YYY:
            ...
            return true;
        default:
            break;
    }
    return super.onOptionsItemSelected(item);
}