Implementing proper back navigation and home button handling using Toolbar in Android

前端 未结 6 1822
粉色の甜心
粉色の甜心 2021-01-30 23:31

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

相关标签:
6条回答
  • 2021-01-31 00:13

    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;
        }
    
    0 讨论(0)
  • 2021-01-31 00:17

    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.

    0 讨论(0)
  • 2021-01-31 00:21

    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.

    0 讨论(0)
  • 2021-01-31 00:24

    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.

    0 讨论(0)
  • 2021-01-31 00:26

    "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.

    0 讨论(0)
  • 2021-01-31 00:32

    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.

    0 讨论(0)
提交回复
热议问题