1、Activity与Fragment通信
我们知道 Activity 与 Activity 中间通信可以通过 startActivityForResult() 和 Intent,它们通过的都是 Bundle 这个容器,我们的 Fragment 如果要与 Activity 通信也可以使用 Bundle。
Activity 向 Fragment 传数据,我们可以在 Activity 中创建 Bundle 数据包,并调用 Fragment 的 setArguments(Bundle bundle) 方法,在 Fragment 类里通过 getArguments() 方法获得传入的数据。
Fragment 向 Fragment 传数据,我们需要在 Fragment 中定义一个内部回调接口,再让包含该 Fragment 的 Activity 实现该回调接口,Fragment 通过调用该回调方法把数据传递给 Activity。
1、Activity到Fragment
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<EditText
android:id="@+id/id_et"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:id="@+id/id_btn"
android:text="发送"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<FrameLayout
android:id="@+id/frame"
android:layout_width="match_parent"
android:layout_height="match_parent"></FrameLayout>
</LinearLayout>
public class MainActivity extends AppCompatActivity {
private EditText mEdit;
private Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mEdit = (EditText) findViewById(R.id.id_et);
button = (Button) findViewById(R.id.id_btn);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String text = mEdit.getText().toString();
Fragment1 fragment1 = new Fragment1();
Bundle bundle = new Bundle();
bundle.putString("data", text);
fragment1.setArguments(bundle);
FragmentManager fm = getFragmentManager();
FragmentTransaction transaction = fm.beginTransaction();
transaction.add(R.id.frame, fragment1);
transaction.commit();
}
});
}
}
public class Fragment1 extends Fragment {
private TextView textView;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment1, container, false);
textView = (TextView) view.findViewById(R.id.txt1);
Bundle bundle = getArguments();
String text = bundle.getString("data");
textView.setText(text);
return view;
}
}
这个小例子就是要注意 setArguments() 方法的使用,其它都是我们掌握了的知识,这里如果大家想要让我们的 Activity 更简洁,可以把创建数据包的代码逻辑构造成 Fragment 的静态方法。
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String text = mEdit.getText().toString();
Fragment1 fragment1 = Fragment1.newInstance(text);
FragmentManager fm = getFragmentManager();
FragmentTransaction transaction = fm.beginTransaction();
transaction.add(R.id.frame, fragment1);
transaction.addToBackStack(null);
transaction.commit();
}
});
static Fragment1 newInstance(String text) {
Fragment1 fragment1 = new Fragment1();
Bundle bundle = new Bundle();
bundle.putString("data", text);
fragment1.setArguments(bundle);
return fragment1;
}
2、Fragment到Activity
我们写个回调来让 Activity 获取 Fragment 的 TextView 的字体的大小。
public interface MyListener {
public void getData(int size);
}
private MyListener listener;
public void setOnMyListener(MyListener listener) {
this.listener = listener;
this.listener.getData((int) textView.getTextSize());
}
我们先在 Fragment 里创建回调接口,再创建一个在 Activity 可以调用的与 Fragment 交互的方法,最后在 setOnMyListener() 中设置接口方法的参数值。
接着就在 Activity 实现回调:
private Fragment1.MyListener mListener = new Fragment1.MyListener() {
@Override
public void getData(int size) {
mEdit.setText(size + "");
}
};
最后在 button 的点击事件里设置 listener 的回调:
fragment1.setOnMyListener(mListener);
我设置的 TextView 的字体大小为 20sp,换算为 px 就是40,所以我们成功了。
但是要注意在 Fragment 没有被添加到 FragmentManager 之前,我们可以通过 Fragment.setArguments() 来设置参数,并在 Fragment 中,使用 getArguments() 来取得参数。在 Fragment 被添加到 FragmentManager 后,一旦被使用,我们再次调用 setArguments() 将会导致 java.lang.IllegalStateException: Fragment already active 异常,例如像上面的方法,如果用静态加载,由于在一开始 Activity 就和 Fragment 绑定了,所以会出错。
不过回调就没有这个问题了,只要不要将逻辑代码放在 onCreateView() 中,因为不会在被调用。
FragmentManager fm = getFragmentManager();
Fragment fragment = fm.findFragmentById(R.id.fragment);
Fragment1 fragment1 = (Fragment1) fragment;
这样就可以接着使用 Fragment1 中的方法。
而不能使用的 setArguments() 方法,我们可以使用setter和getter方法进行数据的存储和获取。
3、setter 和 getter
我们在 Fragment 里为它的 TextView 加上 get() 和 set() 方法,在 Activity 获取到它的对象后就可以直接调用设置值了,这种方法是非常简单的,不过最好在静态加载中使用:
private TextView textView;
public TextView getTextView() {
return textView;
}
public void setTextView(TextView textView) {
this.textView = textView;
}
mEdit = (EditText) findViewById(R.id.id_et);
FragmentManager fm = getFragmentManager();
Fragment fragment = fm.findFragmentById(R.id.fragment);
Fragment1 fragment1 = (Fragment1) fragment;
String text = mEdit.getText().toString();
TextView textView = fragment1.getTextView();
textView.setText(text);
要想传递大的数据,我们可以使用序列化,这个方法和 Activity 的传递方式是一样的,大家可以看我的博客Android四大组件–Activity详解(一)。
2、Fragment各种类的方法使用
在上一篇博客中我介绍了不少关于 Fragment 的方法,方法本身的作用相信大家都能够理解,但可能一用就抓瞎了,尤其是不同参数的方法,对于各种的参数的效果和使用对我们开发者又是一种考验,所以在开头,我先整理一些方法用实例来实际展示一下。
1、add()
public class MainActivity extends AppCompatActivity {
private int mSize = 20;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void click(View view) {
Fragment1 fragment1 = new Fragment1();
mSize += 10;
Bundle bundle = new Bundle();
bundle.putInt("size", mSize);
fragment1.setArguments(bundle);
addFragment(fragment1, "fragment1");
}
private void addFragment(Fragment fragment, String tag) {
FragmentManager fm = getFragmentManager();
FragmentTransaction transaction = fm.beginTransaction();
transaction.add(R.id.frame, fragment, tag);
transaction.commit();
}
}
public class Fragment1 extends Fragment {
private TextView textView;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment1, container, false);
textView = (TextView) view.findViewById(R.id.txt1);
Bundle bundle = getArguments();
int size = bundle.getInt("size");
textView.setTextSize(size);
return view;
}
}
我们可以很清楚的看到 add() 方法在添加 Fragment 的时候并不会把原来的 Fragment 移除,而是将它覆盖,而且在像是这样的情况下,原来的 Fragment 依然在页面上可以看到,所以我们可以知道 Fragment 像 Activity 一样有一个栈,每次新添加的 Fragment 就会处于栈顶,这个我后面会说。
2、remove()
我们在上面的 add() 中经过测试,发现 Fragment 有一个 ADD 队列,如果不移除,Fragment 就会不断叠加,我们现在用 remove() 测试下:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void click(View view) {
switch (view.getId()) {
case R.id.add1 :
Fragment1 fragment1 = new Fragment1();
addFragment(fragment1, "fragment1");
break;
case R.id.add2 :
Fragment2 fragment2 = new Fragment2();
addFragment(fragment2, "fragment2");
break;
case R.id.remove2 :
removeFragment("fragment2");
break;
}
}
private void addFragment(Fragment fragment, String tag) {
FragmentManager fm = getFragmentManager();
FragmentTransaction transaction = fm.beginTransaction();
transaction.add(R.id.frame, fragment, tag);
transaction.commit();
}
private void removeFragment(String tag) {
FragmentManager manager = getFragmentManager();
Fragment fragment = manager.findFragmentByTag(tag);
FragmentTransaction transaction = manager.beginTransaction();
if
transaction.remove(fragment);
transaction.commit();
}
}
在 Fragment1 上加了三个 Fragment2,然后用 remove() 方法,它会从最后添加的 Fragment2 开始移除,并不是每次都是移除最上层的 Fragment,它是从最上层往下找。
3、replace()
添加 Fragment 一般最常见的有两种方法,一种是前面说得 add() 方法,因为 ADD 队列里的 Fragment 都会出现在界面上,所以我们会看到重叠,于是 add() 方法一般都会和 hide()、show() 一起使用,通过隐藏显示来避免重叠。另一种就是 replace() 方法啦。
既然要replace(),那它是针对哪个fragment进行 replace() 呢?replace() 传进去的第一个参数是容器 ID,第二个参数是要新增的fragment。没有指定要替换的 Fragment,因为这里的 replace 操作会把这个 cotainer 中所有 Fragment 清空,然后再把指定的 Fragment 添加进去。
add() 方法在添加 Fragment 时不会刷新 ADD 队列,但 replae() 会清空 ADD 队列,界面就只显示现在指定的 Fragment。
public void click(View view) {
switch (view.getId()) {
case R.id.replace1 :
replaceFragment(new Fragment1(), "fragment1");
break;
case R.id.remove1 :
removeFragment("fragment1");
break;
case R.id.remove2 :
removeFragment("fragment2");
break;
case R.id.replace2 :
replaceFragment(new Fragment2(), "fragment2");
break;
}
}
private void replaceFragment(Fragment fragment, String tag) {
FragmentManager fm = getFragmentManager();
FragmentTransaction transaction = fm.beginTransaction();
transaction.replace(R.id.frame, fragment, tag);
transaction.commit();
}
可以看到没有重叠而且点击了多次添加,ADD 队列中也只有一个 Fragment。
replace方式 如果使用这种方式,是可以避免重叠的问题,但是每次replace() 会把生命周期全部执行一遍,如果在这些生命周期函数里拉取数据的话,就会不断重复的加载刷新数据,所以并不被推荐使用。
4、hide()、show()
再讲两个方法,hide() 和 show() 的使用自然是与 add() 一起使用的:
public void click(View view) {
switch (view.getId()) {
case R.id.add1 :
Fragment1 fragment1 = new Fragment1();
addFragment(fragment1, "fragment1");
break;
case R.id.add2 :
Fragment2 fragment2 = new Fragment2();
addFragment(fragment2, "fragment2");
break;
case R.id.hide1 :
hideFragment("fragment1");
break;
case R.id.hide2 :
hideFragment("fragment2");
break;
case R.id.show1 :
showFragment("fragment1");
break;
case R.id.show2 :
showFragment("fragment2");
break;
}
}
private void showFragment(String tag) {
FragmentManager fm = getFragmentManager();
FragmentTransaction transaction = fm.beginTransaction();
Fragment fragment = fm.findFragmentByTag(tag);
transaction.show(fragment);
transaction.commit();
}
private void hideFragment(String tag) {
FragmentManager fm = getFragmentManager();
FragmentTransaction transaction = fm.beginTransaction();
Fragment fragment = fm.findFragmentByTag(tag);
transaction.hide(fragment);
transaction.commit();
}
虽然是把 Fragment 隐藏了,但是 Fragment 在 ADD 队列中的位置并没有变,新添加的 Fragment 仍是在最上层。
5、detach()、attach()
这两个方法在上篇博客介绍的时候,说它们的功能分别是解除与 Activity 的绑定和与 Activity 进行绑定,它们也能实现将 Fragment 从视图上去除的效果,但与 show() 和 hide() 不同的是,这两个方法将 Fragment 从 ADD 队列清除了,用 Fragment.isAdded() 我们可以知道 fragment 是否在 队列里,fragment 被 detach 后就会返回 false,attach 后返回 true,用 show() 则一直是 true。
public void click(View view) {
switch (view.getId()) {
case R.id.add1 :
Fragment1 fragment1 = new Fragment1();
addFragment(fragment1, "fragment1");
break;
case R.id.add2 :
Fragment2 fragment2 = new Fragment2();
addFragment(fragment2, "fragment2");
break;
case R.id.detach1 :
detachFragment("fragment1");
break;
case R.id.detach2 :
detachFragment("fragment2");
break;
case R.id.attach1 :
attachFragment("fragment1");
break;
case R.id.attach2 :
attachFragment("fragment2");
break;
case R.id.isAdded :
FragmentManager fm = getFragmentManager();
Fragment fragment = fm.findFragmentByTag("fragment1");
Toast.makeText(fragment.getActivity(), "fragment1: isAdded() = " + fragment.isAdded(),
Toast.LENGTH_SHORT).show();
break;
}
}
private void attachFragment(String tag) {
FragmentManager fm = getFragmentManager();
FragmentTransaction transaction = fm.beginTransaction();
Fragment fragment = fm.findFragmentByTag(tag);
transaction.attach(fragment);
transaction.commit();
}
private void detachFragment(String tag) {
FragmentManager fm = getFragmentManager();
FragmentTransaction transaction = fm.beginTransaction();
Fragment fragment = fm.findFragmentByTag(tag);
transaction.detach(fragment);
transaction.commit();
}
我们在图上看到这两个方法没有将 Fragment 实例给销毁,只是把它从 ADD 队列中去掉了,attach() 后就会出现在队首。
6、add()和replace()不能共用
关于添加 Fragment 的两种方式,还有个必须要注意的地方,那就是我们不能同时使用 add() 和 replace() 去添加 Fragment,我们可以来测试下,就会发现结果跟我们想的完全不一样:
public void click(View view) {
switch (view.getId()) {
case R.id.add1 :
Fragment1 fragment1 = new Fragment1();
addFragment(fragment1, "fragment1");
break;
case R.id.add2 :
Fragment2 fragment2 = new Fragment2();
addFragment(fragment2, "fragment2");
break;
case R.id.replace1 :
replaceFragment(new Fragment1(), "fragment1");
break;
case R.id.remove1 :
removeFragment("fragment1");
break;
case R.id.remove2 :
removeFragment("fragment2");
break;
}
}
我们看完上面这个图,我先是添加了一个 Fragment1,接着是 Fragment2,Fragment1,Fragment2,最后用 replace() 方法添加了 Fragment1,按照我们想得现在 ADD 队列里应该只有一个 Fragment1 才对,但是实际上并不是,ADD 队列里自顶向下依次是 Fragment1、Fragment2、Fragment2。
我们会很奇怪,不是说 replace() 不是会清空 ADD 队列吗?为什么在 remove() 的时候会发现还有多出来的 fragment 呢。看来 replace() 的工作机制和我们想得并不一样,它真正的算法其实是这样的:
for (int i=0;i<mManager.mAdded.size(); i++) {
Fragment old = mManager.mAdded.get(i);
……
mManager.removeFragment(old, mTransition, mTransitionStyle);
}
mAdded 就是 ADD 队列,i = 0 的元素是栈底,在这个例子中就是我们第一次添加的 Fragment1,可以知道它是一个一个的删除 ADD 队列中的元素,于是原因就出来了,它删除了 i = 0 的元素后,原本 i = 1 的元素它的 i 就变成了 0,但下次删除的还是第 i 个元素,于是就漏掉了一个 Fragment,删掉了第三个添加的 Fragment。
在使用 replace() 方法前我们的 ADD 队列从下往上是这样的:
fragment1 –> fragment2 –> fragment1 –> fragment2
fragment1 | fragment2 | fragment1 | fragment2 |
---|---|---|---|
0 | 1 | 2 | 3 |
已删 | 0 | 1 | 2 |
已删 | 0 | 已删 | 1 |
因为要删除下标为 i 的元素,但每删除一个,后面元素的下标都要往前移一位,所以我们的例子中 replace() 其实只删除了两个 Fragment,在第三次循环时要删除 i = 2 的 Fragment,但队列中已经没有第三个 Fragment 了。于是最后循环完了就在栈顶添加上了 Fragment1。我在 remove 的时候,大家都已经看到了,从上往下 ADD 队列的结构是: Fragment1 –> Fragment2 –>Fragment2,这和我们推理的一样。
如果你在使用了几次 add() 方法后,又使用了 replace() 方法,就可能会出现上面例子中的情况,所以 add() 和 replace() 不能共用。
3、Fragment回退栈
在前面的介绍中,我们已经提到了 Fragment 有一个 ADD 队列,但那不是 Fragment 的回退栈,ADD 队列不是有我们控制添加移除的,但回退栈我们可以决定是否将 Fragment 添加进去,有了它我们可以更好的对有数层的 Fragment 做操作,要想更直观的展现 Fragment 的回退栈,我们还要介绍两个方法:
//在commit()之前,使用addToBackStack()将fragment添加到回退栈中。
//tag是自己给的值,为了使用popBackStack(),为null是也可添加进回退栈
FragmentTransaction.addToBackStack(String tag);
//在需要回退时,使用popBackStack()将最上层的操作弹出回退栈。
FragmentManager.popBackStack();
没有参数的 popBackStack() 是弹出默认的最上层的栈顶内容。
当栈中有多层时,我们可以根据参数 id 或 TAG 标识来指定弹出到的操作所在层:
//id是当提交事务时transaction.commit()的返回值。
void popBackStack(int id, int flags);
//name是transaction.addToBackStack(String tag)中的tag值。
void popBackStack(String name, int flags);
//flags有两个取值:0或FragmentManager.POP_BACK_STACK_INCLUSIVE;
//当取值0时,表示除了参数一指定这一层之上的所有层都退出栈,指定的这一层为栈顶层;
//当取值POP_BACK_STACK_INCLUSIVE时,表示连着参数一指定的这一层一起退出栈;
我们用 addBackToStack() 把 Fragment 添加进了回退栈,那么我们在点 Back 键的时候就不会直接退出程序,而是退回到上一个 Fragment 的状态,等到把所有 Fragment 都退出栈的时候才会退出程序。(这里指得是上一次提交的事务,回退是以 commit() 提交的一次事务为单位的,而不是以其中的 add(),replace() 方法等等操作为单位回退的,比如说一次事务中一共添加了三个 Fragment,后面回退时这三个 Fragment 都会被删除。)
接着我演示了对 popBackStack() 几种方法的操作,在添加了四个 Fragment 后,点击不加参数的 popBackStack() 方法,就把最上层的 Fragment4 从栈中去掉。有参数的方法我设置的对照的 Fragment 是 Fragment1,先是 flags = 0,我们可以看到把 Fragment1 之上的 Fragment 全部出栈,最后一个就是在 Fragment1 之上包括 Fragment1 的 Fragment 出栈。
1、回退栈监听
在 FragmentManager 中还为我们提供了监控回退栈状态改变的方法:
//添加监听器
FragmentManager.addOnBackStackChangedListener(listener);
//移除监听器
FragmentManager.removeOnBackStackChangedListener(listener);
这个使用起来很简单的,首先我们在 Activity 的 onCreate() 中注册这个监听器:
private FragmentManager.OnBackStackChangedListener listener;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
FragmentManager fm = getFragmentManager();
listener = new FragmentManager.OnBackStackChangedListener() {
@Override
public void onBackStackChanged() {
Log.d("TAG", "Back State Change");
}
};
fm.addOnBackStackChangedListener(listener);
}
最后只要记得在 onDestroy() 中把监听器 remove 掉,步骤就完成啦:
@Override
protected void onDestroy() {
super.onDestroy();
FragmentManager fm = getFragmentManager();
fm.removeOnBackStackChangedListener(listener);
}
无论是把 Fragment 添加进回退栈,还是从回退栈中删除都会被监听器监听到,上面的 log 信息就是我添加了四个 Fragment 再把它们一一退出。
要注意不管是这里的回退栈的监听还是其它的监听器,在页面对应的销毁时,都要记得remove掉,不然会造成页面不释放,这也是造成OOM的问题之一。
结束语:本文仅用来学习记录,参考查阅。