监听Fragment中的回退事件

1、如何监听Fragment中的回退事件

一、问题阐述

在Activity中监听回退事件是件很是容易的事,由于直接重写onBackPressed()函数就行了,但当你们想要监听Fragment中的回退事件时,想固然的也想着重写onBackPressed()方法,这时候你会发现:Fragment中根本就没有onBackPressed()方法给你重写。这可怎么破!

想一想,在前面的例子中,咱们在Activity的一个fragment_container里依次Add进fragment1,fragment2,fragment3,fragment4,在咱们点击回退栈时,会将Transaction回退栈中的fragment操做一个个出栈!那,这些回退事件Fragment是从哪来的?

首先,回退事件老是发给Activity的!在发给Activity之后再由Activity本身处理。好比它将Fragment回退栈中的内容一个个出栈这种操做。
其次:你们要知道:Fragment只是Activity中的一个控件而已,虽然咱们可能把他作成了像Activity同样大小覆盖整个页面,看起来跟Activity样子上没什么区别,但他仍是个控件!系统怎么会给一个控件分发回退事件呢?这固然是不可能的。
html

二、解决方案

既然清楚了Fragment只是一个控件,而回退事件也只能在Activity中拦截。那咱们就能够想办法了。
首先,咱们能够在Fragment类中我们本身写一个onBackPressed()方法来处理回调事件。
而后,能够利用回调,将要处理回退事件的fragment实例,传给Activity。
最后,在拿到fragment实例之后,就能够在Activity的onBackPress()方法中,调用这个fragment实例的onBackPressed()方法了。
这样,咱们就在fragment中拦截了回退事件了。
java

三、实例

下面,咱们就经过一个例子来看下效果。
效果图以下:android


你们从下面的效果图中也能够看到,当fragment3中点击返回按钮时,捕捉了返回事件,并将fragment3上的TextView显示为”ragment3捕捉到了回退事件哦!”,但我只捕捉一次,当第二次点击时,就退出执行默认操做:即Transaction出栈。
ide

下面看下具体的实现过程:
有关MainActivity布局及fragment的添加就再也不讲了,下面直接从回调开始
一、在Fragment3中定义onBackPress()函数及处理:
函数

[java] view plain copy 在CODE上查看代码片派生到个人代码片布局

  1. public class Fragment3 extends Fragment {  测试

  2.     private boolean mHandledPress = false;  this

  3.     TextView tv;  spa

  4.       

  5.      …………  .net

  6.     @Override  

  7.     public void onActivityCreated(Bundle savedInstanceState) {  

  8.         super.onActivityCreated(savedInstanceState);  

  9.         tv = (TextView)getView().findViewById(R.id.fragment3_tv);  

  10.     }  

  11.   

  12.     public boolean onBackPressed(){  

  13.         if (!mHandledPress){  

  14.             tv.setText("Fragment3 \n 捕捉到了回退事件哦!");  

  15.             mHandledPress = true;  

  16.             return true;  

  17.         }  

  18.         return false;  

  19.     }  

  20. }  

上面的代码,没什么难度,就是定义了一个onBackPressed()函数,其返回一个布尔值;意思是,若是对返回事件进行了处理就返回TRUE,若是不作处理就返回FALSE,让上层进行处理。
变量mHandledPress用来指定只处理一次,当处理一次之后这里的onBackPressed()就返回FALSE了.
二、在Fragment3中定义回调函数,将本身实例的引用传出去
(1)、先定义一个接口用作回调,以及对应的变量:

[java] view plain copy 在CODE上查看代码片派生到个人代码片

  1. protected BackHandlerInterface backHandlerInterface;  

  2. public interface BackHandlerInterface {  

  3.     public void setSelectedFragment(Fragment3 backHandledFragment);  

  4. }  

注意,在回调中传进去的是Fragment3的实例!由于咱们要在主Activity处理onBackPress()时,调用咱们在Fragment3中本身写的onBackPressed()函数,因此咱们要传进去Fragment3的实例
(2)、而后是给backHandlerInterface变量赋值
跟上篇同样,咱们要强制Activity实现这个接口,因此咱们使用强制转换的方式来赋值。在上篇中,咱们在onAttach()函数中进行的强制转换,代码以下:

[java] view plain copy 在CODE上查看代码片派生到个人代码片

  1. public void onAttach(Activity activity) {  

  2.     super.onAttach(activity);  

  3.     try{  

  4.         backHandlerInterface = (BackHandlerInterface) getActivity();  

  5.     }catch (Exception e){  

  6.         throw new ClassCastException("Hosting activity must implement BackHandlerInterface");  

  7.     }  

  8. }  

其实在onAttach()回调时就已经把Fragment与Activity绑定在了一块儿,因此只要生命流程在onAttach()以后的任意一个生命周期,咱们均可以经过getActivity来获取Activity的实例,来进行强制转换,因此在这里咱们就换个地方,在onCreate()函数中来作:

[java] view plain copy 在CODE上查看代码片派生到个人代码片

  1. public void onCreate(Bundle savedInstanceState) {  

  2.     super.onCreate(savedInstanceState);  

  3.     if (!(getActivity() instanceof BackHandlerInterface)) {  

  4.         throw new ClassCastException("Hosting activity must implement BackHandlerInterface");  

  5.     } else {  

  6.         backHandlerInterface = (BackHandlerInterface) getActivity();  

  7.     }  

  8. }  

这里抛出异常也没有使用try...catch...来作,而是直接利用instanceof来判断当前Activity是否是BackHandlerInterface的实例,便是否已经派生了BackHandlerInterface,若是没有就直接抛异常,若是派生了就强制转换赋值。
(3)、在适当的位置将本身的实例经过回调传过去。代码以下:

[java] view plain copy 在CODE上查看代码片派生到个人代码片

  1. backHandlerInterface.setSelectedFragment(this);  

有关这个设置Fragment3实例的代码,只要在生命周期中Fragment3实例已经产生了均可以设置,便可以放在生命周期在onCreate()后的函数里,即onCreate()、onCreateView()、onActivityCreated()、onStart();虽然通过我测试,放在这几个函数中的任意一个都是可行的,但onActivityCreated()后才是Activity最终onCreate()执行完,因此放在onActivityCreated()或onStart()中是最保险的。因此这里放在了onStart()中来处理,代码以下:

[java] view plain copy 在CODE上查看代码片派生到个人代码片

  1. public void onStart() {  

  2.     super.onStart();  

  3.     backHandlerInterface.setSelectedFragment(this);  

  4. }  

因此完整的代码逻辑是这样的:

[java] view plain copy 在CODE上查看代码片派生到个人代码片

  1. public class Fragment3 extends Fragment {  

  2.     //定义回调函数及变量  

  3.     protected BackHandlerInterface backHandlerInterface;  

  4.     public interface BackHandlerInterface {  

  5.         public void setSelectedFragment(Fragment3 backHandledFragment);  

  6.     }  

  7.      

  8.     @Override  

  9.     public void onCreate(Bundle savedInstanceState) {  

  10.         super.onCreate(savedInstanceState);  

  11.         //回调函数赋值  

  12.         if(!(getActivity()  instanceof BackHandlerInterface)) {  

  13.             throw new ClassCastException("Hosting activity must implement BackHandlerInterface");  

  14.         } else {  

  15.             backHandlerInterface = (BackHandlerInterface) getActivity();  

  16.         }  

  17.     }  

  18.   

  19.     @Override  

  20.     public void onStart() {  

  21.         super.onStart();  

  22.         //将本身的实例传出去  

  23.         backHandlerInterface.setSelectedFragment(this);  

  24.     }  

  25. }  

三、在MainActivity中,回退拦截,代码以下:

[java] view plain copy 在CODE上查看代码片派生到个人代码片

  1. public class MainActivity extends FragmentActivity implements Fragment3.BackHandlerInterface {  

  2.     private Fragment3 selectedFragment;  

  3.     …………  

  4.     @Override  

  5.     public void setSelectedFragment(Fragment3 backHandledFragment) {  

  6.         this.selectedFragment = backHandledFragment;  

  7.     }  

  8.   

  9.     @Override  

  10.     public void onBackPressed() {  

  11.         if(selectedFragment == null || !selectedFragment.onBackPressed()) {  

  12.             super.onBackPressed();  

  13.         }  

  14.     }  

  15.   

  16. }  

(1)、首先,将MainActivity实现Fragment3.BackHandlerInterface接口
在这里实现setSelectedFragment()函数,代码以下:

[java] view plain copy 在CODE上查看代码片派生到个人代码片

  1. public class MainActivity extends FragmentActivity implements Fragment3.BackHandlerInterface {  

  2.     private Fragment3 selectedFragment;  

  3.     …………  

  4.     @Override  

  5.     public void setSelectedFragment(Fragment3 backHandledFragment) {  

  6.         this.selectedFragment = backHandledFragment;  

  7.     }  

  8. }  

(2)、而后在onBackPressed()回调中进行回退拦截

[java] view plain copy 在CODE上查看代码片派生到个人代码片

  1. public void onBackPressed() {  

  2.     if(selectedFragment == null || !selectedFragment.onBackPressed()) {  

  3.         super.onBackPressed();  

  4.     }  

  5. }  

注意这里的逻辑,在调用super.onBackPressed();的前提是selectedFragment.onBackPressed()返回FALSE,即Fragment3中的onBackPressed()返回FALSE,即再也不拦截回退事件,才会执行默认的操做。

源码在文章底部给出

2、执行Replace操做后,怎样保存fragment状态

首先,咱们先阐述一个现象,你们先看下面这个DEMO:


这个过程是这样的:
一、首先在Fragment1的EditText中先几个字
二、而后若是调用addFragment()添加Fragment2,而后当从fragment2返回时,发现这几个字仍是有的。
三、但若是咱们经过调用replace()添加Fragment2的话,会发现,当返回的时候,那几个字没了!

这说明了一个问题,调用addFragment添加的fragment的View会保存到视图树(ViewTree)中,其中各个控件的状态都会被保存。但若是调用replace()来添加fragment,咱们前面讲到过,replace()的实现是将同一个Container中的全部fragment视图从ViewTree中所有清空!而后再添加指定的fragment。因为repalce操做会把之前的全部视图所有清空,因此当使用Transaction回退时,也就只有重建每个fragment视图,因此就致使从replace操做回退回来,全部的控件都被重建,之前的用户输入所有没了。

到这里,你们首先要明白一个问题,repalce()操做,会清空同一个container中的全部fragment视图!注意用词:请空的是fragment的VIEW!fragment的实例并不会被销毁!由于fragment的实例是经过FragmentManager来管理的。当fragment的VIEW被销毁时,fragment实例并不会被销毁。他们两个不是同时的,即在fragment中定义的变量,所上次运行中被赋予的值是一直存在的。那fragment实例何时会被销毁呢,固然是在不会被用到的时候才会被销毁。那何时不会被用到呢,即不可能再回退到这个操做的时候,就会被销毁。
在上面的例子中,fragment1虽然被fragment2的repalce操做把它的视图给销毁了,但在执行replace操做时,将操做加入到了回退栈,这时候,FragmentManager就知道,用户还可能经过回退再次用到fragment1,因此就会保留fragment1的实例。相反,若是,在执行repalce操做时,没有加入到回退栈,那FragmentManager就确定也知道,用户不可能再回到上次那个Fragment1界面了,因此它的fragment实例就会在清除fragment1视图的同时也被清除了。

说了那么多,如今若是咱们想在利用repace操做的时候,同时保存上一个fragment界面的状态,那要怎么办?

方法一:控件状态保存与还原

上面咱们讲到,在清除Fragment视图的时候,若是咱们将操做同时加入到回退栈,那么它的VIEW虽然从ViewTree中清除了,但它的实例会被保存在FragmentManager中,那它的变量也会一直保存着,直到下次回来。但视图在回来的时候会重建。
那第一个方法来了,咱们能够用一个变量来保存EditText当前字符串,在replace前将EditText中的值保存在这个变量中,当返回来再次建立视图时,再次给EditTxt赋值不就行了。
代码以下:

[java] view plain copy 在CODE上查看代码片派生到个人代码片

  1. public class Fragment1 extends Fragment {  

  2.     private String mEditStr;  

  3.     private EditText editText;  

  4.     @Override  

  5.     public View onCreateView(LayoutInflater inflater, ViewGroup container,  

  6.             Bundle savedInstanceState) {  

  7.         View rootView = inflater.inflate(R.layout.fragment1, container, false);  

  8.         editText = (EditText)rootView.findViewById(R.id.fragment1_edittext);  

  9.         editText.setText(mEditStr);  

  10.         return rootView;  

  11.     }  

  12.   

  13.     @Override  

  14.     public void onActivityCreated(Bundle savedInstanceState) {  

  15.         super.onActivityCreated(savedInstanceState);  

  16.         Button btnReplace = (Button)getView().findViewById(R.id.fragment1_repalce);  

  17.         btnReplace.setOnClickListener(new View.OnClickListener() {  

  18.             @Override  

  19.             public void onClick(View v) {  

  20.                 mEditStr = editText.getText().toString();  

  21.                   

  22.                 …………  

  23.             }  

  24.         });  

  25.         …………  

  26.     }  

  27. }  

上面的代码老是就是两步:
第一步:在repalce前保存状态

[java] view plain copy 在CODE上查看代码片派生到个人代码片

  1. mEditStr = editText.getText().toString();  

第二步:在建立时还原状态

[java] view plain copy 在CODE上查看代码片派生到个人代码片

  1. editText.setText(mEditStr);  

这虽然能完成工做,但若是咱们的控件很是多呢?内容很是复杂呢?这将不是一个好办法。由于不少变量的初始化及赋值将会使代码看的异常丑陋难懂。

方法二:只须要为控件添加ID值

在实时中还遇到一个解决方法,就是给EditText控件添加上id,只要给EditText控件添加上id,不须要上面的那些replace前的值的保存即建立时的还原,它的内容就会被保存。不知道其它控件是否也能够经过添加ID值的方式来保存用户的输入值,即:

[html] view plain copy 在CODE上查看代码片派生到个人代码片

  1. <EditText  

  2.     android:id="@+id/fragment1_edittext"  

  3.     android:layout_width="match_parent"  

  4.     android:layout_height="match_parent"  

  5.     android:gravity="top|left"  

  6.     android:background="#ffffff"  

  7.     android:hint="这里是EditText,在这里输入文字哦"/>  

方法三:保存FragmentView视图

方法一和方法二感受都仍是太靠谱的解决方法,既然fragment中的变量都会被保存,那咱们直接将Fragment的视图直接保存到变量中,在系统在利用onCreateView()建立视图的时候,咱们直接返回保存的视图不就得了。
基于上面的想法,代码上咱们这样作:
一、建立一个变量,保存Fragment的视图:

[java] view plain copy 在CODE上查看代码片派生到个人代码片

  1. private View rootView;  

二、而后来看onCreateView的实现

[java] view plain copy 在CODE上查看代码片派生到个人代码片

  1. public View onCreateView(LayoutInflater inflater, ViewGroup container,  

  2.         Bundle savedInstanceState) {  

  3.     return getPersistentView(inflater, container, savedInstanceState, R.layout.fragment1);  

  4. }  

能够看到,相比之前直接返回inflater.inflate(R.layout.fragment1, container,false);重建视图,这里返回的是一个getPersistentView()函数,下面看看这个函数的实现:

[java] view plain copy 在CODE上查看代码片派生到个人代码片

  1. public View getPersistentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState, int layout) {  

  2.     if (rootView == null) {  

  3.         // Inflate the layout for this fragment  

  4.         rootView = inflater.inflate(layout, container,false);  

  5.     } else {  

  6.         ((ViewGroup) rootView.getParent()).removeView(rootView);  

  7.     }  

  8.     return rootView;  

  9. }  

这段代码就是返回rootView的。即当rootView==null,即第一次建立时,就利用inflater.inflate()来建立初始化状态的视图,当下次再进到这个界面时,好比下面的经过回退操做进入到fragment1时,这时候的rootView就再也不是空了。但在onCreateView()中返回的视图是要添加到ViewTree中去的。而这里的rootView视图在上次已经添加到里面去了,一个视图实例不能被add两次,否则就会被下面这个错误!因此,咱们针对这种状况,若是rootView已经存在于ViewTree中的时候,要先从ViewTree中移除。

好了,到这里就讲完了,源码都会在下面给出。下面先来看看最终的效果图吧:

相关文章
相关标签/搜索