Fragment,碎片,是Android 3.0(API level 11)之后新增加的特性。Fragment必须被嵌入到Activity中使用,并且它的生命周期直接受到Activity的生命周期的影响,当Activity被暂停时,在它里面的Fragment也会被暂停,当Activity被销毁时,它里面所有的Fragment都会被销毁。在Activity中的Fragment可以理解为一种行为或者用户UI界面的一部分,可以在一个Activity中包含一个或者多个Fragment,结合起来创建一个UI布局,并且可以在多个Activity中重复使用同一个Fragment。在Activity的运行周期内,Fragment拥有自己的生命周期,接收自己的监听事件,并且可以在Activity运行时,通过Fragment事务动态添加或者移除Fragment,并把Fragment事务加入到Activity的任务栈中,这样即便是点击了回退键(向后导航),返回的也是上一个Fragment操作,而非上一个Activity。
接下来将详细介绍Fragment的使用。
Fragment,主要是为了让大屏幕设备上能支持更具有动态性和灵活性的UI设计,如平板电脑。因为现在Android设备的屏幕越来越大,使用Fragment可以更灵活的管理视图层次的变化。Fragment像Activity一样,可以创建Fragment来加载一个View进行布局,但是Fragment必须嵌入在Activity中,不能单独存在,而且一个Activity可以嵌入多个Fragment,同时一个Fragment可以被多个Activity重复使用。
上图以很清晰的说明Activity和Fragment的关系。在平板电脑中,因为屏幕大,显示的内容全,如果通过Activity跳转的方式去加载新的Activity显示详细内容,将浪费屏幕资源。而如左上图所示,可以结合Fragment布局,使一个Activity左右分别包含一个Fragment,这样可以通过对左边Fragment的操作来影响右边Fragment的显示内容,例如:新闻阅读,系统设置等。如果一个应用的是采用Activity+Fragment结合的方式布局,那么可以很方便的在平板与手机之间相互移植,大部分代码是可以重复利用的,并且Fragment无需在AndroidManifest.xml清单文件中注册。
使用Fragment必须继承这个类或其子类,Fragment或其子类的代码包含了一些类似于Activity的生命周期方法,如:onCreate()、onStart()、onPaused()、onStop()等。实际上如果需要把一个现有的Activity转换成一个Fragment,只需要简单的移植Activity的一些方法即可。
通常情况下,并不需要对Fragment包含的生命周期方法全部重写,只需要重写其中的一部分。一般而言,会重写如下几个Fragment方法:
· onCreate():当创建Fragment的时候会调用这个方法,一般在这个方法中做一些Fragment初始化的工作。
· onCreateView():当Fragment首次被绘制在UI界面上时,系统会调用这个方法,返回一个View对象,这个View是Fragment布局的根View,如果Fragment不需要显示,那么可以返回一个null。
· onPaused():在用户暂停Fragment的时候被调用,应该在其中做Fragment被销毁前的准备工作,例如回收资源、保存临时数据等。
在上述三个推荐实现的Fragment生命周期方法中,着重讲解onCreateView()方法,它用来加载Fragment的显示View。onCreateView()的完整签名如下:
public View onCreateView(LayoutInflater inflater,ViewGroup container,Bundle savedInstanceState)
其中参数的意义如下:
· inflater:当前布局的内容填充者,可以用inflater.inflate()方法去填充一个布局资源文件。
· container:为包裹当前Fragment的容器,一般是一个布局控件。
· savedInstanceState:如果为非空,表示之前被非正常销毁时保存的状态。
onCreateView()方法返回一个View对象,用于表示Fragment显示的视图,在其中可以使用inflater.inflate()方法动态膨胀一个XML布局资源,得到Fragment显示的View。inflate()方法的完整签名如下:
public View inflate(int resource,ViewGroup root,boolean attachToRoot)
其中参数的意义如下:
· resource:动态膨胀的布局资源ID。
· root:膨胀出的View的上层布局对象,一般传递onCreateView的container参数即可。
· attachToRoot:指定展开的布局是否依附到root这个ViewGroup中,一般传递false即可。
使用Fragment需要通过<fragment…/>元素来在XML布局资源文件中添加Fragment,它的属性与大多数UI组件一样,但是有两个属性需要特别注意:
· android:name:这个Fragment的实现类。
· android:layout_weight:当前Fragment在Activity中的权重,数值越大,在Activity中占的空间越大。
示例:演示如何在Activity中使用Fragment进行布局,在此示例中,为一个Activity中加入两个Fragment。
首先创建Activity的布局资源文件。
代码清单:\codes\05\05\FragmentDemo\res\layout\activity_fragment.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" > <fragment android:id="@+id/fragment1" android:name="com.example.fragmentSimple.Fragment1" android:layout_width="0px" android:layout_height="match_parent" android:layout_weight="2" /> <fragment android:id="@+id/fragment2" android:name="com.example.fragmentSimple.Fragment2" android:layout_width="0px" android:layout_height="match_parent" android:layout_weight="1" /> </LinearLayout>
定义两个Fragment,分别加载一个简单的XML布局资源。
代码清单:\codes\05\05\FragmentDemo\src\com\bookdemo\fragmentSimple\Fragment1.java
public class Fragment1 extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // 填充一个布局View到ViewGrope中 return inflater.inflate(R.layout.fragment1, container, false); } }
代码清单:\Fragment2.java
public class Fragment2 extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment2, container, false); } }
在模拟器上运行效果如下图:
Fragment只能依托于Activity,但是在Activity的运行时,Fragment还具有自己的生命周期,除了前面介绍过的一些Fragment生命周期方法,Fragment还提供了不同的与生命周期对应的方法,如下图所示:
从上图中可以看出Fragment的生命周期和Activity基本一样,但有两个生命周期方法需要注意,即onAttach()附加和onDetach()剥离。从这两个方法的位置可以看出,Fragment在创建的时候,是先附加到Activity中,然后才开始从onCreateView()中加载View的。在生命周期结束的时候,也是先销毁onDestroy()然后才回调onDetach()从Activity中剥离这个Fragment。
要管理Fragment,必须保证这个Fragment能够被找到,所以必须在创建Fragment的时候,为它指定一个唯一的标识,方便Activity在运行时通过此标识找到待操作的Fragment。有三种方式可以为Fragment提供唯一的标识:
· 使用android:id属性,为Fragment指定一个唯一的ID。
· 使用android:tag属性,为Fragment指定一个唯一的标识。
· 如果以上两种属性均没有设置,那么系统会使用包裹容器中的ID
Fragment具有唯一标识之后,还需要使用Activity中的FragmentManager类来管理其中的Fragment。为了获得FragmentManager对象,可以在Activity中调用getFragmentManager()方法。使用FragmentManager可以做的的事情包括:
· 使用findFragmentByID()或者findFragmentByTag()方法从Activity中获得唯一的Fragment对象
· 使用popBackStack()方法模拟一个向后导航,使当前回退栈中的Fragment弹栈。
· 使用addOnBackStackChangeListener()方法注册一个回退栈改变的监听器,监听在回退栈中加入Fragment的事件
Fragment很大的一个特点就是可以在Activity中动态的添加、移除和执行。在Activity中对Fragment的每一组改变被称为一个Fragment事务,以FragmentTransaction对象表示,这个对象可以通过FragmentManager.beginTransaction()方法获得,它将开启一个Fragment事务,用于操作ViewGroup中的Fragment。
FragmentTransaction的常用方法有:
· add():增加一个Fragment,具有多个重载。
· replace():替换一个Fragment,具有多个重载。
· remove():移除掉一个指定的Fragment。
· addToBackStack():将此次改变Fragment的事务添加到Activity的任务栈中。
· commit():提交事务的改变。
其中add、replace、remove都是很常见的方法。但是addToBackStack()方法在正常情况下,应用中的Activity是有一个任务栈去管理它的。默认情况下,在不同的Activity中跳转时,点击回退总是能回到上一个Activity中。而Fragment是嵌套在Activity,所以默认无法向Activity的任务栈中添加,当点击回退的时候只会回到上一个Activity,不会理会Fragment的操作(add、replace、remove),而使用addToBackStack()可以将当前的事务添加到另一个栈中,这个栈由Fragment的Activity管理,这个栈中的每一条都是一个Fragment的一次事务,有了这个栈去管理Fragment,就可以通过回退按键,反向回滚Fragment的事务。这一点很重要,因为Fragment无需在清单文件中配置,所以现在有些应用会使用Fragment来布局跳转。在每次Fragment事务的结束,还必须调用commit()方法,用于提交事务,否则不予执行。
示例:演示如何使用Fragment事务动态操作Fragment。在示例中,会实现一个类似于分栏的效果,在左边点击不同的项会动态改变右边的内容。
首先定义一个Activity的布局资源文件。
代码清单:\codes\05\05\FragmentDemo\res\layout\activity_fragmenttab.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" > <LinearLayout android:layout_width="wrap_content" android:layout_height="match_parent" android:orientation="vertical" > <TextView android:id="@+id/tabfgt1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="fragment1" /> <TextView android:id="@+id/tabfgt2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="fragment2" /> <TextView android:id="@+id/tabfgt3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="fragment3" /> </LinearLayout> <LinearLayout android:id="@+id/content" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > </LinearLayout> </LinearLayout>
再声明一个Activity,使用Fragment事务动态操作Fragment。
代码清单:\codes\05\05\FragmentDemo\src\com\bookdemo\fragmentTab\FragmentTabActivity.java
public class FragmentTabActivity extends Activity { private TextView tabfgt1, tabfgt2, tabfgt3; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_fragmenttab); tabfgt1 = (TextView) findViewById(R.id.tabfgt1); tabfgt2 = (TextView) findViewById(R.id.tabfgt2); tabfgt3 = (TextView) findViewById(R.id.tabfgt3); tabfgt1.setOnClickListener(click); tabfgt2.setOnClickListener(click); tabfgt3.setOnClickListener(click); } private View.OnClickListener click = new View.OnClickListener() { @Override public void onClick(View v) { tabfgt1.setBackgroundColor(Color.GRAY); tabfgt2.setBackgroundColor(Color.GRAY); tabfgt3.setBackgroundColor(Color.GRAY); // 获取FragmentManager对象 FragmentManager fm = getFragmentManager(); // 开启事务 FragmentTransaction ft = fm.beginTransaction(); switch (v.getId()) { case R.id.tabfgt1: tabfgt1.setBackgroundColor(Color.GREEN); // 替换R.id.content中的Fragment ft.replace(R.id.content, new Fragment1()); break; case R.id.tabfgt2: tabfgt2.setBackgroundColor(Color.YELLOW); ft.replace(R.id.content, new Fragment2()); break; case R.id.tabfgt3: tabfgt3.setBackgroundColor(Color.RED); ft.replace(R.id.content, new Fragment3()); break; default: break; } // 提交事务 ft.commit(); } }; }
在模拟器上运行效果如下:
如何操作两个不同Fragment中的控件?在Activity中,操作一个控件需要通过findViewById(int)方法通过控件的ID去找到控件,而使用Fragment其实到最后Fragment.onCreateActivity()的时候是把膨胀的View加载到Activity中,可以直接在Activity中通过findViewById()方法找到控件,进而操作它,这一点和直接操作Activity的方式一致。但是如果需要在一个Fragment中操作另外一个Fragment的控件,就需要用到Fragment.getActivity()来获取到当前Fragment承载的Activity对象,拿到这个Activity对象,获取到其中的控件就不成问题了。
示例:演示如何在不同的Fragment中进行交互。在Activity中加载三个Fragment,在其中一个Fragment中的Button点击的时候,修改其它Fragment的值。
首先声明一个XML布局资源文件,其中加载了三个Fragment,且有两个Fragment是一样的。
代码清单:\codes\05\05\FragmentDemo\res\layout\activity_fragmentturn.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" > <fragment android:id="@+id/fragment1" android:name="com.example.fragmentSimple.Fragment1" android:layout_width="0px" android:layout_height="match_parent" android:layout_weight="1" /> <!-- 加载了两个Fragment1 --> <fragment android:id="@+id/fragment2" android:name="com.example.fragmentSimple.Fragment1" android:layout_width="0px" android:layout_height="match_parent" android:layout_weight="1" /> <fragment android:id="@+id/fragment3" android:name="com.example.fragmentTurn.Fragment3" android:layout_width="0px" android:layout_height="match_parent" android:layout_weight="1" /> </LinearLayout>
声明的第三个Fragment带Button组件,在其中添加点击事件监听器,修改其它Fragment的数据。
代码清单:\codes\05\05\FragmentDemo\src\com\bookdemo\fragmentTurn\Fragment3.java
public class Fragment3 extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment3, container, false); } @Override public void onStart() { super.onStart(); // 方法2: 在Fragment中获取操作其它Fragment的控件 Button btnGetText2=(Button)getActivity().findViewById(R.id.btnGetText2); btnGetText2.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // 使用FragmentManager通过ID找到需要操作的fragment对象 Fragment fragment=getFragmentManager().findFragmentById(R.id.fragment2); // 获得Fragment对象显示的根View View view=fragment.getView(); // 找到控件 TextView tv=(TextView) view.findViewById(R.id.tvFragment1); tv.setText("Fragment中动态改变"); } }); } }
声明一个Activity来承载XML布局资源,并且在其中为第三个Fragment的一个Button组件添加点击事件修改其它Fragment的数据。
代码清单:\codes\05\05\FragmentDemo\src\com\bookdemo\fragmentTurn\FragmentTurnActivity.javas
public class FragmentTurnActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_fragmentturn); // 方法1:在Activity中操作旗下Fragment中的控件 Button btn=(Button)findViewById(R.id.btnGetText); btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { TextView tv=(TextView)findViewById(R.id.tvFragment1); tv.setText("Activity中动态修改"); } }); } }
在模拟器上运行,分别点击Fragment3下的两个按钮,效果如下图:
上面的例子有一个问题,无论是在Activity中使用findViewById()还是在Fragment中使用getActivity().findViewById(),虽然可以获取到控件,但是有个例外的情况。就是在Activity中,同时使用了两个相同的Fragment,这个时候仅仅使用上面介绍的方法,只能通过id获取到第一个Fragment中的控件。因为,在布局文件中定义的控件,就算ID重复了,Android SDK自动维护的R.java类中,也只会声明一次,想在Activity中通过控件ID区分同一个Fragment类的两个实例中的控件,是无法做到的。
那么就需要换一个思路,可以在Fragment使用getFragmentManager().findFragmentById(int)通过Fragment的Id找到待操作的Fragment对象,然后使用getView()方法获得其中的View对象,使用View.findViewById(int)找到Fragment的对应Id的控件,进而操作它,如示例所示。
为了兼容低版本的Android设备,Google推出了android.support.v4.app.Fragment开发包。如果考虑向下兼容的问题,推荐直接引用android.support.v4.app.Fragment包进行开发。