碎片(Fragment)是一种可以嵌入在活动当中的UI片段,它能让程序更加合理和充分地利用大屏幕的空间,因而在平板上应用得非常广泛。虽然碎片对你来说应该是个全新的概念,但我相信你学习起来应该毫不费力,因为它和活动实在是太像了,同样都能包含布局,同样都有自己的生命周期。你甚至可以将碎片理解成一个迷你型的活动,虽然这个迷你型的活动有可能和普通的活动是一样大的。
那么究竟要如何使用碎片才能充分地利用平板屏幕的空间呢?想象我们正在开发一个新闻应用,其中一个界面使用RecyclerView展示了一组新闻的标题,当点击了其中一个标题时,就打开另一个界面显示新闻的详细内容。如果是在手机中设计,我们可以将新闻标题列表放在一个活动中,将新闻的详细内容放在另一个活动中,如图4.1所示。
图 4.1 手机的设计方案
可是如果在平板上也这么设计,那么新闻标题列表将会被拉长至填充满整个平板的屏幕,而新闻的标题一般都不会太长,这样将会导致界面上有大量的空白区域,如图4.2所示。
图 4.2 平板的新闻列表
因此,更好的设计方案是将新闻标题列表界面和新闻详细内容界面分别放在两个碎片中,然后在同一个活动里引入这两个碎片,这样就可以将屏幕空间充分地利用起来了,如图4.3所示。
图 4.3 平板的双页设计
介绍了这么多抽象的东西,也是时候学习一下碎片的具体用法了。你已经知道,碎片通常都是在平板开发中使用的,因此我们首先要做的就是创建一个平板模拟器。创建模拟器的方法我们在第1章已经学过了,创建完成后启动平板模拟器,效果如图4.4所示。
图 4.4 平板模拟器的运行效果
这里我们准备先写一个最简单的碎片示例来练练手,在一个活动当中添加两个碎片,并让这两个碎片平分活动空间。
新建一个左侧碎片布局left_fragment.xml,代码如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="Button"
/>
</LinearLayout>
这个布局非常简单,只放置了一个按钮,并让它水平居中显示。然后新建右侧碎片布局right_fragment.xml,代码如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:background="#00ff00"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:textSize="20sp"
android:text="This is right fragment"
/>
</LinearLayout>
可以看到,我们将这个布局的背景色设置成了绿色,并放置了一个TextView用于显示一段文本。
接着新建一个LeftFragment 类,并让它继承自Fragment。注意,这里可能会有两个不同包下的Fragment供你选择,一个是系统内置的android.app.Fragment,一个是support-v4库中的android.support.v4.app.Fragment。这里我强烈建议你使用support-v4库中的Fragment,因为它可以让碎片在所有Android系统版本中保持功能一致性。比如说在Fragment中嵌套使用Fragment,这个功能是在Android 4.2系统中才开始支持的,如果你使用的是系统内置的Fragment,那么很遗憾,4.2系统之前的设备运行你的程序就会崩溃。而使用support-v4库中的Fragment就不会出现这个问题,只要你保证使用的是最新的support-v4库就可以了。另外,我们并不需要在build.gradle文件中添加support-v4库的依赖,因为build.gradle文件中已经添加了appcompat-v7库的依赖,而这个库会将support-v4库也一起引入进来。
现在编写一下LeftFragment 中的代码,如下所示:
public class LeftFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.left_fragment, container, false);
return view;
}
}
这里仅仅是重写了Fragment的onCreateView()
方法,然后在这个方法中通过LayoutInflater的inflate()
方法将刚才定义的left_fragment布局动态加载进活动中来,整个方法简单明了。
**问题来了,上面所提到的,将自己对应的布局文件left_fragment.xml
以及right_fragment.xml
加载进来,那什么时候加载进来呢?**这个问题在碎片布局的引入执行逻辑一章中再进行回答。
接着我们用同样的方法再新建一个RightFragment ,代码如下所示:
public class RightFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.right_fragment, container, false);
return view;
}
}
基本上代码都是相同的,相信已经没有必要再做什么解释了。接下来修改activity_main.xml中的代码,如下所示:
<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:id="@+id/left_fragment"
android:name="com.example.fragmenttest.LeftFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<fragment
android:id="@+id/right_fragment"
android:name="com.example.fragmenttest.RightFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
</LinearLayout>
可以看到,我们使用了<fragment>
标签在布局中添加碎片,其中指定的大多数属性都是你熟悉的,只不过这里还需要通过android:name 属性来显式指明要添加的碎片类名,注意一定要将类的包名也加上(因为不加上就不知道此fragment标签是由哪一个类实现的)。
这样最简单的碎片示例就已经写好了,现在运行一下程序,效果如图4.5所示。
图 4.5 碎片的简单运行效果
正如我们所期待的一样,两个碎片平分了整个活动的布局。不过这个例子实在是太简单了,在真正的项目中很难有什么实际的作用,因此我们马上来看一看,关于碎片更加高级的使用技巧。
现在可以回答上述问题了,究竟何时何地加载了两个碎片布局。由于我们在MainActivity方法中调用了方法:setContentView(R.layout.activity_main);
所以只会加载布局文件activity_main.xml
,而我们在此布局文件中添加了两个fragment控件,而实际上其通过:android:name="com.example.fragmenttest.LeftFragment"
指向了类文件:LeftFragment.java
,(我们不是通过android:id="@+id/left_fragment"
知道这个碎片控件实现类是谁,而是android:name
来控制的),而类文件LeftFragment.java
则重写了方法onCreateView()
,使其返回一个我们所指定的的布局View对象,而这个对象是由R.layout.left_fragment
指向了:left_fragment.xml
。
所以执行逻辑可以认为是大致如下:
MainActivity#onCreate
-> activity_main.xml -> <fragment>
-> <fragment>
标签中的android:name
-> LeftFragment
类 ->LayoutInflater#inflate(int, android.view.ViewGroup, boolean)
方法 -> left_fragment.xml
-> right_fragment同理。
可以发现实际上上述代码执行顺序和我们写代码的顺序是完全相反的,我们首先要写一个关于fragment的布局xml文件,接着创建一个碎片类去引用这个布局文件,最后第二步是在activity_main文件中通过android:name
来引用这个碎片类,最后才是在MainActivity中加载activity_main布局。可以说这样写代码的好处是不会IDE是不会报错引用错误,坏处是和程序的执行顺序正好相反,但是如果你深谙代码的执行逻辑,首先就是在activity_main文件中通过android:name
来引用这个碎片类,一步步你想思维,我想可能也是一个写Android代码的好思维方式。
所以如果你知道如果引用控件的话,那么碎片的加入布局文件与引用控件则有非常大的区别。其不能通过inclue来实现,比如说:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<include layout="@layout/right_fragment"/>
<include layout="@layout/left_fragment"/>
</LinearLayout>
此布局对于app主活动的影响就是:
如果你以下两句代码交换顺序,
<include layout="@layout/right_fragment"/>
<include layout="@layout/left_fragment"/>
那么就会达到以下的情况:(由于两个布局都是全屏幕的,所以第二个引入完全没有起到效果)
所以说这样一来完全没有能够得到想要的碎片布局的效果。
在上一节当中,你已经学会了在布局文件中添加碎片的方法,不过碎片真正的强大之处在于,它可以在程序运行时动态地添加到活动当中。根据具体情况来动态地添加碎片,你就可以将程序界面定制得更加多样化。
我们还是在上一节代码的基础上继续完善,新建another_right_fragment.xml,代码如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:background="#ffff00"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:textSize="20sp"
android:text="This is another right fragment"
/>
</LinearLayout>
这个布局文件的代码和right_fragment.xml中的代码基本相同,只是将背景色改成了黄色,并将显示的文字改了改。然后新建AnotherRightFragment 作为另一个右侧碎片,代码如下所示:
public class AnotherRightFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.another_right_fragment, container,
false);
return view;
}
}
代码同样非常简单,在onCreateView() 方法中加载了刚刚创建的another_right_fragment布局。这样我们就准备好了另一个碎片,接下来看一下如何将它动态地添加到活动当中。修改activity_main.xml,代码如下所示:
<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:id="@+id/left_fragment"
android:name="com.example.fragmenttest.LeftFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<FrameLayout
android:id="@+id/right_layout"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" >
</FrameLayout>
</LinearLayout>
可以看到,现在将右侧碎片替换成了一个FrameLayout中,还记得这个布局吗?在上一章中我们学过,这是Android中最简单的一种布局,所有的控件默认都会摆放在布局的左上角**。由于这里仅需要在布局里放入一个碎片,不需要任何定位,因此非常适合使用FrameLayout**。
下面我们将在代码中向FrameLayout里添加内容,从而实现动态添加碎片的功能。修改MainActivity中的代码,如下所示:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(this);
replaceFragment(new RightFragment());
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.button:
replaceFragment(new AnotherRightFragment());
break;
default:
break;
}
}
private void replaceFragment(Fragment fragment) {
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.replace(R.id.right_layout, fragment);
transaction.commit();
}
}
可以看到,首先我们给左侧碎片中的按钮注册了一个点击事件,然后调用replaceFragment() 方法动态添加了RightFragment这个碎片。当点击左侧碎片中的按钮时,又会调用replaceFragment() 方法将右侧碎片替换成AnotherRightFragment。结合replaceFragment() 方法中的代码可以看出,动态添加碎片主要分为5步。
(1) 创建待添加的碎片实例。
(2) 获取FragmentManager,在活动中可以直接通过调用getSupportFragmentManager() 方法得到。
(3) 开启一个事务,通过调用beginTransaction() 方法开启。
(4) 向容器内添加或替换碎片,一般使用replace() 方法实现,需要传入容器的id和待添加的碎片实例。
(5) 提交事务,调用commit() 方法来完成。
这样就完成了在活动中动态添加碎片的功能,重新运行程序,可以看到和之前相同的界面,然后点击一下按钮,效果如图4.6所示。
图 4.6 动态添加碎片的效果
如果想要得到一种效果:按BUTTON一下就会使右边的两个布局切换,只要将MainActivity.java的onCreate()
方法改成以下逻辑即可:
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.button:
if (flag) {
replaceFragment(new AnotherRightFragment());
flag = false;
break;
}
replaceFragment(new RightFragment());
flag=true;
break;
default:
break;
}
}
在上一小节中,我们成功实现了向活动中动态添加碎片的功能,不过你尝试一下就会发现,通过点击按钮添加了一个碎片之后,这时按下Back键程序就会直接退出。如果这里我们想模仿类似于返回栈的效果,按下Back键可以回到上一个碎片,该如何实现呢?
其实很简单,FragmentTransaction中提供了一个addToBackStack() 方法,可以用于将一个事务添加到返回栈中,修改MainActivity中的代码,如下所示:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
...
private void replaceFragment(Fragment fragment) {
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.replace(R.id.right_layout, fragment);
transaction.addToBackStack(null);
transaction.commit();
}
}
这里我们在事务提交之前调用了FragmentTransaction的addToBackStack() 方法,它可以接收一个名字用于描述返回栈的状态,一般传入null 即可。现在重新运行程序,并点击按钮将AnotherRightFragment添加到活动中,然后按下Back键,你会发现程序并没有退出,而是回到了RightFragment界面,继续按下Back键,RightFragment界面也会消失,再次按下Back键,程序才会退出。
虽然碎片都是嵌入在活动中显示的,可是实际上它们的关系并没有那么亲密。你可以看出,碎片和活动都是各自存在于一个独立的类当中的,它们之间并没有那么明显的方式来直接进行通信。如果想要在活动中调用碎片里的方法,或者在碎片中调用活动里的方法,应该如何实现呢?
为了方便碎片和活动之间进行通信,FragmentManager提供了一个类似于findViewById() 的方法,专门用于从布局文件中获取碎片的实例,代码如下所示:
RightFragment rightFragment = getSupportFragmentManager()
.findFragmentById(R.id.right_fragment);
调用FragmentManager的findFragmentById() 方法,可以在活动中得到相应碎片的实例,然后就能轻松地调用碎片里的方法了。
掌握了如何在活动中调用碎片里的方法,那在碎片中又该怎样调用活动里的方法呢?其实这就更简单了,在每个碎片中都可以通过调用getActivity() 方法来得到和当前碎片相关联的活动实例,代码如下所示:
MainActivity activity = getActivity();
有了活动实例之后,在碎片中调用活动里的方法就变得轻而易举了。另外当碎片中需要使用Context 对象时,也可以使用getActivity() 方法,因为获取到的活动本身就是一个Context 对象。
这时不知道你心中会不会产生一个疑问:既然碎片和活动之间的通信问题已经解决了,那么碎片和碎片之间可不可以进行通信呢?
说实在的,这个问题并没有看上去那么复杂,它的基本思路非常简单,首先在一个碎片中可以得到与它相关联的活动,然后再通过这个活动去获取另外一个碎片的实例,这样也就实现了不同碎片之间的通信功能,因此这里我们的答案是肯定的。
和活动一样,碎片也有自己的生命周期,并且它和活动的生命周期实在是太像了,我相信你很快就能学会,下面我们马上就来看一下。
还记得每个活动在其生命周期内可能会有哪几种状态吗?没错,一共有运行状态、暂停状态、停止状态和销毁状态这4种。类似地,每个碎片在其生命周期内也可能会经历这几种状态,只不过在一些细小的地方会有部分区别。
结合之前的活动状态,相信你理解起来应该毫不费力吧。同样地,Fragment 类中也提供了一系列的回调方法,以覆盖碎片生命周期的每个环节。其中,活动中有的回调方法,碎片中几乎都有,不过碎片还提供了一些附加的回调方法,那我们就重点看一下这几个回调。
onAttach() 。当碎片和活动建立关联的时候调用。
onCreateView() 。为碎片创建视图(加载布局)时调用。
onActivityCreated() 。确保与碎片相关联的活动一定已经创建完毕的时候调用。
onDestroyView() 。当与碎片关联的视图被移除的时候调用。
onDetach() 。当碎片和活动解除关联的时候调用。
碎片完整的生命周期示意图可参考图4.7,图片源自Android官网。
图 4.7 碎片的生命周期
为了让你能够更加直观地体验碎片的生命周期,我们还是通过一个例子来实践一下。例子很简单,仍然是在FragmentTest项目的基础上改动的。
修改RightFragment中的代码,如下所示:
public class RightFragment extends Fragment {
public static final String TAG = "RightFragment";
@Override
public void onAttach(Context context) {
super.onAttach(context);
Log.d(TAG, "onAttach");
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, "onCreate");
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Log.d(TAG, "onCreateView");
View view = inflater.inflate(R.layout.right_fragment, container, false);
return view;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
Log.d(TAG, "onActivityCreated");
}
@Override
public void onStart() {
super.onStart();
Log.d(TAG, "onStart");
}
@Override
public void onResume() {
super.onResume();
Log.d(TAG, "onResume");
}
@Override
public void onPause() {
super.onPause();
Log.d(TAG, "onPause");
}
@Override
public void onStop() {
super.onStop();
Log.d(TAG, "onStop");
}
@Override
public void onDestroyView() {
super.onDestroyView();
Log.d(TAG, "onDestroyView");
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy");
}
@Override
public void onDetach() {
super.onDetach();
Log.d(TAG, "onDetach");
}
}
我们在RightFragment中的每一个回调方法里都加入了打印日志的代码,然后重新运行程序,这时观察logcat中的打印信息,如图4.8所示。
图 4.8 启动程序时的打印日志
可以看到,当RightFragment第一次被加载到屏幕上时,会依次执行onAttach() 、onCreate() 、onCreateView() 、onActivityCreated() 、onStart() 和onResume() 方法。然后点击LeftFragment中的按钮,此时打印信息如图4.9所示。
图 4.9 替换成AnotherRightFragment时的打印日志
由于AnotherRightFragment替换了RightFragment,此时的RightFragment进入了停止状态,因此onPause() 、onStop() 和onDestroyView() 方法会得到执行。当然如果在替换的时候没有调用addToBackStack() 方法,此时的RightFragment就会进入销毁状态,onDestroy() 和onDetach() 方法就会得到执行。
接着按下Back键,RightFragment会重新回到屏幕,打印信息如图4.10所示。
图 4.10 返回RightFragment时的打印日志
由于RightFragment重新回到了运行状态,因此onCreateView() 、onActivityCreated() 、onStart() 和onResume() 方法会得到执行。注意此时onCreate() 方法并不会执行,因为我们借助了addToBackStack() 方法使得RightFragment并没有被销毁。
现在再次按下Back键,打印信息如图4.11所示。
图 4.11 退出程序时的打印日志
依次会执行onPause() 、onStop() 、onDestroyView() 、onDestroy() 和onDetach() 方法,最终将碎片销毁掉。这样碎片完整的生命周期你也体验了一遍,是不是理解得更加深刻了?
另外值得一提的是,在碎片中你也是可以通过onSaveInstanceState() 方法来保存数据的,因为进入停止状态的碎片有可能在系统内存不足的时候被回收。保存下来的数据在onCreate() 、onCreateView() 和onActivityCreated() 这3个方法中你都可以重新得到,它们都含有一个Bundle类型的savedInstanceState 参数。具体的代码我就不在这里给出了,如果你忘记了该如何编写,可以参考2.4.5小节。