8

What I want.

In a tab sliding menu context, I want to replace a fragment to another inside a tab, and maintaining the tab menu, and also the current tab. When sliding to another tab and returning to original, I want the last fragment to be displayed.

For example, I have tab_a with Fragment_1, tab_b with Fragment_4 and tab_c with Fragment_7. Now I want a button in Fragment_1 that opens me Fragment_2, but I want to be able to swipe to the fragments of the tab_b and tab_c. And when I return to tab_a, Fragment_2 must be displayed.

enter image description here

MainActivity
    |
    |
ContainerFragment
    |
    |
    |_ _ _ Tab A
    |        |_ _ _ Fragment 1
    |        |
    |        |_ _ _ Fragment 2
    |        |
    |        |_ _ _ Fragment 3
    |        |
    |        |_ _ _ ...
    |        
    |_ _ _ Tab B
    |        |_ _ _ Fragment 4
    |        |
    |        |_ _ _ Fragment 5
    |        |
    |        |_ _ _ Fragment 6
    |        |
    |        |_ _ _ ...
    |
    |_ _ _ Tab C
    |        |_ _ _ Fragment 7
    |        |
    |        |_ _ _ Fragment 8
    |        |
    |        |_ _ _ Fragment 8
    |        |
    |        |_ _ _ ...

My current main activity.

Xml

<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

        <android.support.design.widget.TabLayout
            android:id="@+id/tabs"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:tabMode="fixed"
            app:tabGravity="fill"/>
    </android.support.design.widget.AppBarLayout>

    <android.support.v4.view.ViewPager
        android:id="@+id/viewpager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"  />

</android.support.design.widget.CoordinatorLayout>

Code

public class MainActivity extends AppCompatActivity {

    private TabLayout tabLayout;

    private final static int[] tabIcons = {
            R.drawable.ic_tab_a,
            R.drawable.ic_tab_b,
            R.drawable.ic_tab_c
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ViewPager viewPager = (ViewPager) findViewById(R.id.viewpager);
        setupViewPager(viewPager);

        tabLayout = (TabLayout) findViewById(R.id.tabs);
        tabLayout.setupWithViewPager(viewPager);
        setupTabIcons();
    }

    private void setupTabIcons() {
        tabLayout.getTabAt(0).setIcon(tabIcons[0]);
        tabLayout.getTabAt(1).setIcon(tabIcons[1]);
        tabLayout.getTabAt(2).setIcon(tabIcons[2]);
    }

    private void setupViewPager(ViewPager viewPager) {
        ViewPagerAdapter adapter = new ViewPagerAdapter(getSupportFragmentManager());
        adapter.addFrag(new Fragment1());
        adapter.addFrag(new Fragment4());
        adapter.addFrag(new Fragment7());
        viewPager.setAdapter(adapter);
    }

    class ViewPagerAdapter extends FragmentPagerAdapter {

        private final List<Fragment> mFragmentList = new ArrayList<>();

        ViewPagerAdapter(FragmentManager manager) {
            super(manager);
        }

        @Override
        public Fragment getItem(int position) {
            return mFragmentList.get(position);
        }

        @Override
        public int getCount() {
            return mFragmentList.size();
        }

        void addFrag(Fragment fragment) {
            mFragmentList.add(fragment);
        }
    }
}

Fragments

Xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:background="@android:color/holo_blue_light"
    android:id="@+id/fragment_1"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="myContext">

    <Button
        android:text="Go to next Fragment"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true"
        android:id="@+id/button_nextFrag" />

    <FrameLayout
        android:id="@+id/tab1_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</RelativeLayout>

Code

public class Fragment1 extends Fragment {

    private void goNextFragment() {
        FragmentTransaction trans = getFragmentManager().beginTransaction();
        trans.replace(R.id.tab1_container, new Fragment2());
        trans.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
        trans.addToBackStack(null);
        trans.commit();
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment.
        View root = inflater.inflate(R.layout.fragment_1, container, false);
        // Go to next Fragment Button.
        final Button nextFrag = (Button) root.findViewById(R.id.button_nextFrag);
        nextFrag .setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                goNextFragment();
            }
        });
        return root;
    }
}

Edit:

I've got some solutions, but none is perfect. If I put a Framelayaout in MainActivity, Fragment_1 is correctly replaced to Fragment_2, but it is also in tab_b and tab_c. If the FrameLayout is in Fragment_1, I can swipe the other tabs correctly, but the replacement goes not good, cause Fragment_2 is opened, but Fragment_1 is also there.

Santiago Gil
  • 1,292
  • 7
  • 21
  • 52
  • What exactly happens in your case? Does the screen orientation repopulates all the fragments at initial state? – Reaz Murshed Aug 18 '16 at 10:33
  • @ReazMurshed I havn't reached that point yet, I'm still working in replace fragments correctly. I have seen another solutions, and some consist in a big switch for each fragment possible, but if we consider a big scenario with a lot of fragments, this solution can't be the best one, and I am sure there has to be a better solution. – Santiago Gil Aug 18 '16 at 10:46
  • @SantiGil you mean you want something like Flipboard app? tabs inside tabs? – Amir Aug 20 '16 at 16:17
  • @Amir I explain it a bit better. I currently have 3 tabs with one fragment each one (`Fragment1`, `Fragment4` and `Fragment7` respectively). Now I want a button in the `tab_a`, for example, which opens `Fragment2`, and maintains the tab order. If I swipe to `tab_b`, and return to `tab_a`, I want `Fragment2` to be displayed. And I want also to be able to go back from `Fragment2` to `Fragment1`. – Santiago Gil Aug 20 '16 at 16:26
  • Edited question with more information of my problem. – Santiago Gil Aug 22 '16 at 19:43
  • 1
    I wrote a blog article on that topic a few months ago: https://medium.com/@nilan/separate-back-navigation-for-a-tabbed-view-pager-in-android-459859f607e4#.s0jiyd6gu :) – marktani Aug 23 '16 at 18:44
  • @mcwise Worked! And in a very smart way, +1. How would you implement with this code a method that "kills" the two previous fragments in a tab? I want to give you the bounty. – Santiago Gil Aug 23 '16 at 20:12
  • 1
    I just posted an answer! :) If you mean you want to pop the backstack twice, you could fire the back button event a second time as a hack: `this.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK)); ` – marktani Aug 23 '16 at 20:49

3 Answers3

3

Based on what you explained I Implement following:

1- In your Activity create your Tabs and ViewPager:

    mPager = (ViewPager) findViewById(R.id.vp_pager);
    mPager.setOffscreenPageLimit(3);
    PagerAdapter mAdapter = new PagerAdapter(getSupportFragmentManager(), getContext());
    mPager.setAdapter(mAdapter);
    //tab.setupWithViewPager(mPager);

2- In each Tabs create ViewPager (I assume you want tabs inside tabs if you don't need tabs just removed them), Implementation of each fragment would be something like :

    ViewPager mPager = (ViewPager) rootView.findViewById(R.id.vp_pager);
    mPager.setOffscreenPageLimit(3);
    CategoryAdapter mAdapter = new CategoryAdapter(getChildFragmentManager());//make sure this ChildFragmentManager
    mPager.setAdapter(mAdapter);

Also in fragment create a Button which when Click goes to another fragment:

 button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                mPager.setCurrentItem(1); // go to second fragment
            }
        });

final result would be something like following and when If you go to Home Tabs and then goes back to Category Tab you're your Fragment position not changed.

Two point you should remember:

  1. set mPager.setOffscreenPageLimit(3); if you don't want to destroy your fragment
  2. In inner fragment pass getChildFragmentManager() instead of FragmentManager()

enter image description here

If you don't need Inner ViewPager And you Just want load one instance of Fragment at time here is another option:

1- Do first item of previous way.

2- In your fragment xml put FrameContainer and load your fragment inside it:

 CategoryResultFragment f = CategoryResultFragment.newInstance();
        getFragmentManager().beginTransaction()
                .replace(R.id.frame_result_container, f)
                .commit();

Edit: This is not best approach but I think solve your issue:

1- Create a static field in your MainActivity like following:

static String[] type = new String[3]; 

2- When you call onClick in your fragment, update String value in your MainActivity.

public static update currentType(int pos,String type);

first value is ViewPager position, Second value is your inner Fragment type (e.g: fragment_d);

3- In your ViewPager

 @Override
        public Fragment getItem(int position) {
            switch (position) {
                case 0: // first tab
                    if(type[position] == "fragment_d")
                        return new Fragment_D();
                    else
                        return new Fragment_B();

            }
        }
Amir
  • 16,067
  • 10
  • 80
  • 119
  • I am trying your solution. The first `mPager.setOffscreenPageLimit(3);` I guess that is because I have 3 tabs `tab_a` `tab_b` `tab_c`. But the second one, why is also 3? – Santiago Gil Aug 20 '16 at 17:21
  • 1
    @SantiGil I assume your inner ViewPager have 3 pages. ( also by default setOffscreenPageLimit is 3 ) – Amir Aug 20 '16 at 17:22
  • Ok! Should I have a `root fragment` for each tab, inside its viewpager I put my 3 fragments? This would be a total of 4 fragments. 1 `root fragment` and 3 `normal fragments` – Santiago Gil Aug 20 '16 at 17:32
  • let me put some simple example in github may be it's better. – Amir Aug 20 '16 at 17:33
  • Ok! In each tab I just want one instance of Fragment at time. And when I click `button`, I want to load next Fragment (the second way you suggest) – Santiago Gil Aug 20 '16 at 17:48
  • 1
    @SantiGil https://github.com/AmirHadifar/InnerTabsExample this repository may gives you some Idea. – Amir Aug 20 '16 at 17:59
  • Umm, but I don't still know how to open another fragment, not sliding the screen. In your code example, imagine that inside `Tab0 > Inner Tabs0` there is a button to open a different fragment. How would be that? – Santiago Gil Aug 20 '16 at 18:11
  • 1
    @SantiGil I set **todo** in CategoryFramgent you should setCurrentPager in OnClickListener. – Amir Aug 20 '16 at 18:13
  • But if the fragment to open is not one in the viewpager? Just another – Santiago Gil Aug 20 '16 at 18:15
  • @SantiGil by this code it's not possible ( you need add some hack to do this) – Amir Aug 20 '16 at 18:16
  • How would be the code for what I'm trying? Still don't work :I – Santiago Gil Aug 22 '16 at 11:49
  • @SantiGil what have you trying so far ? – Amir Aug 22 '16 at 11:53
  • For example, I have `tab_a` with `Fragment_1`, `tab_b` with `Fragment`_4 and `tab_c` with `Fragment_7`. Now I want a button in `Fragment_1` that opens me `Fragment_2`, but I want to be able to swipe to the fragments of the `tab_b` and `tab_c`. And when I return to `tab_a`, `Fragment_2` must be displayed. – Santiago Gil Aug 22 '16 at 11:54
  • In fragment_1 put a FrameLayout and add fragment with code like: getFragmentManager().beginTransaction() .replace(R.id.container, SimpleFragment.newInstance()) .commit(); – Amir Aug 22 '16 at 12:00
  • 1
    @SantiGil Also for handling backPressed in inner fragment do this: http://stackoverflow.com/a/29166971/1462770 – Amir Aug 22 '16 at 12:13
  • Now I have a Button and FrameLayout in my `Fragment_1`. If i click the button, `Fragment_2` is opened and i can swipe the others `tabs` correctly; but button from `Fragment_1` is still there, in `tab_a`. – Santiago Gil Aug 22 '16 at 12:29
  • @SantiGil it's not standard guideline So you should handle it with some hacks. – Amir Aug 22 '16 at 12:37
  • @SantiGil update my code a bit. it's kind of hacky but I think solve your issue. – Amir Aug 23 '16 at 18:43
  • Done, but now button doesn't open new Fragment_2. **1.** In MainActivity `static int tab1Fragment = R.id.fragment_1;` and `public static void update (int newFragment) { tab1Fragment = newFragment; }`. **2.** In Fragment_1 `MainActivity.update(R.id.fragment_2r);` in the onClick. **3.** And in ViewPager [this code](https://gyazo.com/cd0622094c60f9638396a9d2e26f8647). – Santiago Gil Aug 23 '16 at 19:18
2

You can use a ViewPager together with a TabLayout.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<android.support.v4.view.ViewPager
    android:id="@+id/viewpager"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="1"/>
<android.support.design.widget.TabLayout
    android:id="@+id/tablayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"/>
</LinearLayout>

Then create a class that extends FragmentStatePagerAdapter to initialize the ViewPager with your initial Fragments:

public class CustomPagerAdapter extends FragmentStatePagerAdapter {
    private final List<String> tabTitles = new ArrayList<String>() {{
        add("Fragment 1");
        add("Fragment 4");
        add("Fragment 7");
    }};

    private List<Fragment> tabs = new ArrayList<>();

    public CustomPagerAdapter(FragmentManager fragmentManager) {
        super(fragmentManager);

        initializeTabs();
    }

    private void initializeTabs() {
        tabs.add(HostFragment.newInstance(new Fragment1()));
        tabs.add(HostFragment.newInstance(new Fragment4()));
        tabs.add(HostFragment.newInstance(new Fragment7()));
    }

    @Override
    public Fragment getItem(int position) {
        return tabs.get(position);
    }

    @Override
    public int getCount() {
        return tabs.size();
    }

    @Override
    public CharSequence getPageTitle(int position) {
        return tabTitles.get(position);
    }
}

The fragments containing the actual contents should be wrapped by a HostFragment, that uses its child FragmentManager to replace the current fragment with a new one if you want to navigate, or to pop the last fragment from the fragment stack. Call its replaceFragment to navigate:

public class HostFragment extends BackStackFragment {
    private Fragment fragment;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        super.onCreateView(inflater, container, savedInstanceState);
        View view = inflater.inflate(R.layout.host_fragment, container, false);
        if (fragment != null) {
            replaceFragment(fragment, false);
        }
        return view;
    }

    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();
        }
    }

    public static HostFragment newInstance(Fragment fragment) {
        HostFragment hostFragment = new HostFragment();
        hostFragment.fragment = fragment;
        return hostFragment;
    }
}

The HostFragment should extend the abstract class BackstackFragment

public abstract class BackStackFragment extends Fragment {
    public static boolean handleBackPressed(FragmentManager fm)
    {
        if(fm.getFragments() != null){
            for(Fragment frag : fm.getFragments()){
                if(frag != null && frag.isVisible() && frag instanceof BackStackFragment){
                    if(((BackStackFragment)frag).onBackPressed()){
                        return true;
                    }
                }
            }
        }
        return false;
    }

    protected boolean onBackPressed()
    {
        FragmentManager fm = getChildFragmentManager();
        if(handleBackPressed(fm)){
            return true;
        } else if(getUserVisibleHint() && fm.getBackStackEntryCount() > 0){
            fm.popBackStack();
            return true;
        }
        return false;
    }
}

Wire everthing up in the MainActivity:

public class MainActivity extends AppCompatActivity {
    private CustomPagerAdapter customPagerAdapter;
    private ViewPager viewPager;
    private TabLayout tabLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        viewPager = (ViewPager) findViewById(R.id.viewpager);
        tabLayout = (TabLayout) findViewById(R.id.tablayout);
        customPagerAdapter = new CustomPagerAdapter(getSupportFragmentManager());

        // 2 is enough for us; increase if you have more tabs!
        viewPager.setOffscreenPageLimit(2);
        viewPager.setAdapter(customPagerAdapter);
        tabLayout.setupWithViewPager(viewPager);
    }

    @Override
    public void onBackPressed()
    {
        if(!BackStackFragment.handleBackPressed(getSupportFragmentManager())){
            super.onBackPressed();
        }
    }

    public void openNextFragment() {
        HostFragment hostFragment = (HostFragment) customPagerAdapter.getItem(viewPager.getCurrentItem());

        // your logic to change the fragments...
    }
}

This solution is certainly not perfect but gets the job done. Read more fluff about it in my blog post on the issue.

marktani
  • 7,578
  • 6
  • 37
  • 60
  • 1
    Perfect! I [added this](https://gyazo.com/8f55aeb51323113b093ad5c6e40ba2a7) to do `openNextFragment` more flexible. – Santiago Gil Aug 23 '16 at 20:55
0

You can change fragments inside page fragment. For example, TAB_A implements logic of picking fragment 1 or 2, and displaying picked fragment inside. Hierarchy looks like:

ViewPager -> TAB_A -> fragment 1 or 2 (displaying inside TAB_A).

Also you should use getChildFragmentManager() to manage fragments inside TAB_A.

Ufkoku
  • 2,384
  • 20
  • 44