I have an app with master/detail layout (1 activity, 1 ListView fragment and 1 detail fragment). When the user clicks an item in the ListView, a fragment transaction instantiates a detail fragment on the right-pane that includes the information corresponding to that item. When the detail fragment is shown I hide the initial action bar buttons/items and show 3 new AB items (done/delete/cancel). The user can clean the right-pane and return to the initial UI state by either pressing the back button or by pressing one of the 3 AB items.
The issue I'm experiencing is that when the user selects the app's home icon (i.e. "up navigation") the activity gets re-loaded (i.e. the animation that indicates that the activity is starting can be seen as both the action bar and the UI is been redrawn). The issue only happens when the app home icon is pressed. If the user presses the back button or a cancel/done/delete action bar button, the fragment is simply remove from the right-pane and the UI returns to initial state without any "re-loading".
The XML layout for the activity is the following (inside LinearLayout; prettify is hiding that line):
<fragment class="*.*.*.ListFragment"
android:id="#+id/titles" android:layout_weight="1"
android:layout_width="0px"
android:layout_height="match_parent" />
<FrameLayout android:id="#+id/details" android:layout_weight="2"
android:layout_width="0px"
android:layout_height="match_parent" />
The DetailsFragement has the actionBar.setDisplayHomeAsUpEnabled statement in its onCreate method:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
ActionBar actionBar = getSherlockActivity().getSupportActionBar();
actionBar.setDisplayHomeAsUpEnabled(true);
}
For both the ListView fragment and the Detail fragments the onCreateOptionsMenu() and onOptionsItemSelected() method are implemented within the fragments. Below the code for the Details fragment:
#Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.edit_menu, menu);
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
// some variable statements...
switch (item.getItemId()) {
case android.R.id.home:
//Toast.makeText(getSherlockActivity(), "Tapped home", Toast.LENGTH_SHORT).show();
onHomeSelectedListener.onHomeSelected();
return true;
case R.id.menu_edit_item_done:
editedTask.setTitle(editedTaskTitle);
onTaskEditedListener.onTaskEdited(editedTask, UPDATE_TASK, true);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
In the host activity I implement the onHomeSelectedListner to handle the app home icon press (i.e. "up navigation":
public void onHomeSelected(){
FragmentManager manager = getSupportFragmentManager();
FragmentTransaction ft = manager.beginTransaction();
TaskFragment taskFragment = (TaskFragment)getSupportFragmentManager().findFragmentById(R.id.details);
ft.remove(taskFragment);
ft.commit();
manager.popBackStack();
}
The activity's listener in charged of handling all other action bar buttons (i.e. done/delete/cancel) is onTaskEditedListener and, aside of other code that processes some data, it has the same fragment transactions shown above.
Update(1/24)
Based on tyczj and straya feedback I placed log statements inside onCreate(), onResume(), onPause() of the activity to determine the differences between onHomeSelected and onTaskEdited listeners. I'm able to confirm that during the "up navigation" event (i.e. onHomeSelected) onPause(), onCreate() and onResume() are called. Whereas during the onTaskEdited call (i.e. back button or done/delete/cancel press) none of those events are called.
Update (1/25)
Based on a suggestion by Mark Murphy, I commented out the onHomeSelected method call in the "case android.R.id.home" statement just to see what would the Activity do. The thinking was that the app would do nothing since the are no statements. Turns out that is not the case. Even without a call to the listener method (i.e. that removes the fragment), the activity is restarted and the detail fragment is removed from the fragment container.
Update (2/28)
I temporarily workaround the fact that my main activity was getting restarted by disabling the window animations (as highlighted in my own answer). However, through further testing I uncovered a bug. Thanks to Wolfram Rittmeyer's sample code I was able to figure out the real reason(s) why my activity was restarting (in master/detail single layout) during up navigation:
1) Although I was using this "onHomeSelectedListener" to properly remove the fragment from the backstack, I still had some remnant code in the ListView fragment's onOptionsItemSelected that was creating a new intent to start the hosting activity. That's why pressing the app's home icon was re-starting the activity.
2) In my final implementation (shown in my own answer), I got rid of the onHomeSelectedListener in the activity and replace the startActivity intent (i.e. offending code) inside the ListView's onOptionsItemSelected to use the fragment removal + popBackStack code originally in the onHomeSelectedListener.
After much research and poking around, turns out that only reason why my activity was restarting during "up navigation" for master/detail configuration was because I left some code in the ListView Fragment's onOptionsItemSelected that was creating an intent to start the main activity in addition to my full fragment transaction code elsewhere. Below is the final implementation with which I got "up navigation" to work properly on both phone (multiple activities) and tablet (single activity/multi-pane) configurations. Thanks to Wolfram Rittmeyer for a couple of hints in his code (link in the comment section) that help me pinpoint my problem!
Main Activity: Hosts the fragments and performs some other app-specific operations
ListView Fragment: Handles "up navigation in table configuration
#Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
if(mDualPane){
FragmentManager manager = getSherlockActivity().getSupportFragmentManager();
FragmentTransaction ft = manager.beginTransaction();
DetailFragment detailFragment = (DetailFragment)manager.findFragmentById(R.id.details);
ft.remove(detailFragment);
ft.commit();
manager.popBackStack();
getSherlockActivity().getSupportActionBar().setDisplayHomeAsUpEnabled(false);
getSherlockActivity().getSupportActionBar().setHomeButtonEnabled(false);
}
return true;
// Other case statements...
default:
return super.onOptionsItemSelected(item);
}
}
Details Fragment: Handles up navigation in phone configuration
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
// Sets "up navigation" for both phone/tablet configurations
ActionBar actionBar = getSherlockActivity().getSupportActionBar();
actionBar.setDisplayHomeAsUpEnabled(true);
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
if(!mDualPane){
Intent parentActivityIntent = new Intent(getSherlockActivity(), MainActivity.class);
parentActivityIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(parentActivityIntent);
getSherlockActivity().finish();
}
return true;
// Other case statements...
default:
return super.onOptionsItemSelected(item);
}
}
If you look at the Navigation Design Pattern you will see that you want to return to the starting activity when the home button is hit.
So say you have 2 Activities call them A1 and A2. Clicking on something in A1 takes you to A2. If the user hits the home button you should return them to A1 clearing the stack of everything up until that activity like this
Intent intent = new Intent(this, A1.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
this is what the flag Intent.FLAG_ACTIVITY_CLEAR_TOP does
If set, and the activity being launched is already running in the current task, then instead of launching a new instance of that activity, all of the other activities on top of it will be closed and this Intent will be delivered to the (now on top) old activity as a new Intent.
For example, consider a task consisting of the activities: A, B, C, D. If D calls startActivity() with an Intent that resolves to the component of activity B, then C and D will be finished and B receive the given Intent, resulting in the stack now being: A, B.`
The currently running instance of activity B in the above example will either receive the new intent you are starting here in its onNewIntent() method, or be itself finished and restarted with the new intent. If it has declared its launch mode to be "multiple" (the default) and you have not set FLAG_ACTIVITY_SINGLE_TOP in the same intent, then it will be finished and re-created; for all other launch modes or if FLAG_ACTIVITY_SINGLE_TOP is set then this Intent will be delivered to the current instance's onNewIntent().
This launch mode can also be used to good effect in conjunction with FLAG_ACTIVITY_NEW_TASK: if used to start the root activity of a task, it will bring any currently running instance of that task to the foreground, and then clear it to its root state. This is especially useful, for example, when launching an activity from the notification manager.
don't: break and then return super.onOptionsItemSelected(item), rather just: return true;
UPDATE:
So you're saying the Activity is "restarted" based on what you see happen with Views, but can you confirm what may or may not happen to the Activity (and Fragments for that matter) by using logging in the various lifecycle methods? That way you can be sure of what the current (erroneous) behaviour is before moving forward with diagnosis.
UPDATE:
OK, good to be sure about behaviour :)
Now regarding your question "What is the correct way to implement "up navigation" for a master/detail layout (1 activity/2fragments)? ":
The typical way is that the 2 Fragments got added within a single FragmentTransaction and you simply popBackStack to remove them and go back to whatever previous state was. I think you're doubling up by manually removing a Fragment within a FragmentTransaction and then popping backstack. Try just popBackStack. Oh and just to be sure and consistent, since you're using ActionBarSherlock and support.v4 are you using a FragmentActivity (rather than an Activity) and SherlockFragment?
I think you should handle the Up button only inside the activity.
If youre in a phone, the up button will be handled by activity that acts as a wrapper of that fragment, in tablet (master/detail pattern) you dont want it anyways
Related
I'm so very confused and have been reading on this topic for a while.
I have a MainActivity that has multiple possible contents (switched between via navigation drawer), which I've set via multiple fragments (lets call them Fragment1, Fragment2 and Fragment3).
That works fine.
One of my fragments, Fragment3, is a View that can segue to a new activity, View2.
View2 has a back button. When I press on the View2 back button I want to see Fragment3 on my MainActivity, not Fragment1, which is what I currently get. This is because OnCreate, by default, loads Fragment1.
What is the best way to do this?
Thanks so much in advance! (vent: iOS makes this so much easier).
The official documentation for Fragments states that you should make sure to call addToBackStack() before commiting your fragment transaction on your first activity holding the 3 fragments.
In order to go back to the last used fragment from the second activity , you should override the onBackPressed() method in this activity :
#Override
public void onBackPressed() {
if (getFragmentManager().getBackStackEntryCount() > 0) {
getFragmentManager().popBackStack();
} else {
super.onBackPressed();
}
}
The documentation is very complete on the subject : Read it here
Update:
I just added this to the back button code:
finish();
return true;
I had to do it within onOptionsItemSelected(MenuItem item)... for some reason onBackPressed is not fired.
I've been researching a solution to my problem but not a single result fixes my problem.
I have five Java activities. Each activity has five buttons, four to the other activities and one to itself. It's basically just a bar of bottoms at the bottom of the page.
My issue is that let's say I'm in one activity, and then I click that button to the activity again, it opens and loads an entirely new activity. And then when I click back, it shows the previous activity open as I left it. But if I press it ten times, I need to click back ten times. Also, if I switch between each activity twice and I want to click back to main to exit, I need to click back ten times instead of five.
I want it to be such that when I click a button, it opens the activity. If I press the button again, it does nothing if I'm in that activity. And if I'm switching between activities and I repress an activity I already opened, I want that to be brought to the top of the stack from its lower location, not added. So at most, I only want five activities running.
It's a very confusing problem as I see people suggesting to use intent flags, which I'm not sure if I place them in the manifest, which does nothing with I try. And I've seen people suggest using LaunchMode single instance, which also does nothing.
I really think fragments are the best method here. And yes, it's a little tricky but you can use fragments within fragments. For navigating without creating extra instances, I think you might be looking for something along the lines of this:
public boolean onNavigationItemSelected(MenuItem item) {
// Handle navigation view item clicks here.
int id = item.getItemId();
if(id == R.id.nav_home){
Fragment newFragment;
FragmentManager fragmentManager = getSupportFragmentManager();
// Look for fragment by tag
Fragment foundFragment = fragmentManager.findFragmentByTag(MAIN_FRAGMENT);
if(foundFragment != null) { //if fragment's already in the backstack
newFragment = foundFragment; //replace fragment with backstack fragment
}else{
newFragment = new MainFragment(); // use a new fragment instance
}
// Add new fragment instance to fragment manager
fragmentManager.beginTransaction()
.replace(R.id.fragment_container, newFragment, MAIN_FRAGMENT)
.addToBackStack(MAIN_FRAGMENT)
.commit();
}
// ...
}
I've been researching all day/night for a solution, but it seems there are lots of options to go from an Activity to a Fragment, but none are working for me on S.O. In practice, I am in an Activity, and I want to use my app logo in the ActionBar to click it and then return to a Fragment. This Fragment is the "parent class" of my Activity, meaning there was a button in the Fragment I clicked that took me to my Activity.
But I can't get all the code snippets I've seen to work.
I have put this in my onCreate() of my Activity:
// Shows the up carat near app icon in ActionBar
getSupportActionBar().setDisplayUseLogoEnabled(false);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
I also included this in my onOptionsItemSelected() method of my Activity:
// Handle action bar actions click
switch (item.getItemId()) {
case android.R.id.home:
android.app.FragmentManager fm= getFragmentManager();
fm.popBackStack();
return true;
default:
return super.onOptionsItemSelected(item);
}
The result is that I see a "back button" carat (as shown below), but when I click it nothing happens. I'm supposed to go back to the Fragment I came from. FYI, my Fragment class actually extends Fragment (not FragmentActivity). My Activity extends ActionBarActivity, so I am looking for an answer that will work for Android 4.0+. Also, my Fragment does not need the same instance (necessarily) when it is returned to. It only has buttons on there, so a new instance is fine, if it gets created, upon returning.
Thanks for your help!!
One small line was needed: finish(). Since the FragmentManager pops off one item in its backstack, by using fm.popBackStack();, it still needs some sort of action to go to the previous fragment. Adding finish() enabled the current Activity to end. The line in context:
#Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar actions click
switch (item.getItemId()) {
case android.R.id.home:
android.app.FragmentManager fm= getFragmentManager();
fm.popBackStack();
finish();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
The rest of the code I had above was correct and is needed too, to make it all work. Now, I am able to navigate to my NavigationDrawer fragment, click a button there to go to an Activity, then press the navigation UP caret to return to my Fragment at anytime. This was tested successfully on a Samsung Galaxy5 phone.
One thing that you do not read about in the Navigating Up with the App Icon section of the ActionBar Android doc is that since you are using a fragment to return to, you cannot use their <meta-data> tag instruction to specify the parent activity in the manifest file, because you are not returning to an Activity! But rather a Fragment. So a workaround had to be achieved by using FragmentManager.
I am using a Navigation Drawer on the main activity of my app with three options. Option one is the default fragment being used in the main activity. Clicking option two just replaces the old fragment with a new, but stays in the same main activity. This is all good.
Next step is that I click one of the list items in option two and it opens a new activity+fragment. I implemented the Home Up button in the new fragment with
((ActionBarActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(true);
The problem comes up with I hit the Home Up button. It takes me to the main activity with the initial fragment (option one) and not the option two fragment. How do I change this?
You need to override onOptionsItemSelected in Activity two and just finish it when home pressed
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
I have an ActionBar activity. In this activity I have implemented Navigation Drawer from Android API.
One option of navigation drawer set a ListFragment with some elements inside its list.
When I click some elements I want to create a new fragment and set previous ListFragment to the stack. Also I want to destroy this new fragment by clicking ActionBar home button, in order to return to the previous ListFragment.
My problem comes here: When I click home button of the actionbar, drawer layout is displayed, instead of destroy the fragment... What should I do?
I have Overriden onOptionsItemSelected method in the fragment:
#Override
public boolean onOptionsItemSelected(MenuItem menuItem) {
switch (menuItem.getItemId()) {
case android.R.id.home:
getFragmentManager()
.popBackStack();
}
return (super.onOptionsItemSelected(menuItem));
}
Also I have set ListFragment to the backstack when Inflating the new fragment:
getFragmentManager()
.beginTransaction()
.setCustomAnimations(android.R.anim.slide_in_left, android.R.anim.slide_out_right)
.replace(R.id.activity_main_fragment_container, fragment)
.addToBackStack(null)
.commit();
Well, as the docs say here, popBackStack() is async. I suppose that the drawer layout is displayed because you call super.onOptionsItemSelected(menuItem).
I suggest you to return true for all cases you handle this selection by yourself (in this specific situation: case android.R.id.home:), and call getActivity().onBackPressed() (assuming that by pressing hardware back button the last fragment gets removed, as it should) instead of popping back stack directly. I've implemented a similar solution and it works for me.