java Android FragmentTabHost - 还没有完全成熟?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/13686282/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-31 13:36:38  来源:igfitidea点击:

Android FragmentTabHost - Not fully baked yet?

javaandroidandroid-fragmentsandroid-tabhost

提问by jamis0n

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

我想看看是否有人在使用新的 Android API 级别 17 附带的 FragmentTabHost 自定义选项卡方面取得了成功。

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.

我很高兴能够在我的 ViewPager SherlockFragments 中嵌套一个 tabHost,但是我在做一些简单的事情时遇到了麻烦,比如将选项卡移到底部或更改选项卡的布局。

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.

这是我在 Android 文档中可以找到的唯一示例,并且几乎没有描述它的用途。它似乎也忽略了布局中定义的任何内容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.

我想我的问题是,是否有人遇到过一个很好的教程 re:FragmentTabHost,或者他们是否知道如何 a) 将嵌套选项卡放在底部或 b) 更改所述选项卡的布局。

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

我已经尝试了所有常用的方法,但是由于 XML 布局文件似乎被覆盖了,所以我运气不佳。

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 codehas provided a suggestion to this:

在做了一些研究之后,在支持库中初始化 FragmentTabHost 时似乎存在问题。谷歌代码上的用户对此提出了建议:

FragmentTabHost.java

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:

片段的 XML 布局:

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

采纳答案by jamis0n

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.

我终于明白了这一点。FragmentTabHost.java 存在一个问题,它始终为您创建一个 TabHost 元素,无论您在 XML 中定义什么并事先进行膨胀。

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

因此,我在编写自己的 FragmentTabHost.java 版本时注释掉了那部分代码。

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

确保在您的 XML 布局中使用您的新版本, <com.example.app.MyFragmentTabHost

And of course inflate it:

当然,还要给它充气:

Fragment1.java:

片段1.java:

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

MyFragmentTabHost.java:

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

回答by nfirex

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). ViewGroupin constructor just init variables, and set mChildrenCount = 0(grepcode).

我认为,将方法设置initFragmentTabHost()为构造函数是错误的。那时 TabHost 没有他的孩子 - 它发生在之后。LinearLayout例如,在onMeasure()方法(grepcode)中与他的孩子一起工作。ViewGroup在构造函数中只是初始化变量,然后设置mChildrenCount = 0grepcode)。

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

所有我能做的,只是服装化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):

和服装化Tabs(标签高度有问题,我在代码中解决了它们):

<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 TabWidgetwe can do only with programmatically manipulating, like this:

我认为TabWidget我们的其他自定义只能通过编程操作来完成,如下所示:

    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.

恕我直言,这不好。

回答by Luciano Rodríguez

as far as i tested jamisOnsolution 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...

据我测试jamisOn解决方案是好的。重要的是不要使用其构造函数初始化 MyFragmentTabHost。至少如果持有 MyFragmentTabHost 的类是一个片段。我还没有用 FragmentActivity 测试过...

回答by foo64

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

我想提及更多有关 FragmentTabHost 的问题。我正在使用ViewPager,其中每个页面(视图)都包含一个 FragmenTabHost,我必须克服几个问题:

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...

1) FragmentTabHost 假定它是其父 FragmentManager 中唯一的 FragmentTabHost(到 的第二个参数FragmentTabHost.setup())。这导致了其余的问题......

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 reusethose tabs. Yes, page 2 controls page 1...

2) 您在调用时提供的“标签”会addTab()直接传递给 FragmentManager,因此如果您只对所有页面使用硬编码标签(这是完全合理的做法),您的第一页将创建标签片段,而其他所有页面都将重复使用那些标签。是的,第 2 页控制第 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.

3) 所有选项卡片段都被放置在一个仅由“视图 ID”标识的容器中(到的第三个参数FragmentTabHost.setup())。这意味着当 FragmentManager 将 viewId 解析为 View 时,它总是会找到第一个实例(从第一页开始)。您的所有其他页面都将被忽略。

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

解决方案是为您的“标签内容”视图分配唯一的 id,例如:

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.

4)它在销毁时不会删除标签片段。虽然 ViewPager 在您滑动时会破坏未使用的视图,但这些视图中包含的 FragmentTabHosts 会“泄漏”选项卡片段。当 ViewPager 重新实例化之前看到的页面(使用之前使用过的标签)时,FragmentTabHost 会注意到这些选项卡的片段已经存在并简单地重新附加它们。这会爆炸,因为片段指向已被 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

解决方法是在 FragmentTabHost 被销毁时移除碎片。您需要将此代码添加到onDetachedFromWindow()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() ... ).

您也可以通过使用 FragmentPagerAdapter 或 FragmentStatePagerAdapter(生成 Fragments)而不是标准 PagerAdapter(生成视图)来解决这些问题。然后你会打电话给FragmentTabHost.setup( ... fragment.getChildFragmentManager() ... )