Dialog是APP开发中经常使用的控件,同Activity相似,拥有独立的Window窗口,可是Dialog跟Activity仍是有必定区别的,最明显的就是:默认状况下Dialog不是全屏的,因此布局实现不如Activity舒服,好比顶部对齐,底部对齐、边距、宽度、高度等。若是将Dialog定义成全屏的就会省去不少问题,能够彻底按照经常使用的布局方式来处理。网上实现方式有很多,通常状况下也都能奏效,不过可能会有很多疑虑,好比:为何有些窗口属性(隐藏标题)必需要在setContentView以前设置才有效,相反,也有些属性(全屏)要在以后设置才有效。这里挑几个简单的实现方式,而后说下缘由,因为Android的窗口管理以及View绘制是挺大的一块,这里不过多深刻。先看实现效果:javascript
这里对象分为两种,一种是针对传统的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);
}
}复制代码
其实纯代码的效果跟这三个属性对应,那么这三个属性究竟有什么做用,设置的时机为什么又有限制,下面就简单分析一下缘由。
针对如下三个属性一步步分析。
<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会进一步调用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的窗口