鸿蒙里怎么接贴片广告——就是你看视频前那段广告
本文介绍了在鸿蒙系统(HarmonyOS)中接入贴片广告的实现方法。贴片广告是视频播放前/中/后插入的广告形式,华为Ads Kit已封装好相关接口。开发者需要配置权限(包括OAID获取权限和网络权限),准备广告位ID,并通过Ads Kit提供的API实现广告加载和展示。文章详细说明了代码结构,包括主页面权限请求、广告加载管理类和广告展示页面,并强调了贴片广告需要与视频播放器配合使用的特性。测试阶段
鸿蒙里怎么接贴片广告——就是你看视频前那段广告
你用视频类 App 的时候肯定遇到过这种场景:点开一个视频,先给你播一段 5 秒、15 秒或者 30 秒的广告,倒计时结束之后才能看正经内容。这种广告在行业里有个专门的术语,叫"贴片广告"(Roll Ad),意思就是"贴"在视频前面、中间或者后面的广告。华为在 HarmonyOS 的 Ads Kit 里把这个能力封装好了,你作为开发者,只需要调几个接口就能让你的 App 里也跑起贴片广告。
这篇文章就带你一步一步把贴片广告接进去,从配置权限到加载广告再到展示广告,每一行代码都会讲到。
贴片广告是什么
贴片广告(Roll Ad)是一种跟视频播放绑定的广告形式。它可以出现在三个位置:
- 前贴片:视频开始播放之前展示的广告(最常见的场景)
- 中贴片:视频播放到一半的时候插进来的广告
- 后贴片:视频播放结束之后展示的广告
不管哪个位置,贴片广告都可以是视频广告也可以是图片广告。大多数情况下你看到的是视频广告——一个全屏的视频画面,右上角有个倒计时"5秒后跳过",还有个小叉号可以手动关闭。
从开发者的角度来看,华为 Ads Kit 把贴片广告的 adType 定义为 60。这个数字很重要,后面构建广告请求参数的时候要用到。作为对比,其他广告类型的 adType 分别是:Banner 广告是 8,插屏广告是 13,激励广告是 35,开屏广告是 32。
贴片广告有一个比较特别的地方——它需要跟视频播放器配合使用。你的页面上得有一个视频播放的区域,广告就覆盖在这个区域上面展示。广告播完了,把覆盖层拿掉,底下的视频就开始播放了。这个交互逻辑你需要自己处理,Ads Kit 只负责广告的加载和展示。
你需要准备什么
在正式写代码之前,有几件事要先搞清楚。
1. 环境要求
- DevEco Studio:NEXT Developer Beta1 及以上
- HarmonyOS SDK:NEXT Developer Beta1 SDK 及以上
- 测试设备:华为手机(标准系统,只支持手机和平板)
2. 广告位 ID
贴片广告需要一个广告位 ID(adId),这就像是你 App 里一个广告"坑位"的编号。测试阶段华为提供了一个测试用的广告位 ID:testy3cglm3pj0。
这个测试 ID 只能用来调测,广告内容是华为的测试广告素材。等你准备正式上线的时候,需要去华为的 Petal Ads(鲸鸿动能)平台申请真实的广告位 ID。
3. OAID
OAID 是 Open Anonymous Device Identifier 的缩写,翻译过来就是"开放匿名设备标识符"。它是华为提供的一个设备标识方案,用来替代 Android 时代的 IMEI。广告系统需要这个标识来做广告的定向投放和效果追踪。
获取 OAID 需要用户授权,所以要声明 ohos.permission.APP_TRACKING_CONSENT 权限,然后在运行时请求用户同意。
配置权限
打开项目的 entry/src/main/module.json5,在 module 节点下添加两个权限:
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.APP_TRACKING_CONSENT",
"reason": "$string:app_tracking_permission_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
},
{
"name": "ohos.permission.INTERNET"
}
]
}
}
两个权限的作用:
ohos.permission.APP_TRACKING_CONSENT:用于获取 OAID。注意这个权限是运行时权限,需要用户手动授权,不是装上 App 就自动有的。reason字段是向用户展示的权限说明理由,你需要把它定义在resources里的字符串资源文件中。ohos.permission.INTERNET:网络权限。广告加载需要联网,没有这个权限广告拉不下来。
至于 oh-package.json5——@kit.AdsKit 是 HarmonyOS 系统内置的 Kit,不需要额外添加依赖。你直接在代码里 import 就能用,不需要像第三方库那样配置依赖关系。
整体代码结构
在看具体代码之前,先了解一下这个案例的代码结构:
entry/src/main/ets
├── viewmodel
│ └── AdsViewModel.ets // 广告加载管理类
├── pages
│ ├── Index.ets // 主页面(OAID 获取 + 导航)
│ └── RollAdPage.ets // 贴片广告展示页面
└── entryability
└── EntryAbility.ets // 应用入口
核心就三个文件:
Index.ets是主页面,负责获取 OAID、展示两个按钮(直接加载 / 预加载)AdsViewModel.ets是广告加载的逻辑封装RollAdPage.ets是贴片广告的展示页面,包含AdComponent和交互回调处理
第一步:主页面 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 { AdsViewModel } from '../viewmodel/AdsViewModel';
import { RollAdPage } from './RollAdPage';
const TAG: string = 'Ads Demo-Index';
导入说明:
abilityAccessCtrl:权限管理模块,用于运行时请求 OAID 权限advertising:广告服务模块,提供AdLoader、AdRequestParams等核心类型identifier:广告标识服务模块,提供getOAID()方法AdsViewModel:我们自己封装的广告加载管理类RollAdPage:贴片广告展示页面
请求 OAID 权限
在写主页面之前,先看一个独立函数——请求 OAID 权限并获取 OAID 值:
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;
}
这段代码的流程是:
- 创建权限管理器
abilityAccessCtrl.createAtManager() - 调用
requestPermissionsFromUser()向用户请求APP_TRACKING_CONSENT权限。这会弹出一个系统对话框,让用户选择"允许"或"拒绝" - 检查返回结果,判断用户是否授权了
- 如果授权了,调用
identifier.getOAID()获取 OAID 值 - 如果用户拒绝了或者获取失败了,返回
undefined
注意这里返回的是 Promise<string | undefined>,意思是可能返回 OAID 字符串,也可能返回 undefined(用户拒绝或者出错了)。调用方需要处理这两种情况。
主页面组件
@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());
这里有几个值得注意的点:
@ComponentV2而不是@Component:这是 HarmonyOS 新版的状态管理装饰器,配套使用@Local和@Trace等。如果你用的是旧版 API,可以用@Component+@State,效果类似。buttonsOptions:一个按钮配置数组,每个按钮对应一种广告加载方式(直接加载 / 预加载)navPathStack:导航栈,用于页面跳转。通过AppStorageV2.connect()从全局存储中获取viewModel:广告加载管理类的实例,封装了广告加载逻辑
初始化按钮配置
async aboutToAppear() {
const oaid = await requestOAID(this.context);
// 贴片广告 — 直接加载
this.buttonsOptions.push({
text: $r('app.string.request_placement_ad_btn'),
adRequestParams: {
adId: 'testy3cglm3pj0',
adType: 60,
isPreload: false,
oaid: oaid
}
});
// 贴片广告 — 预加载模式
this.buttonsOptions.push({
text: $r('app.string.request_placement_preload_ad_btn'),
adRequestParams: {
adId: 'testy3cglm3pj0',
adType: 60,
isPreload: true,
oaid: oaid
}
});
}
这里定义了两个按钮,分别对应贴片广告的两种加载模式:
按钮 1 — 直接加载(isPreload: false):点击后跳转到广告展示页面,在页面里加载广告并展示。用户看到的是:点按钮 → 跳页面 → 加载广告 → 播放广告 → 播完看内容。
按钮 2 — 预加载(isPreload: true):点击后立刻开始加载广告到缓存,但不跳转页面。加载成功后弹个 Toast 提示"预加载成功"。这样用户下次真正要看的时候,广告已经在缓存里了,不需要再等待加载。
两个按钮共用同一个广告位 ID testy3cglm3pj0,唯一的区别就是 isPreload 字段。
adRequestParams 的参数结构:
| 字段 | 类型 | 说明 |
|---|---|---|
adId |
string | 广告位 ID,测试用 testy3cglm3pj0 |
adType |
number | 广告类型,贴片广告固定为 60 |
isPreload |
boolean | 是否为预加载模式 |
oaid |
string | undefined | 设备匿名标识符 |
页面布局和导航
build() {
Navigation(this.navPathStack) {
Column() {
Repeat<ButtonOptions>(this.buttonsOptions).each((repeatItem: RepeatItem<ButtonOptions>) => {
Button(repeatItem.item.text)
.fontSize(20)
.fontWeight(FontWeight.Normal)
.width('90%')
.margin({ top: 10, bottom: 10 })
.onClick(() => {
const options: ButtonOptions = repeatItem.item;
if (options.adRequestParams.isPreload === true) {
this.viewModel.loadAd(options.adRequestParams);
return;
}
this.navPathStack.pushPathByName('Roll', options.adRequestParams);
})
})
}
.justifyContent(FlexAlign.Center)
.height('100%')
.width('100%')
}
.title($r('app.string.roll_ads_demo_title'))
.titleMode(NavigationTitleMode.Mini)
.mode(NavigationMode.Stack)
.hideBackButton(true)
.navDestination(this.pageMap)
}
点击按钮的逻辑:
- 如果是预加载模式:直接调用
this.viewModel.loadAd()加载广告,不跳转页面 - 如果是直接加载模式:通过
navPathStack.pushPathByName('Roll', ...)跳转到贴片广告展示页面,把广告请求参数传过去
pushPathByName 的第二个参数会传递给目标页面,目标页面可以通过 navPathStack.getParamByName('Roll') 获取到。
页面路由映射
@Builder
pageMap(name: string) {
if (name === 'Roll') {
RollAdPage()
}
}
}
interface ButtonOptions {
text: ResourceStr;
adRequestParams: advertising.AdRequestParams;
}
pageMap 是一个 @Builder 方法,用于把路由名称映射到对应的页面组件。当 name 是 'Roll' 的时候,渲染 RollAdPage 组件。
ButtonOptions 是一个接口定义,描述每个按钮的配置:按钮文本 + 广告请求参数。
第二步:广告加载管理类 AdsViewModel.ets
这个类封装了广告加载的核心逻辑,被主页面和广告展示页面共同使用。
import { common } from '@kit.AbilityKit';
import { advertising } from '@kit.AdsKit';
import { AppStorageV2 } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG: string = 'Ads Demo-AdsViewModel';
@ObservedV2
export class AdsViewModel {
@Trace ads: advertising.Advertisement[] = [];
adOptions: advertising.AdOptions = {
totalDuration: 30
};
adDisplayOptions: advertising.AdDisplayOptions = {
mute: true
};
navPathStack: NavPathStack;
private uiContext: UIContext;
private context: common.UIAbilityContext;
constructor(uiContext: UIContext) {
this.uiContext = uiContext;
this.context = uiContext.getHostContext() as common.UIAbilityContext;
this.navPathStack = AppStorageV2.connect(NavPathStack)!;
}
逐个看这些属性:
ads:广告对象数组,用@Trace修饰,意思是这个数组的变化会被追踪,UI 会自动响应。当广告加载成功后,ads会被赋值,触发 UI 刷新。adOptions:广告加载选项。totalDuration: 30表示贴片广告的总时长限制为 30 秒。如果广告素材时长超过 30 秒,会被截断。adDisplayOptions:广告展示选项。mute: true表示默认静音播放广告。很多视频 App 的贴片广告确实是默认静音的,用户可以手动点击取消静音。navPathStack:从全局存储获取的导航栈,用于页面跳转uiContext和context:UI 上下文和应用上下文,广告加载器需要 context 参数
从导航参数获取广告配置
getParamsFromNav(): advertising.AdRequestParams {
return this.navPathStack.getParamByName('Roll')[0] as advertising.AdRequestParams;
}
当从主页面跳转到 RollAdPage 的时候,广告请求参数是通过 pushPathByName('Roll', adRequestParams) 传过来的。这个方法就是把它取出来。
getParamByName 返回的是一个数组,所以用 [0] 取第一个元素。
核心方法:加载广告
async loadAd(adRequestParams: advertising.AdRequestParams): Promise<void> {
const adLoadListener: advertising.AdLoadListener = {
onAdLoadFailure: (errorCode: number, errorMsg: string) => {
hilog.error(0x0000, TAG, `Failed to load ad. Code is ${errorCode}, message is ${errorMsg}`);
if (adRequestParams.isPreload === true) {
try {
this.uiContext.getPromptAction().showToast({ message: 'Preload content is empty' });
} catch (e) {
hilog.error(0x0000, 'testTag', `Failed to show toast. Code is ${e.code}, message is ${e.message}`);
}
return;
}
},
onAdLoadSuccess: (ads: Array<advertising.Advertisement>) => {
hilog.info(0x0000, TAG, 'Succeeded in loading ad');
if (adRequestParams.isPreload === true) {
try {
this.uiContext.getPromptAction().showToast({ message: 'Succeeded in preloading ad' });
} catch (e) {
hilog.error(0x0000, 'testTag', `Failed to show toast. 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:
onAdLoadFailure(加载失败):
- 打印错误日志,包含错误码和错误信息
- 如果是预加载模式(
isPreload === true),弹一个 Toast 告诉用户"预加载内容为空",然后 return。预加载失败不需要更新 UI,因为根本没有跳转到广告页面 - 如果是直接加载模式,什么都不做——广告页面会展示一个空的状态或者跳过广告直接播放视频
onAdLoadSuccess(加载成功):
- 如果是预加载模式,弹 Toast 告诉用户"预加载成功",然后 return。广告数据会被系统缓存起来,下次直接使用时不需要再加载
- 如果是直接加载模式,把广告对象数组赋值给
this.ads。因为ads被@Trace修饰,UI 会自动刷新,广告就会展示出来
创建加载器并加载:
const adLoader: advertising.AdLoader = new advertising.AdLoader(this.context);
adLoader.loadAd(adRequestParams, this.adOptions, adLoadListener);
创建一个 AdLoader 实例,传入应用的 context。然后调用 loadAd() 方法,传入三个参数:
adRequestParams:广告请求参数(adId、adType、isPreload、oaid)this.adOptions:广告选项(totalDuration)adLoadListener:加载监听器(成功/失败回调)
这个方法本身是异步的(async),但 loadAd 内部通过回调返回结果,不是 Promise。所以这里用 async 只是为了让方法签名看起来整齐,实际上广告的结果是在 adLoadListener 的回调里处理的。
第三步:广告展示页面 RollAdPage.ets
这是最关键的一个文件——贴片广告在这里展示,所有的交互回调也在这里处理。
import { common } from '@kit.AbilityKit';
import { AdComponent, advertising } from '@kit.AdsKit';
import { window } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { AdsViewModel } from '../viewmodel/AdsViewModel';
const TAG: string = 'Ads Demo-RollAdPage';
@ComponentV2
export struct RollAdPage {
@Local private countDownText: string = '';
@Local private rollPlayState: number = 1;
@Local private isPlayVideo: boolean = false;
@Local private ratio: number = 16 / 9;
private playedAdCnt: number = 0;
private countDownTextPlaceholder: string = '%d | %s';
private viewModel: AdsViewModel = new AdsViewModel(this.getUIContext());
private context: common.UIAbilityContext = this.getUIContext().getHostContext() as common.UIAbilityContext;
状态变量说明:
| 变量 | 类型 | 初始值 | 作用 |
|---|---|---|---|
countDownText |
string | '' |
倒计时文本,比如"5 | VIP会员免广告" |
rollPlayState |
number | 1 |
广告播放状态,1 表示播放中 |
isPlayVideo |
boolean | false |
是否开始播放实际视频内容(广告播完后变为 true) |
ratio |
number | 16/9 |
页面宽高比,竖屏广告用 16:9,横屏广告用 -1(铺满) |
playedAdCnt |
number | 0 |
已播完的广告计数(一次可能加载多条广告) |
countDownTextPlaceholder |
string | '%d | %s' |
倒计时文本模板,%d 是秒数,%s 是提示文字 |
页面初始化
aboutToAppear() {
try {
const countDownTextDesc =
this.context.resourceManager.getStringSync($r('app.string.ad_free_for_VIP_members').id);
this.countDownTextPlaceholder = this.countDownTextPlaceholder.replace('%s', countDownTextDesc);
} catch (e) {
hilog.error(0x0000, 'testTag',
`Failed to get count down text. Code is ${e.code}, message is ${e.message}`);
}
const adRequestParams: advertising.AdRequestParams = this.viewModel.getParamsFromNav();
this.viewModel.loadAd(adRequestParams);
}
页面显示之前做两件事:
- 初始化倒计时文本模板:从资源文件中读取"VIP 会员免广告"之类的提示文字,拼接到模板里。最终效果可能是 “5 | VIP会员免广告” 这样的倒计时文案。
- 加载广告:从导航参数中获取广告请求配置,然后调用
viewModel.loadAd()开始加载广告。
页面销毁时的清理
aboutToDisappear(): void {
this.setWindowPreferredOrientation(window.Orientation.UNSPECIFIED);
this.setWindowSystemBar(['status', 'navigation']);
}
贴片广告播放过程中可能会把屏幕方向改成横屏、隐藏系统状态栏。当页面销毁的时候,需要把这些设置恢复原样:
- 屏幕方向恢复为
UNSPECIFIED(系统自动决定) - 系统状态栏和导航栏重新显示出来
如果不做这个恢复,用户退出广告页面回到其他页面之后,屏幕方向和状态栏就会是错的。
核心:AdComponent 广告组件
现在来看最重要的部分——build() 方法里的广告组件:
build() {
NavDestination() {
Stack({ alignContent: Alignment.TopEnd }) {
if (this.viewModel.ads.length !== 0 && !this.isPlayVideo) {
AdComponent({
ads: [...this.viewModel.ads],
rollPlayState: this.rollPlayState,
displayOptions: this.viewModel.adDisplayOptions,
interactionListener: {
onStatusChanged: (status: string, ad: advertising.Advertisement, data: string) => {
// ... 回调处理
}
}
})
.width('100%')
.height('100%')
AdComponent 是华为提供的广告展示组件,它直接作为 ArkUI 组件嵌入到页面里。
参数解释:
ads:广告对象数组,来自viewModel.ads。用展开运算符[...]拷贝了一份,是为了避免直接修改原始数组。广告对象是onAdLoadSuccess回调里传过来的advertising.Advertisement类型的数组。rollPlayState:播放状态。1表示播放中。这个名字有点误导,实际上它不是控制播放/暂停的开关,而是告诉组件当前的播放上下文。displayOptions:展示选项,就是前面在AdsViewModel里定义的{ mute: true }。interactionListener:交互事件监听器,这是最核心的部分。
广告交互回调详解
interactionListener 里面只有一个方法 onStatusChanged,它通过 status 字符串来区分不同的事件:
onStatusChanged: (status: string, ad: advertising.Advertisement, data: string) => {
switch (status) {
case 'onAdFail':
this.isPlayVideo = true;
break;
case 'onPortrait':
this.setWindowPreferredOrientation(window.Orientation.PORTRAIT);
this.setWindowSystemBar(['status', 'navigation']);
this.ratio = 16 / 9;
break;
case 'onLandscape':
this.setWindowPreferredOrientation(window.Orientation.LANDSCAPE);
this.setWindowSystemBar([]);
this.ratio = -1;
break;
case 'onMediaProgress':
break;
case 'onMediaStart':
break;
case 'onMediaPause':
break;
case 'onMediaStop':
break;
case 'onMediaComplete':
this.playedAdCnt++;
if (this.playedAdCnt === this.viewModel.ads.length) {
this.isPlayVideo = true;
}
break;
case 'onMediaError':
break;
case 'onMediaCountdown':
const parseData: Record<string, Object> = this.safeParseData(data);
this.countDownText = this.countDownTextPlaceholder
.replace('%d', String(parseData.countdownTime));
break;
case 'onBackClicked':
this.viewModel.navPathStack.pop();
break;
}
}
逐个事件解释:
| status | 触发时机 | 处理逻辑 |
|---|---|---|
onAdFail |
广告展示失败 | 直接把 isPlayVideo 设为 true,跳过广告播放视频 |
onPortrait |
广告切换到竖屏 | 设置窗口为竖屏方向,显示状态栏和导航栏,比例设为 16:9 |
onLandscape |
广告切换到横屏 | 设置窗口为横屏方向,隐藏状态栏和导航栏,比例设为 -1(铺满全屏) |
onMediaProgress |
视频播放进度更新 | 什么都不做(可以用来做进度条) |
onMediaStart |
视频开始播放 | 什么都不做 |
onMediaPause |
视频暂停 | 什么都不做 |
onMediaStop |
视频停止 | 什么都不做 |
onMediaComplete |
单条广告播放完毕 | 计数器 +1,如果所有广告都播完了,设置 isPlayVideo 为 true |
onMediaError |
视频播放出错 | 什么都不做 |
onMediaCountdown |
倒计时更新 | 解析 data 里的 countdownTime,更新倒计时文本 |
onBackClicked |
用户点击返回 | 弹出当前页面 |
这里面最需要说清楚的是两个逻辑:
横竖屏切换:贴片广告可能是横屏视频也可能是竖屏视频。当广告素材是横屏的时候,onLandscape 会触发,你需要把整个窗口切到横屏模式,同时隐藏状态栏和导航栏,让广告全屏展示。当切回竖屏的时候,onPortrait 触发,恢复竖屏和系统栏。
广告播放完毕的判断:一次广告请求可能返回多条广告(比如一个广告位配了多个广告素材),所以需要用 playedAdCnt 计数器来追踪。每条广告播完触发一次 onMediaComplete,当计数器等于广告总数的时候,说明所有广告都播完了,这时候才设置 isPlayVideo = true 开始播放实际的视频内容。
倒计时跳过按钮
// 倒计时跳过按钮
Text(this.countDownText)
.fontSize(12)
.lineHeight(12)
.maxLines(1)
.textAlign(TextAlign.Center)
.fontColor($r('sys.color.font_on_primary'))
.textOverflow({ overflow: TextOverflow.Ellipsis })
.backgroundColor($r('app.color.count_down_background'))
.border({ radius: 25 })
.padding(8)
.margin(16)
.height(24)
.onClick(() => {
this.isPlayVideo = true;
})
.visibility(this.countDownText ? Visibility.Visible : Visibility.None)
这是一个覆盖在广告右上角的小按钮,显示倒计时文本(比如"5 | VIP会员免广告")。
布局细节:
- 外层
Stack用了alignContent: Alignment.TopEnd,所以子组件默认贴右上角 - 按钮是圆角药丸形状(
borderRadius: 25,高度 24,所以两端是半圆) - 白色文字、深色半透明背景
- 点击后直接把
isPlayVideo设为 true,跳过广告
visibility 绑定了 countDownText:如果倒计时文本为空(广告还没加载好或者倒计时还没开始),按钮不显示。
广告播完后的视频内容
// 广告播放完毕后展示的视频内容
Video({
src: $rawfile('videoTest.mp4'),
previewUri: $r('app.media.video_preview'),
controller: new VideoController()
})
.visibility(this.isPlayVideo ? Visibility.Visible : Visibility.None)
.autoPlay(this.isPlayVideo)
.controls(false)
.width('100%')
.height('100%')
当 isPlayVideo 为 true 的时候(广告播完了或者被跳过了),底层的 Video 组件显示出来并自动播放。
这里用的是 $rawfile('videoTest.mp4'),意思是视频文件放在了 resources/rawfile/ 目录下。previewUri 是视频封面图,放在了 resources/base/media/ 目录下。
controls(false) 隐藏了视频播放器的控制栏,你可以根据需求改成 true。
辅助方法
private setWindowPreferredOrientation(orientation: window.Orientation): void {
try {
const windowClass = window.findWindow('mainWindow');
windowClass.setPreferredOrientation(orientation);
} catch (exception) {
hilog.error(0x0000, TAG,
`Failed to set window preferred orientation. Code is ${exception.code}, message is ${exception.message}`);
}
}
private setWindowSystemBar(systemBarProp: Array<string>): void {
try {
const windowClass = window.findWindow('mainWindow');
const enable: boolean = systemBarProp.length > 0;
windowClass.setWindowSystemBarEnable(enable ? systemBarProp : []);
} catch (exception) {
hilog.error(0x0000, TAG,
`Failed to set window system bar. Code is ${exception.code}, message is ${exception.message}`);
}
}
private safeParseData(data: string): Record<string, Object> {
try {
return JSON.parse(data) as Record<string, Object>;
} catch (e) {
hilog.error(0x0000, TAG, `Failed to parse data. Code is ${e.code}, message is ${e.message}`);
return {};
}
}
三个辅助方法:
setWindowPreferredOrientation:设置窗口的屏幕方向。横屏传window.Orientation.LANDSCAPE,竖屏传window.Orientation.PORTRAIT,自动传UNSPECIFIED。通过window.findWindow('mainWindow')获取主窗口实例。setWindowSystemBar:控制状态栏和导航栏的显示/隐藏。传入['status', 'navigation']表示都显示,传入[]空数组表示都隐藏。safeParseData:安全地解析 JSON 字符串。onMediaCountdown回调里的data参数是 JSON 字符串,需要 parse 之后才能拿到countdownTime字段。加了 try-catch 是为了防止解析失败导致崩溃。
贴片广告和预加载模式
前面提到了两种加载模式,这里再详细解释一下区别。
直接加载模式(isPreload: false)的流程是:
用户点击按钮 → 跳转到广告页面 → 加载广告(需要等待) → 展示广告 → 播完跳转到视频
这种模式下,用户点击按钮后会先看到一个加载中的状态(广告还没加载好),等广告数据回来了才开始播放。用户可能会感受到短暂的等待。
预加载模式(isPreload: true)的流程是:
用户在主页面时 → 后台预先加载广告(用户感知不到)
用户真正需要看的时候 → 直接从缓存读取广告 → 立即展示
这种模式下,广告提前加载好了放在缓存里,用户真正要看的时候不需要等待加载,体验更流畅。
预加载适合的场景是:你知道用户马上就要看到广告了(比如视频列表页加载完之后,用户点击某个视频的概率很高),这时候可以提前把贴片广告加载好。预加载成功后,后续的 loadAd 调用(isPreload: false)会直接从缓存取数据,不需要再走网络请求。
Ads Kit 的 API 全景
最后把这篇文章用到的核心 API 整理一下:
@kit.AdsKit
├── advertising.AdLoader // 广告加载器
│ └── loadAd(params, options?, listener)
├── advertising.AdRequestParams // 广告请求参数
│ ├── adId: string // 广告位 ID
│ ├── adType: number // 广告类型(60 = 贴片广告)
│ ├── isPreload: boolean // 是否预加载
│ └── oaid: string | undefined // 设备匿名标识
├── advertising.AdOptions // 广告加载选项
│ └── totalDuration: number // 广告总时长限制(秒)
├── advertising.AdDisplayOptions // 广告展示选项
│ └── mute: boolean // 是否静音播放
├── advertising.AdLoadListener // 广告加载监听
│ ├── onAdLoadSuccess(ads) // 加载成功
│ └── onAdLoadFailure(code, msg) // 加载失败
├── advertising.Advertisement // 广告对象
├── AdComponent // 广告展示组件
│ ├── ads // 广告对象数组
│ ├── rollPlayState // 播放状态
│ ├── displayOptions // 展示选项
│ └── interactionListener // 交互回调
└── identifier.getOAID() // 获取设备 OAID
如果你以后还要接其他类型的广告(Banner、插屏、激励等),核心流程是一样的——创建 AdLoader,构建 AdRequestParams(改 adType),调用 loadAd,拿到广告对象后用 AdComponent 展示。区别主要在于 adType 的值不同、展示方式不同(贴片是全屏视频,Banner 是横条,插屏是弹出层)。
总结一下
贴片广告的接入主要分四步:
- 配置权限:在
module.json5里声明 OAID 权限和网络权限 - 获取 OAID:在页面初始化时请求权限并获取设备标识
- 加载广告:通过
advertising.AdLoader创建加载器,传入广告请求参数、加载选项和回调监听器 - 展示广告:用
AdComponent组件展示广告,处理各种交互回调(播放状态、倒计时、横竖屏切换、播放完毕等)
整个过程中,比较容易出问题的地方是权限配置(OAID 权限没声明或者用户拒绝了)和广告位 ID 填错了。测试阶段一定要用 testy3cglm3pj0 这个测试 ID,不要填真实的广告位 ID,否则在开发环境里可能拉不到广告。另外,@kit.AdsKit 是系统内置的,不需要在 oh-package.json5 里添加依赖,直接 import 就行——这个点很容易跟其他第三方库搞混。
更多推荐

所有评论(0)