优雅解决DialogFragment所有布局问题

原文:优雅解决DialogFragment所有布局问题 - 知乎 (zhihu.com)

关于本文

关于DialogFragment的布局问题,相信很多人都遇到过,如设置背景颜色无效、无法全屏、无法隐藏状态栏等等,而这些问题网上的各种博客也提供许多解决方法。但我查阅了多篇博客之后,发现解决方法很多种,有相同的,也有不同的,太杂了,有的还不一定生效。

经过我自己一番的时间的研究与实操,这里将告诉你,如何以一种非常优雅的方式,统一解决掉你遇到过的几乎所有DialogFragment的布局问题,并告诉你DialogFragment其他一些你可能不知道的东西。

先说Title,本文将以一种不同于大多数网上博文的方式,更优雅、更有效、更统一的解决以下两个问题:

1、实际的显示布局效果错乱问题,如背景颜色、背景透明、设置宽高均无效,无法撑满屏幕等问题。

2、无法隐藏状态栏内容或高度,实现不了沉浸式布局。

对于弹窗Dialog,从Android3.0开始,Google官方就不太推荐使用Dialog了,而是推荐使用DialogFragment,至于原因,本文就不做过多描述了吧,简要说两点,其余更详细的,我会在其他文章里进行归纳、描述。

选DialogFragment不选Dialog的主要原因:

1、DialogFragment本质上也是一个Fragment,其生命周期由FragmentManager统一管理,可做到Activity因为某种原因自动重建时,DialogFragment也可被正确恢复,这点Dialog做不到。
2、由于Fragment本身可以和与之绑定的Activity进行数据通信,因而使用DialogFragment实现的弹窗,也很方便与Activity进行数据通讯。

既然如此,为什么还是会有很多Android开发者更愿意使用Dialog而不太愿意使用DialogFragment呢?主要还是因为Dialog比较独立,使用起来更方便。DialogFragment不熟悉的话,比较多坑,比如本文说的布局显示问题,很多时候,一个正常的布局文件,在Android Studio里显示是正常的,使用Dialog来承载显示也是正常的,但如果使用DialogFragment来承载显示,就不一定正常了,可能会出现各种奇怪的现象,比如:

  • 设置宽高无效
  • 设置背景透明无效
  • 设置背景颜色无效
  • 无法全屏

等等,我有理由相信,初次使用DialogFragment的你,很大概率都遇到过上面所罗列的问题。然后去百度、Google,查到各种博文,会发现解决方法有很多种,有的说在布局文件里多嵌套一层父View,有的说在onCreate方法里头设置某些FLAG,有的说在onCreateView里且在setContentView之前设置window的某些属性,还有的说在onResume里设置window宽高……等等。

事实上,解决方法有多种很正常,关键是正是由于网上说的解决方案太多了,不容易去记住,哪种处理是解决哪个问题的,比如,如果按照上述所说的解决思路,设置宽高无效和设置背景透明无效的处理方式是不一样的,那么下次遇到同样的问题时,可能你还得去重新查。其实有一种更简单、更高效、更优雅的解决方式,可解决几乎所有你遇到的DialogFragment布局显示问题,包括上述罗列的那几个,这就是本文想要告诉你的。

解决DialogFragment布局显示问题的终极解决方案

其实,上面所说到的多种解决思路,有一个共性:就是要解决某个问题,就需要在某个方法被回调之前或者调用某个方法之前,设置某些FLAG、设置某些属性,然后这个共性,这里浓缩为属性的配置时机。因为有一个配置时机的先后问题,因此同样是设置同一个属性,在某句代码或某个方法前设置或者后设置可能得到的效果不一样。在DialogFragment中,onCreate、onCreateView、onResume等都属于生命周期的方法,且是被动调用的,正是因为是被动调用,才会出现各种时机不对的问题。举个例子,看下面的示例代码:

public interface Callback {
    void onCallback();
}
​
private Callback callback;
​
public void method() {
    //...do something
    
    if (callback != null) {
        callback.onCallback();
    }
    
    //...do something
}

我们平时所看到的onCreate、onCreateView、onResume等回调方法,就类似上述代码一样,在回调前,原方法可能会做某些操作,回调后,原方法也可能会做某些操作,而我们平时应该也这么写过,这也就是为什么在被动调用的方法里头做某些设置会出现无效的问题。

说回DialogFragment,由于它属于弹窗控件,系统对它的布局主要是通过主题属性来控制,即平常在style文件里头设置的各种Style Theme,而你平常去查到的很多解决思路,设置FLAG,设置window属性,其实也是在对这个Dialog的主题进行配置。那么之前所提到的属性配置时机在这里头就表现为:如果在onCreateView里设置FLAG,那系统底层到底是先去读取已配置好的FLAG或者是默认配置的FLAG,还是先回调onCreateView,如果先回调onCreateView再配置各种FLAG,那么我们在onCreateView设置FLAG是有效的,反过来,就无效,因为程序已经走过了设置FLAG的步骤。

而这里说有一种方式可以解决绝大多数的布局显示问题,就是在任何DialogFragment的生命周期方法被回调之前全部配置好想要的主题效果,就可以忽略由于回调的时机先后而产生的各种问题了,当然,这里就不过多去描述系统底层的回调时机和逻辑了,有兴趣的,可自行阅读源代码。

那么,在什么时候配置主题效果,可以在所有生命周期方法执行之前先执行呢?答案就是创建DialogFragment对象的时候。我们在创建DialogFragment对象时,就直接new一个DialogFragment,传入所需参数即可,但如果我们不调用show方法的话,创建好的这个DialogFragment对象啥事都不会发生,也不会触发生命周期的方法调用。于是,我们就可以在调用show方法前,设置好所有需要的主题效果,即构造方法中设置。

但是这里强烈不建议,甚至说建议你禁止在直接在构造方法里头设置主题效果,如果你非要在构造方法里头设置,请确保这个构造方法是无参的。但是问题来了,无参的构造方法并不能满足绝大多数的外部设置数据需求,希望外部能传参到DialogFragment还是非常常见的场景需求的,但是对于DialogFragment,我们最好不要定义有参的构造方法,理由简单说下:

1、如果定义了有参的构造函数,请确保多重载一个无参的构造函数。这是因为,DialogFragment在被自动重建进行恢复的时候,会默认调用无参的构造函数来创建新实例,如果此时我们得DialogFragment没有无参构造方法,DialogFragment重建时就会产生一个叫找不到默认构造方法的异常,进而产生崩溃;
2、如果我们定义了有参构造函数,同时也重载了一个默认的无参构造函数,这时DialogFragment在重建时不会产生异常了,但是随之而来的问题是,你在有参构造函数里进行数据初始化操作,而重建时并不会调用你的有参构造函数,更不会凭空产生你原本主动调用有参构造函数时所穿的参数,因此,重建恢复回来的DialogFragment是数据状态不正确的。

也正是因为上述两点原因,官方推荐的初始化DialogFragment的做法是,在DialogFragment定义一个静态的newInstance方法,然后通过这个静态方法进行传参,并初始化,而我们,也是在这个方法中进行主题属性的配置,下面看代码。

解决样式错乱

第一步: newInstance里引用自定义主题样式的Style

public static MyDialogFragment newInstance(long id, String name) {
    MyDialogFragment dialog = new MyDialogFragment();
    // 设置主题,这里只能通过xml方式设置主题,不能通过Java代码处理,因为这是getWindow还是null,
    // 而且window的几乎所有属性,都可以通过xml设置
    dialog.setStyle(STYLE_NORMAL, R.style.MyDialogTheme);
    // 设置触摸、点击弹窗外部不可关闭
    dialog.setCancelable(false);
​
    // 对于DialogFragment,设置外部传的参数,通过bundle设置,然后在onCreateView读取
    Bundle bundle = new Bundle();
    bundle.putLong("id", id);
    bundle.putString("name", name);
​
    // 把外部传进的参数放到bundle里, 在onCreateView里通过继续getArguments()读取参数,
    // 通过bundle来处理,是因为就算DialogFragment被重建了,也能恢复回来并初始化
    dialog.setArguments(bundle);
​
    return dialog;
}

第二步:自定义配置主题属性

第二步是解决各种布局问题的关键,代码很简单,如下:

 <!-- 这里的parent必须是Theme.AppCompat.Dialog -->
<style name="MyDialogTheme" parent="Theme.AppCompat.Dialog">
    <!-- 这两个属性对于一个常规的Dialog,一般必须设置的-->
    <!-- 这两个属性按照下面的值设置之后,确保了弹窗的实际显示效果,跟你在layout文件中的定义效果是一样的 -->
    <item name="android:windowIsFloating">false</item>
    <item name="android:windowBackground">@android:color/transparent</item>
</style>

很简单,就这样子就行了,无需继续在DialogFragment中的onCreate、onCreateView、onResume中继续做其他操作,你可以试试,当然,指定的属性到底是什么意思,你ctrl+鼠标左击进去看源码注释说明就可。

沉浸式布局实现

上面的主题样式属性配置,仅仅是解决了你的layout文件根布局无法设置背景、指定宽高无效等问题,要实现沉浸式布局还需要其他额外的主题属性配置,但同样的,也是配置好Style的自定义主题就行了,这里先说一下沉浸式布局的两种情况:

1、显示状态栏内容,但状态栏背景是透明的。

2、完全隐藏状态栏,即内容和高度都消失。

沉浸式 - 透明状态栏

 <!-- 这里的parent必须是Theme.AppCompat.Dialog -->
<style name="FullSreenDialogTheme" parent="Theme.AppCompat.Dialog">
    <!-- 上面说过,只要是Dialog,这两个属性必须设置 -->
    <item name="android:windowIsFloating">false</item>
    <item name="android:windowBackground">@android:color/transparent</item>
    
    <!--设置透明状态栏,适用于SDK19(4.4)及以上版本-->
    <item name="android:windowTranslucentStatus" >true</item>
    <!-- 如果你不需要自定义状态栏颜色,下面两个可不要 -->
    <item name="android:windowDrawsSystemBarBackgrounds" >true</item>
    <item name="android:statusBarColor">@android:color/transparent</item>
    
    <!-- 透明导航栏 -->
    <item name="android:windowTranslucentNavigation">true</item>
</style>

沉浸式 - 无状态栏

 <!-- 这里的parent必须是Theme.AppCompat.Dialog -->
<style name="FullSreenDialogTheme" parent="Theme.AppCompat.Dialog">
    <!-- 上面说过,只要是Dialog,这两个属性必须设置 -->
    <item name="android:windowIsFloating">false</item>
    <item name="android:windowBackground">@android:color/transparent</item>
    
    <!--隐藏状态栏内容和高度,适用于SDK19(4.4)及以上版本-->
    <item name="android:windowFullscreen">true</item>
    <!-- 对于28及以上的版本,需要指定该属性,否则对于异形屏的手机,无法让布局铺满异性区域 -->
    <item name="android:windowLayoutInDisplayCutoutMode" >shortEdges</item>
    <item name="android:windowTranslucentStatus" >true</item>
    
    <!-- 透明导航栏 -->
    <item name="android:windowTranslucentNavigation">true</item>
</style>

这里需要注意的是,网上很多博文提到的隐藏状态栏的方案,几乎都没有配置这个属性:

<item name="android:windowLayoutInDisplayCutoutMode" >shortEdges</item>

导致很多网友在采纳他们的方案时,往往不成功,当然也有成功的情况。

这是为什么呢?这主要是跟Android系统的版本有关,在Android 27(8.1)及以下的系统版本,不需指定windowLayoutInDisplayCutoutMode的配置就可真正隐藏状态栏了,于是乎,很多博主高高兴兴的认为自己的配置可行,便把自己的实现方案分享于博客之中,但却忽略了Android 版本的问题,导致有些网友在采纳时不生效的情况发生。

这里简单说明一下,安卓手机在开始有刘海屏之后,相继出现了其他的异形屏,如水滴屏,挖孔屏等,有了异形屏之后,系统底层限制这个异形区域高度为非常规绘制区域,即默认不会允许我们自定义的绘制内容绘制到这个区域,当然真要绘制也是可以的,于是就有了各种刘海屏、异形屏的适配解决方案出来。

而单独拿DialogFragment主题来说,DialogFragment本身依托于window,那么有些布局显示是会受到window的限制的,而在window的主题属性配置里头,有这么一个类:DisplayCutout,官方源码解释是不可用的功能显示区域,

这是源码中,对windowLayoutInDisplayCutoutMode默认值的一个说明,你仔细品:

The window is allowed to extend into the  DisplayCutout area, only if the  DisplayCutout is fully contained within a system bar. Otherwise, the window is laid out such that it does not overlap with the  DisplayCutoutarea.

而windowLayoutInDisplayCutoutMode属性是在Android SDK 28版本以后才开始有的,在这之前,直接设置

<item name="android:windowFullscreen">true</item>

就可以隐藏状态栏,使得我们得layout布局能够全屏占满,包括异形屏,只要Android SDK版本小于28就行。

但是从SDK 28开始,Google 官方就新增了windowLayoutInDisplayCutoutMode这个属性,默认情况下,我们得layout布局不会占用异形屏区域,想要占用,则需指定该属性的值为shortEdges才行,即:

<item name="android:windowLayoutInDisplayCutoutMode" >shortEdges</item>

而这个配置,如果不是在Style.xml中配置,在Java代码中配置的话,就需要在DialogFragment中的onCreateView方法里头指定:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
    Window window = getDialog().getWindow();
    WindowManager.LayoutParams lp = window.getAttributes();
    // 关键是这句,其实跟xml里的配置长得几乎一样
    lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
    window.setAttributes(lp);
}

所以,你才会看到有些博客文章也出现了这段代码,其实两者一样的,但上述代码中,getDialog()和getWindow()都可能会出现null的情况,而在style.xml统一配置,安全、抽象、可扩展、可复用,它不更香吗?这就是所谓的:优雅

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值