【Android API指南】App组件(3) - Activities(1) - Fragments

 一个Fragment代表一个Activity的一种行为或者用户界面的一部分。你可以组合多个fragment到一个activity来创建一个多面板的UI,或者复用一个fragment到多个activity中。你可以认为fragment就是activity的一个模块,它有自己的生命周期,接收直接的输入事件,在activity运行中可以随意添加和删除。

一个fragment通常需要嵌入到一个activity中,这个activity的生命周期也直接影响fragment的生命周期。例如,activity暂停时,包含在其中的fragment也暂停,activity被销毁时,fragment也被销毁。不过,当activity在运行中时,你可以单独的操作任何一个fragment,比如添加和删除。当你执行这些fragment修改时,你也可以添加这些fragment到一个activity管理的后退堆栈中。这个堆栈记录了fragment的改变,让用户可以恢复到改变前的状态。

当你添加fragment到activity的布局中时,fragment被放在ViewGroup中,fragment也有自己的视图布局。你可以再布局文件中使用<fragment>元素来添加一个fragment,也可以在代码中添加到一个已存在的ViewGroup中。一个fragment可以是界面的一部分,也可以隐藏着为activity工作。

设计原理

fragment在Android3.0中被加入,只要是为了支持平板等大屏幕设备,让你可以在activity运行中动态和灵活的操作界面的元素,而且可以把这些操作记录到后退堆栈中,方便管理。

例如,你可以使用一个fragment在屏幕左边显示一个文章列表,用另外一个fragment在右边显示选中状态下的文章内容,这些可以在一个activity中完成。

你应该定义每个fragment为一个模块化和可重用的activity组件。因为每个fragment都定义有自己的布局,行为,生命周期,你可以包含一个fragment到多个activity中,所以你应该避免使用一个fragment直接去操作另外一个fragment。另外一个应该注意的是,模块化的fragment可以让你在不同的尺寸在改变fragment的组合。你可以基于你的屏幕大小来优化用户体验,例如,在手机上,你可能需要一次只显示一个fragment。

根据上图显示,在大屏幕下,一个Activity可以包含两个fragment,而在手机上没有足够的空间,一个Activity只能包含一个fragment,当用户选择第一个fragment时才启动包含另外一个fragment的activity。

创建一个Fragment

为了创建fragment,你需要继承Fragment类,Fragment类的代码和Activity很相似,也有onCreate(), onStart(), onPause(), onStop()函数。

通常情况下,你至少需要实现下面这些声明回调函数:

onCreate()
创建fragment时调用,你应该在这个函数中初始化一些基本组件,这些组件是fragment暂停,或者停止时你还希望能保持的。

onCreateView()
在fragment第一次绘制的时候调用,为了绘制fragment的界面,你需要在这个方法中返回一个view,如果不提供界面的话就可以返回null。

onPause()
用户离开fragment时调用,离开不代表被销毁。通常在这个方法中保存那些需要持久保存的数据,因为用户可以不会再回到这个界面来了。

大多数程序都需要实现上面三个方法,不过fragment还有一些其他的生命周期方法(见下图),我们在下面的章节介绍。


下面有一些fragment的子类你可能想要扩展:

DialogFragment
显示一个浮动对话框。

ListFragment
显示一个列表,列表的元素可以被一个适配器管理,类似一个ListActivity。

PreferenceFragment
像list一样显示一个Preference对象层。类似PreferenceActivity,通常用来为程序创建一个“设置”功能的activity。

添加一个用户界面
一个fragment通常是activity界面的一部分,不过它也有自己的布局。

你需要实现onCreateView()函数来提供一个fragment的布局,然后返回一个fragment根布局。

提示:如果你的fragment是一个ListFragment,onCreateView()默认返回一个ListView,那么你就不需要实现它。

为了从onCreateView()中返回一个布局,你可以从布局资源文件中取得布局,而且onCreateView()还提供了一个LayoutInflater对象来帮助你。

下面的例子是从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参数传递了一个父类ViewGroup,让你的fragment布局添加到里面。savedInstanceState参数是这个fragment先前的实例留下的数据,可以让你恢复它以前的状态。

inflate()提供了三个参数:
  • 布局资源ID
  • 包含这个fragment的父类容器,一个ViewGroup。
  • 一个布尔值,指定了布局是否附加到第二个参数的ViewGroup中,上面的例子设置为false,因为系统已经把布局插入到了容器中,如果为true的话,会创建一个view group来包含fragment布局。
添加一个fragment到activity
有两种添加fragment的方法:

1. 在activity布局文件中声明fragment。
你可以在XML文件直接定义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布局时,会实例化每个fragment指定的布局,然后调用onCreateView()来接收每个fragment的布局,最后系统直接插入返回的view到对应的<fragment>元素上。

提示:每个fragment都要有一个唯一的标示符,让系统在重启activity时恢复fragment。下面有三个方法为fragment提供一个ID:
  • 提供android:id属性
  • 提供android:tag属性
  • 每个都没提供,那么系统会使用容器View的ID
2. 编程的方式添加fragment到一个已存在的ViewGroup。
在activity运作中的任何时候,你都可以添加一个fragment到activity布局中,你只需简单的指定一个用来保护fragment的ViewGroup。

为了在activity中改变fragment,你需要使用FragmentTranslation类:
FragmentManager fragmentManager = getFragmentManager()
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
下面执行一个添加操作:
ExampleFragment fragment = new ExampleFragment();
fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit();
add()的第一个参数用来填充fragment的ViewGroup,第二个参数就是fragment了。

要让修改完成你必须调用commit()方法。

添加一个没有UI的fragment
你可以使用add(Fragment, string)来添加一个没有UI的fragment到activity中(第二个参数string为fragment提供一个唯一的tag,而不是ID)。由于这个fragment和布局无关,所以不用实现onCrateView()方法。

tag字符串在可视的fragment也可以使用,不过没有UI的fragment一定要有tag,activity需要使用findFragmentByTag()来使用fragment。

管理Fragments

要管理你的fragment需要使用FragmentManager,在activity中调用getFragmentManager()获得。

你可以通过FragmentManager做下面这个事:
  • 使用findFragmentById()或者findFragmentByTag()取得已经存在的fragment。
  • 使用popBackStack()把fragment从堆栈中取出。
  • 使用addOnBackStackChangedListener()注册堆栈的改变的监听。

执行Fragment事务

fragment能在activity中被添加,删除,替换等,这些操作都叫做事务,这些事务通过FragmentTransaction来处理。你也可以保存每个事务到activity管理的后退堆栈中,让用户可以返回以前的状态。

你可以从FragmentManager获得一个FragmentTransaction实例:
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
每个事务是一些你想同时改变的集合,意思就是你可以使用add(), remove(), replace()等方法组成一组操作集,然后通过commit()方法同时提交并执行。

在调用commit()之前,你可以调用addToBackStack()来添加事务到后退堆栈中,用户在按back键的时候能够恢复先前的fragment状态。

下面的例子是替换一个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();
如果你添加有多个改变的事务到后退堆栈中,那么恢复时候也是恢复所有的操作。

添加到FragmentTransaction的操作顺序并不重要,重要的是:
  • 你必须在最后调用commit()。
  • 如果你添加多个fragment到相同的容器中,那么添加的顺序指定了显示的顺序。
如果删除fragment时不调用addToBackStack(),那么提交后这个fragment会被销毁,如果调用了addToBackStack(),那么这个fragment只是被停止。

提示:每个fragment事务都可以添加一个事务动画,在提交前调用setTransition()实现。

调用commit()后不会立即执行事务,会在activity的UI线性允许的时候执行,如果必要的话,你也可以在UI线程中调用executePendingTransactions()来立即执行。

警告:commit()只能在activity保存状态前执行,不然会抛出异常。如果在保存状态后执行,在activity恢复状态时,提交的事务将会丢失。如果你不建议丢失事务,你可以使用commitAllowingStateLoss()代替commit()。

与Activity通信

fragment可以通过getActivity()来取得Activity布局中的视图:
View listView = getActivity().findViewById(R.id.list);
而activity可以通过FragmentManager的方法findFragmentById()或者findFragmentByTag()来取得fragment:
ExampleFragment fragment = (ExampleFragment) getFragmentManager().findFragmentById(R.id.example_fragment);
给activity创建事件回调
在一些情况下,你可能需要fragment分享事件给activity。实现这个的好办法就是在fragment中定义一个回调接口,然后在activity中实现这个接口。

例如,如果一个程序中有两个fragment,一个用来显示文章列表(fragment A),一个用来显示文件内容(fragment B),那么A在用户点击列表时需要告诉Activity那个文章被选中,然后B显示相应的文章内容。我们现在在A中声明一个OnArticleSelectedListener接口:
public static class FragmentA extends ListFragment {
    ...
    // Container Activity must implement this interface
    public interface OnArticleSelectedListener {
        public void onArticleSelected(Uri articleUri);
    }
    ...
}
为了保证activity必须实现这个接口,fragment的onAttach()(当activity添加一个fragment时会调用这个方法)回调函数需要实例化一个OnArticleSelectedListener。
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成员对象就持有一个实现了OnArticleSelectedListener的activity的引用。那么fragment A就可以使用mListener来调用activity中实现的onArticleSelected()方法:
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);
    }
    ...
}
id参数就是选项的行ID,activity使用这个id从ContentProvider中查询文章内容。

添加菜单项到Action Bar
你的fragment可以分配菜单项到activity的Options Menu(现在使用Action Bar),通过onCreateOptionsMenu()实现。

你也可以在fragment布局中注册一个view来提供context menu。

提示:activity会先接受到menu item选择的回调,如果activity不处理,才会传递到fragment中。

处理fragment生命周期

fragment和activity一样存在3个状态:

Resumed
在运行中的activity中可见。

Paused
其他activity在前台,不过包含fragment的activity依然可见。

Stopped
fragment不可见。activity被停止时,或者fragment被移除但是被添加到了后退堆栈时。这个状态下,会随着activity被杀掉而被杀掉。

和activity一样,你也可以使用Bundle保存fragment的状态,可以在onSaveInstanceState()中保存状态,在onCreate(), onCreateView(), 或者onActivityCreated()中恢复状态。

和activity不同的是activity的后退堆栈由系统管理,而fragment的堆栈由宿主activity管理。而且需要明确调用addToBackStack()才能记录事务。

警告:如果你需要一个Context对象,那么调用getActivity()取得,前提是fragment已经附加到activity中,不然返回null。

和activity生命周期协调工作
activity的生命周期直接影响fragment的生命周期,例如,当activity的onPause()被调用时,fragment对应的onPause()也被调用。

fragment有一些额外的生命周期回调:

onAttach()
fragment已经和activity有关联时调用。

onCreateView()
调用它创建和fragment有关的view层。

onActivityCreated()
activity的onCreate()调用返回时执行。

onDestroyView()
和view层关联的fragment被移除时调用。

onDetach()
fragment与activity失去关联时调用。



一旦activity处于resumed状态,你就可以随意添加和删除fragment了。当activity离开resumed状态时,你就可以随着上图周期对应的方法执行下去。

实例

下面是一个包含两个不同fragment的实例,一个显示莎士比亚剧集标题,一个显示对应剧集介绍。

在主activity应用一个布局:
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setContentView(R.layout.fragment_layout);
}
布局文件是:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent" android:layout_height="match_parent">

    <fragment class="com.example.android.apis.app.FragmentLayout$TitlesFragment"
            android:id="@+id/titles" android:layout_weight="1"
            android:layout_width="0px" android:layout_height="match_parent" />

    <FrameLayout android:id="@+id/details" android:layout_weight="1"
            android:layout_width="0px" android:layout_height="match_parent"
            android:background="?android:attr/detailsElementBackground" />

</LinearLayout>
使用这个布局,系统实例化TitlesFragment在activity加载布局时,刚开始的FrameLayout是空的,直到列表被选择。

不是所有的屏幕都能显示两个fragment,只有在横屏的时候才使用这个布局,所以保持这个文件在res/layout-land/fragment_layout.xml中。

当屏幕是竖屏时应用保存在下面文件中的布局:res/layout/fragment_layout.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="match_parent">
    <fragment class="com.example.android.apis.app.FragmentLayout$TitlesFragment"
            android:id="@+id/titles"
            android:layout_width="match_parent" android:layout_height="match_parent" />
</FrameLayout>
这个布局只包含TitlesFragment。意思就是说,竖屏时只显示剧集列表。当用户点击列表时,程序要启动一个新的activity来显示剧集介绍。

下面我先来看TitlesFragment是怎么实现的,这个fragment继承ListFragment来处理列表的工作。

当用户点击列表时,依据是否有两个布局来判断是创建一个新的fragment,还是开启一个新的activity。
public static class TitlesFragment extends ListFragment {
    boolean mDualPane;
    int mCurCheckPosition = 0;

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

        // Populate list with our static array of titles.
        setListAdapter(new ArrayAdapter<String>(getActivity(),
                android.R.layout.simple_list_item_activated_1, Shakespeare.TITLES));

        // Check to see if we have a frame in which to embed the details
        // fragment directly in the containing UI.
        View detailsFrame = getActivity().findViewById(R.id.details);
        mDualPane = detailsFrame != null && detailsFrame.getVisibility() == View.VISIBLE;

        if (savedInstanceState != null) {
            // Restore last state for checked position.
            mCurCheckPosition = savedInstanceState.getInt("curChoice", 0);
        }

        if (mDualPane) {
            // In dual-pane mode, the list view highlights the selected item.
            getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
            // Make sure our UI is in the correct state.
            showDetails(mCurCheckPosition);
        }
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putInt("curChoice", mCurCheckPosition);
    }

    @Override
    public void onListItemClick(ListView l, View v, int position, long id) {
        showDetails(position);
    }

    /**
     * Helper function to show the details of a selected item, either by
     * displaying a fragment in-place in the current UI, or starting a
     * whole new activity in which it is displayed.
     */
    void showDetails(int index) {
        mCurCheckPosition = index;

        if (mDualPane) {
            // We can display everything in-place with fragments, so update
            // the list to highlight the selected item and show the data.
            getListView().setItemChecked(index, true);

            // Check what fragment is currently shown, replace if needed.
            DetailsFragment details = (DetailsFragment)
                    getFragmentManager().findFragmentById(R.id.details);
            if (details == null || details.getShownIndex() != index) {
                // Make new fragment to show this selection.
                details = DetailsFragment.newInstance(index);

                // Execute a transaction, replacing any existing fragment
                // with this one inside the frame.
                FragmentTransaction ft = getFragmentManager().beginTransaction();
                ft.replace(R.id.details, details);
                ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
                ft.commit();
            }

        } else {
            // Otherwise we need to launch a new activity to display
            // the dialog fragment with selected text.
            Intent intent = new Intent();
            intent.setClass(getActivity(), DetailsActivity.class);
            intent.putExtra("index", index);
            startActivity(intent);
        }
    }
}
第二个fragment,DetailsFragment显示TitlesFragment中选择的列表项对应的剧集介绍:
public static class DetailsFragment extends Fragment {
    /**
     * Create a new instance of DetailsFragment, initialized to
     * show the text at 'index'.
     */
    public static DetailsFragment newInstance(int index) {
        DetailsFragment f = new DetailsFragment();

        // Supply index input as an argument.
        Bundle args = new Bundle();
        args.putInt("index", index);
        f.setArguments(args);

        return f;
    }

    public int getShownIndex() {
        return getArguments().getInt("index", 0);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        if (container == null) {
            // We have different layouts, and in one of them this
            // fragment's containing frame doesn't exist.  The fragment
            // may still be created from its saved state, but there is
            // no reason to try to create its view hierarchy because it
            // won't be displayed.  Note this is not needed -- we could
            // just run the code below, where we would create and return
            // the view hierarchy; it would just never be used.
            return null;
        }

        ScrollView scroller = new ScrollView(getActivity());
        TextView text = new TextView(getActivity());
        int padding = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                4, getActivity().getResources().getDisplayMetrics());
        text.setPadding(padding, padding, padding, padding);
        scroller.addView(text);
        text.setText(Shakespeare.DIALOGUE[getShownIndex()]);
        return scroller;
    }
}
下面是DetailsActivity的实现,竖屏的时候会使用到:
public static class DetailsActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (getResources().getConfiguration().orientation
                == Configuration.ORIENTATION_LANDSCAPE) {
            // If the screen is now in landscape mode, we can show the
            // dialog in-line with the list so we don't need this activity.
            finish();
            return;
        }

        if (savedInstanceState == null) {
            // During initial setup, plug in the details fragment.
            DetailsFragment details = new DetailsFragment();
            details.setArguments(getIntent().getExtras());
            getFragmentManager().beginTransaction().add(android.R.id.content, details).commit();
        }
    }
}
如果是竖屏的话,结束这个activity,以便横屏时可以显示DetailsFragment到TitlesFragment旁边。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值