200字范文,内容丰富有趣,生活中的好帮手!
200字范文 > 8 嵌套片段

8 嵌套片段

时间:2021-05-10 18:37:22

相关推荐

8 嵌套片段

1.引入

1.0 引入

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

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

1.1 需求引入--嵌套片段

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

增加一个新的秒表片段

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

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

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

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

不过方法稍有不同

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

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

1.3 从活动到片段的转换

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

不使用布局文件activity_stopwatch.xml,而要使用布局文件fragment_Stopwatch.xml确保方法的访问限制是真确的如何指定布局?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;@Overridepublic 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;}}}@Overridepublic 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;}@Overridepublic void onPause() {super.onPause();//如果片段暂停,记录秒表原来是否在运行wasRunning = running;//然后将它停止running = false;}@Overridepublic void onResume() {super.onResume();if (wasRunning) {//如果暂停前秒表在运行,再设置为运行running = true;}}//活动撤销前将变量放在Bundle中,用户旋转设备时会使用这些变量@Overridepublic 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() {@Overridepublic 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="/apk/res/android"xmlns:tools="/tools"android:layout_width="match_parent"android:layout_height="match_parent"><TextViewandroid: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" /><Buttonandroid: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" /><Buttonandroid: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" /><Buttonandroid: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="/apk/res/android"android:layout_height="match_parent"android:layout_width="match_parent"android:orientation="vertical"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:textAppearance="?android:attr/textAppearanceLarge"android:text=""android:id="@+id/textTitle" /><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text=""android:id="@+id/textDescription" /><FrameLayoutandroid: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。

代码如下:

@Overridepublic 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);//提交事务mit();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@Overridepublic 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);//提交事务mit();return inflater.inflate(R.layout.fragment_workout_detail,container,false);}@Overridepublic 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()中获取这个值@Overridepublic 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()方法包含一个片段事务,会把秒表片段替换成为一个全新的片段。这说明会发生两件事:

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

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

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

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

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

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

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

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

:这取决于你的应用,另外要看你想达到什么目的。

使用片段的一个主要好处是可以用片段支持多种不同的屏幕大小。比如,可以选择在平板电脑上并排显示片段,而在较小的设备上用单独的屏幕显示片段。

2.总结

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

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。