通过自定义弹窗实现自定义样式的Toast
====================

#### 问题现象

HarmonyOS的Toast接口，不支持设定圆角样式。如何实现类似其他平台的Toast样式？

#### 效果预览

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/5c/v3/MnsOToPCRf6FOeRY_LoQFg/zh-cn_image_0000002628555228.gif?HW-CC-KV=V1&HW-CC-Date=20260709T134036Z&HW-CC-Expire=86400&HW-CC-Sign=A2188ECBD954E5780D42ACBCAA47DF0864899FA72337F38C58DDE4DE15A5E160 "点击放大")

#### 背景知识

*   [不依赖UI组件的全局自定义弹出框(openCustomDialog)](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-uicontext-custom-dialog)，适用于在相对应用复杂的场景来实现自定义弹出框，相较于[CustomDialogController](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-methods-custom-dialog-box#customdialogcontroller)优势点在于页面解耦，支持[动态刷新](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-arkui-componentcontent#update)。
*   ArkUI提供轻量的UI元素复用机制[@Builder](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-builder)，其内部UI结构固定，仅与使用方进行数据传递。可将重复使用的UI元素抽象成函数，在build函数中调用。
*   [Text组件](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-basic-components-text)可以自定义展示文本框的UI样式，包括[边框圆角](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-universal-attributes-border#borderradius)、[内边距](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-universal-attributes-size#padding)等。
*   [setTimeout接口](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-timer)支持设置一个定时器，该定时器在定时器到期后执行一个函数。

#### 解决方案

如果需要实现类似其他平台的Toast样式，可借助自定义弹窗实现。

主要实现思路为，借助Text组件自定义类似其他平台Toast的UI样式，并封装为@Builder构建函数，将该函数传入ComponentContent创建弹窗对象，通过getUIContext开启该弹窗对象，开启后执行setTimeout，等待指定的时间后，执行关闭弹窗对象。详细步骤如下：

1.  配置ToastContent组件属性并封装为@Builder构建函数。
    
    @Component
    struct ToastContent {
      public text: string \= '';
      public clickText: string \= '';
      public clickListener \= () \=\> {
      };
      private textList: string\[\] \= \[\];
    
      aboutToAppear(): void {
        if (this.clickText.length \> 0) {
          this.textList \= this.text.split(this.clickText);
        }
      }
    
      build() {
        Column() {
          if (this.clickText \=== '') {
            Text(this.text).toastText();
          } else {
            Text() {
              ForEach(this.textList, (item: string, num: number) \=\> {
                Span(item);
                if (num < this.textList.length \- 1) {
                  Span(this.clickText).fontColor(Color.Yellow);
                }
              });
            }.onClick(this.clickListener).toastText();
          }
        }
        .borderRadius(5)
        .backgroundColor(Color.Black)
        .padding(10)
        .justifyContent(FlexAlign.SpaceBetween)
        .margin({ left: '5%', right: '5%' });
      }
    }
    
    _//_ _封装Toast的@Builder方法_
    @Builder
    function buildText(params: Params) {
      ToastContent({ text: params.text, clickText: params.clickText, clickListener: params.clickListener });
    }
    
    _//_ _封装公共样式_
    @Extend(Text)
    function toastText() {
      .fontSize(20)
      .fontColor(Color.White);
    }
    
2.  创建Toast类，并创建构造方法与Toast实例方法。
    
    _/\*\*_
     _\* 封装全局蓝色浮动提示，支持点击_
     _\*/_
    export class Toast {
      private toastParams: Params;
    
      constructor(text: string, time: number \= 2000) {
        this.toastParams \= new Params(text, time);
      }
    
      setClick(clickText: string, clickListener: () \=\> void): Toast {
        this.toastParams.setClick(clickText, clickListener);
        return this;
      };
    
      async show() {
        let uiContext \= AppStorage.get('currentUIContext') as UIContext;
        let click \= this.toastParams.clickListener;
        let contentNode \= new ComponentContent(uiContext, wrapBuilder(buildText), this.toastParams);
        uiContext.getPromptAction().openCustomDialog(contentNode, {
          showInSubWindow: this.toastParams.clickText \=== '' ? false : true,
          isModal: false,
          offset: { dx: 0, dy: '10%' }
        }).then(() \=\> {
          setTimeout(() \=\> {
            uiContext.getPromptAction().closeCustomDialog(contentNode);
          }, this.toastParams.time);
        });
        this.toastParams.clickListener \= () \=\> {
          click();
          uiContext.getPromptAction().closeCustomDialog(contentNode);
        };
      };
    }
    
3.  创建并弹出Toast，并且可以在Toast的setClick回调方法内实现点击Toast后的逻辑，如页面跳转。
    
    new Toast('点击Toast后屏幕将退出横屏，进入到竖屏状态', 3000).setClick('关闭自动添加', () \=\> {
      this.windowClass.setPreferredOrientation(window.Orientation.PORTRAIT);
      this.pathStack.pushPathByName('DetailPage', null);
    }).show();
    

完整示例参考如下：

1.  Index.ets。
    
    import { common } from '@kit.AbilityKit';
    import { Toast } from './ToastContent';
    import { window } from '@kit.ArkUI';
    
    @Entry
    @Component
    struct Index {
      pathStack: NavPathStack \= new NavPathStack();
      private context \= this.getUIContext().getHostContext() as common.UIAbilityContext;
      private windowClass \= (this.context as common.UIAbilityContext).windowStage.getMainWindowSync();
    
      setOrientation(orientation: number) {
        this.windowClass.setPreferredOrientation(orientation).then(() \=\> {
        }).catch(() \=\> {
        });
      }
    
      async aboutToAppear(): Promise<void\> {
        await this.windowClass.setPreferredOrientation(window.Orientation.LANDSCAPE);
        AppStorage.setOrCreate('currentUIContext', this.getUIContext());
      }
    
      build() {
        Navigation(this.pathStack) {
          RelativeContainer() {
            Column() {
              Text('我的记录')
                .fontSize(50)
                .width('100%')
                .textAlign(TextAlign.Center)
                .fontWeight(FontWeight.Bold)
              Button('保存')
                .onClick(() \=\> {
                  new Toast('点击Toast后屏幕将退出横屏，进入到竖屏状态', 3000).setClick('关闭自动添加', () \=\> {
                    this.windowClass.setPreferredOrientation(window.Orientation.PORTRAIT);
                    this.pathStack.pushPathByName('DetailPage', null);
                  }).show();
                })
                .backgroundColor(Color.Blue)
                .fontColor(Color.White)
            }
            .alignRules({
              center: { anchor: '\_\_container\_\_', align: VerticalAlign.Center },
              middle: { anchor: '\_\_container\_\_', align: HorizontalAlign.Center }
            })
          }
          .width('100%')
        }
        .mode(NavigationMode.Stack)
        .height('100%')
        .width('100%')
        .height('100%')
        .hideTitleBar(true)
      }
    }
    
2.  ToastContent.ets。
    
    import { ComponentContent } from '@kit.ArkUI';
    _/\*\*_
     _\* 封装全局蓝色浮动提示，支持点击_
     _\*/_
    export class Toast {
      private toastParams: Params;
    
      constructor(text: string, time: number \= 2000) {
        this.toastParams \= new Params(text, time);
      }
    
      setClick(clickText: string, clickListener: () \=\> void): Toast {
        this.toastParams.setClick(clickText, clickListener);
        return this;
      };
    
      async show() {
        let uiContext \= AppStorage.get('currentUIContext') as UIContext;
        let click \= this.toastParams.clickListener;
        let contentNode \= new ComponentContent(uiContext, wrapBuilder(buildText), this.toastParams);
        uiContext.getPromptAction().openCustomDialog(contentNode, {
          showInSubWindow: this.toastParams.clickText \=== '' ? false : true,
          isModal: false,
          offset: { dx: 0, dy: '10%' }
        }).then(() \=\> {
          setTimeout(() \=\> {
            uiContext.getPromptAction().closeCustomDialog(contentNode);
          }, this.toastParams.time);
        });
        this.toastParams.clickListener \= () \=\> {
          click();
          uiContext.getPromptAction().closeCustomDialog(contentNode);
        };
      };
    }
    _//_ _参数_
    class Params {
      text: string \= '';
      time: number \= 2000;
      clickText: string \= '';
      clickListener \= () \=\> {
      };
    
      constructor(text: string, time: number \= 2000) {
        this.text \= text;
        this.time \= time;
      }
    
      setClick(clickText: string, clickListener: () \=\> void) {
        this.clickText \= clickText;
        this.clickListener \= clickListener;
      };
    }
    @Component
    struct ToastContent {
      public text: string \= '';
      public clickText: string \= '';
      public clickListener \= () \=\> {
      };
      private textList: string\[\] \= \[\];
    
      aboutToAppear(): void {
        if (this.clickText.length \> 0) {
          this.textList \= this.text.split(this.clickText);
        }
      }
    
      build() {
        Column() {
          if (this.clickText \=== '') {
            Text(this.text).toastText();
          } else {
            Text() {
              ForEach(this.textList, (item: string, num: number) \=\> {
                Span(item);
                if (num < this.textList.length \- 1) {
                  Span(this.clickText).fontColor(Color.Yellow);
                }
              });
            }.onClick(this.clickListener).toastText();
          }
        }
        .borderRadius(5)
        .backgroundColor(Color.Black)
        .padding(10)
        .justifyContent(FlexAlign.SpaceBetween)
        .margin({ left: '5%', right: '5%' });
      }
    }
    
    _//_ _封装Toast的@Builder方法_
    @Builder
    function buildText(params: Params) {
      ToastContent({ text: params.text, clickText: params.clickText, clickListener: params.clickListener });
    }
    
    _//_ _封装公共样式_
    @Extend(Text)
    function toastText() {
      .fontSize(20)
      .fontColor(Color.White);
    }
    
3.  DetailPage.ets
    
    @Builder
    export function DetailPageBuilder() {
      DetailPage()
    }
    
    @Component
    export struct DetailPage {
      pageInfos: NavPathStack \= new NavPathStack();
    
      build() {
        NavDestination() {
          Column() {
          }.width('100%').height('100%')
        }.title('DetailPage')
    
      }
    }
    

#### 总结

该知识是综合了自定义弹窗和延时关闭功能，实现了自定义样式的Toast。可直接将上述show接口的实现代码，结合业务需要自定义Toast样式，并粘贴至自己的文件调用。