顺序显示多个弹窗方案-责任链模式

本文介绍了如何在Android应用中实现顺序弹窗的管理,通过`DialogChainOwner`、`DialogChain`和`ChainNode`接口进行弹窗流程的控制。当用户进入应用首页时,可以按顺序展示运营活动、评分、反馈和更新等多个弹窗。`DialogChainOwner`负责添加和展示弹窗,`DialogChain`负责执行弹窗链,而`ChainNode`接口则由具体的弹窗实现,用于处理每个弹窗的展示逻辑。示例代码展示了如何创建和展示弹窗链。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在这里插入图片描述

在应用中,用户进入应用首页以后经常会遇到顺序弹出多个弹窗的业务。例如进入首页以后,首先弹出运营活动弹窗,然后弹出应用评分或反馈弹窗,最后弹出应用更新弹窗等等。于是想到封装一套代码可以控制弹窗流程执行。

DialogChainOwner

弹窗链持有者。负责弹窗链节点的加入和展示入口。

/**
 * 弹窗链拥有者,用于控制链节点的添加和展示以及取消,销毁
 */
public class DialogChainOwner implements LifecycleEventObserver {

    private List<ChainNode> chainNodes = new ArrayList<>();  //链节点列表

    private Set<Integer> cancelledIds;  //取消执行的链节点集合

    private boolean isDestroyed = false;  //是否销毁

    public DialogChainOwner(@NonNull LifecycleOwner owner) {
        owner.getLifecycle().addObserver(this);
    }

    @Override
    public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
        if (event == Lifecycle.Event.ON_DESTROY) {
            destroy();
        }
    }

    /**
     * 加入链节点
     *
     * @param chainNode
     * @return
     */
    public DialogChainOwner join(ChainNode chainNode) {
        chainNodes.add(chainNode);
        return this;
    }

    //开始展示
    public void show() {
        DialogChain chain = new DialogChain(this, chainNodes, 0);
        chain.proceed();
    }

    //销毁
    public void destroy() {
        isDestroyed = true;
        chainNodes.clear();
        if (cancelledIds != null) {
            cancelledIds.clear();
        }
    }

    /**
     * 取消链节点
     *
     * @param id
     */
    public void cancel(int id) {
        if (cancelledIds == null) {
            cancelledIds = new HashSet<>();
        }

        cancelledIds.add(id);
    }

    boolean isCancelled(int id) {
        return cancelledIds != null && cancelledIds.contains(id);
    }

    boolean isDestroyed() {
        return isDestroyed;
    }
}

DialogChain

负责取对应的节点执行操作。

final class DialogChain {

    private DialogChainOwner chainOwner;
    private List<ChainNode> chainNodes;

    private final int index;

    DialogChain(DialogChainOwner owner, List<ChainNode> chainNodes, int index) {
        this.chainOwner = owner;
        this.chainNodes = chainNodes;
        this.index = index;
    }

    final void proceed() {
        if (chainOwner != null && chainOwner.isDestroyed()) {
            destroy();
            return;
        }

        if (chainNodes == null || index >= chainNodes.size()) return;

        DialogChain next = new DialogChain(chainOwner, chainNodes, index + 1);
        ChainNode chainNode = chainNodes.get(index);
        if (chainOwner != null && chainOwner.isCancelled(chainNode.getId())) {
            next.proceed();
        } else {
            chainNode.proceed(next);
        }
    }

    void destroy() {
        chainOwner = null;
        chainNodes.clear();
    }
}

ChainNode

链节点接口。弹窗实现该接口执行操作。

public interface ChainNode {
    /**
     * 返回链节点对于的Id
     *
     * @return
     */
    int getId();

    /**
     * 处理链节点
     *
     * @param next 下一个弹窗链节点
     */
    void proceed(DialogChain next);
}

示例

首先需要弹出的弹窗实现对应的链节点接口。

public class CustomDialog extends Dialog implements ChainNode {

    private String content;

    public CustomDialog(@NonNull Context context) {
        super(context, R.style.CustomDialog);
    }

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

        setContentView(R.layout.dialog_custom);

        TextView textView = findViewById(R.id.content);
        textView.setText(content);
    }

    public void setContent(String content) {
        this.content = content;
    }

    @Override
    public int getId() {
        return 0;
    }

    @Override
    public void proceed(DialogChain chain) {
        show();
        setOnDismissListener(dialog -> chain.proceed());
    }
}

然后,依次加入对应的链接口并弹出。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        DialogChainOwner owner = new DialogChainOwner(this);
        CustomDialog dialog1 = new CustomDialog(this);
        dialog1.setContent("第一个弹窗");
        CustomDialog dialog2 = new CustomDialog(this);
        dialog2.setContent("第二个弹窗");
        CustomDialog dialog3 = new CustomDialog(this);
        dialog3.setContent("第三个弹窗");
        CustomDialog dialog4 = new CustomDialog(this);
        dialog4.setContent("第四个弹窗");
        owner.join(dialog1).join(dialog2).join(dialog3).join(dialog4).show();
    }
}

感谢大家的支持,如有错误请指正,如需转载请标明原文出处!

<think>我们面对的核心问题:在多个显示屏上同时显示Toast消息。 从引用中获取的信息: 1. 引用[1]提到在Android多屏设备下,需要获取当前活动屏幕的DisplayMetrics,然后确定Toast的位置,最后创建并显示Toast。但这种方法只能在一个屏幕上显示。 2. 引用[2]提到在Presentation(用于多屏显示的类)中显示Toast,但同样也是在一个屏幕上显示。 3. 引用[3]提到DovaToast通过使用TYPE_TOAST实现全局弹窗,绕过了NotificationManagerService,且不受通知权限限制。这给我们一个思路:使用自定义窗口(TYPE_TOAST或TYPE_APPLICATION_OVERLAY)来实现在多个屏幕上显示。 4. 引用[4]和[5]提到Toast在Android 9.0的重复显示问题以及使用WeakReference避免内存泄漏,这些在自定义实现时需要注意。 分析: 系统自带的Toast(通过enqueueToast())默认只能显示在当前焦点窗口所在的屏幕上,无法同时显示多个屏幕上。因此,我们需要自定义实现。 方案: 我们可以为每个目标Display创建一个自定义的Toast窗口。具体步骤如下: 步骤1:获取所有可用的Display(屏幕) 步骤2:为每个Display创建一个窗口(使用WindowManager) 步骤3:在每个窗口上显示我们自定义的Toast视图 注意: 1. 窗口类型:可以使用TYPE_APPLICATION_OVERLAY(Android O及以上)或TYPE_TOAST(低版本,但注意权限问题)。 2. 需要为每个Display指定不同的WindowManager(通过Context.createDisplayContext()获取对应Display的Context,然后获取WindowManager)。 3. 避免内存泄漏:使用WeakReference或者确保在不需要时移除视图。 实现示例: 1. 获取所有Display: ```java DisplayManager displayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); Display[] displays = displayManager.getDisplays(); ``` 2. 遍历每个Display,创建自定义Toast窗口: ```java for (Display display : displays) { // 排除无效的Display(如虚拟屏幕) if (display.getDisplayId() == Display.DEFAULT_DISPLAY || display.isValid()) { // 为当前Display创建Context Context displayContext = context.createDisplayContext(display); // 创建自定义Toast视图 View toastView = LayoutInflater.from(displayContext).inflate(R.layout.custom_toast, null); // 设置Toast内容等... // 获取该Display的WindowManager WindowManager wm = (WindowManager) displayContext.getSystemService(Context.WINDOW_SERVICE); // 设置LayoutParams WindowManager.LayoutParams params = new WindowManager.LayoutParams( WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, // 根据版本选择窗口类型 Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : WindowManager.LayoutParams.TYPE_TOAST, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, PixelFormat.TRANSLUCENT ); // 设置位置(例如居中) params.gravity = Gravity.CENTER; // 设置Display ID params.displayId = display.getDisplayId(); // 添加视图到WindowManager wm.addView(toastView, params); // 为了能够移除,需要保存这个视图和WindowManager的引用,并在适当时候移除 // 可以使用Handler延时移除 new Handler(Looper.getMainLooper()).postDelayed(() -> { if (toastView != null && toastView.isAttachedToWindow()) { wm.removeView(toastView); } }, 3000); // 3秒后移除 } } ``` 3. 自定义Toast视图:需要设计一个布局(R.layout.custom_toast),类似于系统Toast的样式。 注意事项: 1. 权限:在Android O及以上,使用TYPE_APPLICATION_OVERLAY需要申请权限: ```xml <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> ``` 并且需要动态请求。而TYPE_TOAST在低版本不需要权限,但注意在Android 7.1(API 25)之后,应用无法再使用TYPE_TOAST添加窗口(除了系统应用),所以一般使用TYPE_APPLICATION_OVERLAY。 2. 内存泄漏:由于我们持有视图和WindowManager,必须确保在视图不再需要时(比如Activity销毁时)移除视图。上面的代码使用Handler延时移除,但如果Activity在Toast显示期间被销毁,则可能无法移除视图,导致内存泄漏。因此,更好的做法是将这些视图保存在一个全局管理类中,并在Activity的onDestroy时移除所有视图。 3. 多线程:必须在主线程中操作视图。 4. 性能:如果有多个屏幕,会同时创建多个窗口,注意不要过多消耗资源。 5. 兼容性:对于不同Android版本,窗口类型的选择需要谨慎处理。 6. 系统Toast的样式和动画:自定义Toast视图可以模仿系统Toast的样式(背景、圆角、文字大小等),动画可以使用系统Toast的动画(通过设置WindowManager.LayoutParams的windowAnimations属性)。 7. 重复显示:如果同时显示多个Toast,需要管理队列(例如使用一个队列,每个Toast显示一定时间后移除,再显示下一个)。但根据需求,我们是要同时显示多个屏幕上,所以不需要队列,而是同时显示。 8. 引用[3]提到的DovaToast是一个开源库,它解决了系统Toast的一些问题,但它也是在一个屏幕上显示。我们可以参考其实现,但需要扩展为多屏。 总结: 通过为每个Display创建自定义窗口,我们可以实现在多个屏幕上同时显示Toast。但需要注意权限、内存管理、兼容性和样式等问题。 相关问题: 1. 如何动态申请SYSTEM_ALERT_WINDOW权限? 2. 如何实现自定义Toast的动画效果? 3. 如何管理多个Toast的显示队列(如果同时触发多个Toast)?</think>### 如何在多个显示屏上同时显示 Toast 消息 在 Android 多屏场景下,系统原生的 `Toast` 类无法直接指定目标显示屏。以下是三种实现方案,按推荐度排序: --- ### 方案 1:自定义窗口 + 多 Display 管理(推荐) 通过 `WindowManager` 为每个显示屏创建独立窗口: ```java // 获取所有显示屏 DisplayManager dm = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); Display[] displays = dm.getDisplays(); for (Display display : displays) { // 为每个显示屏创建专属上下文 Context displayContext = context.createDisplayContext(display); // 创建自定义 Toast 视图 View toastView = LayoutInflater.from(displayContext) .inflate(R.layout.custom_toast, null); // 设置窗口参数 WindowManager.LayoutParams params = new WindowManager.LayoutParams( WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : WindowManager.LayoutParams.TYPE_TOAST, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT ); // 关键:绑定到目标显示屏 params.displayId = display.getDisplayId(); params.gravity = Gravity.CENTER; // 可自定义位置 // 获取目标显示屏的 WindowManager WindowManager wm = (WindowManager) displayContext.getSystemService(Context.WINDOW_SERVICE); wm.addView(toastView, params); // 自动移除(避免内存泄漏) new Handler(Looper.getMainLooper()).postDelayed(() -> { if (toastView.isAttachedToWindow()) { wm.removeView(toastView); } }, 3000); // 3秒后移除 } ``` #### 优势: 1. **多屏同步显示**:每个显示屏独立渲染 2. **规避系统限制**:绕过 `NotificationManagerService` 队列机制[^3] 3. **兼容凹口屏**:可自定义安全区域位置[^1] #### 内存泄漏防护: - 使用 `WeakReference` 持有 `Context`: ```java static class SafeToastContext extends ContextWrapper { private final WeakReference<Context> mWeakContext; public SafeToastContext(Context base) { super(base); mWeakContext = new WeakReference<>(base); } @Override public void startActivity(Intent intent) { Context context = mWeakContext.get(); if (context != null) context.startActivity(intent); } } ``` - 在 `Activity.onDestroy()` 中移除所有视图[^5] --- ### 方案 2:反射修改系统 Toast(Android 10+) 强制修改 Toast 内部 `mDisplayId` 字段: ```java public static void showOnDisplay(Context context, int displayId, String msg) { Toast toast = Toast.makeText(context, msg, Toast.LENGTH_SHORT); try { // 反射获取 TN 对象 Field tnField = Toast.class.getDeclaredField("mTN"); tnField.setAccessible(true); Object tn = tnField.get(toast); // 修改 Display ID Field displayIdField = tn.getClass().getDeclaredField("mDisplayId"); displayIdField.setAccessible(true); displayIdField.setInt(tn, displayId); } catch (Exception e) { e.printStackTrace(); } toast.show(); } ``` #### 限制: - 仅适用于 Android 10-12(Android 13+ 权限收紧) - 分屏模式下可能被输入法遮挡[^3] - 华为等设备需处理通知权限问题[^3] --- ### 方案 3:Presentation 类扩展 在副屏通过 `Presentation` 显示 Toast: ```java public class MultiDisplayToast extends Presentation { public MultiDisplayToast(Context outerContext, Display display) { super(outerContext, display); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.custom_toast); // 显示逻辑 Button btn = findViewById(R.id.toast_trigger); btn.setOnClickListener(v -> { Toast.makeText(getContext(), "副屏Toast", LENGTH_SHORT).show(); }); } } ``` #### 适用场景: - 副屏需常驻交互界面 - 需与主屏保持不同 UI 状态[^2] --- ### 多屏显示效果对比 | 特性 | 自定义窗口 | 反射修改 | Presentation | |---------------------|---------------------|------------------|-----------------| | 多屏同步显示 | ✅ | ❌ (单屏顺序显示)| ✅ | | 规避通知权限限制 | ✅ [^3] | ❌ | ✅ | | 分屏模式兼容性 | ✅ (独立窗口) | ❌ (可能遮挡) | ✅ | | 系统版本兼容 | Android 4.4+ | Android 10-12 | Android 4.2+ | | 内存泄漏风险 | 可控[^5] | 低 | 中 | > **推荐选择**: > - 需要精确控制多屏显示 → **方案 1(自定义窗口)** > - 副屏有独立交互需求 → **方案 3(Presentation)** > - 临时兼容旧设备 → **方案 2(反射)** --- ### 适配建议 1. **动态获取 Display ID**: ```java // 获取主屏ID int mainDisplayId = context.getDisplay().getDisplayId(); // 获取副屏ID(示例) int secondDisplayId = displays[1].getDisplayId(); ``` 2. **凹口屏安全区域计算**: ```java if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { DisplayCutout cutout = getWindow().getDecorView().getRootWindowInsets().getDisplayCutout(); if (cutout != null) { int safeInsetTop = cutout.getSafeInsetTop(); params.y = safeInsetTop + 50; // 下移50px避让凹口 } } ``` 3. **华为设备特殊处理**: ```java // 检测华为设备通知权限 if (Build.MANUFACTURER.equalsIgnoreCase("huawei") && !NotificationManagerCompat.from(context).areNotificationsEnabled()) { // 启用自定义窗口方案 showCustomToastOnAllDisplays(); } ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值