鸿蒙插屏广告接入指南——弹出来就是全屏的那种

你在刷信息流或者看文章的时候,突然"砰"一下弹出来一个全屏广告——可能是一段短视频,也可能是一张大图,上面有个小叉号可以关掉。这种广告就叫"插屏广告"(Interstitial Ad)。它的特点是:不打断你当前的操作流程(不像开屏广告那样必须看完才能进 App),但确实挡住了你的视线,所以转化率通常比 Banner 广告高不少。

华为 Ads Kit 里的插屏广告实现方式跟之前讲的贴片广告有一个很大的区别——插屏广告不需要你自己写 UI 页面,它是由系统直接弹出一个全屏窗口来展示的。你只需要加载广告、调一个 showAd() 方法就行了。这篇文章就带你把插屏广告接进去。


插屏广告是什么

插屏广告(Interstitial Ad)是在应用的自然切换间隙展示的全屏广告。它可以是视频也可以是图片,支持两种形式:

  • 插屏视频广告:全屏播放一段视频广告,用户可以看完也可以手动关闭
  • 插屏图片广告:全屏展示一张图片广告,用户点击可以跳转到广告落地页

从广告类型的编号来看,插屏广告的 adType 是 12。作为对比,贴片广告是 60,Banner 是 8,激励广告是 35,开屏广告是 32。

插屏广告跟贴片广告有一个根本性的区别:

  • 贴片广告:你需要创建一个专门的页面,在里面放一个 AdComponent 组件来展示广告。广告是嵌在你的页面里的。
  • 插屏广告:你不需要创建专门的页面。广告加载成功后,调用 advertising.showAd() 方法,系统会自动弹出一个全屏窗口来展示广告。广告是一个系统级的弹出层,覆盖在你的页面上方。

这个区别决定了两者的代码结构完全不同。插屏广告的接入其实更简单,因为你不用操心 UI 布局、屏幕旋转、系统栏显隐这些事情——系统都帮你处理好了。


测试广告位 ID

插屏广告有两种测试广告位 ID:

广告类型 测试广告位 ID
插屏视频广告 testb4znbuh3n2
插屏图片广告 teste9ih9j0rc3

测试阶段用这两个 ID 就行,正式上线需要去鲸鸿动能平台申请真实的广告位 ID。


权限配置

跟贴片广告一样,插屏广告也需要两个权限:OAID 权限和网络权限。在 module.json5 里配置:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.APP_TRACKING_CONSENT",
        "reason": "$string:app_tracking_permission_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.INTERNET"
      }
    ]
  }
}

不需要额外依赖,@kit.AdsKit 是系统内置的。


代码结构

插屏广告的代码结构比贴片广告简单很多,因为它不需要专门的广告展示页面:

entry/src/main/ets/
├── constant/
│   ├── AdType.ets                    // 广告类型枚举
│   └── AdStatus.ets                  // 广告回调状态枚举
├── event/
│   └── InterstitialAdStatusHandler.ets  // 插屏广告事件订阅
├── pages/
│   └── Index.ets                     // 主页面(按钮入口)
└── viewmodel/
    └── AdsViewModel.ets              // 广告加载与展示逻辑

关键就三个文件:

  • Index.ets:主页面,放两个按钮(视频插屏 / 图片插屏)
  • AdsViewModel.ets:广告加载和展示的核心逻辑
  • InterstitialAdStatusHandler.ets:插屏广告的事件订阅处理

没有 InterstitialAdPage.ets——因为插屏广告是系统弹出的,不需要自定义页面。


广告类型常量

先定义好广告类型的枚举,后面会用到:

export enum AdType {
  SPLASH = 1,
  NATIVE = 3,
  REWARD = 7,
  BANNER = 8,
  INTERSTITIAL = 12,
  ROLL = 60
}

广告回调状态的枚举:

export enum AdStatus {
  AD_OPEN = 'onAdOpen',
  AD_CLOSED = 'onAdClose',
  AD_CLICKED = 'onAdClick',
  AD_LOAD = 'onAdLoad',
  AD_FAIL = 'onAdFail',
  AD_REWARDED = 'onAdReward',
  VIDEO_PLAY_BEGIN = 'onVideoPlayBegin',
  VIDEO_PLAY_END = 'onVideoPlayEnd',
  MEDIA_PROGRESS = 'onMediaProgress',
  MEDIA_START = 'onMediaStart',
  MEDIA_PAUSE = 'onMediaPause',
  MEDIA_STOP = 'onMediaStop',
  MEDIA_COMPLETE = 'onMediaComplete',
  MEDIA_ERROR = 'onMediaError',
  MEDIA_COUNTDOWN = 'onMediaCountdown',
  LANDSCAPE = 'onLandscape',
  PORTRAIT = 'onPortrait',
  BACK_CLICKED = 'onBackClicked',
}

这个枚举里包含所有广告类型的回调状态。对于插屏广告来说,实际用到的只有 AD_OPENAD_CLICKEDAD_CLOSEDVIDEO_PLAY_BEGINVIDEO_PLAY_END 这五个。其他的是贴片广告、激励广告等用到的。


插屏广告事件订阅 InterstitialAdStatusHandler.ets

这是插屏广告最特别的地方。贴片广告的回调是通过 AdComponentinteractionListener 来处理的,而插屏广告因为是由系统弹出的,它的事件不是通过组件回调传递的,而是通过 CommonEvent(公共事件) 来通知你的应用的。

import { BusinessError, commonEventManager } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { AdStatus } from '../constant/AdStatus';

const TAG: string = 'Ads Demo-InterstitialAdStatusHandler';
const KEY_INTERSTITIAL_STATUS: string = 'interstitial_ad_status';

export class InterstitialAdStatusHandler {
  private subscriber: commonEventManager.CommonEventSubscriber | null = null;

  registerPPSReceiver(): void {
    if (this.subscriber) {
      this.unRegisterPPSReceiver();
    }
    const subscribeInfo: commonEventManager.CommonEventSubscribeInfo = {
      events: ['com.huawei.hms.pps.action.PPS_INTERSTITIAL_STATUS_CHANGED'],
      publisherBundleName: 'com.huawei.hms.adsservice'
    };
    commonEventManager.createSubscriber(subscribeInfo,
      (err: BusinessError, commonEventSubscriber: commonEventManager.CommonEventSubscriber) => {
        if (err) {
          hilog.error(0x0000, TAG, `Failed to create subscriber. Code is ${err.code}, message is ${err.message}`);
          return;
        }
        hilog.info(0x0000, TAG, 'Succeeded in creating subscriber');
        this.subscriber = commonEventSubscriber;
        commonEventManager.subscribe(this.subscriber,
          (err: BusinessError, commonEventData: commonEventManager.CommonEventData) => {
            if (err) {
              hilog.error(0x0000, TAG, `Failed to subscribe. Code is ${err.code}, message is ${err.message}`);
            } else {
              hilog.info(0x0000, TAG, 'Succeeded in subscribing data');
              const status: string = commonEventData?.parameters?.[KEY_INTERSTITIAL_STATUS];
              switch (status) {
                case AdStatus.AD_OPEN:
                  hilog.info(0x0000, TAG, 'Status is onAdOpen');
                  break;
                case AdStatus.AD_CLICKED:
                  hilog.info(0x0000, TAG, 'Status is onAdClick');
                  break;
                case AdStatus.AD_CLOSED:
                  hilog.info(0x0000, TAG, 'Status is onAdClose');
                  this.unRegisterPPSReceiver();
                  break;
                case AdStatus.VIDEO_PLAY_BEGIN:
                  hilog.info(0x0000, TAG, 'Status is onVideoPlayBegin');
                  break;
                case AdStatus.VIDEO_PLAY_END:
                  hilog.info(0x0000, TAG, 'Status is onVideoPlayEnd');
                  break;
              }
            }
          });
      });
  }

  unRegisterPPSReceiver(): void {
    commonEventManager.unsubscribe(this.subscriber, (err: BusinessError) => {
      if (err) {
        hilog.error(0x0000, TAG, `Failed to unsubscribe. Code is ${err.code}, message is ${err.message}`);
      } else {
        hilog.info(0x0000, TAG, 'Succeeded in unsubscribing');
        this.subscriber = null;
      }
    });
  }
}

这段代码做的事情是:

注册订阅registerPPSReceiver):

  1. 如果之前已经注册过订阅(this.subscriber 不为 null),先取消之前的订阅,避免重复订阅
  2. 创建一个 CommonEventSubscribeInfo,指定要监听的事件名为 com.huawei.hms.pps.action.PPS_INTERSTITIAL_STATUS_CHANGED,发布者是 com.huawei.hms.adsservice(华为广告服务)
  3. 调用 commonEventManager.createSubscriber() 创建订阅者
  4. 创建成功后,调用 commonEventManager.subscribe() 开始监听
  5. 收到事件后,从 commonEventData.parameters 中取出 interstitial_ad_status 字段,根据状态值做不同的处理

事件处理

状态 说明 处理
onAdOpen 广告弹出展示 打印日志(你可能会在这里暂停背景音乐之类的)
onAdClick 用户点击了广告 打印日志(用户跳转到广告落地页了)
onAdClose 广告被关闭 打印日志,然后自动取消订阅
onVideoPlayBegin 视频广告开始播放 打印日志
onVideoPlayEnd 视频广告播放结束 打印日志

有一个细节值得注意:当广告关闭(onAdClose)的时候,代码会自动调用 this.unRegisterPPSReceiver() 取消订阅。这样做的原因是——插屏广告每次展示都是一次性的,关掉了就没了,没必要继续监听。下次再展示插屏广告的时候,会重新注册订阅。

取消订阅unRegisterPPSReceiver):
调用 commonEventManager.unsubscribe() 取消订阅,成功后把 this.subscriber 置为 null。


广告加载与展示 AdsViewModel.ets

AdsViewModel 是整个广告逻辑的核心。插屏广告的加载逻辑跟贴片广告类似,但展示方式完全不同。

import { common } from '@kit.AbilityKit';
import { advertising } from '@kit.AdsKit';
import { AppStorageV2 } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { AdType } from '../constant/AdType';
import { InterstitialAdStatusHandler } from '../event/InterstitialAdStatusHandler';

const TAG: string = 'Ads Demo-AdsViewModel';

@ObservedV2
export class AdsViewModel {
  @Trace ads: advertising.Advertisement[] = [];

  adOptions: advertising.AdOptions = {};

  adDisplayOptions: advertising.AdDisplayOptions = {
    mute: true,
    refreshTime: 30000
  };

  navPathStack: NavPathStack;
  private context: common.UIAbilityContext;

  constructor(uiContext: UIContext) {
    this.context = uiContext.getHostContext() as common.UIAbilityContext;
    this.navPathStack = AppStorageV2.connect(NavPathStack)!;
  }

属性说明:

  • ads:广告对象数组(@Trace 修饰,变化会触发 UI 刷新)
  • adOptions:广告加载选项。插屏广告这里没有设 totalDuration(跟贴片广告不同),因为插屏广告不是跟视频绑定的,没有"总时长"的概念
  • adDisplayOptions:广告展示选项。mute: true 默认静音,refreshTime: 30000 是广告刷新间隔(30 秒),这个参数在插屏广告中用得比较少,主要是 Banner 广告轮播用的

核心:加载并展示广告

  async loadAd(adRequestParams: advertising.AdRequestParams): Promise<void> {
    const adType = adRequestParams.adType;
    const adLoadListener: advertising.AdLoadListener = {
      onAdLoadFailure: (errorCode: number, errorMsg: string) => {
        hilog.error(0x0000, TAG, `Failed to load ad. Code is ${errorCode}, message is ${errorMsg}`);
      },
      onAdLoadSuccess: (ads: Array<advertising.Advertisement>) => {
        hilog.info(0x0000, TAG, 'Succeeded in loading ad');
        if (adType === AdType.INTERSTITIAL) {
          // 插屏广告核心逻辑
          new InterstitialAdStatusHandler().registerPPSReceiver();
          try {
            advertising.showAd(ads[0], this.adDisplayOptions, this.context);
          } catch (e) {
            hilog.error(0x0000, 'testTag', `Failed to show ad. Code is ${e.code}, message is ${e.message}`);
          }
          return;
        }
        this.ads = ads;
      }
    };
    const adLoader: advertising.AdLoader = new advertising.AdLoader(this.context);
    try {
      adLoader.loadAd(adRequestParams, this.adOptions, adLoadListener);
    } catch (e) {
      hilog.error(0x0000, 'testTag', `Failed to load ad. Code is ${e.code}, message is ${e.message}`);
    }
  }
}

这段代码的逻辑是:

  1. 创建 AdLoadListener,定义成功和失败的回调
  2. 失败时打印错误日志
  3. 成功时判断广告类型:
    • 如果是插屏广告adType === AdType.INTERSTITIAL):
      • 第一步:注册插屏广告的事件订阅 new InterstitialAdStatusHandler().registerPPSReceiver()
      • 第二步:调用 advertising.showAd(ads[0], this.adDisplayOptions, this.context) 展示广告
      • 然后直接 return,不执行后面的逻辑
    • 如果是其他类型广告(贴片、Banner 等):
      • 把广告数组赋给 this.ads,等 UI 组件去渲染
  4. 创建 AdLoader 并调用 loadAd()

这里就是插屏广告和贴片广告最大的区别

  • 插屏广告在 onAdLoadSuccess 里直接调用 advertising.showAd() 展示广告,不需要把广告对象存到 ads 数组里,也不需要自定义 UI 组件来渲染。广告会以系统弹出窗口的形式展示。

  • 贴片广告在 onAdLoadSuccess 里把广告对象存到 ads 数组里,然后在 RollAdPage 页面中用 AdComponent 组件来渲染展示。

advertising.showAd() 是一个静态方法,接收三个参数:

  1. ads[0]:广告对象(取数组第一个元素)
  2. this.adDisplayOptions:展示选项(静音、刷新间隔等)
  3. this.context:应用上下文

主页面 Index.ets

主页面负责获取 OAID、配置按钮、处理按钮点击事件。

import { abilityAccessCtrl, common, PermissionRequestResult } from '@kit.AbilityKit';
import { advertising, identifier } from '@kit.AdsKit';
import { AppStorageV2 } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { AdType } from '../constant/AdType';
import { AdsViewModel } from '../viewmodel/AdsViewModel';

const TAG: string = 'Ads Demo-Index';

@Entry
@ComponentV2
struct Index {
  @Local private buttonsOptions: ButtonOptions[] = [];
  private context: common.UIAbilityContext = this.getUIContext().getHostContext() as common.UIAbilityContext;
  private navPathStack: NavPathStack = AppStorageV2.connect(NavPathStack, () => new NavPathStack())!;
  private viewModel: AdsViewModel = new AdsViewModel(this.getUIContext());

初始化按钮

  async aboutToAppear() {
    const oaid = await requestOAID(this.context);

    // 插屏视频广告
    this.buttonsOptions.push({
      text: $r('app.string.request_interstitial_video_ad_btn'),
      shouldShowAd: true,
      adRequestParams: {
        adId: 'testb4znbuh3n2',
        adType: AdType.INTERSTITIAL,
        oaid: oaid
      }
    });

    // 插屏图片广告
    this.buttonsOptions.push({
      text: $r('app.string.request_interstitial_image_ad_btn'),
      shouldShowAd: true,
      adRequestParams: {
        adId: 'teste9ih9j0rc3',
        adType: AdType.INTERSTITIAL,
        oaid: oaid
      }
    });
  }

两个按钮分别对应两种插屏广告:

  • 插屏视频广告:测试广告位 ID 是 testb4znbuh3n2
  • 插屏图片广告:测试广告位 ID 是 teste9ih9j0rc3

注意这里有一个 shouldShowAd: true 的字段。这个字段是用来区分"直接弹出展示的广告"和"跳转到专门页面展示的广告"的。插屏广告和激励广告都属于"直接弹出"的类型(shouldShowAd: true),而贴片广告、Banner 广告、开屏广告等需要跳转到专门页面(没有 shouldShowAd 字段或者为 false)。

按钮点击处理

  build() {
    Navigation(this.navPathStack) {
      Column() {
        List() {
          Repeat<ButtonOptions>(this.buttonsOptions).each((repeatItem: RepeatItem<ButtonOptions>) => {
            ListItem() {
              Button(repeatItem.item.text)
                .fontSize(20)
                .fontWeight(FontWeight.Normal)
                .width('90%')
                .margin({ top: 10, bottom: 10 })
                .onClick(() => {
                  const options: ButtonOptions = repeatItem.item;
                  if (options.shouldShowAd) {
                    this.viewModel.loadAd(options.adRequestParams);
                    return;
                  }
                  if (options.adRequestParams?.adType) {
                    this.navPathStack.pushPathByName(
                      AdType[options.adRequestParams?.adType], options.adRequestParams
                    );
                  }
                })
            }
          })
        }
      }
    }
    .title($r('app.string.ads_demo_title'))
    .titleMode(NavigationTitleMode.Mini)
    .mode(NavigationMode.Stack)
    .hideBackButton(true)
  }
}

interface ButtonOptions {
  text: ResourceStr;
  adRequestParams: advertising.AdRequestParams;
  shouldShowAd?: boolean;
}

点击逻辑分两条路:

  • 如果 shouldShowAd 为 true(插屏广告、激励广告),直接调用 this.viewModel.loadAd() 加载并展示广告,不需要页面跳转
  • 如果 shouldShowAd 不为 true(贴片广告、Banner 等),通过 navPathStack.pushPathByName() 跳转到对应的广告展示页面

请求 OAID

OAID 的获取方式跟贴片广告完全一样,这里不再赘述。核心就是调用 identifier.getOAID(),前提是用户已经授权了 ohos.permission.APP_TRACKING_CONSENT 权限。

async function requestOAID(context: Context): Promise<string | undefined> {
  let isPermissionGranted: boolean = false;
  try {
    const atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
    const result: PermissionRequestResult =
      await atManager.requestPermissionsFromUser(context, ['ohos.permission.APP_TRACKING_CONSENT']);
    isPermissionGranted = result.authResults[0] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
  } catch (err) {
    hilog.error(0x0000, TAG, `Failed to request permission. Code is ${err.code}, message is ${err.message}`);
  }
  if (isPermissionGranted) {
    hilog.info(0x0000, TAG, 'Succeeded in requesting permission');
    try {
      const oaid = await identifier.getOAID();
      hilog.info(0x0000, TAG, 'Succeeded in getting OAID');
      return oaid;
    } catch (err) {
      hilog.error(0x0000, TAG, `Failed to get OAID. Code is ${err.code}, message is ${err.message}`);
    }
  } else {
    hilog.error(0x0000, TAG, 'Failed to request permission. User rejected');
  }
  return undefined;
}

插屏广告和贴片广告的完整对比

既然这篇文章和上一篇都讲了广告,这里做一个完整的对比,帮你搞清楚两种广告的差别:

对比维度 插屏广告 (Interstitial) 贴片广告 (Roll)
adType 12 60
展示方式 advertising.showAd() 系统弹出 AdComponent 组件嵌入页面
需要自定义 UI 吗 不需要,系统全屏弹出 需要,自己搭页面和组件
需要单独的页面吗 不需要 需要(RollAdPage)
事件监听 CommonEvent 公共事件订阅 AdComponent 的 interactionListener 回调
横竖屏处理 系统自动处理 需要手动设置窗口方向和系统栏
倒计时 没有 有,通过 onMediaCountdown 回调
广告关闭 系统自动关闭 通过 BACK_CLICKED 事件手动处理
回调事件数量 5 个 16 个
典型场景 信息流间隙、关卡过渡、页面切换 视频播放前中后
代码复杂度

总的来说,插屏广告的接入比贴片广告简单得多。你不需要操心 UI 的事情,系统帮你搞定。你唯一需要关心的就是:在合适的时机触发广告加载,在回调里处理你的业务逻辑(比如暂停背景音乐、恢复游戏计时等)。


什么时候该用插屏广告

插屏广告适合用在应用的自然切换间隙,也就是用户预期会有一个短暂等待的时刻。比如:

  • 游戏通关后、下一关开始前
  • 从列表页进入详情页的瞬间
  • 工具类 App 完成一次操作后的等待间隙
  • 阅读类 App 翻页的间隙

关键原则是:不要在用户正在进行关键操作的时候突然弹出插屏广告,这会严重影响用户体验。应该在用户操作的"间隙"弹出,这样用户既不会太反感,广告曝光率也能保证。


总结一下

插屏广告的接入核心就三步:

  1. 注册事件订阅:创建 InterstitialAdStatusHandler 并调用 registerPPSReceiver(),开始监听广告状态变化
  2. 加载广告:通过 AdLoader 加载广告,adType 设为 12
  3. 展示广告:在 onAdLoadSuccess 回调里调用 advertising.showAd(),系统自动全屏弹出广告

代码量其实不多,核心逻辑集中在 AdsViewModel.etsloadAd 方法里,总共也就十几行。但有一个比较容易忽视的地方——事件订阅的生命周期管理。广告关闭后一定要取消订阅(代码里已经自动处理了),否则可能会收到过时的事件通知。另外,每次展示广告之前都要重新注册订阅,因为上一次的订阅在广告关闭后已经取消了。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐