Android内部分享[11]——创建弹框DialogFragment和AlertDialog

对话框是一个小窗口,提示用户做出决定或输入附加信息。对话框不会填满屏幕,通常用于一些在用户执行某些操作前的提前选择。

日期和时间对话框 日期和时间对话框

Dialog 类是对话框的基类,但是我们不应该直接去实例化一个 Dialog 类,而是要实例化它的子类:

  • AlertDialog:可以显示标题、最多三个按钮、可选项目列表或自定义布局的对话框。
  • DatePickerDialog:带有预定义UI的对话框,允许用户选择日期。
  • TimePickerDialog:带有预定义UI的对话框,允许用户选择时间。

上面这些类定义对话框的结构和样式,但是你应该使用 DialogFragment 作为对话框容器而不是使用上面的 Dialog 子类,DialogFragment 类提供了创建对话框并管理其外观所需的所有控件。使用 DialogFragment 管理对话框可以确保它正确地处理生命周期事件,例如当用户按下后退按钮或旋转屏幕时。DialogFragment 类还允许您将对话框的 UI 作为可嵌入组件重用到更大的 UI 中,就像传统的 Fragment 一样(例如,当您希望对话框 UI 在大屏幕和小屏幕上以不同的方式显示时)。

创建一个 DialogFragment

通过扩展 DialogFragment 并在 onCreateDialog() 回调方法中创建 AlertDialog,您可以完成各种各样的对话框设计,包括自定义布局和对话框设计指南中描述的那些。

例如,这里有一个基本的 AlertDialog,它是在 DialogFragment 中管理的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class MyDialogFragment extends DialogFragment {

    @NonNull
    @Override
    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {

        // 创建 AlertDialog
        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());

        //添加对话框提示信息
        builder.setMessage("一个测试弹框")

        //设置两个按钮文本和监听
        .setPositiveButton("确认", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                ToastUtils.showToast(getContext(), "点击了确认");
            }
        }).setNegativeButton("取消", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                ToastUtils.showToast(getContext(), "点击了取消");
            }
        });
        return builder.create();
    }
}

然后在 Activity 中调用显示这个弹框:

1
2
3
public void testSimpleDialog(View view) {
    new MyDialogFragment().show(getSupportFragmentManager(), "TestSimpleDialog");
}

显示弹框结果 显示弹框结果

上面演示了如何去显示一个 FragmentDialog,你可以通过从 Activity 中调用 getSupportFragmentManager() 或从 Fragment 中调用 getFragmentManager() 来获得 FragmentManager。第二个参数 “TestSimpleDialog” 是一个惟一的标记名称,系统使用它在必要时保存和恢复片段状态。标记还允许您通过调用 findFragmentByTag() 获得片段的句柄。

事实上我们可以不使用 FragmentDialog 来作为对话框容器,直接使用 AlertDialog 显示,但是不推荐你这样做,至于什么原因请往下看。

AlertDialog 显示

我们完全可以不使用 FragmentDialog,这是因为最早创建一个对话框的方式如下,当时还没有 FragmentDialog 这个类。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public void testSimpleAlertDialog(View view) {
    createAlertDialog().show();
}

private AlertDialog createAlertDialog(){
    AlertDialog.Builder builder = new AlertDialog.Builder(this);
    builder.setMessage("一个测试弹框")
            .setPositiveButton("确认", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    ToastUtils.showToast(MainActivity.this, "点击了确认");
                }
            }).setNegativeButton("取消", new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            ToastUtils.showToast(MainActivity.this, "点击了取消");
        }
    });
    return builder.create();
}

上面代码通过 AlertDialog.show() 方法来显示了弹框,显示的样式上和上图一致,没有什么变化。

一个 AlertDialog 可以设置标题、显示内容、动作按钮,如下图:

一个 AlertDialog 的三个元素 一个 AlertDialog 的三个元素

其实通过上面的两个弹框的代码你可以看出来了,创建 AlertDialog 要使用它的 Builder 类来实现,可以给设置标题、显示内容、动作按钮。本文出自水寒的博客,转载请说明出处:https://dp2px.com

第一步:创建 AlertDialog.Builder 对象

1
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());

第二步:设置标题和内容

1
builder.setMessage(R.string.dialog_message).setTitle(R.string.dialog_title);

第三步:调用 Builder 的 create() 方法创建 AlertDialog 对象。

1
AlertDialog dialog = builder.create();

第四步:添加动作按钮(可选)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
    public void onClick(DialogInterface dialog, int id) {
        // User clicked OK button
    }
});
builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
    public void onClick(DialogInterface dialog, int id) {
        // User cancelled the dialog
    }
});

Positive 按钮你应该使用它来接受并继续操作(“OK”操作)。 Negative 按钮你应该使用这个来取消操作。 Neutral 按钮当用户可能不想继续执行操作,但又不一定想要取消时,您应该使用此功能。

显示内容上除了上面的 setMessage() 去设置一行提示文本外,我们还可以通过 setItems() 方法来显示一个列表对话框。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
    AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
    builder.setTitle(R.string.pick_color)
           .setItems(R.array.colors_array, new DialogInterface.OnClickListener() {
               public void onClick(DialogInterface dialog, int which) {
                   //...
           }
    });
    return builder.create();
}

显示一个列表对话框 显示一个列表对话框

这些样式可能并不是我们想要的,有时候我们需要自定义内容样式,我们可以使用 setView() 方法来实现这种自定义。

1
2
3
4
AlertDialog.Builder builder = new AlertDialog.Builder(this);
View dialogView = getLayoutInflater().inflate(R.layout.custome_dialog_view, null);
builder.setView(dialogView);
builder.create().show();

使用 setView() 设置一个 View 对象给弹框即可,布局内容如下(使用的约束布局):

自定义弹框布局的约束关系 自定义弹框布局的约束关系

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?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:padding="40dip"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginEnd="8dp"
        app:layout_constraintBottom_toTopOf="@+id/textView"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_chainStyle="packed"
        app:srcCompat="@mipmap/ic_launcher" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="40dp"
        android:layout_marginEnd="8dp"
        android:text="水寒的博客:DP2PX.COM"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/imageView" />
</android.support.constraint.ConstraintLayout>

自定义的弹框 自定义的弹框

DialogFragment 的使用

从上面的这些弹框演示中可以看出来,DialogFragment 完全可以不存在,因为创建和显示一个弹框的内容和 DialogFragment 无关, DialogFragment 只是对弹框的声明周期和做一些显示和关闭的控制逻辑。所以严格意义上来说 DialogFragment 并不是弹框,而是一个弹框的控制和生命周期的包裹

DialogFragment 需要确保 Fragment 和 Dialog 状态发生的情况保持一致。 为此,它会监视对话框中的关闭事件,并在事件发生时删除其自身的状态。 这意味着您应该使用 show(android.app.FragmentManager, java.lang.String) 或 show(android.app.FragmentTransaction, java.lang.String) 向您的 UI 添加 DialogFragment 实例,因为它们可以跟踪如何 退出对话框时, DialogFragment 应该删除自身。还有一个重要原因是直接使用 AlertDialog 容易引起内存泄漏,举两个例子:

  1. 如果 dialog 消失时做了 1s 的动画,就可能出现 activity 被 finish 了,但 dialog 还存在的情况,出现内存泄漏。
  2. Message 是任何线程共用的,looper 会不停的从阻塞队列 messageQueue 中取出 message 进行处理。当没有可消费的 message 对象时,就会开始阻塞,如果最后取出的正好是 mDismissMessage,那么也会出现泄漏。

上面的弹框 AlertDialog 代码都可以放在 DialogFragment 的 onCreateDialog() 方法中去创建,其实我们也可以直接在 DialogFragment 中创建一个 View 返回显示。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class MyDialogFragment2 extends DialogFragment {

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, 
                             @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = requireActivity().getLayoutInflater().inflate(R.layout.custome_dialog_view, null);
        return view;
    }
}

DialogFragment 数据传递

我们的 Activity 和 DialogFragment 的数据传递和 Fragment 之间的数据传递是类似的,一个是我们需要将 Activity 中的数据变化通知到 DialogFragment,另一个是 DialogFragment 中的数据变化反馈到 Activity. 下面通过一个案例来说明一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class MyDialogFragment3 extends DialogFragment {

    private TextView mTextView;
    private EditText mEditTextView;
    private OnMyDialogListener mDialogListener;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater,
                             @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = requireActivity().getLayoutInflater().inflate(R.layout.custome_dialog_edit_view, null);
        mEditTextView = view.findViewById(R.id.editText);
        mTextView = view.findViewById(R.id.textView2);
        mEditTextView.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {

            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {

            }

            @Override
            public void afterTextChanged(Editable s) {
                if(mDialogListener != null){
                    mDialogListener.editChanged(getEditTextContent());
                }
            }
        });
        return view;
    }

    public interface OnMyDialogListener{
        public void editChanged(String content);
    }

    public void setMyDialogListener(OnMyDialogListener listener){
        mDialogListener = listener;
    }

    public String getEditTextContent(){
        if(mEditTextView == null) return "";
        return mEditTextView.getText().toString();
    }

    public void changeTextShow(String text){
        if(mTextView == null) return;
        mTextView.setText(text);
    }
}

其实过程很简单,很容易实现,外部的 Activity 中的 DialogFragment 对象可以监听我们定义的事件变化接口。

1
2
3
4
5
6
7
8
MyDialogFragment3 dialogFragment = new MyDialogFragment3();
dialogFragment.setMyDialogListener(new MyDialogFragment3.OnMyDialogListener() {
    @Override
    public void editChanged(String content) {
        ToastUtils.showToast(MainActivity.this, content);
    }
});
dialogFragment.show(getSupportFragmentManager(), "TestDialogFragmentDataRecive");

可以在示例代码中看到效果,本文所有代码均在 GitHub 仓库:https://github.com/lxqxsyu/InnerShareCode11

根据屏幕宽度选择是否将弹框嵌入到界面

在 UI 设计中,您可能希望 UI 的一部分在某些情况下显示为对话框,但在其他情况下显示为全屏或嵌入式片段(可能取决于设备是大屏幕还是小屏幕)。DialogFragment 类为您提供了这种灵活性,因为它仍然可以作为一个可嵌入的片段。

下面是一个 DialogFragment 示例,它既可以作为对话框出现,也可以作为可嵌入的片段出现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
boolean isLargeLayout = false;

public void testDialogCanEmbeddedFragment(View view) {
    FragmentManager fragmentManager = getSupportFragmentManager();
    MyDialogFragment4 dialogFragment = new MyDialogFragment4();
    FragmentTransaction transaction = fragmentManager.beginTransaction();

    if (isLargeLayout) {
        dialogFragment.show(fragmentManager, "TestDialogCanEmbeddedFragment");
    } else {
        transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
        transaction.add(R.id.fragment_container, dialogFragment)
                .addToBackStack(null).commit();
    }

    isLargeLayout = !isLargeLayout;
}

可以点击两次查看不同的显示效果。

关闭弹框

当用户触摸 AlertDialog 创建的任何操作按钮时。生成器,系统将为您取消对话框。当用户触摸对话框列表中的项目时,系统还会关闭对话框,除非该列表使用单选按钮或复选框。否则,您可以通过调用 DialogFragment 上的 remove() 来手动取消对话框。如果需要在对话框消失时执行某些操作,可以在对 DialogFragment 中实现 onDismiss() 方法。

DialogFragment 提供了两个关闭的方法,分别是 dismiss() 和 dismissAllowingStateLoss(),前者对应的是 fragmentTransaction.commit(),后者对应的是 fragmentTransaction.commitAllowingStateLoss()。用 dismissAllowingStateLoss() 的好处是可以让我们忽略异步关闭 dialog 时的状态问题,让我们不用考虑当前 activity 的状态,这会减少很多线上的崩溃。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public void dismiss() {
    dismissInternal(false);
}

public void dismissAllowingStateLoss() {
    dismissInternal(true);
}

void dismissInternal(boolean allowStateLoss) {
    mDismissed = true;
    mShownByMe = false;
    if (mDialog != null) {
        mDialog.dismiss();
        mDialog = null;
    }
    mViewDestroyed = true;
    
    // 处理多个dialogFragment的问题
    if (mBackStackId >= 0) {
        getFragmentManager().popBackStack(mBackStackId,
                FragmentManager.POP_BACK_STACK_INCLUSIVE);
                
        mBackStackId = -1;
    } else {
        FragmentTransaction ft = getFragmentManager().beginTransaction();
        ft.remove(this); // 移除当前的fragment
        
        if (allowStateLoss) {
            ft.commitAllowingStateLoss();
        } else {
            ft.commit();
        }
    }
}

另外说一下 dismiss() 和 cancel() 的区别:

  • dismiss() 表示用户离开了对话框,不完成任何任务,等于忽略了对话框。
  • cancel 表示用户主动取消了当前操作,是一个主动的选择。
  • 调用 onCancel() 后默认会立即调用 onDismiss().
  • 调用 dialogFragment.dismiss() 后并不会触发 onCancel().
  • 当用户在对话框中按 “ok” 按钮后,从视图中移除对话框时,会自动调用 onDismiss().

本文源码下载地址:https://github.com/lxqxsyu/InnerShareCode11