IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> 8 嵌套片段 -> 正文阅读

[移动开发]8 嵌套片段

1.引入

1.0 引入

你已经知道利用活动中的片段可以重用代码,让应用更灵活。

这一章中,我们会介绍如何把一个片段嵌套在另一个片段中。你会看到如何使用子片段管理器驯服不守规矩的片段事务。在这个过程中,你将了解为什么活动与片段之间的差别这么重要。

1.1 需求引入--嵌套片段

并不只是活动可以包含片段,片段还可以嵌套在其他片段中。为了了解这种嵌套片段的实际工作,下面为我们的训练项目详细信息片段增加一个秒表片段

增加一个新的秒表片段

我们要增加一个新的秒表片段,名为StopwatchFragment.java,它使用布局fragment_stopwatch.xml。这个片段建立在第4章创建的秒表活动基础上。

活动和片段在很多方面都很类似,不过我们也知道片段是完全不同的一种对象,片段不是活动的子类有没有办法重写活动代码,让它看上去像是一个片段呢

1.2 片段和活动有类似的生命周期

要了解如何将活动重写为片段,我们要先考虑这二者之间的相似和不同之处。如果查看片段和活动的声明周期,会发现他们非常相似:

不过方法稍有不同

?片段生命周期方法与活动生命周期方法几乎一样,不过有一个重要的区别:活动生命周期方法是保护的,而片段生命周期方法是公共的。我们已经知道,片段从布局资源文件创建布局的方式有所不同。

另外,在片段中我们不能直接调用类似findviewById()的方法。实际上,需要先找到一个View对象的引用,然后调用view.findviewById()。

1.3 从活动到片段的转换

要将之前写的StopwatchActivity活动代码,转换成一个StopwatchFragment片段,需要注意以下几点:

  1. 不使用布局文件activity_stopwatch.xml,而要使用布局文件fragment_Stopwatch.xml
  2. 确保方法的访问限制是真确的
  3. 如何指定布局?
  4. runTimer()方法不能调用findViewById(),所以可能要为runTimer()传入一个视图对象。

完整的代码:

package com.hfad.workout;


import android.os.Bundle;

import androidx.fragment.app.Fragment;

import android.os.Handler;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;


public class StopwatchFragment extends Fragment {
    //经过的秒数
    private int seconds = 0;
    //running指示秒表现在是否在运行
    private boolean running;
    //wasRunning指示秒表暂停前是否在运行
    private boolean wasRunning;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //屏幕翻转,片段撤销,需要加载撤销前的局部变量
        //从savedInstanceState Bundle 恢复变量的状态
        if (savedInstanceState != null) {
            seconds = savedInstanceState.getInt("seconds");
            running = savedInstanceState.getBoolean("running");
            wasRunning = savedInstanceState.getBoolean("wasRunning");
            if (wasRunning) {
                running = true;
            }
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        //在onCreateView()中设置片段的布局
        View layout = inflater.inflate(R.layout.fragment_stopwatch, container, false);
        //启动runTimer()方法并传入布局
        runTimer(layout);
        //获取按钮视图,并设置单击监视器
        Button startButton = (Button) layout.findViewById(R.id.start_button);
        startButton.setOnClickListener((View.OnClickListener) this);
        Button stopButton = (Button) layout.findViewById(R.id.stop_button);
        stopButton.setOnClickListener((View.OnClickListener) this);
        Button resetButton = (Button) layout.findViewById(R.id.reset_button);
        resetButton.setOnClickListener((View.OnClickListener) this);
        return layout;
    }

    @Override
    public void onPause() {
        super.onPause();
        //如果片段暂停,记录秒表原来是否在运行
        wasRunning = running;
        //然后将它停止
        running = false;
    }

    @Override
    public void onResume() {
        super.onResume();
        if (wasRunning) {
            //如果暂停前秒表在运行,再设置为运行
            running = true;
        }
    }

    //活动撤销前将变量放在Bundle中,用户旋转设备时会使用这些变量
    @Override
    public void onSaveInstanceState(Bundle savedInstanceState) {
        savedInstanceState.putInt("seconds", seconds);
        savedInstanceState.putBoolean("running", running);
        savedInstanceState.putBoolean("wasRunning", wasRunning);
    }


    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.start_button:
                onClickStart(v);
                break;
            case R.id.stop_button:
                onClickStop(v);
                break;
            case R.id.reset_button:
                onClickReset(v);
                break;
        }
    }

    public void onClickStart(View view) {
        running = true;
    }

    public void onClickStop(View view) {
        running = false;
    }

    public void onClickReset(View view) {
        running = false;
        seconds = 0;
    }

    private void runTimer(View view) {
        //把代码放在Handler中意味着它可以在后台现场中运行
        final TextView timeView = (TextView) view.findViewById(R.id.time_view);
        final Handler handler = new Handler();
        handler.post(new Runnable() {
            @Override
            public void run() {
                int hours = seconds / 3600;
                int minutes = (seconds % 3600) / 60;
                int secs = seconds % 60;
                String time = String.format("%d:%02d:%02d",
                        hours, minutes, secs);
                timeView.setText(time);
                if (running) {
                    seconds++;
                }
                handler.postDelayed(this, 1000);
            }
        });
    }
}

然后是布局的代码:

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

    <TextView
        android:id="@+id/time_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="0dp"
        android:text=""
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:textSize="92sp" />

    <Button
        android:id="@+id/start_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/time_view"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="20dp"
        android:text="@string/start" />

    <Button
        android:id="@+id/stop_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/start_button"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="10dp"
        android:text="@string/stop" />

    <Button
        android:id="@+id/reset_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/stop_button"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="10dp"
        android:text="@string/reset" />
</RelativeLayout>

1.4 为WorkoutDetailFragment增加秒表片段

要在WorkoutDetailFragment中增加StopwatchFragment.平板电脑上MainActivity的用户界面如下所示:

如果在一个片段中嵌套另一个片段,需要通过编程来增加嵌套片段

通过编程增加这个片段

你已经看到了,增加片段有两种方法:可以使用布局文件,也可以编写Java代码。

如果为另一个片段的布局增加片段,结果可能很怪异,所以我们使用Java代码将stopwatchFragment增加到workoutDetailFragment。

这说明,就像为活动增加workoutDetailFragment一样,我们会用同样的方式为workoutDetailFragment增加片段stopwatchFragment。这里只有一点不同,后面还会谈到。

1.5 增加帧布局-在片段所在的位置增加FrameLayout

通过Java代码来增加片段,需要在布局中片段出现的位置上增加一个帧布局

我们想把StopwatchFragment放在WorkoutDetailFragment中训练项目名和描述的西面,因此在名和描述文本视图下面增加一个帧布局,用来包含StopwatchFragment:

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

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

为布局增加了帧布局,下面要在Java代码中为它增加片段

创建WorkoutDetailFragment的视图时,希望在帧布局中增加StopwatchFragment.

要使用一个片段事务替换帧布局中显示的片段。下面来回顾第7章中使用的代码:

我们使用以上代码替换活动中显示的片段,不过这一次有个很大的区别。

这里不是替换活动中显示的片段,而是要替换片段中显示的片段。这说明对于如何创建片段事务要做个小小的修改。

在活动中显示一个片段时,我们使用了活动的片段管理器来创建片段事务,如下所示:

?getFragmentManager()方法得到与这个片段父活动关联的片段管理器。这说明,片段事务会关联到活动

要想在一个片段中显示另一个片段,需要使用稍有不同的一个片段管理器,这个片段管理器应当与父片段关联。这说明,所有片段事务会关联到父片段,而不是活动

要得到与父片段关联的片段管理器,可以使用getChildFragmentManager()方法。这说明启动事务的代码如下所示:

FragmentTransaction ft=getChildFragmentManager().beginTransaction();

这样就会得到片段的片段管理器的引用。

1.6 片段事务的嵌套-getChildFragmentManager()

1.6.1 使用getFragmentManager()的后果

下面先来看如果workoutDetailFragment使用getFragmentManager()创建片段事务来显示StopwatchFragment会发生什么

用户单击一个训练项目时,我们希望应用能显示这个训练项目的详细信息以及秒表。MainActivity会创建一个事务显示workoutDetailFragment。如果使用getFragmentManager()同时显示StopwatchFragment,后退堆栈上就会有两个事务。

会将两个片段分别以事务的形式,加入到后退堆栈

1.6.2 当心后退按钮--会产生问题

使用两个事务显示训练项目存在一个问题:用户按下后退按钮时,会有奇怪的事情发生。

用户单击一个训练项目时,然后单击后退按钮,他们希望屏幕回到之前看到的那个屏幕。不过,后退按钮只是撤销了后退堆栈上的最后一个事务。这意味着,如果我们创建两个事务来显示训练项目的详细信息和秒表,用户单击后退按钮时,只会删除秒表。用户必须再次点击后退按钮才能删除训练项目的详细信息部分。

使用getFragmentMangager()创建片段事务显示StopwatchFragment

1.6.3??嵌套片段需要嵌套事务--getChildFragmentManager()

由于对嵌套片段使用多个事务会带来这种问题,所以需要创建子片段管理器

子片段管理器创建的事务会放在主事务中,所以使用getChildFragmentManager().beginTransaction()创建的事务为workoutDetail-Fragment增加StopwatchFragment时,会如下嵌套事务:

后退堆栈有一个事务,这个事务中包含了两个事务。用户按下后退按钮时,会撤销显示详细信息片段的事务,这说明显示秒表片段的事务也会同时被撤销。现在用户按下后退按钮时,应用的表现就正常了:

?

?1.6.4 替换片段

在父片段的onCreateView()方法中显示片段

我们希望创建workoutDetailFragment的视图时为帧布局增加StopwatchFragment。创建workoutDetail-Fragment的视图时,会调用它的onCreateview()方法,所以这里要为onCreateview()方法增加一个片段事务来显示StopwatchFragment。

代码如下:

@Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        if (savedInstanceState!=null){
            workoutId=savedInstanceState.getLong("workoutId");//设置workoutId的值
        }
        //启动事务
        FragmentTransaction ft = getChildFragmentManager().beginTransaction();
        StopwatchFragment stopwatchFragment = new StopwatchFragment();
        //替换帧布局中的片段
        ft.replace(R.id.stopwatch_container,stopwatchFragment);
        //向 后退栈 增加事务
        ft.addToBackStack(null);
        //设置过渡动画方式
        ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
        //提交事务
        ft.commit();
        return inflater.inflate(R.layout.fragment_workout_detail,container,false);
    }

这个代码看上去与在活动中显示片段所有的代码几乎完全相同。

主要区别在于,我们要在一个片段中显示另一个片段,因此要使用getChildFragmentManager().

:如果我在一个片段中增加了另一个片段,子片段管理器可以很好地处理这种情况。不过,如果我把一个片段放在另一个片段中,再把它们放在下一个片段中,然后再放在下一个片段中……会怎么样呢?

:事务会相互嵌套,最后在活动级只留下一个事务。因此,只需要单击一次后退按钮,这组嵌套的子事务都会撤销。

完整的WorkoutDetailFragment代码:

package com.hfad.workout;

import android.app.FragmentTransaction;
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的值
        }
        //使用一个片段事务向帧布局增加秒表片段
        //启动事务
        FragmentTransaction ft = getChildFragmentManager().beginTransaction();
        StopwatchFragment stopwatchFragment = new StopwatchFragment();
        //替换帧布局中的片段
        ft.replace(R.id.stopwatch_container,stopwatchFragment);
        //向 后退栈 增加事务
        ft.addToBackStack(null);
        //设置过渡动画方式
        ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
        //提交事务
        ft.commit();
        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;
    }


}

1.7 片段中的按钮点击事件修正

1.7.1 在片段中单击按钮,应用崩溃的原因

如果在布局XML文件中,对Button视图以android:onClick属性来指定点击各个按钮时要调用哪个方法,则在应用在运行的时候会发生崩溃

?原因onClick属性调用活动中的方法,而不是片段中的方法

因为,使用android:onClick属性指定点击视图时要调用哪个方法,这里指定的是调用 当前活动 中的那个方法。如果视图处于一个活动的布局中,就没有问题,但是如果视图在一个片段中,就会带来问题。Android不会调用片段中的方法,而只会调用父活动中的方法。如果它无法在这个活动中找到相应的方法,应用就会崩溃

1.7.2 单击按钮时调用片段中的方法

要让片段中的按钮调用片段中的方法而不是活动中的方法,需要做到两件事:

1.从片段布局中删除android:onCliek的引用。

使用android:onclick属性时,按钮就会试图调用活动中的方法,所以要从片段布局中将它们删除。

2.实现一个onCIickListener将按钮与片段中的方法绑定。

这样才能确保单击按钮时调用正确的方法。

1.7.3 将onClickListener关联到按钮

为片段类StopwatchFragment实现View.onClickListener接口:

这样会把片段类StopwatchFragment转换为View.OnClickListener类型,这样一来,单击视图时它就能做出响应。

通过实现View.onClickListener onClick()方法,可以告诉片段如何响应点击。只要单击了片段中的一个视图,就会调用这个方法

onClick()方法有一个View参数,这是用户单击的视图。可以使用View getId()方法得到用户单击哪个视图,然后确定如何做出反应。

将onClickListener关联到按钮

要让视图响应点击事件,需要调用各个视图的setonclickListener()方法。setonClickListener()方法需要一个onclickListener对象作为参数。由于stopwatchFragment实现了onclickListener接口,我们可以传递这个片段作为onclickListener。

?要在创建片段的视图之后调用各个视图的setonclickListener()方法。这说明要把这些代码放在StopwatchFragmentonCreateview()方法中,如下所示:

完整的代码在之前已经给出。

1.8 旋转设备会重新创建活动

运行应用然后旋转设备时,会撤销并重新创建正在运行的活动。活动代码中的所有变量都会重置回它们的默认值;如果希望在活动撤销之前保存这些值,需要使用活动的onSaveInstancestate()方法。

如果活动包含一个片段,活动和片段生命周期是紧密相关的,旋转设备时,片段会发生什么?

1.活动包含一个片段

2.用户旋转设备,片段会随活动一同被撤销

3.重新创建活动,并调用它的onCreate()方法

onCreate()方法包含一个setContentView()调用。

4.运行活动的setContentView()方法时,它会读取活动的布局,重放它的片段事务。

片段随着最后一个事务重新创建。

旋转设备时,片段应当回到设备旋转之前的状态。那么这里为什么会重置秒表?为了找出线索,下面来看WorkoutDetailFragment onCreateView()方法。

重放事务之后运行onCreateView()

活动重放所有片段事务之后运行onCreateView()方法。

onCreateView()方法包含一个片段事务,会把秒表片段替换成为一个全新的片段。这说明会发生两件事:

  1. ?活动重放它的片段事务,使秒表片段恢复到设备旋转之前的状态。
  2. onCreateview()方法删除活动恢复的秒表片段,把它替换为一个全新的片段。由于这是一个全新版本的片段,秒表重置为0。

为了防止这种情况发生,我们要确保只有当savedInstances-tate Bundle为null才替换片段。这意味着只是在第一次创建活动时才会显示一个全新的StopwatchFragment。

因为本来秒表片段也是带保存局部变量的功能,因此在运行之后,旋转设备,秒表时间也不会被重置。

∶如果在片段布局代码中使用android:onclick属性,Android真的会调用活动中的方法吗?

:没错,确实是这样。所以不要使用android:onClick属性让视图响应单击事件,应当实现一个onclickListener。
?

?问:这适用嵌套片段,还是一般意义上的片段?

:所有片段都是这样,不论它们是否嵌套在另一个片段中。

:我要在我自己的应用中使用片段吗?

:这取决于你的应用,另外要看你想达到什么目的。
使用片段的一个主要好处是可以用片段支持多种不同的屏幕大小。比如,可以选择在平板电脑上并排显示片段,而在较小的设备上用单独的屏幕显示片段。

2.总结

  1. 片段可以包含其他片段。
  2. 如果在一个片段中嵌套另一个片段,需要通过编程在Java代码中增加这个嵌套片段。
  3. 在一个嵌套片段上执行事务时,要使用getChildFragmentManager()创建事务。
  4. 如果在片段中使用android:onclick属性,Android会在这个片段的父活动中查找同名的方法。
  5. 不要在片段中使用android:onclick属性,而应当让片段实现view.onclickListener接口,并实现它的onclick()方法。
  6. 设备配置改变时,活动和它的片段会被撤销。活动重新创建时,它会在onCreate()方法的setContentview()调用中重放它的片段事务。
  7. 活动重放片段事务之后会运行片段的oncreateview ()方法。
    ?

?

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-03-30 18:36:39  更:2022-03-30 18:39:54 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 18:56:33-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码