Separate Back Navigation for a Tabbed View Pager in Android
Asked Answered
D

3

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.

Deposal answered 18/8, 2016 at 10:22 Comment(8)
What exactly happens in your case? Does the screen orientation repopulates all the fragments at initial state?Samal
@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.Deposal
@SantiGil you mean you want something like Flipboard app? tabs inside tabs?Indefatigable
@Indefatigable 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.Deposal
Edited question with more information of my problem.Deposal
I wrote a blog article on that topic a few months ago: medium.com/@nilan/… :)Wilfredwilfreda
@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.Deposal
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)); Wilfredwilfreda
W
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.

Wilfredwilfreda answered 23/8, 2016 at 20:47 Comment(1)
Perfect! I added this to do openNextFragment more flexible.Deposal
I
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();

            }
        }
Indefatigable answered 20/8, 2016 at 16:55 Comment(19)
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?Deposal
@SantiGil I assume your inner ViewPager have 3 pages. ( also by default setOffscreenPageLimit is 3 )Indefatigable
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 fragmentsDeposal
let me put some simple example in github may be it's better.Indefatigable
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)Deposal
@SantiGil github.com/AmirHadifar/InnerTabsExample this repository may gives you some Idea.Indefatigable
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?Deposal
@SantiGil I set todo in CategoryFramgent you should setCurrentPager in OnClickListener.Indefatigable
But if the fragment to open is not one in the viewpager? Just anotherDeposal
@SantiGil by this code it's not possible ( you need add some hack to do this)Indefatigable
How would be the code for what I'm trying? Still don't work :IDeposal
@SantiGil what have you trying so far ?Indefatigable
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.Deposal
In fragment_1 put a FrameLayout and add fragment with code like: getFragmentManager().beginTransaction() .replace(R.id.container, SimpleFragment.newInstance()) .commit();Indefatigable
@SantiGil Also for handling backPressed in inner fragment do this: https://mcmap.net/q/63955/-how-to-implement-onbackpressed-in-fragmentsIndefatigable
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.Deposal
@SantiGil it's not standard guideline So you should handle it with some hacks.Indefatigable
@SantiGil update my code a bit. it's kind of hacky but I think solve your issue.Indefatigable
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.Deposal
W
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.

Wilfredwilfreda answered 23/8, 2016 at 20:47 Comment(1)
Perfect! I added this to do openNextFragment more flexible.Deposal
D
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.

Decane answered 23/8, 2016 at 18:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.