I have a ListView inside of a Fragment attached to a CursorAdapter. The Fragment has setRetainInstance(true)
. Under the Fragment's onCreate()
method, I instantiate the CursorAdapter (storing it in variable adapter
). I then call listView.setAdapter(adapter)
under my Fragment's onCreateView
method. The Cursor in the CursorAdapter is loaded by a CursorLoader. Inside my LoaderCallbacks' onLoadFinished()
, I call adapter.swapCursor(cursor)
.
In sum: Everything seems to be in place such that the ListView should not scroll to top when changing between tabs and back. But it still does! Could I be missing something?
Here's some code.
Fragment
public class Home extends Fragment{
...
private HomeFeedAdapter adapter; // HomeFeedAdapter extends CursorAdapter
private AutoQuery autoQuery; // AutoQuery extends LoaderCallbacks<Cursor>
... // (See inner class, at the end)
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
if(adapter == null)
adapter = new HomeFeedAdapter(getActivity(), null);
if(autoQuery == null)
autoQuery = new AutoQuery();
getLoaderManager().initLoader(LOADER_INITIAL, null, autoQuery);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// Layout
View v = inflater.inflate(R.layout.home, container, false);
l = (ListView) v.findViewById(R.id.listview);
l.setAdapter(adapter);
return v;
}
private class AutoQuery implements LoaderCallbacks<Cursor>{
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
...
return new CursorLoader(getActivity(), uri,
null, null, null, null);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
adapter.swapCursor(cursor);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
adapter.swapCursor(null);
}
}
}
Activity
public class MainActivity extends SherlockFragmentActivity {
...
private class TabsListener implements ActionBar.TabListener {
private Fragment fragment;
private String tag;
public TabsListener(Fragment fragment, String tag) {
this.fragment = fragment;
this.tag = tag;
}
@Override
public void onTabReselected(Tab tab, FragmentTransaction ft) {
// Do nothing
}
@Override
public void onTabSelected(Tab tab, FragmentTransaction ft) {
ft.replace(R.id.fragment_container, fragment, tag);
}
@Override
public void onTabUnselected(Tab tab, FragmentTransaction ft) {
ft.remove(fragment);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Layout
getSupportActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
getSupportActionBar().setHomeButtonEnabled(false);
getSupportActionBar().setDisplayShowTitleEnabled(false);
setContentView(R.layout.activity_main);
// Loads fragment
Fragment fHome, fActivity, fProfile;
if((fHome = getSupportFragmentManager().findFragmentByTag(HOME)) == null) fHome = new Home();
if((fActivity = getSupportFragmentManager().findFragmentByTag(ACTIVITY)) == null) fActivity = new FriendsList();
if((fProfile = getSupportFragmentManager().findFragmentByTag(PROFILE)) == null) fProfile = new Profile();
ActionBar.Tab tab;
tab = getSupportActionBar().newTab();
tab.setTabListener(new TabsListener(
fHome,
HOME
));
getSupportActionBar().addTab(tab, false);
tab = getSupportActionBar().newTab();
tab.setTabListener(new TabsListener(
fActivity,
ACTIVITY
));
getSupportActionBar().addTab(tab, false);
tab = getSupportActionBar().newTab();
tab.setTabListener(new TabsListener(
fProfile,
PROFILE
));
getSupportActionBar().addTab(tab, false);
...
}
}
With Greg Giacovelli's help in leading me in the right direction, I've found a solution to my problem.
I'll begin with a disclaimer that I don't quite understand how ListView positions are saved. My ListView instance is recreated every time that my Fragment's onCreateView()
is called. This happens when the screen rotates, for example. And yet, in the specific case of screen rotations, even though onCreateView()
is called and thus the ListView is reinstantiated, the ListView's state is nonetheless restored. So if the ListView is being recreated, something else must be telling it what its position previously was... and I don't know what that is. I think that it's attributable to the machinery of setRetainInstance(true)
.
But let's look at my main issue: Why did the ListView scroll to top between tab changes? As Greg suggested, it was because I was re-adding the Fragment with replace()
, and thus destroying my Fragment and re-creating every time the user flipped to another tab and back.
My solution was to check if the tab was already added; if so, then not add it; else, add it. Then, when the user clicks out of a tab, I simply detach the fragment (not remove it), and attach a new one. This way the unselected tab's Fragment is still alive, though detached.
// Tabs listener
private class TabsListener implements ActionBar.TabListener {
private Fragment fragment;
private String tag;
public TabsListener(Fragment fragment, String tag) {
this.fragment = fragment;
this.tag = tag;
}
@Override
public void onTabReselected(Tab tab, FragmentTransaction ft) {
if(fragment instanceof ScrollableToTop) ((ScrollableToTop) fragment).scrollToTop();
}
@Override
public void onTabSelected(Tab tab, FragmentTransaction ft) {
if(!fragment.isAdded()){
ft.add(R.id.fragment_container, fragment, tag);
}
ft.attach(fragment);
}
@Override
public void onTabUnselected(Tab tab, FragmentTransaction ft) {
ft.detach(fragment);
}
}