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 中管理的:

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 中调用显示这个弹框:

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

显示弹框结果

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

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

AlertDialog 显示

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

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 要使用它的 Builder 类来实现,可以给设置标题、显示内容、动作按钮。本文出自水寒的博客,转载请说明出处:https://dp2px.com

第一步:创建 AlertDialog.Builder 对象

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

第二步:设置标题和内容

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

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

AlertDialog dialog = builder.create();

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

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() 方法来显示一个列表对话框。

@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() 方法来实现这种自定义。

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 对象给弹框即可,布局内容如下(使用的约束布局):

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

<?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 返回显示。

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. 下面通过一个案例来说明一下:

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 对象可以监听我们定义的事件变化接口。

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 示例,它既可以作为对话框出现,也可以作为可嵌入的片段出现。

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 的状态,这会减少很多线上的崩溃。

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