Related
I am using MaterialDrawer from:
https://github.com/mikepenz/MaterialDrawer
and when the user presses the back button, the app goes one activity backwards, instead of just closing the drawer.
is it a bug or there is a way to fix it?
This is expected behavior. If you want to close Drawer on Back press, you have to override the onBackPressed() function.
First get a reference to your DrawerLayout :
DrawerLayout myDrawer = (DrawerLayout) findViewById(R.id.my_drawer);
Then override the onBackPressed() function to close drawer when it is open instead of closing activity:
#Override
public void onBackPressed() {
if (myDrawer.isDrawerOpen(GravityCompat.START)) {
myDrawer.closeDrawer(GravityCompat.START);
}
else {
super.onBackPressed();
}
}
Hope it helps.
just close your drawer if it is open on pressing back button
#override
public void onBackPressed(){
//DrawerLayout drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
DrawerLayout drawerLayout = result.getDrawerLayout();
if(drawerLayout.isDrawerOpen(GravityCompat.START)) {
//drawer is open
drawerLayout.closeDrawer(GravityCompat.START);
//result.closeDrawer();
}
}
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.
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.
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.
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.
"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.
"App does not redefine the expected function of a system icon (such as the Back button)."
"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.
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;
}
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.
I am trying to set up proper navigation in my application, which replaces Fragments in a main content area, so I have only one single Activity. I have one main Fragment and several subFragments, for example a Fragment for preferences. Everything works fine when using the back button, but I want to implement the up navigation including icon in addition to this. I am using the ActionBar fetched with Activity.getSupportActionBar() together with a Toolbar from appcompat and an ActionBarDrawerToggle.
I followed this tutorial when setting up my Drawer in the first place.
Current behavior:
When I start the app, the list/drawer icon is shown in the left part of the ActionBar. When I click this, the Drawer opens and I can select items. Sub Fragments are replaced into my content and the back button pops the stack, taking me back to the previous Fragment.
Missing behavior:
The list/drawer icon in the top left is never replaced by the back arrow icon and I cannot figure out how to implement this properly. The Drawer is always pulled out when clicking the list/drawer icon, no matter which Fragment I am in.
What have I tried:
I tried following this answer. It kinda works, meaning that the back arrow icon is set in the sub Fragments, but clicking the back arrow still opens the Drawer instead of providing up navigation. Also, when using the back button to go "up", the list/drawer icon is replaced by nothing.
I also tried following this answer. Here, the desired ActionBar behavior/look is implemented in the onCreate() method of the various Fragments. Using this I could get the back arrow up, but still the Drawer is pulled when clicking the arrow.
Various other minor things and hacks.
My questions:
What is wrong in my code below?
Is it correct/normal to use the combination ActionBar, Toolbar and ActionBarDrawerToggle to implement the Drawer navigation together with up navigation?
MyActivity.onCreate():
#Override
protected void onCreate(Bundle savedInstanceState)
{
// Other stuff
// Setup drawer.
mDrawerFragment = (DrawerFragment)
getSupportFragmentManager().findFragmentById(R.id.mm_navigation_drawer);
mDrawerFragment.initialize(this, (DrawerLayout)findViewById(R.id.mm_drawer_layout), toolbar);
}
DrawerFragment class
public class DrawerFragment extends Fragment
{
private MyActivity mMyActivity;
private MyActionBarDrawerToggle mMyBarDrawerToggle;
private DrawerLayout mDrawerLayout;
private FragmentDrawerListener mFragmentDrawerListener;
private View mContainerView;
public void initialize(MyActivity myActivity, final DrawerLayout drawerLayout, final Toolbar toolbar)
{
mMyActivity = myActivity;
mFragmentDrawerListener = mMyActivity;
mContainerView = myActivity.findViewById(R.id.mm_navigation_drawer);
mMyActionBarDrawerToggle = new MyActionBarDrawerToggle(myActivity, drawerLayout, toolbar, R.string.mm_drawer_open, R.string.mm_drawer_close);
mDrawerLayout = drawerLayout;
mDrawerLayout.setDrawerListener(mMyActionBarDrawerToggle);
mDrawerLayout.post(new Runnable()
{
#Override
public void run()
{
mMyActionBarDrawerToggle.syncState();
}
});
}
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, final ViewGroup container, Bundle savedInstanceState)
{
// Not relevant, just create and return the View.
}
}
MyActivity.onDrawerItemSelected()
The implementation of the interface FragmentDrawerListener is done in the MyActivity class. It simply replaces the content area with other Fragments, using FragmentTransactions.
#Override
public void onDrawerItemSelected(View view, int postion)
{
switch (postion)
{
case DrawerAdapter.ITEM_FILTERED_RECIPES:
showFilteredRecipesFragment();
break;
case DrawerAdapter.ITEM_SELECTED_RECIPES:
showSelectedRecipesFragment();
break;
case DrawerAdapter.ITEM_SHOPPING_LIST:
showShoppingListFragment();
break;
case DrawerAdapter.ITEM_SETTINGS:
showSettingsFragment();
break;
case DrawerAdapter.ITEM_ABOUT:
showAboutFragment();
break;
}
}
MyActionBarDrawerToggle class
public class MyActionBarDrawerToggle extends ActionBarDrawerToggle
{
private MyActivity mMyActivity;
private Toolbar mToolbar;
public MyActionBarDrawerToggle(Activity activity, DrawerLayout drawerLayout, Toolbar toolbar, int openDrawerContentDescRes, int closeDrawerContentDescRes)
{
super(activity, drawerLayout, toolbar, openDrawerContentDescRes, closeDrawerContentDescRes);
mMyActivity = (MyActivity) activity;
mToolbar = toolbar;
}
#Override
public void onDrawerOpened(View drawerView)
{
super.onDrawerOpened(drawerView);
mMyActivity.invalidateOptionsMenu();
}
#Override
public void onDrawerClosed(View drawerView)
{
super.onDrawerClosed(drawerView);
mMyActivity.invalidateOptionsMenu();
}
#Override
public void onDrawerSlide(View drawerView, float slideOffset)
{
super.onDrawerSlide(drawerView, slideOffset);
mToolbar.setAlpha(1 - slideOffset / 2);
}
}
The DrawerFragment is inflated in the main layout using a simple, static Fragment instance like this:
<fragment
android:id="#+id/my_navigation_drawer"
android:name="com.my.company.gui.drawer.DrawerFragment"
android:layout_width="#dimen/my_nav_drawer_width"
android:layout_height="match_parent"
android:layout_gravity="start"
app:layout="#layout/my_drawer_navigation_fragment"
tools:layout="#layout/my_drawer_navigation_fragment">
</fragment>
If you're using material design for you app, then it is expected that you use the Toolbar to replace the Actionbar in your activity. You will still maintain your ActionBarDrawerToggle and functionality will remain the same.
As for your fragments, as long as they are 'housed' by the same activtiy (i.e the activity with the drawer), changing fragments will not cause the drawer toggle to change the back arrow. It will only change if you navigate to a new activity. Only then will the main activity be treated as home and with the other activities having a back arrow to navigate back
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:
Change drawer icon back to back arrow - I successfully removed the drawer icon, but in place there's.... nothing. No up caret, no back button, no icon. I suspect this is because my Activity has no parent, but other than a cheap work around (create another Activity that acts as a parent which launches the main Activity), I'm at a lost of what to do.
Switching between Android Navigation Drawer image and Up caret when using fragments - Similar to the above, yet has far more detail. Ultimately, the icon still doesn't turn into a back button.
Android lollipop toolbar switch between open/close drawer and back button - I find this hard to follow, but ultimately the drawer icon can be tapped and does nothing (although I believe I know how to make it act as a back press). However, the icon doesn't change.
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:
Cannot catch toolbar home button click event
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();
}
});
}
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);
}
});
}
}
});
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);
}
}
});
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.
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.
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.
//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
The setup
I have an activity whose contentView is an instance of a DrawerLayout, which has a navigation drawer with a drawer indicator displayed in the action bar. The activity contains a Fragment, let's call it ListFragment, which contains a list of options. When an option is clicked, I replace the ListFragment with a DetailFragment.
At this point, I would like to display an "up" navigation option instead of the navigation drawer indicator. I'm able to display the "up" icon if I disable the drawer indicator by calling mDrawerToggle.setDrawerIndicatorEnabled(false), but this only removes the drawer icon--it does not remove the functionality--that is, when I click the caret, the navigation drawer is still opened.
Additionally, in these subviews, I would like to disable the opening of the drawer by dragging from the edge of the screen. I have tried doing this by calling setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) but it doesn't seem to have disabled this functionality.
I have tried extending the ActionBarDrawerToggle class to prevent opening the drawer when the indicator is clicked--however, all that happens is that the overriding action (the "up" navigation) is performed, but the drawer still opens.
I have also implemented the steps in Switching between Android Navigation Drawer image and Up caret when using fragments . It works insofar as displaying the caret goes, but despite overriding the up button functionality, the menu still opens (the app does navigate back--it just also opens the drawer).
Question
So, long story short: is there any (preferably clean and elegant, but at this point I'll go with hacky) way to achieve these things when my layout root is a DrawerLayout:
Replace the drawer indicator with an "up" caret (tentatively doable via mDrawerToggle.setDrawerIndicatorEnabled(false))
Prevent the drawer from opening when the caret is clicked, and instead override with my own "up" functionality
Prevent the drawer from opening when I drag from the edge of the screen.
Edit
All right, it looks like if I both override ActionBarDrawerToggle AND onOptionsItemSelected, the menu does not open when I click the caret. But it still opens if I drag from the edge. Help!
Short Code
public void setDrawerState(boolean isEnabled) {
if ( isEnabled ) {
mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
drawerToggle.onDrawerStateChanged(DrawerLayout.LOCK_MODE_UNLOCKED);
drawerToggle.setDrawerIndicatorEnabled(true);
drawerToggle.syncState();
}
else {
mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
drawerToggle.onDrawerStateChanged(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
drawerToggle.setDrawerIndicatorEnabled(false);
drawerToggle.syncState();
}
}
This is only part of the solution that I arrived at, but it was quite hard to figure out this bug, so I'm leaving this here for posterity's sake.
This how I was defining the ListView for my navigation drawer:
<ListView
android:id="#+id/listview_drawer"
android:layout_width="240dp"
android:layout_height="match_parent"
android:layout_gravity="start|bottom"
android:background="#111"
android:choiceMode="singleChoice"
android:divider="#android:color/transparent"
android:dividerHeight="0dp" />
Even after calling setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) I was still able to slide the drawer open.
However, after changing the layout_gravity to "start" this problem seems to be resolved.
I was able to reproduce this issue in a sample, navigation-drawer-only app, so it does appear to be a reproducible issue not unique to my situation.
Building on sonida's answer. After calling setDrawerIndicatorEnabled(false), onNavigateUp wasn't being called still. So, I just created a new onClickListener that called it:
public void setDrawerState(boolean isEnabled) {
if ( isEnabled ) {
mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
drawerToggle.setDrawerIndicatorEnabled(true);
drawerToggle.syncState();
}
else {
mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
drawerToggle.setDrawerIndicatorEnabled(false);
drawerToggle.setToolbarNavigationClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
onSupportNavigateUp();
}
});
drawerToggle.syncState();
}
}
also I think
drawerToggle.onDrawerStateChanged(DrawerLayout.LOCK_MODE_UNLOCKED);
has been depreciated, but it works fine without it.
You need to disable swipe and disable the actionbar home button:
Use the below code that builds on the code already given to disable swipe
public void setDrawerState(boolean isEnabled) {
if ( isEnabled ) {
mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
mDrawerToggle.onDrawerStateChanged(DrawerLayout.LOCK_MODE_UNLOCKED);
mDrawerToggle.setDrawerIndicatorEnabled(true);
mDrawerToggle.syncState();
getActivity().getActionBar().setHomeButtonEnabled(true);
}
else {
mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
mDrawerToggle.onDrawerStateChanged(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
mDrawerToggle.setDrawerIndicatorEnabled(false);
mDrawerToggle.syncState();
getActivity().getActionBar().setHomeButtonEnabled(false);
}
}
Building on answer by #sonida And after using the tweaks given by #luca992 and #jai.
I tried above suggested codes But the "up" or "Back" arrow in left side of action bar was just not showing up in my app. But luckily I was able to fix that.
I had to add this extra line of code in setNavigationDrawerState() [Ref: android.support.v7.app.ActionBarDrawerToggle.setHomeAsUpIndicator ]
toggle.setHomeAsUpIndicator(R.drawable.ic_keyboard_backspace_white_24dp);
I downloaded the drawable: ic_keyboard_backspace_white_24dp from Material.io
Here is the complete code:
MainActivity.java -> onCreate()
DrawerLayout drawer;
ActionBarDrawerToggle toggle;
#Override
protected void onCreate(Bundle savedInstanceState) {
// Start: Code automatically generated by Android Studio
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
toggle = new ActionBarDrawerToggle(
this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
drawer.setDrawerListener(toggle);
toggle.syncState();
// End: Code automatically generated by Android Studio
// I had to add this listener as the "back" arrow was totally unresponsive
// Thanks to #luca992
toggle.setToolbarNavigationClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
onBackPressed();
}
});
// Start: Code automatically generated by Android Studio
NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);
navigationView.setNavigationItemSelectedListener(this);
// End: Code automatically generated by Android Studio
// More custom code for other stuff
// ...
}
MainActivity.java -> setNavigationDrawerState()
public void setNavigationDrawerState(boolean isEnabled) {
if ( isEnabled ) {
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
toggle.setDrawerIndicatorEnabled(true);
toggle.syncState();
}
else {
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
toggle.setDrawerIndicatorEnabled(false);
// the extra line of code goes here
toggle.setHomeAsUpIndicator(R.drawable.ic_keyboard_backspace_white_24dp);
toggle.syncState();
}
MainActivity.java -> onBackPressed()
#Override
public void onBackPressed() {
DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
if (drawer.isDrawerOpen(GravityCompat.START)) {
drawer.closeDrawer(GravityCompat.START);
} else if(getSupportFragmentManager().getBackStackEntryCount() > 0){
getSupportFragmentManager().popBackStack();
}else {
super.onBackPressed();
}
}
MainActivity.java -> startFragment() [dummy function for example]
public void startFragment(){
MyFrag myFrag = new MyFrag();
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.frag_container ,myFrag)
.addToBackStack(null)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.commit();
}
MyFrag.java --> onViewCreated()
#Override
public void onViewCreated (View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
// Say, using an implemented interface Make call to MainActivitiy's setNavigationDrawerState() passing false
// setNavigationDrawerState(false)
// ...
}
MyFrag.java --> onDestroyView()
#Override
public void onDestroyView(){
// Say, using an implemented interface Make call to MainActivitiy's setNavigationDrawerState() passing true
// setNavigationDrawerState(true)
super.onDestroyView();
}