三句代碼創(chuàng)建全屏Dialog或者DialogFragment:帶你從源碼角度實(shí)現(xiàn)

Dialog是APP開發(fā)中常用的控件,同Activity類似,擁有獨(dú)立的Window窗口,但是Dialog跟Activity還是有一定區(qū)別的,最明顯的就是:默認(rèn)情況下Dialog不是全屏的,所以布局實(shí)現(xiàn)不如Activity舒服,比如頂部對(duì)齊,底部對(duì)齊、邊距、寬度、高度等。如果將Dialog定義成全屏的就會(huì)省去很多問題,可以完全按照常用的布局方式來處理。網(wǎng)上實(shí)現(xiàn)方式有不少,一般情況下也都能奏效,不過可能會(huì)有不少疑慮,比如:為什么有些窗口屬性(隱藏標(biāo)題)必須要在setContentView之前設(shè)置才有效,相反,也有些屬性(全屏)要在之后設(shè)置才有效。這里挑幾個(gè)簡(jiǎn)單的實(shí)現(xiàn)方式,然后說下原因,由于Android的窗口管理以及View繪制是挺大的一塊,這里不過多深入。先看實(shí)現(xiàn)效果:

全屏Dialog

全屏Dialog實(shí)現(xiàn)方法

這里對(duì)象分為兩種,一種是針對(duì)傳統(tǒng)的Dialog,另一種是針對(duì)DialogFragment(推薦),方法也分為兩種一種是利用代碼實(shí)現(xiàn),另一種是利用主題樣式Theme來實(shí)現(xiàn)。

針對(duì)Dialog的實(shí)現(xiàn)方式

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

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

這里牽扯到四個(gè)點(diǎn),關(guān)鍵點(diǎn)1要在setContentView之前設(shè)置,主要是為了兼容一些低版本的,不讓顯示Title部分,關(guān)鍵點(diǎn)2就是常用的setContentView,關(guān)鍵點(diǎn)3根4就是為了全屏對(duì)話框做的修改,關(guān)鍵點(diǎn)4必須要放在setContent的后面,因?yàn)槿绻旁趕etContent該屬性會(huì)被沖掉無效,原因再后面說。如果你想封裝一個(gè)統(tǒng)一的全屏Dialog,那可以吧關(guān)鍵點(diǎn)1放在構(gòu)造方法中,把關(guān)鍵點(diǎn)3與4放在onStart中,其實(shí)就是主要是保證setContentView的執(zhí)行順序,

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的做法。

針對(duì)DialogFragment的實(shí)現(xiàn)方式

Android比較推薦采用DialogFragment實(shí)現(xiàn)對(duì)話框,它完全能夠?qū)崿F(xiàn)Dialog的所有需求,并且還能復(fù)用Fragment的生命周期管理,被后臺(tái)殺死后還能自動(dòng)恢復(fù)。其實(shí)現(xiàn)全屏的原理同Dialog一樣,只不過是時(shí)機(jī)的把握

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) {
    <!--關(guān)鍵點(diǎn)1-->
        getDialog().getWindow().requestFeature(Window.FEATURE_NO_TITLE);
        super.onActivityCreated(savedInstanceState);
    <!--關(guān)鍵點(diǎn)2-->
        getDialog().getWindow().setBackgroundDrawable(new ColorDrawable(0x00000000));
        getDialog().getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT);
    }

}

先看下這里為什么放在onActivityCreated中處理,如果稍微跟下DialogFragment的實(shí)現(xiàn)源碼就會(huì)發(fā)現(xiàn),其setContentView的時(shí)機(jī)是在onActivityCreated,看如下代碼關(guān)鍵點(diǎn)1

@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");
        }
        <!--關(guān)鍵點(diǎn)1-->
        mDialog.setContentView(view);
    }
    ...
}

當(dāng)然,也完全可以參考基類Dialog的實(shí)現(xiàn)方式,其實(shí)關(guān)鍵就是把握 setContentView的調(diào)用時(shí)機(jī)。之后來看第二種方案,利用Theme來實(shí)現(xiàn)。

利用Theme主題來實(shí)現(xiàn)全拼對(duì)話框

第一步在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>

第二步:設(shè)置樣式,以DialogFragment為例,只需要在onCreate中setStyle(STYLE_NORMAL, R.style.Dialog_FullScreen)即可。(推薦使用DialogFragment,它復(fù)用了Fragment的聲明周期,被殺死后,可以恢復(fù)重建)

public class FragmentFullScreen extends DialogFragment {

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

如果是在Dialog中,設(shè)置如下代碼即可。

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

其實(shí)純代碼的效果跟這三個(gè)屬性對(duì)應(yīng),那么這三個(gè)屬性究竟有什么作用,設(shè)置的時(shí)機(jī)為何又有限制,下面就簡(jiǎn)單分析一下原因。

全屏Dialog實(shí)現(xiàn)原理

針對(duì)以下三個(gè)屬性一步步分析。

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

首先看下第一個(gè)屬性,android:windowIsFloating,這個(gè)屬性可能是Activity默認(rèn)樣式同Dialog最大的區(qū)別之一,對(duì)比一下默認(rèn)的Dialog主題與Activity主題,兩者都是繼承Theme,在Theme中

Theme

    <style name="Theme">
            ...
         <item name="windowIsFloating">false</item>
    </style>

但是Dialog的一般都進(jìn)行了覆蓋,而Activity默認(rèn)沒有覆蓋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采用了默認(rèn)的 <item name="windowIsFloating">false</item>,而Dialog的一般是True,這兩者在創(chuàng)建Window的時(shí)候有什么區(qū)別呢?進(jìn)入PhoneWindow.java中,當(dāng)Window在第一次創(chuàng)建DecorView的時(shí)候是需要根據(jù)該屬性去創(chuàng)建頂層布局參數(shù)的,也就是RootMeasureSpec,Window被新建的時(shí)候,WindowManager.LayoutParams默認(rèn)采用的是MATCH_PARENT,但是如果windowIsFloating 被設(shè)置為True,WindowManager.LayoutParams參數(shù)中的尺寸就會(huì)被設(shè)置成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());
     <!--關(guān)鍵點(diǎn)1-->
    if (mIsFloating) {
        setLayout(WRAP_CONTENT, WRAP_CONTENT);
        setFlags(0, flagsToUpdate);
    } else {
        setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);
    }
    ...       
}

從關(guān)鍵點(diǎn)1可以看到,如果windowIsFloating被配置為true,就會(huì)通過setLayout(WRAP_CONTENT, WRAP_CONTENT)將Window的窗口屬性WindowManager.LayoutParams設(shè)置為WRAP_CONTENT,這個(gè)屬性對(duì)于根布局MeasureSpec參數(shù)的生成起著關(guān)鍵作用

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之后設(shè)置參數(shù),是因?yàn)間enerateLayout一般是通過setContentView調(diào)用的,所以即使提前設(shè)置了壓根沒效果,PhoneWindow仍然是根據(jù)windowIsFloating來設(shè)置WindowManager.LayoutParams。其實(shí)View真正顯示的點(diǎn)是在Activity resume的時(shí)候,讓W(xué)MS添加View,其實(shí)是這里調(diào)用WindowManagerGlobal的addView,這里有個(gè)很關(guān)鍵的布局參數(shù)params,其實(shí)傳就是WindowManager.LayoutParams l = r.window.getAttributes();如果是Dialog默認(rèn)主題,該參數(shù)的寬高其實(shí)是WRAP_CONTENT,是測(cè)量最初限定參數(shù)值的起點(diǎn),也就是說,一個(gè)Window究竟多大,這個(gè)參數(shù)是有最終話語權(quán)的,具體的View繪制流程這不詳述,只看下View 的measureHierarchy,是如何利用window參數(shù)構(gòu)造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就是上面設(shè)置的參數(shù),如果是Activity,默認(rèn)是ViewGroup.LayoutParams.MATCH_PARENT,而如果是Dialog,就是ViewGroup.LayoutParams.WRAP_CONTENT,而根據(jù)MeasureSpec的默認(rèn)生成規(guī)則,如下:

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,就是會(huì)之后就會(huì)利用MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST)生成RootMeasureSpec,也就是最大是屏幕尺寸,實(shí)際效果就是我們常用的wrap_content,之后會(huì)利用該RootMeasureSpec對(duì)DecorView進(jìn)行測(cè)量繪制。

  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);
    }
}

以上就是默認(rèn)Dialog無法全屏的關(guān)鍵原因之一, 接著看第二屬性 android:windowBackground,這個(gè)屬性如果采用默認(rèn)值,設(shè)置會(huì)有黑色邊框,其實(shí)這里主要是默認(rèn)背景的問題,默認(rèn)采用了有padding的InsetDrawable,設(shè)置了一些邊距,導(dǎo)致上面的狀態(tài)欄,底部的導(dǎo)航欄,左右都有一定的邊距

<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在繪制的時(shí)候,會(huì)將這里的邊距考慮進(jìn)去,而且對(duì)于windowIsFloating = false的Window,會(huì)將狀態(tài)欄及底部導(dǎo)航欄考慮進(jìn)去(這里不分析)。之后再來看最后遺留的一個(gè)問題,為什么么要Window.FEATURE_NO_TITLE屬性,并且需要在setContentView被調(diào)用之前。

為什么需要在setContentView之前設(shè)置Window.FEATURE_NO_TITLE屬性

如果不設(shè)置該屬性,有可能出現(xiàn)如下效果:

不設(shè)置Window.FEATURE_NO_TITLE

在上面的分析中我們知道,setContentView會(huì)進(jìn)一步調(diào)用generateLayout創(chuàng)建根布局,Android系統(tǒng)默認(rèn)實(shí)現(xiàn)了多種樣式的根布局應(yīng),以應(yīng)對(duì)不同的場(chǎng)景,選擇的規(guī)則就是用戶設(shè)置的主題樣式(Window屬性),比如需不需要Title,而布局樣式在選定后就不能再改變了(大小可以),有些屬性是選擇布局文件的參考,如果是在setContentView之后再設(shè)定,就是失去了意義,另外Android也不允許在選定布局后,設(shè)置一些影響布局選擇的屬性,會(huì)拋出異常,原理如下。

    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");
    }
    ...
    }

以上就是對(duì)全屏Dialog定制的一些處理以及對(duì)全屏原理的淺析(這里不包括對(duì)狀態(tài)欄的處理,那部分涉及到SystemUI)。

創(chuàng)建沉浸式全屏Dialog

在全屏Dialog的基礎(chǔ)上,我們可以創(chuàng)建沉浸式Dialog,也就是讓Dialog的內(nèi)容區(qū)域延展到狀態(tài)欄下方,由于這里用到了fitsystemwindow,所以要牽扯DecorView及rootView的設(shè)置,在上面的基礎(chǔ)上有兩點(diǎn)需要處理,一是:內(nèi)容區(qū)域延展上去;二是:狀態(tài)欄變成透明(不透明你也看不見?。绾窝诱箖?nèi)容區(qū)域,參考全屏、沉浸式、fitSystemWindow使用及原理分析:全方位控制“沉浸式”的實(shí)現(xiàn),具體實(shí)現(xiàn)如下:

    @Override
    public void show() {
        if (getWindow() != null && getWindow().getDecorView() != null) {
            getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                    | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
        }
        super.show();
    }

如何設(shè)置狀態(tài)欄透明呢?(不兼容5.0以下),主題中添加如下屬性,設(shè)置狀態(tài)欄顏色透明即可,當(dāng)然,代碼中也可實(shí)現(xiàn)。

    <item name="android:statusBarColor">@color/transparent</item>

效果如下

21526023112_.pic.jpg

作者:看書的小蝸牛
原文鏈接: 三句代碼創(chuàng)建全屏Dialog或者DialogFragment:帶你從源碼角度實(shí)現(xiàn)

能完全使用Java代碼創(chuàng)建創(chuàng)建沉浸式全屏Dialog嗎?

不能,原因如下:只有!mWindow.mIsFloating的時(shí)候,才能修改狀態(tài)欄顏色,如果無法修改顏色,自然無法沉浸式

WindowInsets updateColorViews(WindowInsets insets, boolean animate) {
    WindowManager.LayoutParams attrs = mWindow.getAttributes();
    int sysUiVisibility = attrs.systemUiVisibility | getWindowSystemUiVisibility();

    if (!mWindow.mIsFloating) {
        boolean disallowAnimate = !isLaidOut();
        disallowAnimate |= ((mLastWindowFlags ^ attrs.flags)
                & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0;
        mLastWindowFlags = attrs.flags;
            ...
        boolean navBarToRightEdge = isNavBarToRightEdge(mLastBottomInset, mLastRightInset);
        boolean navBarToLeftEdge = isNavBarToLeftEdge(mLastBottomInset, mLastLeftInset);
        int navBarSize = getNavBarSize(mLastBottomInset, mLastRightInset, mLastLeftInset);
        updateColorViewInt(mNavigationColorViewState, sysUiVisibility,
                mWindow.mNavigationBarColor, mWindow.mNavigationBarDividerColor, navBarSize,
                navBarToRightEdge || navBarToLeftEdge, navBarToLeftEdge,
                0 /* sideInset */, animate && !disallowAnimate, false /* force */);

        boolean statusBarNeedsRightInset = navBarToRightEdge
                && mNavigationColorViewState.present;
        boolean statusBarNeedsLeftInset = navBarToLeftEdge
                && mNavigationColorViewState.present;
        int statusBarSideInset = statusBarNeedsRightInset ? mLastRightInset
                : statusBarNeedsLeftInset ? mLastLeftInset : 0;
        updateColorViewInt(mStatusColorViewState, sysUiVisibility,
                calculateStatusBarColor(), 0, mLastTopInset,
                false /* matchVertical */, statusBarNeedsLeftInset, statusBarSideInset,
                animate && !disallowAnimate,
                mForceWindowDrawsStatusBarBackground);
    }

僅供參考,歡迎指正

參考文檔

Android 官方推薦 : DialogFragment 創(chuàng)建對(duì)話框
如何控制寬度
Android Project Butter分析
淺析 android 應(yīng)用界面的展現(xiàn)流程(四)創(chuàng)建繪制表面
淺析Android的窗口

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容