一、如何監聽Fragment中的回退事件
1、問題闡述
在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樣子上沒什么區別,但他還是個控件!系統怎么會給一個控件分發回退事件呢?這當然是不可能的。
2、解決方案
既然清楚了Fragment只是一個控件,而回退事件也只能在Activity中攔截。那我們就可以想辦法了。
首先,我們可以在Fragment類中咱們自己寫一個onBackPressed()方法來處理回調事件。
然后,可以利用回調,將要處理回退事件的fragment實例,傳給Activity。
最后,在拿到fragment實例以后,就可以在Activity的onBackPress()方法中,調用這個fragment實例的onBackPressed()方法了。
這樣,我們就在fragment中攔截了回退事件了。
3、實例
下面,我們就通過一個例子來看下效果。
效果圖如下:

大家從下面的效果圖中也可以看到,當fragment3中點擊返回按鈕時,捕捉了返回事件,並將fragment3上的TextView顯示為”ragment3捕捉到了回退事件哦!”,但我只捕捉一次,當第二次點擊時,就退出執行默認操作:即Transaction出棧。
下面看下具體的實現過程:
有關MainActivity布局及fragment的添加就不再講了,下面直接從回調開始
1、在Fragment3中定義onBackPress()函數及處理:
public class Fragment3 extends Fragment {
private boolean mHandledPress = false;
TextView tv;
…………
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
tv = (TextView)getView().findViewById(R.id.fragment3_tv);
}
public boolean onBackPressed(){
if (!mHandledPress){
tv.setText("Fragment3 \n 捕捉到了回退事件哦!");
mHandledPress = true;
return true;
}
return false;
}
}
上面的代碼,沒什么難度,就是定義了一個onBackPressed()函數,其返回一個布爾值;意思是,如果對返回事件進行了處理就返回TRUE,如果不做處理就返回FALSE,讓上層進行處理。
變量mHandledPress用來指定只處理一次,當處理一次以后這里的onBackPressed()就返回FALSE了.
2、在Fragment3中定義回調函數,將自己實例的引用傳出去
(1)、先定義一個接口用做回調,以及對應的變量:
protected BackHandlerInterface backHandlerInterface;
public interface BackHandlerInterface {
public void setSelectedFragment(Fragment3 backHandledFragment);
}
注意,在回調中傳進去的是Fragment3的實例!因為我們要在主Activity處理onBackPress()時,調用我們在Fragment3中自己寫的onBackPressed()函數,所以我們要傳進去Fragment3的實例
(2)、然后是給backHandlerInterface變量賦值
跟上篇一樣,我們要強制Activity實現這個接口,所以我們使用強制轉換的方式來賦值。在上篇中,我們在onAttach()函數中進行的強制轉換,代碼如下:
public void onAttach(Activity activity) {
super.onAttach(activity);
try{
backHandlerInterface = (BackHandlerInterface) getActivity();
}catch (Exception e){
throw new ClassCastException("Hosting activity must implement BackHandlerInterface");
}
}
其實在onAttach()回調時就已經把Fragment與Activity綁定在了一起,所以只要生命流程在onAttach()之后的任意一個生命周期,我們都可以通過getActivity來獲取Activity的實例,來進行強制轉換,所以在這里我們就換個地方,在onCreate()函數中來做:
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (!(getActivity() instanceof BackHandlerInterface)) {
throw new ClassCastException("Hosting activity must implement BackHandlerInterface");
} else {
backHandlerInterface = (BackHandlerInterface) getActivity();
}
}
這里拋出異常也沒有使用try...catch...來做,而是直接利用instanceof來判斷當前Activity是不是BackHandlerInterface的實例,即是否已經派生了BackHandlerInterface,如果沒有就直接拋異常,如果派生了就強制轉換賦值。
(3)、在適當的位置將自己的實例通過回調傳過去。代碼如下:
backHandlerInterface.setSelectedFragment(this);
有關這個設置Fragment3實例的代碼,只要在生命周期中Fragment3實例已經產生了都可以設置,即可以放在生命周期在onCreate()后的函數里,即onCreate()、onCreateView()、onActivityCreated()、onStart();雖然經過我測試,放在這幾個函數中的任意一個都是可行的,但onActivityCreated()后才是Activity最終onCreate()執行完,所以放在onActivityCreated()或onStart()中是最保險的。所以這里放在了onStart()中來處理,代碼如下:
public void onStart() {
super.onStart();
backHandlerInterface.setSelectedFragment(this);
}
所以完整的代碼邏輯是這樣的:
public class Fragment3 extends Fragment {
//定義回調函數及變量
protected BackHandlerInterface backHandlerInterface;
public interface BackHandlerInterface {
public void setSelectedFragment(Fragment3 backHandledFragment);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//回調函數賦值
if(!(getActivity() instanceof BackHandlerInterface)) {
throw new ClassCastException("Hosting activity must implement BackHandlerInterface");
} else {
backHandlerInterface = (BackHandlerInterface) getActivity();
}
}
@Override
public void onStart() {
super.onStart();
//將自己的實例傳出去
backHandlerInterface.setSelectedFragment(this);
}
}
3、在MainActivity中,回退攔截,代碼如下:
public class MainActivity extends FragmentActivity implements Fragment3.BackHandlerInterface {
private Fragment3 selectedFragment;
…………
@Override
public void setSelectedFragment(Fragment3 backHandledFragment) {
this.selectedFragment = backHandledFragment;
}
@Override
public void onBackPressed() {
if(selectedFragment == null || !selectedFragment.onBackPressed()) {
super.onBackPressed();
}
}
}
(1)、首先,將MainActivity實現Fragment3.BackHandlerInterface接口
在這里實現setSelectedFragment()函數,代碼如下:
public class MainActivity extends FragmentActivity implements Fragment3.BackHandlerInterface {
private Fragment3 selectedFragment;
…………
@Override
public void setSelectedFragment(Fragment3 backHandledFragment) {
this.selectedFragment = backHandledFragment;
}
}
(2)、然后在onBackPressed()回調中進行回退攔截
public void onBackPressed() {
if(selectedFragment == null || !selectedFragment.onBackPressed()) {
super.onBackPressed();
}
}
注意這里的邏輯,在調用super.onBackPressed();的前提是selectedFragment.onBackPressed()返回FALSE,即Fragment3中的onBackPressed()返回FALSE,即不再攔截回退事件,才會執行默認的操作。
二、執行Replace操作后,怎樣保存fragment狀態
首先,我們先闡述一個現象,大家先看下面這個DEMO:

這個過程是這樣的:
1、首先在Fragment1的EditText中先幾個字
2、然后如果調用addFragment()添加Fragment2,然后當從fragment2返回時,發現這幾個字還是有的。
3、但如果我們通過調用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賦值不就好了。
代碼如下:
public class Fragment1 extends Fragment {
private String mEditStr;
private EditText editText;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment1, container, false);
editText = (EditText)rootView.findViewById(R.id.fragment1_edittext);
editText.setText(mEditStr);
return rootView;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
Button btnReplace = (Button)getView().findViewById(R.id.fragment1_repalce);
btnReplace.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditStr = editText.getText().toString();
…………
}
});
…………
}
}
上面的代碼總是就是兩步:
第一步:在repalce前保存狀態
mEditStr = editText.getText().toString();
第二步:在創建時還原狀態
editText.setText(mEditStr);
這雖然能完成工作,但如果我們的控件非常多呢?內容非常復雜呢?這將不是一個好辦法。因為很多變量的初始化及賦值將會使代碼看的異常丑陋難懂。
方法二:只需要為控件添加ID值
在實時中還遇到一個解決方法,就是給EditText控件添加上id,只要給EditText控件添加上id,不需要上面的那些replace前的值的保存即創建時的還原,它的內容就會被保存。不知道其它控件是否也可以通過添加ID值的方式來保存用戶的輸入值,即:
<EditText
android:id="@+id/fragment1_edittext"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="top|left"
android:background="#ffffff"
android:hint="這里是EditText,在這里輸入文字哦"/>
方法三:保存FragmentView視圖
方法一和方法二感覺都還是太靠譜的解決方法,既然fragment中的變量都會被保存,那我們直接將Fragment的視圖直接保存到變量中,在系統在利用onCreateView()創建視圖的時候,我們直接返回保存的視圖不就得了。
基於上面的想法,代碼上我們這樣做:
1、創建一個變量,保存Fragment的視圖:
private View rootView;
2、然后來看onCreateView的實現
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return getPersistentView(inflater, container, savedInstanceState, R.layout.fragment1);
}
可以看到,相比以前直接返回inflater.inflate(R.layout.fragment1, container,false);重建視圖,這里返回的是一個getPersistentView()函數,下面看看這個函數的實現:
public View getPersistentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState, int layout) {
if (rootView == null) {
// Inflate the layout for this fragment
rootView = inflater.inflate(layout, container,false);
} else {
((ViewGroup) rootView.getParent()).removeView(rootView);
}
return rootView;
}
這段代碼就是返回rootView的。即當rootView==null,即第一次創建時,就利用inflater.inflate()來創建初始化狀態的視圖,當下次再進到這個界面時,比如下面的通過回退操作進入到fragment1時,這時候的rootView就不再是空了。但在onCreateView()中返回的視圖是要添加到ViewTree中去的。而這里的rootView視圖在上次已經添加到里面去了,一個視圖實例不能被add兩次,不然就會被下面這個錯誤!所以,我們針對這種情況,如果rootView已經存在於ViewTree中的時候,要先從ViewTree中移除。
好了,到這里就講完了,源碼都會在下面給出。下面先來看看最終的效果圖吧:

