Implementing proper back navigation and home butto

2019-03-08 11:38发布

问题:

I am using a single activity and multiple fragments(screenshot attached) within the same activity to provide a seamless navigation. But after implementing the latest toolbar and navigation view, it seems hard to handle the navigation and home buttons. I am having trouble with the following things.

  • Managing the Hamburger/Back button at left top. Toggling the icon and functionality to Menu and Back nav.
    • Page title - Changing the page titles whenever a fragment in pushed and popped.

I have tried several things like overriding onBackPressed(), setHomeAsUpIndicator, popping fragments manually. Earlier i was using ActionBarDrawer toggle to handle this but it is failing somehow now. I checked the google samples they seem to use separate activities at most of the places.

Can anyone guide me how to implement a proper back navigation to handle the NavigationView, Back button in inner fragments and page titles? I am using AppCompatActivity, android.app.Fragment, NavigationView and Toolbar.

回答1:

It's much easier to illustrate with some sort of division of responsibility for your Activity and Fragment.

Problem 1: Managing the Hamburger/Back button at left top. Toggling the icon and functionality to Menu and Back nav.

From the illustration, the solution should be encapsulated by the Activity, which will look something like this:

public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener {

    private ActionBarDrawerToggle mDrawerToggle;
    private DrawerLayout mDrawer;
    private ActionBar mActionBar;

    private boolean mToolBarNavigationListenerIsRegistered = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        mActionBar = getSupportActionBar();

        mDrawer = (DrawerLayout) findViewById(R.id.drawer_layout);
        mDrawerToggle = new ActionBarDrawerToggle(this, mDrawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
        mDrawer.addDrawerListener(mDrawerToggle);
        mDrawerToggle.syncState();

        NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);
        navigationView.setNavigationItemSelectedListener(this);

        // On orientation change savedInstanceState will not be null.
        // Use this to show hamburger or up icon based on fragment back stack.
        if(savedInstanceState != null){
            resolveUpButtonWithFragmentStack();
        } else {
            // You probably want to add your ListFragment here.
        }
    }

    @Override
    public void onBackPressed() {

        if (mDrawer.isDrawerOpen(GravityCompat.START)) {
            mDrawer.closeDrawer(GravityCompat.START);

        } else {
            int backStackCount = getSupportFragmentManager().getBackStackEntryCount();

            if (backStackCount >= 1) {
                getSupportFragmentManager().popBackStack();
                // Change to hamburger icon if at bottom of stack
                if(backStackCount == 1){
                    showUpButton(false);
                }
            } else {
                super.onBackPressed();
            }
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;

        } else if (id == android.R.id.home) {
            // Home/Up logic handled by onBackPressed implementation
            onBackPressed();
        }

        return super.onOptionsItemSelected(item);
    }

    @SuppressWarnings("StatementWithEmptyBody")
    @Override
    public boolean onNavigationItemSelected(MenuItem item) {
        // Handle navigation view item clicks here.
        int id = item.getItemId();

        // Navigation drawer item selection logic goes here

        mDrawer.closeDrawer(GravityCompat.START);
        return true;
    }

    private void replaceFragment() {
        /**
        * Your fragment replacement logic goes here
        * e.g.
        * FragmentTransaction ft = getFragmentManager().beginTransaction();
        * String tag = "MyFragment";
        * ft.replace(R.id.content, MyFragment.newInstance(tag), tag).addToBackStack(null).commit();
        */

        // The part that changes the hamburger icon to the up icon
        showUpButton(true);
    }

    private void resolveUpButtonWithFragmentStack() {
        showUpButton(getSupportFragmentManager().getBackStackEntryCount() > 0);
    }

    private void showUpButton(boolean show) {
        // To keep states of ActionBar and ActionBarDrawerToggle synchronized,
        // when you enable on one, you disable on the other.
        // And as you may notice, the order for this operation is disable first, then enable - VERY VERY IMPORTANT.
        if(show) {
            // Remove hamburger
            mDrawerToggle.setDrawerIndicatorEnabled(false);
            // Show back button
            mActionBar.setDisplayHomeAsUpEnabled(true);
            // when DrawerToggle is disabled i.e. setDrawerIndicatorEnabled(false), navigation icon
            // clicks are disabled i.e. the UP button will not work.
            // We need to add a listener, as in below, so DrawerToggle will forward
            // click events to this listener.
            if(!mToolBarNavigationListenerIsRegistered) {
                mDrawerToggle.setToolbarNavigationClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        onBackPressed();
                    }
                });

                mToolBarNavigationListenerIsRegistered = true;
            }

        } else {
            // Remove back button
            mActionBar.setDisplayHomeAsUpEnabled(false);
            // Show hamburger
            mDrawerToggle.setDrawerIndicatorEnabled(true);
            // Remove the/any drawer toggle listener 
            mDrawerToggle.setToolbarNavigationClickListener(null);
            mToolBarNavigationListenerIsRegistered = false;
        }

        // So, one may think "Hmm why not simplify to:
        // .....
        // getSupportActionBar().setDisplayHomeAsUpEnabled(enable);
        // mDrawer.setDrawerIndicatorEnabled(!enable);
        // ......
        // To re-iterate, the order in which you enable and disable views IS important #dontSimplify.
    }
}

Problem 2: Page title - Changing the page titles whenever a fragment in pushed and popped.

Essentially, this can be handled in the onStart for each Fragment i.e. your ListFragment, DetailsFragment and CommentsFragment look something like this:

@Override
public void onStart() {
    super.onStart();
    // where mText is the title you want on your toolbar/actionBar
    getActivity().setTitle(mText);
}

Probably worth having setRetainInstance(true) in the onCreate of your fragments as well.



回答2:

tl;dr

Watch this: https://youtu.be/ANpBWIT3vlU

Clone this: https://github.com/shredderskelton/androidtemplate.

This is a really common problem and one that I've overcome by creating a kind of template project which I use whenever I start a new Android project. The idea is to abstract as much of the logic that handles the back button, the 'hamburger' indicator and fragment management into reusable classes:

Start by creating a BaseActivity and BaseFragment class. This is where you are going to as much of the reusable code as possible.

Lets start with your BaseActivity

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    fragmentManager = getSupportFragmentManager();
    fragmentHandler = new AddFragmentHandler(fragmentManager);
    fragmentManager.addOnBackStackChangedListener(backStackListener);
}

The FragmentManager is the key to owning the back stack, so you need to listen for changes to the back stack from here. The AddFramentHandler is a little class I cooked up to make it easier to add Fragments, from Fragments. More on that later.

@Override
public void onBackPressed() {
    if (sendBackPressToDrawer()) {
        //the drawer consumed the backpress
        return;
    }

    if (sendBackPressToFragmentOnTop()) {
        // fragment on top consumed the back press
        return;
    }

    //let the android system handle the back press, usually by popping the fragment
    super.onBackPressed();

    //close the activity if back is pressed on the root fragment
    if (fragmentManager.getBackStackEntryCount() == 0) {
        finish();
    }
}

onBackPressed is where most of the magic happens. You notice the plain text formatting of the methods.. I'm a huge Clean Code fan - if you need to write comments, your code isn't clean. Basically you need to really have a central place where you can run to when you're not sure why a back button press is not happening the way you expect. This method is that place.

private void syncDrawerToggleState() {
    ActionBarDrawerToggle drawerToggle = getDrawerToggle();
    if (getDrawerToggle() == null) {
        return;
    }
    if (fragmentManager.getBackStackEntryCount() > 1) {
        drawerToggle.setDrawerIndicatorEnabled(false);
        drawerToggle.setToolbarNavigationClickListener(navigationBackPressListener); //pop backstack
    } else {
        drawerToggle.setDrawerIndicatorEnabled(true);
        drawerToggle.setToolbarNavigationClickListener(drawerToggle.getToolbarNavigationClickListener()); //open nav menu drawer
    }
}

This is the other key part of the BaseActivity. Basically this method checks whether you are at the root fragment and sets up the indicator accordingly. Notice that it changes the listener depending on how many fragments are in the back stack.

Then there is the BaseFragment:

@Override
public void onResume() {
    super.onResume();
    getActivity().setTitle(getTitle());
}

protected abstract String getTitle();

The code above shows how the title is handled by the fragments.



回答3:

Try something like this:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    if (getSupportActionBar()!=null) {
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
    }

    drawer = (DrawerLayout) findViewById(R.id.drawer_layout);

    final ActionBarDrawerToggle drawerToggle = new ActionBarDrawerToggle(
            this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
    drawer.addDrawerListener(drawerToggle);
    drawerToggle.syncState();

    final View.OnClickListener originalToolbarListener = drawerToggle.getToolbarNavigationClickListener();

    final View.OnClickListener navigationBackPressListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            getFragmentManager().popBackStack();
        }
    };

    getFragmentManager().addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener() {
        @Override
        public void onBackStackChanged() {
            if (getFragmentManager().getBackStackEntryCount() > 0) {
                drawerToggle.setDrawerIndicatorEnabled(false);
                drawerToggle.setToolbarNavigationClickListener(navigationBackPressListener);
            } else {
                drawerToggle.setDrawerIndicatorEnabled(true);
                drawerToggle.setToolbarNavigationClickListener(originalToolbarListener);
            }
        }
    });

    // Though below steps are not related but I have included to show drawer close on Navigation Item click. 

    navigationView = (NavigationView) findViewById(R.id.nav_view);
    navigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
        @Override
        public boolean onNavigationItemSelected(MenuItem item) {
            int id = item.getItemId();
            /**
             * handle item clicks using id
             */
            drawer.closeDrawer(GravityCompat.START);
            return true;
        }
    });
}

Handle the drawer state onBackPressed:

@Override
public void onBackPressed() {
    if (drawer.isDrawerOpen(GravityCompat.START)) {
        drawer.closeDrawer(GravityCompat.START);
    } else {
        super.onBackPressed();
    }
}

To reload previous fragment on back press, always add the fragment transaction to back stack like this:

FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
SomeFragment fragmentToBeLoaded = new SomeFragment();
fragmentTransaction.replace(R.id.fragment_container, fragmentToBeLoaded,
                fragmentToBeLoaded.getName());
fragmentTransaction.addToBackStack(fragmentToBeLoaded.getName());
fragmentTransaction.commit();

To dynamically change the page title, you can call this from every Fragments onStart or onResume method:

@Override
public void onStart() {
   super.onStart();
   getActivity().setTitle("Title for fragment");
}

Note: I have considered standard layout declaration and thus I have not included any layouts.



回答4:

"Page title - Changing the page titles whenever a fragment in pushed and popped"

When you remove a fragment, there is the method isRemoving(). It helps to change title back.

@Override
public void onStop() {
    super.onStop();
    if (isRemoving()) {
        // Change your title here
    }
}

"functionality to Menu and Back nav"

Suggestion: we have to rely on the default android navigation system. If we use addToBackStack() for our fragments, in theory we don't have to override onBackPressed() at all.

  1. "App does not redefine the expected function of a system icon (such as the Back button)."
  2. "App supports standard system Back button navigation and does not make use of any custom, on-screen "Back button" prompts."

Core App Quality: https://developer.android.com/distribute/essentials/quality/core.html

"Managing the Hamburger/Back button at left top"

I suggest to use activity instead of 'MainActivityDetailFragment' to avoid complication.



回答5:

Add this in your MainActivity where you are calling Fragments. getBackStackEntryCount() Return number of fragments in the back stack. where the fragment on the bottom of the stack has index 0. popBackStack() Pop the top Fragment off the back stack

 @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();

        if (id == android.R.id.home) {
            if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
                getSupportFragmentManager().popBackStack();
            } else {
                super.onBackPressed();
            }
        }
        return true;
    }

And in your Fragment where you want to go back use this function

  @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();
        if (id == android.R.id.home) {
            getActivity().onBackPressed();
        }
        return true;
    }


回答6:

Ok, after a lot of tests I finally succeeded to setup a good navigation. I needed exactly the same as you, the only difference is that I am using v4 Fragments, but I don't think this will change anything here.

I am not using ActionBarDrawerToggle since the latest examples from Google do not use this component anymore.

The solution below also works for deep navigation: parent activity --> fragment --> fragment etc.

The only change needed in the Fragments is to change the title:

@Override
public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);

    getActivity().setTitle(R.string.targets);
}

In the parent Activity onCreate method, I initialize the following:

    mNavigationView = (NavigationView) findViewById(R.id.navigation_view);
    setupDrawerContent(mNavigationView);

    final Toolbar toolbar = (Toolbar) findViewById(R.id.drawer_toolbar);
    setSupportActionBar(toolbar);

    getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_menu_24);// Set the hamburger icon
    getSupportActionBar().setDisplayHomeAsUpEnabled(true);// Set home button pressable

    // Handle the changes on the actionbar
    getSupportFragmentManager().addOnBackStackChangedListener(
            new FragmentManager.OnBackStackChangedListener() {
                public void onBackStackChanged() {
                    // When no more fragments to remove, we display back the hamburger icon and the original activity title
                    if (getSupportFragmentManager().getBackStackEntryCount() <= 0) {
                        getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_menu_24);
                        setTitle(R.string.app_name);
                    }
                    // Else displays the back arrow
                    else {
                        getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_arrow_back_24);
                    }
                }
            });

Here is now the code to handle the action on the Home button:

@Override
public boolean onOptionsItemSelected(MenuItem item){
    // Close the soft keyboard right away
    Tools.setSoftKeyboardVisible(mViewPager, false);

    switch (item.getItemId()) {
        case android.R.id.home:

            // When no more fragments to remove, open the navigation drawer
            if (getSupportFragmentManager().getBackStackEntryCount() <= 0) {
                mDrawerLayout.openDrawer(GravityCompat.START);
            }
            // Removes the latest fragment
            else {
                getSupportFragmentManager().popBackStack();
            }

            return true;
    }
    return super.onOptionsItemSelected(item);
}

And finally the code to handle the back press action:

@Override
public void onBackPressed() {
    // When no more fragments to remove, closes the activity
    if (getSupportFragmentManager().getBackStackEntryCount() <= 0) {
        super.onBackPressed();
    }
    // Else removes the latest fragment
    else {
        getSupportFragmentManager().popBackStack();
    }
}

NOTE: I am using an AppCompatActivity, a NavigationView and the theme Theme.AppCompat.Light.NoActionBar.