Dialog是APP開發中常用的控制元件,同Activity類似,擁有獨立的Window視窗,但是Dialog跟Activity還是有一定區別的,最明顯的就是:預設情況下Dialog不是全屏的,所以佈局實現不如Activity舒服,比如頂部對齊,底部對齊、邊距、寬度、高度等。如果將Dialog定義成全屏的就會省去很多問題,可以完全按照常用的佈局方式來處理。網上實現方式有不少,一般情況下也都能奏效,不過可能會有不少疑慮,比如:為什麼有些視窗屬性(隱藏標題)必須要在setContentView之前設定才有效,相反,也有些屬性(全屏)要在之後設定才有效。這裡挑幾個簡單的實現方式,然後說下原因,由於Android的視窗管理以及View繪製是挺大的一塊,這裡不過多深入。先看實現效果:
全屏Dialog實現方法
這裡物件分為兩種,一種是針對傳統的Dialog,另一種是針對DialogFragment(推薦),方法也分為兩種一種是利用程式碼實現,另一種是利用主題樣式Theme來實現。
針對Dialog的實現方式
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的執行順序,
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的做法。
針對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,看如下程式碼關鍵點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");
}
<!--關鍵點1-->
mDialog.setContentView(view);
}
...
}複製程式碼
當然,也完全可以參考基類Dialog的實現方式,其實關鍵就是把握 setContentView的呼叫時機。之後來看第二種方案,利用Theme來實現。
利用Theme主題來實現全拼對話方塊
第一步在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採用了預設的
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屬性
如果不設定該屬性,有可能出現如下效果:
在上面的分析中我們知道,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的視窗