关闭

从零学Android(十)、Fragment基础知识

标签: FragmentFragment生命周期
236人阅读 评论(0) 收藏 举报
分类:

概述

一个Fragment代表着Activity中一种行为或者Activity用户界面中的一部分。我们可以将多个Fragment组合在一个Activity中,组成一个多窗格(multi-pane)布局;同样我们也可以在多个Activity中重复使用某个Fragment。我们可以将Fragment当作一个Activity中的小模块(它有它自己的生命周期,自己的事件处理机制),在Activity运行过程中,我们可以动态地添加或者移除这个模块。

Fragment在使用时,必须被嵌套在一个Activity中,我们可以称这个Activity为Fragment的“宿主Activity”,宿主Activity的生命周期直接影响到其中Fragment的生命周期。比如说:当宿主Activity进入暂停状态,其中所有的Fragment也会进入暂停状态,如果宿主Activity被销毁,其中包含的Fragment也会被销毁。然而,当宿主Activity正在运行,即处于resumed状态,我们可以独立地去操控某个Fragment,比如添加或者移除它。当我们执行一个Fragment事务的时候,我们可以将Fragment对象加入到宿主Activity管理的后台堆栈中,这个后台堆栈的每一个条目都是一次Fragment的事务记录。这个后台堆栈的作用是按下回退键时扭转Fragment事务的发生,即向后导航。

当我们将Fragment作为Activity的Layout布局的一部分加入到Activity中时,它会寄居在一个宿主Activity的View结构中的某个ViewGroup下,而且这个Fragment有自己的View布局。我们可以直接通过在Activity布局文件中使用<fragment>元素标签来声明插入Fragment到Activity中或者在代码中将它加入到一个ViewGroup下(所以,如果调用FragmentTransaction的add()方法,必须指定containerViewId,否则不会显示)。然而,Fragment并不需要一定作为Activity布局的一部分,我们有时可以使用一个没有UI且不可见的Fragment作为Activity的一个工人去帮Activity做一些事情。

Fragment是在Android 3.0(API 11)时提出来的,主要是用来让大屏设备(如:平板设备)的UI设计更加灵活和动态化。由于平板设备的屏幕空间远大于手持设备的屏幕空间,所以平板设备有更大的空间来组合控件和替换控件。Fragment不需要你去管理那些View结构的复杂变化 。通过将Activity的布局分割到Fragment中,我们可以在Activity的运行过程中,动态改变其UI并且将这种改变保存到Activity管理的一个栈中。

比如说:我们的应用中有一个文章列表和文章详情页面,由于平板设备空间大,列表Fragment和详情Fragment可以放在同一个页面中,而在手持设备上,则分为两个Activity作展示。如下图:


1. 创建一个Fragment

创建一个Fragment必须继承自Fragment类(或者它的某个子类,如果是API < 11的低版本,则继承V4包下的Fragment类)。Fragment的有些代码和Activity的看起来有点像。它包含了一些和Activity相似的回调方法,如:onCreate()onStart()onPause()onStop()。实际上,当你改变现有App结构去使用Fragment的时候,你甚至会发现,你只要将Activity的回调方法中的代码移动到Fragment中对应的回调方法中就行了。一般来说,创建的Fragment中,我们需要实现下面三个生命周期方法:

【1】onCreate()

这个回调方法在系统创建Fragment的时候被调用,一般在这个方法中去初始化一些组件。

【2】onCreateView()

这个回调方法在Fragment第一次绘制自己的用户界面时被调用。为了绘制UI界面,这个方法必须返回一个View对象,作为Fragment的布局结构的根布局,如果你不希望Fragment有UI界面,而只是单纯地帮Activity做一些事,你可以返回null。 

【3】onPause()

这个回调方法会在用户离开这个Fragment页面时被调用(虽然这个时候Fragment并没有被销毁)。

其它还有很多有关Fragment的生命周期方法会在后面的小节中学习。

下面是几个Fragment的常用子类,我们有时候会选择继承它们,而不是去继承Fragment:

【1】DialogFragment

它展示的是一个悬浮的对话框。在Activity中使用这个类去创建一个对话框,替代之前的对话框。因为你可以将Fragment加入Activity管理的栈中,当用户点击返回按钮,可以让Fragment消失掉。

【2】ListFragment

它就像ListActivity一样,展示的是一个由数据适配器管理的数据列表。它提供了很多的方法去管理一个列表视图,比如item的点击回调方法onListItemClick()

【3】PreferenceFragment

就像PreferenceActivity一样,用来展示一个选项列表。我们可以使用它来做一个“设置”页面。


1.1 添加一个用户界面

Fragment通过作为Activity用户界面的一部分,它的布局也会加入到Activity布局中。

给Fragment提供一个layout,必须实现onCreateView()方法,系统会在Fragment第一次绘制它的布局时回调这个方法。这个方法必须返回一个View对象,作为Fragment的布局结构的根布局。注意:如果Fragment继承自ListFragment时,onCreateView()会默认返回一个ListView,所以我们不用去实现这个方法。为了返回这个View,我们可能需要去加载一个XML布局资源,所以这个方法中,给我们提供了一个布局加载器LayoutInflater的对象。

如下:一个Fragment的子类,加载example_fragment.xml布局资源作为内容布局:

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);
    }
}
其中container参数代表着Fragment被插入到宿主Activity布局中时的父容器。参数savedInstanceState是一个Bundle对象,如果当前Fragment是处于恢复状态,那么这参数将会提供之前Fragment被销毁时的状态和数据。

再看看inflate()方法的三个参数的含义:

·第一个参数代表你要加载的布局资源的ID。

·第二个参数为当前加载的资源的父容器,即parent ViewGroup。传递container参数是重要的,这样方便系统将container指定的布局参数应用到当前加载的布局资源上。

·第三个参数表示是否在加载资源过程中将当前加载的布局资源关联到第二个参数container ViewGroup上。(这个例子中我们传递了一个false,因为系统已经将这个加载的布局资源加入到container中;如果传递为true的话讲会创建一层多余ViewGroup,而且会报运行时异常)

另外,关于这个方法的返回值:布局层次的根布局。如果container不为空且attachToRoot为true,那么返回的就是container,否则返回的就是加载的布局资源的根View。

1.2 添加Fragmnet到Activity中

有两种方法将Fragment加入到Activity中:

【1】直接在Activity的layout布局文件中声明使用Fragment

当使用这种方式时,我们可以将Fragment当做一个普通的View去指定它的布局属性(值得注意的是,我们必须给这个fragment指定一个id属性或tag属性,或者将其放置到一个包含id属性的ViewGroup中,用来当Activity再启动时恢复Fragment,或者用来执行移除等事务),如下:

<?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时,它会去初始化每一个在layout文件中指定的Fragment,并且调用其onCreateView()方法去获取对应的Fragment的layout。系统会将Fragment的onCreateView()方法返回的View放置到原来<fragment>标签的位置上。


【2】在代码中将Fragment加入到一个存在的ViewGroup

在Activity运行过程中任何时候,我们都可以将Fragment加入到Activity中。我们仅仅需要指定将Fragment插入到哪一个ViewGroup下。

为了在Activity中执行Fragment的事务(如添加,移除或替换Fragment),我们需要用到FragmentTransaction的API。我们可以在Activity中获取FragmentTransaction的实例,如下:

FragmentManager fragmentManager = getFragmentManager()
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
如果使用的是V4包下的Fragment,则需要去获取响应的V4包下的事务等类:
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction transaction = fm.beginTransaction();
我们可以通过add()方法加入Fragment到Activity中,需要指定插入Fragment的View,另外事务需要commit提交才会执行:
ExampleFragment fragment = new ExampleFragment();
fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit();

1.3 加入一个没有UI界面的Fragment

上面两种方式我们算是为了提供一个UI而加入了Fragment,我们有时候可能会加入一些没有UI界面的Fragment到Activity中,只是为了让Fragment执行一些后台行为。

为了添加一个没有UI界面的Fragment,我们需要调用add(Fragment, String)方法(提供一个独一无二的字符串Tag用来标识这个Fragment,而不是一个View的ID)将Fragment加入到Activity中。这种方式加入的Fragment,由于它没有和Activity layout布局中的某个View联系起来,所以我们无需去实现onCreateView()方法。如果Fragmnet没有UI,那么tag就是唯一标识Fragment的途径,我们可以在Activity中使用findFragmentByTag()方法去获取这个Fragment。


2. 执行Fragment事务

在Activity中使用Fragment的一大特点就可以通过执行添加,移除,替换或者一些其它行为来响应用户的交互。这些动作被称为事务(FragmentTransaction)。我们可以将这些事务保存到一个Activity维护的栈中,以便反向导航。

我们可以通过FragmentManager去获取一个FragmentTransaction的实例:

FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction transaction = fm.beginTransaction();
每一个事务就是你希望同时执行的一系列变化。你可以通过使用add()remove()replace()方法去建立一系列的变化,然后调用commit()方法提交这个事务。在调用commit()方法前,有时候我们会先调用addToBackStack()方法,将这次事务加入到Activity维护的栈中,以便用户能通过点击返回键返回到之前的状态。

比如,下面的示例代码中先用一个Fragment替换了另外一个Fragment,并且在栈中保留了之前的状态:

// Create new fragment and transaction
Fragment newFragment = new ExampleFragment();
FragmentTransaction transaction = getFragmentManager().beginTransaction();

// Replace whatever is in the fragment_container view with this fragment,
// and add the transaction to the back stack
transaction.replace(R.id.fragment_container, newFragment);
transaction.addToBackStack(null);

// Commit the transaction
transaction.commit();
在这个例子中,newFragment代替了当前Activity layout布局中ID为R.id.fragment_container的Fragment。然后通过调用addToBackStack()将当前这次替换的事务保存到了栈中,当用户按下返回键时,可以恢复到之前的状态。

如果你添加了多个改变在事务中,然后调用了addToBackStack(),那么这些改变会在你调用commit()前被当做一个事务保存,然后当点击返回键时一起恢复。对于事务而言,你添加的变化的顺序没有影响,除了以下两点:

【1】你必须最后才调用commit()

【2】如果你对同一个布局容器添加了多个变化,那么你添加这些变化的顺序决定 了它们在View层次中出现的顺序。

如果你在执行一个移除Fragment的事务时,没有调用addToBackStack()方法,那么当事务被提交后,这个Fragment就会被销毁掉,而且用户点击返回键也不会恢复。如果你有调用addToBackStack()方法,那么这个Fragment就会进入stopped状态,当用户点击返回时,会再恢复。

建议:对于每一个Fragment事务,我们都可以加入一个过渡动画,通过在提交事务前调用setTransition()来绑定动画。

调用commit()方法不会立即执行这个事务。它只会将它加入到UI线程中,直到主线程再次准备好才执行。在必要时,我们可以在调用完commit()方法后立即调用FragmentManager的executePendingTransactions()方法,来让UI线程立即执行这个事务。

注意:我们只能在宿主Activity保存它自身状态前使用这个commit()方法去提交一个事务,如果在宿主Activity保存它自身状态之后再使用这个方法,会出现异常。因为执行完commit()方法后,事务相关的状态会直接丢失,这样Activity在恢复这个状态时将无法恢复。在这种情况下,我们可以使用commitAllowingStateLoss()去代替commit()方法。


3.Fragment与Activity的通信

尽管一个Fragment的实现和Activity完全独立,而且一个Fragment可以被应用在多个Activity中,但是一个具体的Fragment的实例却是直接和一个宿主Activity相连。

所以Fragment很容易就能获取到它的宿主Activity实例,也就能轻易获取到宿主Activity中的某些控件:

View listView = getActivity().findViewById(R.id.list);
同时,在宿主Activity中也能通过使用FragmentManagerfindFragmentById()方法或findFragmentByTag()方法轻易获取到Fragment的实例:
ExampleFragment fragment = (ExampleFragment) getFragmentManager().findFragmentById(R.id.example_fragment);

3.1 为Activity创建一个回调

有时候,我们可能需要将Fragment中某些事件暴露给宿主Activity。这个时候最好的做法就是在Fragment中创建一个回调接口,并且要求宿主Activity必须实现这个接口。当宿主Activity通过这个接口收到一个回调时,如果必要的话,它就能和其它的Fragment分享这个数据。

举个例子:如果我们的应用中的某个Activity包含两个Fragment,其中FragmentA展示了一个文章列表,FragmentB则显示一个文章的详情,那么当FragmentA的列表项被选中时,它必须告知宿主Activity,然后由宿主Activity告诉FragmentB具体展示哪个文章。这种情况下,我们可以在FragmentA中创建一个名为OnArticleSelectedListener的接口:

public static class FragmentA extends ListFragment {
    ...
    // Container Activity must implement this interface
    public interface OnArticleSelectedListener {
        public void onArticleSelected(Uri articleUri);
    }
    ...
}
然后,宿主Activity去实现这个接口,并且实现其中的方法用来通知FragmentB由FragmentA传递的信息。为了确保宿主Activity有实现接口,我们可以在FragmentA的onAttach()方法中进行判断(进行强制类型转换,没出异常则有实现),如下:
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");
        }
    }
    ...
}
如果没有抛出异常,那么mListener就持有了一个实现了OnArticleSelectedListener接口的宿主Activity的引用,这个时候FragmentA可以通过调用OnArticleSelectedListener接口的定义方法来和宿主Activity进行事件共享:
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);
    }
    ...
}
(其中关于新知识Content Providers暂不去深究)

4.Fragment的生命周期 

管理Fragment的生命周期就像管理Activity的生命周期函数;和Activity一样,Fragment同样存在3种状态:

【1】Resumed状态:宿主Activity正在运行且Fragment可见。

【2】Paused状态:另外一个Activity运行在前台,当时当前宿主Activity仍然可见

【3】Stopped状态:当前Fragment不可见,因为宿主Activity已经进入Stopped状态或者Fragmnet已经被移除了,但是添加到了宿主Activity管理的栈中。Stopped状态下的Fragment仍然是“活着的”(即系统还保存着它所有的状态和信息)。但是它不再对用户可见,当宿主Activity被杀死,它也会被杀死。

和Activity一样,Fragment同样是使用一个Bundle来恢复数据,以防宿主Activity的进程被杀死后重新创建Activity时需要恢复Fragment。同样是在onSaveInstanceState()方法中保存数据,然后在onCreate()方法、onCreateView()方法或者onActivityCreated()中恢复数据。

Activity和Fragment在生命周期中最大的不同点就在于他们各自的回退栈了。当Activity被暂停时,它会被加入到一个系统管理的Activity回退栈中,这也是为什么我们可以按返回键退回上一个页面的原因。然后,只有你使用事务移除Fragment时调用addToBackStack()方法才能将Fragment加入到Activity管理的回退栈中。其它的生命后期方法和Activity基本一致,有兴趣了解的可以去阅读之前的文章从零学Android(五)、Activity的生命周期

注意:当我们在Fragment中需要使用一个Context对象的时候,我可以通过调用getActivity()方法获取,但是这个方法必须是在Fragment绑定到一个Activity之后,且在Fragment解除绑定中间使用,否则会返回null。


4.1 Fragment和宿主Activity生命周期的对应

在上面的学习中,我们有提到过,宿主Activity的生命周期会直接影响到Fragment的生命周期,Activity收到每一个生命周期的回调方法,Fragment都会收到一个类似的生命周期回调方法。比如说:当Activity回调onPause()方法时,Activity中的每一个Fragment都会回调onPause()方法。

Fragment还有一些区别于Activity的生命周期方法,用来处理一些和Activity的独特的交互,以便执行一些行为,比如说创建和销毁Fragment的UI。这些额外生命周期方法有如下一些:

【1】onAttach():当Fragment和Activity已经关联起来的时候被调用。

【2】onCreateView():当创建Fragment的UI结构时调用。

【3】onActivityCreated():当宿主Activity的onCreate()方法执行完的时候调用。

【4】onDestroyView():当Fragment的UI结构被移除时调用。

【5】onDetach():当Fragment和宿主Activity解除关联后调用。

这些Fragment的生命周期函数,都受到宿主Activity的影响,如下图:

在这张图中,你可以发现每一个Activity的状态都决定了Fragment将会回调哪个生命周期函数。比如说:当Activity收到onCreate()方法回调时,Activity中的Fragment最多只会收到方法onActivityCreated()回调(这个最多是指最后回调的生命周期函数,而不是指生命周期函数个数为1)。

一旦Activity进入到resumed状态,我们就可以随意添加和移除Fragment。因此,只有当Activity处于resumend状态时,Fragment的生命周期才可以独立改变。但是当Activity离开resumed状态,Fragment又会被Activity推动其生命周期。

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:4956次
    • 积分:192
    • 等级:
    • 排名:千里之外
    • 原创:15篇
    • 转载:0篇
    • 译文:0篇
    • 评论:0条
    文章存档