14.Fragment碎片

1、碎片Fragment是什么?

自从谷歌在Android 3.0(API 11)推出Fragment以后,Fragment就成为了绝大多数APP的必备元素,其重要程度一点也不亚于四大组件。

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

从图中我们可以看到,在平板中,一个Activity A包含了两个Fragment,分别是Fragment A和Fragment B,

但在手机中呢,就需要两个Activity,分别是Activity A包含Fragment A和Activity B包含Fragment B。

同时每个Fragment都具有自己的一套生命周期回调方法,并各自处理自己的用户输入事件。

因此,在平板中使用一个Activity就可以了,左侧是列表,右边是内容详情。

Fragment优点:

(1)Fragment能够将Activity分离成多个可重用的组件,每个都有它自己的生命周期和UI

(2)Fragment可以轻松得创建动态灵活的UI设计,可以适应于不同的屏幕尺寸。从手机到平板电脑。

(3)Fragment是一个独立的模块,紧紧地与Activity绑定在一起。可以运行中动态地移除、加入、替换等。

(4)Fragment提供一个新的方式,可以在不同的安卓设备上统一的UI。

(5)Fragment轻量切换,更省内存,解决Activity间的切换不流畅的问题。

(6)Fragment替代TabActivity做导航,性能更好。

(7)Fragment在4.2版本中新增嵌套Fragment使用方法,能够生成更好的界面效果。

(8)Fragment做局部内容更新更方便,原来为了到达这一点,要把多个布局放到一个Activity里面,现在可以用多Fragment来代替,只有在需要的时候才加载Fragment,提高性能。

2、Fragment的使用

我们知道碎片通常是在平板开发中使用,因此我们首先要创建一个平板模拟器。

Fragment是依赖于Activity的,不能独立存在,一个Activity里可以有多个Fragment,一个Fragment也可以被多个Activity重用。

我们创建一个FragmentTest项目,默认创建MainActivity活动,该活动用于包含Fragment。

(1)静态添加Fragment碎片

我们要在MainActivity活动中添加两个碎片,并让这两个碎片平分活动空间。

碎片也是由布局文件逻辑代码组成,所以需要分别创建XML文件和Java文件。

我们使用向导创建两个碎片:LeftFragment 和 RightFragment

 

  1.修改左侧碎片布局fragment_left.xml,代码如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="Button" />
</LinearLayout>

这个布局非常简单,只放置了一个按钮,并让它水平居中。

2.修改LeftFragment类,它继承自androidx.fragment.app.Fragment类,代码如下:

import android.os.Bundle;

import androidx.fragment.app.Fragment;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class LeftFragment extends Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_left, container, false);
        return view;
    }
}

这里仅仅是重写了Fragment类的onCreateView()方法,然后在这个方法总通过LayoutInflater的inflate()方法将刚才定义的left_fragment布局动态的加载进来。

3.修改右侧碎片布局fragment_right.xml,代码如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#00ff00"
    android:orientation="vertical">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="这是一个右侧碎片"
        android:textSize="20sp" />
</LinearLayout>

我们将这个布局的背景色设置成了绿色,并放置了一个TextView用于显示一段文本。

4.同样再修改RightFragment类,代码如下:

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;

public class RightFragment extends Fragment {
    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.right_fragment, container, false);
        return view;
    }
}

5.修改activity_main.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/left_fragment"
        android:name="com.sdbi.fragmenttest.LeftFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />

    <fragment
        android:id="@+id/right_fragment"
        android:name="com.sdbi.fragmenttest.RightFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />
</LinearLayout>

可以看到,我们使用了<fragment>标签在布局中添加碎片,其中指定的大多数属性都是我们熟悉的,只不过这里还需要通过android:name属性来显式指明要添加的碎片类名,注意一定要将类的包名也加上

这样最简单的碎片示例就已经写好了,现在运行一下程序,效果如图所示。

 这时,如果我们将布局文件activity_main.xml切换到“Split”视图下,会发现有错误和警告提示,这些错误和警告都不会影响程序运行,不过我们还是要介绍一下。

Android Studio会提示一个错误(①处),但运行时不受影响。该信息是提示我们“<androidx.fragment.app.FragmentContainerView>标签允许一个布局文件在运行时动态包含不同的布局。在编辑布局时,不知道要使用哪个特定布局,您可以选择要预览的布局。

另外还有一个警告(②处),提示我们“缺少基线对齐”(baselineAligned)属性,这是因为我们在<LinearLayout>中包含了两个<fragment>,存在两个<fragment>中文本基线是否对齐的情况,默认<LinearLayout>的android:baselineAligned属性值是"true",这里我们可以把它改成"false"。

<?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:baselineAligned="false"
    android:orientation="horizontal">

    <fragment
        android:id="@+id/left_fragment"
        android:name="com.sdbi.fragmenttest.LeftFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        tools:layout="@layout/fragment_left" />

    <fragment
        android:id="@+id/right_fragment"
        android:name="com.sdbi.fragmenttest.RightFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        tools:layout="@layout/fragment_right" />
</LinearLayout>

运行程序,我们可以看到,两个碎片平分了整个活动的布局。

这个例子实在是太简单了,在真正的项目中很难有什么实际的作用,下面我们接着来学习关于碎片的高级使用技巧。

(2)动态添加Fragment碎片

Activity运行时可以动态地添加、替换或删除Fragment。

1.在上一节代码的基础上继续完善,新建fragment_another_right.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="#ffff00"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="这是另外一个右侧碎片"
        android:textSize="20sp" />
</LinearLayout>

这个布局文件的代码和fragment_right.xml中的代码基本相同,只是将背景色改成了黄色,并将显示的文字改了改。

2.新建AnotherRightFragment类,代码如下:

import android.os.Bundle;

import androidx.fragment.app.Fragment;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class AnotherRightFragment extends Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_another_right, container, false);
        return view;
    }
}

代码同样非常简单,在onCreateView()方法中加载了刚刚创建的fragment_another_right布局。

这样我们就准备好了另一个碎片,接下来看一下如何将它动态地添加到活动当中。

3.修改activity_main.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:baselineAligned="false"
    android:orientation="horizontal">

    <fragment
        android:id="@+id/left_fragment"
        android:name="com.sdbi.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>

可以看到,现在将右侧碎片<fragment>标签替换成了一个FrameLayout(帧布局),在这个布局中,所有的控件默认都会摆放在布局的左上角。

由于这里仅需要在布局里放入一个碎片,不需要任何定位,因此非常适合使用FrameLayout(帧布局)。

4.将在代码中向FrameLayout里添加内容,从而实现动态添加碎片的功能,这里的代码修改工作就稍微复杂一些了。

修改MainActivity中的代码,如下所示:

import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;

import android.annotation.SuppressLint;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

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

        addFragment(new RightFragment()); // 调用自定义的方法,动态添加右侧碎片
        // 直接使用findViewById()会提示错误,因为是Activity中包含Fragment,这个Button是在Fragment的布局文件中定义的。但是程序运行不受影响
//        Button button = (Button) findViewById(R.id.button);
        // 我们可以通过FragmentManager在Activity中指定获取某个Fragment视图,再获取Fragment里定义的控件
        Button button = getSupportFragmentManager().findFragmentById(R.id.left_fragment).getView().findViewById(R.id.button);
        button.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.button:
                replaceFragment(new AnotherRightFragment()); // 调用自定义的方法,动态替换右侧碎片
                break;
        }
    }

    // 自定义一个方法,用于动态添加右侧碎片
    private void addFragment(Fragment fragment) {
        FragmentManager fragmentManager = getSupportFragmentManager(); // 得到Fragment的管理者
        FragmentTransaction transaction = fragmentManager.beginTransaction(); // 开始一个事务
        transaction.add(R.id.right_layout, fragment); // 添加碎片
        transaction.commit(); // 提交事务
    }

    // 自定义一个方法,用于动态替换右侧碎片
    private void replaceFragment(Fragment fragment) {
        FragmentManager fragmentManager = getSupportFragmentManager(); // 得到Fragment的管理者
        FragmentTransaction transaction = fragmentManager.beginTransaction(); // 开始一个事务
        transaction.replace(R.id.right_layout, fragment); // 替换碎片
        transaction.commit(); // 提交事务
    }
}

可以看到,首先我们在MainActivity中自定义了两个方法addFragment()和replaceFragment(),用于动态添加和替换Fragment。

其次,在onCreate()方法中调用了addFragment()动态添加了RightFragment这个碎片;接着给左侧碎片中的按钮注册了一个点击事件,当点击左侧碎片中的按钮时调用replaceFragment()方法,将右侧碎片替换成AnotherRightFragment。

结合addFragment()和replaceFragment()两个方法中的代码可以看出,动态添加和替换碎片主要分为5步。

①创建待添加的碎片实例,new RightFragment()或new AnotherRightFragment()。

②获取FragmentManager实例,在活动中可以直接通过调用getSupportFragmentManager()方法得到。

③开启一个事务,通过调用FragmentManager的beginTransaction()方法开启。

④向容器内添加或替换碎片,使用事务的add()方法和replace()方法实现,需要传入容器的id和待添加的碎片实例。

⑤提交事务,调用事务的commit()方法来完成。

什么是事务?

事务是应用程序中一系列严密的操作,所有操作必须成功完成,否则在每个操作中所作的所有更改都会被撤消。

一个事务中的一系列的操作要么全部成功,要么一个都不做。 

事务的结束有两种,当事务中的所以步骤全部成功执行时,事务提交。

如果其中一个步骤失败,将发生回滚操作,撤消之前直到事务开始时的所有操作。

这样就完成了在活动中动态添加碎片的功能,重新运行程序,可以看到和之前相同的界面,然后点击一下按钮,效果如图所示。

(3)在碎片中模拟返回栈

之前我们成功实现了向活动中动态添加/替换碎片的功能,不过我们尝试一下就会发现,通过点击按钮替换了一个碎片之后,这时按下Back键程序就会直接退出。如果这里我们想模仿类似于返回栈的效果,按下Back键可以回到上一个碎片,该如何实现呢?

其实很简单,FragmentTransaction中提供了一个addToBackStack()方法,可以用于将一个事务(替换碎片的事务)添加到返回栈中,修改MainActivity中的代码,如下所示:

import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;

import android.annotation.SuppressLint;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

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

        addFragment(new RightFragment()); // 调用自定义的方法,动态添加右侧碎片
        // 这里会提示错误,因为是Activity中包含Fragment,这个Button是在Fragment的布局文件中定义的,但是程序运行不受影响
//        Button button = (Button) findViewById(R.id.button);
        // 我们可以通过FragmentManager在Activity中指定获取某个Fragment视图,再获取Fragment里定义的控件
        Button button = getSupportFragmentManager().findFragmentById(R.id.left_fragment).getView().findViewById(R.id.button);
        button.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.button:
                replaceFragment(new AnotherRightFragment()); // 调用自定义的方法,动态替换右侧碎片
                break;
        }
    }

    // 自定义一个方法,用于动态添加右侧碎片
    private void addFragment(Fragment fragment) {
        FragmentManager fragmentManager = getSupportFragmentManager(); // 得到Fragment的管理者
        FragmentTransaction transaction = fragmentManager.beginTransaction(); // 开始一个事务
        transaction.add(R.id.right_layout, fragment); // 添加碎片
        transaction.commit(); // 提交事务
    }

    // 自定义一个方法,用于动态替换右侧碎片
    private void replaceFragment(Fragment fragment) {
        FragmentManager fragmentManager = getSupportFragmentManager(); // 得到Fragment的管理者
        FragmentTransaction transaction = fragmentManager.beginTransaction(); // 开始一个事务
        transaction.replace(R.id.right_layout, fragment); // 替换碎片
        transaction.addToBackStack(null); // 将一个事务添加到返回栈中
        transaction.commit(); // 提交事务
    }
}

这里我们在replaceFragment()方法中的事务提交之前调用了FragmentTransaction的addToBackStack()方法,它可以接收一个名字用于描述返回栈的状态,一般传入null即可。

现在重新运行程序,并点击按钮将AnotherRightFragment添加到活动中,然后按下Back键,你会发现程序并没有退出,而是回到了RightFragment界面,再次按下Back键,程序退出。

(4)碎片和活动之间进行通信

虽然碎片都是嵌入在活动中显示的,可是碎片和活动都是各自存在于一个独立的类当中的,它们之间如何进行通信呢?

1.在活动中调用碎片的方法

FragmentManager提供了一个类似于findViewById()的方法,专门用于从布局文件中获取碎片的实例,代码如下所示:

RightFragment rightFragment = (RightFragment) getSupportFragmentManager().findFragmentById(R.id.right_fragment);

调用FragmentManager的findFragmentById()方法,可以在活动中得到相应碎片的实例,然后就能轻松地调用碎片里的方法了。

因为Activity中的getFragmentManager()方法已经弃用,所以我们使用getSupportFragmentManager()来获取当前Activity对应的FragmentManager。

2.在碎片中调用活动的方法

在每个碎片中都可以通过调用getActivity()方法来得到和当前碎片相关联的活动实例,代码如下所示:

MainActivity activity = (MainActivity) getActivity();

有了活动实例之后,在碎片中调用活动里的方法就变得轻而易举了。

另外当碎片中需要使用Context对象时,也可以使用getActivity()方法,因为获取到的活动本身就是一个Context对象。

既然碎片和活动之间的通信问题已经解决了,那么,碎片和碎片之间可不可以进行通信呢?

3.在碎片中调用碎片的方法

基本思路非常简单,首先在一个碎片中可以得到与它相关联的活动,然后再通过这个活动去获取另外一个碎片的实例,这样也就实现了不同碎片之间的通信功能。

3、碎片的生命周期

和活动一样,碎片也有自己的生命周期,并且它和活动的生命周期实在是太像了。

(1)碎片的状态和回调方法

还记得每个活动在其生命周期内可能会有哪几种状态吗?没错,一共有运行状态、暂停状态、停止状态和销毁状态这4种。

类似地,每个碎片在其生命周期内也可能会经历这几种状态,只不过在一些细小的地方会有部分区别。

1.运行状态

当一个碎片是可见的,并且它所关联的活动正处于运行状态时,该碎片也处于运行状态。

2.暂停状态

当一个活动进入暂停状态时(由于另一个未占满屏幕的活动被添加到了栈顶),与它相关联的可见碎片就会进入到暂停状态。

3.停止状态

当一个活动进入停止状态时,与它相关联的碎片就会进入到停止状态;或者通过调用FragmentTransaction的remove()、replace()方法将碎片从活动中移除,但如果在事务提交之前调用addToBackStack()方法(将其放入返回栈),这时的碎片也会进入到停止状态。总的来说,进入停止状态的碎片对用户来说是完全不可见的,有可能会被系统回收。

4.销毁状态

碎片总是依附于活动而存在的,因此当活动被销毁时,与它相关联的碎片就会进入到销毁状态;或者通过调用FragmentTransaction的remove()、replace()方法将碎片从活动中移除,但在事务提交之前并没有调用addToBackStack()方法(没有放入返回栈),这时的碎片也会进入到销毁状态。

结合之前的活动状态,相信你理解起来应该毫不费力吧。同样地,Fragment类中也提供了一系列的回调方法,以覆盖碎片生命周期的每个环节。其中,活动中有的回调方法,碎片中几乎都有,不过碎片还提供了一些附加的回调方法。

  • onAttach():当碎片和活动建立关联的时候调用。可以通过该方法获取Activity引用,还可以通过getArguments()获取参数。(attach:贴上,粘上)
  • onCreate():当Fragment被创建时调用。
  • onCreateView():当碎片创建视图(加载布局)时调用。
  • onActivityCreated():当Activity完成onCreate()时调用。
  • onStart():当Fragment可见时调用。
  • onResume():当Fragment可见且可交互时调用。
  • onPause():当Fragment不可交互但可见时调用。
  • onStop():当Fragment不可见时调用。
  • onDestroyView():当与碎片关联的视图被移除的时候调用。
  • onDestroy():当销毁Fragment时调用。
  • onDetach():当碎片和活动解除关联的时候调用。(detach:分离)

左图是碎片完整的生命周期示意图,右图是Activity与Fragment生命周期对应关系。

         

 

(2)体验碎片的生命周期

为了更加直观的体验碎片的生命周期,我们通过实例来观察RightFragment这个实例。

还是在FragmentTest项目的基础上来修改。修改RightFragment中的代码,如下所示:

import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;

public class RightFragment extends Fragment {
    private static final String TAG = "RightFragment";

    @Override
    public void onAttach(@NonNull Context context) {
        super.onAttach(context);
        Log.d(TAG, "onAttach");
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "onCreate");
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        Log.d(TAG, "onCreateView");
        View view = inflater.inflate(R.layout.right_fragment, container, false);
        return view;
    }

    @Override
    public void onActivityCreated(@Nullable 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中的打印信息,如图所示。

可以看到,当RightFragment第一次被加载到屏幕上时,会依次执行onAttach()、onCreate()、onCreateView()、onActivityCreated()、onStart()和onResume()方法。

然后点击LeftFragment中的按钮,此时打印信息如图所示。

由于AnotherRightFragment替换了RightFragment,此时的RightFragment进入了停止状态,因此onPause()、onStop()和onDestroyView()方法会得到执行。

当然如果在替换的时候没有调用addToBackStack()方法,此时的RightFragment就会进入销毁状态,onDestroy()和onDetach()方法就会得到执行(这种情况下,如果按下Back键,MainActicity将直接被销毁)。

接着按下Back键,RightFragment会重新回到屏幕,打印信息如图所示。

由于RightFragment重新回到了运行状态,因此onCreateView()、onActivityCreated()、onStart()和onResume()方法会得到执行。

注意此时onCreate()方法并不会执行,因为我们借助了addToBackStack()方法使得RightFragment和它的视图并没有销毁。

再次按下Back键退出程序,打印信息如图所示。

依次会抗行onPause()、onStop()、onDestroyView()、onDestroy()和onDetach()方法,最终将活动和碎片一起销毁。

这样碎片完整的生命周期我们也体验了一遍,是不是理解得更加深刻了?

另外值得一提的是,在碎片中也是可以通过onSaveInstanceState()方法来保存数据的,因为进入停止状态的碎片有可能在系统内存不足的时候被回收。

保存下来的数据在onCreate()、onCreateView()和onActivityCreated()这3个方法中都可以重新得到,它们都含有一个Bundle类型的savedInstanceState参数。

具体的代码就不在这里给出了,如果忘记了如何编写,可以参考《回收活动》那一部分。

【补充】在RightFragment中跳转到其他Activity,体验RightFragment进入暂停和停止状态。

类似体验《活动的生命周期》那节课(可以从ActivitylifeCycleTest项目中复制NormalActivity和DialogActivity相关代码,别忘了在清单文件里注册这两个活动)。

<activity
    android:name=".NormalActivity"
    android:exported="false" />
<activity
    android:name=".DialogActivity"
    android:exported="false"
    android:theme="@style/Theme.AppCompat.Dialog" />

我们在RightFragment中定义两个按钮,分别用于启动普通活动NormalActivity和对话框活动DialogActivity,我们需要在RightFragment的onActivityCreated()方法中为这两个按钮添加单击事件。

修改RightFragment的onActivityCreated()方法,代码如下:

import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;

public class RightFragment extends Fragment {
    private static final String TAG = "RightFragment";

    @Override
    public void onAttach(@NonNull Context context) {
        super.onAttach(context);
        Log.d(TAG, "onAttach");
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "onCreate");
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        Log.d(TAG, "onCreateView");
        View view = inflater.inflate(R.layout.right_fragment, container, false);
        return view;
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        Log.d(TAG, "onActivityCreated");
        Button btnNormal = (Button) getActivity().findViewById(R.id.btnNormal);
        btnNormal.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(getActivity(), NormalActivity.class);
                startActivity(intent);
            }
        });
        Button btnDialog = (Button)getActivity().findViewById(R.id.btnDialog);
        btnDialog.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(getActivity(), DialogActivity.class);
                startActivity(intent);
            }
        });
    }

    @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");
    }
}

运行程序,如图所示,点击右侧的两个按钮,注意观察Log输出信息。

在RightFragment中启动普通活动,运行结果如图。

在RightFragment中启动对话框活动,运行结果如图。

(3)onActivityCreated弃用后的替代方案

1)通过Lifecycle获取Activity的onCreate()事件

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

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

 

@Override
public void onAttach(@NonNull Context context) {
    super.onAttach(context);
    Log.d(TAG, "onAttach");

    //requireActivity() 返回的是宿主Activity
    requireActivity().getLifecycle().addObserver(new LifecycleEventObserver() {
        @Override
        public void onStateChanged(@NonNull @NotNull LifecycleOwner source, @NonNull @NotNull Lifecycle.Event event) {
            if (event.getTargetState() == Lifecycle.State.CREATED) {
                //在这里编写逻辑

                getLifecycle().removeObserver(this);  //这里是删除观察者
            }
        }
    });
}

3)使用onViewCreated()方法

在一般情况下,初始化View时,已经可以使用onViewCreated()方法来替代onActivityCreated()。

但是要注意:onViewCreated()方法的调用要早于onActivityCreated()的调用。

也就是说onViewCreated()方法调用时,碎片所依附的活动还没有创建完成,这时候无法去获取碎片或者时碎片中的控件,getActivity().findViewById()会返回空指针,有可能会出现空指针异常!!!

4、动态加载布局的技巧

之前的学习中,虽然能够动态添加碎片,但是它毕竟只是在一个布局文件中进行一些添加和替换操作。

如果程序能够根据设备的分辨率或屏幕大小在运行时来决定加载哪个布局,那我们可发挥的空间就更多了。

因此接下来我们就来探讨一下Android中动态加载布局的技巧。

(1)使用限定符

如果我们经常使用平板电脑,应该会发现现在很多的平板应用都采用的是双页模式(程序会在左侧的面板上显示一个包含子项的列表,在右侧的面板上显示内容),因为平板电脑的屏幕足够大,完全可以同时显示下两页的内容,但手机的屏幕一次就只能显示一页的内容,因此两个页面需要分开显示。

那么怎样才能在运行时判断程序应该是使用双页模式还是单页模式呢?这就需要借助限定符(Qualifers)来实现了。

1.修改FragmentTest项目中的activity_main.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/left_fragment"
        android:name="com.sdbi.fragmenttest.LeftFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

这里将多余的代码都删掉(删掉右侧FrameLayout布局),只留下一个左侧碎片,并让它充满整个父布局(宽度修改为match_parent)。

2.在res目录下新建layout-large文件夹(中划线/减号),在这个文件夹下新建一个布局,也叫作activity_main.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/left_fragment"
        android:name="com.sdbi.fragmenttest.LeftFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />

    <fragment
        android:id="@+id/right_fragment"
        android:name="com.sdbi.fragmenttest.RightFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="3" />

</LinearLayout>

可以看到,layout/activity_main布局只包含了一个碎片,即单页模式,而layout-large/activity_main布局包含了两个碎片,即双页模式。

其中large就是一个限定符,那些屏幕被认为是large的设备就会自动加载layout-large文件夹下的布局,而小屏幕的设备则还是会加载layout文件夹下的布局。

然后将MainActivity中addFragment()和replaceFragment()方法的定义以及调用都注释掉,并在平板模拟器上重新运行程序,效果如图所示。

 

再启动一个手机模拟器,并在这个模拟器上重新运行程序,效果如图所示。

 这样我们就实现了在程序运行时动态加载布局的功能。

【注意】由于Android Studio默认的是Android显示方式,而在这种方式下新建的layout-large文件夹是无法看到的,所以此时需要切换到Project方式。

Android中一些常见的屏幕限定符可以参考下表。

屏幕特征

屏幕特征

屏幕特征

大小

small

提供给小屏幕设备的资源

normal

提供给中等屏幕设备的资源

large

提供给大屏幕设备的资源

xlarge

提供给超大屏幕设备的资源

分辨率

ldpi

提供给低分辨率设备的资源(120dpi以下)

mdpi

提供给中等分辨率设备的资源(120dpi到160dpi)

hdpi

提供给高分辨率设备的资源(160dpi到240dpi)

xhdpi

提供给超高分辨率设备的资源(240dpi到320dpi)

xxhdpi

提供给超超高分辨率设备的资源(320dpi到480dpi)

xxxhdpi

提供给超超超高分辨率设备的资源(480dpi到640dpi)

nodpi

与屏幕密度无关的资源.系统不会针对屏幕密度对其中资源进行压缩或者拉伸

tvdpi

介于mdpi与hdpi之间,特定针对213dpi,专门为电视准备的,手机应用开发不需要关心这个密度值

方向

land

提供给横屏设备的资源

port

提供给竖屏设备的资源

宽高比

long

比标准屏幕宽高比明显的高或者宽的屏幕

notlong

和标准屏幕配置一样的屏幕宽高比

(2)使用最小宽度限定符

之前使用large限定符成功解决了单页双页的判断问题,不过很快又有一个新的问题出现了,large到底是指多大呢?

有的时候我们希望可以更加灵活地为不同设备加载布局,不管它们是不是被系统认定为large,这时就可以使用最小宽度限定符(Smallest-width Qualifier)了。

最小宽度限定符允许我们对屏幕的宽度指定一个最小值(以dp为单位),然后以这个最小值为临界点,屏幕宽度大于这个值的设备就加载一个布局,屏幕宽度小于这个值的设备就加载另一个布局。

在res目录下新建layout-sw600dp文件夹,然后在这个文件夹下新建activity_main.xml布局,代码和layout-large/activity_main基本一样,我们稍调整一下宽度比例,代码如下所示:

<?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/left_fragment"
        android:name="com.sdbi.fragmenttest.LeftFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />

    <fragment
        android:id="@+id/right_fragment"
        android:name="com.sdbi.fragmenttest.RightFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />

</LinearLayout>

这就意味着,当程序运行在屏幕宽度大于600dp的设备上时,会加载layout-sw600dp/activity_main布局,当程序运行在屏幕宽度小于600dp的设备上时,则仍然加载默认的layout/activity_main布局。

 

 最小宽度限定符是在Android 3.2版本引入的,由于这里我们最低兼容的系统版本是9.0,所以可以放心地使用它。

5、碎片的最佳实践----一个简易版的新闻应用

碎片很多时候都是在平板开发当中使用的,主要是为了解决屏幕空间不能充分利用的问题。那是不是就表明,我们开发的程序都需要提供一个手机版和一个Pad版呢?确实有不少公司都是这么做的,但是这样会浪费很多的人力物力。因为维护两个版本的代码成本很高,每当增加什么新功能时,需要在两份代码里各写一遍,每当发现一个bug时,需要在两份代码里各修改一次。因此今天我们最佳实践的内容就是,如何编写同时兼容手机和平板的应用程序。

编写一个简易版的新闻应用,要求是可以同时兼容手机和平板的。

【分析】

(1)横屏,最少宽度600dp,双页模式,一个Activity,左边是新闻列表RecyclerView,右边显示新闻内容。点击新闻列表中的子项目,右侧显示新闻内容。

 (2)竖屏,单页模式,第一页新闻列表,第二页显示新闻内容,两个Activity。点击第一页新闻列表RecyclerView中的子项,启动第二页显示新闻内容。

   

【实战】

新建一个FragmentBestTest项目。

(1)准备好一个新闻的实体类,新建类News。

News类的代码比较简单,有两个属性:title字段表示新闻标题,content字段表示新闻内容。代码如下所示:

public class News {
    private String title; // 新闻标题
    private String content; // 新闻内容

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

(2)创建显示新闻内容的Fragment(双页模式右边)

使用向导创建一个用于显式新闻内容的碎片,名为NewsContentFragment,对应的布局文件是fragment_news_content.xml。

1)显示新闻内容的布局文件fragment_news_content.xml,代码如下:

<?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">
    <!-- 新闻内容的布局分为两部分 -->
    <LinearLayout
        android:id="@+id/visiblity_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:visibility="invisible">
        <!-- 1、头部部分:显示新闻标题 -->
        <TextView
            android:id="@+id/news_title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:padding="10dp"
            android:textSize="20sp" />
        <!-- 中间用一条黑线隔开  -->
        <View
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:background="#000" />
        <!-- 2、正文部分:显示新闻内容 -->
        <TextView
            android:id="@+id/news_content"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:padding="15dp"
            android:textSize="18sp" />
    </LinearLayout>

    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:layout_alignParentLeft="true"
        android:background="#000" />
</RelativeLayout>

通过上面代码可以看到,用于显示新闻内容的布局(<LinearLayout>)的可见性属性android:visibility初始状态是“不可见的”(invisible),后期我们可以在代码中动态的设置它的可见性。

新闻内容的布局主要可以分为两个部分,头部部分显示新闻标题,正文部分显示新闻内容,中间使用一条细线分隔开。

这里的细线是利用<View>标签来实现的,将View的宽或高设置1dp,再通过background属性给细线设置一下颜色就可以了。这里我们把细线设置成黑色。

2)碎片类NewsContentFragment,加载新闻内容布局文件,代码如下所示:

import android.os.Bundle;

import androidx.fragment.app.Fragment;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

public class NewsContentFragment extends Fragment {
    private View view; // 全局变量

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        view = inflater.inflate(R.layout.fragment_news_content, container, false);
        return view;
    }

    public void refresh(String newsTitle, String newsContent) {
        View visibilityLayout = view.findViewById(R.id.visiblity_layout);
        visibilityLayout.setVisibility(View.VISIBLE);
        //分别获取到新闻标题和新闻内容的控件
        TextView newsTilteText = (TextView) view.findViewById(R.id.news_title);
        TextView newsContentText = (TextView) view.findViewById(R.id.news_content);
        newsTilteText.setText(newsTitle); //刷新新闻的标题
        newsContentText.setText(newsContent); //刷新新闻的内容
    }
}

首先在onCreateView()方法里加载了我们刚刚创建的fragment_news_content布局,对于全局变量view对象进行了赋值。

接下来又定义了一个refresh()方法,在这个方法中先通过view对象的findViewById()方法找到之前设置的那个不可见的线性布局,使其可见;

然后通过view对象的findViewById()方法分别获取到新闻标题和内容的控件,将方法传递进来的参数(新闻的标题和内容)显示到相应的控件上。

(3)创建单页模式使用的Activity(单页模式)

这样我们就把新闻内容的碎片和布局都创建好了,但是它们都是在双页模式中使用的,如果想在单页模式中使用的话,我们还需要再创建一个活动。

右击com.sdbi.fragmentbesttest包 > New > Activity > Empty Activity,使用向导新建一个NewsContentActivity,指定布局文件名称为activity_news_content。

1)先修改activity_news_content.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="vertical">

    <fragment
        android:id="@+id/news_content_fragment"
        android:name="com.sdbi.fragmentbesttest.NewsContentFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

这里我们充分发挥了Fragment的复用性,直接在布局中引入了之前定义好的NewsContentFragment,这样也就相当于把fragment_news_content布局的内容自动加了进来。

2)然后修改NewsContentActivity中的代码,如下所示:

import androidx.appcompat.app.AppCompatActivity;

import android.content.Context;
import android.content.Intent;
import android.os.Bundle;

public class NewsContentActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_news_content);
        String newsTitle = getIntent().getStringExtra("news_title");
        String newsContent = getIntent().getStringExtra("news_content");
        NewsContentFragment newsContentFragment = (NewsContentFragment) getSupportFragmentManager().findFragmentById(R.id.news_content_fragment);
        newsContentFragment.refresh(newsTitle, newsContent); // 刷新NewsContentFragment界面
    }

    // 自定义静态方法,在启动活动的同时,传递数据
    public static void actionStart(Context context, String newsTitle, String newsContent) {
        Intent intent = new Intent(context, NewsContentActivity.class);
        intent.putExtra("news_title", newsTitle);
        intent.putExtra("news_content", newsContent);
        context.startActivity(intent);
    }
}

可以看到,在onCreate()方法中我们通过Intent获取到了传入的新闻标题和新闻内容,然后调用FragmentManager的findFragmentById()方法得到了NewsContentFragment的实例,接着调用它的refresh()方法,并将新闻的标题和内容传入,就可以把这些数据显示出来了。

【注意】这里我们还定义了一个静态方法actionStart()方法,在我们的活动中定义这样一个方法的好处是,保证了actionStart()方法中存放数据的键和onCreate()方法中获取数据的键一致,别的程序员只要想启动我们当前这个活动,不需要自己定义Intent来启动活动和存放数据,只要通过类名调用这个静态actionStart()方法,传入对应的参数(当前上下文和数据内容)即可。这样可以清晰的知道,要启动目标活动需要传递哪些数据,从而减少使用我们定义Activity的出错几率。这是一种非常好的定义Activity的方式。

(4)创建显示新闻列表的Fragment(双页模式左边)

要创建显示新闻列表的布局,我们就要使用到RecyclerView,查看是否可以直接使用该控件。

  

使用向导创建显示新闻列表的碎片,名为NewsTitleFragment,对应的布局文件是fragment_news_title.xml。

1)修改fragment_news_title.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="vertical">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/news_title_recyclerview"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

这个布局的代码非常简单,里面只有一个用于显示新闻列表的RecyclerView。既然要用到RecyclerView,那么就必定少不了子项的布局。

2)新建news_item.xml作为RecyclerView子项的布局,代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/news_title"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:ellipsize="end"
    android:paddingLeft="10dp"
    android:paddingTop="15dp"
    android:paddingRight="10dp"
    android:paddingBottom="15dp"
    android:singleLine="true"
    android:textSize="18sp" />

子项的布局也非常简单,只有一个TextView。仔细观察TextView的几个属性:

  • android:padding表示给控件的周围(外边距)加上补白,这样不至于让文本内容会紧靠在边缘上;
  • android:singleline设置为true,表示让这个TextView只能单行显示;
  • android:ellipsize用于设定当文本内容超出控件宽度时,文本的缩略方式,这里指定成end表示省略号在尾部进行缩略。此外还有其他几个属性值可以选择:"start"表示省略号在开头;"middle"表示省略号在中间;"marquee"表示跑马灯效果。

3)然后,修改显示新闻列表的碎片类NewsTitleFragment,加载刚刚创建的新闻列表布局fragment_news_title.xml,代码如下所示:

import android.os.Bundle;

import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class NewsTitleFragment extends Fragment {
    private boolean isTwoPage; // 全局变量

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

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        if (getActivity().findViewById(R.id.news_content_layout) != null) {
            isTwoPage = true;
        } else {
            isTwoPage = false;
        }
    }
}

可以看到,在onCreateView()方法中加载了fragment_news_title布局,这个没什么好说的。

我们要注意看一下onActivityCreated()方法,这个方法通过在活动中能否找到一个id为news_content_layout的View来判断当前是双页模式还是单页模式。

因此我们需要让这个id为news_content_layout的View只在双页模式中才会出现。(这时我们使用R.id.news_content_layout会报错,因为我们还没有定义这个资源id)

这里注意,虽然onActivityCreated()方法已提示废弃使用,我们在这里也不能使用onViewCreated()方法来替代onActivityCreated()。

因为onViewCreated()方法里无法获取到news_content_layout控件,此时Activity还没有创建完成。

(5)让主活动包含新闻列表碎片(左边)和新闻内容碎片(右边)

我们需要借助限定符来让系统自动判断是双页模式还是单页模式。

首先修改layout文件夹下的activity_main.xml文件,代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/news_title_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <fragment
        android:id="@+id/news_title_fragment"
        android:name="com.sdbi.fragmentbesttest.NewsTitleFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</FrameLayout>

上述代码表示,在单页模式下,只使用<FrameLayout>标记加载一个新闻标题的碎片(双页模式下左边部分)。

然后新建layout-sw600dp文件夹,在这个文件夹下再新建一个activity_main.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/news_title_fragment"
        android:name="com.sdbi.fragmentbesttest.NewsTitleFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />

    <FrameLayout
        android:id="@+id/news_content_layout"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="3">

        <fragment
            android:id="@+id/news_content_fragment"
            android:name="com.sdbi.fragmentbesttest.NewsContentFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </FrameLayout>
</LinearLayout>

可以看出,在双页模式下我们同时引入了两个碎片,并将新闻内容的碎片放在了一个FrameLayout布局下,而这个布局的id正是news_content_layout

因此,能够找到这个id的时候就是双页模式,否则就是单面模式。

(6)在NewsTitleFragment中通过RecyclerView将新闻列表展示出来

在NewsTitleFragment中新建一个内部类NewsAdapter来作为RecyclerView的适配器,代码如下所示: 

import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class NewsTitleFragment extends Fragment {
    private boolean isTwoPage; // 全局变量

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_news_title, container, false);
        return view;
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        if (getActivity().findViewById(R.id.news_content_layout) != null) {
            isTwoPage = true;
        } else {
            isTwoPage = false;
        }
    }

    class NewsAdapter extends RecyclerView.Adapter<NewsAdapter.ViewHolder> {
        private List<News> mNewsList;

        class ViewHolder extends RecyclerView.ViewHolder {
            TextView newsTitleText;

            public ViewHolder(View itemView) {
                super(itemView);
                newsTitleText = itemView.findViewById(R.id.news_title);
            }
        }

        public NewsAdapter(List<News> newsList) {
            mNewsList = newsList;
        }

        @NonNull
        @Override
        public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.news_item, parent, false);
            ViewHolder holder = new ViewHolder(view);
            view.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    News news = mNewsList.get(holder.getAdapterPosition());
                    if (isTwoPage) {
                        // 如果是双页模式,则刷新NewsContentFragment的内容
                        // 这里使用getFragmentManager()会提示弃用,这里我们可以使用getParentFragmentManager()获得当前Fragment所依附的Activity的FragmentManager
//                        NewsContentFragment newsContentFragment = (NewsContentFragment) getFragmentManager().findFragmentById(R.id.news_content_fragment);
                        NewsContentFragment newsContentFragment = (NewsContentFragment) getParentFragmentManager().findFragmentById(R.id.news_content_fragment);
                        newsContentFragment.refresh(news.getTitle(), news.getContent());
                    } else {
                        // 如果是单页模式,则直接启动NewsContentActivity
                        NewsContentActivity.actionStart(getActivity(), news.getTitle(), news.getContent());
                    }
                }
            });
            return holder;
        }

        @Override
        public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
            News news = mNewsList.get(position);
            holder.newsTitleText.setText(news.getTitle());
        }

        @Override
        public int getItemCount() {
            return mNewsList.size();
        }
    }
}

FragmentManager说明

我们在上一段程序中想通过getFragmentManager()方法获取FragmentManager,但是发现Fragment类中的这个方法被弃用了,我们如何替代呢?
这里给大家介绍一下几个获取FragmentManager的方法,要根据当前是哪个类来决定。
getSupportFragmentManager():与 Activity 关联,可以将其视为 Activity 的 FragmentManager;
getChildFragmentManager():与 Fragment 关联,可以将其视为 Fragment 的 FragmentManager;
getParentFragmentManager():情况稍微复杂,正常情况返回的是该 Fragment 依附的Activity的FragmentManager。如果该 Fragment 是另一个 Fragment 的子 Fragment,则返回的是其父 Fragment的 getChildFragmentManager()。

这里,RecyclerView的用法我们已经相当熟练了,因此这个适配器的代码对你来说应该没有什么难度吧?

需要注意的是,之前我们都是将适配器写成一个独立的类,其实也是可以写成内部类的,这里写成内部类的好处就是可以直接访问NewsTitleFragment的变量,比如isTwoPane

观察一下onCreateViewHolder()方法中注册的点击事件,首先获取到了点击项的News实例,然后通过isTwoPane变量来判断当前是单页还是双页模式,如果是单页模式,就启动一个新的活动去显示新闻内容,如果是双页模式,就更新新闻内容碎片里的数据。

(7)向RecyclerView中填充数据

修改NewsTitleFragment中的代码,如下所示:

import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class NewsTitleFragment extends Fragment {
    private boolean isTwoPage; // 全局变量

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,  Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_news_title, container, false);
        RecyclerView newsTitleRecyclerView = view.findViewById(R.id.news_title_recyclerview);
        LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
        newsTitleRecyclerView.setLayoutManager(layoutManager);
        NewsAdapter adapter = new NewsAdapter(getNews());
        newsTitleRecyclerView.setAdapter(adapter);
        return view;
    }

    // 新增自定义方法:getNews(),用于获取测试数据
    private List<News> getNews() {
        List<News> newsList = new ArrayList<>();
        for (int i = 0; i <= 50; i++) {
            News news = new News();
            news.setTitle("This is news title " + i);
            news.setContent(getRandomLengthContent("This is news content " + i + "."));
            newsList.add(news);
        }
        return newsList;
    }

    // 新增自定义方法:getRandomLengthContent(),用于随机生成不同长度的新闻内容
    private String getRandomLengthContent(String content) {
        Random random = new Random();
        int length = random.nextInt(20) + 1;
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < length; i++) {
            builder.append(content);
        }
        return builder.toString();
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        if (getActivity().findViewById(R.id.news_content_layout) != null) {
            isTwoPage = true;
        } else {
            isTwoPage = false;
        }
    }

    class NewsAdapter extends RecyclerView.Adapter<NewsAdapter.ViewHolder> {
        private List<News> mNewsList;

        class ViewHolder extends RecyclerView.ViewHolder {
            TextView newsTitleText;

            public ViewHolder(View itemView) {
                super(itemView);
                newsTitleText = itemView.findViewById(R.id.news_title);
            }
        }

        public NewsAdapter(List<News> newsList) {
            mNewsList = newsList;
        }

        @NonNull
        @Override
        public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.news_item, parent, false);
            ViewHolder holder = new ViewHolder(view);
            view.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    News news = mNewsList.get(holder.getAdapterPosition());
                    if (isTwoPage) {
                        // 如果是双页模式,则刷新NewsContentFragment的内容
                        // 这里使用getFragmentManager()会提示弃用,这里我们可以使用getParentFragmentManager()获得当前Fragment所依附的Activity的FragmentManager
//                        NewsContentFragment newsContentFragment = (NewsContentFragment) getFragmentManager().findFragmentById(R.id.news_content_fragment);
                        NewsContentFragment newsContentFragment = (NewsContentFragment) getParentFragmentManager().findFragmentById(R.id.news_content_fragment);
                        newsContentFragment.refresh(news.getTitle(), news.getContent());
                    } else {
                        // 如果是单页模式,则直接启动NewsContentActivity
                        NewsContentActivity.actionStart(getActivity(), news.getTitle(), news.getContent());
                    }
                }
            });
            return holder;
        }

        @Override
        public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
            News news = mNewsList.get(position);
            holder.newsTitleText.setText(news.getTitle());
        }

        @Override
        public int getItemCount() {
            return mNewsList.size();
        }
    }
}

可以看到,onCreateView()方法中添加了RecyclerView标准的使用方法,在碎片中使用RecyclerView和在活动中使用几乎是一模一样的,相信没有什么需要解释的。另外,这里调用了getNews()方法来初始化50条模拟新闻数据,同样使用了一个getRandomLengthContent()方法来随机生成新闻内容的长度,以保证每条新闻的内容差距比较大,相信你对这个方法肯定不会陌生了。

这样我们所有的编写工作就已经完成了,赶快来运行一下吧。

首先在手机模拟器上运行,可以看到许多条新闻的标题,然后点击第一条新闻,会启动一个新的活动来显示新闻的内容效果,如图所示。

     

接下来,将程序在平板模拟器上运行,同样点击第一条新闻,效果如图所示。

(8)通过网络获取真实新闻

1)增加一个常量类

定义获取数据的URL,测试这些URL可以获取数据

Const.java

public class Const {
    public static final String URL = "http://218.7.112.123:10001"; //内网服务器http://10.54.38.55:10001/
    public static final String PRESS_PRESS_LIST = URL + "/prod-api/press/press/list";
}

2)定义JsonBean类

保存Json数据

import java.util.List;

public class JsonBean {
    private int total;
    private int code;
    private String msg;
    private List<RowsBean> rows;

    public int getTotal() {
        return total;
    }

    public void setTotal(int total) {
        this.total = total;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public List<RowsBean> getRows() {
        return rows;
    }

    public void setRows(List<RowsBean> rows) {
        this.rows = rows;
    }

    public class RowsBean {
        private Object searchValue;
        private String createBy;
        private String createTime;
        private String updateBy;
        private String updateTime;
        private Object remark;
        private Object params;
        private int id;
        private String appType;
        private String cover;
        private String title;
        private Object subTitle;
        private String content;
        private String status;
        private String publishDate;
        private Object tags;
        private int commentNum;
        private int likeNum;
        private int readNum;
        private String type;
        private String top;
        private String hot;

        public Object getSearchValue() {
            return searchValue;
        }

        public void setSearchValue(Object searchValue) {
            this.searchValue = searchValue;
        }

        public String getCreateBy() {
            return createBy;
        }

        public void setCreateBy(String createBy) {
            this.createBy = createBy;
        }

        public String getCreateTime() {
            return createTime;
        }

        public void setCreateTime(String createTime) {
            this.createTime = createTime;
        }

        public String getUpdateBy() {
            return updateBy;
        }

        public void setUpdateBy(String updateBy) {
            this.updateBy = updateBy;
        }

        public String getUpdateTime() {
            return updateTime;
        }

        public void setUpdateTime(String updateTime) {
            this.updateTime = updateTime;
        }

        public Object getRemark() {
            return remark;
        }

        public void setRemark(Object remark) {
            this.remark = remark;
        }

        public Object getParams() {
            return params;
        }

        public void setParams(Object params) {
            this.params = params;
        }

        public int getId() {
            return id;
        }

        public void setId(int id) {
            this.id = id;
        }

        public String getAppType() {
            return appType;
        }

        public void setAppType(String appType) {
            this.appType = appType;
        }

        public String getCover() {
            return cover;
        }

        public void setCover(String cover) {
            this.cover = cover;
        }

        public String getTitle() {
            return title;
        }

        public void setTitle(String title) {
            this.title = title;
        }

        public Object getSubTitle() {
            return subTitle;
        }

        public void setSubTitle(Object subTitle) {
            this.subTitle = subTitle;
        }

        public String getContent() {
            return content;
        }

        public void setContent(String content) {
            this.content = content;
        }

        public String getStatus() {
            return status;
        }

        public void setStatus(String status) {
            this.status = status;
        }

        public String getPublishDate() {
            return publishDate;
        }

        public void setPublishDate(String publishDate) {
            this.publishDate = publishDate;
        }

        public Object getTags() {
            return tags;
        }

        public void setTags(Object tags) {
            this.tags = tags;
        }

        public int getCommentNum() {
            return commentNum;
        }

        public void setCommentNum(int commentNum) {
            this.commentNum = commentNum;
        }

        public int getLikeNum() {
            return likeNum;
        }

        public void setLikeNum(int likeNum) {
            this.likeNum = likeNum;
        }

        public int getReadNum() {
            return readNum;
        }

        public void setReadNum(int readNum) {
            this.readNum = readNum;
        }

        public String getType() {
            return type;
        }

        public void setType(String type) {
            this.type = type;
        }

        public String getTop() {
            return top;
        }

        public void setTop(String top) {
            this.top = top;
        }

        public String getHot() {
            return hot;
        }

        public void setHot(String hot) {
            this.hot = hot;
        }
    }
}

3)修改NewsTitleFragment

增加获取网络数据访问的逻辑。

NewsTitleFragment.java

import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;

import com.google.gson.Gson;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class NewsTitleFragment extends Fragment {
    private static final String TAG = "NewsTitleFragment";
    private boolean isTwoPage; // 记录是否时双页的一个标识
    private String strJson; // 全局变量,存放响应结果的字符串
    private RecyclerView newsTitleRecyclerview; // 变为全局变量

    private Handler handler = new Handler() {
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case 1:
                    Log.d(TAG, "handleMessage: strJson = " + strJson);
                    NewsAdapter adapter = new NewsAdapter(getNews());
                    newsTitleRecyclerview.setAdapter(adapter);
                    break;
            }
        }
    };

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_news_title, container, false);

        newsTitleRecyclerview = view.findViewById(R.id.news_title_recyclerview);
        LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
        newsTitleRecyclerview.setLayoutManager(layoutManager);

        // 调用我们定义的GET异步请求方法,使用匿名内部类的方式,创建一个回调对象,作为第二个参数传入
        getJsonByGetAsync(Const.PRESS_PRESS_LIST, new Callback() {
            @Override
            public void onFailure(@NonNull Call call, @NonNull IOException e) {
                // 失败了,怎么办?
                Log.d(TAG, "onFailure: e = " + e.getMessage());
                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(getActivity(), "访问失败" + e, Toast.LENGTH_SHORT).show();
                    }
                });
            }

            @Override
            public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
                // 成功了,怎么办?
                if (response.isSuccessful()) {
                    strJson = response.body().string(); // 保存到了全局变量里
                    Log.d(TAG, "onResponse:response = " + strJson);
                    // 通过getActivity()我们可以在Fragment中获取他所在的Activity对象
                    // runOnUiThread()是Activity中定义的方法,他可以将子线程中的数据传递给UI主线程
                    getActivity().runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            handler.sendEmptyMessage(1); // 给主线程发一个空消息
                        }
                    });
                }
            }
        });
        return view;
    }

    // 重写生命周期方法
    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        if (getActivity().findViewById(R.id.news_content_framelayout) != null) {
            // news_content_layout帧式布局是在双页布局文件中包含右侧碎片的布局id
            isTwoPage = true;
        } else {
            isTwoPage = false;
        }
    }

    class NewsAdapter extends RecyclerView.Adapter<NewsAdapter.ViewHolder> {
        private List<News> mNewsList;

        public class ViewHolder extends RecyclerView.ViewHolder {
            TextView newsTitleText;

            public ViewHolder(View itemView) {
                super(itemView);
                newsTitleText = itemView.findViewById(R.id.news_title);
            }
        }

        public NewsAdapter(List<News> newsList) {
            mNewsList = newsList;
        }

        @NonNull
        @Override
        public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false);
            ViewHolder holder = new ViewHolder(view);
            view.setOnClickListener(new View.OnClickListener() { // 给题目碎片当中的子项目绑定单击事件
                @Override
                public void onClick(View v) {
                    News news = mNewsList.get(holder.getAdapterPosition());
                    // 双页和单页情况
                    if (isTwoPage) {
                        // 双页情况
                        // 当前是在左侧的碎片中,我们需要去调用右侧碎片中定义的刷新方法,更新显示的内容
                        NewsContentFragment newsContentFragment = (NewsContentFragment) getActivity().getSupportFragmentManager().findFragmentById(R.id.news_content_fragment);
                        newsContentFragment.refresh(news.getTitle(), news.getContent());
                    } else {
                        // 单页情况
                        NewsContentActivity.actionStart(getActivity(), news.getTitle(), news.getContent());
                    }
                }
            });
            return holder;
        }

        @Override
        public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
            News news = mNewsList.get(position);
            holder.newsTitleText.setText(news.getTitle());
        }

        @Override
        public int getItemCount() {
            return mNewsList.size();
        }
    }

    // 自定义方法,模拟一些新闻数据
    private List<News> getNews() {
        // Gson
        Gson gson = new Gson();
        JsonBean jsonBean = gson.fromJson(strJson, JsonBean.class);

        List<News> newsList = new ArrayList<>();
        for (int i = 0; i < jsonBean.getRows().size(); i++) {
            News news = new News();
            news.setTitle(jsonBean.getRows().get(i).getTitle());
            news.setContent(jsonBean.getRows().get(i).getContent());
            newsList.add(news);
        }
        return newsList;
    }

    // 新增自定义方法:getRandomLengthContent(),用于随机生成不同长度的新闻内容
    private String getRandomLengthContent(String content) {
        Random random = new Random();
        int length = random.nextInt(20) + 1;
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < length; i++) {
            builder.append(content);
        }
        return builder.toString();
    }

    // GET方式,同步方式
//    public String getJsonByGetSync(String url) {
//        String strJson = "";
//        OkHttpClient okHttpClient = new OkHttpClient();  // 创建OkHttpClient对象
//        Request request = new Request.Builder().url(url).build(); // 创建一个Request对象,设置请求参数
//        Call call = okHttpClient.newCall(request);
//        Response response = null; // 得到响应对象
//        try {
//            response = call.execute();
//            if (response.isSuccessful()) {
//                strJson = response.body().string();
//            }
//        } catch (IOException e) {
//            e.printStackTrace();
//        }
//        return strJson;
//    }

    // GET方式,异步方式
    public void getJsonByGetAsync(String url, Callback callback) {
        OkHttpClient okHttpClient = new OkHttpClient();  // 创建OkHttpClient对象
        Request request = new Request.Builder().url(url).build(); // 创建一个Request对象,设置请求参数
        Call call = okHttpClient.newCall(request); // 发送请求
        call.enqueue(callback); // 回调,反馈
    }

}

4)修改新闻内容碎片布局文件

因为本例从网络上获取的新闻内容是HTML内容,所以我们要使用WebView控件来解析HTML文件。

fragment_news_content.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
    tools:context=".NewsContentFragment">

    <LinearLayout
        android:id="@+id/visiblity_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:visibility="invisible">

        <TextView
            android:id="@+id/news_title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:padding="10dp"
            android:text="标题"
            android:textSize="20sp" />

        <View
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:background="#000" />

        <WebView
            android:id="@+id/news_content"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:padding="15dp"
            android:text="内容"
            android:textSize="18sp" />

    </LinearLayout>

    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:layout_alignParentLeft="true"
        android:background="#000" />

</RelativeLayout>

5)修改新闻内容碎片类

让WebView控件能够解析HTML文件内容和图片。

NewsContentFragment.java

import android.os.Bundle;

import androidx.fragment.app.Fragment;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebView;
import android.widget.LinearLayout;
import android.widget.TextView;

public class NewsContentFragment extends Fragment {
    private View view;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        view = inflater.inflate(R.layout.fragment_news_content, container, false);
        return view;
    }

    // 定义一个方法,根据选中的新闻,刷新碎片中显示的内容
    public void refresh(String newsTitle, String newsContent) {
        LinearLayout visiblity_layout = view.findViewById(R.id.visiblity_layout);
        visiblity_layout.setVisibility(View.VISIBLE); // 把原来不可见的新闻内容布局变成可见

        TextView newsTitleText = view.findViewById(R.id.news_title);
//        TextView newsContentText = view.findViewById(R.id.news_content);
        WebView newsContentText = view.findViewById(R.id.news_content);
        newsTitleText.setText(newsTitle);
//        newsContentText.setText(newsContent);
        newsContent = newsContent.replace("/prod-api", Const.URL + "/prod-api");
        newsContentText.loadData(newsContent, "text/html", "utf-8");
    }
}

 

运行程序

 

        

 

posted @ 2022-09-23 09:46  熊猫Panda先生  阅读(644)  评论(1编辑  收藏  举报