Android高手進階之兩幅圖搞定DilogFragment

Hanking發表於2019-01-12

前言

關於DilogFragment之前寫過幾篇部落格,但是並不是很深入,有些問題也沒有解決。 例如:

1、DialogFragment和Activity的關係。

2、DilogFragment生命週期和和設定佈局大小無效問題。

為了解決上面的2個問題,需要從兩個方面來入手,1、DialogFragment和Activity的關係。2、DialogFragment和Dialog的關係。理清楚了這些關係後上面的三點問題也就解決了。

1、DialogFragment和Activity的關係。

DialogFragment繼承Fragment生命週期和所在的Activity生命週期相關,由FragmentManager管理。Activity的生命週期由Framework層中的ActivityManagerService來管理的,而Fragment只對Activity可見,對Framework層並不可見,也就是Framework並不知道Fragment的存在,Fragment的生命週期完全由Activity中的FragmentManager來管理。Activity和Fragment關係如下圖所示:

在這裡插入圖片描述

Activity通過FragmentManager來管理Fragment。FragmentManager可以通過FragmentTransaction把Fragment加入到Back Stack中,FragmentTransaction和資料庫操作的方法一樣,有add(),remove(),commit()等方法來操作Fragment。已經知道了Fragment是依賴於Activity的,下面給出Fragment和Activity生命週期的關係。

在這裡插入圖片描述
Fragment和Activity的生命週期的對應關係後面我會專門寫一篇部落格。其實原理並不複雜,就是在Activity的各個生命週期中去分別呼叫其包含的Fragment對應的生命週期的函式。在Activtiy中使用Fragment非常簡單如下:

public class DemoActivity extends FragmentActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_crime);
        FragmentManager fm = getSupportFragmentManager();
        Fragment fragment = fm.findFragmentById(R.id.fragmentContainer);
        if (fragment == null) {
            fragment = new CrimeFragment();
            fm.beginTransaction()
                    .add(R.id.fragmentContainer, fragment)
                    .commit();
        }
    }
}

複製程式碼

上面的例子是在Activity的onCreate方法中呼叫FragmentManager 來將Fragment加入到Activity中。

如果在Activity的其他生命週期中將Fragment加入到Activity中呢?比如說在Activity處於stopped,paused,或者running狀態時,加入Fragment的生命週期是怎樣的?

這個時候FragmentManager會立即執行Fragment需要的生命狀態直到和activity相匹配的狀態。比如說在Activity處於running狀態時加入Fragment,此時Fragment會執行生命 onAttach(Activity), onCreate(Bundle), onCreateView(…), onActivityCreated(Bundle), onStart(),最後執行 onResume(). 如下圖:在Activity處於onResume的時候addFragment,此時Fragment執行的生命週期。

在這裡插入圖片描述

2、DilogFragment生命週期和和設定佈局大小無效問題。

下面看看在Activity中使用DialogFragment的例子,然後分析兩點:

1、為什麼DialogFragment會以彈框的形式顯示。

2、為什麼在要在onStart()方法中動態改變DialogFragment才能動態改變佈局。

new MyDialogFragment().show(getFragmentManager(),"id");
複製程式碼

DialogFragment的使用十分簡單,直接在Activity中呼叫DialogFragment的show()方法同時傳入當前Activity中的FragmentManager,傳入的FragmentManager肯定是用來管理DialogFragment的。

    public void show(FragmentManager manager, String tag) {
        mDismissed = false;
        mShownByMe = true;
        FragmentTransaction ft = manager.beginTransaction();
        ft.add(this, tag);
        ft.commit();
    }
複製程式碼

DialogFragment中的show()方法呼叫FragmentManager把自己加入到back stack中,生命週期由FragmentManager管理。在Activity中呼叫DialogFragment的show()方法此時並不會立即顯示,這個時候關聯了DialogFragment和activity的生命週期,那麼DialogFragment在什麼時候顯示的呢?

    @Override
    public void onStart() {
        super.onStart();
        if (mDialog != null) {
            mViewDestroyed = false;
            mDialog.show();
        }
    }
複製程式碼

只有DialogFragment的生命週期執行到onStart()的時候才會呼叫mDialog.show();顯示彈框,在DialogFragment原始碼解析中已經說過,DialogFragment的佈局會被加到mDialog中,所以DialogFragment才會顯示成彈框形式。這解決了為什麼DialogFragment會以彈框的形式顯示的問題。

在onCreateView中定義的佈局最後會被加入到Dialog中,所以要改變彈出框的大小其實是要改變Dialog的大小。Dialog的大小要在Dialog.show()方法之後才能動態改變。下面分析一下為什麼Dialog的大小在Dialog.show()方法之後才能改變。看看Dialog.show()的原始碼:

public void show() {
        if (mShowing) {
            return;
        }
        if (!mCreated) {
            dispatchOnCreate(null);
        } else {
        }
    mWindowManager.addView(mDecor, l);    
}
  
複製程式碼

上面程式碼是簡化之後的,如果Dialog還沒有建立就會呼叫dispatchOnCreate(null);方法來建立Dialog的佈局,下面看看dispatchOnCreate(null)方法,簡化後如下:

    void dispatchOnCreate(Bundle savedInstanceState) {
        if (!mCreated) {
            mDialog.setContentView(selectContentView());
            mCreated = true;
        }
    }
//獲取相應的佈局樣式
    private int selectContentView() {
        if (mButtonPanelSideLayout == 0) {
            return mAlertDialogLayout;
        }
        if (mButtonPanelLayoutHint == AlertDialog.LAYOUT_HINT_SIDE) {
            return mButtonPanelSideLayout;
        }
        return mAlertDialogLayout;
    }
複製程式碼

由上面可以看出,使用Dialog的子類AlertDialog時,使用的contentView是Android自帶樣式和大小的Layout,使用者自定義的view被加到mDecor上,所以在show()之前設定xml的大小是無效的,最後還是會在show中被覆蓋成系統自帶的格式,只有在show後面改變佈局屬性才會生效。

這也就解釋了為什麼DialogFragment在onCreate()和onCreateView()中設定佈局大小無效,因為onCreate()和onCreateView()生命週期在onStart()生命週期之前,此時還未呼叫Dialog.show()方法,設定大小無效。可以總結出Activity和DialogFragment的關係如下圖:

在這裡插入圖片描述

DialogFragment自定義佈局大小坑分析

坑自定義彈框的大小 在自定義佈局中設定的大小是不起作用的,要設定自定義佈局的大小隻有在程式碼中動態設定,在onStart中重寫佈局大小,在onCreat或者onCreateView中無效

    /**
     * 修改佈局的大小
     */
    @Override
    public void onStart() {
        super.onStart();
        XLLog.d(TAG, "onStart");
        resizeDialogFragment();

    }

    private void resizeDialogFragment() {
        Dialog dialog = getDialog();
        if (null != dialog) {
            Window window = dialog.getWindow();
            WindowManager.LayoutParams lp = getDialog().getWindow().getAttributes();
            lp.height = (25 * ScreenUtil.getScreenHeight(getContext()) / 32);//獲取螢幕的寬度,定義自己的寬度
            lp.width = (8 * ScreenUtil.getScreenWidth(getContext()) / 9);
            if (window != null) {
                window.setLayout(lp.width, lp.height);
            }
        }
    }
複製程式碼

這裡不得不提到的坑是,看起來已經動態設定了自己想要的佈局大小。但實際執行出來的大小和定義的尺寸有偏差。上面程式碼中設定的寬度是螢幕的8/9,執行程式碼是得到的 lp.width=960,但我用Layout Inspector檢測出來自定義的佈局寬度僅僅是876,這中間差了84。所以肯定是系統在自定義的佈局外面又包了一層其他的東西,導致設定出來的寬度和實際顯示的不一樣。 通過

        Dialog dialog = getDialog();
        if (null != dialog) {
            Window window = dialog.getWindow();
            Log.d(TAG, "padding.................." + window.getDecorView().getPaddingLeft() + "............................." + window.getDecorView().getPaddingTop());
//結果:padding..................42.............................42
複製程式碼

由上面結果可知,在自定義佈局外面還有一個padding的距離,這個padding 距離四周的距離都是42,876+42*2=960,正好和設定的寬度相同。

檢測佈局

用Android studio 中的Layout Inspector檢測佈局

在這裡插入圖片描述
由上圖可以看到佈局結構:
在這裡插入圖片描述
整個彈出框的根佈局是DecorView,DecorView裡面包含了一個FragmentLayout,FragmentLayout裡面包含兩個佈局一個是content,就是我們自定義的佈局,Action_mode_bar_stub這個是actionBar,我們這裡的actionBar佈局為null什麼都沒有。 其中DecorView的定義可以看一段英文:

The DecorView is the view that actually holds the window’s background drawable. Calling getWindow().setBackgroundDrawable() from your Activity changes the background of the window by changing the DecorView‘s background drawable. As mentioned before, this setup is very specific to the current implementation of Android and can change in a future version or even on another device.

其中的padding=42就是DecorView與其他佈局的間距,所以獲取到DecorView再設定它的padding就好了。

解決方案:設定透明背景

要在onCreateView中設定背景為透明,原來dialogFragment在自定義的佈局外加了一個背景,設定為透明背景後,尺寸就和設定的尺寸一樣了。加上

getDialog().getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
複製程式碼
 @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        getDialog().getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
        View view = inflater.inflate(R.layout.message_share_websit_dialog, container);
        return view;

    }
複製程式碼

搞定

總結

本文主要從DialogFragment和Activity的關係以及DialogFragment的生命週期特點來分析為什麼DialogFragment會顯示彈框形式,以及動態設定佈局時為什麼要在onStart()方法中生效,在onCreate()和onCreateView()中設定不生效問題。

參考文獻

1、Android Programming_ The Big Nerd Ranch Guide

2、developer.android.com/courses/fun…

備註:2連結中有google官方提供的初級和高階教程,而且配有ppt講解十分詳細,是很好的學習資源。

相關文章