Fragment 的使用

Fragment

Fragment 表示应用界面中可重复使用的一部分。Fragment 定义和管理自己的布局,具有自己的生命周期,并且可以处理自己的输入事件。Fragment 不能独立存在,而是必须由 Activity 或另一个 Fragment 托管。Fragment 的视图层次结构会成为宿主的视图层次结构的一部分,或附加到宿主的视图层次结构。

上面是官方对 Fragment 的介绍。其实 Fragment 和 Activity 很相似,但使用 Fragment 可以将一个完整的界面划分为离散的区块,然后嵌入到 Activity 中,从而将模块化和可重用性引入 Activity 的界面。说白了就是可以在不切换 Acitivty 的同时变更 UI 界面中某一区域的内容,Activity 适合放置全局元素,而 Fragment 更适合定义和管理单个屏幕或部分屏幕的界面。

Fragment 的简单使用

使用步骤:

  1. 编写 Fragment 的布局文件 fragment_left.xml 和 fragment_right.xml。

    <!-- fragment_left.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_left_fragment"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:text="Button" />
    
    </LinearLayout>
    
    <!-- fragment_right.xml -->
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        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:text="This is right fragment." />
    
    </LinearLayout>
    
  2. 分别创建对应的 Fragment 类 LeftFragment 和 RightFragment,并继承自 Fragment。重写 onCreateView() 方法。

    public class LeftFragment extends Fragment {
        @Nullable
        @Override
        public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
            // 使用布局加载器去加载对应 Fragment 的布局文件,转化成 View 对象并返回
            return inflater.inflate(R.layout.fragment_left, container, false);
        }
    }
    
  3. 在 Activity 的布局文件中添加 <fragment> 标签,指定 android:name 属性为对应的 Fragment 类。

    <fragment
        android:id="@+id/left_fragment"
        android:name="com.amie.test.LeftFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />
    
    <fragment
        android:id="@+id/right_fragment"
        android:name="com.amie.test.RightFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />
    

动态添加 Fragment

接着上面的例子继续添加一个 Fragment。

  1. 编写一个新的 Fragment 布局文件 fragment_another_right.xml。

    <!-- fragment_another_right.xml -->
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#ffff00"
        android:orientation="vertical">
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:text="This is another right fragment." />
    
    </LinearLayout>
    
  2. 创建对应 Fragment 类 AnotherRightFragment,并继承自 Fragment。重写 onCreateView() 方法。

    public class AnotherRightFragment extends Fragment {
        @Nullable
        @Override
        public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
            // 使用布局加载器去加载对应 Fragment 的布局文件,转化成 View 对象并返回
            return inflater.inflate(R.layout.fragment_another_right, container, false);
        }
    }
    
  3. 修改 Activity 的布局文件,将右边 Fragment 的替换成 <FrameLayout>。因为要把动态加载的 Fragment 放入一个布局中,这个布局就只有这个 Fragment 没有其他东西,也不需要任何定位,那么会默认把控件放到左上角的 FrameLayout 就很适合。

    这里先不采用官方建议的 FragmentContainerView 替代 <fragment> 标签和 <FrameLayout> 标签。

    <fragment
        android:id="@+id/left_fragment"
        android:name="com.amie.test.LeftFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />
    
    <FrameLayout
        android:id="@+id/right_frame_layout"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />
    
  4. 在 Activtiy 中给 FrameLayout 添加内容,实现动态添加 Fragment。

    public class FragmentActivity extends AppCompatActivity implements View.OnClickListener {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_fragment);
            Button button = (Button) findViewById(R.id.button_left_button);
            button.setOnClickListener(this);
            replaceFragment(new RightFragment());
        }
    
        /**
        * 创建一个用来替换 Fragment 的方法
        * @param fragment 需要被替换成的 Fragment 的实例
        */
        private void replaceFragment(Fragment fragment) {
            // 调用 getSupportFragmentManager() 方法获取 FragmentManager 对象
            FragmentManager fragmentManager = getSupportFragmentManager();
            // 通过 FragmentManager 调用 beginTransaction() 开启一个事务并获得 FragmentTransaction 对象
            FragmentTransaction transaction = fragmentManager.beginTransaction();
            // 向容器内添加或替换 Fragment,可通过 replace() 方法实现
            // 第一个参数是容器的资源 ID,第二个参数是需要被替换成的 Fragment 实例对象
            transaction.replace(R.id.right_frame_layout, fragment);
            // 提交事务
            transaction.commit();
        }
    
        @Override
        public void onClick(View v) {
            switch (v.getId()) {
                case R.id.button_left_button:
                    replaceFragment(new AnotherRightFragment());
                    break;
            }
        }
    }
    

在 Fragment 中模拟返回栈,返回上一个 Fragment

private void replaceFragment(Fragment fragment) {
    // 调用 getSupportFragmentManager() 方法获取 FragmentManager 对象
    FragmentManager fragmentManager = getSupportFragmentManager();

    // 通过 findFragmentById() 方法获取当前 Fragment
    Fragment current = fragmentManager.findFragmentById(R.id.right_frame_layout);
    if (current != null) {
        // 当前 Fragment 不为空时,判断是否与目标 Fragment 类型相同,减少重复创建
        if (current.getClass() == fragment.getClass()) {
            return;
        }
    }

    FragmentTransaction transaction = fragmentManager.beginTransaction();
    transaction.replace(R.id.right_frame_layout, fragment);
    
    // 将此事务添加到后台堆栈。这意味着事务将在提交后被记住,并在以后从堆栈中弹出时将其操作反转
    // 传入 null 即可,其他字符串也行
    transaction.addToBackStack(null);
    // 提交事务
    transaction.commit();
}

Fragment 和 Activity 之间进行通信

前面已经知道在 Activity 中通过 getSupportFragmentManager().findFragmentById() 方法获取对应的 Fragment 对象,那也就可以调用 Fragment 的方法进行操作了。但是在 Fragment 中如何调用 Activity 中的方法呢?其实很简单,在 Fragment 中直接调用 getActivity() 就可以获取到 Activity 对象了(也可以使用 requireActivity(),内部多了一个判空操作)。而 Fragment 与 Fragment 之间通信则可以通过 Activity 作为跳板来实现。

使用限定符动态加载布局

前面使用的 Fragment 是显示在平板等大屏设备的双页模式下,但如果要在手机上运行应该显示成单页模式,那如何判断应该显示单页还是双页模式呢?这里可以在 res 资源目录下新建 layout-large 文件夹,在该文件夹下建立一个同名的布局文件,在这里编写双页模式的布局。这里的 large 就是一个限定符(Qualifier),系统会帮我们判断当前运行的设备是否是 large 大屏设备,然后加载对应的布局。当然,代码中的逻辑处理也需要改动一下。

large 限定符可以解决大屏设备上的单双页判断问题,但是我们并不知道 large 限定的大屏是多大。对此,我们还可以使用最小限定符(Smallest-width Qualifier),它可以指定屏幕宽度的最小值(以 dp 为单位),但屏幕宽度大于该值就加载这里的布局。如,在 res 资源目录下新建 layout-sw600dp 文件夹。

Fragment 的生命周期

API 28 之前的旧版 Fragment 生命周期:
Fragment 的生命周期

  1. onAttach(): 当 Fragment 和 Activity 相关联时调用。可以通过该方法获取 Activity 引用,还可以通过 getArguments() 获取参数。
  2. onCreate(): 当 Fragment 被创建时调用。
  3. onCreateView(): 当 Fragment 的布局被创建时调用。
  4. onActivityCreated(): 当 Activity 完成 onCreate() 时调用,已被弃用。
  5. onStart(): 当 Fragment 可见时调用。
  6. onResume(): 当 Fragment 可见且可交互时调用。
  7. onPause(): 当 Fragment 不可交互但可见时调用。
  8. onStop(): 当 Fragment 不可见时调用。
  9. onDestroyView(): 当 Fragment 的从视图结构中移除时调用。
  10. onDestroy(): 当销毁 Fragment 时调用。
  11. onDetach(): 当 Fragment 和 Activity 解除关联时调用。

API 28 之后 的新版 Fragment 生命周期:
Fragment 生命周期状态和回调以及 Fragment 视图生命周期的关系

新版 API 主要表现在 onActivityCreated() 被弃用,onViewCreated() 出现。将在后续篇章 onActivityCreated() 被弃用 中再作说明。

Fragment 生命周期状态和回调
在确定 Fragment 的生命周期状态时,FragmentManager 请考虑以下几点:

  1. Fragment 的最大状态由它的 FragmentManager 决定。 一个 Fragment 不能超越 FragmentManager 的状态。
  2. 作为 FragmentTransaction 的一部分,您可以使用 setMaxLifecycle() 为 Fragment 设置最大生命周期状态。
  3. Fragment 的生命周期状态永远不能大于其父级。例如,父 Fragment 或 Activity 必须在其子 Fragment 之前启动。同样,子 Fragment 必须在其父 Fragment 或 Activity 之前停止。

注意:避免使用 <fragment> 标签来添加使用 XML 的Fragment ,因为 <fragment> 标签允许 Fragment 状态超出其 FragmentManager 状态. 相反,应该使用 FragmentContainerView 用于 XML 添加 Fragment 。

androidx.fragment.app.FragmentContainerView

FragmentContainerView 是专为 Fragments 设计的自定义 Layout。它扩展了 FrameLayout,因此它可以可靠地处理 Fragment Transactions,并且它还具有与 Fragment 行为协调的附加功能。

官方已经建议将 <fragment> 标签和用于切换 Fragment 的 <FrameLayout> 标签替换成 androidx.fragment.app.FragmentContainerView 了。那在代码中又要做出什么更改呢?

在上面的例子中,如果直接替换容器的 <FrameLayout> 标签而不修改其他代码是没有问题的,不会影响程序运行。但是如果直接替换 <fragment> 标签就会出现在 Activity 中无法直接通过 findViewById() 获取到 Fragment 中的控件,这就会导致控件的 setOnClickListenner() 失败,报空指针异常。

因为使用 <FrameLayout> 标签是需要动态加载 Fragment 的,而 androidx.fragment.app.FragmentContainerView 继承自 FrameLayout,也需要动态加载,所以替换后并不需要修改逻辑代码。动态加载的 Fragment,想要获取其内部控件必须要在 onCreateView() 中得到 Fragment 的布局 View 之后才可以。

观察 Fragment 的源码发现,其中有个 onViewCreated(View view, Bundle savedInstanceState) 方法。该方法在 onCreateView() 结束后立即调用,其中的参数 View 正是 onCreateView() 返回的视图。

onCreateView() 一般用于初始化 Fragment 的视图,onViewCreated() 一般用于初始化视图内各个控件,而 onCreate() 用于初始化与 Fragment 视图无关的变量。

到这里其实就很好解决了,在 Fragment 中获取控件,然后将事件监听对象设置为 Activity 就好了。下面是修改后的代码。

public class LeftFragment extends Fragment {

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return  inflater.inflate(R.layout.left_fragment, container, false);
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        // 获取对应 Activity
        FragmentActivity activity = (FragmentActivity) getActivity();
        // 获取控件实例
        Button button1 = view.findViewById(R.id.button1_left_fragment);
        // 将控件的事件监听交给 Activity 处理
        // 让 Activity 处理是因为 FragmentManager 在 Activity 中
        button.setOnClickListener((View.OnClickListener) activity);
    }

}
<!-- activity_fragment.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/left_fragment_container"
        android:name="com.amie.firstlineofcode.LeftFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/right_fragment_container"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />

</LinearLayout>
// 依旧实现 View.OnClickListener 接口
public class FragmentActivity extends AppCompatActivity implements View.OnClickListener {

    // ...

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.button_left_fragment:
                replaceFragment(new AnotherRightFragment());
                break;
        }
    }
}

onActivityCreated() 被弃用

onActivityCreated() 是在宿主 Activity 的 onCreate() 完成之后立即调用,这也确保了宿主 Activity 的视图是完成了初始化。当然在这个方法内也可以操作宿主 Activity 视图的控件 View 或者获知其他 Fragment。

执行该方法时,与 Fragment 绑定的 Activity 的 onCreate() 方法已经执行完成并返回,在 onActivityCreated() 内可以进行与 Activity 交互的 UI 操作。但是,如果在 onActivityCreated() 之前 Activity 的 onCreate() 方法尚未执行完成,提前进行交互操作,会引发空指针异常。

替代方案:
谷歌为了管理 Fragment 的生命周期,实现了 LifecycleOwner,暴露了一个可以通过getLifecycle() 方法访问的 Lifecycle 对象。

因为 onActivityCreated() 是宿主 Activity 的 onCreate() 之后立即调用,所以可以在 onAttach() 的时候,通过订阅 Activity 的 lifecycle 来获取 Activity 的 onCreate() 事件,记得要 removeObserver()

 @Override
public void onAttach(@NonNull Context context) {
    super.onAttach(context);

    //requireActivity() 返回的是宿主 Activity
    requireActivity().getLifecycle().addObserver(new LifecycleEventObserver() {
        @Override
        public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
            if (event.getTargetState() == Lifecycle.State.CREATED){
                // TODO
                getLifecycle().removeObserver(this);  // 最后删除观察者
            }
        }
    });
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿咩AmieVastness

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值