Skip to content

高阶扩展用法

Kongzue edited this page Feb 8, 2025 · 17 revisions

🌐 View English Document | 繁體中文文檔

🆙 高阶扩展用法

DialogX 结构图

DialogX 结构图

单独针对组件生效的属性

尽管已经有全局的配置,但开发者依然可以单独针对某一个组件进行一些设置。

例如如果全局配置为禁止使用点击外部区域关闭对话框,而要 BottomDialog 例外,那么可以使用如下配置:

BottomDialog.overrideCancelable = BaseDialog.BOOLEAN.TRUE;

还可以针对组件的动画进行单独设置,例如:

BottomDialog.overrideEnterDuration = 100;    //入场动画时长为100毫秒
BottomDialog.overrideExitDuration = 100;     //出场动画时长为100毫秒
BottomDialog.overrideEnterAnimRes = R.anim.anim_dialogx_top_enter;  //入场动画资源
BottomDialog.overrideExitAnimRes = R.anim.anim_dialogx_top_exit;    //出场动画资源

这样的方法单独对某一个组件进行进出场动画的重写设置。注意这些方法不会影响全局其他组件,只是单独设置单一组件的动画效果。

优先级为:实例使用方法设置 > 组件override设置 > 全局设置。

另外 FullScreenDialog 只提供动画时长设置,不提供动画资源设置(此组件只允许上下进出场动画);

CustomDialog 额外拥有了 overrideMaskEnterAnimResoverrideMaskExitAnimRes,可以覆盖背景遮罩的动画效果,设置为 0 时取消动画。

此时,在当前项目中其他的对话框组件将依然遵循全局配置,而单独针对组件生效的属性的对话框则会以单独属性设置为主。

注意此配置要区别于针对单一对话框实例的属性设置,例如:

BottomDialog.build()
        ...
        .setCancelable(true)   
        .show();

重写部分主题

例如想要将 PopTip 吐司提示不按照主题的设定(例如屏幕底部)显示,而是以自己的要求显示(例如屏幕中央),但对于 PopTip 的 align 属性属于主题控制的,此时可以通过重写主题来调整对话框的部分行为,例如:

DialogX.globalStyle = new MaterialStyle(){
    @Override
    public PopTipSettings popTipSettings() {
        return new PopTipSettings() {
            @Override
            public ALIGN align() {
                return ALIGN.CENTER;
            }
        };
    }
};

完全自定义开启、关闭动画

请注意此方法针对的是需要对对话框内容组件和细节完全自行控制的情况,如果你只是想改变整体动画而不需要控制细节那么请参阅某种对话框的自定义进入和关闭动画章节,例如 《基础对话框 MessageDialog 和 输入对话框 InputDialog - 自定义进入和关闭动画》

使用 DialogXAnimInterface 接口可以完全自定义开启、关闭动画。

由于 DialogX 对话框组件的内部元素都是暴露的,你可以轻松获取并访问内部实例,利用这一点,再加上 DialogXAnimInterface 会负责对话框启动和关闭的动画行为,你可以充分利用它实现你想要的效果。

例如对于一个 CustomDialog,你可以这样控制其启动和关闭动画:

CustomDialog.show(new OnBindView<CustomDialog>(R.layout.layout_custom_dialog) {
            @Override
            public void onBind(final CustomDialog dialog, View v) {
                //...
            }
        })
        //实现完全自定义动画效果
        .setDialogXAnimImpl(new DialogXAnimInterface<CustomDialog>() {
            @Override
            public void doShowAnim(CustomDialog customDialog, ViewGroup dialogBodyView) {
                Animation enterAnim;
                int enterAnimResId = com.kongzue.dialogx.R.anim.anim_dialogx_top_enter;
                enterAnim = AnimationUtils.loadAnimation(me, enterAnimResId);
                enterAnim.setInterpolator(new DecelerateInterpolator(2f));
                long enterAnimDurationTemp = enterAnim.getDuration();
                enterAnim.setDuration(enterAnimDurationTemp);
                customDialog.getDialogImpl().boxCustom.startAnimation(enterAnim);
                ValueAnimator bkgAlpha = ValueAnimator.ofFloat(0f, 1f);
                bkgAlpha.setDuration(enterAnimDurationTemp);
                bkgAlpha.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        if (customDialog.getDialogImpl() == null || customDialog.getDialogImpl().boxRoot == null) {
                            return;
                        }
                        customDialog.getDialogImpl().boxRoot.setBkgAlpha((Float) animation.getAnimatedValue());
                    }
                });
                bkgAlpha.start();
            }
            @Override
            public void doExitAnim(CustomDialog customDialog, ViewGroup dialogBodyView) {
                int exitAnimResIdTemp = com.kongzue.dialogx.R.anim.anim_dialogx_default_exit;
                Animation exitAnim = AnimationUtils.loadAnimation(me, exitAnimResIdTemp);
                customDialog.getDialogImpl().boxCustom.startAnimation(exitAnim);
                ValueAnimator bkgAlpha = ValueAnimator.ofFloat(1f, 0f);
                bkgAlpha.setDuration(exitAnim.getDuration());
                bkgAlpha.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        if (customDialog.getDialogImpl() == null || customDialog.getDialogImpl().boxRoot == null) {
                            return;
                        }
                        customDialog.getDialogImpl().boxRoot.setBkgAlpha((Float) animation.getAnimatedValue());
                    }
                });
                bkgAlpha.start();
            }
        });

对于 animProgress 它本质上是个反向回调执行器,因为动画时长不定,你需要通知 DialogX 当前你的动画到达哪个阶段了,对话框需要根据这个阶段进行操作处理,例如关闭动画执行过程应当是 1f 至 0f 的过程,完毕后应当销毁对话框,那么当 animProgress.run(0f) 时就会执行销毁流程,而启动动画应当是 0f 至 1f 的过程,当 animProgress.run(1f) 时启动对话框的动画完全执行完毕。

队列对话框

某些场景下需要有“模态”对话框的需要,即,一次性创建多个对话框,组成队列,逐一显示,当上一个对话框关闭时自动启动下一个对话框,此时可以使用队列对话框来完成。

示例代码如下,在 DialogX.showDialogList(...) 中构建多个对话框,请注意这些对话框必须是没有启动的状态,使用 .build() 方法完成构建,以 “,” 分隔组成队列,即可自动启动。

DialogX.showDialogList(
        MessageDialog.build().setTitle("提示").setMessage("这是一组消息对话框队列").setOkButton("开始").setCancelButton("取消")
                .setCancelButton(new OnDialogButtonClickListener<MessageDialog>() {
                    @Override
                    public boolean onClick(MessageDialog dialog, View v) {
                        dialog.cleanDialogList();
                        return false;
                    }
                }),
        PopTip.build().setMessage("每个对话框会依次显示"),
        PopNotification.build().setTitle("通知提示").setMessage("直到上一个对话框消失"),
        InputDialog.build().setTitle("请注意").setMessage("你必须使用 .build() 方法构建,并保证不要自己执行 .show() 方法").setInputText("输入文字").setOkButton("知道了"),
        TipDialog.build().setMessageContent("准备结束...").setTipType(WaitDialog.TYPE.SUCCESS),
        BottomDialog.build().setTitle("结束").setMessage("下滑以结束旅程,祝你编码愉快!").setCustomView(new OnBindView<BottomDialog>(R.layout.layout_custom_dialog) {
            @Override
            public void onBind(BottomDialog dialog, View v) {
                ImageView btnOk;
                btnOk = v.findViewById(R.id.btn_ok);
                btnOk.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                                        dialog.dismiss();
                                    }
                });
            }
        })
);

使用过程中,随时可以使用 .cleanDialogList() 来停止接下来的队列对话框的显示。

实现模式(实验性)

调整 DialogX 的实现模式有两种方法,其一是全局调整,即直接修改 DialogX 的全局配置,例如:

DialogX.implIMPLMode= DialogX.IMPL_MODE.WINDOW;

另一个方式为单次修改,仅在一个弹窗中生效,例如:

MessageDialog.build()    //必须使用 build() 方法构建时指定 DialogImplMode 才会生效。
        .setDialogImplMode(DialogX.IMPL_MODE.WINDOW)
        .setTitle("Title text")
        .setMessage("Message content")
        .setOkButton("OK")
        .show();

Window 实现模式

DialogX 默认使用 View 实现模式,自适应生命周期,你也可以选择 Window 的实现方案(与 AlertDialog 类似),请注意,使用 Window 实现模式是实验性质的,且存在一定的风险和已知 bug,你可以使用以下方法启用 Window 实现模式:

DialogX.implIMPLMode= DialogX.IMPL_MODE.WINDOW;

WindowUtil 提供了额外自定义WindowManager.LayoutParams的接口:

WindowUtil.windowSettings = new WindowUtil.WindowSettings() {
    @Override
    public WindowManager.LayoutParams overrideWindowLayoutParamsInterface(Context context, View dialogView, WindowManager.LayoutParams originWindowLayoutParams) {
        //处理 originWindowLayoutParams...
        return originWindowLayoutParams;
    }

    @Override
    public ViewGroup overrideRootView(Context context) {
        //自定义根布局,对话框布局会被添加到根布局中,返回 null 代表使用默认的 FrameLayout
        return null;
    }
};

此外,如果 app 具有悬浮窗权限,在 Window 模式下还可以实现全局弹窗,请在初始化时使用以下代码开启:

DialogX.globalHoverWindow = true;

DialogFragment 实现方式

在最新的版本 0.0.41.beta4 中,提供了 DialogFragment 实现方式,使用以下方法启用 DialogFragment 实现:

DialogX.implIMPLMode= DialogX.IMPL_MODE.DIALOG_FRAGMENT;

此模式下目前尚未发现待处理的问题,但依然不建议在生产环境使用。

FloatingActivity 实现方式

类似于 Window,使用一个透明底的 Activity 作为对话框的底层承担显示,这样的做法可以再一定程度上从后台启动(PendingIntent)。

DialogX.implIMPLMode= DialogX.IMPL_MODE.FLOATING_ACTIVITY;

背景模糊(Android 12+)

在 Android 12 以上的设备可以借助 RenderEffect 实现背景模糊效果。

背景模糊(Android 12+)

要在单一的对话框中实现此效果,可以利用生命周期事件对 activity 的元素添加模糊,从而达到背景模糊的效果,范例代码如下:

.setDialogLifecycleCallback(new DialogLifecycleCallback<CustomDialog>() {
    @Override
    public void onShow(CustomDialog dialog) {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
            RenderEffect blurEffect = RenderEffect.createBlurEffect(20f, 20f, Shader.TileMode.CLAMP);
            ((ViewGroup) getWindow().getDecorView()).getChildAt(0).setRenderEffect(blurEffect);
        }
    }
    @Override
    public void onDismiss(CustomDialog dialog) {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
            ((ViewGroup) getWindow().getDecorView()).getChildAt(0).setRenderEffect(null);
        }
    }
})

如果要实现全局的对话框背景模糊效果,可以利用全局生命周期实现,注意以下代码会过滤掉 PopTip 和 PopNotification,需要过滤其他的对话框请自行判断处理:

DialogX.dialogLifeCycleListener = new DialogLifecycleCallback<BaseDialog>() {
    @Override
    public void onShow(BaseDialog dialog) {
        super.onShow(dialog);
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S && !(dialog instanceof NoTouchInterface) ) {
            RenderEffect blurEffect = RenderEffect.createBlurEffect(20f, 20f, Shader.TileMode.CLAMP);
            ((ViewGroup) dialog.getOwnActivity().getWindow().getDecorView()).getChildAt(0).setRenderEffect(blurEffect);
        }
    }
    @Override
    public void onDismiss(BaseDialog dialog) {
        super.onDismiss(dialog);
        List<BaseDialog> sameActivityRunningDialog = BaseDialog.getRunningDialogList(dialog.getOwnActivity());
        Iterator<BaseDialog> iterator = sameActivityRunningDialog.iterator();
        while (iterator.hasNext()) {
            if (iterator.next() instanceof NoTouchInterface) {
                iterator.remove();
            }
        }
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S && !(dialog instanceof PopTip) && !(dialog instanceof PopNotification) && (sameActivityRunningDialog.isEmpty() || sameActivityRunningDialog.get(0) == dialog)) {
            ((ViewGroup) dialog.getOwnActivity().getWindow().getDecorView()).getChildAt(0).setRenderEffect(null);
        }
    }
};

请注意上述方法仅针对 Android 12+ 有效,如果无法调用 RenderEffect 请调整 app 的 targetSdkVersion 至 31,RenderEffect 是否可用取决于系统支持,在符合版本的系统下也可能存在不支持导致无效的情况。

事务穿透

数据穿透(对话框的临时储物柜)

当需要在对话框生命周期内临时存储数据(如网络请求结果),或在对话框构建时预置初始化数据时,可以使用数据穿透功能。

使用场景

  • 对话框不同组件间共享数据
  • 预存初始化参数供对话框使用
  • 临时缓存用户操作记录
// 存储数据(支持任意对象类型)
dialog.setData("userInfo", userObject)

// 获取数据(自动类型适配)
User user = dialog.getData("userInfo"); 

动作穿透(对话框的快捷功能键)

当需要在对话框不同交互节点(显示时/按钮点击时)复用相同业务逻辑,同时避免暴露对话框内部UI组件时,可使用预置动作功能。

使用场景

  • 多位置触发相同业务逻辑(如数据刷新)
  • 封装敏感操作防止外部调用
  • 解耦复杂业务逻辑

核心方法

// 预定义动作(示例为1号动作)
dialog.setActionRunnable(1, () -> {
    // 这里编写通用业务逻辑
    refreshData(); 
    updateUI();
});

// 执行预置动作(可在任意位置调用)
dialog.runAction(1);

举例,我们有一个在对话框启动后需要调用网络请求加载数据,将结果刷新显示对话框内的内容 UI 的业务,同时我们点击对话框的按钮执行提交后数据可能有所变化需要重新刷新对话框内容,此时因为按钮回调接口和对话框启动后显示内容的逻辑不在同一层级(分属于不同的内部类)无法互相调用,要解决此问题就可以使用动作穿透来实现,例如:

MessageDialog.show("标题", "这里是正文内容。", "确定")
  	//在对话框显示后执行...
        .onShow(new DialogXRunnable<MessageDialog>() {
            @Override
            public void run(MessageDialog dialog) {
                //预置操作:动作1
                dialog.setActionRunnable(1, new DialogXRunnable<MessageDialog>() {
                    @Override
                    public void run(MessageDialog dialog) {
                        tip("action 1 run!");
                        //举例:需要通过网络加载数据到 dialog 中显示
                    }
                });
                //执行动作1
                dialog.runAction(1);
            }
        })
  	//点击按钮后执行
        .setOkButton(new OnDialogButtonClickListener<MessageDialog>() {
            @Override
            public boolean onClick(MessageDialog dialog, View v) {
                PopTip.show("点击确定按钮");
                //举例:点击ok按钮后执行了一些操作但需要在不关掉对话框情况下重新加载执行动作1
                //穿透执行动作1
                dialog.runAction(1);
                return true;
            }
        });

设计优势

  1. 安全隔离:业务逻辑封装在对话框内部,不暴露UI组件
  2. 逻辑复用:同一操作可在不同场景触发(显示时/按钮点击后)
  3. 灵活控制:通过动作编号管理多个独立业务流程
  4. 生命周期安全:自动跟随对话框销毁释放资源
Clone this wiki locally