默认的Dialog
布局,就一个简单的textview,弄个背景,好区分
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="TextView"
android:background="#88888888"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
默认的显示成这样,可以看到宽度默认是有限制的,我们的match是无效的,
修改宽度大家应该知道的,下边的代码
override fun onStart() {
super.onStart()
val window = dialog.window
val param = window!!.attributes
param.width = resources.displayMetrics.widthPixels
window.attributes = param
}
现在长这样,可以看到,两边还是有边距的,宽度设置和屏幕一样没用的,其实吧,我个人觉得这样挺好的,你是个dialog,留个边距才是合理的,铺满了咋还能看出是dialog。
可实际中,ui可能就需要你不准留间距,那就只好想办法解决了
我去看看源码有没有设置这个的地方
看AlertDialog里边可以发现它是用AlertController来生成的,这个类跳不过去,自己去源码下边找下,然后打开看了下,view的添加过程
构造方法
可以看到里边加载了好多布局,看名字大概就知道干啥的了,我们普通的布局就是这个了布局alert_dialog
public AlertController(Context context, DialogInterface di, Window window) {
mContext = context;
mDialogInterface = di;
mWindow = window;
mHandler = new ButtonHandler(di);
final TypedArray a = context.obtainStyledAttributes(null,
R.styleable.AlertDialog, R.attr.alertDialogStyle, 0);
mAlertDialogLayout = a.getResourceId(
R.styleable.AlertDialog_layout, R.layout.alert_dialog);
mButtonPanelSideLayout = a.getResourceId(
R.styleable.AlertDialog_buttonPanelSideLayout, 0);
mListLayout = a.getResourceId(
R.styleable.AlertDialog_listLayout, R.layout.select_dialog);
mMultiChoiceItemLayout = a.getResourceId(
R.styleable.AlertDialog_multiChoiceItemLayout,
R.layout.select_dialog_multichoice);
mSingleChoiceItemLayout = a.getResourceId(
R.styleable.AlertDialog_singleChoiceItemLayout,
R.layout.select_dialog_singlechoice);
mListItemLayout = a.getResourceId(
R.styleable.AlertDialog_listItemLayout,
R.layout.select_dialog_item);
a.recycle();
}
alert_dialog.xml
这个布局到源码platforms里对应的版本里可以找到,自己随便找个版本吧
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/parentPanel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="9dip"
android:paddingBottom="3dip"
android:paddingStart="3dip"
android:paddingEnd="1dip">
//省略,下边就是标题,内容,以及3个button按钮了,就不帖了
AlertController里布局的添加
mWindow.setContentView(contentView); 可以看到就是把我们dialog默认的布局id传进去了,就是上边那个alert_dialog.xml
public void installContent() {
/* We use a custom title so never request a window title */
mWindow.requestFeature(Window.FEATURE_NO_TITLE);
int contentView = selectContentView();
mWindow.setContentView(contentView);
setupView();
setupDecor();
}
private int selectContentView() {
if (mButtonPanelSideLayout == 0) {
return mAlertDialogLayout;
}
if (mButtonPanelLayoutHint == AlertDialog.LAYOUT_HINT_SIDE) {
return mButtonPanelSideLayout;
}
// TODO: use layout hint side for long messages/lists
return mAlertDialogLayout;
}
这个dialog的里边的布局逻辑就不管了,我们知道我们这个view最后是通过windown方法添加到页面上了。
打印下dialog的页面构成
既然知道和window有关,那就打印下
val mWindow = dialog.window
val decor = mWindow.getDecorView()
checkView(decor)
private fun checkView(decor:View){
if(decor!=null&&decor is ViewGroup){
val vg=decor as ViewGroup
val count=vg.childCount
repeat(count){
val child=vg.getChildAt(it)
println("parent=${vg}======child${count}====${it}====$child")
if(child is ViewGroup){
checkView(child)
}
}
}
}
打印完可以看到,他的页面结构和activity差不多,或者说是一样的。
这里有一张activity页面的布局图,可以参考https://www.jianshu.com/p/1e222b3ac7a0 后边有分析。
我们这里的dialog,对应的就是黑色字体的部分。
也就是dialog里我们自定义的布局,是放在一个id是content的Framelayout下边的
我们打印出的view,其实可以看到left,top,right,bottom这几个值的
从最里边到最外边,parentPanel这个id上边的布局文件有
android.widget.LinearLayout{6a24d84 V.E...... ........ 0,0-531,150 #10202f0 android:id/parentPanel}
android.widget.FrameLayout{40c6797 V.E...... ........ 0,0-531,150 #1020002 android:id/content}
android.widget.FrameLayout{6932216 V.E...... ........ 16,16-547,166}
com.android.internal.policy.PhoneWindow$DecorView{ab44b31 V.E...... R....... 0,0-563,182}
我们看到decorview下边的那个FrameLayout,他的left,top,right,bottom值,可以猜到它设置了一个margin,或者也可能是decorview设置的padding。
打印下的结果是decorview设置了padding,4个padding都是16.
找到原因,那就改呗
override fun onStart() {
super.onStart()
val window = dialog.window
val param = window!!.attributes
param.width = resources.displayMetrics.widthPixels
window.attributes = param
window.decorView.setPadding(0,0,0,0)
}
改完成这样了,文字部分不见了,而且打印结果来看,padding确实没了,我们的view宽度也确实和屏幕一样了。对比上边的图,应该知道,我们的文字就是默认的textView.
从打印结果,我们的view宽度和屏幕一样宽了,从图上来看,Textview明显也就是在最左边了,只是左边那部分看不到了。感觉是布局没刷新?
后来想到弄个背景颜色看看,结果发现弄完背景就正常了,甚至那paddding都不需要,如下
override fun onStart() {
super.onStart()
val window = dialog.window
val param = window!!.attributes
param.width = resources.displayMetrics.widthPixels
window.attributes = param
// window.decorView.setPadding(0,0,0,0)
// window.decorView.setBackgroundColor(Color.RED)
window.setBackgroundDrawable(ColorDrawable(Color.RED))
}
我们看下源码,sources目录下随便找个版本
D:\sdk\sources\android-23\com\android\internal\policy\PhoneWindow.java
@Override
public final void setBackgroundDrawable(Drawable drawable) {
if (drawable != mBackgroundDrawable || mBackgroundResource != 0) {
mBackgroundResource = 0;
mBackgroundDrawable = drawable;
if (mDecor != null) {
mDecor.setWindowBackground(drawable);
}
if (mBackgroundFallbackResource != 0) {
mDecor.setBackgroundFallback(drawable != null ? 0 : mBackgroundFallbackResource);
}
}
}
//DecorView 也在这个类里边
public void setWindowBackground(Drawable drawable) {
if (getBackground() != drawable) {
setBackgroundDrawable(drawable);
if (drawable != null) {
drawable.getPadding(mBackgroundPadding);
} else {
mBackgroundPadding.setEmpty();
}
drawableChanged();
}
}
可以看到window.setBackgroundDrawable(ColorDrawable(Color.RED))最终也是给decorview设置背景的
那么这样写可以不
window.decorView.setBackgroundColor(Color.RED)
背景没问题,不过这种padding还在,就 需要加上
window.decorView.setPadding(0,0,0,0)
也就是,下边这两句的效果和window.setBackgroundDrawable(ColorDrawable(Color.RED))一样
window.decorView.setBackgroundColor(Color.RED)
window.decorView.setPadding(0,0,0,0)
那么仔细看下就可以发现window.setBackgroundDrawable(ColorDrawable(Color.RED))这个方法里最后多了一行代码drawable.getPadding(mBackgroundPadding);
看下这个方法,可以看到把Rect的值弄成0了.
public boolean getPadding(@NonNull Rect padding) {
padding.set(0, 0, 0, 0);
return false;
}
而后边还调用了一个方法drawableChanged();
下图可以看到,用到了这个padding,而这个padding被弄成0了都,仔细看看了,感觉他好像本来就是0
也就是初始化,然后上边setWindowBackground()方法里用到,然后就下图了。
另外一个mFramePadding也差不多
分析下布局的加载过程
decorview-framlayout-framlayout-dialog的布局
dialog的show方法如下,其他代码省略,就留了2行,
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
final Window w = new PhoneWindow(mContext);
public void show() {
onStart();
if (!mCreated) {
dispatchOnCreate(null);//这个会调用dialog的onCreate方法的
}
mDecor = mWindow.getDecorView();//如果 为空,里边会创建DecorView的
mWindowManager.addView(mDecor, l);
}
看下AlertDialog的布局是咋加进去的
//AlertDialog方法
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mAlert.installContent();
}
//
public void installContent() {
int contentView = selectContentView();
mWindow.setContentView(contentView);
setupView();
}
看下phoneWindow
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);//这里添加的,mContentParent是decorview里加的那个布局
}
mContentParentExplicitlySet = true;
}
看下phonewindow,这里会分析DecorView的创建,以及上边mContentParent的添加过程。
public final View getDecorView() {
if (mDecor == null || mForceDecorInstall) {
installDecor();
}
return mDecor;
}
下边会省略无关的代码,只保留view的创建相关的代码
private DecorView mDecor;
// This is the view in which the window contents are placed. It is either
// mDecor itself, or a child of mDecor where the contents go.
ViewGroup mContentParent;
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor(-1);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
}
继续
protected ViewGroup generateLayout(DecorView decor) {
WindowManager.LayoutParams params = getAttributes();
// The rest are only done if this window is not embedded; otherwise,
// the values are inherited from our container.
if (getContainer() == null) {
if (mBackgroundDrawable == null) {
if (mBackgroundResource == 0) {
mBackgroundResource = a.getResourceId(
R.styleable.Window_windowBackground, 0);
}
if (mFrameResource == 0) {
mFrameResource = a.getResourceId(R.styleable.Window_windowFrame, 0);
}
}
}
//
int layoutResource;
//根据条件不同获取到layoutresource
layoutResource = R.layout.screen_swipe_dismiss;
layoutResource = R.layout.screen_title_icons;
layoutResource = R.layout.screen_progress;
layoutResource = R.layout.screen_custom_title;
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
}
下边DecorView的方法就是把上边的布局加载到DecorView里
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
mDecorCaptionView = createDecorCaptionView(inflater);
final View root = inflater.inflate(layoutResource, null);
if (mDecorCaptionView != null) {
if (mDecorCaptionView.getParent() == null) {
addView(mDecorCaptionView,
new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mDecorCaptionView.addView(root,
new MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
} else {
// Put it below the color views.
addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mContentRoot = (ViewGroup) root;//新加的这个布局成了容器,后边的东西都是加到这里的
initializeElevation();
}
查找这个padding到底是哪里弄的
我打印了相关的几个rect,前边分析过decorview在setbackground以后会调用一个drawableChanged()的方法,这个方法里调用了setpadding方法,如下
private void drawableChanged() {
if (mChanging) {
return;
}
setPadding(mFramePadding.left + mBackgroundPadding.left,
mFramePadding.top + mBackgroundPadding.top,
mFramePadding.right + mBackgroundPadding.right,
mFramePadding.bottom + mBackgroundPadding.bottom);
requestLayout();
invalidate();
打印下这4个的值
private final Rect mDrawingBounds = new Rect();
private final Rect mBackgroundPadding = new Rect();
private final Rect mFramePadding = new Rect();
private final Rect mFrameOffsets = new Rect();
使用反射获取
val decor = dialog.window.getDecorView()
reflectValue(decor,"mDrawingBounds")
reflectValue(decor,"mBackgroundPadding")
reflectValue(decor,"mFramePadding")
reflectValue(decor,"mFrameOffsets")
private fun reflectValue(decor: View,filedName:String){
try {
var filed=decor.javaClass.getDeclaredField(filedName)
filed.isAccessible=true
val rect= filed.get(decor) as Rect
println("$filedName==========${rect.toShortString()}")
} catch (e: Exception) {
e.printStackTrace()
}
}
结果,这种我修改过宽度为屏幕宽1024,
mDrawingBounds==========[0,0][1024,204]
mBackgroundPadding==========[16,16][16,16]
mFramePadding==========[0,0][0,0]
mFrameOffsets==========[0,0][0,0]
可以看到mBackgroundPadding是有值的,那看下这个值设置的,可以查到是在给DecorView设置背景的时候
drawable.getPadding(mBackgroundPadding); 获取到的,好像我就知道InsetDrawable有padding,其他Drawable还真没研究过,抽空看看还有哪个带padding的
public void setWindowBackground(Drawable drawable) {
if (getBackground() != drawable) {
setBackgroundDrawable(drawable);
if (drawable != null) {
drawable.getPadding(mBackgroundPadding);
} else {
mBackgroundPadding.setEmpty();
}
drawableChanged();
}
}
找到哪里设置的,那么就简单了,现在查下系统啥时候给它设置的背景,背景哪里来的,可以看到用的就是主题里的windowBackground资源,那去找下这个资源文件啥样
protected ViewGroup generateLayout(DecorView decor){
if (mBackgroundResource == 0) {
mBackgroundResource = a.getResourceId(
R.styleable.Window_windowBackground, 0);
}
if (mFrameResource == 0) {
mFrameResource = a.getResourceId(R.styleable.Window_windowFrame, 0);
}
}
去主题里找下
因为现在用的都是Theme.AppCompat 主题,所以就去appcompat-v7下去找
<style name="Base.ThemeOverlay.AppCompat.Dark" parent="Platform.ThemeOverlay.AppCompat.Dark">
<item name="android:windowBackground">@color/background_material_dark</item>
<style name="Base.ThemeOverlay.AppCompat.Light" parent="Platform.ThemeOverlay.AppCompat.Light">
<item name="android:windowBackground">@color/background_material_light</item>
<style name="Base.V7.Theme.AppCompat.Dialog" parent="Base.Theme.AppCompat">
<item name="android:windowBackground">@drawable/abc_dialog_material_background</item>
<style name="Base.V7.Theme.AppCompat.Light.Dialog" parent="Base.Theme.AppCompat.Light">
<item name="android:windowBackground">@drawable/abc_dialog_material_background</item>
<style name="Base.V7.ThemeOverlay.AppCompat.Dialog" parent="Base.ThemeOverlay.AppCompat">
<item name="android:windowBackground">@drawable/abc_dialog_material_background</item>
<style name="Platform.AppCompat" parent="android:Theme.Holo">
<item name="android:windowBackground">@color/background_material_dark</item>
<style name="Platform.AppCompat.Light" parent="android:Theme.Holo.Light">
<item name="android:windowBackground">@color/background_material_light</item>
我们这里是dialog,那么肯定找dialog那个图片了abc_dialog_material_background
果然和猜想的一样,是个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="@android:color/white" />
</shape>
</inset>
总结
终于找到这个padding哪里设置的了。
中间碰到的问题
我studio打开了一份api27的DecorView源码,然后看到那个mResizingBackgroundDrawable【保存DecorView的背景的】是个全局变量,然后想通过反射获取来着,结果在我6.0的本子上一直提示反射失败,找不到这个字段。可我明明看到有,最后想着去api23的源码看了下,发现6.0的还真没这个字段。
哎。看来以后反射失败找不到字段的时候得注意下,是不是手机版本和自己看的api源码不一致。
解释下最上边解决办法的逻辑
有两种
- 这种分析过,这个方法最终里边调用了DecorView的setWindowBackground,而这个方法里会根据drawable获取padding,因为我们这里传的是ColorDrawable,所以padding就成0了。
window.setBackgroundDrawable(ColorDrawable(Color.RED))
- 下边这种,因为系统默认图片就是带padding的,而且也setpadding不为空,所以我们即需要修改padding为0,还得修改默认的背景图
window.decorView.setPadding(0,0,0,0)
window.decorView.setBackgroundColor(Color.RED)
//或者这样写也行,反正就是设置背景的3种方法,当然可别设置insetDrawable额
//window.decorView.background=ColorDrawable(Color.RED)
//window.decorView.setBackgroundResource()