Dialog最佳实践

本文会不定期更新,推荐watch下项目。

如果喜欢请star,如果觉得有纰漏请提交issue,如果你有更好的点子可以提交pull request。

本文的示例代码主要是基于EasyDialog这个库编写的,若你有其他的技巧和方法可以参与进来一起完善这篇文章。

本文固定连接:https://github.com/tianzhijiexian/Android-Best-Practices


背景

Dialog最佳实践_第1张图片
image_1bk93uogp1qhvr2911u1p3f10ms6c.png-124kB

无论是大型项目还是小型项目,设计给出的对话框样式都是千变万化的,很难形成统一的模块化风格。经过长期的分析发现下列问题普遍存在在各个项目中:

  • 不用android原生的dialog样式,全部自定义
  • dialog没有统一的风格,至少有三种以上的风格
  • 自定义dialog众多,没有统一设计,难以扩展和关联
  • 多数dialog和业务强绑定,独立性极差

我们希望可以利用原生的api来实现高扩展性的自定义的dialog。经过长期的探索,我找到了一个更加轻量的集成方案。

需求

  • 模块化的封装dialog,由dialogfragment来做管理者
  • 利用原生的api来配置dialog,降低学习成本
  • 让dialog的builder支持继承,实现组合+继承的形式
  • 一个配置项可将原本的自定义dialog变成从底部弹出的样式
  • 允许设置dialog的背景,支持透明背景
  • 可通过直接修改style的方式,将原生dialog变成自定义的样式
  • 屏幕旋转后dialog中的数据不应丢失
  • 能监听到dialog的消失、点击空白处关闭等事件
  • dialog可以和activity之间进行事件联动
  • 实现从底部拉出的dialog样式

实现

模块化的封装Dialog

我们最早就有了dialog这个类,我们一般都会用它的子类——alertDialog,在v7中的AlertDialog还提供了theme和各种能力(单选、多选),一般在activity中的用法如下:

new AlertDialog.Builder(this)
        .setTitle("title")
        .setIcon(R.drawable.ic_launcher)
        .setPositiveButton("好", new positiveListener())
        .setNeutralButton("中", new NeutralListener())
        .setNegativeButton("差", new NegativeListener())
        .creat()
        .show();

但这里有个很明显的问题——dialog的独立性太差!

因为alertDialog是通过builder的形式new出来的,所以它让dialog丧失了可继承的特性。如果一个项目里面的dialog有一些通用的代码,我们肯定要进行整理。如果你还希望dialog能被统一管理,那么肯定要建立一个封装类

public class DialogHelper {

    private String title, msg;

    /**
     * 各种自定义参数,如:title
     */
    public void setTitle(String title) {
        this.title = title;
    }

    /**
     * 各种自定义参数,如:message
     */
    public void setMsg(String msg) {
        this.msg = msg;
    }

    public void show(Context context) {
        // 通过配置的参数来建立一个dialog
        AlertDialog dialog = new AlertDialog.Builder(context)
                .setTitle(title)
                .setMessage(msg)
                .create();
        // ...
        // 通用的设置
        Window window = dialog.getWindow();
        window.setBackgroundDrawable(new ColorDrawable(0xffffffff)); // 白色背景
        dialog.show();
    }
}

包装类解决了重复代码多的问题,但是仍旧没有解决dialog数据保存和生命周期管理等问题。后来google在android3.0的时候引入了一个新的类:dialogFragment。现在,我们完全可以使用dialogFragment作一个control来管理alertDialog。

public class MyDialogFragment extends DialogFragment{

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        // 不推荐的写法
        return inflater.inflate(R.layout.dialog, null);
    }
}

注意,注意!
如果你在onCreateView中做了dialog布局,那么我们之前的所有工作都可能没有意义了,而且会破坏模块化。我强烈建议通过onCreateDialog来建立dialog!

Dialog最佳实践_第2张图片

正确的做法是AlertDialog被DialogFragment管理,DialogFragment被FragmentManager管理,这样才是真正的面向对象的封装方式,代码自然也会干净很多。

@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
    Builder builder = new AlertDialog.Builder(getActivity());
    builder.setTitle("我是标题")
        .setMessage(getResources().getString(R.string.hello_world))
        .setPositiveButton("我同意", this)
        .setNegativeButton("不同意", this)
        .setCancelable(false);
        //.show(); // show cann't be use here
    
    return builder.create();
}

如果你要做自定义的dialog,那么直接通过setView就能做到:

builder.setView(view)  // 设置自定义view

这样的话他们的职责就很明确了:

  1. fragmentManager管理fragment的生命周期和activity的绑定关系
  2. dialogFragment来处理各种事件(onDismiss等)和接收外部传参(bundle)
  3. alertDialog负责dialog的内容和样式的展示
public class MyDialog extends DialogFragment{

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Bundle bundle = getArguments();
        // ...
        // 得到各种配置参数
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        // 根据得到的参数,建立一个dialog
        return new AlertDialog.Builder(getActivity())
                .setMessage("message")
                .create();
    }

    @Override
    public void onDismiss(DialogInterface dialog) {
        super.onDismiss(dialog);
        // 处理响应事件
    }
}

至此,dialog三部曲就已经完成:

  1. 在onCreate中拿到外部传入的参数
  2. 在onCreateDialog中构建一个alertDialog对象
  3. 通过DialogFragment的show()来显示对话框

理解DialogFragment的方法调用

因为fragment本身就是一个复杂的管理器,而且很多开发者对于dialogFragment中的各种回调方法会产生理解上的偏差,所以我做了下面的图示:

Dialog最佳实践_第3张图片
image_1bk895qo21fb71qkn14bb1vk3175p9.png-36.3kB
public class MyDialog extends android.support.v4.app.DialogFragment {

    private static final String TAG = "MyDialog";

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 得到各种外部参数
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
        // 这里返回null,让fragment作为一个control
        return null;
    }

    @NonNull
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        // 根据参数建立dialog
        return new AlertDialog.Builder(getActivity())
                .setMessage("msg")
                .setTitle("title")
                .create();
    }

    @Override
    public void setupDialog(Dialog dialog, int style) {
        super.setupDialog(dialog, style);
        // 上面建立好dialog后,这里可以进行进一步的配置操作
    }


    @Override
    public void onStart() {
        super.onStart();
        // 这里的view来自于onCreateView,所以是null
        View view = getView(); 
        // ...
        // 可以进行dialog的findViewById操作
    }
}

调用流程为:

Dialog最佳实践_第4张图片
image_1bk8b5uh6ikp3the4k1iv8ote13.png-49.9kB

这里需要注意的是:只有在onStart中再去执行findView操作,因为在此之前window还没有配置完成,WindowManager还没有把整个window挂载上去,会报错!

利用原生的Builder进行传参

上面说到了我们可以通过intent给fragment进行传参,但实际中的参数数目会很多。一般的项目中我们都会建立一个简单的Serializable对象来做一次性装配,最后将其再塞给fragment。

public class BuildParams implements Serializable {

    public int mIconId = 0;

    public int themeResId;

    public CharSequence title;

    public CharSequence message;

    public CharSequence positiveText;

    public CharSequence neutralText;

    public CharSequence negativeText;

    public CharSequence[] items;

    public boolean[] checkedItems;

    public boolean isMultiChoice;

    public boolean isSingleChoice;

    public int checkedItem;

}

有了数据对象后,我们自然要想到要通过build模式将其进行封装:

public class BuildParamsBuilder {

    private int mIconId;

    private int mThemeResId;

    // ... 省略部分参数
 
    public BuildParamsBuilder setIconId(int iconId) {
        mIconId = iconId;
        return this;
    }

    public BuildParamsBuilder setThemeResId(int themeResId) {
        mThemeResId = themeResId;
        return this;
    }

    // ... 省略部分代码

    public BuildParams build() {
        return new BuildParams(mIconId, mThemeResId, mTitle, mMessage, mPositiveText, mNeutralText, mNegativeText, mItems, mCheckedItems,
                mIsMultiChoice, mIsSingleChoice, mCheckedItem);
    }
}

这时,我们可以明显的发现这里的builder和alert的builder是极其类似的。那么我们能否直接拿来用呢?
通过阅读源码我们发现AlertController.AlertParams是原生api提供的存放各种参数的对象,我们可以将其和自定义的BuildParams进行映射,这样就可以省去了自造builder的工作了。

映射过程:

public BuildParams getBuildParams(AlertController.AlertParams p) {
    BuildParams data = new BuildParamsBuilder().createBuildParams();
    data.themeResId = themeResId;

    data.mIconId = p.mIconId;
    data.title = p.mTitle;
    data.message = p.mMessage;
    data.positiveText = p.mPositiveButtonText;
    data.neutralText = p.mNeutralButtonText;
    data.negativeText = p.mNegativeButtonText;
    data.items = p.mItems;
    data.isMultiChoice = p.mIsMultiChoice;
    data.checkedItems = p.mCheckedItems;
    data.isSingleChoice = p.mIsSingleChoice;
    data.checkedItem = p.mCheckedItem;

    return data;
}

build过程:

public  D build() {
    EasyDialog dialog = createDialog();
    AlertController.AlertParams p = getParams();

    Bundle bundle = new Bundle();
    bundle.putSerializable(KEY_BUILD_PARAMS, getBuildParams(p));
    bundle.putBoolean(KEY_IS_BOTTOM_DIALOG, isBottomDialog);
    dialog.setArguments(bundle);

    dialog.setOnCancelListener(p.mOnCancelListener);
    dialog.setOnDismissListener(p.mOnDismissListener);

    dialog.setPositiveListener(p.mPositiveButtonListener);
    dialog.setNeutralListener(p.mNeutralButtonListener);
    dialog.setNegativeListener(p.mNegativeButtonListener);
    dialog.setOnClickListener(p.mOnClickListener);
    dialog.setOnMultiChoiceClickListener(p.mOnCheckboxClickListener);
    
    dialog.setCancelable(p.mCancelable);
    return (D) dialog;
}

这样我们可以直接将装配好的各种参数扔给fragment了。

让原生builder支持继承

通常情况下,我们的builder都是不支持继承的,但是对于dialog这种形式,我们希望可以存在父子类的关系。

对话框一号:

Dialog最佳实践_第5张图片
image_1bk8e07oi1rjqmg01rcs1me94ha1g.png-23.9kB

对话框二号:

Dialog最佳实践_第6张图片
image_1bk8e7psmimlrac6dcbfq93s1t.png-25kB

这两个对话框很像,我们想要做点有趣的事情。我第二个对话框没有icon,如果外面传入的title字段的值是“Title”,我就将其变为新的值,即“New Title”。

public class MyEasyDialog extends EasyDialog{

    /**
     * 继承自父类的Builder
     */
    public static class Builder extends EasyDialog.Builder {

        public Builder(@NonNull Context context) {
            super(context);
        }

        protected EasyDialog createDialog() {
            return new MyEasyDialog();
        }
    }

    @Override
    protected void modifyOriginBuilder(EasyDialog.Builder builder) {
        super.modifyOriginBuilder(builder);
        
        builder.setIcon(0); // 去掉icon
        if (TextUtils.equals(getBuildParams().title, "Title")) {
            builder.setTitle("New Title");
        }
    }
}

这里有两个重要的方法:

  • modifyOriginBuilder():用来修改原本父类的builder对象
  • getBuildParams():得到原本父类中builder中设置的各个参数

我们现在只需要继承自父类的builder,然后复写createDialog方法就好,其余的工作都在modifyOriginBuilder中。

试想,如果我们不用继承的话。要完成这个工作,就必须在原本的dialogFragment加一些条件判断,实在不够灵活。

利用原生builder的示例代码:

EasyDialog.Builder builder = new EasyDialog.Builder();
builder.setTitle("Title")
        .setMessage(R.string.hello_world)
        .setOnCancelListener(new OnCancelListener() {
            public void onCancel(DialogInterface dialog) {
                // onCancel - > onDismiss
            }
        })
        .setOnDismissListener(new OnDismissListener() {
            public void onDismiss(DialogInterface dialog) {

            }
        })
        .setNeutralButton("no", null)
        .setPositiveButton("ok", new OnClickListener() {
            public void onClick(DialogInterface dialog, int which) {
                
            }
        })
        .setNegativeButton("cancel", new OnClickListener() {
            public void onClick(DialogInterface dialog, int which) {
               
            }
        });

EasyDialog dialog = builder.build();
dialog.setCancelable(true); // 点击空白是否可以取消
dialog.show(getSupportFragmentManager(), TAG);

模板化的制作自定义dialog

1. 自定义一个builder

自定义一个dialog肯定要先定义一个继承自BaseEasyDialog.Builder的builder,这个builder当然也支持bundle类型的传参。

public static class Builder extends BaseEasyDialog.Builder {

    private Bundle bundle = new Bundle(); // 通过bundle来支持参数

    public Builder setImageBitmap(Bitmap bitmap) {
        bundle.putByteArray(KEY_IMAGE_BITMAP, bitmap2ByteArr(bitmap));
        return this;
    }

    public Builder setInputText(CharSequence text, CharSequence hint) {
        bundle.putCharSequence(KEY_INPUT_TEXT, text);
        bundle.putCharSequence(KEY_INPUT_HINT, hint);
        return this;
    }

    protected DemoSimpleDialog createDialog() {
        DemoSimpleDialog dialog = new DemoSimpleDialog();
        dialog.setArguments(bundle);
        return dialog;
    }

}

说明:上面的泛型传入的参数是当前的Builder类

2. 建立一个继承自BaseCustomDialog的Dialog

编写dialog的方式也是有流程可循的:

  1. 拿到数据
  2. 设置布局文件
  3. 绑定view
  4. 设置view和其相关事件
  5. 销毁view
public class DemoSimpleDialog extends BaseCustomDialog {

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 拿到参数
        Bundle arguments = getArguments();
        if (arguments != null) {
            mInputText = arguments.getCharSequence(KEY_INPUT_TEXT);
        }
    }

    @Override
    protected int getLayoutResId() {
        // 设置布局文件
        return R.layout.demo_dialog_layout;
    }

    @Override
    protected void bindViews(View root) {
        // 绑定view
        mInputTextEt = findView(R.id.input_et);
    }

    @Override
    public void setViews() {
        // 设置view
        if (mInputText != null) {
            mInputTextEt.setVisibility(View.VISIBLE);
            if (!isRestored()) {
                // 如果是从旋转屏幕或其他状态恢复的fragment
                mInputTextEt.setText(mInputText);
            }
        }
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        // 销毁相关的view
        mInputTextEt = null;
    }

}

自定义从底部弹出的dialog

效果

Dialog最佳实践_第7张图片
image_1bk8m111gth5p41ijr1rt4jkq2a.png-47.4kB

这种从底部弹出的dialog我们并不少见,可android原生并不提供这样的样式,那么就来自定义吧。自定义的方式也很简单,也是继承自BaseCustomDialog

public class CustomBottomSheetDialog extends BaseCustomDialog {

    public static class Builder extends BaseEasyDialog.Builder {

        public Builder(@NonNull Context context) {
            super(context);
        }
        
        protected EasyDialog createDialog() {
            return new CustomBottomSheetDialog();
        }
    }

    @Override
    protected int getLayoutResId() {
        return R.layout.custom_dialog_layout;
    }

    @Override
    protected void bindViews(View root) {
        // findView...
    }

    @Override
    protected void setViews() {
        ((TextView) findView(R.id.message_tv)).setText(getBuildParams().message);
    }
}

唯一的区别是需要在构建的时候加一个标志位:

CustomBottomSheetDialog.Builder builder = new CustomBottomSheetDialog.Builder(this);
        builder.setIsBottomDialog(true); // 表明这是从底部弹出的
        CustomBottomSheetDialog dialog = builder.build();
        dialog.show(getSupportFragmentManager(), "dialog");

原理

这里的原理是用了support包中提供的BottomSheetDialog。BottomSheetDialog里面已经配置好了BottomSheetBehavior,它还自定义了一个容器:





    

    


我们都知道BottomSheetBehavior是会通过app:layout_behavior="@string/bottom_sheet_behavior"这个标识来找“底部布局”的,而我们的自定义布局又是在design_bottom_sheet中,所以自然就有了底部弹出的效果了。

顺便说一下,因为这个容器的布局在源码里已经写死了,你自定义的布局又在容器内,所以你自定义布局中写

app:behavior_hideable="true"
app:behavior_peekHeight="40dp"
app:layout_behavior="@string/bottom_sheet_behavior"

是完全没有任何作用的,如果想要起作用,那么请使用style="?attr/bottomSheetStyle"

除了这种方式外,你当然也可以自己在setViews中实现此效果:

@Override
protected void setViews() {
    // 得到屏幕宽度
    final DisplayMetrics dm = new DisplayMetrics();
    getActivity().getWindowManager().getDefaultDisplay().getMetrics(dm);

    // 建立layoutParams
    final WindowManager.LayoutParams layoutParams = getDialog().getWindow().getAttributes();
    
    int padding = getResources().getDimensionPixelOffset(R.dimen.kale_dialog_padding);
    layoutParams.width = dm.widthPixels - (padding * 2);

    layoutParams.gravity = Gravity.BOTTOM; // 位置在底部
    getDialog().getWindow().setAttributes(layoutParams); // 通过attr设置

    // 也可通过setLayout来设置
    // getDialog().getWindow().setLayout(dm.widthPixels, getDialog().getWindow().getAttributes().height);
}

以上就是实现底部弹出dialog的标准方式了。

从底部拉出的dialog样式

Dialog最佳实践_第8张图片
652417-6c205a491048768c.gif-164.8kB

android给出了一个完善好用的BottomSheet来实现底部弹窗效果。



    





    

    
    
    // ... 藏起来的部分

    


// 得到 Bottom Sheet 的视图对象所对应的 BottomSheetBehavior 对象
behavior = BottomSheetBehavior.from(findViewById(R.id.ll_sheet_root));
if (behavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
    behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
} else {
    behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}

其实它看起来是dialog,但其本质就是一个布局文件,和dialog的关系不大。

正确的设置dialog的背景

设置dialog背景的方法有两种:

1、给window设置setBackgroundDrawable

在dialogFragment#onStart的时候:

getDialog().getWindow().setBackgroundDrawable(new ColorDrawable()); // 去除dialog的背景
getDialog().getWindow().setBackgroundDrawable(new ColorDrawable(0xffffffff)); // 白色背景
getDialog().getWindow().setBackgroundDrawableResource(R.drawable.dialog_bg_custom_red); // 资源文件

2、在style中设置


@drawable/dialog_bg_custom

实际中我们的设计一般都会给我们一个圆角+边距的样式:

Dialog最佳实践_第9张图片
image_1bk8ntkeanu61ic2f1l151l10u42n.png-123.4kB

我们的目标是做圆角和外边距,那么很自然就想到了shapeinset两个标签:





    
        
        
    

Dialog最佳实践_第10张图片
image_1bk8o2fkg1nhf1j2c51m1ar9olc34.png-66.7kB

A Drawable that insets another Drawable by a specified distance or fraction of the content bounds. This is used when a View needs a background that is smaller than the View's actual bounds.

inset标签可能有同学没有用过。你可以理解为view设置这个资源为background后,它会和view的外边距保留一定的距离,成为一个比view小的背景图片。

Dialog最佳实践_第11张图片
image_1bk8okdprcea1eg34ra11lpvml3h.png-305.7kB

如果你的dialog是像上图一样上部透明,下部规整的样式,你可以考虑用layer-listinset来实现:



    
        
              // 透明区域
        

        
            
                
                
            
        
    

Dialog最佳实践_第12张图片
image_1bk8oonia8431su6cjiojr5r3u.png-88.6kB

通过修改style来改变样式

如果你项目中的dialog很简单,仅仅是想要对原生的样式做轻微的定制,你可以考虑修改一下dialog的style。修改的方式是在项目的theme中设置alertDialogTheme属性。




关键在于Theme.Dialog中的各种属性:


这里的属性我已经做了详细的解释,就不多做说明了,里面关键的是:


@style/AlertDialogStyle


如果你想要稍微修改原生样式,你可以直接copy原生的layout,修改后将新的layout放到这里就行了。

修改布局前:

Dialog最佳实践_第13张图片
image_1bk8pj19b1qdsa9431k10dadj74b.png-23.8kB

修改布局后:

Dialog最佳实践_第14张图片
image_1bk8pjjr45kj1slr1ens1ibs1t964o.png-40.7kB

样式完全变了,但代码一行没动,效果还是很神奇的。

注意:原生的layout代码会随着support版本的不同而发生改变,所以每次更新support包的时候需要检查这里,防止出现不可知的崩溃。

屏幕旋转后保持dialog中的数据

1.保存view的状态

我们知道,当Activity调用了onSaveInstanceState()后,便会对它的View Tree进行保存,而进一步对每一个子View调用其onSaveInstanceState()来保存状态。
如果你的dialog没有什么异步和特别的数据,仅仅是一个editText,那么android自己view的自动保存机制就已经帮你实现了自动保存数据了。

横屏:

Dialog最佳实践_第15张图片
image_1bk8r00as1ng01rok1dgolk3tmg55.png-31.3kB

竖屏:

Dialog最佳实践_第16张图片
image_1bk8r0eig1ka81u66128k1qbm6o5i.png-26kB

如果你的dialog中有自定义的view,自定义view中你并没有处理view的onSaveInstanceState(),那么旋转后dialog中的数据很有可能不会如你想象的一样保留下来。

关于如何处理自定义view的状态,可以参考《android中正确保存view的状态》一文。

2.保存intent中的数据

每次旋转屏幕后onCreate都会重新触发,onCreate中拿到的bundle中的数据仍旧会和之前一样,所以不用担心是否要手动保存通过getArgument()拿到的bundle。

只不过你可以用isRestored()来判断当前dialog是否是重建的,这样来避免新设置一个title反而会冲掉eidtText自动保存的输入值的问题。

@Override
protected void setViews() {
    // ...
    if (!isRestored()) {
        editText.setText("default value");
    } 
}

3.保存逻辑数据

Dialog最佳实践_第17张图片
image_1bk8t7mb71trfd07128k1cql1e595v.png-175.3kB

利用fragment管理dialog的一大好处就是可以用它本身的数据保存方案:

public class MyEasyDialog extends EasyDialog {

    private static final String TAG = "MyEasyDialog";

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }
    
    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
    }
    
    @Override
    protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
    }
}

当我们的dialog中的操作有异步网络操作的时候,简单的view保存方案已经不能满足我们了。可以考虑将网络请求的状态和结果通过onSaveInstanceState进行保存,在onRestoreInstanceState中来恢复。

Dialog相关的事件处理

为了简单起见,我仍旧采用了builder模式来设置dialog的监听事件:

EasyDialog.Builder builder = new MyEasyDialog.Builder(this);
builder.setTitle("Title")
.setIcon(R.mipmap.ic_launcher)
.setMessage(R.string.hello_world)
.setOnCancelListener(new DialogInterface.OnCancelListener() {
    @Override
    public void onCancel(DialogInterface dialog) {
        // 点空白处消失时才会触发!!!!
    }
})
.setOnDismissListener(new DialogInterface.OnDismissListener() {
    @Override
    public void onDismiss(DialogInterface dialog) {
        // 对话框消失的时候触发
    }
})
.setPositiveButton("ok", new DialogInterface.OnClickListener() {
    @Override
    public void onClick(DialogInterface dialog, int which) {
        
    }
})
.setNegativeButton("cancel", new DialogInterface.OnClickListener() {
    @Override
    public void onClick(DialogInterface dialog, int which) {
        dialog.dismiss(); // cancel -> dismiss
    }
})
// ...

这样做好处是简单,坏处是转屏后,dialog的各种listener都成了null。所以,如果你要保证转屏后dialog的事件不丢,那么你还是得采用activity实现接口的方式来做。

需要特别注意的是:

  1. dialog的出现和消失并不会触发activity的onPause()和onResume()
  2. onCancelListener仅仅是监听点击空白处dialog消失的事件

总结

dialog是一个我们很常用的控件,但它的知识点其实并不少。如果我们从头思考它,你会发现它涉及封装技巧、生命周期、windowManager挂载、fragment&activity通信等方面。
我相信如果大家可以通过最简单的api,简化现有的dialog设计,利用原生或者现成的方案来满足自己项目的需求,不用再杂乱无章的四处定义对话框。

Dialog最佳实践_第19张图片
微博:@天之界线2010

参考文章:

  • 详细解读DialogFragment - - developer_Kale
  • Android 官方推荐 : DialogFragment 创建对话框
  • BottomSheet、BottomSheetDialog使用详解
  • Android 如何保存与恢复自定义View的状态? -
  • android中正确保存view的状态 - 泡在网上的日子
  • Android: Bottom sheet - 泡在网上的日子

你可能感兴趣的:(Dialog最佳实践)