Android - Fragment inside a Fragment inside a ViewPager problems - android

In my application I have a View pager that rotates 4 Fragments. Some of those fragments have other fragments inside them. The first time Viewpager starts a Fragment it works fine, but if I rotate around and return to the previous fragment, the inner fragments fail to show ( and I have noticed that getActivity() in them returns null).
For example, the app starts, I move through viewpager and see all the main fragments and they appear fine, then I scroll back, every main fragment that has inner fragments does not show properly.
I use "FragmentStatePagerAdapter" for my ViewPager and the code is below (variables names are for example).
private class ContentPagerAdapter extends FragmentStatePagerAdapter {
public ContentPagerAdapter(FragmentManager fm) {
super(fm);
}
#Override
public Parcelable saveState() {
return null;
}
#Override
public Fragment getItem(int pos) {
Fragment fragment = null;
try {
if (pos == POS1) {
if (fragment1 == null) {
fragment1 = Fragment1.newInstance();
}
fragment = fragment1;
} else if (pos == POS2) {
if (fragment2 == null) {
fragment2 = Fragment2.newInstance(something);
}
fragment = fragment2;
} else if (pos == POS3) {
if ( fragment3 == null ) {
fragment3 = Fragment3.newInstance(something3);
}
fragment = fragment3;
} else { //default
if (fragment4 == null) {
fragment4 = Fragment4.newInstance(something4);
}
fragment = fragment4;
}
}catch (Exception e) {
Log.e(AppConstants.APP_TAG, "Cannot create fragment", e);
finish();
}
return fragment;
}
#Override
public int getCount() {
int count = 4;
return count;
}
#Override
public int getItemPosition(Object object) {
return POSITION_NONE;
}
}

When instantiating the PagerAdapter, use getChildFragmentManager() instead of getFragmentManager(). If more info needed, similiar problem was solved here ViewPager in TabFragment not loading second time

Related

Problems when reloading fragment in ViewPager

I have problem with fragments in ViewPager. In my app I have 4 tabs implemented with FragmentStatePagerAdapter.
Here is my SectionsPageAdapter.java
public class SectionsPageAdapter extends FragmentStatePagerAdapter {
private String fragment0 = "";
private Fragment fragAt0;
private FragmentManager manager;
private String[] tabNames;
Context context;
public SectionsPageAdapter(FragmentManager fm, Context ctx, String lastFrag) {
super(fm);
context = ctx;
tabNames = ctx.getResources().getStringArray(R.array.tabs_name);
this.manager = fm;
this.listener = new fragmentChangeListener();
fragment0 = lastFrag;
}
#Override
public Fragment getItem(int position) {
switch (position) {
case 0:
return ActivityTypeFragment.newInstance(0);
case 1:
return EventsFragment.newInstance();
case 2:
return CalendarFragment.newInstance();
case 3:
if(fragAt0 == null){
switch(fragment0){
case "Chart":
fragAt0 = ChartFragment.newInstance(listener);
break;
case "Hour":
fragAt0 = HourChartFragment.newInstance(listener);
break;
case "Daily":
fragAt0 = DailyChartFragment.newInstance(listener);
break;
default:
fragAt0 = ChartFragment.newInstance(listener);
}
}
return fragAt0;
default:
return ActivityTypeFragment.newInstance(0);
}
}
#Override
public int getCount() {
return 4;
}
#Nullable
#Override
public CharSequence getPageTitle(int position) {
return tabNames[position];
}
#Override
public int getItemPosition(#NonNull Object object) {
if (object instanceof ChartFragment && fragAt0 instanceof DailyChartFragment
|| object instanceof HourChartFragment && fragAt0 instanceof DailyChartFragment)
{
return POSITION_NONE;
}
else if (object instanceof DailyChartFragment && fragAt0 instanceof ChartFragment
|| object instanceof HourChartFragment && fragAt0 instanceof ChartFragment){
return POSITION_NONE;
}
else if (object instanceof HourChartFragment && fragAt0 instanceof ChartFragment)
{
return POSITION_NONE;
}
else if (object instanceof ChartFragment && fragAt0 instanceof HourChartFragment
|| object instanceof DailyChartFragment && fragAt0 instanceof HourChartFragment)
{
return POSITION_NONE;
}
return POSITION_NONE;
}
public interface nextFragmentListener {
void fragment0Changed(String newFragmentIdentification, FragmentManager fragmentManager);
}
private nextFragmentListener listener;
private final class fragmentChangeListener implements nextFragmentListener {
#Override
public void fragment0Changed(String fragment, FragmentManager fm) {
fragment0 = fragment;
manager.beginTransaction().remove(fragAt0).commitNow();
switch (fragment) {
case "Chart":
fragAt0 = ChartFragment.newInstance(listener);
break;
case "Daily":
fragAt0 = DailyChartFragment.newInstance(listener);
break;
case "Hour":
fragAt0 = HourChartFragment.newInstance(listener);
break;
}
SectionsPageAdapter.this.notifyDataSetChanged();
}
}
public String getFragmentName()
{
return fragment0;
}
}
So in last tab I have a fragment - ChartFragment, where I have Spinner with options to select another chart. And when user selects another chart, this another chart reloads in this tab by calling method fragment0Changed that I have in my SectionsPageAdapter
Class. I found this solution in https://stackoverflow.com/a/27304530/1573414.
Here is my function onItemSelected which I have in every fragment that I want to be appeared in Charts tab
#Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (view != null) {
String selectedMenu = ((TextView) view).getText().toString();
if (selectedMenu == getString(R.string.best_appealing_chart)) {
LoadBestAppealingChart();
} else if (selectedMenu == getString(R.string.hour_chart)) {
chartListener.fragment0Changed("Hour", getActivity().getSupportFragmentManager());
}
else
{
chartListener.fragment0Changed("Daily", getActivity().getSupportFragmentManager());
}
}
else {
LoadBestAppealingChart();
}
}
So, everything works fine until I rotate device, after that selecting another chart does not work. App throwing the exception:
java.lang.IllegalStateException: Fragment host has been destroyed
at android.support.v4.app.FragmentManagerImpl.ensureExecReady(FragmentManager.java:2183)
at android.support.v4.app.FragmentManagerImpl.execSingleAction(FragmentManager.java:2211)
at android.support.v4.app.BackStackRecord.commitNow(BackStackRecord.java:643).
I’ve read the article https://www.androiddesignpatterns.com/2013/08/fragment-transaction-commit-state-loss.html,but in my case I have nor asynchronus methods where I would replace a fragment, nor I call it in Activity lifecycle methods. I make a fragment transaction in a onSelectionChanged method of my spinner. What I tried: using commitNowAllowingStateLoss() do not make things better, it just didn’t throw an exception, fragments still do not reloading; passing getSupportFragmentManager() parameter to fragment0Changed method doesn’t help either, but doing this does not throw any exception even if i use simple commitNow() method. But what helps is when after rotation I choose first or second tab of my app then go to my fourth tab(Charts tab), then I can switch(reload) my fragments without any problem. Interesting thing that switching to third tab(Calendar tab) does not help either. After switching to third tab and returning to fourth tab switching fragments still do not reloaded.
I think that its a common mistake on Android to communicate with Fragment passing listener. Listener on Fragment must be implemented by the Activity or a child Fragment. So you have to make your Activity or Fragment implements NextFragmentListener... Second, never make a reference to a Fragment on ViewPager, you are leaking the old fragment0
public class SubAdapter extends FragmentStatePagerAdapter
{
/* just pass the behavior in super() call so every time resume method will call so that you can reload any thing you want
But you should use subclass extending */
private String tile_list[];
public SubAdapter (FragmentManager fm, String[] tile_list)
{
super(fm,FragmentStatePagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
this.tile_list = tile_list;
}
#Override
public Fragment getItem(int position)
{
Fragment fragment;
switch (position)
{
case 0:
fragment=new new_fragment();
break;
default:
fragment=new new_fragment();
}
return fragment;
}
#Override
public int getCount()
{
return tile_list.length;
}
#Nullable
#Override
public CharSequence getPageTitle(int position)
{
return tile_list[position];
}
}

Replacing fragment in ViewPager causes IllegalStateException

Following this example I'm trying to replace one fragement with another within a single tab of a ViewPager. I'm getting this exception:
java.lang.IllegalStateException: Can't change tag of fragment DailyWordFragment
class ViewPagerAdapter extends FragmentPagerAdapter {
private final FragmentManager fragmentManager;
Fragment fragment;
public ViewPagerAdapter(FragmentManager manager) {
super(manager);
fragmentManager = manager;
}
#Override
public Fragment getItem(int position) {
switch (position){
case 0:
if(fragment == null){
fragment = DailyWordFragment.newInstance();
}
return fragment;
case 1:
if(fragment == null){
fragment = WordListFragment.newInstance(new WordListFragment.OnWordItemAddListener() {
#Override
public void onWordItemAddListener() {
fragmentManager.beginTransaction().remove(fragment).commit();
fragment = AddWordFragment.newInstance();
notifyDataSetChanged();
}
});
}
return fragment;
default:
return null;
}
}
#Override
public int getItemPosition(Object object) {
if(object instanceof WordListFragment && fragment instanceof AddWordFragment)
return POSITION_NONE;
return POSITION_UNCHANGED;
}
#Override
public int getCount() {
return 2;
}
}
According to other posts this exception seems to be caused by adding the same fragment repeatedly, or getting miscounting the number of fragments but I can't see where either of these are occurring if at all? Clearly I'm missing something. I would appreciate any help. Thank you.
Have you tried before you initialize the Fragment to set it to null?
#Override
public Fragment getItem(int position) {
fragment = null
switch (position){
case 0:
if(fragment == null){
fragment = DailyWordFragment.newInstance();
}
return fragment;
case 1:
if(fragment == null){
fragment = WordListFragment.newInstance(new WordListFragment.OnWordItemAddListener() {
#Override
public void onWordItemAddListener() {
fragmentManager.beginTransaction().remove(fragment).commit();
fragment = AddWordFragment.newInstance();
notifyDataSetChanged();
}
});
}
return fragment;
default:
return null;
}
}
To SWAP fragments in one tab of a View Pager you have to do it in a different way:
You need a fragment that goes in the ViewPager that acts as a container. which is managed by the ViewPager. This Fragment is allways there in the tab, never swaps or changes.
Inside that Fragment you have 2 child fragments that you can swap using transactions with the ChildFragmentManager. But the ViewPager doesn't even know about their existance.

Retain fragment in viewpager after rotating

Edit: I'm terrible at explaining these things so here is a video
http://sendvid.com/mefqxd8f
Context:
The application consists of a single Activity with a ViewPager that holds 3 tabs. Tab1 uses an interface called PageFragmentListener to switch between 2 different fragments in the same Tab when a button is pressed. When the app first loads, Fragment1 is inflated in Tab1. After clicking the button, Fragment2 is inflated in Tab1. I'd like Fragment1 to remain in Tab1 permanently after this.
The issue:
If Tab3 is currently active (visible to the user) and then the device is rotated Tab1 is completely destroyed. When it is recreated, it inflates Tab1 with Fragment1 instead of restoring Fragment2. In the video you'll see that Tab1 is blue instead of purple.
Question:
Is there a way to restore Fragment2 to Tab1 after it is destroyed rather than inflating Fragment1? I'm assuming savedInstanceState can be used somehow.
class MyAdapter extends FragmentStatePagerAdapter {
static final String TAG = "MyAdapter";
private final FragmentManager mFragmentManager;
public BaseFragment mFragmentA;
public BaseFragment mFragmentB;
/**
* PageFragmentListener for switching fragmentA.
*/
public PageFragmentListener mListenerA = new PageFragmentListener() {
#Override
public void onSwitchToNextFragment(final String id) {
mFragmentManager.beginTransaction().remove(mFragmentA).commit();
if (mFragmentA instanceof FragmentA)
{
mFragmentA = FragmentA2.newInstance(mListenerA); // => switch fragment a2
}
else if (mFragmentA instanceof FragmentA2){
mFragmentA = FragmentA.newInstance(mListenerA);
}
notifyDataSetChanged(); // notify changes
}
};
/**
* PageFragmentListener for switching fragmentB.
*/
public PageFragmentListener mListenerB = new PageFragmentListener() {
#Override
public void onSwitchToNextFragment(final String id) {
mFragmentManager.beginTransaction().remove(mFragmentB).commit();
if (mFragmentB instanceof ItemListFragment){ // current fragment is List Fragment
Bundle arguments = new Bundle();
arguments.putString(Constants.ARG_ITEM_ID, id); // selected item id
mFragmentB = ItemOneDetailFragment.newInstance(mListenerB); // switch detail fragment
mFragmentB.setArguments(arguments);
}else if (mFragmentB instanceof ItemOneDetailFragment) { // DetailFragment
mFragmentB = ItemListFragment.newInstance(mListenerB); // => switch list fragment
}
notifyDataSetChanged(); // notify changes
}
};
public MyAdapter(FragmentManager fm) {
super(fm);
Log.d(TAG, "MyAdapterConstructor");
mFragmentManager = fm;
List<Fragment> fragments = fm.getFragments();
if(fragments != null){
for(Fragment f : fragments){
if (f instanceof FragmentA || f instanceof FragmentA2){
mFragmentA = (BaseFragment) f;
}
if(f instanceof ItemListFragment || f instanceof ItemOneDetailFragment){
mFragmentB = (BaseFragment) f;
}
}
}
}
#Override
public Fragment getItem(int position) {
Log.d(TAG, "getItem()");
if (position == 0){ // Tab-1
if (!(mFragmentA instanceof FragmentA2)) {
mFragmentA = FragmentA.newInstance(mListenerA);
}
return mFragmentA;
}
if (position == 1) // Tab-2
return FragmentB.newInstance();
if (position == 2) { // Tab-3
if (!(mFragmentB instanceof ItemOneDetailFragment)){
mFragmentB = ItemListFragment.newInstance(mListenerB);
}
return mFragmentB;
}
else{
return null;
}
}
/**
* To set tab title.
* #param position the currently visible ViewPager fragment
* #return the title of the fragment
*/
#Override
public CharSequence getPageTitle(int position) {
Log.d(TAG, "getPageTitle()");
if (position == 0) { // Tab-1
return "Tab 1";
}
if (position == 1) { // Tab-2
return "Tab 2";
}
if (position == 2) { //Tab-3
return "Tab 3";
}
return null;
}
#Override
public int getCount() { // Count of Tabs
return 3;
}
#Override
public int getItemPosition(Object object) {
Log.i("Adapter", "ItemPosition>>>" + object.toString());
if (object instanceof ItemListFragment && mFragmentB instanceof ItemOneDetailFragment) { // fragment changed
return POSITION_NONE;
}
if (object instanceof ItemOneDetailFragment && mFragmentB instanceof ItemListFragment) { // fragment changed
return POSITION_NONE;
}
if (object instanceof FragmentA && mFragmentA instanceof FragmentA2) {
return POSITION_NONE;
}
if (object instanceof FragmentA2 && mFragmentA instanceof FragmentA){
return POSITION_NONE;
}
return POSITION_UNCHANGED;
}
}
Simple solution to retain the state of the fragment while rotate is by calling
setRetainInstance(true);
In order to add fragment into stack try like this
transaction.addToBackStack(fragment.getClass.getName());
to get previous fragment (may be on while calling of onBackPressed()) try this
getFragmentManager.popBackStack();
My (poor) solution was to create a Static Class Boolean of MainActivity called hasSavedInstance. I set this to true in onCreate() of MainActivity as follows:
if (savedInstanceState == null){
Log.d(TAG, "onCreate() FIRST");
}
else{
Log.d(TAG, "onCreate() ADDITIONAL");
hasSavedInstance = true;
}
I then use this value to determine which Fragment to return in MyAdapter:
#Override
public Fragment getItem(int position) {
Log.d(TAG, "getItem()");
if (position == 0){ // Tab-1
Log.d(TAG, "hasSavedInstance="+MainActivity.hasSavedInstance);
if (mFragmentA == null && MainActivity.hasSavedInstance){
mFragmentA = FragmentA2.newInstance(mListenerA);
}
else if (!(mFragmentA instanceof FragmentA2)) {
mFragmentA = FragmentA.newInstance(mListenerA);
}
return mFragmentA;
}
This seems to work but I feel like setting this flag is a very inelegant way of working around this problem.

FragmentPagerAdapter is not removing items (Fragments) correctly

I have implemented the FragmentPagerAdapter and and using a List<Fragment> to hold all fragments for my ViewPager to display. On addItem() I simply add an instantiated Fragment and then call notifyDataSetChanged(). I am not sure if this is necessary or not.
My problem simply...
start with fragment 1
[Fragment 1]
add new Fragment 2
[Fragment 1] [Fragment 2]
remove Fragment 2
[Fragment 1]
add new Fragment 3
[Fragment 1] [Fragment 2]
When adding new fragments everything seems great. Once I remove a fragment and then add a newly instantiated fragment the old fragment is still displayed. When i go a .getClass.getName() it is giving me Fragment 3's name however I still see Fragment 2.
I believe this might be an issue with instantiateItem() or such but I thought the adapter was to handle that for us. Any suggestions would be great.
adapter code...
public class MyPagerAdapter extends FragmentPagerAdapter {
public final ArrayList<Fragment> screens2 = new ArrayList<Fragment>();
private Context context;
public MyPagerAdapter(FragmentManager fm, Context context) {
super(fm);
this.context = context;
}
public void removeType(String name){
for(Fragment f: screens2){
if(f.getClass().getName().contains(name)){ screens2.remove(f); return; }
}
this.notifyDataSetChanged();
}
public boolean addSt(String tag, Class<?> clss, Bundle args){
if(clss==null) return false;
if(!clss.getName().contains("St")) return false;
if(!args.containsKey("cId")) return false;
boolean has = false;
boolean hasAlready = false;
for(Fragment tab: screens2){
if(tab.getClass().getName().contains("St")){
has = true;
if(tab.getArguments().containsKey("cId"))
if(tab.getArguments().getLong("cId") == args.getLong("cId")){
hasAlready = true;
}
if(!hasAlready){
// exists but is different so replace
screens2.remove(tab);
this.addScreen(tag, clss, args, C.PAGE_ST);
// if returned true then it notifies dataset
return true;
}
}
hasAlready = false;
}
if(!has){
// no st yet exist in adapter
this.addScreen(tag, clss, args, C.PAGE_ST);
return true;
}
return false;
}
public boolean removeCCreate(){
this.removeType("Create");
return false;
}
#Override
public int getItemPosition(Object object) {
return POSITION_NONE; //To make notifyDataSetChanged() do something
}
public void addCCreate(){
this.removeCCreate();
Log.w("addding c", " ");
this.addScreen("Create C", CreateCFragment.class, null, C.PAGE_CREATE_C);
}
public void addScreen(String tag, Class<?> clss, Bundle args, int type){
if(clss!=null){
screens2.add(Fragment.instantiate(context, clss.getName(), args));
}
}
#Override
public int getCount() {
return screens2.size();
}
#Override
public Fragment getItem(int position) {
return screens2.get(position);
}
}
I realize the code uses some "ghetto" means of determining the fragment type however I wrote this code strictly for testing functionality. Any help or ideas would be great as it seems that not many people ventured into the world of FragmentPagerAdapters.
I got same problem,and my solution was overring the method "destroyItem" as following.
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
FragmentManager manager = ((Fragment)object).getFragmentManager();
FragmentTransaction trans = manager.beginTransaction();
trans.remove((Fragment)object);
trans.commit();
}
It's work for me,does anybody have another solutions?
Updated:
I found those code made Fragment removed unnecessary,so I added a condition to avoid it.
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
if (position >= getCount()) {
FragmentManager manager = ((Fragment) object).getFragmentManager();
FragmentTransaction trans = manager.beginTransaction();
trans.remove((Fragment) object);
trans.commit();
}
}
Updated this post and included my solution (if someone can improve let me know)
Ok i've now solved my problem in a hackish way, but yeah it's working ;). If someone can improve my solution please let me know. For my new solution i now use a CustomFragmentStatePagerAdapter but it doesn't save the state like it should and stores all the Fragments in a list. This can cause a memory problem if the user has more than 50 fragments, like the normal FragmentPagerAdapter does. It would be great if someone can add the State-thing back to my solution without removing my fixes. Thanks.
So here's my CustomFragmentStatePagerAdapter.java
package com.tundem.webLab.Adapter;
import java.util.ArrayList;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.view.PagerAdapter;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
public abstract class CustomFragmentStatePagerAdapter extends PagerAdapter {
private static final String TAG = "FragmentStatePagerAdapter";
private static final boolean DEBUG = false;
private final FragmentManager mFragmentManager;
private FragmentTransaction mCurTransaction = null;
public ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();
public ArrayList<Fragment> mFragments = new ArrayList<Fragment>();
private Fragment mCurrentPrimaryItem = null;
public CustomFragmentStatePagerAdapter(FragmentManager fm) {
mFragmentManager = fm;
}
/**
* Return the Fragment associated with a specified position.
*/
public abstract Fragment getItem(int position);
#Override
public void startUpdate(ViewGroup container) {}
#Override
public Object instantiateItem(ViewGroup container, int position) {
// If we already have this item instantiated, there is nothing
// to do. This can happen when we are restoring the entire pager
// from its saved state, where the fragment manager has already
// taken care of restoring the fragments we previously had instantiated.
// DONE Remove of the add process of the old stuff
/* if (mFragments.size() > position) { Fragment f = mFragments.get(position); if (f != null) { return f; } } */
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
Fragment fragment = getItem(position);
if (DEBUG)
Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
if (mSavedState.size() > position) {
Fragment.SavedState fss = mSavedState.get(position);
if (fss != null) {
try // DONE: Try Catch
{
fragment.setInitialSavedState(fss);
} catch (Exception ex) {
// Schon aktiv (kA was das heißt xD)
}
}
}
while (mFragments.size() <= position) {
mFragments.add(null);
}
fragment.setMenuVisibility(false);
mFragments.set(position, fragment);
mCurTransaction.add(container.getId(), fragment);
return fragment;
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
Fragment fragment = (Fragment) object;
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
mCurTransaction.remove(fragment);
/*if (mCurTransaction == null) { mCurTransaction = mFragmentManager.beginTransaction(); } if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object + " v=" + ((Fragment)
* object).getView()); while (mSavedState.size() <= position) { mSavedState.add(null); } mSavedState.set(position, mFragmentManager.saveFragmentInstanceState(fragment));
* mFragments.set(position, null); mCurTransaction.remove(fragment); */
}
#Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
Fragment fragment = (Fragment) object;
if (fragment != mCurrentPrimaryItem) {
if (mCurrentPrimaryItem != null) {
mCurrentPrimaryItem.setMenuVisibility(false);
}
if (fragment != null) {
fragment.setMenuVisibility(true);
}
mCurrentPrimaryItem = fragment;
}
}
#Override
public void finishUpdate(ViewGroup container) {
if (mCurTransaction != null) {
mCurTransaction.commitAllowingStateLoss();
mCurTransaction = null;
mFragmentManager.executePendingTransactions();
}
}
#Override
public boolean isViewFromObject(View view, Object object) {
return ((Fragment) object).getView() == view;
}
#Override
public Parcelable saveState() {
Bundle state = null;
if (mSavedState.size() > 0) {
state = new Bundle();
Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];
mSavedState.toArray(fss);
state.putParcelableArray("states", fss);
}
for (int i = 0; i < mFragments.size(); i++) {
Fragment f = mFragments.get(i);
if (f != null) {
if (state == null) {
state = new Bundle();
}
String key = "f" + i;
mFragmentManager.putFragment(state, key, f);
}
}
return state;
}
#Override
public void restoreState(Parcelable state, ClassLoader loader) {
if (state != null) {
Bundle bundle = (Bundle) state;
bundle.setClassLoader(loader);
Parcelable[] fss = bundle.getParcelableArray("states");
mSavedState.clear();
mFragments.clear();
if (fss != null) {
for (int i = 0; i < fss.length; i++) {
mSavedState.add((Fragment.SavedState) fss[i]);
}
}
Iterable<String> keys = bundle.keySet();
for (String key : keys) {
if (key.startsWith("f")) {
int index = Integer.parseInt(key.substring(1));
Fragment f = mFragmentManager.getFragment(bundle, key);
if (f != null) {
while (mFragments.size() <= index) {
mFragments.add(null);
}
f.setMenuVisibility(false);
mFragments.set(index, f);
} else {
Log.w(TAG, "Bad fragment at key " + key);
}
}
}
}
}
}
Here's my normal FragmentAdapter.java
package com.tundem.webLab.Adapter;
import java.util.LinkedList;
import java.util.List;
import android.support.v4.app.FragmentManager;
import com.tundem.webLab.fragments.BaseFragment;
import com.viewpagerindicator.TitleProvider;
public class FragmentAdapter extends CustomFragmentStatePagerAdapter implements TitleProvider {
public List<BaseFragment> fragments = new LinkedList<BaseFragment>();
private int actPage;
public FragmentAdapter(FragmentManager fm) {
super(fm);
}
public void setActPage(int actPage) {
this.actPage = actPage;
}
public void addItem(BaseFragment fragment) {
// TODO if exists don't open / change to that tab
fragments.add(fragment);
}
public BaseFragment getActFragment() {
return getItem(getActPage());
}
public int getActPage() {
return actPage;
}
#Override
public BaseFragment getItem(int position) {
if (position < getCount()) {
return fragments.get(position);
} else
return null;
}
#Override
public int getCount() {
return fragments.size();
}
#Override
public String getTitle(int position) {
return fragments.get(position).getTitle();
}
#Override
public int getItemPosition(Object object) {
return POSITION_NONE;
}
}
And this is the way i delete a Fragment. (I know it's a bit more than only .remove() ). Be free to improve my solution, you can also add this code somewhere in the adapter so yeah. It's up to the user who tries to implement this. I use this in my TabHelper.java (A class which handles all tab operations like delete, add, ...)
int act = Cfg.mPager.getCurrentItem();
Cfg.mPager.removeAllViews();
Cfg.mAdapter.mFragments.remove(act);
try {
Cfg.mAdapter.mSavedState.remove(act);
} catch (Exception ex) {/* Already removed */}
try {
Cfg.mAdapter.fragments.remove(act);
} catch (Exception ex) {/* Already removed */}
Cfg.mAdapter.notifyDataSetChanged();
Cfg.mIndicator.notifyDataSetChanged();
Description of the Cfg. thing. I save the reference to those objects in a cfg, class so i can always use them without the need of a special Factory.java ...
Yeah. i hope i was able to help. Feel free to improve this, but let me know so i can improve my code too.
Thanks.
If i missed any code let me know.
My old answer also works but only if you have different Fragments. FileFragment, WebFragment, ... Not if you use one of those fragmenttypes twice.
I got it pseudo working for now. It's a really dirty solution and i'm still searching for a better one. Please help.
I changed the code, where i delete a tab to this one:
public static void deleteActTab()
{
//We set this on the indicator, NOT the pager
int act = Cfg.mPager.getCurrentItem();
Cfg.mAdapter.removeItem(act);
List<BaseFragment> frags = new LinkedList<BaseFragment>();
frags = Cfg.mAdapter.fragments;
Cfg.mPager = (ViewPager)Cfg.act.findViewById(R.id.pager);
Cfg.mPager.setAdapter(Cfg.mAdapter);
Cfg.mIndicator.setViewPager(Cfg.mPager);
Cfg.mAdapter.fragments = frags;
if(act > 0)
{
Cfg.mPager.setCurrentItem(act-1);
Cfg.mIndicator.setCurrentItem(act-1);
}
Cfg.mIndicator.notifyDataSetChanged();
}
If someone can improve this code let me know. If someone can tell us the real answer for that problem. please add it here. There are many many people who experience this issue. I added a reputation of 50 for the one who solve it. I can also give a donation for the one who solve it.
Thanks
Maybe this answer helps you.
Use FragmentStatePagerAdapter instead of FragmentPagerAdapter.
Because FragmentPagerAdapter does not destory the views. For more information read this answer.
Taking "the best of both worlds" (I mean the answers by #Tericky Shih and #mikepenz) we have it short and simple:
public class MyPagerAdapter extends FragmentPagerAdapter {
public ArrayList<Fragment> fragments = new ArrayList<Fragment>();
...
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
super.destroyItem(container, position, object);
if (position >= getCount()) fm.beginTransaction().remove((Fragment) object).commit();
}
#Override
public int getItemPosition(Object object) {
if (fragments.contains(object)) return fragments.indexOf(object);
else return POSITION_NONE;
}
}
The main difference is that if some fragment is not changed you don't have to destroy its view and don't have to return POSITION_NONE for it. At the same time, I've encountered a situation when ViewPager was holding a reference to an item which was already destroyed, therefore the check if (fragments.contains(object)) helps to determine if that item is not needed anymore.
I had a situation similar to yours. I recently needed to add and remove Fragments from the ViewPager. In the first mode, I have Fragments 0, 1, and 2 and in the second mode I have Fragments 0 and 3. I want Fragment 0 to be same for both modes and hold over information.
All I needed to do was override FragmentPagerAdapter.getItemId to make sure that I returned a unique number for each different Fragment - the default is to return "position". I also had to set the adapter in the ViewPager again - a new instance will work but I set it back to the same instance. Setting the adapter results in the ViewPager removing all the views and trying to add them again.
However, the trick is that the adapter only calls getItem when it wants to instantiate the Fragment - not every time it shows it. This is because they are cached and looks them up by the "position" returned by getItemId.
Imagine you have three Fragments (0, 1 and 2) and you want to remove "1".
If you return "position" for getItemId then removing Fragment 1 will not work because when you try to show Fragment 2 after deleting Fragment 1 the pager/adapter will think it's already got the Fragment for that "position" and will continue to display Fragment 1.
FYI: I tried notifyDataSetChanged instead of setting the adapter but it didn't work for me.
First, the getItemId override example and what I did for my getItem:
public class SectionsPagerAdapter extends FragmentPagerAdapter
{
...
#Override
public long getItemId(int position)
{
// Mode 1 uses Fragments 0, 1 and 2. Mode 2 uses Fragments 0 and 3
if ( mode == 2 && position == 1 )
return 3;
return position;
}
#Override
public Fragment getItem(int position)
{
if ( mode == 1 )
{
switch (position)
{
case 0:
return <<fragment 0>>;
case 1:
return <<fragment 1>>;
case 2:
return <<fragment 2>>;
}
}
else // Mode 2
{
switch (position)
{
case 0:
return <<fragment 0>>;
case 1:
return <<fragment 3>>;
}
}
return null;
}
}
Now the change of mode:
private void modeChanged(int newMode)
{
if ( newMode == mode )
return;
mode = newMode;
// Calling mSectionsPagerAdapter.notifyDataSetChanged() is not enough here
mViewPager.setAdapter(mSectionsPagerAdapter);
}
Didn't work out for me. My solution was put FragmentStatePagerAdapter.java in my project, renamed to FragmentStatePagerAdapter2.java.
In destroyItem(), I changed a little based on error logs. From
// mFragments.set(position, null);
to
if (position < mFragments.size())mFragments.remove(position);
Maybe you don't have the same problem, just check the log.Hope this helps someone!
After a lot of trying, i got it to work so that it removes or attaches a third fragment at the end position correctly.
Object fragments[] = new Object[3];
int mItems = 2;
MyAdapter mAdapter;
ViewPager mPager;
public void addFragment(boolean bool) {
mAdapter.startUpdate(mPager);
if (!bool) {
mAdapter.destroyItem(mPager, 2, fragments[2]);
mItems = 2;
fNach = false;
}
else if (bool && !fNach){
mItems = 3;
mAdapter.instantiateItem(mPager,2);
fNach = true;
}
mAdapter.finishUpdate(mPager);
mAdapter.notifyDataSetChanged();
}
public class MyAdapter extends FragmentPagerAdapter {
MyAdapter(FragmentManager fm) {
super(fm);
}
#Override
public int getCount() {
return mItems;
}
#Override
public CharSequence getPageTitle(int position) {
... (code for the PagerTitleStrip)
}
#Override
public Fragment getItem(int position) {
Fragment f = null;
switch (position) {
case 0:
f = new Fragment1();
break;
case 1:
f = new Fragment2();
break;
case 2:
f = new Fragment3();
break;
}
return f;
}
#Override
public Object instantiateItem(ViewGroup container, int position) {
Object o = super.instantiateItem(container,position);
fragments[position] = o;
return o;
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
super.destroyItem(container, position, object);
System.out.println("Destroy item " + position);
if (position >= getCount()) {
FragmentManager manager = ((Fragment) object).getFragmentManager();
FragmentTransaction ft = manager.beginTransaction();
ft.remove((Fragment) object);
ft.commit();
}
}
}
Some clarification: to get the object reference to call destroyItem, i stored the objects returned from instantiateItem in an Array. When you are adding or removing fragments, you have to announce it with startUpdate, finishUpdate and notifyDataSetChanged. The number of items have to be changed manually, for adding you increase it and instantiate it, getItem creates it then. For deletion, you call destroyItem, and in this code, it is essential the position >= mItems, because destroyItem is also called if a fragment goes out of the cache. You don't want to delete it then. The only thing which doesn't work is the swiping animation. After removing the last page, the "cannot swipe left" animation is not restored correctly on the new last page. If you swipe it, a blank page is shown and it bounces back.
The real problem is that FragmentPagerAdapter uses the position of the fragment in your list as ID. So if you add a new List or simply remove items "instantiateItem" item will find different fragments for new items in list...
#Override
public Object instantiateItem(ViewGroup container, int position) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
final long itemId = getItemId(position);
// Do we already have this fragment?
String name = makeFragmentName(container.getId(), itemId);
Fragment fragment = mFragmentManager.findFragmentByTag(name);
if (fragment != null) {
if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
mCurTransaction.attach(fragment);
} else {
fragment = getItem(position);
if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
mCurTransaction.add(container.getId(), fragment,
makeFragmentName(container.getId(), itemId));
}
if (fragment != mCurrentPrimaryItem) {
fragment.setMenuVisibility(false);
fragment.setUserVisibleHint(false);
}
return fragment;
}
and
private static String makeFragmentName(int viewId, long id) {
return "android:switcher:" + viewId + ":" + id;
}
and
* Return a unique identifier for the item at the given position.
* <p>
* <p>The default implementation returns the given position.
* Subclasses should override this method if the positions of items can change.</p>
*
* #param position Position within this adapter
* #return Unique identifier for the item at position
*/
public long getItemId(int position) {
return position;
}
I have been having this same issue until I dawned on me, I was creating my PagerView from within another Fragment and not the main activity.
My solution was to pass the ChildFragment Manager into the constructor of the Fragment(State)PagerAdapter and not the Fragment Manager of the parent Fragment.
Using the ChildFragmentManager all the Fragments created by the ViewPagerAdapter are cleaned up automatically when the parent Fragment is destroyed.

Replace Fragment inside a ViewPager

I'm trying to use Fragment with a ViewPager using the FragmentPagerAdapter.
What I'm looking for to achieve is to replace a fragment, positioned on the first page of the ViewPager, with another one.
The pager is composed of two pages. The first one is the FirstPagerFragment, the second one is the SecondPagerFragment. Clicking on a button of the first page. I'd like to replace the FirstPagerFragment with the NextFragment.
There is my code below.
public class FragmentPagerActivity extends FragmentActivity {
static final int NUM_ITEMS = 2;
MyAdapter mAdapter;
ViewPager mPager;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.fragment_pager);
mAdapter = new MyAdapter(getSupportFragmentManager());
mPager = (ViewPager) findViewById(R.id.pager);
mPager.setAdapter(mAdapter);
}
/**
* Pager Adapter
*/
public static class MyAdapter extends FragmentPagerAdapter {
public MyAdapter(FragmentManager fm) {
super(fm);
}
#Override
public int getCount() {
return NUM_ITEMS;
}
#Override
public Fragment getItem(int position) {
if(position == 0) {
return FirstPageFragment.newInstance();
} else {
return SecondPageFragment.newInstance();
}
}
}
/**
* Second Page FRAGMENT
*/
public static class SecondPageFragment extends Fragment {
public static SecondPageFragment newInstance() {
SecondPageFragment f = new SecondPageFragment();
return f;
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
//Log.d("DEBUG", "onCreateView");
return inflater.inflate(R.layout.second, container, false);
}
}
/**
* FIRST PAGE FRAGMENT
*/
public static class FirstPageFragment extends Fragment {
Button button;
public static FirstPageFragment newInstance() {
FirstPageFragment f = new FirstPageFragment();
return f;
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
//Log.d("DEBUG", "onCreateView");
View root = inflater.inflate(R.layout.first, container, false);
button = (Button) root.findViewById(R.id.button);
button.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
FragmentTransaction trans = getFragmentManager().beginTransaction();
trans.replace(R.id.first_fragment_root_id, NextFragment.newInstance());
trans.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
trans.addToBackStack(null);
trans.commit();
}
});
return root;
}
/**
* Next Page FRAGMENT in the First Page
*/
public static class NextFragment extends Fragment {
public static NextFragment newInstance() {
NextFragment f = new NextFragment();
return f;
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
//Log.d("DEBUG", "onCreateView");
return inflater.inflate(R.layout.next, container, false);
}
}
}
...and here the xml files
fragment_pager.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:padding="4dip"
android:gravity="center_horizontal"
android:layout_width="match_parent" android:layout_height="match_parent">
<android.support.v4.view.ViewPager
android:id="#+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1">
</android.support.v4.view.ViewPager>
</LinearLayout>
first.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/first_fragment_root_id"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button android:id="#+id/button"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:text="to next"/>
</LinearLayout>
Now the problem... which ID should I use in
trans.replace(R.id.first_fragment_root_id, NextFragment.newInstance());
?
If I use R.id.first_fragment_root_id, the replacement works, but Hierarchy Viewer shows a strange behavior, as below.
At the beginning the situation is
after the replacement the situation is
As you can see there is something wrong, I expect to find the same state shown as in the first picture after I replace the fragment.
There is another solution that does not need modifying source code of ViewPager and FragmentStatePagerAdapter, and it works with the FragmentPagerAdapter base class used by the author.
I'd like to start by answering the author's question about which ID he should use; it is ID of the container, i.e. ID of the view pager itself. However, as you probably noticed yourself, using that ID in your code causes nothing to happen. I will explain why:
First of all, to make ViewPager repopulate the pages, you need to call notifyDataSetChanged() that resides in the base class of your adapter.
Second, ViewPager uses the getItemPosition() abstract method to check which pages should be destroyed and which should be kept. The default implementation of this function always returns POSITION_UNCHANGED, which causes ViewPager to keep all current pages, and consequently not attaching your new page. Thus, to make fragment replacement work, getItemPosition() needs to be overridden in your adapter and must return POSITION_NONE when called with an old, to be hidden, fragment as argument.
This also means that your adapter always needs to be aware of which fragment that should be displayed in position 0, FirstPageFragment or NextFragment. One way of doing this is supplying a listener when creating FirstPageFragment, which will be called when it is time to switch fragments. I think this is a good thing though, to let your fragment adapter handle all fragment switches and calls to ViewPager and FragmentManager.
Third, FragmentPagerAdapter caches the used fragments by a name which is derived from the position, so if there was a fragment at position 0, it will not be replaced even though the class is new. There are two solutions, but the simplest is to use the remove() function of FragmentTransaction, which will remove its tag as well.
That was a lot of text, here is code that should work in your case:
public class MyAdapter extends FragmentPagerAdapter
{
static final int NUM_ITEMS = 2;
private final FragmentManager mFragmentManager;
private Fragment mFragmentAtPos0;
public MyAdapter(FragmentManager fm)
{
super(fm);
mFragmentManager = fm;
}
#Override
public Fragment getItem(int position)
{
if (position == 0)
{
if (mFragmentAtPos0 == null)
{
mFragmentAtPos0 = FirstPageFragment.newInstance(new FirstPageFragmentListener()
{
public void onSwitchToNextFragment()
{
mFragmentManager.beginTransaction().remove(mFragmentAtPos0).commit();
mFragmentAtPos0 = NextFragment.newInstance();
notifyDataSetChanged();
}
});
}
return mFragmentAtPos0;
}
else
return SecondPageFragment.newInstance();
}
#Override
public int getCount()
{
return NUM_ITEMS;
}
#Override
public int getItemPosition(Object object)
{
if (object instanceof FirstPageFragment && mFragmentAtPos0 instanceof NextFragment)
return POSITION_NONE;
return POSITION_UNCHANGED;
}
}
public interface FirstPageFragmentListener
{
void onSwitchToNextFragment();
}
As of November 13th 2012, repacing fragments in a ViewPager seems to have become a lot easier. Google released Android 4.2 with support for nested fragments, and it's also supported in the new Android Support Library v11 so this will work all the way back to 1.6
It's very similiar to the normal way of replacing a fragment except you use getChildFragmentManager. It seems to work except the nested fragment backstack isn't popped when the user clicks the back button. As per the solution in that linked question, you need to manually call the popBackStackImmediate() on the child manager of the fragment. So you need to override onBackPressed() of the ViewPager activity where you'll get the current fragment of the ViewPager and call getChildFragmentManager().popBackStackImmediate() on it.
Getting the Fragment currently being displayed is a bit hacky as well, I used this dirty "android:switcher:VIEWPAGER_ID:INDEX" solution but you can also keep track of all fragments of the ViewPager yourself as explained in the second solution on this page.
So here's my code for a ViewPager with 4 ListViews with a detail view shown in the ViewPager when the user clicks a row, and with the back button working. I tried to include just the relevant code for the sake of brevity so leave a comment if you want the full app uploaded to GitHub.
HomeActivity.java
public class HomeActivity extends SherlockFragmentActivity {
FragmentAdapter mAdapter;
ViewPager mPager;
TabPageIndicator mIndicator;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mAdapter = new FragmentAdapter(getSupportFragmentManager());
mPager = (ViewPager)findViewById(R.id.pager);
mPager.setAdapter(mAdapter);
mIndicator = (TabPageIndicator)findViewById(R.id.indicator);
mIndicator.setViewPager(mPager);
}
// This the important bit to make sure the back button works when you're nesting fragments. Very hacky, all it takes is some Google engineer to change that ViewPager view tag to break this in a future Android update.
#Override
public void onBackPressed() {
Fragment fragment = (Fragment) getSupportFragmentManager().findFragmentByTag("android:switcher:" + R.id.pager + ":"+mPager.getCurrentItem());
if (fragment != null) // could be null if not instantiated yet
{
if (fragment.getView() != null) {
// Pop the backstack on the ChildManager if there is any. If not, close this activity as normal.
if (!fragment.getChildFragmentManager().popBackStackImmediate()) {
finish();
}
}
}
}
class FragmentAdapter extends FragmentPagerAdapter {
public FragmentAdapter(FragmentManager fm) {
super(fm);
}
#Override
public Fragment getItem(int position) {
switch (position) {
case 0:
return ListProductsFragment.newInstance();
case 1:
return ListActiveSubstancesFragment.newInstance();
case 2:
return ListProductFunctionsFragment.newInstance();
case 3:
return ListCropsFragment.newInstance();
default:
return null;
}
}
#Override
public int getCount() {
return 4;
}
}
}
ListProductsFragment.java
public class ListProductsFragment extends SherlockFragment {
private ListView list;
public static ListProductsFragment newInstance() {
ListProductsFragment f = new ListProductsFragment();
return f;
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View V = inflater.inflate(R.layout.list, container, false);
list = (ListView)V.findViewById(android.R.id.list);
list.setOnItemClickListener(new OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
// This is important bit
Fragment productDetailFragment = FragmentProductDetail.newInstance();
FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
transaction.addToBackStack(null);
transaction.replace(R.id.products_list_linear, productDetailFragment).commit();
}
});
return V;
}
}
Based on #wize 's answer, which I found helpful and elegant, I could achieve what I wanted partially, cause I wanted the cability to go back to the first Fragment once replaced. I achieved it bit modifying a bit his code.
This would be the FragmentPagerAdapter:
public static class MyAdapter extends FragmentPagerAdapter {
private final class CalendarPageListener implements
CalendarPageFragmentListener {
public void onSwitchToNextFragment() {
mFragmentManager.beginTransaction().remove(mFragmentAtPos0)
.commit();
if (mFragmentAtPos0 instanceof FirstFragment){
mFragmentAtPos0 = NextFragment.newInstance(listener);
}else{ // Instance of NextFragment
mFragmentAtPos0 = FirstFragment.newInstance(listener);
}
notifyDataSetChanged();
}
}
CalendarPageListener listener = new CalendarPageListener();;
private Fragment mFragmentAtPos0;
private FragmentManager mFragmentManager;
public MyAdapter(FragmentManager fm) {
super(fm);
mFragmentManager = fm;
}
#Override
public int getCount() {
return NUM_ITEMS;
}
#Override
public int getItemPosition(Object object) {
if (object instanceof FirstFragment && mFragmentAtPos0 instanceof NextFragment)
return POSITION_NONE;
if (object instanceof NextFragment && mFragmentAtPos0 instanceof FirstFragment)
return POSITION_NONE;
return POSITION_UNCHANGED;
}
#Override
public Fragment getItem(int position) {
if (position == 0)
return Portada.newInstance();
if (position == 1) { // Position where you want to replace fragments
if (mFragmentAtPos0 == null) {
mFragmentAtPos0 = FirstFragment.newInstance(listener);
}
return mFragmentAtPos0;
}
if (position == 2)
return Clasificacion.newInstance();
if (position == 3)
return Informacion.newInstance();
return null;
}
}
public interface CalendarPageFragmentListener {
void onSwitchToNextFragment();
}
To perfom the replacement, simply define a static field, of the type CalendarPageFragmentListener and initialized through the newInstance methods of the corresponding fragments and call FirstFragment.pageListener.onSwitchToNextFragment() or NextFragment.pageListener.onSwitchToNextFragment() respictevely.
I have implemented a solution for:
Dynamic fragment replacement inside the tab
Maintenance of the history per tab
Working with orientation changes
The tricks to achieve this are the following:
Use the notifyDataSetChanged() method to apply the fragment replacement
Use the fragment manager only for back stage and no for fragament replacement
Maintain the history using the memento pattern (stack)
The adapter code is the following:
public class TabsAdapter extends FragmentStatePagerAdapter implements ActionBar.TabListener, ViewPager.OnPageChangeListener {
/** The sherlock fragment activity. */
private final SherlockFragmentActivity mActivity;
/** The action bar. */
private final ActionBar mActionBar;
/** The pager. */
private final ViewPager mPager;
/** The tabs. */
private List<TabInfo> mTabs = new LinkedList<TabInfo>();
/** The total number of tabs. */
private int TOTAL_TABS;
private Map<Integer, Stack<TabInfo>> history = new HashMap<Integer, Stack<TabInfo>>();
/**
* Creates a new instance.
*
* #param activity the activity
* #param pager the pager
*/
public TabsAdapter(SherlockFragmentActivity activity, ViewPager pager) {
super(activity.getSupportFragmentManager());
activity.getSupportFragmentManager();
this.mActivity = activity;
this.mActionBar = activity.getSupportActionBar();
this.mPager = pager;
mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
}
/**
* Adds the tab.
*
* #param image the image
* #param fragmentClass the class
* #param args the arguments
*/
public void addTab(final Drawable image, final Class fragmentClass, final Bundle args) {
final TabInfo tabInfo = new TabInfo(fragmentClass, args);
final ActionBar.Tab tab = mActionBar.newTab();
tab.setTabListener(this);
tab.setTag(tabInfo);
tab.setIcon(image);
mTabs.add(tabInfo);
mActionBar.addTab(tab);
notifyDataSetChanged();
}
#Override
public Fragment getItem(final int position) {
final TabInfo tabInfo = mTabs.get(position);
return Fragment.instantiate(mActivity, tabInfo.fragmentClass.getName(), tabInfo.args);
}
#Override
public int getItemPosition(final Object object) {
/* Get the current position. */
int position = mActionBar.getSelectedTab().getPosition();
/* The default value. */
int pos = POSITION_NONE;
if (history.get(position).isEmpty()) {
return POSITION_NONE;
}
/* Checks if the object exists in current history. */
for (Stack<TabInfo> stack : history.values()) {
TabInfo c = stack.peek();
if (c.fragmentClass.getName().equals(object.getClass().getName())) {
pos = POSITION_UNCHANGED;
break;
}
}
return pos;
}
#Override
public int getCount() {
return mTabs.size();
}
#Override
public void onPageScrollStateChanged(int arg0) {
}
#Override
public void onPageScrolled(int arg0, float arg1, int arg2) {
}
#Override
public void onPageSelected(int position) {
mActionBar.setSelectedNavigationItem(position);
}
#Override
public void onTabSelected(final ActionBar.Tab tab, final FragmentTransaction ft) {
TabInfo tabInfo = (TabInfo) tab.getTag();
for (int i = 0; i < mTabs.size(); i++) {
if (mTabs.get(i).equals(tabInfo)) {
mPager.setCurrentItem(i);
}
}
}
#Override
public void onTabUnselected(ActionBar.Tab tab, FragmentTransaction ft) {
}
#Override
public void onTabReselected(ActionBar.Tab tab, FragmentTransaction ft) {
}
public void replace(final int position, final Class fragmentClass, final Bundle args) {
/* Save the fragment to the history. */
mActivity.getSupportFragmentManager().beginTransaction().addToBackStack(null).commit();
/* Update the tabs. */
updateTabs(new TabInfo(fragmentClass, args), position);
/* Updates the history. */
history.get(position).push(new TabInfo(mTabs.get(position).fragmentClass, mTabs.get(position).args));
notifyDataSetChanged();
}
/**
* Updates the tabs.
*
* #param tabInfo
* the new tab info
* #param position
* the position
*/
private void updateTabs(final TabInfo tabInfo, final int position) {
mTabs.remove(position);
mTabs.add(position, tabInfo);
mActionBar.getTabAt(position).setTag(tabInfo);
}
/**
* Creates the history using the current state.
*/
public void createHistory() {
int position = 0;
TOTAL_TABS = mTabs.size();
for (TabInfo mTab : mTabs) {
if (history.get(position) == null) {
history.put(position, new Stack<TabInfo>());
}
history.get(position).push(new TabInfo(mTab.fragmentClass, mTab.args));
position++;
}
}
/**
* Called on back
*/
public void back() {
int position = mActionBar.getSelectedTab().getPosition();
if (!historyIsEmpty(position)) {
/* In case there is not any other item in the history, then finalize the activity. */
if (isLastItemInHistory(position)) {
mActivity.finish();
}
final TabInfo currentTabInfo = getPrevious(position);
mTabs.clear();
for (int i = 0; i < TOTAL_TABS; i++) {
if (i == position) {
mTabs.add(new TabInfo(currentTabInfo.fragmentClass, currentTabInfo.args));
} else {
TabInfo otherTabInfo = history.get(i).peek();
mTabs.add(new TabInfo(otherTabInfo.fragmentClass, otherTabInfo.args));
}
}
}
mActionBar.selectTab(mActionBar.getTabAt(position));
notifyDataSetChanged();
}
/**
* Returns if the history is empty.
*
* #param position
* the position
* #return the flag if empty
*/
private boolean historyIsEmpty(final int position) {
return history == null || history.isEmpty() || history.get(position).isEmpty();
}
private boolean isLastItemInHistory(final int position) {
return history.get(position).size() == 1;
}
/**
* Returns the previous state by the position provided.
*
* #param position
* the position
* #return the tab info
*/
private TabInfo getPrevious(final int position) {
TabInfo currentTabInfo = history.get(position).pop();
if (!history.get(position).isEmpty()) {
currentTabInfo = history.get(position).peek();
}
return currentTabInfo;
}
/** The tab info class */
private static class TabInfo {
/** The fragment class. */
public Class fragmentClass;
/** The args.*/
public Bundle args;
/**
* Creates a new instance.
*
* #param fragmentClass
* the fragment class
* #param args
* the args
*/
public TabInfo(Class fragmentClass, Bundle args) {
this.fragmentClass = fragmentClass;
this.args = args;
}
#Override
public boolean equals(final Object o) {
return this.fragmentClass.getName().equals(o.getClass().getName());
}
#Override
public int hashCode() {
return fragmentClass.getName() != null ? fragmentClass.getName().hashCode() : 0;
}
#Override
public String toString() {
return "TabInfo{" +
"fragmentClass=" + fragmentClass +
'}';
}
}
The very first time you add all tabs, we need to call the method createHistory(), to create the initial history
public void createHistory() {
int position = 0;
TOTAL_TABS = mTabs.size();
for (TabInfo mTab : mTabs) {
if (history.get(position) == null) {
history.put(position, new Stack<TabInfo>());
}
history.get(position).push(new TabInfo(mTab.fragmentClass, mTab.args));
position++;
}
}
Every time you want to replace a fragment to a specific tab you call:
replace(final int position, final Class fragmentClass, final Bundle args)
/* Save the fragment to the history. */
mActivity.getSupportFragmentManager().beginTransaction().addToBackStack(null).commit();
/* Update the tabs. */
updateTabs(new TabInfo(fragmentClass, args), position);
/* Updates the history. */
history.get(position).push(new TabInfo(mTabs.get(position).fragmentClass, mTabs.get(position).args));
notifyDataSetChanged();
On back pressed you need to call the back() method:
public void back() {
int position = mActionBar.getSelectedTab().getPosition();
if (!historyIsEmpty(position)) {
/* In case there is not any other item in the history, then finalize the activity. */
if (isLastItemInHistory(position)) {
mActivity.finish();
}
final TabInfo currentTabInfo = getPrevious(position);
mTabs.clear();
for (int i = 0; i < TOTAL_TABS; i++) {
if (i == position) {
mTabs.add(new TabInfo(currentTabInfo.fragmentClass, currentTabInfo.args));
} else {
TabInfo otherTabInfo = history.get(i).peek();
mTabs.add(new TabInfo(otherTabInfo.fragmentClass, otherTabInfo.args));
}
}
}
mActionBar.selectTab(mActionBar.getTabAt(position));
notifyDataSetChanged();
}
The solution works with sherlock action bar and with swipe gesture.
tl;dr: Use a host fragment that is responsible for replacing its hosted content and keeps track of a back navigation history (like in a browser).
As your use case consists of a fixed amount of tabs my solution works well: The idea is to fill the ViewPager with instances of a custom class HostFragment, that is able to replace its hosted content and keeps its own back navigation history. To replace the hosted fragment you make a call to the method hostfragment.replaceFragment():
public void replaceFragment(Fragment fragment, boolean addToBackstack) {
if (addToBackstack) {
getChildFragmentManager().beginTransaction().replace(R.id.hosted_fragment, fragment).addToBackStack(null).commit();
} else {
getChildFragmentManager().beginTransaction().replace(R.id.hosted_fragment, fragment).commit();
}
}
All that method does is to replace the frame layout with the id R.id.hosted_fragment with the fragment provided to the method.
Check my tutorial on this topic for further details and a complete working example on GitHub!
Some of the presented solutions helped me a lot to partially solve the problem but there is still one important thing missing in the solutions which has produced unexpected exceptions and black page content instead of fragment content in some cases.
The thing is that FragmentPagerAdapter class is using item ID to store cached fragments to FragmentManager. For this reason, you need to override also the getItemId(int position) method so that it returns e. g. position for top-level pages and 100 + position for details pages. Otherwise the previously created top-level fragment would be returned from the cache instead of detail-level fragment.
Furthermore, I'm sharing here a complete example how to implement tabs-like activity with Fragment pages using ViewPager and tab buttons using RadioGroup that allows replacement of top-level pages with detailed pages and also supports back button. This implementation supports only one level of back stacking (item list - item details) but multi-level back stacking implementation is straightforward. This example works pretty well in normal cases except of it is throwing a NullPointerException in case when you switch to e. g. second page, change the fragment of the first page (while not visible) and return back to the first page. I'll post a solution to this issue once I'll figure it out:
public class TabsActivity extends FragmentActivity {
public static final int PAGE_COUNT = 3;
public static final int FIRST_PAGE = 0;
public static final int SECOND_PAGE = 1;
public static final int THIRD_PAGE = 2;
/**
* Opens a new inferior page at specified tab position and adds the current page into back
* stack.
*/
public void startPage(int position, Fragment content) {
// Replace page adapter fragment at position.
mPagerAdapter.start(position, content);
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Initialize basic layout.
this.setContentView(R.layout.tabs_activity);
// Add tab fragments to view pager.
{
// Create fragments adapter.
mPagerAdapter = new PagerAdapter(pager);
ViewPager pager = (ViewPager) super.findViewById(R.id.tabs_view_pager);
pager.setAdapter(mPagerAdapter);
// Update active tab in tab bar when page changes.
pager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
#Override
public void onPageScrolled(int index, float value, int nextIndex) {
// Not used.
}
#Override
public void onPageSelected(int index) {
RadioGroup tabs_radio_group = (RadioGroup) TabsActivity.this.findViewById(
R.id.tabs_radio_group);
switch (index) {
case 0: {
tabs_radio_group.check(R.id.first_radio_button);
}
break;
case 1: {
tabs_radio_group.check(R.id.second_radio_button);
}
break;
case 2: {
tabs_radio_group.check(R.id.third_radio_button);
}
break;
}
}
#Override
public void onPageScrollStateChanged(int index) {
// Not used.
}
});
}
// Set "tabs" radio group on checked change listener that changes the displayed page.
RadioGroup radio_group = (RadioGroup) this.findViewById(R.id.tabs_radio_group);
radio_group.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
#Override
public void onCheckedChanged(RadioGroup radioGroup, int id) {
// Get view pager representing tabs.
ViewPager view_pager = (ViewPager) TabsActivity.this.findViewById(R.id.tabs_view_pager);
if (view_pager == null) {
return;
}
// Change the active page.
switch (id) {
case R.id.first_radio_button: {
view_pager.setCurrentItem(FIRST_PAGE);
}
break;
case R.id.second_radio_button: {
view_pager.setCurrentItem(SECOND_PAGE);
}
break;
case R.id.third_radio_button: {
view_pager.setCurrentItem(THIRD_PAGE);
}
break;
}
});
}
}
#Override
public void onBackPressed() {
if (!mPagerAdapter.back()) {
super.onBackPressed();
}
}
/**
* Serves the fragments when paging.
*/
private class PagerAdapter extends FragmentPagerAdapter {
public PagerAdapter(ViewPager container) {
super(TabsActivity.this.getSupportFragmentManager());
mContainer = container;
mFragmentManager = TabsActivity.this.getSupportFragmentManager();
// Prepare "empty" list of fragments.
mFragments = new ArrayList<Fragment>(){};
mBackFragments = new ArrayList<Fragment>(){};
for (int i = 0; i < PAGE_COUNT; i++) {
mFragments.add(null);
mBackFragments.add(null);
}
}
/**
* Replaces the view pager fragment at specified position.
*/
public void replace(int position, Fragment fragment) {
// Get currently active fragment.
Fragment old_fragment = mFragments.get(position);
if (old_fragment == null) {
return;
}
// Replace the fragment using transaction and in underlaying array list.
// NOTE .addToBackStack(null) doesn't work
this.startUpdate(mContainer);
mFragmentManager.beginTransaction().setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
.remove(old_fragment).add(mContainer.getId(), fragment)
.commit();
mFragments.set(position, fragment);
this.notifyDataSetChanged();
this.finishUpdate(mContainer);
}
/**
* Replaces the fragment at specified position and stores the current fragment to back stack
* so it can be restored by #back().
*/
public void start(int position, Fragment fragment) {
// Remember current fragment.
mBackFragments.set(position, mFragments.get(position));
// Replace the displayed fragment.
this.replace(position, fragment);
}
/**
* Replaces the current fragment by fragment stored in back stack. Does nothing and returns
* false if no fragment is back-stacked.
*/
public boolean back() {
int position = mContainer.getCurrentItem();
Fragment fragment = mBackFragments.get(position);
if (fragment == null) {
// Nothing to go back.
return false;
}
// Restore the remembered fragment and remove it from back fragments.
this.replace(position, fragment);
mBackFragments.set(position, null);
return true;
}
/**
* Returns fragment of a page at specified position.
*/
#Override
public Fragment getItem(int position) {
// If fragment not yet initialized, create its instance.
if (mFragments.get(position) == null) {
switch (position) {
case FIRST_PAGE: {
mFragments.set(FIRST_PAGE, new DefaultFirstFragment());
}
break;
case SECOND_PAGE: {
mFragments.set(SECOND_PAGE, new DefaultSecondFragment());
}
break;
case THIRD_PAGE: {
mFragments.set(THIRD_PAGE, new DefaultThirdFragment());
}
break;
}
}
// Return fragment instance at requested position.
return mFragments.get(position);
}
/**
* Custom item ID resolution. Needed for proper page fragment caching.
* #see FragmentPagerAdapter#getItemId(int).
*/
#Override
public long getItemId(int position) {
// Fragments from second level page hierarchy have their ID raised above 100. This is
// important to FragmentPagerAdapter because it is caching fragments to FragmentManager with
// this item ID key.
Fragment item = mFragments.get(position);
if (item != null) {
if ((item instanceof NewFirstFragment) || (item instanceof NewSecondFragment) ||
(item instanceof NewThirdFragment)) {
return 100 + position;
}
}
return position;
}
/**
* Returns number of pages.
*/
#Override
public int getCount() {
return mFragments.size();
}
#Override
public int getItemPosition(Object object)
{
int position = POSITION_UNCHANGED;
if ((object instanceof DefaultFirstFragment) || (object instanceof NewFirstFragment)) {
if (object.getClass() != mFragments.get(FIRST_PAGE).getClass()) {
position = POSITION_NONE;
}
}
if ((object instanceof DefaultSecondragment) || (object instanceof NewSecondFragment)) {
if (object.getClass() != mFragments.get(SECOND_PAGE).getClass()) {
position = POSITION_NONE;
}
}
if ((object instanceof DefaultThirdFragment) || (object instanceof NewThirdFragment)) {
if (object.getClass() != mFragments.get(THIRD_PAGE).getClass()) {
position = POSITION_NONE;
}
}
return position;
}
private ViewPager mContainer;
private FragmentManager mFragmentManager;
/**
* List of page fragments.
*/
private List<Fragment> mFragments;
/**
* List of page fragments to return to in onBack();
*/
private List<Fragment> mBackFragments;
}
/**
* Tab fragments adapter.
*/
private PagerAdapter mPagerAdapter;
}
I have created a ViewPager with 3 elements and 2 sub elements for index 2 and 3 and here what I wanted to do..
I have implemented this with the help from previous questions and answers from StackOverFlow and here is the link.
ViewPagerChildFragments
To replace a fragment inside a ViewPager you can move source codes of ViewPager, PagerAdapter and FragmentStatePagerAdapter classes into your project and add following code.
into ViewPager:
public void notifyItemChanged(Object oldItem, Object newItem) {
if (mItems != null) {
for (ItemInfo itemInfo : mItems) {
if (itemInfo.object.equals(oldItem)) {
itemInfo.object = newItem;
}
}
}
invalidate();
}
into FragmentStatePagerAdapter:
public void replaceFragmetns(ViewPager container, Fragment oldFragment, Fragment newFragment) {
startUpdate(container);
// remove old fragment
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
int position = getFragmentPosition(oldFragment);
while (mSavedState.size() <= position) {
mSavedState.add(null);
}
mSavedState.set(position, null);
mFragments.set(position, null);
mCurTransaction.remove(oldFragment);
// add new fragment
while (mFragments.size() <= position) {
mFragments.add(null);
}
mFragments.set(position, newFragment);
mCurTransaction.add(container.getId(), newFragment);
finishUpdate(container);
// ensure getItem returns newFragemtn after calling handleGetItemInbalidated()
handleGetItemInbalidated(container, oldFragment, newFragment);
container.notifyItemChanged(oldFragment, newFragment);
}
protected abstract void handleGetItemInbalidated(View container, Fragment oldFragment, Fragment newFragment);
protected abstract int getFragmentPosition(Fragment fragment);
handleGetItemInvalidated() ensures that after next call of getItem() it return newFragment
getFragmentPosition() returns position of the fragment in your adapter.
Now, to replace fragments call
mAdapter.replaceFragmetns(mViewPager, oldFragment, newFragment);
If you interested in an example project ask me for the sources.
Works Great with AndroidTeam's solution, however I found that I needed the ability to go back much like FrgmentTransaction.addToBackStack(null) But merely adding this will only cause the Fragment to be replaced without notifying the ViewPager. Combining the provided solution with this minor enhancement will allow you to return to the previous state by merely overriding the activity's onBackPressed() method. The biggest drawback is that it will only go back one at a time which may result in multiple back clicks
private ArrayList<Fragment> bFragments = new ArrayList<Fragment>();
private ArrayList<Integer> bPosition = new ArrayList<Integer>();
public void replaceFragmentsWithBackOut(ViewPager container, Fragment oldFragment, Fragment newFragment) {
startUpdate(container);
// remove old fragment
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
int position = getFragmentPosition(oldFragment);
while (mSavedState.size() <= position) {
mSavedState.add(null);
}
//Add Fragment to Back List
bFragments.add(oldFragment);
//Add Pager Position to Back List
bPosition.add(position);
mSavedState.set(position, null);
mFragments.set(position, null);
mCurTransaction.remove(oldFragment);
// add new fragment
while (mFragments.size() <= position) {
mFragments.add(null);
}
mFragments.set(position, newFragment);
mCurTransaction.add(container.getId(), newFragment);
finishUpdate(container);
// ensure getItem returns newFragemtn after calling handleGetItemInbalidated()
handleGetItemInvalidated(container, oldFragment, newFragment);
container.notifyItemChanged(oldFragment, newFragment);
}
public boolean popBackImmediate(ViewPager container){
int bFragSize = bFragments.size();
int bPosSize = bPosition.size();
if(bFragSize>0 && bPosSize>0){
if(bFragSize==bPosSize){
int last = bFragSize-1;
int position = bPosition.get(last);
//Returns Fragment Currently at this position
Fragment replacedFragment = mFragments.get(position);
Fragment originalFragment = bFragments.get(last);
this.replaceFragments(container, replacedFragment, originalFragment);
bPosition.remove(last);
bFragments.remove(last);
return true;
}
}
return false;
}
Hope this helps someone.
Also as far as getFragmentPosition() goes it's pretty much getItem() in reverse. You know which fragments go where, just make sure you return the correct position it will be in. Here's an example:
#Override
protected int getFragmentPosition(Fragment fragment) {
if(fragment.equals(originalFragment1)){
return 0;
}
if(fragment.equals(replacementFragment1)){
return 0;
}
if(fragment.equals(Fragment2)){
return 1;
}
return -1;
}
In your onCreateView method, container is actually a ViewPager instance.
So, just calling
ViewPager vpViewPager = (ViewPager) container;
vpViewPager.setCurrentItem(1);
will change current fragment in your ViewPager.
Here's my relatively simple solution to this problem. The keys to this solution are to use FragmentStatePagerAdapter instead of FragmentPagerAdapter as the former will remove unused fragments for you while the later still retains their instances. The second is the use of POSITION_NONE in getItem(). I've used a simple List to keep track of my fragments. My requirement was to replace the entire list of fragments at once with a new list, but the below could be easily modified to replace individual fragments:
public class MyFragmentAdapter extends FragmentStatePagerAdapter {
private List<Fragment> fragmentList = new ArrayList<Fragment>();
private List<String> tabTitleList = new ArrayList<String>();
public MyFragmentAdapter(FragmentManager fm) {
super(fm);
}
public void addFragments(List<Fragment> fragments, List<String> titles) {
fragmentList.clear();
tabTitleList.clear();
fragmentList.addAll(fragments);
tabTitleList.addAll(titles);
notifyDataSetChanged();
}
#Override
public int getItemPosition(Object object) {
if (fragmentList.contains(object)) {
return POSITION_UNCHANGED;
}
return POSITION_NONE;
}
#Override
public Fragment getItem(int item) {
if (item >= fragmentList.size()) {
return null;
}
return fragmentList.get(item);
}
#Override
public int getCount() {
return fragmentList.size();
}
#Override
public CharSequence getPageTitle(int position) {
return tabTitleList.get(position);
}
}
I also made a solution, which is working with Stacks. It's a more modular approach so u don't have to specify each Fragment and Detail Fragment in your FragmentPagerAdapter. It's build on top of the Example from ActionbarSherlock which derives if I'm right from the Google Demo App.
/**
* This is a helper class that implements the management of tabs and all
* details of connecting a ViewPager with associated TabHost. It relies on a
* trick. Normally a tab host has a simple API for supplying a View or
* Intent that each tab will show. This is not sufficient for switching
* between pages. So instead we make the content part of the tab host
* 0dp high (it is not shown) and the TabsAdapter supplies its own dummy
* view to show as the tab content. It listens to changes in tabs, and takes
* care of switch to the correct paged in the ViewPager whenever the selected
* tab changes.
*
* Changed to support more Layers of fragments on each Tab.
* by sebnapi (2012)
*
*/
public class TabsAdapter extends FragmentPagerAdapter
implements TabHost.OnTabChangeListener, ViewPager.OnPageChangeListener {
private final Context mContext;
private final TabHost mTabHost;
private final ViewPager mViewPager;
private ArrayList<String> mTabTags = new ArrayList<String>();
private HashMap<String, Stack<TabInfo>> mTabStackMap = new HashMap<String, Stack<TabInfo>>();
static final class TabInfo {
public final String tag;
public final Class<?> clss;
public Bundle args;
TabInfo(String _tag, Class<?> _class, Bundle _args) {
tag = _tag;
clss = _class;
args = _args;
}
}
static class DummyTabFactory implements TabHost.TabContentFactory {
private final Context mContext;
public DummyTabFactory(Context context) {
mContext = context;
}
#Override
public View createTabContent(String tag) {
View v = new View(mContext);
v.setMinimumWidth(0);
v.setMinimumHeight(0);
return v;
}
}
public interface SaveStateBundle{
public Bundle onRemoveFragment(Bundle outState);
}
public TabsAdapter(FragmentActivity activity, TabHost tabHost, ViewPager pager) {
super(activity.getSupportFragmentManager());
mContext = activity;
mTabHost = tabHost;
mViewPager = pager;
mTabHost.setOnTabChangedListener(this);
mViewPager.setAdapter(this);
mViewPager.setOnPageChangeListener(this);
}
/**
* Add a Tab which will have Fragment Stack. Add Fragments on this Stack by using
* addFragment(FragmentManager fm, String _tag, Class<?> _class, Bundle _args)
* The Stack will hold always the default Fragment u add here.
*
* DON'T ADD Tabs with same tag, it's not beeing checked and results in unexpected
* beahvior.
*
* #param tabSpec
* #param clss
* #param args
*/
public void addTab(TabHost.TabSpec tabSpec, Class<?> clss, Bundle args){
Stack<TabInfo> tabStack = new Stack<TabInfo>();
tabSpec.setContent(new DummyTabFactory(mContext));
mTabHost.addTab(tabSpec);
String tag = tabSpec.getTag();
TabInfo info = new TabInfo(tag, clss, args);
mTabTags.add(tag); // to know the position of the tab tag
tabStack.add(info);
mTabStackMap.put(tag, tabStack);
notifyDataSetChanged();
}
/**
* Will add the Fragment to Tab with the Tag _tag. Provide the Class of the Fragment
* it will be instantiated by this object. Proivde _args for your Fragment.
*
* #param fm
* #param _tag
* #param _class
* #param _args
*/
public void addFragment(FragmentManager fm, String _tag, Class<?> _class, Bundle _args){
TabInfo info = new TabInfo(_tag, _class, _args);
Stack<TabInfo> tabStack = mTabStackMap.get(_tag);
Fragment frag = fm.findFragmentByTag("android:switcher:" + mViewPager.getId() + ":" + mTabTags.indexOf(_tag));
if(frag instanceof SaveStateBundle){
Bundle b = new Bundle();
((SaveStateBundle) frag).onRemoveFragment(b);
tabStack.peek().args = b;
}
tabStack.add(info);
FragmentTransaction ft = fm.beginTransaction();
ft.remove(frag).commit();
notifyDataSetChanged();
}
/**
* Will pop the Fragment added to the Tab with the Tag _tag
*
* #param fm
* #param _tag
* #return
*/
public boolean popFragment(FragmentManager fm, String _tag){
Stack<TabInfo> tabStack = mTabStackMap.get(_tag);
if(tabStack.size()>1){
tabStack.pop();
Fragment frag = fm.findFragmentByTag("android:switcher:" + mViewPager.getId() + ":" + mTabTags.indexOf(_tag));
FragmentTransaction ft = fm.beginTransaction();
ft.remove(frag).commit();
notifyDataSetChanged();
return true;
}
return false;
}
public boolean back(FragmentManager fm) {
int position = mViewPager.getCurrentItem();
return popFragment(fm, mTabTags.get(position));
}
#Override
public int getCount() {
return mTabStackMap.size();
}
#Override
public int getItemPosition(Object object) {
ArrayList<Class<?>> positionNoneHack = new ArrayList<Class<?>>();
for(Stack<TabInfo> tabStack: mTabStackMap.values()){
positionNoneHack.add(tabStack.peek().clss);
} // if the object class lies on top of our stacks, we return default
if(positionNoneHack.contains(object.getClass())){
return POSITION_UNCHANGED;
}
return POSITION_NONE;
}
#Override
public Fragment getItem(int position) {
Stack<TabInfo> tabStack = mTabStackMap.get(mTabTags.get(position));
TabInfo info = tabStack.peek();
return Fragment.instantiate(mContext, info.clss.getName(), info.args);
}
#Override
public void onTabChanged(String tabId) {
int position = mTabHost.getCurrentTab();
mViewPager.setCurrentItem(position);
}
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
#Override
public void onPageSelected(int position) {
// Unfortunately when TabHost changes the current tab, it kindly
// also takes care of putting focus on it when not in touch mode.
// The jerk.
// This hack tries to prevent this from pulling focus out of our
// ViewPager.
TabWidget widget = mTabHost.getTabWidget();
int oldFocusability = widget.getDescendantFocusability();
widget.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
mTabHost.setCurrentTab(position);
widget.setDescendantFocusability(oldFocusability);
}
#Override
public void onPageScrollStateChanged(int state) {
}
}
Add this for back button functionality in your MainActivity:
#Override
public void onBackPressed() {
if (!mTabsAdapter.back(getSupportFragmentManager())) {
super.onBackPressed();
}
}
If u like to save the Fragment State when it get's removed. Let your Fragment implement the interface SaveStateBundle return in the function a bundle with your save state. Get the bundle after instantiation by this.getArguments().
You can instantiate a tab like this:
mTabsAdapter.addTab(mTabHost.newTabSpec("firstTabTag").setIndicator("First Tab Title"),
FirstFragmentActivity.FirstFragmentFragment.class, null);
works similiar if u want to add a Fragment on top of a Tab Stack.
Important: I think, it won't work if u want to have 2 instances of same class on top of two Tabs.
I did this solution quick together, so I can only share it without providing any experience with it.
Replacing fragments in a viewpager is quite involved but is very possible and can look super slick. First, you need to let the viewpager itself handle the removing and adding of the fragments. What is happening is when you replace the fragment inside of SearchFragment, your viewpager retains its fragment views. So you end up with a blank page because the SearchFragment gets removed when you try to replace it.
The solution is to create a listener inside of your viewpager that will handle changes made outside of it so first add this code to the bottom of your adapter.
public interface nextFragmentListener {
public void fragment0Changed(String newFragmentIdentification);
}
Then you need to create a private class in your viewpager that becomes a listener for when you want to change your fragment. For example you could add something like this. Notice that it implements the interface that was just created. So whenever you call this method, it will run the code inside of the class below.
private final class fragmentChangeListener implements nextFragmentListener {
#Override
public void fragment0Changed(String fragment) {
//I will explain the purpose of fragment0 in a moment
fragment0 = fragment;
manager.beginTransaction().remove(fragAt0).commit();
switch (fragment){
case "searchFragment":
fragAt0 = SearchFragment.newInstance(listener);
break;
case "searchResultFragment":
fragAt0 = Fragment_Table.newInstance(listener);
break;
}
notifyDataSetChanged();
}
There are two main things to point out here:
fragAt0 is a "flexible" fragment. It can take on whatever fragment type you give it. This allows it to become your best friend in changing the fragment at position 0 to the fragment you desire.
Notice the listeners that are placed in the 'newInstance(listener)constructor. These are how you will callfragment0Changed(String newFragmentIdentification)`. The following code shows how you create the listener inside of your fragment.
static nextFragmentListener listenerSearch;
public static Fragment_Journals newInstance(nextFragmentListener listener){
listenerSearch = listener;
return new Fragment_Journals();
}
You could call the change inside of your onPostExecute
private class SearchAsyncTask extends AsyncTask<Void, Void, Void>{
protected Void doInBackground(Void... params){
.
.//some more operation
.
}
protected void onPostExecute(Void param){
listenerSearch.fragment0Changed("searchResultFragment");
}
}
This would trigger the code inside of your viewpager to switch your fragment at position zero fragAt0 to become a new searchResultFragment. There are two more small pieces you would need to add to the viewpager before it became functional.
One would be in the getItem override method of the viewpager.
#Override
public Fragment getItem(int index) {
switch (index) {
case 0:
//this is where it will "remember" which fragment you have just selected. the key is to set a static String fragment at the top of your page that will hold the position that you had just selected.
if(fragAt0 == null){
switch(fragment0){
case "searchFragment":
fragAt0 = FragmentSearch.newInstance(listener);
break;
case "searchResultsFragment":
fragAt0 = FragmentSearchResults.newInstance(listener);
break;
}
}
return fragAt0;
case 1:
// Games fragment activity
return new CreateFragment();
}
Now without this final piece you would still get a blank page. Kind of lame, but it is an essential part of the viewPager. You must override the getItemPosition method of the viewpager. Ordinarily this method will return POSITION_UNCHANGED which tells the viewpager to keep everything the same and so getItem will never get called to place the new fragment on the page. Here's an example of something you could do
public int getItemPosition(Object object)
{
//object is the current fragment displayed at position 0.
if(object instanceof SearchFragment && fragAt0 instanceof SearchResultFragment){
return POSITION_NONE;
//this condition is for when you press back
}else if{(object instanceof SearchResultFragment && fragAt0 instanceof SearchFragment){
return POSITION_NONE;
}
return POSITION_UNCHANGED
}
Like I said, the code gets very involved, but you basically have to create a custom adapter for your situation. The things I mentioned will make it possible to change the fragment. It will likely take a long time to soak everything in so I would be patient, but it will all make sense. It is totally worth taking the time because it can make a really slick looking application.
Here's the nugget for handling the back button. You put this inside your MainActivity
public void onBackPressed() {
if(mViewPager.getCurrentItem() == 0) {
if(pagerAdapter.getItem(0) instanceof FragmentSearchResults){
((Fragment_Table) pagerAdapter.getItem(0)).backPressed();
}else if (pagerAdapter.getItem(0) instanceof FragmentSearch) {
finish();
}
}
You will need to create a method called backPressed() inside of FragmentSearchResults that calls fragment0changed. This in tandem with the code I showed before will handle pressing the back button. Good luck with your code to change the viewpager. It takes a lot of work, and as far as I have found, there aren't any quick adaptations. Like I said, you are basically creating a custom viewpager adapter, and letting it handle all of the necessary changes using listeners
This is my way to achieve that.
First of all add Root_fragment inside viewPager tab in which you want to implement button click fragment event. Example;
#Override
public Fragment getItem(int position) {
if(position==0)
return RootTabFragment.newInstance();
else
return SecondPagerFragment.newInstance();
}
First of all, RootTabFragment should be include FragmentLayout for fragment change.
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/root_frame"
android:layout_width="match_parent"
android:layout_height="match_parent">
</FrameLayout>
Then, inside RootTabFragment onCreateView, implement fragmentChange for your FirstPagerFragment
getChildFragmentManager().beginTransaction().replace(R.id.root_frame, FirstPagerFragment.newInstance()).commit();
After that, implement onClick event for your button inside FirstPagerFragment and make fragment change like that again.
getChildFragmentManager().beginTransaction().replace(R.id.root_frame, NextFragment.newInstance()).commit();
Hope this will help you guy.
I found simple solution, which works fine even if you want add new fragments in the middle or replace current fragment. In my solution you should override getItemId() which should return unique id for each fragment. Not position as by default.
There is it:
public class DynamicPagerAdapter extends FragmentPagerAdapter {
private ArrayList<Page> mPages = new ArrayList<Page>();
private ArrayList<Fragment> mFragments = new ArrayList<Fragment>();
public DynamicPagerAdapter(FragmentManager fm) {
super(fm);
}
public void replacePage(int position, Page page) {
mPages.set(position, page);
notifyDataSetChanged();
}
public void setPages(ArrayList<Page> pages) {
mPages = pages;
notifyDataSetChanged();
}
#Override
public Fragment getItem(int position) {
if (mPages.get(position).mPageType == PageType.FIRST) {
return FirstFragment.newInstance(mPages.get(position));
} else {
return SecondFragment.newInstance(mPages.get(position));
}
}
#Override
public int getCount() {
return mPages.size();
}
#Override
public long getItemId(int position) {
// return unique id
return mPages.get(position).getId();
}
#Override
public Object instantiateItem(ViewGroup container, int position) {
Fragment fragment = (Fragment) super.instantiateItem(container, position);
while (mFragments.size() <= position) {
mFragments.add(null);
}
mFragments.set(position, fragment);
return fragment;
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
super.destroyItem(container, position, object);
mFragments.set(position, null);
}
#Override
public int getItemPosition(Object object) {
PagerFragment pagerFragment = (PagerFragment) object;
Page page = pagerFragment.getPage();
int position = mFragments.indexOf(pagerFragment);
if (page.equals(mPages.get(position))) {
return POSITION_UNCHANGED;
} else {
return POSITION_NONE;
}
}
}
Notice: In this example FirstFragment and SecondFragment extends abstract class PageFragment, which has method getPage().
I doing something to similar to wize but in my answer yo can change between the two fragments whenever you want. And with the wize answer I have some problems when changing the orientation of the screen an things like that. This is the PagerAdapter looks like:
public class MyAdapter extends FragmentPagerAdapter
{
static final int NUM_ITEMS = 2;
private final FragmentManager mFragmentManager;
private Fragment mFragmentAtPos0;
private Map<Integer, String> mFragmentTags;
private boolean isNextFragment=false;
public MyAdapter(FragmentManager fm)
{
super(fm);
mFragmentManager = fm;
mFragmentTags = new HashMap<Integer, String>();
}
#Override
public Fragment getItem(int position)
{
if (position == 0)
{
if (isPager) {
mFragmentAtPos0 = new FirstPageFragment();
} else {
mFragmentAtPos0 = new NextFragment();
}
return mFragmentAtPos0;
}
else
return SecondPageFragment.newInstance();
}
#Override
public int getCount()
{
return NUM_ITEMS;
}
#Override
public Object instantiateItem(ViewGroup container, int position) {
Object obj = super.instantiateItem(container, position);
if (obj instanceof Fragment) {
// record the fragment tag here.
Fragment f = (Fragment) obj;
String tag = f.getTag();
mFragmentTags.put(position, tag);
}
return obj;
}
public void onChange(boolean isNextFragment) {
if (mFragmentAtPos0 == null)
mFragmentAtPos0 = getFragment(0);
if (mFragmentAtPos0 != null)
mFragmentManager.beginTransaction().remove(mFragmentAtPos0).commit();
if (!isNextFragment) {
mFragmentAtFlashcards = new FirstPageFragment();
} else {
mFragmentAtFlashcards = new NextFragment();
}
notifyDataSetChanged();
}
#Override
public int getItemPosition(Object object)
{
if (object instanceof FirstPageFragment && mFragmentAtPos0 instanceof NextFragment)
return POSITION_NONE;
if (object instanceof NextFragment && mFragmentAtPos0 instanceof FirstPageFragment)
return POSITION_NONE;
return POSITION_UNCHANGED;
}
public Fragment getFragment(int position) {
String tag = mFragmentTags.get(position);
if (tag == null)
return null;
return mFragmentManager.findFragmentByTag(tag);
}
}
The listener I implemented in the adapter container activity to put it to the fragment when attaching it, this is the activity:
public class PagerContainerActivity extends AppCompatActivity implements ChangeFragmentListener {
//...
#Override
public void onChange(boolean isNextFragment) {
if (pagerAdapter != null)
pagerAdapter.onChange(isNextFragment);
}
//...
}
Then in the fragment putting the listener when attach an calling it:
public class FirstPageFragment extends Fragment{
private ChangeFragmentListener changeFragmentListener;
//...
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
changeFragmentListener = ((PagerContainerActivity) activity);
}
#Override
public void onDetach() {
super.onDetach();
changeFragmentListener = null;
}
//...
//in the on click to change the fragment
changeFragmentListener.onChange(true);
//...
}
And finally the listener:
public interface changeFragmentListener {
void onChange(boolean isNextFragment);
}
I followed the answers by #wize and #mdelolmo and I got the solution. Thanks Tons. But, I tuned these solutions a little bit to improve the memory consumption.
Problems I observed:
They save the instance of Fragment which is replaced. In my case, it is a Fragment which holds MapView and I thought its costly. So, I am maintaining the FragmentPagerPositionChanged (POSITION_NONE or POSITION_UNCHANGED) instead of Fragment itself.
Here is my implementation.
public static class DemoCollectionPagerAdapter extends FragmentStatePagerAdapter {
private SwitchFragListener mSwitchFragListener;
private Switch mToggle;
private int pagerAdapterPosChanged = POSITION_UNCHANGED;
private static final int TOGGLE_ENABLE_POS = 2;
public DemoCollectionPagerAdapter(FragmentManager fm, Switch toggle) {
super(fm);
mToggle = toggle;
mSwitchFragListener = new SwitchFragListener();
mToggle.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
mSwitchFragListener.onSwitchToNextFragment();
}
});
}
#Override
public Fragment getItem(int i) {
switch (i)
{
case TOGGLE_ENABLE_POS:
if(mToggle.isChecked())
{
return TabReplaceFragment.getInstance();
}else
{
return DemoTab2Fragment.getInstance(i);
}
default:
return DemoTabFragment.getInstance(i);
}
}
#Override
public int getCount() {
return 5;
}
#Override
public CharSequence getPageTitle(int position) {
return "Tab " + (position + 1);
}
#Override
public int getItemPosition(Object object) {
// This check make sures getItem() is called only for the required Fragment
if (object instanceof TabReplaceFragment
|| object instanceof DemoTab2Fragment)
return pagerAdapterPosChanged;
return POSITION_UNCHANGED;
}
/**
* Switch fragments Interface implementation
*/
private final class SwitchFragListener implements
SwitchFragInterface {
SwitchFragListener() {}
public void onSwitchToNextFragment() {
pagerAdapterPosChanged = POSITION_NONE;
notifyDataSetChanged();
}
}
/**
* Interface to switch frags
*/
private interface SwitchFragInterface{
void onSwitchToNextFragment();
}
}
Demo link here.. https://youtu.be/l_62uhKkLyM
For demo purpose, used 2 fragments TabReplaceFragment and DemoTab2Fragment at position two. In all the other cases I'm using DemoTabFragment instances.
Explanation:
I'm passing Switch from Activity to the DemoCollectionPagerAdapter. Based on the state of this switch we will display correct fragment. When the switch check is changed, I'm calling the SwitchFragListener's onSwitchToNextFragment method, where I'm changing the value of pagerAdapterPosChanged variable to POSITION_NONE. Check out more about POSITION_NONE. This will invalidate the getItem and I have logics to instantiate the right fragment over there. Sorry, if the explanation is a bit messy.
Once again big thanks to #wize and #mdelolmo for the original idea.
Hope this is helpful. :)
Let me know if this implementation has any flaws. That will be greatly helpful for my project.
after research i found solution with short code.
first of all create a public instance on fragment and just remove your fragment on onSaveInstanceState if fragment not recreating on orientation change.
#Override
public void onSaveInstanceState(Bundle outState) {
if (null != mCalFragment) {
FragmentTransaction bt = getChildFragmentManager().beginTransaction();
bt.remove(mFragment);
bt.commit();
}
super.onSaveInstanceState(outState);
}

Categories

Resources