在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 佈局
public class Fragment3 extends Fragment { 測試
private boolean mHandledPress = false; this
TextView tv; spa
………… .net
@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了.
二、在Fragment3中定義回調函數,將本身實例的引用傳出去
(1)、先定義一個接口用作回調,以及對應的變量:
[java] view plain copy
protected BackHandlerInterface backHandlerInterface;
public interface BackHandlerInterface {
public void setSelectedFragment(Fragment3 backHandledFragment);
}
注意,在回調中傳進去的是Fragment3的實例!由於咱們要在主Activity處理onBackPress()時,調用咱們在Fragment3中本身寫的onBackPressed()函數,因此咱們要傳進去Fragment3的實例
(2)、而後是給backHandlerInterface變量賦值
跟上篇同樣,咱們要強制Activity實現這個接口,因此咱們使用強制轉換的方式來賦值。在上篇中,咱們在onAttach()函數中進行的強制轉換,代碼以下:
[java] view plain copy
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()函數中來作:
[java] view plain copy
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)、在適當的位置將本身的實例經過回調傳過去。代碼以下:
[java] view plain copy
backHandlerInterface.setSelectedFragment(this);
有關這個設置Fragment3實例的代碼,只要在生命週期中Fragment3實例已經產生了均可以設置,便可以放在生命週期在onCreate()後的函數裏,即onCreate()、onCreateView()、onActivityCreated()、onStart();雖然通過我測試,放在這幾個函數中的任意一個都是可行的,但onActivityCreated()後纔是Activity最終onCreate()執行完,因此放在onActivityCreated()或onStart()中是最保險的。因此這裏放在了onStart()中來處理,代碼以下:
[java] view plain copy
public void onStart() {
super.onStart();
backHandlerInterface.setSelectedFragment(this);
}
因此完整的代碼邏輯是這樣的:
[java] view plain copy
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);
}
}
三、在MainActivity中,回退攔截,代碼以下:
[java] view plain copy
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()函數,代碼以下:
[java] view plain copy
public class MainActivity extends FragmentActivity implements Fragment3.BackHandlerInterface {
private Fragment3 selectedFragment;
…………
@Override
public void setSelectedFragment(Fragment3 backHandledFragment) {
this.selectedFragment = backHandledFragment;
}
}
(2)、而後在onBackPressed()回調中進行回退攔截
[java] view plain copy
public void onBackPressed() {
if(selectedFragment == null || !selectedFragment.onBackPressed()) {
super.onBackPressed();
}
}
注意這裏的邏輯,在調用super.onBackPressed();的前提是selectedFragment.onBackPressed()返回FALSE,即Fragment3中的onBackPressed()返回FALSE,即再也不攔截回退事件,纔會執行默認的操做。
源碼在文章底部給出
首先,咱們先闡述一個現象,你們先看下面這個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
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前保存狀態
[java] view plain copy
mEditStr = editText.getText().toString();
第二步:在建立時還原狀態
[java] view plain copy
editText.setText(mEditStr);
這雖然能完成工做,但若是咱們的控件很是多呢?內容很是複雜呢?這將不是一個好辦法。由於不少變量的初始化及賦值將會使代碼看的異常醜陋難懂。
在實時中還遇到一個解決方法,就是給EditText控件添加上id,只要給EditText控件添加上id,不須要上面的那些replace前的值的保存即建立時的還原,它的內容就會被保存。不知道其它控件是否也能夠經過添加ID值的方式來保存用戶的輸入值,即:
[html] view plain copy
<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,在這裏輸入文字哦"/>
方法一和方法二感受都仍是太靠譜的解決方法,既然fragment中的變量都會被保存,那咱們直接將Fragment的視圖直接保存到變量中,在系統在利用onCreateView()建立視圖的時候,咱們直接返回保存的視圖不就得了。
基於上面的想法,代碼上咱們這樣作:
一、建立一個變量,保存Fragment的視圖:
[java] view plain copy
private View rootView;
二、而後來看onCreateView的實現
[java] view plain copy
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()函數,下面看看這個函數的實現:
[java] view plain copy
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中移除。
好了,到這裏就講完了,源碼都會在下面給出。下面先來看看最終的效果圖吧: