42

I've been searching for a while on how to change between the drawer open/close icon (going from a hamburger to the arrow) to a simple back arrow. My application at the moment only has one Activity which switches between several fragments. At one point, I want to transition between one of the main fragments (ie, one of the fragments in the drawer) to a fragment that hierarchically is under the previous fragment (ie, an "Add New " fragment). In this new fragment, I want to have the Toolbar to show the back button instead of the drawer button.

I've been looking around and trying different solutions for quite a while. Here are the most notable:

At the moment, I'm thinking of a long, arduous method of creating a custom icon that I hide and show (and hide/show the native drawer icon). However, is there a better way to switch between the drawer and back buttons?

As a side yet related question, I've been looking at the Material Design docs, and a few examples have an X in the top left corner. How different is that to implement than implementing the drawer vs back/up buttons?

Thanks~

Edit:

I can figure out how to replace the icon, but how would I get the click event?

So far, this was my best lead:

What I've tried now:

  • Disabled the DrawerToggle when necessary (ie, mDrawerToggle.setDrawerIndicatorEnabled(useDrawer);)
  • Added logs in onOptionsItemSelected in my NavigationDrawerFragment, my Activity, as well as the DialogFragment I'm currently testing which run if item.getItemId() == android.R.id.home is true. None of these log statements go off

For better context, I now have a full screen fragment which adds a "Save" button to the menu and changes the drawer icon to an "X". The fragment can get the save menu event, yet not even the Activity and Drawer can get when the X is tapped.

Edit2:

As requested, here is some code. Note that this is all from this Github repo, which I'm actively working on (note that I have a few useless functions here or there from rapid testing).

ActivityMain:

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

    // Add the toolbar
    mToolbar = (Toolbar) findViewById(R.id.toolbar);
    if (mToolbar != null) {
        setSupportActionBar(mToolbar);
    }

    // Initialize the drawer
    mNavigationDrawerFragment = (NavigationDrawerFragment)
            getSupportFragmentManager().findFragmentById(R.id.navigation_drawer);

    // Set up the drawer
    mNavigationDrawerFragment.setUp(
            R.id.navigation_drawer,
            (DrawerLayout) findViewById(R.id.drawer_layout),
            mToolbar);

    // TODO: Check if this helps to catch the main toolbar button click
    getSupportActionBar().setDisplayShowHomeEnabled(true);

    // Get the titles for the Toolbar
    mTitles = getResources().getStringArray(R.array.drawer_items);

    mDrawerPosition = -1;
    if (savedInstanceState == null) {
        // If there was no saved position, then the default, starting position should be used
        forceChangeItemSelected(0);
    }
    else {
        // Otherwise, get the saved position from the bundle
        int position = savedInstanceState.getInt(KEY_DRAWERPOS);
        mNavigationDrawerFragment.setSelectedItem(position);
        // Title needs to be re-set
        getSupportActionBar().setTitle(mTitles[position]);
    }

    // If I include the below bit, then the DrawerToggle doesn't function
        // I don't know how to switch it back and forth
    mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Log.d(LOG_TAG, "Navigation was clicked");

        }
    });
}

@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.
    Log.d(LOG_TAG, "Activity responding to menu click...");
    if(item.getItemId() == android.R.id.home) Log.d(LOG_TAG, "Activity got it....");

    // If the fragment is supposed to handle things, then let it
    if(mIsFragmentHandlingMenus) return false;

    int id = item.getItemId();
    if(id == R.id.save) {
        // This isn't implemented! If chosen, then there's a bug!
        Log.e(LOG_TAG, "onOptionsItemSelected: Save was selected!");
    }

    return super.onOptionsItemSelected(item);
}

@Override
public void fragmentHandlingMenus(boolean isFragmentHandlingMenus) {
    // Simply store the setting
    mIsFragmentHandlingMenus = isFragmentHandlingMenus;

    // Toggle the drawer as necessary
    mNavigationDrawerFragment.toggleDrawerUse(!isFragmentHandlingMenus);
}

NavigationDrawerFragment:

public void toggleDrawerUse(boolean useDrawer) {
    // Enable/Disable the icon being used by the drawer
    mDrawerToggle.setDrawerIndicatorEnabled(useDrawer);

    // TODO: Enable/Disable the drawer even being able to open/close
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    Log.d(LOGTAG, "Drawer responding to menu click...");
    if(item.getItemId() == android.R.id.home) Log.d(LOGTAG, "Drawer got it....");
    if (mDrawerToggle.onOptionsItemSelected(item)) {
        return true;
    }

    return super.onOptionsItemSelected(item);
}

GoalAdderFragment:

@Override
public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    // Allow this fragment to handle toolbar menu items
    setHasOptionsMenu(true);

    // Set up the toolbar
    ((ActionBarActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(true);
    ((ActionBarActivity) getActivity()).getSupportActionBar().setHomeAsUpIndicator(android.R.drawable.ic_menu_close_clear_cancel);
    ((ActionBarActivity) getActivity()).getSupportActionBar().setTitle(getResources().getString(R.string.title_addgoal));
}

@Override
public void onAttach(Activity activity) {
    super.onAttach(activity);

    // Cache the Activity as the frag handler if necessary
    if(mFragHandler == null)
        mFragHandler = (TransactionHandler.FragmentTransactionHandler) getActivity();
    // Tell the Activity to let fragments handle the menu events
    mFragHandler.fragmentHandlingMenus(true);
}

@Override
public void onDetach() {
    super.onDetach();

    // Tell the Activity that it can now handle menu events once again
    mFragHandler.fragmentHandlingMenus(false);
}

@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    inflater.inflate(R.menu.save_menu, menu);
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    Log.d(LOGTAG, "Item id: " + item.getItemId() + " | Save id: " + R.id.save);
    Toast.makeText(getActivity(), "Fragment activated!", Toast.LENGTH_SHORT).show();

    switch (item.getItemId()) {
        case R.id.save:
            return true;
        case android.R.id.home:
            return true;
        default:
            break;
    }

    return false;
}

Solution:

This is the ultimate solution I ended up on, with the help of natario's answer below:

NavigationDrawerFragment:

private View.OnClickListener mOriginalListener;

public void setUp(int fragmentId, DrawerLayout drawerLayout, Toolbar toolbar) {
     /* Rest of setting up code */

     // Save the default listener after setting everything else up
     mOriginalListener = mDrawerToggle.getToolbarNavigationClickListener();
}

// Tells the toolbar+drawer to switch to the up button or switch back to the normal drawer
public void toggleDrawerUse(boolean useDrawer) {
    // Enable/Disable the icon being used by the drawer
    mDrawerToggle.setDrawerIndicatorEnabled(useDrawer);

    // Switch between the listeners as necessary
    if(useDrawer)
        mDrawerToggle.setToolbarNavigationClickListener(mOriginalListener);
    else
        mDrawerToggle.setToolbarNavigationClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(getActivity(), "Custom listener", Toast.LENGTH_SHORT).show();
            }
        });
}
DragonJawad
  • 1,846
  • 3
  • 20
  • 28
  • 1
    Use the v7 support library of `ActionBarDrawerListener`. If you call the constructor and implement the methods, it will take care of everything for you. Make sure you use the v7 version, not the v4 version as it is deprecated and will not work. I know this from hours of frustration. – Dylan Vander Berg Aug 10 '15 at 15:01

6 Answers6

48

Put this code into onCreate() of your Activity. Works well for me. Even using compileSdk 23 and higher.

    drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
    final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    if(toolbar != null) {
        toggle = new ActionBarDrawerToggle(
                this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
        toggle.syncState();
        drawer.setDrawerListener(toggle);
        getSupportFragmentManager().addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener() {
            @Override
            public void onBackStackChanged() {
                if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
                    getSupportActionBar().setDisplayHomeAsUpEnabled(true); // show back button
                    toolbar.setNavigationOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            onBackPressed();
                        }
                    });
                } else {
                    //show hamburger
                    getSupportActionBar().setDisplayHomeAsUpEnabled(false);
                    toggle.syncState();
                    toolbar.setNavigationOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            drawer.openDrawer(GravityCompat.START);
                        }
                    });
                }
            }
        });
matusalem
  • 2,461
  • 2
  • 26
  • 35
  • 2
    This is the best solution I found so far, you should get the accepted answer and at least the highest value. With this solution I don't have to worry about coordinating arrow button and loading previous fragment. In my opinion, this should be built in the toolbar and if someone wants to modify its behaviour, than override a method. Thank you @matusalem – AndaluZ Feb 15 '16 at 14:55
  • Yes, this solution automatically works with fragment replacing approach without calling any methods. Glad to see I have helped. You are welcomed. Happy coding :) – matusalem Feb 15 '16 at 16:39
  • Dont know what wrong I am doing.But it is not working for me: – kgandroid Jul 08 '16 at 13:15
  • if(id==R.id.ranking_item) { fragment = new KPIDetailFragment(); } if (fragment != null) { FragmentManager fragmentManager = getSupportFragmentManager(); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); fragmentTransaction.replace(R.id.main_layout, fragment); fragmentTransaction.commit(); // set the toolbar title getSupportActionBar().setTitle("KPI DETAIL"); } – kgandroid Jul 08 '16 at 13:16
  • It helped me unlike many other tips. And it's so easy and natural (toolbar.setNavigationOnClickListener()) !! Thank you very much. – iramm Jul 28 '16 at 15:37
28

It should work even for latest API 24.

In your activity onCreate() do this:

final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
final DrawerLayout drawer = (DrawerLayout) view.findViewById(R.id.drawer_layout);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);

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

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

getSupportFragmentManager().addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener() {
    @Override
    public void onBackStackChanged() {
        if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
            toggle.setDrawerIndicatorEnabled(false);
            toggle.setToolbarNavigationClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    getSupportFragmentManager().popBackStack();
                }
            });
        } else {
            toggle.setDrawerIndicatorEnabled(true);
            toggle.setToolbarNavigationClickListener(originalToolbarListener);
        }
    }
});
mixel
  • 25,177
  • 13
  • 126
  • 165
  • 3
    Your `getToolbarNavigationClickListener()` inspired me to use `setToolbarNavigationClickListener()` and solved my issue!! Thanks a lot!!! – Lancelot Oct 26 '15 at 00:04
12

That's probably not what you would like to hear, but even from a conceptual point of view I would go for a new activity rather than a fragment.

Your main activity is strictly linked to the drawer, so loading a new fragment without any access to the drawer makes no sense to me (but feel free wait for other answers if you think so). A new activity would solve both problems, since it would have no drawer and could be a child of the main one.

Your side question looks spot on also. A "Add New" activity could nicely fit into the "full-screen dialog" visual pattern from the guidelines. See:

http://www.google.com/design/spec/components/dialogs.html#dialogs-full-screen-dialogs

This pattern has a "save", positive button on top-right, and a X. Conceptually, the X button is to cancel/abort a process, rather than navigating up some backstack. It means you are dismissing something without letting any action happen. This fits well for what you want to do.

From a design point of view, it's easily made by a new Activity, that can stay on top of others. Also, if the point of fragments is basically being able to represent two or more at once in tablets and bigger screen - again - I wouldn't be so happy with an old fragment on my left and an "Add New" fragment on the right.

Rather - on tablets - I would go for a floating dialog, as suggested by the guidelines.

http://www.google.com/design/spec/components/dialogs.html#dialogs-confirmation-dialogs

So full-screen activity with a X button for phones, and floating dialog (with buttons at the bottom) for tablets. This, to me, is the most guidelines-coherent approach.


I recommend reading the whole link. On the difference between <- and X,

The X differs from an Up arrow, which is used when the view’s state is constantly being saved or when apps have draft or autosave capabilities. For example, an Up arrow is used in Settings because all changes are committed immediately.

And also

Touching the X in this Settings example will discard all changes. Changes will be saved only upon touching Save.

natario
  • 24,954
  • 17
  • 88
  • 158
  • Thanks for the answer! The dialog patterns (particularly using a full screen dialog on a smaller device) seems like a great idea for the "Add New" functionality. As for using multiple Activities... That was my original intention. However, I've been focusing on transitions lately and it seems far too arduous for far too little to try to create Activity transitions for pre-Lollipop devices (as it's not natively supported at all). The best method I could think of to use transition animations was instead to use fragments. – DragonJawad Feb 15 '15 at 23:08
  • This is a great answer, however I most likely will need to switch the Toolbar button sooner than later so I'll keep the question open still. Once again, thanks~ – DragonJawad Feb 15 '15 at 23:10
  • You're welcome. As for activity transition, you can use the `android.view.Animation` framework along with `overridePendingTransitions()`. Not smooth as a fragment transition, but can be quite good. – natario Feb 16 '15 at 12:14
  • Actually, I went down the full screen dialog (fragment) route and I somehow ended up at a similar obstacle: I can't get the drawer/home button click event. I added a more in depth description to my question. Would you mind helping me out once again? Thanks! – DragonJawad Feb 16 '15 at 18:20
  • I can't add anything to the [answer you linked](http://stackoverflow.com/a/26636045/4288782). If you post some code we can take a look. It depends on how/when you are replacing the drawer icon with the X. – natario Feb 16 '15 at 21:27
  • Thank you for the additional support. I added in the relevant snippets of code (as well as the Github links), and I forgot to mention that I did try setting a NavigationOnClickListener onto the Toolbar. However, the DrawerToggle would no longer function afterwards, and I have no idea how to switch between the two "listeners" – DragonJawad Feb 17 '15 at 05:27
  • Looks OK to me - without any NavigationListener you should be able to catch the `android.R.id.home` (remember that the Activity catches it first, so try managing it there). As a second option, you could try using the NavigationListener and - when needed - call `setNavigationOnClickListener(null)` to unregister. – natario Feb 17 '15 at 08:40
  • Just tested it out- setting it to null doesn't reinstate the drawer toggle functionality, and no matter what I do (maybe except never setting up the NavigationDrawerFragment) the Activity never receives the android.R.id.home event – DragonJawad Feb 17 '15 at 15:30
  • Nevermind, I got it. I'll mark your answer as the accepted solution as it's what led me to the answer as well as edit my question with my final solution, but essentially I cached the DrawerToggle's listener and switched it between a custom listener and the cached listener as necessary. Thanks for all the help! – DragonJawad Feb 17 '15 at 15:40
4

The answer from @matusalem works great. I just had one bit to add to it - be careful because the drawer can also be opened by swiping in from the left side of the screen. For some, this may be desired, but for me I was disabling the drawer because it didn't make sense in any fragment but my main fragment. The swipe is easily disabled here - Navigation drawer - disable swipe

This probably belongs in a comment to the answer, but I don't have enough reputation. My apologies.

Community
  • 1
  • 1
tpankake
  • 366
  • 2
  • 3
  • You're right, but only in your special case. I think there is no reason why drawer could not be able to swiped in anytime. Drawer has been invented for this purpose. See Google apps design - Google Play, Gmail etc. – matusalem Apr 12 '16 at 21:13
0

I had same problem with switching between hamburger menu and back arrow inside same activity when changing fragments. Here is my working solution, hope it helps to someone.

Listener inside your activity:

private View.OnClickListener toolbarMenuListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            //will be called only if toggle.setDrawerIndicatorEnabled(false); !
            Log.v(tag,"toggle onClick:"+v.getId()+" android.R.id.home:"+android.R.id.home);
            onBackPressed();
        }
    };

Code onCreate() something like:

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

//set listener so you know when back on arrow is pressed
toggle.setToolbarNavigationClickListener(toolbarMenuListener);
...
...

Part you are interested in with comments (Class returned is some of mine class, can set to be void):

/**
     * Method to set up action bar drawer.
     * @param enableBackDrawerIcon set true if want to show drawer back arrow,
     *                             false to show hamburger menu.
     * @param title shown next to drawer icon
     */
    public BaseMenusActivity drawerSetupToggle(boolean enableBackDrawerIcon, String title) {
        //NOTE: order of methods call is important!
        // If you change order order of setDrawerIndicatorEnabled and setDisplayHomeAsUpEnabled
        // method calls it won't work, weird bugs will happen (like no icon at all)
        if(enableBackDrawerIcon){
            Log.v(tag,"show drawer back icon");
            //hides hamburger menu and enables View.OnClickListener to be called
            toggle.setDrawerIndicatorEnabled(false);
            //show back arrow
            if(getSupportActionBar()!=null)
                getSupportActionBar().setDisplayHomeAsUpEnabled(true);

        } else {
            Log.v(tag,"show hamburger menu");
            //hide back arrow
            if(getSupportActionBar()!=null)
                getSupportActionBar().setDisplayHomeAsUpEnabled(false);
            //shows hamburger menu and prevents View.OnClickListener to be called
            toggle.setDrawerIndicatorEnabled(true);
        }

        setTitle(title);
        return this;
    }

NOTE: order of called methods is important! Would be better if could just write it in 2 lines like this but WON'T WORK (at least for me):

toggle.setDrawerIndicatorEnabled(!enableBackDrawerIcon);
     getSupportActionBar().setDisplayHomeAsUpEnabled(enableBackDrawerIcon);

If you are interested why order of method calls mess things up, look into implementation of those methods.

user1540907
  • 191
  • 2
  • 5
0
//This if block makes the menu back button to respond to clicks
    //The onOptionsItemSelected fun for whatever reason was not capturing back menu clicks
    if (toolbar != null) {
       /* toggle = ActionBarDrawerToggle(
                this, drawer_layout, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close)
        toggle.syncState()
        drawer_layout.setDrawerListener(toggle)*/
        supportFragmentManager.addOnBackStackChangedListener(object : FragmentManager.OnBackStackChangedListener {
            override fun onBackStackChanged() {
                if (supportFragmentManager.backStackEntryCount > 0) {
                    supportActionBar?.setDisplayHomeAsUpEnabled(true) // show back button
                    toolbar.setNavigationOnClickListener(object : View.OnClickListener {
                        override fun onClick(v: View) {
                            onBackPressed()
                        }
                    })
                } else {
                    //show hamburger
                    supportActionBar?.setDisplayHomeAsUpEnabled(false)
                    toggle.syncState()
                    toolbar.setNavigationOnClickListener(object : View.OnClickListener {
                        override fun onClick(v: View) {
                            drawer_layout.openDrawer(GravityCompat.START)
                        }
                    })
                }
            }
        })

    }

You need to comment out "toggle = ActionBarDrawerToggle( this, drawer_layout, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close) toggle.syncState() drawer_layout.setDrawerListener(toggle)" (4-7 lines) if you are using the auto generated Navigation layout in Android Studio, else the behavior of the back menu button will be erratic. That is what i did and it worked perfectly for me. Hope this helps someone

defemz
  • 519
  • 2
  • 7
  • 20