在Android 4.0、4.1(<4.2)中,嵌套fragment的最佳实践(不使用支持库)。

115
我正在编写一款适用于4.0和4.1平板的应用程序,如果不需要,我不想使用支持库,而是只使用4.x API。
因此,我的目标平台非常明确:>= 4.0并且<= 4.1。
该应用具有多窗格布局(两个片段,一个小片段在左侧,一个内容片段在右侧),以及带有选项卡的操作栏。
类似于这样:

enter image description here

点击操作栏上的选项卡会改变“外部”片段,然后内部片段是具有两个嵌套片段(1. 小左侧列表片段,2. 宽内容片段)的片段。
我现在想知道替换片段,特别是嵌套片段的最佳实践。ViewPager 是支持库的一部分,没有本机 4.x 的此类替代品。在我的意识中似乎已经过时了。 - http://developer.android.com/reference/android/support/v4/view/ViewPager.html 然后我阅读了 Android 4.2 的发行说明,涉及到 ChildFragmentManager,这将是一个很好的选择,但我正在针对 4.0 和 4.1,所以也无法使用它。
ChildFragmentManager 仅在 4.2 中可用。 很不幸,即使在整个Android开发指南中,也几乎找不到任何关于如何最佳实践地使用片段而不使用支持库的好例子,特别是关于嵌套片段的部分。
所以我想知道:是否没有使用支持库和随之而来的所有内容就无法编写带有嵌套片段的4.1应用程序?(需要使用FragmentActivity而不是Fragment等)。或者什么是最佳实践?
我目前在开发中遇到的问题就是这个陈述: Android支持库现在也支持嵌套片段,因此您可以在Android 1.6及更高版本上实现嵌套片段设计。
注意:当布局包括时,您无法将布局填充到片段中。仅在动态添加到片段时支持嵌套片段。
由于我在XML中定义了嵌套片段,这显然会导致错误,如下所示:
Caused by: java.lang.IllegalArgumentException: Binary XML file line #15: Duplicate id 0x7f090009, tag frgCustomerList, or parent id 0x7f090008 with another fragment for de.xyz.is.android.fragment.CustomerListFragment_

目前,我得出结论:即使在4.1上,当我甚至不想针对2.x平台时,如屏幕截图所示的嵌套片段也无法在没有支持库的情况下实现。
(这可能实际上更像是一个维基条目而不是一个问题,但也许其他人之前已经成功了。)
更新:
有一个有用的答案在:Fragment Inside Fragment

22
你有三种选择:1. 仅针对4.2使用本地嵌套片段。2. 使用支持库中的嵌套片段来针对4.x。3. 不要在其他平台目标场景中使用嵌套片段。这应该回答了你的问题。此外,你不能在xml布局中嵌套片段,所有片段必须在代码中添加。几乎没有好的示例展示不使用支持库的最佳做法,支持片段框架复制了本地片段框架,因此任何示例都可以使用其中的一种方式工作。 - user
2
我不熟悉Galaxy Tab上的联系人应用程序,但请记住,您始终可以面对三星公司内部构建的自定义ActionBar实现。请仔细查看ActionBarSherlock,如果有空间,它在ActionBar中具有选项卡。 - user
4
@Luksprog,我相信你已经给出了唯一可以给出的答案,你能否把它作为正式答案提供。 - Warpzit
1
@Pork 我提出问题的主要原因是:有没有关于嵌套片段的解决方法,而不必使用支持库及其它视图元素。也就是说,如果我切换到支持库,我将使用 FragmentActivity 而不是 Fragment。但我想使用 Fragment,我只需要替换 嵌套片段,而不是所有的 v4 组件。例如,上面的截图运行在 4.0 上,我想知道他们是否正在使用 ABS、SupportLib 或其他任何开源库等。 - Mathias Conradt
ABS是建立在支持库之上的... 如果你担心膨胀,那就需要使用Proguard。 - Pork 'n' Bunny
显示剩余4条评论
5个回答

61

限制

无论您使用哪个版本的FragmentManager,在xml中嵌套一个片段到另一个片段是不可能的。

因此,您必须通过代码添加片段。这可能看起来像一个问题,但从长远来看,它使您的布局非常灵活。

没有使用getChildFragmentManager进行嵌套吗?childFragmentManager背后的本质是它会推迟加载,直到上一个片段事务完成。当然,它仅在4.2或支持库中得到自然支持。

嵌套而不使用ChildManager - 解决方案

解决方案,当然可以!我已经这样做很长时间了(自从ViewPager被宣布以来)。

请参见下面;这是一个延迟加载的Fragment,所以Fragment可以在其中加载。

它非常简单,Handler是一个非常有用的类,实际上,处理程序在当前片段事务完成提交后等待在主线程上执行的空间(因为片段会影响UI,它们运行在主线程上)。

// Remember this is an example, you will need to modify to work with your code
private final Handler handler = new Handler();
private Runnable runPager;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
    return inflater.inflate(R.layout.frag_layout, container, false);
}

@Override
public void onActivityCreated(Bundle savedInstanceState)
{
    super.onActivityCreated(savedInstanceState);
    runPager = new Runnable() {

        @Override
        public void run()
        {
          getFragmentManager().beginTransaction().addFragment(R.id.frag_container, MyFragment.newInstance()).commit();
        }
    };
    handler.post(runPager);
}

/**
 * @see android.support.v4.app.Fragment#onPause()
 */
@Override
public void onPause()
{
    super.onPause();
    handler.removeCallbacks(runPager);
}

我不认为这是“最佳实践”,但我已经在使用这个技巧的现有应用程序中,而且我还没有遇到任何问题。

我也使用这种方法来嵌入视图翻页 - https://gist.github.com/chrisjenx/3405429


你如何处理嵌套片段的布局? - pablisco
我能想到的唯一途径是使用CustomLayoutInflater。当你遇到“fragment”元素时,你可以重写其超类实现并尝试自行解析/膨胀它。但这需要非常大的努力,已经超出了StackOverflow问题的范围。 - Chris.Jenkins
大家好,有人能帮我解决这个问题吗?我真的卡住了... http://stackoverflow.com/questions/32240138/oncreateview-of-nested-fragment-is-not-called - Nicks

2
在API 17之前,最好的方法是根本不要这样做。尝试实现这种行为会引起问题。但这并不意味着无法使用当前的API 14欺骗性地实现它。我所做的是:

1-查看片段之间的通信http://developer.android.com/training/basics/fragments/communicating.html

2-将您现有片段中的布局xml FrameLayout移动到Activity布局中,并通过给出高度为0来隐藏它:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:tools="http://schemas.android.com/tools"
          android:layout_width="fill_parent"
          android:layout_height="fill_parent">
<FrameLayout android:id="@+id/content"
          android:layout_width="300dp"
          android:layout_height="match_parent" />


<FrameLayout android:id="@+id/lstResults"
             android:layout_width="300dp"
             android:layout_height="0dp"
             android:layout_below="@+id/content"
             tools:layout="@layout/treeview_list_content"/>


<FrameLayout android:id="@+id/anomalies_fragment"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
        android:layout_toRightOf="@+id/content" />

3 - 在父Fragment中实现接口。
    OnListener mCallback;

// Container Activity must implement this interface
public interface OnListener 
{
    public void onDoSomethingToInitChildFrame(/*parameters*/);
    public void showResults();
    public void hideResults();
}

@Override
public void onAttach(Activity activity) {
    super.onAttach(activity);

    // This makes sure that the container activity has implemented
    // the callback interface. If not, it throws an exception
    try {
        mCallback = (OnFilterAppliedListener) activity;
    } catch (ClassCastException e) {
        throw new ClassCastException(activity.toString()
                + " must implement OnListener");
    }
}

@Override
public void onActivityCreated(Bundle savedInstanceState) 
{
    super.onActivityCreated(savedInstanceState);

    mCallback.showResults();
}

@Override
public void onPause()
{
    super.onPause();

    mCallback.hideResults();
}

public void onClickButton(View view)
{
    // do click action here

    mCallback.onDoSomethingToInitChildFrame(/*parameters*/);
}

将其翻译为中文:

4 - 在父Activity中实现接口

public class YourActivity extends Activity implements yourParentFragment.OnListener {

public void onDoSomethingToInitChildFrame(/*parameters*/)
{
    FragmentTransaction ft = getFragmentManager().beginTransaction();
    Fragment childFragment = getFragmentManager().findFragmentByTag("Results");
    if(childFragment == null)
    {
        childFragment = new yourChildFragment(/*parameters*/);
        ft.add(R.id.lstResults, childFragment, "Results");
    }
    else
    {
        ft.detach(childFragment);

        ((yourChildFragment)childFragment).ResetContent(/*parameters*/);

        ft.attach(childFragment);
    }
    ft.commit();

    showResultsPane();
}

public void showResults()
{
    FragmentTransaction ft = getFragmentManager().beginTransaction();
    Fragment childFragment = getFragmentManager().findFragmentByTag("Results");
    if(childFragment != null)
        ft.attach(childFragment);
    ft.commit();

    showResultsPane();
}

public void showResultsPane()
{
    //resize the elements to show the results pane
    findViewById(R.id.content).getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
    findViewById(R.id.lstResults).getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
}

public void hideResults()
{
    //resize the elements to hide the results pane
    findViewById(R.id.content).getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
    findViewById(R.id.lstResults).getLayoutParams().height = 0;

    FragmentTransaction ft = getFragmentManager().beginTransaction();
    Fragment childFragment = getFragmentManager().findFragmentByTag("Results");
    if(childFragment != null)
        ft.detach(childFragment);
    ft.commit();
}

}

5-享受吧,使用这种方法可以在API 17之前的环境中获得与getChildFragmentManager()函数相同的流畅功能。正如你所注意到的,子片段不再是父片段的真正子项,而是活动的子项,这确实无法避免。

1
由于NavigationDrawer、TabHost和ViewPager的结合使用,我不得不处理这个确切问题,因为由于TabHost的使用,对支持库的使用存在一些复杂性。而且我还必须支持JellyBean 4.1的min API,因此使用getChildFragmentManager来嵌套片段不是一个选项。
所以我的问题可以简化为...
TabHost(用于顶级)
+ ViewPager(仅用于顶级选项卡中的一个)
= 需要嵌套片段(JellyBean 4.1不支持)
我的解决方案是创建嵌套片段的幻象,而不实际嵌套片段。我通过让主活动使用TabHost和ViewPager来管理两个同级视图,通过在0和1之间切换layout_weight来管理它们的可见性。
//Hide the fragment used by TabHost by setting height and weight to 0
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0, 0);
mTabHostedView.setLayoutParams(lp);
//Show the fragment used by ViewPager by setting height to 0 but weight to 1
lp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0, 1);
mPagedView.setLayoutParams(lp);

这实际上允许我的虚假的“嵌套片段”作为独立视图运行,只要我手动管理相关的布局权重。

这是我的activity_main.xml:

<android.support.v4.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.ringofblades.stackoverflow.app.MainActivity">

    <TabHost
        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:background="@drawable/background_image"
                android:layout_width="match_parent"
                android:layout_weight="0.5"
                android:layout_height="0dp"/>
            <android.support.v4.view.ViewPager
                xmlns:tools="http://schemas.android.com/tools"
                android:id="@+id/pager"
                android:background="@drawable/background_image"
                android:layout_width="match_parent"
                android:layout_weight="0.5"
                android:layout_height="0dp"
                tools:context="com.ringofblades.stackoverflow.app.MainActivity">
                <FrameLayout
                    android:id="@+id/container"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent" />
            </android.support.v4.view.ViewPager>
            <TabWidget android:id="@android:id/tabs"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />
        </LinearLayout>
    </TabHost>

    <fragment android:id="@+id/navigation_drawer"
        android:layout_width="@dimen/navigation_drawer_width"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:name="com.ringofblades.stackoverflow.app.NavigationDrawerFragment"
        tools:layout="@layout/fragment_navigation_drawer" />
</android.support.v4.widget.DrawerLayout>

请注意,“@+id/pager”和“@+id/container”是具有“android:layout_weight =“0.5””和“android:layout_height =“0dp””的兄弟节点。这样我就可以在任何屏幕大小的预览器中看到它。它们的权重将在运行时通过代码进行操作。

据我所记,使用ActionBar中的选项卡的一个缺点是它会自动决定将它们显示为下拉菜单(在小屏幕上),这对我来说不是很好。但我并不100%确定。 - WindRider
@Igor,我在SO上看到过一篇文章,说使用带有Navigation DrawerActionBar选项卡不好,因为它会自动将选项卡放在抽屉视图之上。很抱歉,我没有链接来支持这个说法。 - Azurespot
@NoniA。无论如何,TabHost已经过时了。现在请使用Material Design和Toolbar! - IgorGanapolsky
1
@NoniA。http://blog.xamarin.com/android-tips-hello-toolbar-goodbye-action-bar/ 你有在关注Android的开发吗? - IgorGanapolsky
1
哇,谢谢你的链接 @Igor!我一定会查看这个的。我还是个初学者,在 Android 方面有无数其他事情要学习,但这个似乎像宝石一样!再次感谢。 - Azurespot
显示剩余2条评论

1
尽管 OP 可能有特殊情况不能使用 Support Library,但大多数人应该使用它。Android 文档建议使用它,这样可以让你的应用程序面向最广泛的受众。在 此处阅读我的详细回答 中,我提供了一个示例,演示如何使用支持库中的嵌套片段。

enter image description here


我是OP。不使用支持库的原因是因为这是一款公司内部应用程序,所使用的硬件明确定义为>=4.0和<=4.1。没有必要面向广泛的受众,只是内部员工使用,并且没有意图在公司外部使用该应用程序。支持库的唯一原因是向后兼容 - 但人们期望你可以在没有它的情况下实现支持库所能做的一切。为什么“本地”更高版本的功能会比仅用于向下兼容的支持库少呢? - Mathias Conradt
1
当然,你可以使用支持库,我也可以。只是不明白为什么Google只在支持库中提供某些功能,而不是在外部提供,或者为什么他们会称其为支持库,如果它是最佳实践的话,为什么不将其作为总体标准呢?这里有一篇关于支持库的好文章:http://martiancraft.com/blog/2015/06/android-support-library/ - Mathias Conradt
@Mathias,不错的文章。 - Suragch

1
在 @Chris.Jenkins 的回答基础上,这是对我而言一直有效的解决方案,用于在生命周期事件中删除片段(这些事件往往会引发 IllegalStateExceptions 异常)。它使用了 Handler 方法和 Activity.isFinishing() 检查的组合(否则会因“无法在 onSaveInstanceState 后执行此操作”而抛出错误)。
import android.app.Activity;
import android.os.Handler;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;

public abstract class BaseFragment extends Fragment {
    private final Handler handler = new Handler();

    /**
     * Removes the {@link Fragment} using {@link #getFragmentManager()}, wrapped in a {@link Handler} to
     * compensate for illegal states.
     *
     * @param fragment The {@link Fragment} to schedule for removal.
     */
    protected void removeFragment(@Nullable final Fragment fragment) {
        if (fragment == null) return;

        final Activity activity = getActivity();
        handler.post(new Runnable() {
            @Override
            public void run() {
                if (activity != null && !activity.isFinishing()) {
                    getFragmentManager().beginTransaction()
                            .remove(fragment)
                            .commitAllowingStateLoss();
                }
            }
        });
    }

    /**
     * Removes each {@link Fragment} using {@link #getFragmentManager()}, wrapped in a {@link Handler} to
     * compensate for illegal states.
     *
     * @param fragments The {@link Fragment}s to schedule for removal.
     */
    protected void removeFragments(final Fragment... fragments) {
        final FragmentManager fragmentManager = getFragmentManager();
        final FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

        for (Fragment fragment : fragments) {
            if (fragment != null) {
                fragmentTransaction.remove(fragment);
            }
        }

        final Activity activity = getActivity();
        handler.post(new Runnable() {
            @Override
            public void run() {
                if (activity != null && !activity.isFinishing()) {
                    fragmentTransaction.commitAllowingStateLoss();
                }
            }
        });
    }
}

使用方法:

class MyFragment extends Fragment {
    @Override
    public void onDestroyView() {
        removeFragments(mFragment1, mFragment2, mFragment3);
        super.onDestroyView();
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接