First of all, I have searched and found a lot of similar issues like what I'm experiencing, however, I had tried the solutions posted there and none have worked for me.
The problem I'm having is that whenever I switch from tab to tab and then rotate the device, there's a Fragment overlapping the selected one.
Here is the code I'm using. Let me know if there's more information needed.
Activity:
public class SupervisionDetailsActivity : Activity, ActionBar.ITabListener
{
private enum TabType { Summary, Data1, Data2 }
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
SetContentView(Resource.Layout.SupervisionDetails);
ActionBar.NavigationMode = ActionBarNavigationMode.Tabs;
AddTab("Summary", TabType.Summary);
AddTab("External", TabType.Data1);
AddTab("Internal", TabType.Data2);
}
private void AddTab(string label, TabType type)
{
var tab = ActionBar.NewTab();
tab.SetTag(type.ToString());
tab.SetText(label);
tab.SetTabListener(this);
ActionBar.AddTab(tab);
}
public void OnTabSelected(ActionBar.Tab tab, FragmentTransaction ft)
{
var f = FragmentManager.FindFragmentByTag(tab.Tag.ToString());
switch (tab.Position)
{
case 0:
if (f != null)
ft.Show(f);
else
ft.Add(Resource.Id.fragmentContainer, new FragmentSupervisionDetailsSummary(), TabType.Summary.ToString());
break;
case 1:
if (f != null)
ft.Show(f);
else
ft.Add(Resource.Id.fragmentContainer, new FragmentSupervisionDetailsData1(), TabType.Data1.ToString());
break;
case 2:
if (f != null)
ft.Show(f);
else
ft.Add(Resource.Id.fragmentContainer, new FragmentSupervisionDetailsData2(), TabType.Data2.ToString());
break;
default:
break;
}
}
public void OnTabReselected(ActionBar.Tab tab, FragmentTransaction ft)
{
}
public void OnTabUnselected(ActionBar.Tab tab, FragmentTransaction ft)
{
var f = FragmentManager.FindFragmentByTag(tab.Tag.ToString());
ft.Hide(f);
}
protected override void OnSaveInstanceState(Bundle outState)
{
base.OnSaveInstanceState(outState);
// Save selected tab
outState.PutInt("tab", ActionBar.SelectedNavigationIndex);
}
protected override void OnRestoreInstanceState(Bundle savedInstanceState)
{
base.OnRestoreInstanceState(savedInstanceState);
// Restore selected tab
int saved = savedInstanceState.GetInt("tab", 0);
if (saved != ActionBar.SelectedNavigationIndex)
ActionBar.SetSelectedNavigationItem(saved);
}
}
If I replace those lines:
ft.Add(Resource.Id.fragmentContainer, new FragmentSupervisionDetailsSummary(), TabType.Summary.ToString());
ft.Add(Resource.Id.fragmentContainer, new FragmentSupervisionDetailsData1(), TabType.Data1.ToString());
ft.Add(Resource.Id.fragmentContainer, new FragmentSupervisionDetailsData2(), TabType.Data2.ToString());
With those:
ft.Replace(Resource.Id.fragmentContainer, new FragmentSupervisionDetailsSummary(), TabType.Summary.ToString());
ft.Replace(Resource.Id.fragmentContainer, new FragmentSupervisionDetailsData1(), TabType.Data1.ToString());
ft.Replace(Resource.Id.fragmentContainer, new FragmentSupervisionDetailsData2(), TabType.Data2.ToString());
The overlapping problem is gone but the data/information from the fragments are lost which is not what I want.
Now, let me explain why the data is important to keep.
The first time the Fragments are created, there's no information (unless pulled from the database).
The information is collected from 3 places (the 3 tabs), the Summary which contains a little but important information like the Manager, Supervisor, etc and which does some calculations based on the data on the next tabs. The External and Internal contains a lot of controls like EditTexts, RadioButtons, etc.
The information or data is not saved immediately and sometimes is not even saved (not needed to). This is because there are a lot of times that the user captures some data in tab 1 and then switches to tab 3, then to tab 2, and again to tab 1, and so on. And as I said, occasionally is only for getting some quick information calculated in the tab 1 (Summary) which is not useful for saving.
That's the reason the data don't need to be destroyed when changing tabs, hence the use of Hide and Show.
I know I can temporarily store the information using a variety of methods, but it is really a lot of information (like around 180 variables in tab 3 for giving an example). The most practical and easy way I have come is to avoid re-creating the Fragments.
Anyway, with this information in mind, could I get some advice on how I can avoid the overlapping and at the same time retain the information stored in those Fragments when switching through them?
I really appreciate the help, thanks in advance!
Well, after looking over and over again, I ended implementing a combination of ActionBar.Tab with ViewPager like shown here:
HelloSwipeViewWithTabs
And setting the OffscreenPageLimit to a value of 2 for maintaining the data of the 3 tabs.
The result is very similar to what I had just with a nice swipe feature for easily navigating through the Fragments.
Related
When the action bar has tabs, I've noticed that onTabSelected is called when the activity loads on screen. It also is being called whenever an orientation change occurs. My code queries the database depending on the tab being selected, and displays the query results to the loaded layout.
My problem is when saving tab state, and the current selected tab is 1 or higher, on restore state, since onTabSelected is called by default on the 0 tab, it will be called again when restored state tab is 1 or higher. This makes database query on tab 0 useless.
How to configure android that onTabSelected isn't called on tab creation or at least detect that this call is default and not user triggered?
#Override
public void onTabSelected(Tab tab, FragmentTransaction ft) {
int tabPos = tab.getPosition();
switch(tabPos) {
case 0:
// query database and display result
break;
case 1:
// a different query and display result
break;
case 2: ...
}
}
#Override
public void onRestoreInstanceState(Bundle savedInstanceState) {
if(savedInstanceState.containsKey(STATE_SELECTED_TAB)) {
getActionBar().setSelectedNavigationItem(savedInstanceState.getInt(STATE_SELECTED_TAB));
}
super.onRestoreInstanceState(savedInstanceState);
}
#Override
public void onSaveInstanceState(Bundle outState) {
outState.putInt(STATE_SELECTED_TAB, getActionBar().getSelectedNavigationIndex());
super.onSaveInstanceState(outState);
}
Added complication:
When the current selected tab is 0, and the orientation changes, onTabSelected is still called twice! Once when the tabs are initially created, and 2nd time when onRestoreState restores the saved tab selected state, even though it is 0.
What I originally supposed was that onTabSelected was called twice, but I was mistaken. It was my fragment onCreateView being called twice, some errors in fragment transaction that added the same fragment twice on orientation change. onTabSelected is called once, and restore state calls onTabReselected is called too when the restored tab is also 0.
After scouring SO and google, I've found this question to have similar cause of problem.
Creating ActionBar tab also calling its selectTab functions
So after reviewing the reference docs on ActionBar from Google's Android site, addTab method is the one responsible for calling onTabSelected by default.
public abstract void addTab (ActionBar.Tab tab)
Add a tab for use in tabbed navigation mode. The tab will be added at the end of the list. If this is the first tab to be added it will become the selected tab.
Parameters
tab Tab to add
Incidentally, other overloaded methods exist that do not call onTabSelected.
public abstract void addTab (ActionBar.Tab tab, boolean setSelected)
So I used these alternative methods instead and it fixed the problem.
However, once the tabs are displayed, the first tab may appear selected even though it's not. Clicking on it will call onTabSelected and not onTabReselected.
I solved it in this way:
Call setupWithViewPager before tabLayout.addOnTabSelectedListener
tabLayout.setupWithViewPager(viewPager)
tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {....}
.....
}
because when you call setupWithViewPager, this will internally call
setOnTabSelectedListener(new
ViewPagerOnTabSelectedListener(viewPager));
so you better call it before adding tabSelect Listener to tabLayout
i think you can do this (and ignore any typo please :-)) :
public class MainActivity extends FragmentActivity {
boolean isConfigChanged;
int savedTabIndex;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if(savedInstanceState != null){
if(savedInstanceState.containsKey(STATE_SELECTED_TAB)) {
savedTabIndex = savedInstanceState.getInt(STATE_SELECTED_TAB);
//getActionBar().setSelectedNavigationItem(savedTabIndex); actually you do not need this
}
isConfigChanged = true;
}
// here add actionbar tabs
//...}
and in :
below code checks that if configuration changes and the user selected tab is not zero so this is default call and ignore but if isConfigChanged == true and the user selected tab is 0 you must query DB or if isConfigChanged == false you must query DB because it is first time that app is loading. a quick play may fit it to what you want.
#Override
public void onTabSelected(Tab tab, FragmentTransaction ft) {
if(isConfigChanged && savedTabIndex != 0){
isConfigChanged = false;
return;
}
isConfigChanged = false;
int tabPos = tab.getPosition();
switch(tabPos) {
case 0:
// query database and display result
break;
case 1:
// a different query and display result
break;
case 2: ...
}
}
remove onRestore
#Override
public void onSaveInstanceState(Bundle outState) {
outState.putInt(STATE_SELECTED_TAB, getActionBar().getSelectedNavigationIndex());
super.onSaveInstanceState(outState);
}
I have an android application where I use the ActionBar in NAVIGATION_MODE_TABS mode.
Currently, I am heavily using Fragments. So, that worked quite well for the displayed tabs. I am facing a design issue where I think I am going in the wrong direction.
MainActivity has three tabs. One tab in particular have bunch of navigations on it.
MainActivity
Tab1
Tab2
Tab3 (This has buttons that should display different fragments based on what the user clicked)
The requirement is to keep the tabs always visible with the same text. So, I ended up creating bunch of activities that inherit from the MainActivity where I made the third tab content based on what the user clicked on.
This allowed me to sustain the content in a good flow and keep the look consistent. However, It seems an overhead to create an activity.
I tried to leverage the FragmentManager and pushToStack. However, that route didn't seem to work.
I am relying on this interface ActionBar.TabListener to properly attach and detach the fragments based when the user clicks on the tab. That is good. However, the minute I introduce a different fragment that seemed problematic.
Hope That is clear and I am looking for the best advice.
I solved it by relying on the the following logic:
public class FragmentTabListener implements ActionBar.TabListener
{
private String _fragmentClassName;
private Context _context;
private Fragment _fragment;
private Boolean _cleanStack = true;
public FragmentTabListener(Context context, String fragmentClassName,
Boolean cleanStack)
{
_context = context;
_fragmentClassName = fragmentClassName;
_cleanStack = cleanStack;
}
public FragmentTabListener(Context context, String fragmentClassName)
{
this(context, fragmentClassName, false);
}
#Override
public void onTabReselected(Tab tab, FragmentTransaction ft)
{
cleanFragmentManagerStack();
if (this._cleanStack)
{
ft.attach(_fragment);
}
}
private void cleanFragmentManagerStack()
{
if (this._cleanStack)
{
FragmentManager mgr = ((Activity) _context).getFragmentManager();
int backStackCount = mgr.getBackStackEntryCount();
for (int i = 0; i < backStackCount; i++)
{
// Get the back stack fragment id.
int backStackId = mgr.getBackStackEntryAt(i).getId();
mgr.popBackStack(backStackId,
FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
}
}
#Override
public void onTabSelected(Tab tab, FragmentTransaction ft)
{
if (_fragment == null)
{
_fragment = Fragment.instantiate(_context, _fragmentClassName);
ft.add(android.R.id.content, _fragment);
}
else
{
ft.attach(_fragment);
}
}
#Override
public void onTabUnselected(Tab tab, FragmentTransaction ft)
{
this.cleanFragmentManagerStack();
// TODO Auto-generated method stub
ft.detach(_fragment);
}
}
Any action on my original fragment, I was adding a fragment to the current stack.
This worked. Hope it helps someone.
From what I understand you need 3 fragments, each one of them in one tab, and in the 3rd tab you can have, inside that fragment, based on what user clicked, a different fragment inside (nested fragments are supported now). Maybe I didn't understand your problem correctly, if that's the case tell me.
Cheers!
I have this activity where I run an asynctask that gathers a series of data. This data can be represented in several ways, for the sake of simplicity let's say there's two ways. I want the user to be able to switch between these two representations (listview and graphical view).
So I created a FragmentActivity with my ListFragment and my GraphicFragment.
How do you switch between these two so that user can go back and forth? I tried using replace but then the previous fragment gets destroyed which is not of my interest since its reference has the data passed by my Activity. (i.e. I don't want to recreate the fragment each times the user switches between views).
I also tried hiding and showing but I can't get it to work, one of the fragments is not showing.
Also, when I rotate I guess the fragments get destroyed because I get a nullpointerexception when trying to access them.
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_layout);
pBar = (ProgressBar) findViewById(R.id.analyzingProgress);
// However, if we're being restored from a previous state, then don't
// create the fragment again (please)
if (savedInstanceState == null) {
firstFragment = new DirListFragment();
secondFragment = new GraphViewFragment();
getSupportFragmentManager().beginTransaction().add(R.id.fragment_container, secondFragment).commit();
fragments= new ArrayList<IFragment>();
fragments.add(firstFragment);
fragments.add(secondFragment);
}
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
if(item.getItemId() == R.id.listView){
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
/*transaction.hide(secondFragment);
transaction.show(firstFragment);*/
transaction.replace(R.id.fragment_container, firstFragment);
transaction.addToBackStack(null);
transaction.commit();
}
else if(item.getItemId() == R.id.graph){
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
/*transaction.hide(firstFragment);
transaction.show(secondFragment);*/
transaction.replace(R.id.fragment_container, secondFragment);
transaction.addToBackStack(null);
transaction.commit();
}
return true;
}
The problem comes when I try to access the fragment fields to update their data:
for(IMyFragment frag: fragments){
frag.updateData(data);
}
I get a NullPointerException after rotating the screen.
Also when switching fragmenents, their data is not updated (i.e. I start with list fragment, it's showing the proper data, then switch to graphic, no data is shown, switch back to list, no data again.)
If the fields are null when using setRetainInstance(true);, then you are recreating them (the fragments) in your activity. Make sure you are setting something in onSaveInstanceState() in your activity so in onCreate(), savedInstanceState will not be null, it can even be an empty bundle or useless variable, ex:
#Override
public void onSaveInstanceState(Bundle outstate){
super.onSaveInstanceState(outstate);
outstate.putBoolean("stateChanged", true);
}
Just make sure you call super before adding your arguments.
On another note, I personally have never stored my fragments in an array. I use a FrameLayout as a fragment container in my activities, just like you, and if I need to update something I just call: getSupportFragmentManager().findFragmentById(R.id.fragment_container).setWhatINeed(); This way I always get the visible fragment.
Let me know how the update goes and I'll update this post with any information I can.
I have an application running a single activity with multiple (2) fragments at a given time. I've got a fragment on the left which functions as a menu for which fragment to
display on the right hand side.
As an example lets say the menu consists of different sports; Football, Basketball, Baseball, Skiing, etc. When the user selects a sport, a fragment with details on the specific sport is displayed to the right.
I've set up my app to display two fragments at once in layout-large and layout-small-landscape. In layout-small-portrait however, only one fragment is displayed at a given time.
Imagine this; a user is browsing the app in layout-small-landscape (two fragments at a time) and selects a sport, Football. Shortly after he selects Basketball. If he now chooses to rotate into layout-small-portrait (one fragment at a time) I want the following to happen:
The Basketball fragment should be visible, but if he presses the back button, he should return to the menu and not to the Football fragment (!) which by default is the previous fragment in the back stack.
I have currently solved this like the following:
....
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// static fragments
if(menuFragment == null) menuFragment = new MenuFragment();
if(baseFragment == null) baseFragment = new TimerFragment(); // default content fragment
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
// Determine what layout we're in..
if(app().getLayoutBehavior(this) == LayoutBehavior.SINGLE_FRAGMENT) {
// We are currently in single fragment mode
if(savedInstanceState != null) {
if(!rotateFromSingleToDual) {
// We just changed orientation from dual fragments to single fragment!
// Clear the entire fragment back stack
for(int i=0;i<getSupportFragmentManager().getBackStackEntryCount();i++) {
getSupportFragmentManager().popBackStack();
}
ft.replace(R.id.fragmentOne, menuFragment); // Add menu fragment at the bottom of the stack
ft.replace(R.id.fragmentOne, baseFragment);
ft.addToBackStack(null);
ft.commit();
}
rotateFromSingleToDual = true;
return;
}
rotateFromSingleToDual = true;
ft.replace(R.id.fragmentOne, menuFragment);
} else if(app().getLayoutBehavior(this) == LayoutBehavior.DUAL_FRAGMENTS) {
// We are now in dual fragments mode
if(savedInstanceState != null) {
if(rotateFromSingleToDual) {
// We just changed orientation from single fragment to dual fragments!
ft.replace(R.id.fragmentOne, menuFragment);
ft.replace(R.id.fragmentTwo, baseFragment);
ft.commit();
}
rotateFromSingleToDual = false;
return;
}
rotateFromSingleToDual = false;
ft.replace(R.id.fragmentOne, menuFragment);
ft.replace(R.id.fragmentTwo, baseFragment);
}
ft.commit();
}
This works, at least from time to time. However, many times I get java.lang.IllegalStateException: Fragment already added: MenuFragment (....)
Can anyone please give me some pointers as to how to better implement this? My current code is not pretty at all, and I'm sure many developers out there want to achieve exactly this.
Thanks in advance!
A common way to implement this scenario is to only use the fragment stack when in a mode that shows multiple fragments. When you're in the single fragment mode, you start a new activity that's sole job is to display the single fragment and take advantage of the activity back stack.
In your case you'll just need to remember the currently selected spot on rotate to set it as an argument when starting the new activity.
It's explained much better here:-
http://developer.android.com/guide/components/fragments.html
Hope that helps.
I've seen quite a few questions on SO about Fragments and I still can't seem to figure out if what I want to do is possible, and more so if my design pattern is just flawed and I need to re-work the entire process. Basically, like most questions that have been asked, I have an ActionBar with NavigationTabs (using ActionBarSherlock), then within each Tab there is a FragementActivity and then the FragmentActivities push new Fragments when a row is selected (I'm trying to re-create an iOS Project in Android and it's just a basic Navigation based app with some tabs that can drill down into specific information). When I click the back button on the phone the previous Fragment is loaded but the Fragment re-creates itself (so the WebServices are called again for each view) and this isn't needed since the information won't change in a previous view when going backwards. So basically what I want to figure out is how do I setup my Fragments so that when I push the back button on the phone, the previous Fragment is just pulled up with the previous items already created. Below is my current code :
//This is from my FragmentActivity Class that contains the ActionBar and Tab Selection Control
#Override
public void onTabSelected(ActionBar.Tab tab, FragmentTransaction ft) {
// TODO Auto-generated method stub
int selectedTab = tab.getPosition();
if (selectedTab == 0) {
SalesMainScreen salesScreen = new SalesMainScreen();
ft.replace(R.id.content, salesScreen);
}
else if (selectedTab == 1) {
ClientMainScreen clientScreen = new ClientMainScreen();
ft.replace(R.id.content, clientScreen);
}.....
//This is within the ClientMainScreen Fragment Class, which handles moving to the Detail Fragment
row.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
//Do something if Row is clicked
try{
String selectedClientName = clientObject.getString("ClientName");
String selectedClientID = clientObject.getString("ClientID");
String selectedValue = clientObject.getString("ClientValue");
transaction = getFragmentManager().beginTransaction();
ClientDetailScreen detailScreen = new ClientDetailScreen();
detailScreen.clientID = selectedClientID;
detailScreen.clientName = selectedClientName;
detailScreen.clientValue = selectedValue;
int currentID = ((ViewGroup)getView().getParent()).getId();
transaction.replace(currentID,detailScreen);
transaction.addToBackStack(null);
transaction.commit();
}
catch (Exception e) {
e.printStackTrace();
}
}
});....
//And then this is the Client Detail Fragment, with the method being called to Call the Web Service and create thew (since what is displayed on this screen is dependent on what is found in the Web Service
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup group, Bundle saved) {
return inflater.inflate(R.layout.clientdetailscreen, group, false);
}
#Override
public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
//Setup Preferences File Link
this.preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
//initialize the table object
mainTable = (TableLayout)getActivity().findViewById(R.id.mainTable);
//setup the detail table
setupRelatedClientSection();
}
The Client Detail Screen can then drill down one more time, using the same method as the Client Main Screen but when I go back from that new screen to the Detail Screen the seuptRelatedClientSection() method is called again and so the entire Fragment is rebuilt when really I just want to pull up a saved version of that screen. Is this possible with my current setup, or did I approach this the wrong way?
Try using fragementTransaction.add instead of replace
I believe that you are looking for show() and hide().
I think you can still add them to the backstack.
transaction.hide(currentFragment);
transaction.show(detailScreen);
transaction.addToBackStack(null);
transaction.commit();
I didnt have my code to look at but i believe this is how it would go... Try it out unless someone else has a better way.
I have not tried the backstack with show() hide() but i believe that it takes the changes that are made before the transactions commit and will undo them if the back button is pressed. Please get back to me on this cause i am interested to know.
You also have to make sure that the detail fragment is created before you call this. Since it is based on the click of someitem then you should probably create the details fragment every time you click to make sure the correct details fragment is created.
I'm posting this answer for people who may refer this question in future.
Following code will demonstrate how to open FragmentB from FragmentA and going back to FragmentA from FragmentB (without refreshing FragmentA) by pressing back button.
public class FragmentA extends Fragment{
...
void openFragmentB(){
FragmentManager fragmentManager =
getActivity().getSupportFragmentManager();
FragmentB fragmentB = FragmentB.newInstance();
if (fragmentB.isAdded()) {
return;
} else {
fragmentManager.
beginTransaction().
add(R.id.mainContainer,fragmentB).
addToBackStack(FragmentB.TAG).
commit();
}
}
}
public class FragmentB extends Fragment{
public static final String TAG =
FragmentB.class.getSimpleName();
...
public static FragmentB newInstance(){
FragmentB fragmentB = new FragmentB();
return fragmentB;
}
#Override
public void onResume() {
super.onResume();
// add this piece of code in onResume method
this.getView().setFocusableInTouchMode(true);
this.getView().requestFocus();
}
}
In your MainActivity override onBackPressed()
class MainActivity extends Activity{
...
#Override
public void onBackPressed() {
if (getFragmentManager().getBackStackEntryCount() > 0) {
getFragmentManager().popBackStack();
} else {
super.onBackPressed();
}
}
}
You're right, there has been a number of previous questions / documentation on the topic ;)
The documentation on Fragments, specifically the section about Transactions and Saving State, will guide you to the answer.
http://developer.android.com/guide/components/fragments.html#Transactions
http://developer.android.com/guide/components/activities.html#SavingActivityState
Android - Fragment onActivityResult avoid reloading
Fragments can have support for onSaveInstanceState but not onRestoreInstanceState, so if you want to save a reference to the table views, save them to the Bundle and you can access the saved view in your onActivityCreated method. You could also use the Fragments back stack.
This guide/tutorial has very detailed instructions/examples on the back stack and retaining fragment state.
Good luck