鸿蒙插屏广告接入指南——弹出来就是全屏的那种
注册事件订阅:创建并调用,开始监听广告状态变化加载广告:通过AdLoader加载广告,adType设为 12展示广告:在回调里调用,系统自动全屏弹出广告代码量其实不多,核心逻辑集中在的loadAd方法里,总共也就十几行。但有一个比较容易忽视的地方——事件订阅的生命周期管理。广告关闭后一定要取消订阅(代码里已经自动处理了),否则可能会收到过时的事件通知。另外,每次展示广告之前都要重新注册订阅,因为上
鸿蒙插屏广告接入指南——弹出来就是全屏的那种
你在刷信息流或者看文章的时候,突然"砰"一下弹出来一个全屏广告——可能是一段短视频,也可能是一张大图,上面有个小叉号可以关掉。这种广告就叫"插屏广告"(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_OPEN、AD_CLICKED、AD_CLOSED、VIDEO_PLAY_BEGIN、VIDEO_PLAY_END 这五个。其他的是贴片广告、激励广告等用到的。
插屏广告事件订阅 InterstitialAdStatusHandler.ets
这是插屏广告最特别的地方。贴片广告的回调是通过 AdComponent 的 interactionListener 来处理的,而插屏广告因为是由系统弹出的,它的事件不是通过组件回调传递的,而是通过 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):
- 如果之前已经注册过订阅(
this.subscriber不为 null),先取消之前的订阅,避免重复订阅 - 创建一个
CommonEventSubscribeInfo,指定要监听的事件名为com.huawei.hms.pps.action.PPS_INTERSTITIAL_STATUS_CHANGED,发布者是com.huawei.hms.adsservice(华为广告服务) - 调用
commonEventManager.createSubscriber()创建订阅者 - 创建成功后,调用
commonEventManager.subscribe()开始监听 - 收到事件后,从
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}`);
}
}
}
这段代码的逻辑是:
- 创建
AdLoadListener,定义成功和失败的回调 - 失败时打印错误日志
- 成功时判断广告类型:
- 如果是插屏广告(
adType === AdType.INTERSTITIAL):- 第一步:注册插屏广告的事件订阅
new InterstitialAdStatusHandler().registerPPSReceiver() - 第二步:调用
advertising.showAd(ads[0], this.adDisplayOptions, this.context)展示广告 - 然后直接 return,不执行后面的逻辑
- 第一步:注册插屏广告的事件订阅
- 如果是其他类型广告(贴片、Banner 等):
- 把广告数组赋给
this.ads,等 UI 组件去渲染
- 把广告数组赋给
- 如果是插屏广告(
- 创建
AdLoader并调用loadAd()
这里就是插屏广告和贴片广告最大的区别:
-
插屏广告在
onAdLoadSuccess里直接调用advertising.showAd()展示广告,不需要把广告对象存到ads数组里,也不需要自定义 UI 组件来渲染。广告会以系统弹出窗口的形式展示。 -
贴片广告在
onAdLoadSuccess里把广告对象存到ads数组里,然后在RollAdPage页面中用AdComponent组件来渲染展示。
advertising.showAd() 是一个静态方法,接收三个参数:
ads[0]:广告对象(取数组第一个元素)this.adDisplayOptions:展示选项(静音、刷新间隔等)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 翻页的间隙
关键原则是:不要在用户正在进行关键操作的时候突然弹出插屏广告,这会严重影响用户体验。应该在用户操作的"间隙"弹出,这样用户既不会太反感,广告曝光率也能保证。
总结一下
插屏广告的接入核心就三步:
- 注册事件订阅:创建
InterstitialAdStatusHandler并调用registerPPSReceiver(),开始监听广告状态变化 - 加载广告:通过
AdLoader加载广告,adType设为 12 - 展示广告:在
onAdLoadSuccess回调里调用advertising.showAd(),系统自动全屏弹出广告
代码量其实不多,核心逻辑集中在 AdsViewModel.ets 的 loadAd 方法里,总共也就十几行。但有一个比较容易忽视的地方——事件订阅的生命周期管理。广告关闭后一定要取消订阅(代码里已经自动处理了),否则可能会收到过时的事件通知。另外,每次展示广告之前都要重新注册订阅,因为上一次的订阅在广告关闭后已经取消了。
更多推荐

所有评论(0)