7 片段Fragment:模块化

1 引入

1.1 引入

你已经了解了如何创建“始终如一”的应用,不论它们在什么设备上运行都会以同样的方式工作。
不过,如果你希望应用根据运行的不同环境(比如在手机上运行还是在平板电脑上运行)有不同的外观和行为,该怎么做呢?

在这一章中,我们将介绍如何让你的应用根据设备的屏幕大小选择最合适的布局。我们还会介绍片段,采用这种方法能够创建可以由不同活动重用的模块化代码组件

1.2 不同屏幕大小的处理--片段引入

Android开发有很多优点,其中之一就是你可以在屏幕大小和处理器完全不同的设备上用同样的方式运行完全相同的应用。不过,这并不是说应用看上去都一样。
手机上的显示

 平板上的显示

要让手机上和平板电脑上显示的用户界面彼此不同,可以分别为大设备和小设备使用不同的布局

应用可能还需要有不同的行为

只是为不同的设备使用不同的布局还不够。除了布局,还需要运行不同的Java代码,使应用能根据不同的设备有不同的行为。

例如,在Workout应用中,需要为平板电脑提供一个活动,而为手机提供两个活动

 而平板电脑上: 

不过,这可能意味着要重复的代码。 

第2个活动只在手机上运行,需要在布局中插入一个训练项目的详细信息。不过在平板电脑上运行这个应用时,主活动也需要这个代码。也就是说,会有多个活动运行同样的代码。

不用在两个活动中重复同样的代码,这里可以使用片段。什么是片段呢?

1.3 片段Fragment

片段(Fragments)就像可重用的组件或子活动。片段用来控制屏幕的一部分,可以在不同屏幕间重用。这说明,可以为训练项目列表创建一个片段,另外创建一个片段显示一个训练项目的详细信息。然后可以在布局之间共享这些片段

片段有布局

和活动一样,片段也有一个关联的布局

如果精心设计,可以使用java代码完全控制界面。如果片段代码包含控制布局所需的全部内容,就能大大增加这个片段在应用中重用的机会。

2.项目构建

2.1 Workout应用结构

给出应用的结构:

  1. 启动应围时,它会启动活动MainAetivity。这个活动使用布局activity_main.xml。
  2. 这个活动将使用两个片段WorkoutListFragment和WorkoutDetailFragnment。
  3. WorkoufListFragment显示一个训练项目列表。它使用布局fragment_workout_list.xml。
  4. WorkoutDetailFragment显示一个测练项目的详细信息。它使用布fragment_workout_detail.xml。
  5. 这两个片段都从Workout.java得到测练项目的数据。Workout.java包含一个Workout数组。

2.2 步骤

下面是构建这个应用需要完成的步骤:

 1. 创建片段

我们要创建两个片段。WorkoutListFragment用来显示一个训练项目列表,WorkoutDetailFragment用来显示一个特定训练项目的详细信息。我们将在一个活动中同时显示这两个片段。另外还会增加一个普通的Java Workout类,这两个片段将使用这个类来得到它们的数据。

2.关联两个片段

单击workoutListFragment中的一个训练项目时,我们希望在WorkoutDetailFragment中显示这个训练项目的详细信息。

3.创建特定于设备的布局

最后,我们将修改这个应用,让它根据运行应用的设备提供不同的外观和行为。如果在一个有大屏幕的设备上运行,这两个片段会并排显示。如果运行应用的设备没有大屏幕,两个片段就会在单独的活动中显示。
 

2.3 Workout类

Workout类是用于提供数据的,该类里面有个static final 数组,供WorkoutDetailFragment来调用。

package com.hfad.workout;

public class Workout {

    private String name;
    private String description;

    public static final Workout[] workouts = {
            new Workout("The Limb Loosener",
                    "5 Handstand push-ups\n10 1-legged squats\n15 Pull-ups"),
            new Workout("Core Agony",
                    "100 Pull-ups\n100 Push-ups\n100 Sit-ups\n100 Squats"),
            new Workout("The Wimp Special",
                    "5 Pull-ups\n10 Push-ups\n15 Squats"),
            new Workout("Strength and Length",
                    "500 meter run\n21 x 1.5 pood kettleball swing\n21 x pull-ups")
    };

    //Each Workout has a name and description
    private Workout(String name, String description) {
        this.name = name;
        this.description = description;
    }

    public String getDescription() {
        return description;
    }

    public String getName() {
        return name;
    }

    public String toString() {
        return this.name;
    }
}

2.4 片段类Fragment

用Android Studio为工程增加一个新片段,其步骤是选择File->new->Fragment->Fragment(Blank).

创建WorkoutDetailFragment

首先来看片段布局代码

片段布局代码看上去很像活动布局代码

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_height="match_parent"
              android:layout_width="match_parent"
              android:orientation="vertical">

  <TextView
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:textAppearance="?android:attr/textAppearanceLarge"
      android:text=""
      android:id="@+id/textTitle" />

  <TextView
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text=""
      android:id="@+id/textDescription" />
</LinearLayout>

可以看到,片段布局代码看上去与活动布局代码很类似。这是一个非常简单的布局,包括两个文本视图:一个文本视图用大文本显示训练项目的名字,另一个文本视图用小一点的文本显示训练项目的描述。编写你自己的片段布局代码时,完全可以使用之前编写活动布局代码时所用的任何视图和布局。

既然已经为片段创建了布局,下面来看片段代码

Android Studio会自动生成一堆Java代码,将那些代码用下面的代码进行替换:

public class WorkoutDetailFragment extends Fragment {

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

对上面代码进行简要分析:

  • 这个类扩展了Android Fragment类。
  • onCreateView()方法,在Android需要这个片段的布局时会调用这个方法。
  • 在return中的第一个参数,会告诉Android这个片段使用哪个布局(在这里,它会使用fragment_workout_detail)

上面的代码创建了一个基本片段。可以看到,这个类扩展了android.app.Fragment类。所有片段都必须派生Fragment类。

这个片段还实现了onCreateview()方法。每次Android需要这个片段的布局时就会调用onCreateview()方法,要在这个方法中指定片段使用哪一个布局。这个方法是可选的,不过由于只要创建有布局的片段就需要有这个方法,所以几乎每次创建片段时都需要实现这个方法。

使用以下代码指定片段的布局:

inflater.inflate(R.layout.fragment_workout_detail,container,false);

这个片段方法就相当于活动的setContentview ()方法。与setContentview()类似,要用这个方法指定片段应当使用哪个布局,container参数由使用这个片段的活动传入。这是活动中的一个viewGroup,片段布局就要插入到这个视图组中。

2.5  为活动布局增加片段

要在活动的布局中引入片段,则需要对活动的布局文件进行修改:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <fragment
        class="com.hfad.workout.WorkoutDetailFragment"
        android:id="@+id/detail_frag"
        android:layout_height="match_parent"
        android:layout_width="match_parent"/>

</LinearLayout>

可以看到,这个布局包含一个元素<fragment>。要用<fragment>元素为活动布局增加片段。可以使用class属性指定是哪一个片段,将属性值设置为片段的完全限定名。

这里创建了一个片段,让活动在布局中显示这个片段。不过,到目前为止,这个片段还没有具体做任何事情。接下来我们要让活动指定显示哪一个训练项目,然后让片段用这个训练项目的详细信息填充视图。

2.6 向片段传递训练项目ID 

如果有一个使用片段的活动,这个活动通常需要以某种方式与片段交互。例如,如果有一个片段显示详细信息记录,就需要活动告诉片段要显示哪个记录的详细信息。

在这里,我们要让workoutDetailFragment显示一个特殊训练项目的详细信息。为此,需要为片段增加一个简单的设置方法来设置训练项目ID的值。然后活动可以使用这个方法设置训练项目ID。接下来,我们将使用这个训练项目ID更新片段的视图。

public class WorkoutDetailFragment extends Fragment {

    private long workoutId;//这是用户选择的训练项目的ID。
    //接下来,要利用这个ID用训练项目详细信息设置片段视图的值
    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_workout_detail,container,false);
    }
    
    //训练项目ID的设置方法。活动将使用这个方法设置训练项目ID的值
    public void setWorkoutId(long id){
        this.workoutId=id;
    }
    
}

活动需要调用片段的setWorkout()方法,传入一个特定的训练项目的ID。

2.7 让活动设置训练项目ID 

活动与片段交互之前,活动先要得到片段的一个引用

要得到片段的引用,首先使用活动的getFragmentManager()方法得到活动的片段管理器的一个引用。然后使用它的findFragmentById()方法得到这个片段的引用:

findFragmentById()有点类似findViewById(),不过这个方法用来得到片段的引用。

 片段管理器用来管理活动使用的所有片段。可以用片段管理器得到片段的引用以及完成片段事务。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //这会得到WorkoutDetailFragment的一个引用。
        WorkoutDetailFragment frag= (WorkoutDetailFragment)getFragmentManager().findFragmentById(R.id.detail_frag);
        frag.setWorkoutId(1);//这里让WorkoutDetailFragment显示一个训练项目的详细信息,看它是否能正常工作
    }
}

注意:上面代码中可能会报错,解决方法见链接(1条消息) Inconvertible types; cannot cast 'android.app.Fragment' to 'com.example....WorkoutDetailFragment_那个游侠的博客-CSDN博客icon-default.png?t=M276https://blog.csdn.net/shsh_0415/article/details/79377795

可以看到,我们在调用setcontentview ()之后才获取片段的引用。这一点很重要,因为在此之前片段还没有创建。
这里用代码frag.setworkout(1)告诉片段我们希望它显示哪个训练项目的详细信息。这是我们在片段中创建的定制方法。--->这个仅作为测试应用来使用的数据。

对现在来说,只是要在活动的onCreate()方法中设置一个训练项目的ID,希望能看到一些数据。后面还会进一步修改这个代码,允许用户选择他们真正想要查看的训练项目。

下面为用户显示片段时要让片段更新视图。不过在此之前,需要先了解片段的生命周期。
 

3.片段的生命周期

3.1 回顾活动的生命周期

片段包含在活动中,并由活动控制,所以片段生命周期与活动的生命周期紧密相关。下面先来回顾活动要经过的各个不同状态,下一页我们会说明这些状态与片段有什么关系。

创建活动运行活动的onCreate()方法时会创建活动活动已经初始化,活动不可见
启动活动运行onStart()方法时会启动活动活动可见,没有得到焦点
恢复活动运行onResume()方法时会恢复活动的运行活动可见,得到焦点
暂停活动运行onPause()方法时活动暂停活动可见,没有得到焦点
停止活动运行onStop()方法活动停止活动不可见,还存在
撤销活动运行onDestory()方法活动被撤销活动不存在

 

3.2 片段生命周期

片段的生命周期与活动的生命周期很类似,不过片段生命周期中还有另外几个步骤。这是因为,它需要与包含这个片段的活动的生命周期交互。

下面给出片段的生命周期方法,并说明这些方法与不同的活动状态如何交互。

片段生命周期
活动状态片段回调
创建活动onAttach()onAttach(Activity)片段与活动关联时会调用这个方法
onCreate()onCreate(Bundle)这与活动的onCreate()很类似,用于完成片段的初始设置
onCreateView()

onCreateView(LayoutInflater,

ViewGroup,Bundle)

在这个阶段,片段使用一个布局充气泵(layoutinflater)创建视图
onActivityCreated()onActivityCreated(Bundle)活动的onCreate()方法完成时会调用这个方法
启动活动onStart()onStart()片段将要可见会调用onStart()方法
恢复活动onResume()onResume()片段可见而且正在运行时调用onResume()
暂停活动onPause()onPause()片段不再与用户交互时会调用这个方法
停止活动onStop()onStop()片段不再对用户可见时,会调用这个方法
撤销活动onDestoryView()onDestoryView()为片段提供一个机会来清理与其视图关联的所有资源
onDestory()onDestory()在这个方法中,片段可以清理它创建的所有其他资源
onDetach()onDetach()最后片段与活动断开关联

3.3 片段继承了生命周期方法

片段扩展了Andorid Fragment类。这个类允许片段访问片段生命周期方法。

 尽管片段与活动有很多共同之处,不过Fragment类并不扩展Activity类。这说明,活动可用的一些方法在片段中无法使用

Fragment类没有实现context类。与活动不同,片段不是一个上下文类型,不能直接访问有关应用环境的全局信息。实际上,片段必须使用其他对象的上下文来访问这个信息,如它的父活动

3.4 在片段的onStart()方法中设置视图的值

要让workoutDetailFragment用训练项目的详细信息更新它的视图。我们要在启动活动时完成片段视图的更新,所以要使用片段的onstart()方法。代码如下:

public class WorkoutDetailFragment extends Fragment {

    private long workoutId;//这是用户选择的训练项目的ID。
    //接下来,要利用这个ID用训练项目详细信息设置片段视图的值
    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_workout_detail,container,false);
    }

    @Override
    public void onStart() {
        super.onStart();
        View view = getView();//getView()方法得到判断的根视图。然后使用这个根视图得到两个文本视图(训练项目名和描述)的引用
        if (view!=null){
            TextView title = (TextView) view.findViewById(R.id.textTitle);
            Workout workout = Workout.workouts[(int) workoutId];
            title.setText(workout.getName());
            TextView description = (TextView) view.findViewById(R.id.textDescription);
            description.setText(workout.getDescription());
        }
    }

    //训练项目ID的设置方法。活动将使用这个方法设置训练项目ID的值
    public void setWorkoutId(long id){
        this.workoutId=id;
    }

    
}

实现任何片段生命周期方法时都要首先实现相应的超类方法。

上一页我们说过,片段与活动不同,因此并不包含活动的所有方法。例如,片段没有findviewById()方法。要得到片段中的视图的引用,首先必须使用getview ()方法得到片段根视图的引用,然后使用这个根视图查找它的子视图

3.5 测试应用

目前,已经完成了测试应用的代码编写。应该在运行活动之后,应用界面会加载片段,然后片段显示第2个训练项目的详细信息。

 运行应用时会发生什么

 相关问题

问:为什么活动不能调用findViewById()方法来得到片段?
答:因为findviewById()总是返回一个View对象,尽管有些奇怪,不过片段并不是视图

问:为什么片段不能有一个findViewById()方法?

答:因为片段不是视图,也不是活动。实际上,需要使用片段的getview ()方法得到片段根视图的一个引用,然后调用这个视图的findviewById()方法来得到它的子视图

4. 包含列表的片段实现

4.1 需要创建一个包含列表的片段

WorkDetailFragment已经能正常工作了,下面需要创建第二个片段,包含由不同训练项目组成的一个列表。然后就能使用这些片段为手机和平板电脑创建不同的用户界面了。 

你已经了解如何为活动增加列表视图。我们可以创建一个片段,其中包含一个列表视图,然后利用训练项目名更新这个列表视图

之前想要使用只包含一个列表的活动时,可以使用ListActivity。片段也有类似的,名为ListFragment的类

4.2 ListFragment是只包含一个列表的片段

ListFragment是一种专门处理ListView的Fragment。它有一个默认布局,其中包含一个ListView.

列表片段是一种专门处理列表的片段。与列表活动类似,它会自动绑定到一个列表视图,所以不需要另外创建列表视图。下面给出一个列表片段:

ListFragment是Frament的一个子类。 

与列表活动类似,使用列表片段显示数据类别有几个主要的优点:

1.不需要创建你自己的布局。
列表片段会自动定义自己的布局,所以你不用创建或维护XML布局。列表片段生成的布局包括一个列表视图。可以活动代码中使用列表片段的getListview()方法访问这个列表视图。需要得到列表视图才能指定列表视图中应当显示什么数据。

2.不需要突现你自己的事件监听器。
ListFragment类注册为列表视图的一个监听器,会监听什么时候单击列表视图中的列表项。可以使用列表片段的onListItemclick()方法让片段响应单击事件。

import android.app.ListFragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
i

//这个活动要扩展ListFragment 而不是Fragment
public class WorkoutListFragment extends ListFragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        //通过调用超类的onCreateView()方法,可以得到ListFragment的默认布局
        return super.onCreateView(inflater, container, savedInstanceState);
    }
}

上面的代码会创建一个名为workoutListFragment的基本列表片段。由于这是一个列表片段,所以它要扩展ListFragment类而不是Fragment。

onCreateview()方法是可选的。创建片段的视图时会调用onCreateview()方法。之所以在代码中包含这个方法,这是因为我们希望一旦创建片段的列表视图就用数据填充这个列表视图。如果并不需要代码在这个时候做任何事情,就不用包含这个方法。

4.3 使用ArrayAdapter设置ListView中的值

 第6章提到过,可以使用适配器将数据与列表视图关联起来。片段中的列表视图也是一样;ListView是Adapterview类的一个子类,这个类允许视图使用适配器

我们希望为workoutListFragment中的列表视图提供一个训练项目名列表,所以下面使用一个数组适配器将这个数组绑定到列表视图

Fragment不是一个Context类型

要创建一个处理列表视图的数组适配器,可以使用以下代码:

 这里DataType是数据类型,array是数组,context是当前上下文。

在活动中使用这个代码时,可以用它得到当前上下文。之所以可以这么做,是因为活动是一个上下文类型,Activity类是context类的一个子类

在前面已经看到,Fragment类不是context类的子类,所以无法使用这个代码。实际上,我们需要用另外某种方法来得到当前上下文

如果在片段的onCreateview()方法中使用适配器,就像现在一样,可以使用LayoutInflator对象的getContext()方法来得到上下文:

 创建适配器后,要用片段的setListAdapter()方法绑定到ListView:

setListAdapter(listAdapter);

下面使用一个数组适配器在片段的列表视图中填充一个训练项目列表。

//这个活动要扩展ListFragment 而不是Fragment
public class WorkoutListFragment extends ListFragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        String[] names=new String[Workout.workouts.length];
        for (int i=0;i<names.length;i++){
            names[i]=Workout.workouts[i].getName();
        }
        ArrayAdapter<String> adapter=new ArrayAdapter<>(
                inflater.getContext(),//从布局充气泵得到上下文
                android.R.layout.simple_list_item_1,
                names
        );
        setListAdapter(adapter);//将数组适配器绑定到列表视图
        return super.onCreateView(inflater, container, savedInstanceState);
    }
}

4.4 在MainActivity布局中显示WorkoutListFragment

我们要在MainActivity布局中增加新的work-outListFragment,让它在workoutDetail-Fragment左边显示。以这种方式并排显示片段是为平板电脑设计应用的一种常见做法。

为此,我们将使用水平方向的线性布局。这里使用布局权重来控制每个片段所占的水平空间

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <fragment
        class="com.hfad.workout.WorkoutListFragment"
        android:id="@+id/list_frag"
        android:layout_height="match_parent"
        android:layout_width="0dp"
        android:layout_weight="2"
        />

    <fragment
        class="com.hfad.workout.WorkoutDetailFragment"
        android:id="@+id/detail_frag"
        android:layout_height="match_parent"
        android:layout_width="0dp"
        android:layout_weight="3"
        />

</LinearLayout>

目前的视图效果:

4.5 关联列表和详细信息

单击列表中的一项时要让详细信息相应地改变,这有很多方法。我们要完成以下工作:

  1. 在workoutListFragment中增加代码,等待单击某个训练项目
  2. 这个代码运行时,要调用MainActivity.java中的代码
  3. 它将改变详细信息片段中的详细信息。


我们不希望在WorkoutListFragment中写代码与WorkoutDetailFragment直接交互。你知道为什么吗?
原因为了保证重用。我们希望片段对它所在的环境了解越少越好一个片段对使用它的活动需要知道的越多,它的重用性就越差

不希望片段了解它的活动,这样一来就意味着无法在另一个活动中使用了?

我们要用一个接口将片段与活动解耦合

4.6 要用接口解耦合片段

这里有两个对象需要交谈,也就是片段和活动,我们希望它们交谈时不需要了解对方太多。Java中的做法是利用接口

定义接口时,实际上就是给出了一个对象与另一个对象有效交互的最低需求。这说明,我们可以让片段与任何类型的活动交谈,只要这个活动实现了所指定的接口

我们将创建一个名为WorkoutListListener的接口,如下所示:

public interface WorkoutListener {

    void itemClicked(long id);
}

只要活动实现了这个接口,我们就能告诉它已点击列表片段中的一个列表项。

运行时会有以下动作:

  1. workoutListListener告诉片段它想要监听。
  2. 用户点击列表中的一个训练项目。
  3. 调用列表片段中的onListItemclicked()方法。
  4. 这个方法调用workoutListListener的itemClicked ()方法,并提供所单击训练项目的ID。

不过活动什么时候指出它要监听?

活动什么时候告诉片段它准备接收更新信息来了解单击了哪个列表项?再看看片段生命周期,你会发现片段关联到活动时会调用片段的onAttach()方法,并提供活动值:

 可以利用这个方法为片段注册活动。

首先,为列表片段增加接口

//这个活动要扩展ListFragment 而不是Fragment
public class WorkoutListFragment extends ListFragment {

    static interface WorkoutListListener{
        void itemClicked(long id);
    }
    //为片段增加监听器
    private WorkoutListListener listener;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        String[] names=new String[Workout.workouts.length];
        for (int i=0;i<names.length;i++){
            names[i]=Workout.workouts[i].getName();
        }
        ArrayAdapter<String> adapter=new ArrayAdapter<>(
                inflater.getContext(),//从布局充气泵得到上下文
                android.R.layout.simple_list_item_1,
                names
        );
        setListAdapter(adapter);//将数组适配器绑定到列表视图
        return super.onCreateView(inflater, container, savedInstanceState);
    }

    //片段与活动关联时,会调用这个方法
    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        this.listener=(WorkoutListListener)activity;
    }

    //这是ListFragment自带的监视器,只要有在片段上的点击,就会调用这个方法
    //然后,只要判断listener不为null,那么就说明片段已经和活动关联了
    //那么这次的点击,就会调用接口的方法
    @Override
    public void onListItemClick(ListView l, View v, int position, long id) {
        if (listener!=null){
            listener.itemClicked(id);//单击ListView中的一项时告诉监听器
        }
    }
}

让活动实现接口:

现在要让MainActivity.java实现刚创建的WorkoutListListener接口。

public class MainActivity extends AppCompatActivity
            implements WorkoutListFragment.WorkoutListListener {

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

    //这个方法在监听器中定义
    @Override
    public void itemClicked(long id) {
        
    }
}

单击片段中的一个列表项时,会调用活动的itemclicked()方法。可以在这个方法中增加代码,显示刚选择的训练项目的详细信息。

不过如何更新训练项的详细信息呢?

WorkoutDetailFragment片段启动时会更新它的视图。不过,在屏幕上显示片段后,如何让片段更新这些详细信息?你可能认为可以利用片段的生命周期来完成更新。

实际上,每次要改变文本时,我们都会把这个片段替换为一个全新的详细信息片段

4.7 片段处理后退按钮

4.7.1 需求:片段处理后退按钮

假设用户单击了一个训练项目,然后单击第二个训练项目。单击后退按钮时,他们可能希望回到之前选择的第一个训练项目。

 目前为止,在我们构建的所有应用中,后退按钮都会让用户返回到前一个活动。既然我们在使用片段,就要了解单击后退按钮时发生了什么

4.7.2 了解后退堆栈

后退堆栈是一个列表,记录了你在设备上访问过的所有“地方”,每个“地方”都是后退堆栈上的一个事务

很多事务都有可能让你从一个活动转移到另一个活动:

所以进入一个新活动时完成这个活动的事务就会记录到后退堆栈中。如果按下后退按钮,就会逆向执行这个事务,返回到之前的活动

不过后退堆栈事务不一定是活动,有可能只是改变了屏幕上的片段:

 这说明可以用后退按钮撤销片段的改变,就像撤销活动的改变一样

4.7.3 不更新,只替换--帧布局

如果不需要响应用界面的变化,可以使用<fragment>为活动增一个片段,否则要使用<FrameLayout>帧布局.

不用更新WorkoutDetailFragment中的视图,我们要把这个片段替换为WorkoutDetailFragment的一个新实例,这个片段会显示选择的下一个训练项目的详细信息。

这样一来,可以把片段替换保存在一个后退堆栈事务中,用户不能单击后退按钮来撤销所做的修改。不过,怎么把一个片段替换为另一个片段呢?

首先需要对activity_main.xml布局文件做一个修改。不要直接插入Work-outDetailFragment,这里可以使用一个帧布局(frame layout)

帧布局是一种视图组,用来锁定屏幕上的一个区域。要用<FrameLayout>元素定义帧布局。可以用它显示单个的项,在这里就是要显示一个片段。我们把片段放在一个帧布局中,这样就能通过编程控制它的内容

只要单击了workoutListFragment列表视图中的一个列表项,就要把帧布局的内容替换为workoutDetailFragment的一个新实例,显示相应的训练项目的详细信息:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <fragment
        class="com.hfad.workout.WorkoutListFragment"
        android:id="@+id/list_frag"
        android:layout_height="match_parent"
        android:layout_width="0dp"
        android:layout_weight="2"
        />

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

</LinearLayout>

4.7.4 使用片段事务的步骤:

运行时要在片段事务中替换片段。片段事务是与这个片段有关的一组同时发生的改变

要创建一个片段事务,首先从片段管理器得到FragmentTransaction:

然后指定希望在这个事务中包含的所有动作。在这里,我们希望替换帧布局中的片段,这可以使用片段的replace()方法来做到:

在这里,R.id.fragment _ container是包含这个片段的容器的ID。还可以使用add()方法为容器增加片段,或者使用remove()方法删除一个片段:

可以使用setTransition()方法指定希望这个事务完成哪种过渡动画。

 这里transition是动画类型。相应的选项包括TRANSITFRAGMENT_CLOSE(从堆栈删除片段)、TRANSIT__FRAGMENTOPEN(增加片段)、TRANSIT _ FRAGMENT _FADE(片段淡入淡出)和TRANSIT_ NONE(无动画)。

一旦指定了事务中包含的所有动作,接下来可以使用addToBackstack()方法在事务后退堆栈中增加这个事务。这就允许用户按下后退按钮时退回到片段的前一个状态。addToBackstack()方法有一个参数,这是一个String名,可以用来作为事务的标签:

 最后要向活动提交修改,需要调用commit()方法:

 commit()方法会应用这些修改。

4.7.5 更新MainActivity代码

我们希望得到workoutDetailFragment的一个新实例显示正确的训练项目,首先使用一个片段事务在活动中显示片段,然后将这个事务增加到后退按钮的后退堆栈。下面给出完整的代码:

import androidx.appcompat.app.AppCompatActivity;

import android.app.FragmentTransaction;
import android.os.Bundle;


public class MainActivity extends AppCompatActivity
            implements WorkoutListFragment.WorkoutListListener {

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

    //这个方法在监听器中定义
    @Override
    public void itemClicked(long id) {
        WorkoutDetailFragment details=new WorkoutDetailFragment();
        //开启片段事务
        FragmentTransaction ft=getFragmentManager().beginTransaction();
        details.setWorkoutId(id);
        //替换片段,把它增加到后退堆栈
        ft.replace(R.id.fragment_container,details);
        ft.addToBackStack(null);
        //得到新片段和老片段来完成淡入淡出
        ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
        ft.commit();//提交事务
    }
}

4.7.6 应用测试与改进旋转保留局部变量

在不旋转的时候,应用还是能正常运行的。

旋转应用时会出现一个问题:不论你之前选择了哪一个训练项目,旋转设备后,应用都会显示第一个训练项目的详细信息。

前面介绍过活动的生命周期,你已经了解:旋转设备时Android会撤销然后重新创建活动。如果是这样,活动使用的局部变量可能会丢失。如果活动使用了一个片段,这个片段就会随着活动同时撤销然后重新创建。这说明,片段使用的所有局部变量也会丢失它们的状态

workoutDetailFragment中使用了一个名为workoutId的局部变量,用来存储用户在workoutListFragment列表视图中单击的训练项目的ID。用户旋转设备时,workoutId会丢失它的当前值,然后默认地设置为0。片段就会显示ID为0的训练项目的详细信息,也就是这个列表中的第一个训练项目。

与活动中一样,可以采用类似的方式处理片段的这个问题。

首先覆盖片段的onSaveInstancestate()方法,哪些局部变量需要保存状态就把这些局部变量保存到这个方法的Bundle参数中,片段撤销前会调用onSaveInstanceState()方法:

然后在片段的onCreateView()方法中从Bundle中获取这个值:

 

下面给出完整的WorkoutDetailFragment代码:

package com.hfad.workout;

import android.content.Context;
import android.net.Uri;
import android.os.Bundle;

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

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


public class WorkoutDetailFragment extends Fragment {

    private long workoutId;//这是用户选择的训练项目的ID。
    //接下来,要利用这个ID用训练项目详细信息设置片段视图的值
    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        if (savedInstanceState!=null){
            workoutId=savedInstanceState.getLong("workoutId");//设置workoutId的值
        }
        return inflater.inflate(R.layout.fragment_workout_detail,container,false);
    }

    @Override
    public void onStart() {
        super.onStart();
        View view = getView();//getView()方法得到判断的根视图。然后使用这个根视图得到两个文本视图(训练项目名和描述)的引用
        if (view!=null){
            TextView title = (TextView) view.findViewById(R.id.textTitle);
            Workout workout = Workout.workouts[(int) workoutId];
            title.setText(workout.getName());
            TextView description = (TextView) view.findViewById(R.id.textDescription);
            description.setText(workout.getDescription());
        }
    }

    //在片段撤销之前将workoutId的值保存到outState Bundle中,将在onCreateView()中获取这个值
    @Override
    public void onSaveInstanceState(Bundle outState) {
        outState.putLong("workoutId",workoutId);
    }

    //训练项目ID的设置方法。活动将使用这个方法设置训练项目ID的值
    public void setWorkoutId(long id){
        this.workoutId=id;
    }


}

5.不同设备适配应用

5.1 不同的屏幕大小

需要对这个workout应用做一个修改。我们希望根据在手机还是在平板电脑上运行应用让它有不同的表现。

在平板电脑上

在手机上

5.2 手机和平板电脑应用结构

平板电脑上:应用的平板电脑版本与目前应用的做法完全相同。

手机上:不再在MainActivity中同时使用两个片段,MainActivity将使用WorkoutListFragment,而DetailActivity使用WorkoutDetailFragment。用户单击一个训练项目时,MainActivity会启动DetailActivity。

要根据在手机还是在平板电脑上运行这个应用让它有不同的外观和表现。为此,下面来看如何让应用根据运行设备类型选择不同的布局

5.3 不同的设备使用不同的资源

将屏幕特定资源放在屏幕特定文件夹中

通过把不同大小的图像放在不同的drawable文件夹中,可以让不同设备使用适应其屏幕大小的图像资源。例如,如果有些图像要由有高密度屏幕的设备使用,可以把这些图像放在drawable-hdpi文件夹中。

对其他资源也可以采用同样的做法,如布局、菜单和值。如果想为不同的屏幕大小创建相同资源的多个版本,只需要创建多个有适当名字的资源文件夹。设备会在运行时从与屏幕大小最为匹配的文件夹加载资源

不同的文件夹选项

可以把各种类型的资源(图像、布局、菜单和值)放在不同的文件夹中,指定它们要用于哪些类型的设备。屏幕特定的文件夹名可以包含屏幕大小、密度、方向和宽高比,各部分之间用连字符连接。例如,如果想创建一个只能由非常大的平板电脑使用的布局(而且采用水平方向),可以创建一个名为layout-xlarge-land的文件夹,把布局文件放在这个文件夹中。下面是文件夹名可用的各种不同选项:

 Android会在运行时查找最佳匹配来确定要使用哪些资源。如果不存在完全匹配,它会使用为较小屏幕(比当前屏幕稍小)设计的资源。如果只有针对更大屏幕的资源,Android不会使用这些资源,应用将无法正常运行。

如果你只希望应用在某些特定屏幕大小的设备上工作,可以在AndroidManifest.xml中使用<supports-screens>属性来指定。举例来说,如果不希望应用在小屏幕的设备上使用,可以使用以下代码来指定:

<supports-screens android: smallScreens="false" />

通过使用不同的文件夹名,可以分别创建特定于手机和平板电脑的布局。下面先来创建这个应用的平板电脑版本。

5.4 创建平板电脑使用的版本

平板电脑使用layout-large文件夹中的布局

只需要创建一个layout-large文件夹,然后将原来的XML布局文件引入进去就好了。

5.5 MainActivity手机布局

将layout布局中的activity_main.xml文件替换为以下的代码:

<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
          class="com.hfad.workout.WorkoutListFragment"
          android:id="@+id/list_frag"
          android:layout_width="match_parent"
          android:layout_height="match_parent"/>

在手机上运行时,由于MainActivity只需要显示workoutListFragment,所以不用另外创建一个包含<fragment>元素的布局。只有需要显示多个片段时才有这个必要。

有一点需要说明,layout文件夹中的activity _ma in.xml版本不包含fragment _ container帧布局,而layout-large文件夹中的activity_main.xml包含这样一个帧布局。这是因为,只有layout-large文件夹中的activity_main.xml需要显示workoutDetailFragment。后面我们会利用这一点来确定应用在用户设备上使用哪一个布局。

接下来创建另外一个使用workoutDetailFragment的活动。

5.6 创建DetailActivity

手机应用就还需要再创建第二个活动,名为DetailActivity.

这个布局要包含片段WorkoutDetailFragment.下面是代码:

<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
          class="com.hfad.workout.WorkoutDetailFragment"
          android:id="@+id/detail_frag"
          android:layout_width="match_parent"
          android:layout_height="match_parent"/>

除了更新activity_detail布局,还需要更新DetailActivity本身。如果在一个手机上运行这个应用,Mai nActivity需要用一个意图启动DetailActivity。

这个意图要包含用户选择的训练项目ID作为额外信息。然后DetailActivity使用它的setworkout()方法将这个额外信息传递给workoutDetailFragment。

完整的DetailActivity代码:

package com.hfad.workout;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;

public class DetailActivity extends AppCompatActivity {

    public static final String EXTRA_WORKOUT_ID = "id";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_detail);
        WorkoutDetailFragment workoutDetailFragment = (WorkoutDetailFragment)
                getFragmentManager().findFragmentById(R.id.detail_frag);
        int workoutId = (int) getIntent().getExtras().get(EXTRA_WORKOUT_ID);
        workoutDetailFragment.setWorkoutId(workoutId);
    }
}

5.7 利用布局差异区分设备使用哪一个布局

用户单击一个训练项目时,我们希望MainActivity根据设备是否使用layout-large文件夹中的activity_main.xml来完成不同的动作。

在MainActivity中,通过检查设备在使用哪一个布局来处理这两种不同的情况。可以查找一个ID为fragment _ container的视图来区分

如果存在fragment _ container,设备肯定在使用layout-large文件夹中的activity_main.xml,所以我们知道,用户单击一个训练项目时要显示workoutDetailFragment的一个新实例。如果fragment _ container不存在,设备肯定在使用layout文件夹中的activity_main.xml,所以要启动DetailActivity。

package com.hfad.workout;

import androidx.appcompat.app.AppCompatActivity;

import android.app.FragmentTransaction;

import android.content.Intent;
import android.os.Bundle;
import android.view.View;


public class MainActivity extends AppCompatActivity
            implements WorkoutListFragment.WorkoutListListener {

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

    //这个方法在监听器中定义
    @Override
    public void itemClicked(long id) {
        View fragmentContainer = findViewById(R.id.fragment_container);
        if (fragmentContainer!=null){//如果应用在一个大屏幕设备上运行,就会有这个帧布局
            WorkoutDetailFragment details=new WorkoutDetailFragment();
            //开启片段事务
            FragmentTransaction ft=getFragmentManager().beginTransaction();
            details.setWorkoutId(id);
            //替换片段,把它增加到后退堆栈
            ft.replace(R.id.fragment_container,details);
            ft.addToBackStack(null);
            //得到新片段和老片段来完成淡入淡出
            ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
            ft.commit();//提交事务
        }else{//如果不存在帧布局,那就是应用在较小屏幕设备上运行的,那需要启动DetailActivity
            Intent intent = new Intent(MainActivity.this, DetailActivity.class);
            intent.putExtra(DetailActivity.EXTRA_WORKOUT_ID,(int) id);
            startActivity(intent);
        }

    }
}

5.8 应用测试

平板界面: 


 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值