当前位置:网站首页>使用DialogFragment的一些感受及防踩坑经验(getActivity、getDialog为空,cancelable无效等)

使用DialogFragment的一些感受及防踩坑经验(getActivity、getDialog为空,cancelable无效等)

2022-04-23 14:05:00 森之千手

前言

安卓里面创建对话框的方式其实蛮多的。Dialog、AlertDialog、DialogFragment、PopupWindow甚至Activity设置主题为Dialog也能创建对话框。其中,AlertDialog和DialogFragment都是基于Dialog创建的。AlertDialog可以看作是官方的一个Dialog封装类。DialogFragment是在Fragment中内嵌了一个Dialog对象,其最大的好处就是能保证在屏幕方向切换的时候保留之前的状态不变,而且官方也推荐使用DialogFragment,于是项目中就逐渐改为使用它。但是,遇到的坑也是不少的,这里就专门写这篇文章来讲讲,也当作日后的笔记。

一、如何给对话框设置样式

因为dialogFragment内置了一个dialog实体,通过这个dialog实体,我们能使用dialog一切的方法去配置这个窗体:

	Window window=getDialog().getWindow();
	window.setWindowAnimations(R.style.BottomWindowAnim);//设置窗体动画样式
	WindowManager.LayoutParams lp = window.getAttributes();
	lp.gravity = Gravity.BOTTOM;//设置窗体位置
	lp.dimAmount = 0.7f;//设置蒙层透明度 0~1
	lp.width = (int) (getResources().getDisplayMetrics().widthPixels*0.8);//设置窗体宽度(可以是具体的像素值,也可以是WRAP_CONTENT之类的)
	lp.height = ViewGroup.LayoutParams.WRAP_CONTENT;//设置窗体高度
	getDialog().onWindowAttributesChanged(lp);//手动触发,应用配置

二、如何设置点击蒙层或者返回键不让窗体消失

按照之前使用dialog时的经验,我们应该知道,dialog上可以这么设置:

	dialog.setCancelable(false); //设置点击返回键不消失
	dialog.setCanceledOnTouchOutside(false);//设置点击蒙层不消失

然而,当你这样设置后,你会发现,只有点击蒙层那个生效了。点击返回键依旧能消失。

对于返回键的处理,我们应该调用dialogFragment内部的setCancelable来实现:

	dialogFragment.setCancelable(cancelable);

其实这么设计的原因也很好理解。dialog里面的cancelable只能管到它自己,而它的宿主fragment是没法控制的。所以这个方法要写在fragment里面。

三、调用getDialog()返回null

比如在上面的问题中,设置canceledOnTouchOutside的时候报了getDialog()空指针的异常.要弄明白这个问题,我们先来简单看看dialogFragment创建的时候会经历哪些关键阶段。

	@Override
    public void onAttach(@NonNull Context context) {
    
        super.onAttach(context);
        Log.d(TAG, "onAttach.getDialog->" + getDialog());
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
    
        super.onCreate(savedInstanceState);
        Log.d(TAG, "onCreate.getDialog->" + getDialog());
    }

    @NonNull
    @Override
    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
    
        Log.d(TAG, "onCreateDialog.getDialog->" + getDialog());
        return super.onCreateDialog(savedInstanceState);
    }

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    
        Log.d(TAG, "onCreateView.getDialog->" + getDialog());
        rootView = inflater.inflate(getLayoutId(), null);
        return rootView;
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    
        super.onViewCreated(view, savedInstanceState);
        Log.d(TAG, "onViewCreated.getDialog->" + getDialog());
    }

我在上述的代码中打了日志标签,我们来看看走向:

2021-09-18 09:51:56.779 15075-15075/com.cjs.kotlinapp D/BDF: onAttach.getDialog->null
2021-09-18 09:51:56.780 15075-15075/com.cjs.kotlinapp D/BDF: onCreate.getDialog->null
2021-09-18 09:51:56.780 15075-15075/com.cjs.kotlinapp D/BDF: onCreateDialog.getDialog->null
2021-09-18 09:51:56.786 15075-15075/com.cjs.kotlinapp D/BDF: onCreateView.getDialog->android.app.Dialog@8983aa6
2021-09-18 09:51:56.836 15075-15075/com.cjs.kotlinapp D/BDF: onViewCreated.getDialog->android.app.Dialog@8983aa6

不难看出,getDialog()从onCreateView开始,才会有值。
实际上,getDialog返回的是onCreateDialog中返回的dialog。
真相
在这里插入图片描述

也就是说,我们最早能取到dialog值的阶段,就是onCreateDialog中,不过要写成这个形式:

	@NonNull
    @Override
    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
    
        Dialog dialog = super.onCreateDialog(savedInstanceState);
  		//在这里可以写一些dialog的配置操作,例如
  		dialog.setCanceledOnTouchOutside(false);
        return dialog;
    }

因此,我们最早能设置canceledOnTouchOutside的阶段,就是这里了。这样一来,就不会报空指针异常了。

四、窗体大小显示异常

1、问题重现

举个例子,在自定义对话框时,你在配置完后看到的窗体是这样的:
窗体显示异常
窗体布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="10dp">

    <TextView android:id="@+id/tv_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center_vertical" android:drawableLeft="@drawable/ic__info" android:drawablePadding="2dp" android:textColor="@color/dialog_title" android:textSize="18sp" />

    <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:gravity="center" android:orientation="vertical">

        <TextView android:id="@+id/tv_msg" android:minLines="2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="@color/dialog_msg" android:textSize="14sp" />
    </LinearLayout>

    <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="right" android:orientation="horizontal">
        <!--这里的按钮采用TextView而不是用Button的原因是Button内部会有默认的背景,自带宽高,不好格式化-->
        <TextView android:visibility="gone" android:id="@+id/btn_cancel" style="@style/DialogButton" android:textColor="@color/dialog_button_cancel" />

        <TextView android:id="@+id/btn_submit" style="@style/DialogButton"/>
    </LinearLayout>
    
</LinearLayout>

很简单,就是外部一个LinearLayout。然后外层布局宽高的都是match_parent

2、问题分析&解决

出现上述窗体显示异常的原因就是你在使用本文第一点配置对话框时,那些代码的填写位置错误。先来看看第一行代码:

Window window=getDialog().getWindow();

首先,需要确保getDialog()不能为空。根据之前的分析,最早可以在onCreateDialog里面拿到值。但是如果你因此就将第一点的代码写在这里面,恭喜你,成功捕获异常弹窗一枚。
为什么呢?再来想想我们这行代码,我们操作的不是dialog,而是它内部的window对象。我们给对话框的宽高设置的是match_parent。因此就会涉及到其宿主布局的宽高。对话框的宿主是fragment,fragment创建布局的时候是在onCreateView中。view创建完成时是在onViewCreated(View v)中,这里面的v就是onCreateView中返回的那个。view创建完后,自然就有了宿主布局的宽高了,此时再调用第一点的方法,就能解决问题。
正常弹窗

	@Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    
        super.onViewCreated(view, savedInstanceState);
        initWindow(getDialog().getWindow());
    }

这里的initWindow方法就是第一点里面的内容。

五、getActivity()返回null

1、问题重现

dialogFragment中有这么一个方法:
show
该方法用于显示一个dialogFragment。这两个方法其实对应的是Fragment创建时的一些过程。其中,第一个fragmentManager参数,一般来讲,我们可以使用activity的getSupportFragmentManager来取到,于是乎,我们可以自行封装一个简单的show方法:

	public void show(){
    
        show(getActivity().getSupportFragmentManager(),"XXXX");
    }

假如我们自定义了一个MsgDialog的弹窗,要显示这个弹窗,我们可以这样写:

MsgDialog d=new MsgDialog();
d.show();

看起来,是要比官方的方法调用简单很多。但是一旦你运行,就会报getActivity()空指针的异常。

2、问题分析&解决

首先,我们依照解决getDialog为空的解决方法思考,看看,是不是哪一步生命周期里面没有获取到getActivity。

	@Override
    public void onAttach(@NonNull Context context) {
    
        super.onAttach(context);
        Log.d(TAG, "onAttach.getActivity->" + getActivity());
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
    
        super.onCreate(savedInstanceState);
        Log.d(TAG, "onCreate.getActivity->" + getActivity());
    }

    @NonNull
    @Override
    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
    
        Log.d(TAG, "onCreateDialog.getActivity->" + getActivity());
        return super.onCreateDialog(savedInstanceState);
    }

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    
        Log.d(TAG, "onCreateView.getActivity->" + getActivity());
        rootView = inflater.inflate(getLayoutId(), null);
        return rootView;
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    
        super.onViewCreated(view, savedInstanceState);
        Log.d(TAG, "onViewCreated.getActivity->" + getActivity());
    }

但是一看日志,就惊呆了:

2021-09-18 10:48:06.645 15631-15631/com.cjs.kotlinapp D/BDF: onAttach.getActivity->com.cjs.kotlinapp.MainActivity@97996d1
2021-09-18 10:48:06.645 15631-15631/com.cjs.kotlinapp D/BDF: onCreate.getActivity->com.cjs.kotlinapp.MainActivity@97996d1
2021-09-18 10:48:06.645 15631-15631/com.cjs.kotlinapp D/BDF: onCreateDialog.getActivity->com.cjs.kotlinapp.MainActivity@97996d1
2021-09-18 10:48:06.648 15631-15631/com.cjs.kotlinapp D/BDF: onCreateView.getActivity->com.cjs.kotlinapp.MainActivity@97996d1
2021-09-18 10:48:06.678 15631-15631/com.cjs.kotlinapp D/BDF: onViewCreated.getActivity->com.cjs.kotlinapp.MainActivity@97996d1

所有的生命周期都能取到值。onAttach已经是最早的了,这里都能取到值,自然后面的都能。
还有没有比attach更早呢?想了一下,只剩下MsgDialog的构造函数了。,于是乎,我在构造函数里面也打印了一下,结果就发现问题原因了:

2021-09-18 10:48:06.628 15631-15631/com.cjs.kotlinapp D/BDF: 构造
2021-09-18 10:48:06.645 15631-15631/com.cjs.kotlinapp D/BDF: onAttach.getActivity->com.cjs.kotlinapp.MainActivity@97996d1
2021-09-18 10:48:06.645 15631-15631/com.cjs.kotlinapp D/BDF: onCreate.getActivity->com.cjs.kotlinapp.MainActivity@97996d1
2021-09-18 10:48:06.645 15631-15631/com.cjs.kotlinapp D/BDF: onCreateDialog.getActivity->com.cjs.kotlinapp.MainActivity@97996d1
2021-09-18 10:48:06.648 15631-15631/com.cjs.kotlinapp D/BDF: onCreateView.getActivity->com.cjs.kotlinapp.MainActivity@97996d1
2021-09-18 10:48:06.678 15631-15631/com.cjs.kotlinapp D/BDF: onViewCreated.getActivity->com.cjs.kotlinapp.MainActivity@97996d1

我们显示窗体的操作,是先new再show。只new的时候,会发现,不会走dialogFragment的任何生命周期,走的话,必须是在调用show后。然后去看了看show的源码:

	/** * Display the dialog, adding the fragment to the given FragmentManager. This * is a convenience for explicitly creating a transaction, adding the * fragment to it with the given tag, and {@link FragmentTransaction#commit() committing} it. * This does <em>not</em> add the transaction to the fragment back stack. When the fragment * is dismissed, a new transaction will be executed to remove it from * the activity. * @param manager The FragmentManager this fragment will be added to. * @param tag The tag for this fragment, as per * {@link FragmentTransaction#add(Fragment, String) FragmentTransaction.add}. */
    public void show(@NonNull FragmentManager manager, @Nullable String tag) {
    
        mDismissed = false;
        mShownByMe = true;
        FragmentTransaction ft = manager.beginTransaction();
        ft.add(this, tag);
        ft.commit();
    }

发现只有在调用show的时候才会往FragmentTransaction 中添加fragment然后才commit。所以答案就很清楚了,只new的时候就只相当于创建了一个电吹风,只有调用show插上电源后,吹风才会转动。
所以我们还是老老实实哪里调用,就传哪里的fragmentManager.

版权声明
本文为[森之千手]所创,转载请带上原文链接,感谢
https://blog.csdn.net/cjs1534717040/article/details/120351632