監聽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中移除。

好了,到這裏就講完了,源碼都會在下面給出。下面先來看看最終的效果圖吧:

相關文章
相關標籤/搜索