不使用支持库的 Android 4.0、4.1 (<4.2) 中嵌套片段的最佳实践

Posted

技术标签:

【中文标题】不使用支持库的 Android 4.0、4.1 (<4.2) 中嵌套片段的最佳实践【英文标题】:Best practice for nested fragments in Android 4.0, 4.1 (<4.2) without using the support library 【发布时间】:2013-03-14 14:06:16 【问题描述】:

我正在为 4.0 和 4.1 平板电脑编写应用程序,我不想使用支持库(如果不需要的话),因此只能使用 4.x api。

所以我的目标平台非常明确地定义为:>= 4.0 和

该应用具有多窗格布局(两个片段,左侧一个小片段,右侧一个内容片段)和一个带有选项卡的操作栏。

与此类似:

单击操作栏上的选项卡会更改“外部”片段,然后内部片段是具有两个嵌套片段的片段(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 中可用

http://developer.android.com/about/versions/android-4.2.html#NestedFragments http://developer.android.com/reference/android/app/Fragment.html#getChildFragmentManager()

不幸的是,即使在整个 Android 开发人员指南中,也几乎没有任何好的示例可以展示在没有支持库的情况下使用 Fragment 的最佳实践;尤其是与嵌套片段无关。

所以我想知道:如果不使用支持库和它附带的所有东西,就不可能编写带有嵌套片段的 4.1 应用程序吗? (需要使用 FragmentActivity 而不是 Fragment 等?) 或者最佳做法是什么?


我目前在开发中遇到的问题正是这样的说法:

http://developer.android.com/about/versions/android-4.2.html#NestedFragments

Android 支持库现在也支持嵌套片段,因此您 可以在 Android 1.6 及更高版本上实现嵌套片段设计。

注意:当该布局时,您不能将布局膨胀为片段 包括&lt;fragment&gt;。仅在添加时才支持嵌套片段 动态到一个片段。

因为我在 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 平台时,如果没有支持库,截图中所示的嵌套片段是不可能的。

(这实际上可能更像是一个 wiki 条目而不是一个问题,但也许其他人之前已经管理过它)。

更新:

一个有用的答案是:Fragment Inside Fragment

【问题讨论】:

您有三个选择: 1. 仅针对具有原生嵌套片段的 4.2。 2. 目标 4.x 带有来自支持库的嵌套片段 3. 不要将嵌套片段用于任何其他平台目标场景。这应该可以回答您的问题。此外,您不能使用嵌入在 xml 布局中的嵌套片段,所有这些都必须添加到代码中。 在没有支持库的情况下,几乎没有任何好的示例展示片段使用的最佳实践 - 支持片段框架复制了原生框架,因此任何示例都应该以任何方式工作。 @Luksprog 感谢您的 cmets。我更喜欢您的解决方案 2,并且框架在支持库中运行良好,但 ActionBar 中的选项卡不能 - afaik,我需要使用 ActionBarSherlock,但选项卡不会集成到 ActionBar 中,而是仅集成在下方(这不是t 4.x 所必需的)。而且 ActionBar.TabListener 只支持来自 android.app.Fragment 的 Fragments,不支持来自支持库的片段。 我不熟悉 Galaxy 选项卡上的联系人应用程序,但请记住,您总是会遇到 ActionBar(三星内部构建)的自定义实现。仔细看看 ActionBarSherlock,如果有空间,它在 ActionBar 中有标签。 @Luksprog 我相信您已经提供了唯一要给出的答案,请您将其作为正确答案提供。 @Pork 我提出这个问题的主要原因是:嵌套片段是否有任何解决方法,而无需使用支持库及其所有其他视图元素。这意味着,如果我切换到支持库,我将使用 FragmentActivity 而不是 Fragment。但我确实想使用 Fragment,我想要的只是替换 Nested Fragments,但不是所有 v4 组件。 IE。通过其他开源库等。例如上面的屏幕截图在 4.0 上运行,我想知道他们是否使用 ABS、SupportLib 或其他任何东西。 【参考方案1】:

限制

因此,无论您使用哪个版本的FragmentManager,xml 都无法将片段嵌套在另一个片段中。

所以你必须通过代码添加片段,这似乎是个问题,但从长远来看,你的布局会变得超级灵活。

所以嵌套不使用getChildFragmentMangerchildFragmentManager 背后的本质是它延迟加载直到前一个片段事务完成。当然,它只在 4.2 或支持库中自然得到支持。

没有 ChildManager 的嵌套 - 解决方案

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

见下文;这是一个延迟加载的Fragment,因此可以在其中加载Fragments。

它非常简单,Handler 是一个非常方便的类,有效地处理程序在当前片段事务完成提交后等待主线程上执行的空间(因为片段会干扰它们在主线程)。

// 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);

我不认为这是“最佳实践”,但我有使用此 hack 的实时应用程序,我还没有遇到任何问题。

我也使用这种方法嵌入视图寻呼机 - https://gist.github.com/chrisjenx/3405429

【讨论】:

如何处理布局嵌套片段? 我可以看到这个工作的唯一方法是使用 CustomLayoutInflater,当您遇到 fragment 元素时,您将覆盖超级实现并尝试自己解析/膨胀它。但这将是很多努力,超出了 *** 问题的范围。 您好,谁能帮我解决这个问题?我真的被困住了..***.com/questions/32240138/…【参考方案2】:

在 API 17 之前的版本中执行此操作的最佳方法是根本不执行此操作。试图实现这种行为会导致问题。然而,这并不是说使用当前的 API 14 无法令人信服地伪造它。我所做的如下:

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

2 - 将您的布局 xml FrameLayout 从您现有的 Fragment 移动到 Activity 布局并通过将高度设置为 0 来隐藏它:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:tools="http://schemas.android.com/tools"
          android:layout_
          android:layout_>
<FrameLayout android:id="@+id/content"
          android:layout_
          android:layout_ />


<FrameLayout android:id="@+id/lstResults"
             android:layout_
             android:layout_
             android:layout_below="@+id/content"
             tools:layout="@layout/treeview_list_content"/>


<FrameLayout android:id="@+id/anomalies_fragment"
             android:layout_
             android:layout_
        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中实现接口

公共类 YourActivity 扩展 Activity 实现 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() 函数相同的流畅功能。您可能已经注意到子 Fragment 不再是父 Fragment 的子,而是活动的子,这确实是无法避免的。

【讨论】:

【参考方案3】:

由于 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_
    android:layout_
    tools:context="com.ringofblades.***.app.MainActivity">

    <TabHost
        android:id="@android:id/tabhost"
        android:layout_
        android:layout_>
        <LinearLayout android:orientation="vertical"
            android:layout_
            android:layout_>
            <FrameLayout android:id="@android:id/tabcontent"
                android:background="@drawable/background_image"
                android:layout_
                android:layout_weight="0.5"
                android:layout_/>
            <android.support.v4.view.ViewPager
                xmlns:tools="http://schemas.android.com/tools"
                android:id="@+id/pager"
                android:background="@drawable/background_image"
                android:layout_
                android:layout_weight="0.5"
                android:layout_
                tools:context="com.ringofblades.***.app.MainActivity">
                <FrameLayout
                    android:id="@+id/container"
                    android:layout_
                    android:layout_ />
            </android.support.v4.view.ViewPager>
            <TabWidget android:id="@android:id/tabs"
                android:layout_
                android:layout_ />
        </LinearLayout>
    </TabHost>

    <fragment android:id="@+id/navigation_drawer"
        android:layout_
        android:layout_
        android:layout_gravity="start"
        android:name="com.ringofblades.***.app.NavigationDrawerFragment"
        tools:layout="@layout/fragment_navigation_drawer" />
</android.support.v4.widget.DrawerLayout>

请注意,“@+id/pager”和“@+id/container”是具有“android:layout_weight="0.5"' 和 'android:layout_' 的兄弟。这样我就可以在任何屏幕尺寸的预览器中看到它。无论如何,它们的权重将在运行时在代码中进行操作。

【讨论】:

您好,我很好奇您为什么选择使用 TabHost 而不是带有 Tabs 的 ActionBar?我自己从 TabHost 切换到了 ActionBar,我的代码变得更简洁、更紧凑...... 据我记得,在 ActionBar 中使用选项卡的一个缺点是它会自动决定将它们显示为微调器下拉菜单(在小屏幕的情况下),这并不好为了我。但我不是 100% 确定。 @Igor,我在SO 的某处读到,将ActionBar 选项卡与Navigation Drawer 一起使用并不好,因为它会自动将选项卡放在抽屉视图上。抱歉,我没有备份此链接的链接。 @NoniA。 blog.xamarin.com/android-tips-hello-toolbar-goodbye-action-bar你关注Android的发展吗? 哇,感谢@Igor 的链接!我一定会检查的。我还是个初学者,所以还有一百万其他的东西要学 Android,但这似乎是一个宝石!再次感谢。【参考方案4】:

基于@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 Fragments 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();
    

【讨论】:

【参考方案5】:

虽然 OP 可能有特殊情况阻止他使用支持库,但大多数人应该使用它。 Android 文档推荐了它,它将使您的应用能够提供给尽可能多的受众。

在my fuller answer here 中,我做了一个示例来演示如何将嵌套片段与支持库一起使用。

【讨论】:

我是 OP。不使用支持库的原因是因为它是公司内部应用程序,其中使用的硬件明确定义为 >=4.0 和 当然你可以使用支持库,我也可以。只是不明白为什么 Google 只在支持库中而不在外部提供功能,或者为什么他们甚至将其称为支持库而不是将其作为整体标准,如果它是最佳实践的话。这是一篇关于支持库的好文章:martiancraft.com/blog/2015/06/android-support-library @Mathias,好文章。

以上是关于不使用支持库的 Android 4.0、4.1 (<4.2) 中嵌套片段的最佳实践的主要内容,如果未能解决你的问题,请参考以下文章

pulltorefresh android 4.1及更高版本[重复]

Android 4.0 中的蓝牙智能 (4.0) / GATT 支持?

蓝牙 4.0+ BLE 吗?

Android Studio 4.1 插件错误:插件 * 不兼容(仅在 IntelliJ IDEA 中支持)

在不使用支持库的情况下使用选项卡进行活动?

使用 PDF 库的示例 - Android 上的 libharu?