之前我们学习的是一个活动作为一个页面,有了平板之后,页面如果像手机一样设计就会浪费很多的空间,会有很多的空白区域,为了使屏幕充分利用,引入了碎片这样一个概念。
碎片(Fragment):是一种可以嵌入在活动当中的UI片段,它能让程序更加合理和充分的利用大屏幕的空间,在平板上应用得非常广泛。
碎片的使用方式
碎片的静态创建
我们要在一个活动当中存放两个碎片,并让这两个碎片平分活动的空间,首先新建两个碎片布局
新建一个左碎片布局,当中放一个按钮:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="match_parent">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="buttonl"
android:id="@+id/Buttonleft2"/>
</LinearLayout>
新建一个右碎片布局,将背景颜色换为粉色,并放置一个TextView用于显示一段文本,用来区分两块碎片:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:background="#f0a1a8"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="This is right fragment"
android:layout_gravity="center"
android:id="@+id/right2"/>
</LinearLayout>
紧接着新建两个类:LeftFragment与RightFragment,并让其继承Fragment,此时我们需要导包,有不同的包供我们选择,此时应导入import androidx.fragment.app.Fragment;
如果你导的是别的包,有可能会报错,甚至程序崩溃。接下来编写一下里面的代码吧:
LeftFragment中的代码:
import androidx.fragment.app.Fragment;
public class LeftFragment extends Fragment {
@Override
public View onCreateView (LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.left_fragment, container, false);
return view;
}
}
代码当中我们重写了onCreateView方法,是Fragment生命周期的一部分,用于加载Fragment的布局:
LayoutInflater inflater
:用于将XML布局文件实例化为View对象。
ViewGroup container
:Fragment
将被添加到这个容器中。Bundle savedInstanceState
:如果Fragment
之前被保存过状态,这个Bundle
包含了之前保存的状态信息。
同样的方法去重写RightFragment类,基本上代码是相同的。
接下来就去修改承载这两个碎片活动的XML文件代码,由于使用的手机,所以换成了上下平分:
<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:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".StaticFragment">
<fragment
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:id="@+id/left_fragment"
tools:layout="@layout/left_fragment"
android:name="com.example.fragmenttext1.LeftFragment" />
<fragment
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:id="@+id/right_fragment"
tools:layout="@layout/right_fragment"
android:name="com.example.fragmenttext1.RightFragment4" />
</LinearLayout>
此时两个碎片就被加载到了一个活动当中,看一看运行结果吧:
注意,在使用fragment标签时:
- 必须声明android:id或者android:tag属性,否则会有错
android:tag
是一个可以在 XML 布局文件中使用的属性,它为视图或组件提供了一个标签(tag),方便在代码中通过findViewWithTag()
方法来查找和引用特定的视图。
- 与之前不同在于,之前我们可以直接看到视图的预览,而在fragment时,我们需要使用tools:layout="@layout/left_fragment"来看到预览的视图
动态添加碎片
再新建一个another_right_fragment布局文件,此时修改了文本内容与背景颜色:
<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">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#8076a3"
android:gravity="center"
android:text="This is other right fragment"
tools:layout_editor_absoluteX="105dp"
tools:layout_editor_absoluteY="50dp" />
</LinearLayout>
新建一个AnotherRightFragment作为右边的碎片,与之前的代码类似:
import androidx.fragment.app.Fragment;
public class AnotherRightFragment extends Fragment {
@Override
public View onCreateView (LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.another_right_fragment, container, false);
return view;
}
}
代码与之前都非常类似,接下来我们就看看如何动态地添加到活动当中,我们修改放碎片的活动的XML文件:
<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:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".dong_fragment">
<fragment
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:id="@+id/left_fragment"
tools:layout="@layout/activity_dong_fragment"
android:name="com.example.fragmenttext1.LeftFragment" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:id="@+id/right_fragment"/>
</LinearLayout>
此时,我们将右面的碎片替换成了FrameLayout
,下面再通过代码向里面添加内容,从而实现动态的添加碎片,接下来就看放碎片的活动的代码:
public class dong_fragment extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_dong_fragment);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
Button button = (Button) findViewById(R.id.Buttonleft2);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
replaceFragment(new AnotherRightFragment());
}
});
replaceFragment(new RightFragment4());
}
private void replaceFragment (Fragment fragment) {
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.replace(R.id.right_fragment, fragment);
transaction.commit();
}
}
此时我们是对左边碎片的按钮注册了点击事件,一开始会调用replaceFragment
方法将右侧添加为一开始的右侧碎片,与上面静态添加碎片的结果相同,但当我们按下Button按钮,就会触发按钮的点击事件,从而使碎片发生变化,变成另一个右边的碎片。
由上面的代码我们知道动态添加布局需要5步:
- 创建待添加的碎片实例,即上面调用换碎片时所new的实例
- 获取FragmentManager,在活动中可以直接通过调用
getSupportFragmentManager()
获取
FragmentManager
是 Android 开发中用于管理Fragment
生命周期和事务的核心类。它负责将Fragment
附加到Activity
或Fragment
,以及执行Fragment
事务,比如添加、移除、替换和回退等操作。
- 开启一个事务,通过调用
beginTransaction()
方法开启
FragmentTransaction
是 Android 开发中用于执行Fragment
事务操作的类。它允许你在一个事务中执行多个操作,比如添加、移除、替换或隐藏Fragment
。
- 向容器内添加或替换碎片,一般通过
replace()
方法实现需要传入容器的id和待添加的碎片实例 - 提交事务,调用
commit()
方法来完成
重新运行程序,一开始会调用replaceFragment方法将右侧碎片添加到下面,与上面静态添加碎片的结果是一样的,当我们按下Button按钮就会触发点击事件,从而使右侧碎片变化为另一个右侧碎片,效果如图所示:
在碎片中模拟返回栈
我们上面实现向活动中动态添加碎片,当我们按下按钮转换到另一个碎片的时候,再按下返回键,就会直接退出程序,但我们如果想按下Back键可以返回到上一个碎片,又要如何实现呢?
其实,在FragmentTransaction当中就提供了一个addToBackStack()
方法,可以将事务添加到返回栈当中,只需要对上面replaceFragment()
进行小小的修改:
private void replaceFragment (Fragment fragment) {
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.replace(R.id.right_fragment, fragment);
transaction.addToBackStack(null);
transaction.commit();
}
在提交事务之前,我们调用了FragmentTransaction
的addToBackStack()
方法,它可以接收一个名字用于描述返回栈的状态,一般传入null即可。现在运行程序,点击按钮使其另一个右边碎片替换进活动当中,然后按下Back键,你会发现,程序并没有退出,而是回到了第一个界面,继续按下Back键,右边的碎片会消失,再次按下back键,程序才会退出。
碎片的生命周期
碎片的状态和回调
之前学习活动的时候了解到活动有四种状态:运行状态、暂停状态、停止状态、销毁状态。其实每个碎片的生命周期也会经历这几种状态,但在细微的方面有差别。
- 运行状态
当一个碎片是可见的,并且它所关联的活动正处于运行状态时,该碎片也处于运行状态
- 暂停状态
当一个活动进入暂停状态时(由于另一个未占满屏幕的活动被添加到了栈顶),与它相关联的可见碎片就会进入到暂停状态
- 停止状态
当一个活动进入停止状态时,与它相关联的碎片就会进入到停止状态,或者通过调用FragmentTransaction
的remove()
、replace()
方法将碎片从活动中移除,但在事务提交之前调用addToBackStack()
方法,此时的碎片会进入停止状态。进入停止状态的碎片对用户来说是完全不可见的,有可能会被系统回收
- 销毁状态
碎片是依附于活动而存在的,因此当活动被销毁时,与它相关联的碎片就会进入到销毁状态,或者通过调用FragmentTransaction
的remove()
、replace()
方法将碎片从活动中移除,但在事务提交之前没有调用addToBackStack()
方法,此时的碎片也会进入销毁状态。
活动的回调方法大家都已经接触过了,接下来就看看碎片的回调方法。活动的回调方法,碎片中几乎都有,但碎片还提供了一些附加的回调方法:
- onAttach():当碎片和活动建立关联时调用
- onCreateView():为碎片创建视图(加载布局)时调用
- onActivityCreated():确保与碎片相关联的活动一定已经创建完毕的时候调用
- onDestroyView():当与碎片关联的视图被移除的时候调用
- onDetach():当碎片与活动解除关联的时候调用
完整的碎片过程如图所示:
体验碎片的生命周期
仍然按照上面的代码进行修改,在右侧碎片的代码当中进行修改
public class RightFragment4 extends Fragment {
public static final String ATG = "RightFragment";
@Override
public View onCreateView (LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Log.d(ATG, "onCreateView");
View view = inflater.inflate(R.layout.right_fragment, container, false);
return view;
}
@Override
public void onAttach (Context context) {
super.onAttach(context);
Log.d(ATG, "onAttach");
}
@Override
public void onCreate (Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(ATG, "onCreate");
}
@Override
public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
Log.d(ATG, "onActivityCreated");
}
@Override
public void onStart () {
super.onStart();
Log.d(ATG, "onStart");
}
@Override
public void onResume () {
super.onResume();
Log.d(ATG, "onResume");
}
@Override
public void onPause () {
super.onPause();
Log.d(ATG, "onPause");
}
@Override
public void onStop () {
super.onStop();
Log.d(ATG, "onStop");
}
@Override
public void onDestroyView () {
super.onDestroyView();
Log.d(ATG, "onDestroyView");
}
@Override
public void onDestroy () {
super.onDestroy();
Log.d(ATG, "onDestroy");
}
@Override
public void onDetach () {
super.onDetach();
Log.d(ATG, "onDetach");
}
}
我们在每一个回调方法里面都加入了打印日志的方法,然后重新运行程序,此时的打印信息为:
依次执行这5种方法,然后点击左边碎片的按钮跳转到另一个碎片,此时的打印信息:
由于在上面的代码当中我们加入了addToBackStack()
方法,则会将现在的碎片进入停止状态,执行onStart()
、onResume()
方法,但如果在替换的时候没有调用这个方法,此时这个碎片处于销毁状态,则会执行onDestroy()
、onDetach()
方法,再次按下Back按钮回到最初的屏幕:
与第一次屏幕的创建少了两个回调方法:onAttach()
、onCreate()
,再次按下Back键:
这下你对碎片的生命周期就有了更深刻的理解。
动态加载布局的技巧
使用限定符
在使用平板电脑的时候我们会看到双页模式,左侧含有一个列表,右侧显示内容,而在手机当中大部分都只能显示一页的内容,怎样使程序自己判断使用双页模式还是单页模式呢?此时就需要借助限定符(Qualifiers)来实现了
新建一个活动,在对应的XML文件(activity_qualifiers.xml)里面进行修改
<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:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
tools:context=".xianDing">
<fragment
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/left_fragment"
tools:layout="@layout/left_fragment"
android:name="com.example.fragmenttext1.LeftFragment" />
</LinearLayout>
只放置一个左侧布局充满整个父布局,在res目录下新建layout-large文件夹
先将type转换为Layout,再选取Size选项,按到下一页,选取large即可,此时你需要对File进行命名,这时的名字就是第一个布局文件的名字。
在这个文件夹下新建一个名字一样的布局,代码如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
xmlns:tools="http://schemas.android.com/tools">
<fragment
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:id="@+id/left_fragment"
tools:layout="@layout/left_fragment"
android:name="com.example.fragmenttext1.LeftFragment" />
<fragment
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3"
android:id="@+id/right_fragment"
tools:layout="@layout/right_fragment"
android:name="com.example.fragmenttext1.RightFragment4" />
</LinearLayout>
前一个布局里面只包含了一个碎片,即单页模式,另一个布局包含了两个碎片,即双页模式,其中large就是一个限定符,那些屏幕被认为是large的设备就会自动加载下面的这个布局。此时在手机上运行就会看到加载的是第一个布局即只有一个碎片,当用平板运行加载的就是下面两个碎片的布局。
Android中常见的限定符:
使用最小的限定符
在上面我们学习了使用large限定符解决了双页单页问题,但large到底是多大?我们希望更加灵活地为不同的设备加载布局,不论是否被系统认为是large,这时就可以使用最小的限定符(Smallest-widthQualifier)
此时的操作步骤与上面类似,当我们设置最小宽度为600dp时,当屏幕的宽度大于600dp,则会加载现在的这个布局,当屏幕宽度小于600dp时,则会加载默认的布局
到这里就结束了!