注意:建议在白天模式下看!

在学习完 Fragment 之后,其实这个组件也是非常之常见的。QQ、微信、B站等等 app 绝大多数都使用了到了 Fragment,然后结合底部的导航按钮来跳转到不同的 Fragment 的。一看到效果就能很好的想出它的运用场景。

同时也知道 Fragment 的重要性。难度还是有点,不过比起将要学的 ConstraintLayout 还是要容易一些(我个人感觉,确信)

不过,今天一口气看了 5 集的进击的巨人。进击的巨人第二季实在是太燃了。毕竟是第一次看嘛,看完才会有动力学习嘛。

前情概要

Fragment,本意叫碎片、片段之意。它的出现是为了适应平板电脑等大屏设备上,不过也确实是这样,但是不知道为什么,好像兼容 Android 平板的应用其实很少耶,反而这些开发者更喜欢 iPad 的平板。就感觉 Android 平板的生态似乎被抛弃了一样。

Fragment 生命周期

Fragment 也有自己的生命周期,他的生命周期如下所示。不同的是,它不能单独存在,必须要“寄生”在 Activity 之中。

Fragment的生命周期

Fragment 的生命周期的影响

Fragment 的生命周期会受到 Activity 生命周期的影响,如下图所示。Activity 创建了,我 Fragment 也开始做创建相关的动作。这样咋一眼看还真的像是一种“寄生”的关系耶。

不过这样看起来似乎看不出什么东西来,可以重写这些方法来看看 Fragment 是如何执行的。这个还是到后面再来看看吧。

先来学习学习简单的应用,从 AFragment 到 BFragment。来看看是怎样的效果吧

简单示例

首先,建立一个 ContainerActivity 用于放置 Fragment。

然后,建立 AFragment 和 BFragment 的类。AFragment 和 BFragment 都需要继承自 Fragment 类。搞了两张洛天依的画像。哈哈哈

存放 Fragment 的 Acitvity

设置好布局文件

<?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">

    <!-- 用于放置子布局的 -->
    <FrameLayout
        android:id="@+id/frag_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical" />

    <Button
        android:id="@+id/change_fragment"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:text="改变 Fragment"
        android:textAllCaps="false"
        android:layout_alignParentTop="true"/>

</RelativeLayout>

AFragment

AFragment 的 java 代码如下

public class AFragment extends Fragment {

    private TextView fragA;
    private Button changeText, changeFragment, changeOuterText;
    private BFragment bFragment;

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
        Log.d("onCreateView", "-------isRunning------");
        return inflater.inflate(R.layout.fragment_a, container, false);
    }
}

AFragment 的 fragment_a.xml 布局文件如下

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/lty_a"
    android:orientation="vertical"
    android:gravity="center">

    <TextView
        android:id="@+id/frag_a"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="我是 FragmentA"
        android:textSize="20sp"
        android:textColor="@color/colorBlack"/>
    
</LinearLayout>

BFragment

BFragment 的 java 代码如下

public class BFragment extends Fragment {

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
        // 将自定义的布局添加到 view 中
        View view = inflater.inflate(R.layout.fragment_b, container, false);
        return view;
    }
}

BFragment 的 fragment_b.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:background="@drawable/lty_b">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="我是 FragmentB"
        android:textSize="20sp"
        android:gravity="center"
        android:textColor="@color/colorBlack"/>

</LinearLayout>

创建完 BFragment 就可以在 ContainerActivity 中来放置 Fragment 了,ContainerActivity.java 代码如下。

public class ContainerActivity extends AppCompatActivity {

    private Fragment aFragment, bFragment;
    private Button button;

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

        // 得到按钮,用于点击跳转到Bfragment
        button = findViewById(R.id.change_fragment);

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                getFragmentManager().beginTransaction().replace(R.id.frag_layout, bFragment).commitAllowingStateLoss();
            }
        });

        // 加载 AFragment
        aFragment = new AFragment();
        // 加载 BFragment
        bFragment = new BFragment();
        // 添加 Fragment,add 方法需要添加两个参数,一个需要
        getFragmentManager().beginTransaction().add(R.id.frag_layout, aFragment).commitAllowingStateLoss();
    }
}

运行效果

运行的效果如下

效果上面,从 AFragment 跳到 BFragment。

生命周期

在此之前,用一张图来展示了 Fragment 的生命周期。接下来,通过输出来直观的看到 Fragment 的生命周期。了解生命周期的目的就在于,能够何时在某个过程中来执行哪些关键性的代码。这个其实是非常重要的。

我们就利用简单例子中的 AFragment 类中的代码,测试它的生命周期,重写了它的全部关于生命周期相关的代码。

当我们进入的时候,执行了以下方法。

D/fragment: -------onAttach running-------
D/fragment: -------onCreate running-------
D/fragment: -------onCreateView isRunning------
D/fragment: -------onViewCreated isRunning------
D/fragment: -------onActivityCreated running-------
D/fragment: -------onStart isRunning------
D/fragment: -------onResume isRunning------

当熄屏、按下 Home 键、切换到其它程序时,执行了以下方法。

D/fragment: -------onPause isRunning------
D/fragment: -------onStop isRunning------

当亮屏的时候、回到原来的程序时,执行了以下方法。

D/fragment: -------onStart isRunning------
D/fragment: -------onResume isRunning------

当到 BFragment 的时候,执行了以下方法。由于点击了按钮,调用了 replace 方法,导致 AFragment 的 View 已经破坏了。如果返回到 AFragment,它将不是原来的 AFragment,而是新的 Fragment 吧。

D/fragment: -------onPause isRunning------
D/fragment: -------onStop isRunning------
D/fragment: -------onDestroyView isRunning------

当返回到 AFragment 的时候,执行了以下方法。此时执行了 onCreateView 方法,也就是说 AFragment 的 View 是重新创建的。

D/fragment: -------onCreateView isRunning------
D/fragment: -------onActivityCreated running--------
D/fragment: -------onViewCreated isRunning------
D/fragment: -------onStart isRunning------
D/fragment: -------onResume isRunning------

当退出了这个 Activity 的时候,执行了以下方法。

D/fragment: -------onPause isRunning------
D/fragment: -------onStop isRunning------
D/fragment: -------onDestroyView isRunning------
D/fragment: -------onDestroy isRunning------
D/fragment: -------onDetach isRunning------

当有来电时,执行了以下方法。

D/fragment: -------onPause isRunning------

当挂机时,执行了以下方法。

D/fragment: -------onResume isRunning------

回退问题

回退到上一个

Fragment 也有自己的栈,在简单示例的代码中,有一个 add 方法。这个方法就是把 Fragment 添加到所属的栈中,不过,我们在按手机上的返回按钮时。不跳回到 FragmentA,而是直接退出了这个 Activity 中。

这怎么办呢??

其实也不难,只需要调用 addToBackState(null) 这个方法,就可以出栈,回退到上一个 Fragment 了。完整的写法如下。不过还是不懂这个方法中的参数到底是什么意思。

getFragmentManager().beginTransaction().replace(R.id.frag_layout, bFragment).addToBackState(null).commitAllowingStateLoss();

更改之后的效果如下

跳转不销毁

在上面的简单示例代码中和生命周期的演示中,从 AFragment 调用到 BFragment 时发现,原来的 AFragment 已经被销毁了。那是因为我们调用的时候是调用 replace 方法,replace 的动作可以看成是先删除再添加,所以才会造成这种问题的。

以下效果可以展示出,返回到 AFragment 的时候,发现其内容还原了。

为了实现跳转之后不销毁原来的 AFragment,可以调用其 hide 方法,在跳转 BFragment 的时候,先隐藏 AFragment 然后将 BFragment 添加到栈中来。返回到 AFragment 时将其显示出来。

首先,在添加 AFragment 的时候添加一个 tag,这边传入了一个 tag 为 A。

getFragmentManager().beginTransaction().add(R.id.frag_layout, aFragment, "a").commitAllowingStateLoss();

然后,在 AFragment 中的 onViewCreated 方法中获取到跳转按钮,设置点击事件。点击事件中,先获取 tag 为 a 的 Fragment,然后获取到之后将其隐藏,如果没有获取到 a 标签,则不隐藏,直接 AFragment 销毁。

主要代码如下

// 获得按钮并设置跳转
changeFragment = view.findViewById(R.id.btn_fragment_b);
changeFragment.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        if(bFragment == null) {
            bFragment = new BFragment();
        }
        // 点击跳转时,先通过标签获取AFragment
        Fragment fragment = getFragmentManager().findFragmentByTag("a");
        if(fragment != null) {
            // 将A隐藏,并且将B添加进来
        	getFragmentManager().beginTransaction().hide(fragment).add(R.id.frag_layout, bFragment).addToBackStack(null).commitAllowingStateLoss();
        }else{
            getFragmentManager().beginTransaction().replace(R.id.frag_layout, bFragment).addToBackStack(null).commitAllowingStateLoss();
        }
    }
});

最后的运行效果如下

数据传递

数据传递总共有两种方式,一个是从 Activity 传递到 Fragment,另外一个是从 Fragment 到 Activity。从 Activity 到 Fragment 通过 Bundle 的方式传递的。从 Fragment 到 Activity 是通过调用 getActivity 方法,然后在调用 Activity 中的公共方法来达到数据传递的目的。

从 Activity 传递到 Fragment

为了调取方便,我们在 AFragment 中编写一个静态方法,用于创建 AFragment 示例,同时带一个参数用来传递数据。

public class AFragment extends Fragment {

    public static AFragment newInstance(String data){
        AFragment aFragment = new AFragment();
        // 传入参数
        Bundle bundle = new Bundle();
        bundle.putString("data", data);
        // 在Fragment中设置参数
        aFragment.setArguments(bundle);
        return aFragment;
    }
    ......
}

然后,在视图创建完成之后,会执行 onViewCreated 方法,所以,应该在创建之后获取传过来的值并且设置好即可

@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
    // 获取参数
    Bundle bundle = getArguments();
    String data = bundle.getString("data", "我是洛天依A");
    // 设置文字
    TextView textView = view.findViewById(R.id.frag_a);
    textView.setText(data);
}

在 ContainerActivity 创建 Fragment 实例中,将创建的实例更改成

aFragment = AFragment.newInstance("我不是FragmentA,真难听");

最后看看执行效果

从 Fragment 传递到 Activity

因为 Fragment 是依赖于 Activity 的,所以我们可以在 Fragment 中通过 getActivity 方法得到 Fragment 所在的 Activity。最后在调用其 Activity 中的公开的方法即可。

首先,在 ContainerActivity 编写一个公共方法,本实例中的公共方法的例子将 Activity 中的文本组件重新赋值。

public class ContainerActivity extends AppCompatActivity {

    private Fragment aFragment, bFragment;
    private TextView outerText;

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

        button = findViewById(R.id.change_fragment);
        outerText = findViewById(R.id.outer_text);
		......
    }

    public void setOuterText(String msg) {
       outerText.setText(msg);
    }
}

在 AFragment 中的布局文件创建一个按钮。

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/lty_a"
    android:orientation="vertical"
    android:gravity="center">
    
    ......

    <Button
        android:id="@+id/btn_change_outer"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="改变外部文字"/>

</LinearLayout>

在 onViewCreated 方法中,获取按钮并设置点击事件,点击事件中获取当前的 Activity 并且调用其中的方法。

@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
    // 改变外部的文字
    changeOuterText = view.findViewById(R.id.btn_change_outer);
    changeOuterText.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Log.d("getActivity()", getActivity().toString());
            if(getActivity() != null){
                ((ContainerActivity) getActivity()).setOuterText("我已经炸了");
            }
        }
    });
}

点击按钮之后的效果如下所示,这个“我是外部文字”是属于 Activity 的,点击按钮时发现文字内容已经发现变化

后记

项目的代码都在 AndroidStudy 的 github 仓库中。

学习过程中参考了部分博客