Fragment

稀飯_發表於2018-06-03

涵蓋知識點

  • show() hide() 和 attach()detach()和 attach()detach()方法的比較
  • fragment回退棧

      如果上述都會了,可以立馬關掉這個網頁,向你說一聲:對不起,打擾了!如果不會,可以接著往下看。

一.前言

       誠惶誠恐,懷著羞愧的心態寫下這篇文章,羞愧的原因在於本應在三年前會的知識,現在才注意到,我可能是個假程式設計師!於是有了這篇比較偏向基礎知識的文章,以此文來踐踏我的自尊心,進而促使我更大的進步。

二.Fragment

       Fragment中文意思是碎片,我理解的碎片:替代activity的輕量級元件。

三.Fragment生命週期方法

Fragment生命週期有11個方法之多,這裡就對幾個具有標誌性的方法進行解釋:

在這裡我把fragment比做一個裝控制元件的箱子

  • onAttoch方法被呼叫時候:表示Activity對Fragment進行了掛載,掛載就是:activity中fragmentManage持有這fragment物件,並對fragment進行管理。類似建立一個箱子並管理他
  • onDetach方法被呼叫的時候:表示Activity對Fragment取消了掛載,取消掛載就是:fragmentManage放棄持有這fragment物件,並不對fragment進行管理。類似拋棄這個箱子
  • onCreatView方法被呼叫的時候,表示為Fragment新增布局檢視,新增布局檢視:佈局設定給Fragment,同時fragment的檢視新增到顯示的Activity的主檢視樹中。類似向箱子裡邊放控制元件,箱子裡的控制元件掛載到activity的主檢視中,至於箱子是否是新建立的,取決於是否走了onAttoch方法
  • onDetoryView方法被呼叫的時候,表示把Fragment中的檢視(佈局)移除並取消檢視(佈局)和activity主檢視樹(佈局)的關聯。類似把箱子裡邊的控制元件都扔了,但是箱子還留著,至於箱子最後仍不扔,取決於最後是否走了onDetach方法

這樣類比可能還是比較抽象,我們通過程式碼log去測試上邊的方法

      在activity中去操作fragment有兩種方式:靜態載入和動態載入,我們這裡為了比較這些方法對fragment的生命週期的影響,我們這裡只使用動態載入。

activity中:

public class MainActivity extends AppCompatActivity {

    private TestFragment testFragment;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        testFragment = new TestFragment();
        //按鈕一點選事件
        findViewById(R.id.btn1).setOnClickListener(v -> {});

        //按鈕二點選事件
        findViewById(R.id.btn2).setOnClickListener(v -> {});

    }
}複製程式碼

fragment中寫下幾個重要的生命週期的方法,並列印對應方法名字:

public class TestFragment extends Fragment {

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        Log.e("rrrrrrrr","onAttach");
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.e("rrrrrrrr","onCreate");
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        Log.e("rrrrrrrr","onCreateView");
        return inflater.inflate(R.layout.fragment_test,container,false);
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        Log.e("rrrrrrrr","onDestroyView");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.e("rrrrrrrr","onDestroy");
    }

    @Override
    public void onDetach() {
        super.onDetach();
        Log.e("rrrrrrrr","onDetach");
    }
}複製程式碼

測試一:當在按鈕一和按鈕二中寫下如下程式碼:

testFragment = new TestFragment();
//按鈕一點選事件
findViewById(R.id.btn1).setOnClickListener(v -> {
   getSupportFragmentManager().beginTransaction().add(R.id.rongqi, testFragment).commit();

});

//按鈕二點選事件
findViewById(R.id.btn2).setOnClickListener(v -> {
    getSupportFragmentManager().beginTransaction().remove(testFragment).commit();
});複製程式碼

分別點選按鈕列印日誌如下:

點選新增按鈕:

rrrrrrrr: onAttach
rrrrrrrr: onCreate
rrrrrrrr: onCreateView 複製程式碼

點選移除按鈕:

rrrrrrrr: onDestroyView
rrrrrrrr: onDestroy
rrrrrrrr: onDetach複製程式碼

這裡就好比每次都去建立一個新的箱子,並且去放入新的控制元件


測試二:當在按鈕一和按鈕二中寫下如下程式碼:

testFragment = new TestFragment();
getSupportFragmentManager().beginTransaction().add(R.id.rongqi, testFragment).commit();
//按鈕一點選事件
findViewById(R.id.btn1).setOnClickListener(v -> {
    getSupportFragmentManager().beginTransaction().detach(testFragment).commit();
});

//按鈕二點選事件
findViewById(R.id.btn2).setOnClickListener(v -> {
    getSupportFragmentManager().beginTransaction().attach(testFragment).commit();
});複製程式碼

點選按鈕分別列印日誌:

rrrrrrrr: onDestroyView複製程式碼
rrrrrrrr: onCreateView 複製程式碼

這就好比每次箱子裡邊的控制元件都是新建立的,但是箱子還是老箱子


當在按鈕一和按鈕二中寫下如下程式碼:

testFragment = new TestFragment();
getSupportFragmentManager().beginTransaction().add(R.id.rongqi, testFragment).commit();
//按鈕一點選事件
findViewById(R.id.btn1).setOnClickListener(v -> {
    getSupportFragmentManager().beginTransaction().hide(testFragment).commit();
});

//按鈕二點選事件
findViewById(R.id.btn2).setOnClickListener(v -> {
    getSupportFragmentManager().beginTransaction().show(testFragment).commit();
});
複製程式碼

點選按鈕沒有日誌輸出

這就好比,箱子和箱子裡邊的控制元件都是老的,只是把它隱藏起來了而已

但對於介面對使用者展示而言都是看不見了,又看見了

比較三對不同的操作,對fragment生命週期呼叫完全不一樣:

  • 當show和hide的時候,沒有呼叫任何生命週期方法
  • 當add()和remove() 的時候,走了從onAttach到onDetach的整個生命週期方法
  • 當attach()和detach()的時候,走了從onCreateView到 onDestroyView的一段生命週期方法

而對於程式而言,持有相同的物件和不同的物件是完全不同的概念,所以我們要根據不同的需求去選擇呼叫不同的方法去顯示和隱藏fragment。

四. 對於Fragment巢狀在Viewpager中的需求

預設情況下,超過預載入的fragment就會走到onDestroyView,每次回來,裡邊的控制元件都會建立新的,但是fragment不會建立新的。

1.假如需求是要求不去建立新的控制元件,我們可以通過設定viewpagr的預載入數量,完美解決這個不去建立新的控制元件需求

2.假如需求是不要求建立的新控制元件且每次呈現再使用者面前都去重新整理,第一,我們去設定預載入數量,要求使用老的控制元件,第二系統給我們提供結合Viewpager使用的新方法,當在使用者面前顯示的時候都會呼叫,getUserVisibleHint方法,顯示時候返回true,通過這個方法,可以讓頁面每次呈現在使用者面前都去重新整理,再載入一個是否已經請求的標記,可以實現懶載入效果。值得注意的是,該方法不是生命週期方法,且只有再結合viewpager時候才會被呼叫,檢視原始碼可以看出該方法是viewpger手動呼叫的。

五.fragment回退棧

在android系統中,A頁面跳轉到B頁面,然後我們按返回鍵,就又回到頁面A,我們知道這個效果是因為系統給我們新增了棧資料結構去管理所有開啟的頁面。

那麼在一個Activity中如果我們依次新增了Fragment1,Fragment2,Fragment3,然後什麼也不處理的情況下,我們點選返回鍵,直接就關閉了Activity,那麼我們能不能像activity一樣,點選返回鍵,依次推出fragment,最後再關閉頁面呢?當然是可以的!這就是fragment的回退棧。

  • addToBackStack(tag); 將Fragment新增到回退棧中
  • popBackStack(); 清除回退棧中棧頂的Fragment
  • popBackStack(String tag, int i );
    • 如果i=0,回退到該tag所對應的Fragment層
    • 如果i=FragmentManager.POP_BACK_STACK_INCLUSIVE,回退到該tag所對應的Fragment的上一層
  • popBackStackImmediate 立即清除回退棧中棧頂Fragment
  • getBackStackEntryCount(); 獲取回退棧中Fragment的個數
  • getBackStackEntryAt(int index) 獲取回退棧中該索引值下的Fragment


通過以上方法,可以實現Fragment的回退棧效果。下邊用程式碼去講解回退棧效果:


private OneFragment one;
private TwoFragment two;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    one = new OneFragment();
    two = new TwoFragment();
findViewById(R.id.btn2).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        getSupportFragmentManager().beginTransaction().add(R.id.fragment, one, "oneFragment").addToBackStack("oneFragment").commit();
    }
});

findViewById(R.id.btn3).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        getSupportFragmentManager().beginTransaction().replace(R.id.fragment, two, "TwoFragment").addToBackStack("TwoFragment").commit();
    }
});
}

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
    // 判斷當前按鍵是返回鍵
    if (keyCode == KeyEvent.KEYCODE_BACK) {
        // 獲取當前回退棧中的Fragment個數
        int backStackEntryCount = getSupportFragmentManager().getBackStackEntryCount();
        // 回退棧中至少有多個fragment,棧底部是首頁
        if (backStackEntryCount > 1) {
            getSupportFragmentManager().popBackStackImmediate();
        } else {
            finish();
        }

    }
    return true;
}複製程式碼


如上程式碼我們點選返回鍵,實現了點選返回,退出到第一個Fragment中,實現了需求。細心的朋友可以發現我這裡用了replace方法去處理了第二個fragment,我們都知道replace方法是remove方法和add方法的合體,按照之前測試一的log,我們猜測,oneFragment應該走完所有的生命週期方法,並且解除被Activity的管理,事實真的是這樣嗎?我們去測試一下:

觀察oneFragment的log如下:

點選按鈕一:

rrrrrrrrOne: onAttach

rrrrrrrrOne: onCreate

rrrrrrrrOne: onCreateView複製程式碼

點選按鈕二:

rrrrrrrrOne: onDestroyView複製程式碼

驚訝射的滿臉都是,在仔細一想,合理,因為我新增了回退棧,如果真的移除了,還怎麼管理!

合理,合理,非常合理!


可惜的是,上邊程式碼沒有切換的動畫效果!沒有動畫效果?不可能!接下來介紹一個方法setCustomAnimations

這個方法有兩個引數和4個引數的過載,這裡因為有回退棧,就說一下4個引數的:第一個引數和第二個引數是設定Fragment進入和退出的動畫效果,第三個和第四個是設定Fragment回退棧的動畫效果,程式碼如下:

在res下建立anim資料夾,然後建立四個動畫:

enter:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="1000"
        android:fromXDelta="100.0%p"
        android:toXDelta="0.0" />
</set>複製程式碼

out:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="1000"
        android:fromXDelta="0.0"
        android:toXDelta="-100.0%p" />
</set>複製程式碼


pop_Enter:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="1000"
        android:fromXDelta="-100.0%p"
        android:toXDelta="0.0" />
</set>複製程式碼

pop_out:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="1000"
        android:fromXDelta="0.0"
        android:toXDelta="100.0%p" />
</set>複製程式碼

通過manager,就可以設定動畫,程式碼如下:

findViewById(R.id.btn2).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        getSupportFragmentManager().beginTransaction().setCustomAnimations(R.anim.enter, R.anim.out, R.anim.proenter, R.anim.proout).add(R.id.fragment, one, "oneFragment").addToBackStack("oneFragment").commit();
    }
});

findViewById(R.id.btn3).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        getSupportFragmentManager().beginTransaction().setCustomAnimations(R.anim.enter, R.anim.out, R.anim.proenter, R.anim.proout).replace(R.id.fragment, two, "TwoFragment").addToBackStack("TwoFragment").commit();
    }
});複製程式碼

基本算是完美了,你有沒有發現一個問題:

當回退棧的時候每次都走Fragment的onCreatView方法,那假如需求要求回退棧,不去走任何方法,而且要回退棧,我們可以修改程式碼如下,把

replace方法換成add方法,為了防止重疊,我們隱藏第一個fragment:

findViewById(R.id.btn2).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        getSupportFragmentManager().beginTransaction().setCustomAnimations(R.anim.enter, R.anim.out, R.anim.proenter, R.anim.proout).add(R.id.fragment, one, "oneFragment").addToBackStack("oneFragment").commit();
    }
});

findViewById(R.id.btn3).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        getSupportFragmentManager().beginTransaction().setCustomAnimations(R.anim.enter, R.anim.out, R.anim.proenter, R.anim.proout).hide(one).add(R.id.fragment, two, "TwoFragment").addToBackStack("TwoFragment").commit();
    }
});複製程式碼

這樣就滿足了需求,回退棧,並且不去從新建立佈局。

如果結合Viewpager,也可以實現回退棧,通過設定快取數量,而不讓佈局去重新建立,並且也會再回退棧的時候呼叫getUserVisibleHint方法。

那麼我不結合Viewpager,如果fragment回退棧了,那麼fragment會不會有什麼方法被回撥呢?有還是沒有? 

沒有!

原因:Fragment只是Activity中的一個控制元件而已,系統怎麼會給一個控制元件分發回退事件呢?這當然是不可能的。

那我們怎麼去監聽呢?

當然還要從Activity中入手:

//每當回退棧數量變化,不管是增加還是減少,都會回掉這個方法,我們可以根據數量的變化,來監聽到底是哪個Fragment從回退棧中出來
getSupportFragmentManager().addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener() {
    @Override
    public void onBackStackChanged() {

    }
});複製程式碼

然後再呼叫對應的fragment中的方法,來通知fragment被退出棧

亦或是我們用:

//該方法在fragment第一次建立的時候並不去呼叫
@Override
public void onHiddenChanged(boolean hidden) { 
   super.onHiddenChanged(hidden); 
}複製程式碼


到此,關於fragment基本上算是回顧完畢,github上還有關於fragment的庫,如

FragmentationFragmentManager,同時Google在2018 IO大會上提供的組建Navigation

,有時間再探討兩個第三方和一個原生元件對fragment棧的支援!


相關文章