三句代碼建立全屏Dialog或者DialogFragment:帶你從源碼角度實現

Dialog是APP開發中經常使用的控件,同Activity相似,擁有獨立的Window窗口,可是Dialog跟Activity仍是有必定區別的,最明顯的就是:默認狀況下Dialog不是全屏的,因此佈局實現不如Activity舒服,好比頂部對齊,底部對齊、邊距、寬度、高度等。若是將Dialog定義成全屏的就會省去不少問題,能夠徹底按照經常使用的佈局方式來處理。網上實現方式有很多,通常狀況下也都能奏效,不過可能會有很多疑慮,好比:爲何有些窗口屬性(隱藏標題)必需要在setContentView以前設置纔有效,相反,也有些屬性(全屏)要在以後設置纔有效。這裏挑幾個簡單的實現方式,而後說下緣由,因爲Android的窗口管理以及View繪製是挺大的一塊,這裏不過多深刻。先看實現效果:javascript

全屏Dialog

全屏Dialog實現方法

這裏對象分爲兩種,一種是針對傳統的Dialog,另外一種是針對DialogFragment(推薦),方法也分爲兩種一種是利用代碼實現,另外一種是利用主題樣式Theme來實現。php

針對Dialog的實現方式java

public class FullScrreenDialog extends Dialog {
    public FullScrreenDialog(Context context) {
        super(context);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        <!--關鍵點1--> getWindow().requestFeature(Window.FEATURE_NO_TITLE); View view = LayoutInflater.from(getContext()).inflate(R.layout.fragment_full_screen, null); <!--關鍵點2--> setContentView(view); <!--關鍵點3--> getWindow().setBackgroundDrawable(new ColorDrawable(0x00000000)); <!--關鍵點4--> getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT); } }複製代碼

這裏牽扯到四個點,關鍵點1要在setContentView以前設置,主要是爲了兼容一些低版本的,不讓顯示Title部分,關鍵點2就是經常使用的setContentView,關鍵點3根4就是爲了全屏對話框作的修改,關鍵點4必需要放在setContentView的後面,由於若是放在setContentView前面,該屬性會被setContentView函數沖掉無效,緣由再後面說。若是你想封裝一個統一的全屏Dialog,那能夠吧關鍵點1放在構造方法中,把關鍵點3與4放在onStart中,其實就是主要是保證setContentView的執行順序,android

public class FullScreenDialog extends Dialog {
    public FullScreenDialog(Context context) {
        super(context);
        getWindow().requestFeature(Window.FEATURE_NO_TITLE);
    }

    @Override
    protected void onStart() {
        getWindow().setBackgroundDrawable(new ColorDrawable(0x00000000));
        getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT);
    }
}複製代碼

以後再看下DialogFragment的作法。ide

針對DialogFragment的實現方式函數

Android比較推薦採用DialogFragment實現對話框,它徹底可以實現Dialog的全部需求,而且還能複用Fragment的生命週期管理,被後臺殺死後還能自動恢復。其實現全屏的原理同Dialog同樣,只不過是時機的把握佈局

public class FullScreen DialogFragment extends DialogFragment {

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_full_screen, container, false);
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
    <!--關鍵點1-->
        getDialog().getWindow().requestFeature(Window.FEATURE_NO_TITLE);
        super.onActivityCreated(savedInstanceState);
    <!--關鍵點2--> getDialog().getWindow().setBackgroundDrawable(new ColorDrawable(0x00000000)); getDialog().getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT); } }複製代碼

先看下這裏爲何放在onActivityCreated中處理,若是稍微跟下DialogFragment的實現源碼就會發現,其setContentView的時機是在onActivityCreated,看以下代碼關鍵點1spa

@Override
public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    if (!mShowsDialog) {
        return;
    }
    View view = getView();
    if (view != null) {
        if (view.getParent() != null) {
            throw new IllegalStateException("DialogFragment can not be attached to a container view");
        }
        <!--關鍵點1-->
        mDialog.setContentView(view);
    }
    ...
}複製代碼

固然,也徹底能夠參考基類Dialog的實現方式,其實關鍵就是把握 setContentView的調用時機。以後來看第二種方案,利用Theme來實現。.net

利用Theme主題來實現全拼對話框3d

第一步在style中定義全屏Dialog樣式

<style name="Dialog.FullScreen" parent="Theme.AppCompat.Dialog">
    <item name="android:windowNoTitle">true</item>
    <item name="android:windowBackground">@color/transparent</item>
    <item name="android:windowIsFloating">false</item>
</style>複製代碼

第二步:設置樣式,以DialogFragment爲例,只須要在onCreate中setStyle(STYLE_NORMAL, R.style.Dialog_FullScreen)便可。(推薦使用DialogFragment,它複用了Fragment的聲明週期,被殺死後,能夠恢復重建)

public class FragmentFullScreen extends DialogFragment {

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setStyle(STYLE_NORMAL, R.style.Dialog_FullScreen);
    }
}複製代碼

若是是在Dialog中,設置以下代碼便可。

public class FullScreenDialog extends Dialog {
    public FullScreenDialog(Context context) {
        super(context);
        getWindow().requestFeature(Window.FEATURE_NO_TITLE);
      }
}複製代碼

其實純代碼的效果跟這三個屬性對應,那麼這三個屬性究竟有什麼做用,設置的時機爲什麼又有限制,下面就簡單分析一下緣由。

全屏Dialog實現原理

針對如下三個屬性一步步分析。

<item name="android:windowIsFloating">false</item>
    <item name="android:windowBackground">@color/transparent</item>
    <item name="android:windowNoTitle">true</item>複製代碼

首先看下第一個屬性,android:windowIsFloating,這個屬性多是Activity默認樣式同Dialog最大的區別之一,對比一下默認的Dialog主題與Activity主題,二者都是繼承Theme,在Theme中

Theme

<style name="Theme">
             ...
         <item name="windowIsFloating">false</item>
    </style>複製代碼

可是Dialog的通常都進行了覆蓋,而Activity默認沒有覆蓋windowIsFloating屬性

Base.V7.Theme.AppCompat.Dialog

<style name="Base.V7.Theme.AppCompat.Dialog" parent="Base.Theme.AppCompat">
    ...
    <item name="android:windowIsFloating">true</item>
</style>複製代碼

也就是說Activity採用了默認的 false ,而Dialog的通常是True,這二者在建立Window的時候有什麼區別呢?進入PhoneWindow.java中,當Window在第一次建立DecorView的時候是須要根據該屬性去建立頂層佈局參數的,也就是RootMeasureSpec,Window被新建的時候,WindowManager.LayoutParams默認採用的是MATCH_PARENT,可是若是windowIsFloating 被設置爲True,WindowManager.LayoutParams參數中的尺寸就會被設置成WRAP_CONTENT,具體源碼以下:

protected ViewGroup generateLayout(DecorView decor) {
    // Apply data from current theme.
    TypedArray a = getWindowStyle();
    mIsFloating = a.getBoolean(com.android.internal.R.styleable.Window_windowIsFloating, false);
    int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR)
            & (~getForcedWindowFlags());
     <!--關鍵點1--> if (mIsFloating) { setLayout(WRAP_CONTENT, WRAP_CONTENT); setFlags(0, flagsToUpdate); } else { setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate); } ... }複製代碼

從關鍵點1能夠看到,若是windowIsFloating被配置爲true,就會經過setLayout(WRAP_CONTENT, WRAP_CONTENT)將Window的窗口屬性WindowManager.LayoutParams設置爲WRAP_CONTENT,這個屬性對於根佈局MeasureSpec參數的生成起着關鍵做用

public void setLayout(int width, int height) {
    final WindowManager.LayoutParams attrs = getAttributes();
    attrs.width = width;
    attrs.height = height;
    if (mCallback != null) {
        mCallback.onWindowAttributesChanged(attrs);
    }
}複製代碼

至於爲何要在setContentView以後設置參數,是由於generateLayout通常是經過setContentView調用的,因此即便提早設置了壓根沒效果,PhoneWindow仍然是根據windowIsFloating來設置WindowManager.LayoutParams。其實View真正顯示的點是在Activity resume的時候,讓WMS添加View,實際上是這裏調用WindowManagerGlobal的addView,這裏有個很關鍵的佈局參數params,其實傳就是WindowManager.LayoutParams l = r.window.getAttributes();若是是Dialog默認主題,該參數的寬高實際上是WRAP_CONTENT,是測量最初限定參數值的起點,也就是說,一個Window究竟多大,這個參數是有最終話語權的,具體的View繪製流程這不詳述,只看下View 的measureHierarchy,是如何利用window參數構造RootMeasureSpec的:

measureHierarchy(final View host, final WindowManager.LayoutParams lp,
        final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) { 
         ...
         <!--desiredWindowWidth通常是屏幕的寬高-->
         childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
     childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
           ... 
     }  複製代碼

desiredWindowWidth與desiredWindowHeight通常是屏幕的寬度與高度,而WindowManager.LayoutParams lp就是上面設置的參數,若是是Activity,默認是ViewGroup.LayoutParams.MATCH_PARENT,而若是是Dialog,就是ViewGroup.LayoutParams.WRAP_CONTENT,而根據MeasureSpec的默認生成規則,以下:

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {
    case ViewGroup.LayoutParams.MATCH_PARENT:
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
        break;
    case ViewGroup.LayoutParams.WRAP_CONTENT:
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
        break;
    default:
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
        break;
    }
    return measureSpec;
}複製代碼

若是是Dialog,就是會以後就會利用MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST)生成RootMeasureSpec,也就是最大是屏幕尺寸,實際效果就是咱們經常使用的wrap_content,以後會利用該RootMeasureSpec對DecorView進行測量繪製。

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    try {
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}複製代碼

以上就是默認Dialog沒法全屏的關鍵緣由之一, 接着看第二屬性 android:windowBackground,這個屬性若是採用默認值,設置會有黑色邊框,其實這裏主要是默認背景的問題,默認採用了有padding的InsetDrawable,設置了一些邊距,致使上面的狀態欄,底部的導航欄,左右都有必定的邊距

<inset xmlns:android="http://schemas.android.com/apk/res/android"
       android:insetLeft="16dp"
       android:insetTop="16dp"
       android:insetRight="16dp"
       android:insetBottom="16dp">
    <shape android:shape="rectangle"> <corners android:radius="2dp" /> <solid android:color="@color/background_floating_material_dark" /> </shape> </inset>複製代碼

DecorView在繪製的時候,會將這裏的邊距考慮進去,並且對於windowIsFloating = false的Window,會將狀態欄及底部導航欄考慮進去(這裏不分析)。以後再來看最後遺留的一個問題,爲何麼要Window.FEATURE_NO_TITLE屬性,而且須要在setContentView被調用以前。

爲何須要在setContentView以前設置Window.FEATURE_NO_TITLE屬性

若是不設置該屬性,有可能出現以下效果:

不設置Window.FEATURE_NO_TITLE

在上面的分析中咱們知道,setContentView會進一步調用generateLayout建立根佈局,Android系統默認實現了多種樣式的根佈局應,以應對不一樣的場景,選擇的規則就是用戶設置的主題樣式(Window屬性),好比需不須要Title,而佈局樣式在選定後就不能再改變了(大小能夠),有些屬性是選擇佈局文件的參考,若是是在setContentView以後再設定,就是失去了意義,另外Android也不容許在選定佈局後,設置一些影響佈局選擇的屬性,會拋出異常,原理以下。

protected ViewGroup generateLayout(DecorView decor) {
    TypedArray a = getWindowStyle();
          ...
    if (a.getBoolean(com.android.internal.R.styleable.Window_windowNoTitle, false)) {
        requestFeature(FEATURE_NO_TITLE);
    } else if (a.getBoolean(com.android.internal.R.styleable.Window_windowActionBar, false)) {
        requestFeature(FEATURE_ACTION_BAR);
    }

 @Override
 public boolean requestFeature(int featureId) {
    if (mContentParent != null) {
        throw new AndroidRuntimeException("requestFeature() must be called before adding content");
    }
    ...
    }複製代碼

以上就是對全屏Dialog定製的一些處理以及對全屏原理的淺析(這裏不包括對狀態欄的處理,那部分涉及到SystemUI)。

僅供參考,歡迎指正
轉載請註明出處

參考文檔

Android 官方推薦 : DialogFragment 建立對話框
如何控制寬度
Android Project Butter分析
淺析 android 應用界面的展示流程(四)建立繪製表面
淺析Android的窗口

相關文章
相關標籤/搜索