解读Android之Fragment

来源:互联网 发布:java三目运算符怎么用 编辑:程序博客网 时间:2024/06/05 00:09

本文翻译自android官方文档,结合自己测试,整理如下。

概述

Fragment在activity中表示一个行为或者UI的一部分。我们可以在一个activity中使用多个fragments或者在多个activities复用一个fragment。我们可以把fragment作为activity模块化的一部分,fragment有自己的生命周期,UI布局。我们可以在activity运行时动态添加或删除fragment。

fragment必须要嵌入到activity中,fragment的生命周期受到宿主activity的影响。例如,当activity处于暂停状态时,在activity中的所有fragments也进入暂停状态,当activity销毁时,activity中所有的fragments也全部销毁。但是,当activity处于运行状态时,我们可以单独处理每个fragment,例如我们可以添加或者删除它们。当我们执行完一个fragment事务时,我们也可以将fragment添加到Back栈中,该Back栈由宿主activity管理,栈中的实体记录的是fragment发生的事务操作。有了Back栈,我们就可以通过按BACK键回到fragment事务发生之前的状态。

当我们向activity布局中添加fragment时,它保存在ViewGroup中,该ViewGroup将添加到activity视图的层次结构中,fragment可以定义自己的布局文件。可以在布局文件中通过<fragment>标签添加fragment,或者通过代码添加到存在的ViewGroup中。但是fragment不是必须作为activity布局的一部分;我们仍然可以使用没有布局的fragment处理activity不用显示的任务。

下面将具体介绍fragment的详细用法。

设计哲学

Android从3.0(API11)开始引入fragments,主要是为了在大屏幕上支持更加动态和灵活的UI设计,例如平板。因为平板的屏幕远大于手持设备(手机等),有更大的空间来存放和交互UI组件。Fragments能够自动处理view层次变化的设计,不用我们来管理。通过将activity布局分成fragments,我们能够在activity运行时改变activity的呈现状态,并且可以在Back栈中管理这些改变,该Back栈由宿主activity管理。

例如,新闻应用程序可以在左边使用一个fragment显示标题列表,而在右边使用另一个fragment用于显示具体新闻内容。这两个fragments都在同一个activity中,一左一右,每个fragment有自己的生命周期方法并且处理自己的输入事件。这样的话我们可以避免使用两个activities达到这种效果。如下图所示:
fragments
我们应该把fragment设计成模块化可重用的activity组件。这是因为每个fragment可以定义自己的布局,有自己的生命周期,我们可以在多个activities中使用同一个fragment,因此我们应该设计成可重用的避免在fragment中直接操作另一个fragment。模块化的fragment可以允许我们根据不同的屏幕大小改变fragments之间的组合。当设计程序同时支持平板和手机时,我们可以在不同的布局配置中重用我们的fragments,以此来适应不同的屏幕空间,提高用户体检质量。例如上面的新闻客户端的例子。对于大屏的平板来说可以在一个activity中同时显示新闻标题和新闻内容这两个fragment,而对于手机来说,一个fragment只能放在一个activity中。

创建fragment

为了创建一个fragment,我们必须创建一个Fragment类(或者它的子类)的子类。Fragment中的生命周期方法和Activity有很多相同的地方。例如fragment也包含:onCreate()onStart()onPause()onStop()等。实际上,若我们想要把现有的应用程序使用fragment的话,我们只需要将activity的回调方法中的代码移到对应的fragment的回调方法中即可。

通常情况下,我们至少要实现一下方法:

-onCreate()
当创建fragment时系统会调用该方法。我们应该在这里初始化必要的组件,这些组件当fragment进入暂停或停止之后重新启动后仍被保留。
- onCreateView()
当首次为frgment准备UI布局时,系统调用该方法。为了给fragment绘画UI布局,该方法必须返回一个View对象作为fragment布局的根。若fragment不提供布局的话,该方法返回null。
- onPause()
当用户将要离开该fragment时(离开但不一定销毁该fragment),系统调用该方法。通常我们需要在这里保存那些需要永久保存的信息,这是因为用户可能不在回来。

关于生命周期中其它的方法我们将在后续处理fragment生命周期中详细介绍。

除了基类Fragment之外,系统还有我们提供了几个继承Fragment的子类:

  • DialogFrament
    显示为对话框形式。
  • ListFragment
    显示为项目列表。
  • PreferenceFragment
    作为一个列表显示Preference对象的层次结构,类似PreferenceActivity。当在程序中创建设置这样的activity这是非常有用的。
    这里写图片描述

添加UI

fragment通常被作为activity UI的一部分,fragment将自己的布局贡献给activity。

为了提供fragment的布局,我们必须实现onCreateView(),该方法会在系统开始创建该fragment的布局时由系统调用。同时该方法必须返回一个View作为我们frament的布局的根视图。

注意:若我们的fragment是ListFragment的子类,则该方法默认会返回一个ListView,因此不用我们来实现该方法。

为了从onCreateView()返回一个布局,我们利用xml中定义的布局文件,为了达到这个目的,在onCreateView()提供了LayoutInflater对象参数。例如:

public static class ExampleFragment extends Fragment {    @Override    public View onCreateView(LayoutInflater inflater, ViewGroup container,                             Bundle savedInstanceState) {        // Inflate the layout for this fragment        return inflater.inflate(R.layout.example_fragment, container, false);    }}

其中,参数ViewGroup对象表示父视图(来自activity布局),我们的fragment布局需要插入到该布局中。Bundle对象用于保存之前运行的fragment状态,若之前不存在则返回null。

LayoutInflater对象的inflate()方法传递三个参数:

  1. 加载的布局文件ID。
  2. 加载的布局的父视图,即该父视图作为fragment布局的根布局。
  3. boolean表示在加载的过程中加载的布局是否依赖父布局(第二个参数)。这里需要传递false,这是因为系统已经将加载的布局插入到ViewGroup对象中;若传递true,则在最终的布局中会创建一个冗余的视图。

通过以上介绍我们知道了如何创建一个带有布局的fragment,下面,我们介绍如何将fragment添加到activity中。

将fragment添加到activity

通常,fragment作为宿主activity布局的一部分,有两种方法将fragment添加到activity中:

  1. 静态加载
    该方法通过在activity布局文件中声明fragment控件。这种情况下我们可以像指定视图一样指定fragment的布局属性。例如下面声明了带有两个fragments的activity布局:

    <?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="horizontal"android:layout_width="match_parent"android:layout_height="match_parent"><fragment android:name="com.example.news.ArticleListFragment"        android:id="@+id/list"        android:layout_weight="1"        android:layout_width="0dp"        android:layout_height="match_parent" /><fragment android:name="com.example.news.ArticleReaderFragment"        android:id="@+id/viewer"        android:layout_weight="2"        android:layout_width="0dp"        android:layout_height="match_parent" /></LinearLayout>

    <fragment>标签中,android:name属性指定需要实例化的Fragment类。

    当系统创建activity布局时,它实例化在activity布局中指定的每个fragment,通过onCreateView()检索相应的布局,并将这些布局插入到对应<fragement>标签所在的位置。

    注意:每个fragment需要提供一个唯一标识符,通过该标识符系统可以保存这些fragment(在重新启动activity时)。有下列三种方式指定:
    1. 在<fragment>标签中通过android:id指定一个ID;
    2. 在<fragment>标签中通过android:tag指定一个字符串;
    3. 若以上两种都不指定的话,系统使用父控件(inflate()方法的第二个参数)的ID。

  2. 动态加载
    该方法是指将fragment动态地添加到一个存在的ViewGroup中。在activity运行的任何时候,我们能够将fragment添加到activity中。我们只需要指定一个被fragment替换的ViewGroup对象即可。
    若想在activity中进行fragment事务操作(事务是指原子操作,一次事务要么都执行要么都不执行。fragment事务操作如:添加,删除,替换fragment),我们能够使用FragmentTransaction类,可以在activity如下操作:

    FragmentManager fragmentManager = getFragmentManager();FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

    我们可以使用add()方法添加fragment,add()方法接收两个参数,分别为:插入到的父布局和要添加的fragment对象。如下:

    ExampleFragment fragment = new ExampleFragment();fragmentTransaction.add(R.id.fragment_container, fragment);fragmentTransaction.commit();

    每次使用FragmentTransaction对象执行事务之后都需要调用commit()提交事务执行。

添加无UI布局的fragment

上面介绍了如何通过fragment向activity中提供UI布局。然而,我们也可以使用不带布局的fragment,该fragment可以提供后台服务。

为了添加无UI布局的fragment,我们可以在activity中使用add(Fragment,String)(String对象为fragment的tag属性)添加fragment。由于该fragment不需要UI布局(不与activity布局发生关联),因此我们可以不用实现onCreateView()

使用tag字符串不是严格意义上的无布局fragment,我们也可以为有布局的fragment提供tag字符串。但是若fragment没有相关布局的话,那么tag字符串是唯一标识该fragment的方法。若想在之后使用该fragment的话,可以通过findFragmentByTag()

关于后台fragment使用,可以参见之前的内容:在配置改变时保留一个对象用于恢复数据。

管理Fragments

为了在activity中管理fragments,我们可以使用FragmentManager对象,可以在activity中通过`getFragmentManager()获取该对象。

我们可以通过FragmentManager完成以下事宜:

  • 可以通过findFragmentById()(获取有UI布局的fragments)或findFragmentByTag()(获得有/无UI布局的fragments)获取activity中存在的fragments。
  • 通过popBackStack()将fragment从Back栈中弹出(模拟用户BACK命令)。
  • 通过addOnBackStackChangedListener()为Back栈注册事件监听。

关于FragmentManager类更多内容可以参考官方文档中的介绍。

上一小节介绍了可以通过FragmentTransaction开启一个事务操作,下面详细介绍。

执行Fragment事务

通过Fragment事务,我们能够完成添加/删除/替换操作。每次操作的集合都可称为一次事务。我们也可以通过Back栈对事务进行管理,可以通过activity管理Back栈,该Back栈允许用户通过导航回到变化之前的状态(类似activity的Back栈)。

事务由一组同一时刻的操作组成。我们可以使用add()remove()replace()中的1个或多个完成一次事务操作。最后需要调用commit()方法完成事务提交。

在调用commit()之前,我们可能需要调用addToBackStack(String)(参数为Back栈名),该方法能够实现将操作的事务添加到fragment事务的Back栈中。该Back栈由activity管理,通过BACK键可以回到改变之前的fragment状态。

例如:

// 创建fragment和事务Fragment newFragment = new ExampleFragment();FragmentTransaction transaction = getFragmentManager().beginTransaction();// 使用fragment替换id为fragment_container的视图// 将本次事务添加到back栈中transaction.replace(R.id.fragment_container, newFragment);transaction.addToBackStack(null);// 提交事务transaction.commit();

例子中,使用ExampleFragment对象替换 R.id.fragment_container。然后调用addToBackStack(),将替换事务保存到Back栈中。用户可以通过BACK键返回到替换之前的状态。

若在一次事务中执行了多次操作,然后再调用addToBackStack(),则所有在commit()之前的操作都会保存在Back栈中,并作为一次事务。

需要注意一下两点:

  1. 在一次事务操作完之后必须调用commit()
  2. 若在一个布局中添加多个fragment的话,则显示的顺序和添加的顺序一致。

当我们移除一个fragment时,若不调用addToBackStack(),则当移除事务执行时,该fragment将会销毁,用户无法返回移除前的状态。而若调用的话,当移除一个fragment时,该fragment进入停止状态,若用户通过导航返回时,该fragment又会重新启动。

tip:

  1. 对于每个事务,我们可以通过setTransaction()设置事务的动画,该方法在commit()之前调用。
  2. 可以调用hide(Fragment)隐藏当前的Fragment,仅仅是设为不可见,并不会销毁;调用show(Fragment)显示之前隐藏的Fragment。当然这两个操作也是事务,须在调用commit()之前执行。

调用commit()不会立刻执行事务,而是将该事务添加到主线程的任务表中,一旦主线程能够处理时就会执行该事务。我们可以通过调用FragmentManager的executePendingTransactions()方法立即执行commit()方法提交的事务,该方法必须在主线程中执行。通常来说,没有必要这样做,只有在其它线程需要依赖该事务时,才这样做。

注意:commit()方法需要在activity保存状态之前调用,否则的话会抛出异常。这是因为若activity恢复数据的话,在该方法提交之后的状态可能丢失。若允许丢失数据的话,我们可以调用commitAllowingStateLoss()而不是调用commit()

和Activity通信

尽管fragment作为一个独立于activity的对象,并且能够在多个activities中使用同一个fragment,但是我们仍然可以在activity中直接和它包含的fragments进行通信。

在fragment中可以通过getActivity()方法获取activity,然后就可以进行通信,例如获得activity布局中的控件:

View listView = getActivity().findViewById(R.id.list);

同样,activity可以通过FragmentManager使用findFragmentById()或者findFragmentByTag()获取fragment,例如:

ExampleFragment fragment = (ExampleFragment) getFragmentManager().findFragmentById(R.id.example_fragment);

fragment中有两个方法可以设置和获取保存的Bundle对象:setArgument()getArgument()

创建事件回调方法

有些情况下,fragment可能需要和宿主activity共享事件。一个好的办法是在fragment中定义一个回调接口,而宿主activity实现该接口。当activity通过接口接收回调时,若有必要的话,他就能够和其它fragments共享信息。

例如新闻应用程序,它包含两个fragments,一个用于显示文章标题列表(fragment A),另一个用于显示文章内容(fragment B)。则当点击A中标题列表的某个文章标题时,就需要在B中显示该文章的具体内容。因此则需要在A中定义一个OnArticleSelectedListener接口:

public static class FragmentA extends ListFragment {    ...    // Container Activity must implement this interface    public interface OnArticleSelectedListener {        public void onArticleSelected(Uri articleUri);    }    ...}

然后,宿主activity需要实现该接口,并实现onArticleSelected(Uri articleUri),该方法用于通知B显示具体内容事件。为了确保在宿主activity中实现该接口,可以在fragment中的onAttach()中强制转换宿主activity来实例化接口。例如:

public static class FragmentA extends ListFragment {    OnArticleSelectedListener mListener;    ...    @Override    public void onAttach(Activity activity) {        super.onAttach(activity);        try {            mListener = (OnArticleSelectedListener) activity;        } catch (ClassCastException e) {            throw new ClassCastException(activity.toString() + " must implement OnArticleSelectedListener");        }    }    ...}

可以看到若宿主activity没有实现该接口的话会抛出ClassCastException异常。若实现的话,则mListener变量就持有宿主activity的引用,则A就可以通过接口方法实现共享点击事件。例如,假设A继承自ListFragment,每次用户点击列表选项时,都会调用onListItemClick(),我们可以在这里面回调接口方法:

public static class FragmentA extends ListFragment {    OnArticleSelectedListener mListener;    ...    @Override    public void onListItemClick(ListView l, View v, int position, long id) {        // Append the clicked item's row ID with the content provider Uri        Uri noteUri = ContentUris.withAppendedId(ArticleColumns.CONTENT_URI, id);        // Send the event and Uri to the host activity        mListener.onArticleSelected(noteUri);    }    ...}

onListItemClick()中传递的id参数为被点击的选项的行号(来自ContentProvider的内容)。

关于ContentProviders可以参考我之前翻译的文章:
解读Android之ContentProvider(1)CRUD操作和解读Android之ContentProvider(2)创建自己的Provider。

添加到ActionBar选项

fragments可以作为activity的Options菜单(因此也是ActionBar)中的菜单选项,通过实现fragment的onCreateOptionsMenu()。想要回调该方法,必须在onCreate()中调用setHasOptionsMenu(),表明该fragment作为菜单选项。

任何通过fragments添加的选项都会附加在已存在的选项菜单上。当点击某个选项时会回调onOptionsItemSelected()

我们也可以通过registerForContextMenu()在fragment布局中注册一个视图提供context菜单。当用户打开context菜单时,就会调用onCreateContextMenu()。当用户选择某个选项时,就会调用onContextItemSelected()

注意:尽管当用户选择某个选项时,fragment接收点击选项回调方法,但是activity会首先回调相应的方法。若activity中没有实现处理选中的选项,则事件会传递给fragment的回调方法。对于context菜单和Options菜单都是如此。

处理fragment生命周期

fragment的生命周期类似于activity的生命周期,也存在以下三种状态:

  • Resumed
    运行状态。fragment在处于运行状态的activity中可见。
  • Paused
    另外一个activity来到前台并获得焦点,但是此时宿主activity仍然可见(被透明的或dialog样式的activity覆盖)。
  • Stopped
    fragment不再可见。原因要么是宿主activity处于停止状态,要么该fragment被移除但是添加到Back栈了。处于停止状态的fragment依然是存活的(所有状态信息和成员都被系统保存)。若宿主activity被杀死的话,它也不再可以获得并且也将被杀死。

同activity一样,我们也可以在宿主activity所在的进程被杀死时,通过Bundle对象保存fragment状态,然后再activity重建时恢复fragment状态。我们可以在fragment的onSaveInstanceState()中保存状态,在onCreate(),或onCreateView(),或onActivityCreated()中恢复数据。

关于activity和fragment生命周期一个重要的区别在于如何在Back栈中进行保存。默认情况下,activity被放在activities的Back栈中,Back栈由系统管理。然而,fragment的Back栈由宿主activity管理,并且当fragment被移除时只有在我们通过addToBackStack()明确将fragment添加到Back栈才行。也就是说fragment若要进入Back栈必须满足两个条件:
1. 该fragment将要被移除;
2. 在commit()之前必须调用addToBackStack()

fragment的生命周期和activity的生命周期类似,我们需要特别注意的是activity的生命周期如何影响fragment的生命周期。

注意:若我们需要在fragment获得一个Context对象,可以通过getActivity()。然而只有当fragment关联acitivity时才能使用该方法。当fragment还没有关联activity或者解除关联时,该方法返回null。

与activity生命周期相协调

宿主activity的生命周期直接影响fragment的生命周期,因此,activity的每个周期方法都会导致一个类似的fragment周期方法。

除此之外,fragment还有一些其他的周期方法,如下:

  • onAttach()
    当fragment和activity进行关联时调用,此时会传递activity参数。
  • onCreateView()
    当创建fragment视图层次时调用。
  • onActivityCreated()
    当activity方法onCreate()返回时调用。
  • onDestroyView()
    当移除fragment视图时被调用。
  • onDetach()
    当fragment和activity解除关联时被调用。

下图描述了宿主activity如何影响fragment:
activityAndfragment
只有当宿主activity处于运行状态时,我们才能独立处理fragment生命周期了。否则,都会按照上图所示,受到宿主activity的影响。

1 0
原创粉丝点击