迎新年,High 翻天

Android 组内训—— Fragment 特别篇(含惊喜)

QSC TECH最崇拜钥匙的,同时也是最弟弟的成员:三个 杨宇昊

Google 对于 Fragment官方文档

贵潮 Gitlab 上的 FragmentDemo 项目

什么是 Fragment ?

Fragment,简称碎片。自从谷歌在 Android3.0(还记得吗?为了平板而推出的3.0)推出 Fragment 以后,Fragment 就成为了绝大多数 APP 的必备元素,其重要程度一点也不亚于四大组件。

复习:四大组件——

  1. Activity
  2. Service
  3. Content Provider
  4. Broadcast Receiver

提问环节:我将抽一个小朋友提问关于这四大组件的问题,作为新年礼物。会是谁这么幸运呢?

参考答案

Activity

Activity 是一个应用组件,用户可与其提供的屏幕进行交互,以执行拨打电话、拍摄照片、发送电子邮件或查看地图等操作。 每个 Activity 都会获得一个用于绘制其用户界面的窗口。窗口通常会充满屏幕,但也可小于屏幕并浮动在其他窗口之上。

一个应用通常由多个彼此松散联系的 Activity 组成。 一般会指定应用中的某个 Activity 为“主”Activity,即首次启动应用时呈现给用户的那个 Activity。 而且每个 Activity 均可启动另一个 Activity,以便执行不同的操作。 每次新 Activity 启动时,前一 Activity 便会停止,但系统会在堆栈(“返回栈”)中保留该 Activity。 当新 Activity 启动时,系统会将其推送到返回栈上,并取得用户焦点。 返回栈遵循基本的“后进先出”堆栈机制,因此,当用户完成当前 Activity 并按“返回”按钮时,系统会从堆栈中将其弹出(并销毁),然后恢复前一 Activity。

当一个 Activity 因某个新 Activity 启动而停止时,系统会通过该 Activity 的生命周期回调方法通知其这一状态变化。Activity 因状态变化—系统是创建 Activity、停止 Activity、恢复 Activity 还是销毁 Activity— 而收到的回调方法可能有若干种,每一种回调都会为您提供执行与该状态变化相应的特定操作的机会。 例如,停止时,您的 Activity 应释放任何大型对象,例如网络或数据库连接。 当 Activity 恢复时,您可以重新获取所需资源,并恢复执行中断的操作。 这些状态转变都是 Activity 生命周期的一部分。

Service

Service表示一个应用期望去执行一个不与用户交互的较长的操作或者通过一些功能为其他应用去使用。Service是Android提供一个允许长时间留驻后台的一个组件,最常见的用法就是做轮询操作或者想在后台做一些事情,比如后台下载更新。Service通常总是称之为“后台服务”,其中“后台”一词是相对于前台而言的,具体是指其本身的运行并不依赖于用户可视的UI界面,因此,从实际业务需求上来理解,Service的适用场景应该具备以下条件:

  1. 并不依赖于用户可视的UI界面(当然,这一条其实也不是绝对的,如前台Service就是与Notification界面结合使用的);
  2. 具有较长时间的运行特性。

Content Provider

ContentProvider是不同应用程序之间进行数据交换的标准API。ContentProvider可以理解为一个Android应用对外开放的接口,只要是符合它所定义的Uri格式的请求,均可以正常访问执行操作。其他的Android应用可以使用ContentResolver对象通过与ContentProvider同名的方法请求执行,被执行的就是ContentProvider中的同名方法。

Broadcast Receiver

在Android中,Broadcast是一种广泛运用的在应用程序之间传输信息的机制。而BroadcastReceiver是对发送出来的 Broadcast进行过滤接受并响应的一类组件。

下面将详细的阐述如何发送Broadcast和使用BroadcastReceiver过滤接收的过程:

首先在需要发送信息的地方,把要发送的信息和用于过滤的信息(如Action、Category)装入一个Intent对象,然后通过调用 sendOrderBroadcast()或sendStickyBroadcast()方法,把 Intent对象以广播方式发送出去。

当Intent发送以后,所有已经注册的BroadcastReceiver会检查注册时的IntentFilter是否与发送的Intent相匹配,若匹配则就会调用BroadcastReceiver的onReceive()方法。所以当我们定义一个BroadcastReceiver的时候,都需要实现onReceive()方法。

打碎 Activity

从字面上来看,Fragment 的意思是碎片,谷歌的本意在于将一个 Activity 的界面进行碎片化,好让开发者根据不同的屏幕来进行不同的 Fragment 组合以来达到动态布局的效果。

但从目前的情况来看,因为 Android 平板电脑的市场占有率偏低,多数应用都未对平板进行单独适配,即使有适配的 APP 也是单独维护一个平板的项目与手机项目剥离开来进行UI的编写与适配。但是这并没有影响到广大开发者对 Fragment 的喜爱,因为 fragment 作为一个 UI 界面的载体,它的使用上十分灵活。

同时更重要的一点是,同样实现一个界面,Fragment 相对于 Activity 来说更加省内存,可以说它是一个更加轻量级的界面载体。如果说我们的应用里有一百个界面,如果全用 Activity 来进行实现的话,那么整个应用跑起来以后内存的消耗是极大的,而如果采用 Activity+Fragment 的实现方式,则可大大降低内存的消耗。所以 Fragment 在 Android 开发者当中是十分收欢迎的。

你可以把 Fragment 当成 Activity 一个界面的一部分,甚至 Activity 的界面由完全不同的 Fragment 组成,更帅气的是 Fragment 有自己的声明周期和接收、处理用户的事件,这样就不必要在一个 Activity 里面写一堆事件、控件的代码了。更为重要的是,你可以动态的添加、替换、移除某个 Fragment 。

Fragment 和 Activity 的关系

  • Fragment 是依赖于 Activity 的,不能独立存在的。
  • 一个 Activity 里可以有多个 Fragment。
  • 一个 Fragment 可以被多个 Activity 重用。
  • Fragment 有自己的生命周期,并能接收输入事件。
  • 我们能在 Activity 运行时动态地添加或删除 Fragment 。

Fragment 和 Activity 的两者的生命周期

生命周期

可以看到 Fragment 比 Activity 多了几个额外的生命周期回调函数:

  • onAttach(Activity);  //当 Activity 与 Fragment 发生关联时调用
  • onCreateView(LayoutInflater,ViewGroup,Bundle);  //创建该 Fragment 的视图
  • onActivityCreate(bundle);  //当 Activity 的 onCreate() ;方法返回时调用
  • onDestoryView();  //与 onCreateView 相对应,当改 Fragment 被移除时调用
  • onDetach();  //与 onAttach() 相对应,当 Fragment 与 Activity 的关联被取消时调用

注意:除了 onCreateView ,其他的所有方法如果你重写了,必须调用父类对于该方法的实现。

Fragment 优势

  • 模块化(Modularity):我们不必把所有代码全部写在 Activity 中,而是把代码写在各自的 Fragment 中。
  • 可重用(Reusability):多个 Activity 可以重用一个 Fragment 。
  • 可适配(Adaptability):根据硬件的屏幕尺寸、屏幕方向,能够方便地实现不同的布局,这样用户体验更好。

Fragment 优势

在贵潮 Mobile3 中,打开左侧的 Drawer 中的任一按钮,弹出来的都是一个 Fragment 而不是 Activity

同一个 app 内的界面切换 用 Fragment 比较合适,因为 Activity 比较重量级

Fragment —— 轻量级,切换灵活

Note: 如果需要向下兼容(兼容3.0以下的 Android ,即例如让 Android 2.3使用上 Fragment),有 support-fragment 库,由于现在找到 Android 4.0 以下的设备比找女朋友都难(并没有),所以本次内训不考虑向下兼容,统统是面向 Ice-Cream Sandwich 以上的 Android。

Fragement 核心类

Fragment 核心的类有:

  • Fragment:Fragment 的基类,任何创建的 Fragment 都需要继承该类。
  • FragmentManager:管理和维护 Fragment 。他是抽象类,具体的实现类是- FragmentManagerImpl。
  • FragmentTransaction:对 Fragment 的添加、删除等操作都需要通过事务方式进行。他是抽象类,具体的实现类是BackStackRecord。
    Nested Fragment(Fragment 内部嵌套 Fragment 的能力)是 Android 4.2 提出的,support-fragment 库可以兼容到1.6。通过 getChildFragmentManager() 能够获得管理子 Fragment 的 FragmentManager ,在子 Fragment 中可以通过getParentFragment() 获得父 Fragment。

使用方法

把 Fragment 当作控件使用(静态使用)

接下来,就是实践的时候了,要注意了,开始写代码喽~~~~

这种方法是使用 Fragment 的最简单的一种方式了,我们只需要声明一个类继承自 Fragment 实现其 onCreateView 方法,并将 fragment 声明在 Activity 的 xml 里即可。我们来看代码:

步骤:

1、继承 Fragment,重写 onCreateView 决定 Fragment 布局。

2、在 Activity 中声明此 Fragment , 就当和普通的 View 一样。

我们来看代码:

AFragment.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.example.young.fragmentdemo;

import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class AFragment extends Fragment {
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.afragment, container, false);
}
}

afragment.xml

AFragment 的布局文件,在这里我们可以看出,我们可以每个 Fragment 当中进行单独的布局:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="钥匙天下第一!" />
</RelativeLayout>

Note: 将字符串放在布局文件xml里是不稳健的方法,应当放到res->values->strings.xml,以适应多语言种类。然而在这里,千言万语汇成一句:“钥匙牛逼!”

activity_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.example.young.fragmentdemo.MainActivity">

<fragment
android:id="@+id/fragmenta"
android:name="com.example.young.fragmentdemo.AFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />

</LinearLayout>

这样我们就将 AFragment 作为一个控件显示出来了,十分简单,只是需要注意 fragment 控件一定要加 id 属性即可,否则会崩溃。

这里给出 Fragment 稍微完整的使用方式。首先,创建继承 Fragment 的类,名为 Fragment1 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Fragment1 extends Fragment{  
private static String ARG_PARAM = "param_key";
private String mParam;
private Activity mActivity;
public void onAttach(Context context) {
mActivity = (Activity) context;
mParam = getArguments().getString(ARG_PARAM); //获取参数
}
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_1, container, false);
TextView view = root.findViewById(R.id.text);
view.setText(mParam);
return root;
}
public static Fragment1 newInstance(String str) {
Fragment1 frag = new Fragment1();
Bundle bundle = new Bundle();
bundle.putString(ARG_PARAM, str);
fragment.setArguments(bundle); //设置参数
return fragment;
}
}

再次复习!

  1. 什么是LayoutInFlater?
  2. 什么是ViewGroup?
  3. 什么是Bundle?

ANSWER

layoutInflater(布局服务)

说到布局,大家第一时间 可能想起的是写完一个布局的xml,然后调用Activity的setContentView()加载布局,然后把他显示到屏幕上是吧~其实这个底层走的还是这个LayoutInflater,用的Android内置的Pull解析器来解析布局。一般在Android动态加载布局或者添加控件用得较多。

ViewGroup

Android的UI界面都是由View和ViewGroup及其派生类组合而成的。其中,View是所有UI组件的基类,而ViewGroup是容纳View及其派生类的容器,ViewGroup也是从View派生出来的。一般来说,开发UI界面都不会直接使用View和ViewGroup(自定义控件的时候使用),而是使用其派生类。

Bundle

Bundle主要用于传递数据;它保存的数据,是以key-value(键值对)的形式存在的。

我们经常使用Bundle在Activity之间传递数据,传递的数据可以是boolean、byte、int、long、float、double、string等基本类型或它们对应的数组,也可以是对象或对象数组。当Bundle传递的是对象或对象数组时,必须实现Serializable 或Parcelable接口。下面分别介绍Activity之间如何传递基本类型、传递对象。

Fragment 有很多可以复写的方法,其中最常用的就是 onCreateView(),该方法返回 Fragment 的UI布局,需要注意的是 inflate() 的第三个参数是 false ,因为在 Fragment 内部实现中,会把该布局添加到 container 中,如果设为 true ,那么就会重复做两次添加,则会抛出异常。

如果在创建 Fragment 时要传入参数,必须要通过 setArguments(Bundle bundle) 方式添加,而不建议通过为 Fragment 添加带参数的构造函数,因为通过 setArguments() 方式添加,在由于内存紧张导致 Fragment 被系统杀掉并恢复(re-instantiate)时能保留这些数据。

我们可以在Fragment的onAttach()中通过getArguments()获得传进来的参数,并在之后使用这些参数。如果要获取Activity对象,不建议调用getActivity(),而是在onAttach()中将Context对象强转为Activity对象。

创建完Fragment后,接下来就是把Fragment添加到Activity中。在Activity中添加Fragment的方式有两种:

  • 静态添加:在xml中添加,缺点是一旦添加就不能在运行时删除。
  • 动态添加:运行时添加,这种方式比较灵活,因此建议使用这种方式。

虽然Fragment能在XML中添加,但是这只是一个语法糖而已,Fragment并不是一个View,而是和Activity同一层次的。

动态加载 Fragment

首先Activity需要有一个容器存放Fragment,一般是FrameLayout,因此在Activity的布局文件中加入FrameLayout:

在代码中通过 FragmentManager 获取 FragmentTransaction 来进行 Fragment 的动态添加才是我们最常用的使用方式。

1
2
3
4
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

然后在onCreate()中,通过以下代码将Fragment添加进Activity中。

1
2
3
4
5
if (bundle == null) {
getSupportFragmentManager().beginTransaction()
.add(R.id.container, Fragment1.newInstance("hello world"), "f1") //.addToBackStack("fname")
.commit();
}

这里需要注意几点:

  • 因为我们使用了 support 库的 Fragment ,因此需要使用getSupportFragmentManager()获取FragmentManager 。

  • add()是对 Fragment 众多操作中的一种,还有remove(), replace()等,第一个参数是根容器的id(FrameLayout 的 id,即”@id/container”),第二个参数是 Fragment 对象,第三个参数是 fragment 的 tag 名,指定 tag 的好处是后续我们可以通过Fragment1 frag = getSupportFragmentManager().findFragmentByTag("f1")从 FragmentManager 中查找 Fragment 对象。

  • 在一次事务中,可以做多个操作,比如同时做add().remove().replace()

  • commit()操作是异步的,内部通过mManager.enqueueAction()加入处理队列。对应的同步方法为commitNow()commit()内部会有checkStateLoss()操作,如果开发人员使用不当(比如commit()操作在onSaveInstanceState()之后),可能会抛出异常,而commitAllowingStateLoss()方法则是不会抛出异常版本的commit()方法,但是尽量使用commit(),而不要使用commitAllowingStateLoss()

  • addToBackStack("fname")是可选的。FragmentManager 拥有回退栈(BackStack),类似于 Activity 的任务栈,如果添加了该语句,就把该事务加入回退栈,当用户点击返回按钮,会回退该事务(回退指的是如果事务是add(frag1),那么回退操作就是remove(frag1));如果没添加该语句,用户点击返回按钮会直接销毁Activity。

  • Fragment 有一个常见的问题,即 Fragment 重叠问题,这是由于 Fragment 被系统杀掉,并重新初始化时再次将 fragment 加入 activity ,因此通过在外围加 if 语句能判断此时是否是被系统杀掉并重新初始化的情况。

    Fragment 有个常见的异常:

    1
    2
    3
    4
    5
    java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
    at android.support.v4.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1341)
    at android.support.v4.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:1352)
    at android.support.v4.app.BackStackRecord.commitInternal(BackStackRecord.java:595)
    at android.support.v4.app.BackStackRecord.commit(BackStackRecord.java:574)

    该异常出现的原因是:commit()onSaveInstanceState()后调用。首先,onSaveInstanceState()onPause()之后,onStop()之前调用。onRestoreInstanceState()onStart()之后,onResume()之前。

因此避免出现该异常的方案有:

  • 不要把Fragment事务放在异步线程的回调中,比如不要把Fragment事务放在AsyncTask的onPostExecute(),因此onPostExecute()可能会在onSaveInstanceState()之后执行。
  • 逼不得已时使用commitAllowingStateLoss()

上面的内容有点大,我们先来看看一个比较简单的例子。

先来看代码:

MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.example.young.fragmentdemo;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
AFragment mAFragment = new AFragment();
getFragmentManager().beginTransaction()
.replace(R.id.main_container, mAFragment).commit();
getFragmentManager().beginTransaction().show(mAFragment);
}
}

activity_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.example.young.fragmentdemo.MainActivity">

<FrameLayout
android:id="@+id/main_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />

</LinearLayout>

这里我们在 Activity 中获取 FragmentManager 然后再进一步获取到 FragmentTransaction 对象将我们 new 出来的 AFragment add 到 FrameLayout 中。

这种动态加载 Fragment 的方式十分灵活,可以让我们在代码当中动态的决定加载哪些 Fragment 显示出来。这里我们需要重点关注的是 FragmentTransaction 对象。除了例子当中使用的 add 操作以外,它还有 replace,hide,show,remove 等操作,下面就对这几种方法一一进行解释。

1)add(int containerViewId, Fragment fragment)

这个方法是将fragmen添加到我们指定id的layout中.

2)hide(Fragment fragment)和show(Fragment fragment)

隐藏或者显示指定的fragment,类似于我们在View中经常使用的setVisibly方法,需要注意的是,这里的hide和show仅仅只是让fragment显示和隐藏,不会对fragment进行销毁,甚至我们在hide的时候fragment的onPause方法都没有被调用。

3)remove(Fragment fragment)

会将fragment移除,如果被移除的Fragment没有添加到回退栈,该Fragment会同时被销毁。

4)replace(int containerViewId, Fragment fragment)

replace方法是用来进行替换的,实际上也就是对指定的layout id先remove掉其fragment,然后再add上去我们指定的fragment的一种组合操作。

5)detach()

会将view从UI中移除,和remove()不同,此时fragment并没有与Activity断绝关系,所以生命周期的onDestroy方法和onDetach方法并没有被调用

6)attach()

重建view视图,附加到UI上并显示,如果调用完detach方法后再来调用该方法的话不会去走onAttach和onCreate方法。

需要注意的是,我们在进行了上述的各种操作以后一定要调用commit方法提交事务才能生效。

Fragment 与生命周期有关方法

1).onAttach

当该Fragment与Activity发生关联的时候调用,注意的是这个方法里会给我们传入一个Context上下文参数,此时我们可以将其存入成员变量中进行使用,避免在代码中调用getActivity()出现的空指针异常。

2).onCreate

当创建Fragment的时候调用与onAttach方法是一起调用的,如果没有调用到onAttach方法就不会调用该方法。

3).onCreateView

每次创建、绘制Fragment的View时调用,并且返回一个view对象。

4).onActivityCreated

当Fragment所在的Activity被onCreate完成时调用。

5)onDestoryView()

与onCreateView想对应,当该Fragment的视图被移除时调用。

6)onDestroy()方法

与onCreate想对应当Fragment的状态被销毁的时候进行调用。

7)onDetach()

与onAttach相对应,当Fragment与Activity关联被取消时调用,需要注意的是我们调用detach方法的时候并不会调用到该生命周期方法。

上面的方法中,只有onCreateView()在重写时不用写super方法,其他都需要。

Fragment 实现原理和 Back Stack

我们知道 Activity 有任务栈,用户通过 startActivity 将 Activity 加入栈,点击返回按钮将 Activity 出栈。Fragment 也有类似的栈,称为回退栈(Back Stack),回退栈是由 FragmentManager 管理的。默认情况下,Fragment 事务是不会加入回退栈的,如果想将 Fragment 事务加入回退栈,则可以加入addToBackStack("")。如果没有加入回退栈,则用户点击返回按钮会直接将 Activity 出栈;如果加入了回退栈,则用户点击返回按钮会回滚 Fragment 事务。

我们将通过最常见的 Fragment 用法,讲解 Back Stack 的实现原理:

1
2
3
4
5
getSupportFragmentManager().beginTransaction()
.add(R.id.container, f1, "f1")
.addToBackStack("")
.commit();

上面这个代码的功能就是将 Fragment 加入 Activity 中,内部实现为:创建一个 BackStackRecord 对象,该对象记录了这个事务的全部操作轨迹(这里只做了一次 add 操作,并且加入回退栈),随后将该对象提交到FragmentManager 的执行队列中,等待执行。

BackStackRecord 类的定义如下

1
2
class BackStackRecord extends FragmentTransaction implements FragmentManager.BackStackEntry, Runnable {}

从定义可以看出,BackStackRecord 有三重含义:

  • 继承了 FragmentTransaction ,即是事务,保存了整个事务的全部操作轨迹。
  • 实现了 BackStackEntry ,作为回退栈的元素,正是因为该类拥有事务全部的操作轨迹,因此在 popBackStack() 时能回退整个事务。
  • 继承了 Runnable ,即被放入 FragmentManager 执行队列,等待被执行。

先看第一层含义,getSupportFragmentManager.beginTransaction()返回的就是 BackStackRecord 对象,代码如下:

1
2
3
4
public FragmentTransaction beginTransaction() {
return new BackStackRecord(this);
}

BackStackRecord 类包含了一次事务的整个操作轨迹,是以链表形式存在的,链表的元素是 Op 类,表示其中某个操作,定义如下:

1
2
3
4
5
6
static final class Op {
Op next; //链表后一个节点
Op prev; //链表前一个节点
int cmd; //操作是add或remove或replace或hide或show等
Fragment fragment; //对哪个Fragment对象做操作}

我们来看下具体场景下这些类是怎么被使用的,比如我们的事务做 add 操作。add 函数的定义:

1
2
3
4
5
public FragmentTransaction add(int containerViewId, Fragment fragment, String tag) {
doAddOp(containerViewId, fragment, tag, OP_ADD);
return this;
}

doAddOp() 方法就是创建 Op 对象,并加入链表,定义如下:

1
2
3
4
5
6
7
8
9
private void doAddOp(int containerViewId, Fragment fragment, String tag, int opcmd) {
fragment.mTag = tag; //设置fragment的tag
fragment.mContainerId = fragment.mFragmentId = containerViewId; //设置fragment的容器id
Op op = new Op();
op.cmd = opcmd;
op.fragment = fragment;
addOp(op);
}

addOp() 是将创建好的 Op 对象加入链表,定义如下:

1
2
3
4
5
6
7
8
9
10
11
void addOp(Op op) {
if (mHead == null) {
mHead = mTail = op;
} else {
op.prev = mTail;
mTail.next = op;
mTail = op;
}
mNumOp++;
}

addToBackStack(“”)是将 mAddToBackStack 变量记为 true ,在commit()中会用到该变量。commit()是异步的,即不是立即生效的,但是后面会看到整个过程还是在主线程完成,只是把事务的执行扔给主线程的 Handler ,commit()内部是commitInternal(),实现如下:

1
2
3
4
5
6
7
8
9
10
11
int commitInternal(boolean allowStateLoss) {
mCommitted = true;
if (mAddToBackStack) {
mIndex = mManager.allocBackStackIndex(this);
} else {
mIndex = -1;
}
mManager.enqueueAction(this, allowStateLoss); //将事务添加进待执行队列中
return mIndex;
}

如果 mAddToBackStack 为 true ,则调用allocBackStackIndex(this)将事务添加进回退栈, FragmentManager 类的变量 ArrayListmBackStackIndices ;就是回退栈。实现如下:

1
2
3
4
5
6
7
8
9
public int allocBackStackIndex(BackStackRecord bse) {
if (mBackStackIndices == null) {
mBackStackIndices = new ArrayList<BackStackRecord>();
}
int index = mBackStackIndices.size();
mBackStackIndices.add(bse);
return index;
}

commitInternal()中,mManager.enqueueAction(this, allowStateLoss);是将 BackStackRecord 加入待执行队列中,定义如下:

1
2
3
4
5
6
7
8
9
10
11
public void enqueueAction(Runnable action, boolean allowStateLoss) {
if (mPendingActions == null) {
mPendingActions = new ArrayList<Runnable>();
}
mPendingActions.add(action);
if (mPendingActions.size() == 1) {
mHost.getHandler().removeCallbacks(mExecCommit);
mHost.getHandler().post(mExecCommit); //调用execPendingActions()执行待执行队列的事务
}
}

mPendingActions 就是前面说的待执行队列,mHost.getHandler()就是主线程的 Handler,因此 Runnable 是在主线程执行的,mExecCommit 的内部就是调用了execPendingActions(),即把 mPendingActions 中所有积压的没被执行的事务全部执行。执行队列中的事务会怎样被执行呢?就是调用BackStackRecord的run()方法,run()方法就是执行 Fragment 的生命周期函数,还有将视图添加进 container 中。

addToBackStack()对应的是popBackStack(),有以下几种变种:

  • popBackStack():将回退栈的栈顶弹出,并回退该事务。
  • popBackStack(String name, int flag):name 为 addToBackStack(String name) 的参数,通过 name能找到回退栈的特定元素,flag 可以为 0 或者 FragmentManager.POP_BACK_STACK_INCLUSIVE,0 表示只弹出该元素以上的所有元素,POP_BACK_STACK_INCLUSIVE 表示弹出包含该元素及以上的所有元素。这里说的弹出所有元素包含回退这些事务。
  • popBackStack() 是异步执行的,是丢到主线程的 MessageQueue 执行,popBackStackImmediate() 是同步版本。

我们通过讲解 Demo1 来更清晰地了解回退栈的使用。功能如下:共有三个 Fragment:F1, F2, F3,F1 在初始化时就加入Activity,点击F1中的按钮跳转到F2,点击F2的按钮跳转到F3,点击F3的按钮回退到F1。

在Activity的 onCreate() 中,将F1加入Activity中:

1
2
3
4
5
getSupportFragmentManager().beginTransaction()
.add(R.id.container, f1, "f1")
.addToBackStack(Fragment1.class.getSimpleName())
.commit();

F1按钮的 onClick() 内容如下:

1
2
3
4
5
getFragmentManager().beginTransaction()
.replace(R.id.container, f2, "f2")
.addToBackStack(Fragment2.class.getSimpleName())
.commit();

F2按钮的 onClick() 如下:

1
2
3
4
5
getFragmentManager().beginTransaction()
.replace(R.id.container, f3, "f3")
.addToBackStack(Fragment3.class.getSimpleName())
.commit();

F3按钮的 onClick() 如下:

1
2
getFragmentManager().popBackStack(Fragment2.class.getSimpleName(), FragmentManager.POP_BACK_STACK_INCLUSIVE);

这样就完成了整个界面的跳转逻辑。

这里补充一个点
getSupportFragmentManager().findFragmentByTag()是经常用到的方法,他是 FragmentManager 的方法,FragmentManager 是抽象类,FragmentManagerImpl 是继承 FragmentManager 的实现类,他的内部实现是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class FragmentManagerImpl extends FragmentManager {
ArrayList<Fragment> mActive;
ArrayList<Fragment> mAdded;
public Fragment findFragmentByTag(String tag) {
if (mAdded != null && tag != null) {
for (int i=mAdded.size()-1; i>=0; i--) {
Fragment f = mAdded.get(i);
if (f != null && tag.equals(f.mTag)) {
return f;
}
}
}
if (mActive != null && tag != null) {
for (int i=mActive.size()-1; i>=0; i--) {
Fragment f = mActive.get(i);
if (f != null && tag.equals(f.mTag)) {
return f;
}
}
}
return null;
}
}

从上面看到,先从 mAdded 中查找是否有该 Fragment ,如果没找到,再从 mActive 中查找是否有该Fragment 。mAdded 是已经添加到 Activity 的 Fragment 的集合,mActive 不仅包含 mAdded ,还包含虽然不在 Activity 中,但还在回退栈中的 Fragment。

Fragment通信

Fragment向Activity传递数据

首先,在 Fragment 中定义接口,并让 Activity 实现该接口(具体实现省略):

1
2
public interface OnFragmentInteractionListener {    void onItemClick(String str);  //将str从Fragment传递给Activity}

在 Fragment 的 onAttach() 中,将参数 Context 强转为 OnFragmentInteractionListener 对象:

1
2
3
4
5
6
7
8
9
10
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof OnFragmentInteractionListener) {
mListener = (OnFragmentInteractionListener) context;
} else {
throw new RuntimeException(context.toString()
+ " must implement OnFragmentInteractionListener");
}
}

并在 Fragment 合适的地方调用mListener.onItemClick("hello")将 ”hello” 从 Fragment 传递给 Activity 。

FABridge

由于通过接口的方式从 Fragment 向 Activity 进行数据传递比较麻烦,需要在 Fragment 中定义 interface ,并让 Activity 实现该 interface ,FABridge(https://github.com/hongyangAndroid/FABridge) 通过注解的形式免去了这些定义。

在 build.gradle 中添加依赖:

1
2
annotationProcessor 'com.zhy.fabridge:fabridge-compiler:1.0.0'compile 'com.zhy.fabridge:fabridge-api:1.0.0'

首先定义方法 ID,这里为 FAB_ITEM_CLICK ,接着在 Activity 中定义接口:

1
2
3
4
@FCallbackId(id = FAB_ITEM_CLICK)public void onItemClick(String str) {  //方法名任意
Toast.makeText(this, str, Toast.LENGTH_SHORT).show();
}

最后,在 Fragment 中,通过以下形式调用 ”ID=FAB_ITEM_CLICK” 的方法(该方法可能在 Activity 中,也可能在任何类中):

1
2
Fabridge.call(mActivity,FAB_ITEM_CLICK,"data");  //调用ID对应的方法,"data"为参数值

Activity 向 Fragment 传递数据

Activity 向 Fragment 传递数据比较简单,获取 Fragment 对象,并调用 Fragment 的方法即可,比如要将一个字符串传递给 Fragment,则在 Fragment 中定义方法:

1
2
3
4
public void setString(String str) { 
this.str = str;
}

并在 Activity 中调用fragment.setString("hello")即可。

Fragment 之间通信

由于Fragment之间是没有任何依赖关系的,因此如果要进行Fragment之间的通信,建议通过Activity作为中介,不要Fragment之间直接通信。

DialogFragment

DialogFragment是Android 3.0提出的,代替了Dialog,用于实现对话框。他的优点是:即使旋转屏幕,也能保留对话框状态。

如果要自定义对话框样式,只需要继承DialogFragment,并重写onCreateView(),该方法返回对话框UI。这里我们举个例子,实现进度条样式的圆角对话框。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ProgressDialogFragment extends DialogFragment {    @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
getDialog().requestWindowFeature(Window.FEATURE_NO_TITLE); //消除Title区域
getDialog().getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); //将背景变为透明
setCancelable(false); //点击外部不可取消
View root = inflater.inflate(R.layout.fragment_progress_dialog, container);
return root;
}
public static ProgressDialogFragment newInstance() {
return new ProgressDialogFragment();
}
}

进度条动画我们使用 Lottie(https://github.com/airbnb/lottie-android)实现,Lottie 动画从这里(https://www.lottiefiles.com/)找到。使用非常方便,只需要下载 JSON 动画文件,然后在 XML 中写入:

1
2
3
4
5
6
7
<com.airbnb.lottie.LottieAnimationView
android:layout_width="wrap_content" //大小根据JSON文件确定
android:layout_height="wrap_content"
app:lottie_fileName="loader_ring.json" //JSON文件
app:lottie_loop="true" //循环播放
app:lottie_autoPlay="true" /> //自动播放

然后通过下面代码显示对话框:

1
2
3
ProgressDialogFragment fragment = ProgressDialogFragment.newInstance();
fragment.show(getSupportFragmentManager(), "tag");//fragment.dismiss();

为了实现圆角,除了在 onCreateView() 中把背景设为透明,还需要对UI加入背景:

1
2
3
4
5
6
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#ffffff"/>
<corners
android:radius="20dp"/>
</shape>

Fragment的典型应用场景

Fragment的应用场景最多的便是 ViewPager+Fragment 的实现,现在主流的 APP 几乎都能看到它们的身影,那么这一部分我就主要针对该应用场景进行分析。

ViewPager是support v4库中提供界面滑动的类,继承自ViewGroup。PagerAdapter是ViewPager的适配器类,为ViewPager提供界面。但是一般来说,通常都会使用PagerAdapter的两个子类:FragmentPagerAdapter和FragmentStatePagerAdapter作为ViewPager的适配器,他们的特点是界面是Fragment。

默认,ViewPager会缓存当前页相邻的界面,比如当滑动到第2页时,会初始化第1页和第3页的界面(即Fragment对象,且生命周期函数运行到onResume()),可以通过setOffscreenPageLimit(count)设置离线缓存的界面个数。

FragmentPagerAdapter和FragmentStatePagerAdapter需要重写的方法都一样,常见的重写方法如下:

  • public FragmentPagerAdapter(FragmentManager fm): 构造函数,参数为FragmentManager。如果是嵌套Fragment场景,子 PagerAdapter的参数传入getChildFragmentManager()。
  • Fragment getItem(int position): 返回第position位置的Fragment,必须重写。
  • int getCount(): 返回ViewPager的页数,必须重写。
  • Object instantiateItem(ViewGroup container, int position): container是ViewPager对象,返回第position位置的Fragment。
  • void destroyItem(ViewGroup container, int position, Object object): container是ViewPager对象,object是Fragment对象。
  • getItemPosition(Object object): object是Fragment对象,如果返回POSITION_UNCHANGED,则表示当前Fragment不刷新,如果返回POSITION_NONE,则表示当前Fragment需要调用destroyItem()instantiateItem()进行销毁和重建。 默认情况下返回POSITION_UNCHANGED。

ViewPager+Fragment结构

首先我们来看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
public class MainActivity extends FragmentActivity
implements
View.OnClickListener {

private ViewPager mViewPager;

private List<Fragment> mList;
private Fragment mOne;
private Fragment mTwo;
private Fragment mThree;
private Fragment mFour;

private Button mOneButton;
private Button mTwoButton;
private Button mThreeButton;
private Button mFourButton;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

mViewPager = (ViewPager) findViewById(R.id.content_pager);
//加载Fragment
mList = new ArrayList<>();
mOne = new OneFragment();
mTwo = new TwoFragment();
mThree = new ThreeFragment();
mFour = new FourFragment();
mList.add(mOne);
mList.add(mTwo);
mList.add(mThree);
mList.add(mFour);

mOneButton = (Button) findViewById(R.id.one);
mTwoButton = (Button) findViewById(R.id.two);
mThreeButton = (Button) findViewById(R.id.three);
mFourButton = (Button) findViewById(R.id.four);

mOneButton.setOnClickListener(this);
mTwoButton.setOnClickListener(this);
mThreeButton.setOnClickListener(this);
mFourButton.setOnClickListener(this);

//设置到ViewPager中
mViewPager.setAdapter(new ContentsPagerAdapter(
getSupportFragmentManager()));

}

@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.one :
mViewPager.setCurrentItem(0);
break;
case R.id.two :
mViewPager.setCurrentItem(1);
break;
case R.id.three :
mViewPager.setCurrentItem(2);
break;
case R.id.four :
mViewPager.setCurrentItem(3);
break;
}
}


class ContentsPagerAdapter extends FragmentStatePagerAdapter {

public ContentsPagerAdapter(FragmentManager fm) {
super(fm);
}

@Override
public Fragment getItem(int position) {
return mList.get(position);
}

@Override
public int getCount() {
return mList.size();
}
}
}

在这里我们加载了4个Fragment到ViewPager中,同时我在这里使用的是
FragmentStatePagerAdapter 。这里需要提到的是 FragmentStatePagerAdapter 与 FragmentPagerAdapter 的区别。
FragmentPagerAdapter:对于不再需要的fragment,仅仅只会调用到onDestroyView方法,也就是仅仅销毁视图而并没有完全销毁Fragment。
FragmentStatePagerAdapter:会销毁不再需要的fragment,一直调用到 onDetach 方法失去与 Activity 的绑定。销毁时,会调用 onSaveInstanceState(Bundle outState) 方法通过 bundle 将信息保存下来,当用户切换回来,可以通过该 bundle 恢复生成新的 fragment ,也就是说,我们可以在 onSaveInstanceState(Bundle outState) 方法中保存一些数据,在 onCreate 中进行恢复创建。

经验

当页面较少的情况下可以考虑使用FragmentPagerAdapter,通过空间来换取时间上的效率。但当页面多了的时候我们就更需要使用FragmentStatePagerAdapter来做了,因为没有哪个用户希望某个应用会占爆它内存。

懒加载

懒加载主要用于ViewPager且每页是Fragment的情况,场景为微信主界面,底部有4个tab,当滑到另一个tab时,先显示”正在加载”,过一会才会显示正常界面。

默认情况,ViewPager会缓存当前页和左右相邻的界面。实现懒加载的主要原因是:用户没进入的界面需要有一系列的网络、数据库等耗资源、耗时的操作,预先做这些数据加载是不必要的。

这里懒加载的实现思路是:用户不可见的界面,只初始化UI,但是不会做任何数据加载。等滑到该页,才会异步做数据加载并更新UI。

这里就实现类似微信那种效果,整个UI布局为:底部用PagerBottomTabStrip(https://github.com/tyzlmjj/PagerBottomTabStrip)项目实现,上面是ViewPager,使用FragmentPagerAdapter。逻辑为:当用户滑到另一个界面,首先会显示正在加载,等数据加载完毕后(这里用睡眠1秒钟代替)显示正常界面。

ViewPager默认缓存左右相邻界面,为了避免不必要的重新数据加载(重复调用onCreateView()),因为有4个tab,因此将离线缓存的半径设置为3,即setOffscreenPageLimit(3)

懒加载主要依赖Fragment的setUserVisibleHint(boolean isVisible)方法,当Fragment变为可见时,会调用setUserVisibleHint(true);当Fragment变为不可见时,会调用setUserVisibleHint(false),且该方法调用时机:

  • onAttach()之前,调用setUserVisibleHint(false)

  • onCreateView()之前,如果该界面为当前页,则调用setUserVisibleHint(true),否则调用setUserVisibleHint(false)

  • 界面变为可见时,调用setUserVisibleHint(true)
    *界面变为不可见时,调用setUserVisibleHint(false)

    懒加载Fragment的实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    public class LazyFragment extends Fragment {    private View mRootView;
    private boolean mIsInited;
    private boolean mIsPrepared; @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    mRootView = inflater.inflate(R.layout.fragment_lazy, container, false);
    mIsPrepared = true;
    lazyLoad();
    return mRootView;
    }

    public void lazyLoad() {
    if (getUserVisibleHint() && mIsPrepared && !mIsInited) {
    //异步初始化,在初始化后显示正常UI
    loadData();
    }
    }

    private void loadData() {
    new Thread() {
    public void run() {
    //1. 加载数据
    //2. 更新UI
    //3. mIsInited = true
    }
    }.start();
    }

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
    super.setUserVisibleHint(isVisibleToUser);
    if (isVisibleToUser) {
    lazyLoad();
    }
    }

    public static LazyFragment newInstance() {
    return new LazyFragment();
    }
    }

注意点:

  • 在Fragment中有两个变量控制是否需要做数据加载:
    • mIsPrepared:表示UI是否准备好,因为数据加载后需要更新UI,如果UI还没有inflate,就不需要做数据加载,因为setUserVisibleHint()会在onCreateView()之前调用一次,如果此时调用,UI还没有inflate,因此不能加载数据。
    • mIsInited:表示是否已经做过数据加载,如果做过了就不需要做了。因为setUserVisibleHint(true)在界面可见时都会调用,如果滑到该界面做过数据加载后,滑走,再滑回来,还是会调用setUserVisibleHint(true),此时由于mIsInited=true,因此不会再做一遍数据加载。
  • lazyLoad():懒加载的核心类,在该方法中,只有界面可见(getUserVisibleHint()true)、UI准备好(mIsPreparedtrue)、过去没做过数据加载(mIsInited==false)时,才需要调loadData()做数据加载,数据加载做完后把mIsInited置为true。

布局XML主要分两个container,一个是初始显示的状态,即R.id.container_empty,当数据加载完成,就显示R.id.container:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">

<RelativeLayout
android:id="@+id/container_empty"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="正在加载"
/>

</RelativeLayout>
<RelativeLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
>
...
</RelativeLayout>
</FrameLayout>

参考文献