HarmonyOS鸿蒙Next中隐私协议弹窗实现(跳转返回不消失 + 侧滑拦截)

HarmonyOS鸿蒙Next中隐私协议弹窗实现(跳转返回不消失 + 侧滑拦截) 隐私协议弹窗实现(跳转返回不消失 + 侧滑拦截)

7 回复

最简单的就是使用CustomDialogController 实现:

dialogController: CustomDialogController = new CustomDialogController({
  builder: PrivacyDialog({   //自定义弹框页面
    cancel: () => {
      //自定义回调方法
    },
    confirm: () => {
      //自定义回调方法
    },
  }),
  autoCancel: false,  //不能跳过、侧滑关闭
  onWillDismiss: (dismissDialogAction: DismissDialogAction) => {
    console.log('dialog onWillDismiss reason: ' + dismissDialogAction.reason);
    // 1、PRESS_BACK    点击三键back、左滑/右滑、键盘ESC。
    // 2、TOUCH_OUTSIDE    点击遮障层时
    // 3、CLOSE_BUTTON    点击关闭按钮
    if (dismissDialogAction.reason === DismissReason.PRESS_BACK) {
      //进入后台
      let context = getContext(this) as common.UIAbilityContext;
      context.terminateSelf();
    }
  },
})

你只需要自定义一下弹窗的页面就行了~~~

更多关于HarmonyOS鸿蒙Next中隐私协议弹窗实现(跳转返回不消失 + 侧滑拦截)的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


1. 实现原理

我们可以通过levelMode: LevelMode.EMBEDDED将弹窗挂载到指定页面节点,确保页面跳转后弹窗不销毁;利用onWillDismiss回调拦截侧滑、点击遮罩等关闭行为,强制用户点击 “同意 / 拒绝” 按钮。

2. 封装工具类(封装弹窗打开/关闭/参数更新逻辑,支持自定义内容、样式、交互

import { UIContext, ComponentContent, DialogOptions, DismissDialogAction, LevelMode } from '@ohos/ui';

/**
 * 弹窗管理工具类(适配HarmonyOS 5.0+ UIContext弹窗)
 * 封装弹窗打开/关闭/参数更新逻辑,支持自定义内容、样式、交互
 */
export class PromptActionClass {
  // 静态属性:全局唯一实例
  private static instance: PromptActionClass;
  // 弹窗核心配置
  private uiContext: UIContext | null = null;
  private contentNode: ComponentContent | null = null;
  private dialogOptions: DialogOptions = {};
  private dialogId: string | null = null; // 弹窗唯一标识

  /**
   * 单例模式:获取工具类实例
   */
  public static getInstance(): PromptActionClass {
    if (!PromptActionClass.instance) {
      PromptActionClass.instance = new PromptActionClass();
    }
    return PromptActionClass.instance;
  }

  /**
   * 设置UIContext(必须先调用,否则弹窗无法挂载)
   * @param context 页面/组件的UIContext
   */
  public static setContext(context: UIContext): void {
    this.getInstance().uiContext = context;
  }

  /**
   * 设置弹窗内容节点
   * @param node 自定义弹窗内容(ComponentContent类型)
   */
  public static setContentNode(node: ComponentContent): void {
    this.getInstance().contentNode = node;
  }

  /**
   * 设置弹窗配置项
   * @param options 弹窗样式/交互配置(DialogOptions)
   */
  public static setOptions(options: DialogOptions): void {
    const instance = this.getInstance();
    // 默认配置合并用户配置
    instance.dialogOptions = {
      alignment: DialogAlignment.Center, // 默认居中
      isModal: true, // 默认模态弹窗
      levelMode: LevelMode.APPLICATION, // 默认应用层级
      autoCancel: false, // 默认不允许点击遮罩关闭
      ...options
    };
  }

  /**
   * 打开弹窗(核心方法)
   * @returns Promise<string> 弹窗ID
   */
  public static async openDialog(): Promise<string> {
    const instance = this.getInstance();
    // 参数校验
    if (!instance.uiContext) {
      throw new Error('未设置UIContext,请先调用setContext()');
    }
    if (!instance.contentNode) {
      throw new Error('未设置弹窗内容,请先调用setContentNode()');
    }

    try {
      // 调用UIContext弹窗API打开弹窗
      instance.dialogId = await instance.uiContext.openCustomDialog(
        instance.contentNode,
        instance.dialogOptions
      );
      console.log(`弹窗打开成功,ID:${instance.dialogId}`);
      return instance.dialogId;
    } catch (err) {
      console.error('弹窗打开失败:', err);
      throw err;
    }
  }

  /**
   * 关闭弹窗
   * @param dialogId 弹窗ID(不传则关闭当前实例弹窗)
   */
  public static async closeDialog(dialogId?: string): Promise<void> {
    const instance = this.getInstance();
    const targetId = dialogId || instance.dialogId;

    if (!instance.uiContext || !targetId) {
      console.warn('弹窗未打开或UIContext未初始化');
      return;
    }

    try {
      await instance.uiContext.closeCustomDialog(targetId);
      console.log(`弹窗关闭成功,ID:${targetId}`);
      instance.dialogId = null; // 清空弹窗ID
    } catch (err) {
      console.error('弹窗关闭失败:', err);
      throw err;
    }
  }

  /**
   * 更新弹窗内容(支持动态刷新)
   * @param data 新的弹窗数据(需与内容节点绑定的参数匹配)
   */
  public static updateContent(data: any): void {
    const instance = this.getInstance();
    if (!instance.contentNode) {
      console.warn('未设置弹窗内容,无法更新');
      return;
    }

    // 调用ComponentContent的update方法刷新内容
    instance.contentNode.update(data);
    console.log('弹窗内容更新成功');
  }

  /**
   * 检查弹窗是否已打开
   * @returns boolean
   */
  public static isDialogOpen(): boolean {
    return !!this.getInstance().dialogId;
  }

  /**
   * 销毁弹窗实例(释放资源)
   */
  public static destroy(): void {
    const instance = this.getInstance();
    if (instance.dialogId) {
      this.closeDialog(instance.dialogId);
    }
    instance.uiContext = null;
    instance.contentNode = null;
    instance.dialogOptions = {};
    instance.dialogId = null;
  }
}

/**
 * 弹窗对齐方式枚举(补充系统未导出的枚举)
 */
export enum DialogAlignment {
  Top = 0,
  Center = 1,
  Bottom = 2,
  Left = 3,
  Right = 4,
  TopLeft = 5,
  TopRight = 6,
  BottomLeft = 7,
  BottomRight = 8
}

/**
 * 快速创建ComponentContent(简化内容节点构建)
 * @param builder 弹窗内容构建器
 * @param initData 初始数据
 * @returns ComponentContent
 */
export function wrapBuilder<T = any>(builder: (data: T) => JSX.Element, initData?: T): ComponentContent {
  const contentNode = new ComponentContent();
  // 初始化内容
  if (initData) {
    contentNode.update(initData);
  }
  // 绑定构建函数
  contentNode.setBuilder(builder);
  return contentNode;
}

3. 实现完整代码

// 1. 导入封装类PromptActionClass工具类
import { PromptActionClass } from '../utils/PromptActionClass';
import { LevelMode, DismissDialogAction } from '@ohos/ui';

@Entry
@Component
struct PrivacyPage {
  private pageStack = router.getLength() > 0 ? router : undefined;

  build() {
    Column() {
      // 触发弹窗的按钮(绑定页面节点ID)
      Button('打开隐私协议')
        .id('privacyTriggerBtn') // 关键:用于定位挂载页面
        .fontSize(16)
        .width('80%')
        .borderRadius(20)
        .backgroundColor('#0A59F7')
        .margin({ top: 100 })
        .onClick(() => this.openPrivacyDialog())

      // 协议详情跳转入口(演示弹窗不消失场景)
      Text('查看完整隐私协议')
        .fontSize(14)
        .fontColor('#0A59F7')
        .margin({ top: 20 })
        .onClick(() => {
          // 跳转协议页面,弹窗保持显示
          router.pushUrl({ url: 'pages/PrivacyDetail' });
        })
    }
    .width('100%')
    .height('100%')
    .padding(16)
  }

  // 打开隐私弹窗核心方法
  private openPrivacyDialog() {
    // 获取绑定的页面节点
    const triggerNode = this.getUIContext().getFrameNodeById('privacyTriggerBtn');
    if (!triggerNode) return;

    // 1. 构建弹窗内容(协议说明+同意/拒绝按钮)
    const privacyContent = (
      Column() {
        Text('隐私政策说明')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .margin({ bottom: 16 })

        Text('为保障您的使用体验,我们需要获取设备权限,详细条款见隐私协议')
          .fontSize(14)
          .margin({ bottom: 20 })
          .width('100%')

        Row() {
          Button('拒绝')
            .fontSize(14)
            .width('45%')
            .borderRadius(16)
            .backgroundColor('#F1F3F5')
            .onClick(() => PromptActionClass.closeDialog())

          Button('同意')
            .fontSize(14)
            .width('45%')
            .borderRadius(16)
            .backgroundColor('#0A59F7')
            .marginLeft('10%')
            .onClick(() => {
              PromptActionClass.closeDialog();
              // 后续权限申请逻辑
            })
        }
      }
      .width('85%')
      .padding(20)
      .backgroundColor(Color.White)
      .borderRadius(16)
    );

    // 2. 配置弹窗参数(关键:页面挂载+侧滑拦截)
    const uiContext = this.getUIContext();
    PromptActionClass.setContext(uiContext);
    PromptActionClass.setContentNode(privacyContent);
    PromptActionClass.setOptions({
      levelMode: LevelMode.EMBEDDED, // 挂载到指定页面
      levelUniqueId: triggerNode.getUniqueId(), // 绑定节点ID
      isModal: true, // 模态弹窗,遮罩层不可点击
      alignment: DialogAlignment.Center, // 居中显示
      // 侧滑/遮罩关闭拦截
      onWillDismiss: (action: DismissDialogAction) => {
        // 拦截所有非按钮触发的关闭行为
        if (action.reason !== 'manual') {
          return false; // 阻止弹窗关闭
        }
        return true;
      }
    });

    // 3. 打开弹窗
    PromptActionClass.openDialog();
  }
}

4. 效果说明

点击 “打开隐私协议” 弹出居中弹窗,遮罩层半透明,点击遮罩或侧滑不会关闭;

点击 “查看完整隐私协议” 跳转新页面,返回后弹窗仍保持原状态;

仅能通过 “同意 / 拒绝” 按钮关闭弹窗,确保用户阅读协议。

你这没有CustomDialogController 使用简单啊,

有要学HarmonyOS AI的同学吗,联系我:https://www.itying.com/goods-1206.html

谢谢回答,简单做,确实可以直接用CustomDialog。但是项目若需要品牌化设计或者适配复杂场景,自定义要方便改动一些,

在HarmonyOS Next中实现隐私协议弹窗需使用ArkUI的模态弹窗机制,通过@CustomDialog装饰器创建自定义弹窗组件。设置isShow属性控制弹窗显隐,结合onWillDismiss回调处理跳转返回时的状态保持。侧滑拦截通过弹窗的gesture属性配置,禁用边缘滑动手势。页面路由使用router.pushUrlsingleTask模式避免重复实例化,确保弹窗状态持久化。具体实现需在aboutToAppear生命周期初始化弹窗状态,通过@State装饰器管理显示状态。

在HarmonyOS Next中实现隐私协议弹窗,需要结合页面生命周期和手势拦截机制:

  1. 跳转返回不消失

    • 使用Page组件的aboutToAppearaboutToDisappear生命周期
    • aboutToDisappear中保存弹窗状态,在aboutToAppear中恢复显示
    • 通过@State装饰器管理弹窗可见性状态
  2. 侧滑拦截

    • 使用gesture属性配置滑动手势
    • 通过onTouch事件监听滑动操作
    • 设置gestureMask为GestureMask.Ignore防止手势冲突

示例代码:

@Entry
@Component
struct PrivacyDialogPage {
  @State isDialogShow: boolean = true
  
  aboutToAppear() {
    // 恢复弹窗显示状态
    if (/* 需要显示的条件 */) {
      this.isDialogShow = true
    }
  }
  
  aboutToDisappear() {
    // 保存弹窗状态到AppStorage
    AppStorage.setOrCreate('privacyDialogShow', this.isDialogShow)
  }

  build() {
    Column() {
      // 页面内容
      
      if (this.isDialogShow) {
        PrivacyDialog()
          .onTouch((event: TouchEvent) => {
            // 拦截侧滑手势
            if (event.type === TouchType.Move) {
              // 处理滑动逻辑
            }
          })
      }
    }
    .gesture(
      PanGesture({ direction: PanDirection.Horizontal })
        .onActionStart(() => {
          // 手势开始时拦截
        })
    )
  }
}

关键点:

  • 使用AppStorage持久化弹窗状态
  • 通过手势掩码控制手势优先级
  • 合理管理弹窗组件的生命周期
回到顶部