|
|
|
---
|
|
|
|
Fragment
|
|
|
|
---
|
|
|
|
|
|
|
|
#### 目录
|
|
|
|
|
|
|
|
1. 思维导图
|
|
|
|
2. 概述
|
|
|
|
3. 设计原因
|
|
|
|
4. 基本使用
|
|
|
|
- xml 声明
|
|
|
|
- 代码设置
|
|
|
|
- 添加没有 UI 的 fragment
|
|
|
|
5. 生命周期
|
|
|
|
6. 管理 Fragment 和执行事务
|
|
|
|
7. 与 Activity 通信
|
|
|
|
8. 常见问题汇总
|
|
|
|
1. 创建 Fragment 实例传递数据
|
|
|
|
2. getActivity() 引用问题
|
|
|
|
3. FragmentTransaction 的 add 和 replace 区别
|
|
|
|
4. getChildFragmentManager()
|
|
|
|
5. 回退栈的理解
|
|
|
|
6. Fragment 重叠
|
|
|
|
7. onActivityResult()
|
|
|
|
9. 源码分析
|
|
|
|
10. 参考
|
|
|
|
|
|
|
|
#### 思维导图
|
|
|
|
|
|
|
|
![](https://i.loli.net/2019/02/16/5c67a742033b0.png)
|
|
|
|
|
|
|
|
#### 概述
|
|
|
|
|
|
|
|
Fragment 译为 “片段”,必须始终嵌入在 Activity 中,其生命周期直接受宿主 Activity 生命周期的影响。当你以片段作为 Activity 布局的一部分添加时,它存在于 Activity 视图层次结构的某个 ViewGroup 内部,并且片段会定义其自己的视图布局,可以通过在 Activity 的布局文件中以 \<fragment> 形式插入,或者通过代码进行插入。
|
|
|
|
|
|
|
|
**不过,Fragment 并非必须成为 Activity 布局的一部分,可以根据需要将没有 UI 的 Fragment 作为 Activity 的不可见工作线程。**下文中,我们会根据 Google 提供的一个实例来分析其作用。
|
|
|
|
|
|
|
|
#### 设计原因
|
|
|
|
|
|
|
|
既然已经有了 Activity 来展示 UI,为什么还需要 Fragment 呢?
|
|
|
|
|
|
|
|
根据官方文档的说法,Android 在 Android 3.0 引入的 Fragment,主要是为了给大屏幕(如平板电脑)更加动态和灵活的 UI 设计提供支持。
|
|
|
|
|
|
|
|
当然,我觉得在我们的实际开发中,Fragment 可以把 Activity 分离出多个可重用的组件,它们都有自己的生命周期,同时Fragment 之间的切换更加流畅,比 Activity 更加轻量。
|
|
|
|
|
|
|
|
#### 基本使用
|
|
|
|
|
|
|
|
基本使用分为在 xml 里面声明和代码设置。
|
|
|
|
|
|
|
|
##### xml 声明
|
|
|
|
|
|
|
|
```xml
|
|
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
|
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
|
|
android:orientation="vertical"
|
|
|
|
android:layout_width="match_parent"
|
|
|
|
android:layout_height="match_parent">
|
|
|
|
<fragment
|
|
|
|
android:id="@+id/fragment"
|
|
|
|
android:name="com.example.omooo.fragment.FirstFragment"
|
|
|
|
android:layout_width="match_parent"
|
|
|
|
android:layout_height="wrap_content"/>
|
|
|
|
</LinearLayout>
|
|
|
|
```
|
|
|
|
|
|
|
|
当系统创建此 Activity 布局时,会实例化布局中指定的每个片段,并为每个片段调用 onCreateView 方法,系统会直接插入片段返回的 View 来替代 \<fragment> 元素。
|
|
|
|
|
|
|
|
**注意:**
|
|
|
|
|
|
|
|
每个片段都需要一个唯一的标识符,重启 Activity 时,系统可以使用该标识符来恢复片段(也可以使用该标识符来获取 fragment 以执行某些事务,比如将其移出),可以通过三种方式为 fragment 提供 ID:
|
|
|
|
|
|
|
|
1. 通过 android:id 属性提供唯一 ID
|
|
|
|
2. 通过 android:tag 属性提供唯一字符串
|
|
|
|
3. 如果都没设置,系统会使用容器视图的 ID
|
|
|
|
|
|
|
|
##### 代码设置
|
|
|
|
|
|
|
|
```java
|
|
|
|
FragmentManager fragmentManager = getSupportFragmentManager();
|
|
|
|
FragmentTransaction transaction = fragmentManager.beginTransaction();
|
|
|
|
transaction.add(R.id.fl_content, new FirstFragment());
|
|
|
|
transaction.commit();
|
|
|
|
```
|
|
|
|
|
|
|
|
##### 添加没有 UI 的 fragment
|
|
|
|
|
|
|
|
上面的两种使用都是向 Activity 添加 UI,不过,我们还可以使用 Fragment 为 Activity 提供后台行为,而不需要显示 UI。
|
|
|
|
|
|
|
|
要想添加没有 UI 的 fragment,请使用 FragmentTransaction#add(Fragment,String),第二个参数即 tag,这是标识它的唯一方式,之后,我们可以通过 findFragmentByTag 获取该 Fragment。
|
|
|
|
|
|
|
|
以没有 UI 的 Fragment 为 Activity 提供后台行为,这里直接看 Google 给的 APIDemos 的 [FragmentRetainInstance](https://android.googlesource.com/platform/development/+/master/samples/ApiDemos/src/com/example/android/apis/app/FragmentRetainInstance.java) 实例就行了,里面有一个有意思的点:
|
|
|
|
|
|
|
|
1. setRetainInstance(true)
|
|
|
|
|
|
|
|
设置为 true,当 Activity 异常销毁时,Fragment 不会被重新创建,它的实例会保留,当重建 Activity 时,会自动拿来用。
|
|
|
|
|
|
|
|
不过,平心而论,我并不觉得这个例子有什么指导意义。。
|
|
|
|
|
|
|
|
#### 生命周期
|
|
|
|
|
|
|
|
![](https://i.loli.net/2019/02/15/5c6656e256348.png)
|
|
|
|
|
|
|
|
#### 管理 Fragment 和执行事务
|
|
|
|
|
|
|
|
##### 管理 Fragment
|
|
|
|
|
|
|
|
管理 Fragment,需要用到 FragmentManager,想用获取它,可以通过在 Activity 中调用 getSupportFragmentManager()。
|
|
|
|
|
|
|
|
通过 FragmentManager 执行的操作包括:
|
|
|
|
|
|
|
|
1. 通过 findFragmentById 或 findFragmentByTag 获取 Fragment
|
|
|
|
2. 通过 popBackStack 将 Fragment 从返回栈中弹出
|
|
|
|
3. 通过 addOnBackStackChangedListener 注册一个侦听返回栈变化的侦听器
|
|
|
|
4. 通过 beginTransaction 获取 FragmentTransaction
|
|
|
|
|
|
|
|
##### 执行事务
|
|
|
|
|
|
|
|
在 Activity 中使用 Fragment 一大优点是,可以根据用户行为通过它们执行添加、移出、替换以及其他操作。
|
|
|
|
|
|
|
|
常用的 API 有 add()、remove()、replace() 方法,如果想把 Fragment 添加到返回栈,则可以使用 addToBackStack(),不过最后所有的操作都需要 commit。
|
|
|
|
|
|
|
|
对于每个 Fragment 事务,都可以在提交前调用 setTransition() 来设置过渡动画。
|
|
|
|
|
|
|
|
调用 commit() 不会立即执行事务,而是在 UI 线程可以执行该操作时再安排其他线程上运行。不过,如果有必要,可以在 UI 线程调用 commitNow() 以立即执行 commit 提交的事务。通常不必这样做,除非其他线程中的作业依赖该事务。
|
|
|
|
|
|
|
|
注意:只能在 Activity 保存其状态之前使用 commit 提交事务,如果试图在该时间之后提交,则会引发异常。这是因为如需恢复 Activity,则提交后的状态可能会丢失。对于丢失提交无关紧要的情况,可以使用 commitAllowingStateLoss()。
|
|
|
|
|
|
|
|
#### 与 Activity 通信
|
|
|
|
|
|
|
|
Fragment 可以通过 getActivity() 获取 Activity 实例,以此来调用具体实例 Activity 中的公有方法。
|
|
|
|
|
|
|
|
Activity 也可以使用 findFragmentById 或 findFragmentByTag 来获取 Fragment 实例,以此来调用具体 Fragment 内的公有方法。
|
|
|
|
|
|
|
|
当然,最推荐的做法就是接口回调。
|
|
|
|
|
|
|
|
#### 常见问题汇总
|
|
|
|
|
|
|
|
1. 创建 Fragment 实例传递数据
|
|
|
|
|
|
|
|
```java
|
|
|
|
public static OneFragment newInstance(int args){
|
|
|
|
OneFragment oneFragment = new OneFragment();
|
|
|
|
Bundle bundle = new Bundle();
|
|
|
|
bundle.putInt("someArgs", args);
|
|
|
|
oneFragment.setArguments(bundle);
|
|
|
|
return oneFragment;
|
|
|
|
}
|
|
|
|
@Override
|
|
|
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
|
|
|
super.onCreate(savedInstanceState);
|
|
|
|
Bundle bundle = getArguments();
|
|
|
|
int args = bundle.getInt("someArgs");
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
之所以不用带参的构造方法,原因在于 Activity 在一些特殊情况下会发生销毁并重建的情形,比如屏幕旋转、内存吃紧等;对应的,依附于 Activity 存在的 Fragment 也会发生类似的状况。而一旦重建,Fragment 便会调用默认的无参构造函数,导致无法执行有参构造函数进行初始化工作。
|
|
|
|
|
|
|
|
2. getActivity() 引用问题
|
|
|
|
|
|
|
|
当 Fragment 中存在类似网络请求之类的异步耗时任务时,该任务执行完毕回调 Fragment 的方法用到 Activity 对象时,可能宿主 Activity 已经销毁,从而引发空指针异常,所以最好都判空。
|
|
|
|
|
|
|
|
一般情况下,获取 Context,可以通过 getContext() 获取。
|
|
|
|
|
|
|
|
3. FragmentTransaction add 和 replace 区别
|
|
|
|
|
|
|
|
最开始我觉得使用 add 的话在 addBackStack,在按返回键时应该会退到上一个 Fragment,而使用 replace 的在 addBackStack,replace 会把之前的 fragment 清除掉,所以在按返回键时只是把当前 fragment 清除掉,并不会回退到上一个 fragment。
|
|
|
|
|
|
|
|
这完全是错误的理解!
|
|
|
|
|
|
|
|
```java
|
|
|
|
public void showFirstFragment(View view) {
|
|
|
|
mFragmentManager = getSupportFragmentManager();
|
|
|
|
mFragmentTransaction = mFragmentManager.beginTransaction();
|
|
|
|
mFragmentTransaction.add(R.id.fl_content, new FirstFragment());
|
|
|
|
mFragmentTransaction.addToBackStack("fragment1");
|
|
|
|
mFragmentTransaction.commit();
|
|
|
|
}
|
|
|
|
|
|
|
|
public void showSecondFragment(View view) {
|
|
|
|
//注意:这里重新实例化了一次 FragmentTransaction
|
|
|
|
//不然多次调用同一个 FragmentTransaction 的 commit 会崩溃
|
|
|
|
mFragmentTransaction = mFragmentManager.beginTransaction();
|
|
|
|
mFragmentTransaction.replace(R.id.fl_content, new SecondFragment());
|
|
|
|
mFragmentTransaction.addToBackStack("fragment2");
|
|
|
|
mFragmentTransaction.commit();
|
|
|
|
}
|
|
|
|
|
|
|
|
public void pop(View view) {
|
|
|
|
mFragmentManager.popBackStack();
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
这里要分开理解,add 和 replace 影响的只是界面,而控制回退的,是事务。add 的时候是把一个 Fragment 添加到容器里,多次 add 时,视图是添加多层;而使用 replace 是先 remove 掉相同 id 的所有 Fragment,然后在 add。
|
|
|
|
|
|
|
|
而至于会退栈,和事务有关,跟使用 add 还是 replace 没有任何关系。
|
|
|
|
|
|
|
|
4. getChildFragmentManager()
|
|
|
|
|
|
|
|
在 Activity 嵌入 Fragment 时,需要使用 FragmentManager,通过 Activity 提供的 getSupportFragmentManager() 方法即可获取,用于管理 Activity 里面嵌入的所有一级 Fragment。
|
|
|
|
|
|
|
|
然后有时候,我们会在 Fragment 里面继续嵌入多级 Fragment,这时候就需要通过 Fragment 来获取 FragmentManager 对象。
|
|
|
|
|
|
|
|
```java
|
|
|
|
FirstFragment firstFragment = new FirstFragment();
|
|
|
|
FragmentManager fragmentManager = firstFragment.getChildFragmentManager();
|
|
|
|
```
|
|
|
|
|
|
|
|
5. 回退栈的理解
|
|
|
|
|
|
|
|
通过 addToBackStack 保存当前事务,当用户按下返回键时,如果回退栈中保存有之前的事务,便会执行事务回退,而不是 finsh 掉当前 Activity。
|
|
|
|
|
|
|
|
6. Fragment 重叠问题
|
|
|
|
|
|
|
|
前面说过,当 Activity 销毁并重建的时候,Activity 重新执行 onCreate 方法,那么不就是创建两次 Fragment 而导致 UI 重叠嘛?
|
|
|
|
|
|
|
|
解决方法也很简单:
|
|
|
|
|
|
|
|
```java
|
|
|
|
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
|
|
|
super.onCreate(savedInstanceState);
|
|
|
|
setContentView(R.layout.activity_fragment);
|
|
|
|
mFrameLayout = findViewById(R.id.fl_content);
|
|
|
|
|
|
|
|
if (savedInstanceState != null) {
|
|
|
|
mFirstFragment = (FirstFragment) mFragmentManager.findFragmentByTag("fragment1");
|
|
|
|
} else {
|
|
|
|
mFirstFragment = FirstFragment.newInstance();
|
|
|
|
mFragmentTransaction.add(mFirstFragment, "fragment1");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
7. onActivityResult()
|
|
|
|
|
|
|
|
Fragment 类提供了 startActivityForResult 方法用于 Activity 间的页面跳转和数据回传,其实内部也是调用了 Activity 的对应方法,但是在页面返回时 Fragment 没有提供 setResult 方法,但是可以通过拿宿主 Activity 实现。
|
|
|
|
|
|
|
|
但是仍要注意的一点是,嵌套的 Fragment 需要一级一级的分发。
|
|
|
|
|
|
|
|
#### 源码分析
|
|
|
|
|
|
|
|
##### FragmentManager
|
|
|
|
|
|
|
|
我们通过在 Activity 在 getSupportFragmentManager 获取的 FragmentManager 其实是 FragmentManagerImpl,它也是抽象类 FragmentManager 的唯一实现类。
|
|
|
|
|
|
|
|
FragmentManagerImpl 里面有两个重要方法:
|
|
|
|
|
|
|
|
1. beginTransaction()
|
|
|
|
|
|
|
|
```java
|
|
|
|
public FragmentTransaction beginTransaction() {
|
|
|
|
return new BackStackRecord(this);
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
这里 BackStackRecord 也是抽象类 FragmentTransaction 的唯一实现。
|
|
|
|
|
|
|
|
2. popBackStack()
|
|
|
|
|
|
|
|
```java
|
|
|
|
public void popBackStack() {
|
|
|
|
enqueueAction(new PopBackStackState(null, -1, 0), false);
|
|
|
|
}
|
|
|
|
public void enqueueAction(OpGenerator action, boolean allowStateLoss) {
|
|
|
|
if (!allowStateLoss) {
|
|
|
|
checkStateLoss();
|
|
|
|
}
|
|
|
|
synchronized (this) {
|
|
|
|
if (mPendingActions == null) {
|
|
|
|
mPendingActions = new ArrayList<>();
|
|
|
|
}
|
|
|
|
mPendingActions.add(action);
|
|
|
|
scheduleCommit();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
##### BackStackRecord
|
|
|
|
|
|
|
|
```java
|
|
|
|
final class BackStackRecord extends FragmentTransaction implements
|
|
|
|
FragmentManager.BackStackEntry, FragmentManagerImpl.OpGenerator {
|
|
|
|
|
|
|
|
final FragmentManagerImpl mManager;
|
|
|
|
// 构造 Op 的标识位
|
|
|
|
static final int OP_NULL = 0;
|
|
|
|
static final int OP_ADD = 1;
|
|
|
|
static final int OP_REPLACE = 2;
|
|
|
|
static final int OP_REMOVE = 3;
|
|
|
|
//...
|
|
|
|
|
|
|
|
static final class Op {
|
|
|
|
//add、replace、remove 标志位
|
|
|
|
int cmd;
|
|
|
|
Fragment fragment;
|
|
|
|
//进入退出动画
|
|
|
|
int enterAnim;
|
|
|
|
int exitAnim;
|
|
|
|
int popEnterAnim;
|
|
|
|
int popExitAnim;
|
|
|
|
|
|
|
|
Op() {
|
|
|
|
}
|
|
|
|
|
|
|
|
Op(int cmd, Fragment fragment) {
|
|
|
|
this.cmd = cmd;
|
|
|
|
this.fragment = fragment;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
//操作集合
|
|
|
|
ArrayList<Op> mOps = new ArrayList<>();
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
我们通过 FragmentTransaction add Fragment 时:
|
|
|
|
|
|
|
|
```java
|
|
|
|
public FragmentTransaction add(int containerViewId, Fragment fragment){
|
|
|
|
doAddOp(containerViewId, fragment, null, OP_ADD);
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
private void doAddOp(int containerViewId, Fragment fragment, String tag, int opcmd) {
|
|
|
|
fragment.mFragmentManager = mManager;
|
|
|
|
addOp(new Op(opcmd, fragment));
|
|
|
|
}
|
|
|
|
|
|
|
|
void addOp(Op op) {
|
|
|
|
mOps.add(op);
|
|
|
|
op.enterAnim = mEnterAnim;
|
|
|
|
op.exitAnim = mExitAnim;
|
|
|
|
op.popEnterAnim = mPopEnterAnim;
|
|
|
|
op.popExitAnim = mPopExitAnim;
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#### 参考
|
|
|
|
|
|
|
|
[Fragment 官方文档](https://developer.android.com/guide/components/fragments?hl=zh-cn)
|
|
|
|
|
|
|
|
[Android Fragment 的使用,一些你不可不知的注意事项](http://yifeng.studio/2016/12/15/android-fragment-attentions/)
|