Fragment 用法总结(三)
前两篇文章主要讲解Fragment的创建及基本用法、生命周期,本文主要讲Fragment的高级用法。
保存屏幕旋转后的Fragment实例
屏幕旋转或者后台任务返回到前台都可能引起Activity重新启动,例如在Activity中使用下面的代码创建FragmentA:
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
fragmentA = new FragmentA();
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
fragmentTransaction.add(R.id.content_main, fragmentA).commit();
}
运行后旋转手机,每次横竖切换屏幕都会生成一个FragmentA实例。
日志输出如下图:
这是由于Activity重新创建的时候会恢复它的界面,而重新创建又会调用Activity的生命周期方法onCreate()
,因为我们在里面又创建了一个新的Fragment,所以会一直重叠下去。
这个问题官方的解决办法如下:
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (savedInstanceState == null) {
fragmentA = new FragmentA();
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
fragmentTransaction.add(R.id.content_main, fragmentA).commit();
}
}
如果不想Activity重新创建的时候重新创建Fragment,则可以在Fragment构造函数或者onCreate()
方法里调用setRetainInstance(true)
保持Fragment。
通常我们在事物里add或者replace添加Fragment对象到Activity的时候会将Fragment的实例保存到Activity的成员变量里,但是在横竖屏幕切换时候会重新创建Activity,那些Fragment的成员变量也会变空,如果接下来再执行hide或者detach当前Fragment就会出现空指针异常。
这个问题可以用下面的方法解决:
private FragmentA fragmentA;
private FragmentB fragmentB;
private FragmentC fragmentC;
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
fragmentA = (FragmentA) getFragmentManager().findFragmentByTag("A");
fragmentB = (FragmentB) getFragmentManager().findFragmentByTag("B");
fragmentC = (FragmentC) getFragmentManager().findFragmentByTag("C");
if (fragmentA == null) {
fragmentA = new FragmentA();
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
fragmentTransaction.add(R.id.content_main, fragmentA, "A").commit();
}
}
向操作栏添加项目
Fragment可以通过实现 onCreateOptionsMenu()
向 Activity 的选项菜单(并因此向操作栏)贡献菜单项。不过,为了使此方法能够收到调用,必须在 onCreate()
期间调用 setHasOptionsMenu()
,以指示Fragment想要向选项菜单添加菜单项(否则,Fragment将不会收到对 onCreateOptionsMenu()
的调用)。
public MenuFragment() {
setHasOptionsMenu(true);
}
@Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.menu_fragment, menu);
}
之后从Fragment添加到选项菜单的任何菜单项都将追加到现有菜单项之后。 选定菜单项时,Fragment还会收到对 onOptionsItemSelected()
的回调。
@Override public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_fragment_menu1:
Toast.makeText(getActivity(), "Fragment menu1", Toast.LENGTH_SHORT).show();
return true;
case R.id.action_fragment_menu2:
Toast.makeText(getActivity(), "Fragment menu2", Toast.LENGTH_SHORT).show();
return true;
}
return super.onOptionsItemSelected(item);
}
还可以通过调用 registerForContextMenu()
,在Fragment布局中注册一个视图来提供上下文菜单。用户打开上下文菜单时,Fragment会收到对 onCreateContextMenu()
的调用。当用户选择某个菜单项时,Fragment会收到对 onContextItemSelected()
的调用。
注:尽管Fragment会收到与其添加的每个菜单项对应的菜单项选定回调,但当用户选择菜单项时,Activity 会首先收到相应的回调。 如果 Activity 对菜单项选定回调的实现不会处理选定的菜单项,则系统会将事件传递到Fragment的回调。 这适用于选项菜单和上下文菜单。
DialogFragment
DialogFragment 类提供创建对话框和管理其外观所需的所有控件,而不是调用 Dialog 对象上的方法。将 DialogFragment 与 AlertDialog 对象结合使用是google官方推荐做法。这里我用几行代码将ProgressDialog 和DialogFragment 结合定义了一个简单的加载对话框。
public class ProgressDialogFragment extends DialogFragment {
@Override public Dialog onCreateDialog(Bundle savedInstanceState) {
ProgressDialog progressDialog = new ProgressDialog(getActivity());
progressDialog.setTitle("加载中...");
return progressDialog;
}
}
特别简单吧,而且使用 DialogFragment 管理对话框可确保它能正确处理生命周期事件,如用户按“返回”按钮或旋转屏幕时。
这里有google官方推荐对话框:
http://developer.android.com/guide/topics/ui/dialogs.html
http://developer.android.com/reference/android/app/DialogFragment.html
配置变更期间保留对象
没有布局的Fragment的作用,在配置变更期间保留对象,如果重启 Activity 需要恢复大量数据、重新建立网络连接或执行其他密集操作,那么因配置变更而引起的完全重启可能会给用户留下应用运行缓慢的体验。 此外,依靠系统通过 onSaveInstanceState() 保存的 Bundle,可能无法完全恢复 Activity 状态,因为它 并非设计用于携带大型对象(例如位图),而且其中的数据必须先序列化,再进行反序列化, 这可能会消耗大量内存并使得配置变更速度缓慢。在这种情况下,如果 Activity 因配置变更而重启,则可通过保留 Fragment 来减轻重新初始化 Activity 的负担。这个Fragment可包含要保留的所有状态对象的引用。
当 Android 系统因配置变更而关闭 Activity 时,不会销毁已标记为要保留的 Activity 的Fragment。我们可以将此类Fragment添加到 Activity 以保留有状态的对象。
注意:尽管Fragment可以存储任何对象,但是请不要传递与 Activity 绑定的对象,例如,Drawable、Adapter、View 或其他任何与 Context 关联的对象。否则,它将泄漏原始 Activity 实例的所有视图和资源。 (泄漏资源意味着应用将继续持有这些资源,但是无法对其进行垃圾回收,因此可能会丢失大量内存。)
要在运行时配置变更期间将有状态的对象保留在Fragment中,请执行以下操作:
- 扩展 Fragment 类并声明对有状态对象的引用。
- 在创建Fragment后调用 setRetainInstance(boolean)。onCreate只会执行一次。onDestroy不会再执行。
- 将Fragment添加到 Activity。
- 重启 Activity 后,使用 FragmentManager 检索Fragment。
注意不能将这个Fragment添加到返回栈中,如果被popstack,Activity在运行时配置变更后重新创建,则这个Fragment也会重新创建,数据就会丢失。
下面的例子,通过网络加载图片,并在加载过程中横竖切换屏幕,通过RetainedFragment来保存位图Bitmap。
FragmentRetainDataActivity.java
public class FragmentRetainDataActivity extends Activity implements RetainedFragment.LoadComplete {
private RetainedFragment retainedFragment;
private ProgressDialogFragment progressDialog;
@Bind(R.id.iv_pic) ImageView ivPic;
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.a_fragment_retain_data);
ButterKnife.bind(this);
// find the retained fragment on activity restarts
retainedFragment = (RetainedFragment) getFragmentManager().findFragmentByTag("data");
progressDialog = (ProgressDialogFragment) getFragmentManager().findFragmentByTag("dialog");
// create the fragment and data the first time
if (retainedFragment == null) {
// add the fragment
retainedFragment = new RetainedFragment();
getFragmentManager().beginTransaction().add(retainedFragment, "data").commit();
} else {
ivPic.setImageBitmap(retainedFragment.getPic());//注掉这行后横竖屏切换后图片消失
}
}
@OnClick(R.id.iv_pic) void picClick() {
if (ivPic.getDrawable() != null) return;
progressDialog = new ProgressDialogFragment();
progressDialog.show(getFragmentManager(), "dialog");
retainedFragment.loadPic("https://img-blog.youkuaiyun.com/20160307170349803");
}
@Override protected void onDestroy() {
super.onDestroy();
ButterKnife.unbind(this);
}
@Override public void onComplete() {
runOnUiThread(new Runnable() {
@Override public void run() {
ivPic.setImageBitmap(retainedFragment.getPic());
progressDialog.dismiss();
}
});
}
}
RetainedFragment.java
public class RetainedFragment extends BaseFragment {
private Bitmap mBitmap;
private LoadComplete loadComplete;
public interface LoadComplete {
public void onComplete();
}
public RetainedFragment() {
// Required empty public constructor
setRetainInstance(true);
}
@Override public void onAttach(Activity activity) {
super.onAttach(activity);
try {
if (activity instanceof LoadComplete) {
loadComplete = (LoadComplete) activity;
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override public void onDetach() {
super.onDetach();
loadComplete = null;
}
public void loadPic(String url) {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url(url).build();
client.newCall(request).enqueue(new Callback() {
@Override public void onFailure(Call call, IOException e) {
}
@Override public void onResponse(Call call, Response response) throws IOException {
final Bitmap bitmap = BitmapFactory.decodeStream(response.body().byteStream());
try {
Thread.sleep(3000);
mBitmap = bitmap;
if (loadComplete != null) {
loadComplete.onComplete();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
public Bitmap getPic() {
return mBitmap;
}
}
效果如下:
上面使用FragmentDialog和RetainedFragment在屏幕横竖切换的时候完美解决了加载状态和数据保留的问题。这也是google官方推荐处理运行时变化的方法:
http://developer.android.com/guide/topics/resources/runtime-changes.html