Android FragmentTabHost - Not fully baked yet?
Asked Answered
C

4

18

I wanted to see if anyone has had success with customization of tabs using FragmentTabHost that comes with the new Android API level 17.

I was excited to be able to nest a tabHost within my ViewPager SherlockFragments, but I'm having trouble doing simple things like moving the tabs to the bottom or changing the layout of the tabs.

Has anyone seen a good example of using this functionality?

This is the only example I could find in the Android docs, and theres just about nothing that describes its use. It also seems to ignore whatever is defined in the layout for R.id.fragment1.

My question I suppose would be if anyone has come across a good tutorial re:FragmentTabHost or if they have an idea about how to a) put the nested tabs at the bottom or b) change the layout of said tabs.

I've tried all the usual methods, but since it appears the XML layout file is overridden, I haven't had much luck.

private FragmentTabHost mTabHost;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
        Bundle savedInstanceState) {

    setContentView(R.layout.fragment_tabs);
    mTabHost = (FragmentTabHost)findViewById(android.R.id.tabhost);
    mTabHost.setup(this, getSupportFragmentManager(), R.id.realtabcontent);

    mTabHost.addTab(mTabHost.newTabSpec("simple").setIndicator("Simple"),
            FragmentStackSupport.CountingFragment.class, null);
    mTabHost.addTab(mTabHost.newTabSpec("contacts").setIndicator("Contacts"),
            LoaderCursorSupport.CursorLoaderListFragment.class, null);
    mTabHost.addTab(mTabHost.newTabSpec("custom").setIndicator("Custom"),
            LoaderCustomSupport.AppListFragment.class, null);
    mTabHost.addTab(mTabHost.newTabSpec("throttle").setIndicator("Throttle"),
            LoaderThrottleSupport.ThrottledLoaderListFragment.class, null);

    return mTabHost;
}

After doing some research, it appears there may be a glitch with initializing the FragmentTabHost in the support library. The user here on Google code has provided a suggestion to this:

FragmentTabHost.java

private void initFragmentTabHost(Context context, AttributeSet attrs) {
    TypedArray a = context.obtainStyledAttributes(attrs,
            new int[] { android.R.attr.inflatedId }, 0, 0);
    mContainerId = a.getResourceId(0, 0);
    a.recycle();

    super.setOnTabChangedListener(this);

    // If owner hasn't made its own view hierarchy, then as a convenience
    // we will construct a standard one here.


/***** HERE COMMENT CODE BECAUSE findViewById(android.R.id.tabs) EVERY TIME IS NULL WE HAVE OWN         LAYOUT ******//


//        if (findViewById(android.R.id.tabs) == null) {
//            LinearLayout ll = new LinearLayout(context);
//            ll.setOrientation(LinearLayout.VERTICAL);
//            addView(ll, new FrameLayout.LayoutParams(
//                    ViewGroup.LayoutParams.FILL_PARENT,
//                    ViewGroup.LayoutParams.FILL_PARENT));
//
//            TabWidget tw = new TabWidget(context);
//            tw.setId(android.R.id.tabs);
//            tw.setOrientation(TabWidget.HORIZONTAL);
//            ll.addView(tw, new LinearLayout.LayoutParams(
//                    ViewGroup.LayoutParams.FILL_PARENT,
//                    ViewGroup.LayoutParams.WRAP_CONTENT, 0));
//
//            FrameLayout fl = new FrameLayout(context);
//            fl.setId(android.R.id.tabcontent);
//            ll.addView(fl, new LinearLayout.LayoutParams(0, 0, 0));
//
//            mRealTabContent = fl = new FrameLayout(context);
//            mRealTabContent.setId(mContainerId);
//            ll.addView(fl, new LinearLayout.LayoutParams(
//                    LinearLayout.LayoutParams.FILL_PARENT, 0, 1));
//        }
}

XML Layout for fragment:

<android.support.v4.app.FragmentTabHost
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/tabhost"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">  
    <FrameLayout
        android:id="@android:id/tabcontent"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_weight="0"/>
    <FrameLayout
        android:id="@+id/realtabcontent"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"/>
    <TabWidget
        android:id="@android:id/tabs"
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="0"/>

</LinearLayout>
</android.support.v4.app.FragmentTabHost>
Cazzie answered 3/12, 2012 at 15:23 Comment(0)
C
14

I finally got to the bottom of this. There is an issue with FragmentTabHost.java which will always create a TabHost element for you, no matter what you define in XML and inflate beforehand.

As such, I commented out that part of code when writing my own version of FragmentTabHost.java.

Make sure to use your new version of this in your XML layout, <com.example.app.MyFragmentTabHost

And of course inflate it:

Fragment1.java:

mTabHost = (MyFragmentTabHost) view.findViewById(android.R.id.tabhost);
mTabHost.setup(getActivity(), getChildFragmentManager(), android.R.id.tabcontent);

MyFragmentTabHost.java:

package com.example.app;

import java.util.ArrayList;

import android.content.Context;
import android.content.res.TypedArray;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.TabHost;

/**
 * Special TabHost that allows the use of {@link Fragment} objects for
 * its tab content.  When placing this in a view hierarchy, after inflating
 * the hierarchy you must call {@link #setup(Context, FragmentManager, int)}
 * to complete the initialization of the tab host.
 *
 */
public class MyFragmentTabHost extends TabHost
    implements TabHost.OnTabChangeListener {
private final ArrayList<TabInfo> mTabs = new ArrayList<TabInfo>();
private FrameLayout mRealTabContent;
private Context mContext;
private FragmentManager mFragmentManager;
private int mContainerId;
private TabHost.OnTabChangeListener mOnTabChangeListener;
private TabInfo mLastTab;
private boolean mAttached;

static final class TabInfo {
    private final String tag;
    private final Class<?> clss;
    private final Bundle args;
    private Fragment fragment;

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

static class SavedState extends BaseSavedState {
    String curTab;

    SavedState(Parcelable superState) {
        super(superState);
    }

    private SavedState(Parcel in) {
        super(in);
        curTab = in.readString();
    }

    @Override
    public void writeToParcel(Parcel out, int flags) {
        super.writeToParcel(out, flags);
        out.writeString(curTab);
    }

    @Override
    public String toString() {
        return "FragmentTabHost.SavedState{"
                + Integer.toHexString(System.identityHashCode(this))
                + " curTab=" + curTab + "}";
    }

    public static final Parcelable.Creator<SavedState> CREATOR
            = new Parcelable.Creator<SavedState>() {
        public SavedState createFromParcel(Parcel in) {
            return new SavedState(in);
        }

        public SavedState[] newArray(int size) {
            return new SavedState[size];
        }
    };
}

public MyFragmentTabHost(Context context) {
    // Note that we call through to the version that takes an AttributeSet,
    // because the simple Context construct can result in a broken object!
    super(context, null);
    initFragmentTabHost(context, null);
}

public MyFragmentTabHost(Context context, AttributeSet attrs) {
    super(context, attrs);
    initFragmentTabHost(context, attrs);
}

private void initFragmentTabHost(Context context, AttributeSet attrs) {
    TypedArray a = context.obtainStyledAttributes(attrs,
            new int[] { android.R.attr.inflatedId }, 0, 0);
    mContainerId = a.getResourceId(0, 0);
    a.recycle();

    super.setOnTabChangedListener(this);


    /*** REMOVE THE REST OF THIS FUNCTION ***/
    /*** findViewById(android.R.id.tabs) IS NULL EVERY TIME ***/
}

/**
 * @deprecated Don't call the original TabHost setup, you must instead
 * call {@link #setup(Context, FragmentManager)} or
 * {@link #setup(Context, FragmentManager, int)}.
 */
@Override @Deprecated
public void setup() {
    throw new IllegalStateException(
            "Must call setup() that takes a Context and FragmentManager");
}

public void setup(Context context, FragmentManager manager) {
    super.setup();
    mContext = context;
    mFragmentManager = manager;
    ensureContent();
}

public void setup(Context context, FragmentManager manager, int containerId) {
    super.setup();
    mContext = context;
    mFragmentManager = manager;
    mContainerId = containerId;
    ensureContent();
    mRealTabContent.setId(containerId);

    // We must have an ID to be able to save/restore our state.  If
    // the owner hasn't set one at this point, we will set it ourself.
    if (getId() == View.NO_ID) {
        setId(android.R.id.tabhost);
    }
}

private void ensureContent() {
    if (mRealTabContent == null) {
        mRealTabContent = (FrameLayout)findViewById(mContainerId);
        if (mRealTabContent == null) {
            throw new IllegalStateException(
                    "No tab content FrameLayout found for id " + mContainerId);
        }
    }
}

@Override
public void setOnTabChangedListener(OnTabChangeListener l) {
    mOnTabChangeListener = l;
}

public void addTab(TabHost.TabSpec tabSpec, Class<?> clss, Bundle args) {
    tabSpec.setContent(new DummyTabFactory(mContext));
    String tag = tabSpec.getTag();

    TabInfo info = new TabInfo(tag, clss, args);

    if (mAttached) {
        // If we are already attached to the window, then check to make
        // sure this tab's fragment is inactive if it exists.  This shouldn't
        // normally happen.
        info.fragment = mFragmentManager.findFragmentByTag(tag);
        if (info.fragment != null && !info.fragment.isDetached()) {
            FragmentTransaction ft = mFragmentManager.beginTransaction();
            ft.detach(info.fragment);
            ft.commit();
        }
    }

    mTabs.add(info);
    addTab(tabSpec);
}

@Override
protected void onAttachedToWindow() {
    super.onAttachedToWindow();

    String currentTab = getCurrentTabTag();

    // Go through all tabs and make sure their fragments match
    // the correct state.
    FragmentTransaction ft = null;
    for (int i=0; i<mTabs.size(); i++) {
        TabInfo tab = mTabs.get(i);
        tab.fragment = mFragmentManager.findFragmentByTag(tab.tag);
        if (tab.fragment != null && !tab.fragment.isDetached()) {
            if (tab.tag.equals(currentTab)) {
                // The fragment for this tab is already there and
                // active, and it is what we really want to have
                // as the current tab.  Nothing to do.
                mLastTab = tab;
            } else {
                // This fragment was restored in the active state,
                // but is not the current tab.  Deactivate it.
                if (ft == null) {
                    ft = mFragmentManager.beginTransaction();
                }
                ft.detach(tab.fragment);
            }
        }
    }

    // We are now ready to go.  Make sure we are switched to the
    // correct tab.
    mAttached = true;
    ft = doTabChanged(currentTab, ft);
    if (ft != null) {
        ft.commit();
        mFragmentManager.executePendingTransactions();
    }
}

@Override
protected void onDetachedFromWindow() {
    super.onDetachedFromWindow();
    mAttached = false;
}

@Override
protected Parcelable onSaveInstanceState() {
    Parcelable superState = super.onSaveInstanceState();
    SavedState ss = new SavedState(superState);
    ss.curTab = getCurrentTabTag();
    return ss;
}

@Override
protected void onRestoreInstanceState(Parcelable state) {
    SavedState ss = (SavedState)state;
    super.onRestoreInstanceState(ss.getSuperState());
    setCurrentTabByTag(ss.curTab);
}

@Override
public void onTabChanged(String tabId) {
    if (mAttached) {
        FragmentTransaction ft = doTabChanged(tabId, null);
        if (ft != null) {
            ft.commit();
        }
    }
    if (mOnTabChangeListener != null) {
        mOnTabChangeListener.onTabChanged(tabId);
    }
}

private FragmentTransaction doTabChanged(String tabId, FragmentTransaction ft) {
    TabInfo newTab = null;
    for (int i=0; i<mTabs.size(); i++) {
        TabInfo tab = mTabs.get(i);
        if (tab.tag.equals(tabId)) {
            newTab = tab;
        }
    }
    if (newTab == null) {
        throw new IllegalStateException("No tab known for tag " + tabId);
    }
    if (mLastTab != newTab) {
        if (ft == null) {
            ft = mFragmentManager.beginTransaction();
        }
        if (mLastTab != null) {
            if (mLastTab.fragment != null) {
                ft.detach(mLastTab.fragment);
            }
        }
        if (newTab != null) {
            if (newTab.fragment == null) {
                newTab.fragment = Fragment.instantiate(mContext,
                        newTab.clss.getName(), newTab.args);
                ft.add(mContainerId, newTab.fragment, newTab.tag);
            } else {
                ft.attach(newTab.fragment);
            }
        }

        mLastTab = newTab;
    }
    return ft;
}
}
Cazzie answered 12/12, 2012 at 15:12 Comment(5)
If you downvote my answer, please leave a comment for discussion!Cazzie
So, did you happen to track down WHY findViewById(android.R.id.tabs) would return null? In a very sort of general sense, that ID exists and should be found...Vaudeville
@Cazzie not working if you use it in a FragmentActivity and not Fragment Same stack trace received: java.lang.IllegalStateException: No tab known for tag null :(Ptolemaist
@jamis0n, yup happens to me too, in a FragmentActivity "No tab known for tag null" strikes back... Any solution?Wendling
Anyody solved "No tab known for tag null" ? Please help guys. :'(Thirsty
A
2

I think, it was a mistake to set method initFragmentTabHost() to constructor. At that time TabHost don't his children - it happens after. LinearLayout, for example, work with his children in onMeasure() method (grepcode). ViewGroup in constructor just init variables, and set mChildrenCount = 0 (grepcode).

All what I can did, it's only costumize FragmentTabHost:

<android.support.v4.app.FragmentTabHost xmlns:a="http://schemas.android.com/apk/res/android"
    a:id="@android:id/tabhost"
    style="@style/Widget.TabHost"
    a:inflatedId="@+id/content" />

And costumize Tabs (have problems with tab heights, I solve them in code):

<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
    style="@style/Widget.Tab" >
    <TextView
        a:id="@android:id/title"
        style="@style/Widget.TabTitle" />
</LinearLayout>

In code:

    tabSpec = mTabHost.newTabSpec(tag).setIndicator(createTab(caption));

...

    private View createTab(CharSequence title) {
        final View v = View.inflate(getActivity(), LAYOUT_TAB, null);
        ((TextView) v.findViewById(android.R.id.title)).setText(title);
        return v;
    }

I think other customization with TabWidget we can do only with programmatically manipulating, like this:

    final View tabs = (TabWidget) mTabHost.findViewById(android.R.id.tabs);
    final ViewGroup parent = (ViewGroup) mTabHost.getChildAt(0); 
    parent.removeView(tabs);
    parent.addView(tabs);

IMHO, this is not good.

Amalbergas answered 5/12, 2012 at 10:8 Comment(0)
H
0

as far as i tested jamisOn solution is good. It is important to not initialize MyFragmentTabHost with its constructor. At least if the class holding the MyFragmentTabHost is a fragment. I haven`t tested with a FragmentActivity...

Hamamelidaceous answered 15/4, 2013 at 19:27 Comment(0)
M
0

I'd like to mention some more issues with FragmentTabHost. I'm using a ViewPager where each page (View) contains a FragmenTabHost and I had to overcome several problems:

1) FragmentTabHost assumes that it's the only FragmentTabHost in its parent FragmentManager (2nd argument to FragmentTabHost.setup()). This causes the rest of the problems...

2) the "tags" you provide when calling addTab() are passed straight through to the FragmentManager, so if you just use hardcoded tags for all your pages (a perfectly reasonable thing to do) your first page will create tab fragments while every other page will reuse those tabs. Yes, page 2 controls page 1...

Solution is to generate unique tag names. I appended the page number to the hardcoded strings:

public Object instantiateItem( ViewGroup container, int position )
{
    ...
    tabHost.addTab( tabHost.newTabSpec( "tab1_" + position ) ...);
    tabHost.addTab( tabHost.newTabSpec( "tab2_" + position ) ...);
    tabHost.addTab( tabHost.newTabSpec( "tab3_" + position ) ...);
    ...
}

3) All tab fragments get placed in a container identified only by "view id" (the 3rd argument to FragmentTabHost.setup()). This means that when the FragmentManager resolves the viewId to a View, it always finds the first instance (from the first page). All your other pages are ignored.

Solution to this is to assign unique ids to your "tab content" views, for example:

public Object instantiateItem( ViewGroup container, int position )
{
    View view = m_inflater.inflate(R.layout.page, null);

    View tabContent = view.findViewById(R.id.realtabcontent);
    tabContent.setId(m_nextViewId);
    m_nextViewId++;

    MyFragmentTabHost tabHost = (MyFragmentTabHost) view.findViewById(android.R.id.tabhost);
    tabHost.setup(m_activity, m_activity.getSupportFragmentManager(), tabContent.getId());
    ...
}

4) It doesn't remove tab fragments when destroyed. While the ViewPager destroys unused Views as you swipe, the FragmentTabHosts contained within those views "leak" the tab fragments. When the ViewPager re-instantiates a previously seen page (using previously used tags), FragmentTabHost will notice that the fragments for those tabs already exist and simply reattach them. This blows up because the fragments point to views that have been destroyed by the ViewPager.

The solution is to remove fragments when FragmentTabHost is destroyed. You'll want to add this code to onDetachedFromWindow() in your local copy of FragmentTabHost.java

class MyFragmentTabHost
{
    ...

    protected void onDetachedFromWindow()
    {
        super.onDetachedFromWindow();
        mAttached = false;

        boolean removeFragments = false;
        if( mContext instanceof Activity )
        {
            Activity activity = (Activity)mContext;
            removeFragments = !activity.isDestroyed();
        }

        if( removeFragments )
        {
            FragmentTransaction ft = null;
            for (int i = 0; i < mTabs.size(); i++)
            {
                TabInfo tab = mTabs.get(i);
                if (tab.fragment != null)
                {
                    if (ft == null)
                    {
                        ft = mFragmentManager.beginTransaction();
                    }
                    ft.remove(tab.fragment);
                }
            }

            if (ft != null)
            {
                ft.commit();
                mFragmentManager.executePendingTransactions();
            }
        }
    }

You could probably also work around these issues by using a FragmentPagerAdapter or FragmentStatePagerAdapter (makes Fragments) instead of a standard PagerAdapter (makes Views). Then you'd call FragmentTabHost.setup( ... fragment.getChildFragmentManager() ... ).

Massa answered 5/5, 2013 at 4:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.