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);
}
Related
I'm trying to save data a user enters in a fragment to a file.
Scenario:
one viewpager and 7 fragments
A user starts in fragment 0 and can enter text into edittexts,
by swiping, using tabhost or pressing floating arrows the user can switch to other fragments.
I want to save alle entered text of the fragment the user leaves with the methods above.
I tried a OnPageChangeListener, but there i can't get the previous tab. I logged the values of the implementation methods onPageScrolled, onPageSelected, onPageScrollStateChanged.
Non of these seem to work for my needs.
onPageScrolled is called several times and shows only the current tab until it is of screen, the offset is different and not always starts by 0.0, so i can't use this reliably.
onPageSelected is the only reliable one but only returns the new current tab
onPageScrollStateChanged has no information i could use to determine the tab
I also looked into onInterceptTouchEvent in the ViewPager but this is also some times invoked several times (for MOVE events) and does not always work for every tab.
Is there a way to get this cost efficent? I want to store the data in an encrypted file and don't want to do this several times over.
Because the suggestions didn't work for my case I came up with another idea I wan't to share with others.
First instead of focusing on the ViewPager to suite my needs I thought wouldn't it be clever to led the fragment know if its changed and handle that instead.
So I created an abstract class extending the android Fragment with a boolean attribute dataChanged which I check every time the OnPageChangeListener calls onPageSelected (iterate over all fragments in the pager).
Naturally all Fragments in the pager should extend the abstract class. Furthermore I added abstract methods save() and load() to the abstract class.
So in onPageSelected(int position), after saving all changes for all fragments, which should only be one at a time, I load the data of the now selected fragment via the position attribute.
There was but one problem. If a fragment was paused and resumed the dataChanged attribute was always true if I set it in onTextChangeListeners, because of the automatic loading of widget values that android does. So I also override onResume to set the dataChanged to false.
Also every MyFragment has to handle the dataChanged attribute in the save() and load() method.
Abstract Fragment
public abstract class MyFragment extends Fragment {
private boolean dataChanged = false;
#Override
public void onResume() {
super.onResume();
setDataChanged(false);
}
public boolean isDataChanged() {
return dataChanged;
}
public void setDataChanged(boolean dataChanged) {
this.dataChanged = dataChanged;
}
public abstract void save();
public abstract void load();
}
OnPageChangeListener of ViewPager
fragmentViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
...
#Override
public void onPageSelected(int position) {
for(Fragment f : fragments) {
if(f instanceof MyFragment && ((MyFragment)f).isDataChanged()) {
((MyFragment) f).save();
}
}
if(fragmentViewPager.getCurrentItem() == position) {
Fragment fragment = getSupportFragmentManager().findFragmentByTag("android:switcher:" + R.id.view_pager + ":" + fragmentViewPager.getCurrentItem());
if(fragment instanceof MyFragment) {
((MyFragment) fragment).load();
}
}
}
...
});
Explaining my problem :
I spend much time but I can not get this to work.I have view pager in main activty that contains three fragments using (Tabhost).My ViewPagerAdapter class extend FragmentStatePagerAdapter.
The problem I'm facing that my OnResume() Method is not called when I swipe the View .And I want to update the view of viewpager's fragment on swipe.
My OnResume() method is only called when i click on the ListView item and back again . but when I press OnLongClick on the ListView other fragments are not refreshed .
Note : I know that this question was asking before but none of those solutions helped me .
Note 2: When my phone goes to sleep then after unlocking phone 2nd fragment calling onResume()
My OnResume() Method in the first tab :
#Override
public void onResume() {
super.onResume();
adapterLogin.notifyDataSetChanged();
}
My OnResume() Method in the second Tab:
#Override
public void onResume() {
super.onResume();
adapterLogin.UpdateView(databaseHelper.getAllVoitureFavourite(1,username));
adapterLogin.notifyDataSetInvalidated();
adapterLogin.notifyDataSetChanged();
}
My UpdateView() Method in the BaseAdapter :
public void UpdateView(List<Voiture> items) {
this.voitureList = items;
notifyDataSetInvalidated();
notifyDataSetChanged();
}
Screen shot of my App for more understanding mu issue :
Any help will be appreciated.
Use setUserVisibleHint(boolean isVisibleToUser).
#Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser) {
// Do your stuff here
}
}
The behavior you describe is how ViewPager works, there is nothing wrong with it.
Also, if you check the source code of the ViewPager class you will notice that the minimum offscreenPageLimit is 1. Setting it to 0 simply does nothing as it falls back to the default value 1.
What you can do is to add a TabHost.OnTabChangeListener so that on each swipe have the appropriate method called.
mTabHost.setOnTabChangedListener(new TabHost.OnTabChangeListener() {
#Override
public void onTabChanged(String tabId) {
switch (mTabHost.getCurrentTab()) {
case 0:
//fragment 1 update()
break;
case 1:
//fragment 2 update()
break;
case 2:
//fragment 3 update()
break;
}
}
});
if you set offscreen page limit = 0 then this on swipe onviewcreate or onviewcreated method also call again and again this may disturb the flow of your app best approach which I use is using interface method override in your fragment and on page change invoke this override method that will work fine for you
If your ViewPager only has 2 pages, then neither of the fragments will pause during a swipe, and onResume() will never be called.
I believe that it always retains the neighbor pages by default, meaning it has a page limit of 1.
You could try setting the number of pages that it retains to 0.
mViewPager = (ViewPager)findViewById(R.id.pager);
mViewPager.setOffscreenPageLimit(0);
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.
I have a ViewPager (instantiated with FragmentStatePagerAdapter) with some Fragment attached to it.
In a specific usecase I need to reset instanceBean and UI for most of the fragments in the pager.
After some googling I have tried some solutions like this but the side effects were not easy manageable. Other solution like this doesn't match my needs.
So I decided to go straight with the manual reset of the UI and instanceBean obj like in the code below:
The code
Single fragment reset
public void initFragment() {
notaBean = new NoteFragmentTO();
fromSpinnerListener = false;
}
public void resetFragment() {
initFragment();
NoteFragment.retainInstanceState = false;
}
This is done with the following code from the parent Activity:
Fragment reset from parent
private void resetAfterSaving() {
mIndicator.setCurrentItem(POSITION_F*****);
f*****Info.resetFragment();
mIndicator.setCurrentItem(POSITION_NOTE);
noteInfo.resetFragment();
mIndicator.setCurrentItem(POSITION_M*****);
m*****Info.resetFragment();
mIndicator.setCurrentItem(POSITION_V*****);
v*****.resetFragment();
}
AfterViews method:
#AfterViews
public void afterView() {
if (mSavedInstanceState != null) {
restoreState(mSavedInstanceState);
}
NoteFragment.retainInstanceState = true;
// Inits the adapters
noteAdapter = new NoteArrayAdapter(this, noteDefaultList);
sp_viol_nota_default.setAdapter(noteAdapter);
//sp_viol_nota_default.seton
et_viol_nota.setOnFocusChangeListener(new OnFocusChangeListener() {
#Override
public void onFocusChange(View v, boolean hasFocus) {
if (!hasFocus) {
String readText = et_viol_nota.getText().toString().trim();
notaBean.setNota(readText == "" ? null : readText);
}
}
});
}
OnSavedInstanceState
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelableArrayList(KEY_NOTE_D_LIST, (ArrayList<VlzAnagraficaNoteagente>) noteDefaultList);
outState.putInt(KEY_NOTE_D_POSITION, !NoteFragment.retainInstanceState ? 0 : notePosition);
notaBean.setNota(!NoteFragment.retainInstanceState ? "" : et_viol_nota.getText().toString().trim());
outState.putParcelable(NoteFragmentTO.INTENT_KEY, notaBean);
}
Why do I set every page before resetting them?
Because like explained here:
When pages are not visible to the user, their entire fragment may be destroyed, only keeping the saved state of that fragment.
and because until I don't select the relative fragment the #AfterViews method (that is everything processed right after OnCreateView of the fragment) is not executed.
This throws NullPointerException for a thousand of reason (Usually in the #AfterViews method You launch RestoreState method, initializes adapter, do UI stuff).
Setting the relative page before the reset let #AfterViews method be processed.
Before checking what would happened when rotating the device, all the fragment I need are correcly reset.
When rotating the device, the error comes out:
The views (mainly EditText) go back to their previous state BEFORE my reset.
What happens?
When switching between the page, at a certain point the page will be destroyed and OnSavedInstanceState is called everytime for each page.
I have already handled the OnSavedInstanceState (like above) that when the boolean is false saves the state like if it had just been created.
I found that until within AfterView method the EditText has its text set to blank (like I want) but going on with the debug the EditText goes back to its previous state, so at the end it will show the last text it had.
Question
How can I keep the manually set (in OnSavedInstanceState) EditText text after destroying/recreating a fragment?
I implemented my layout based on this tutorial: http://android-developers.blogspot.hu/2011/02/android-30-fragments-api.html
The differences are:
I have different fragments to show, based on the choice in the left
list
The "details fragments" (those that come to the right) have different options menus
My problem is that if I have already selected something from the left and then rotate the phone to portrait, the last optionsmenu is still there and is visible.
I think the problem comes from the last active "details" fragment is recreated after the orientation change. to test it I created these two methods:
#Override
public void onStart() {
super.onStart();
setHasOptionsMenu(true);
}
#Override
public void onStop() {
super.onStop();
setHasOptionsMenu(false);
}
And I'm showing the right fragment like this:
case R.id.prefs_medicines:
if (mDualPane) {
// Check what fragment is shown, replace if needed.
View prefsFrame = getActivity().findViewById(R.id.preferences);
if (prefsFrame != null) {
// Make new fragment to show this selection.
MedicineListF prefF = new MedicineListF();
// Execute a transaction, replacing any existing
// fragment with this one inside the frame.
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.replace(R.id.preferences, prefF);
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
ft.commit();
}
} else {
// Otherwise we need to launch a new activity to display
// the dialog fragment with selected text.
Intent intent = new Intent();
intent.setClass(getActivity(), MedicinePrefsActivity.class);
startActivity(intent);
}
break;
in one of my "details" fragment. when I debugged it, the onstart was called after the rotation.
The problem in pictures:
1: in landscape it's OK
Landscape mode http://img834.imageshack.us/img834/8918/error1d.png
2: in portrait: optionsmenu not needed
Portrait mode http://img860.imageshack.us/img860/8636/error2r.png
How can I get rid of the optionsmenu in portrait mode?
I had the same problem, and resolved it by setting setHasOptionsMenu(true) in the fragment only when savedInstanceState is null. If onCreate gets a bundle then the fragment is being restored in an orientation change to portrait, so don't display the menu.
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if(savedInstanceState == null) {
setHasOptionsMenu(true);
}
}