鸿蒙里怎么接贴片广告——就是你看视频前那段广告

你用视频类 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:广告服务模块,提供 AdLoaderAdRequestParams 等核心类型
  • 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;
}

这段代码的流程是:

  1. 创建权限管理器 abilityAccessCtrl.createAtManager()
  2. 调用 requestPermissionsFromUser() 向用户请求 APP_TRACKING_CONSENT 权限。这会弹出一个系统对话框,让用户选择"允许"或"拒绝"
  3. 检查返回结果,判断用户是否授权了
  4. 如果授权了,调用 identifier.getOAID() 获取 OAID 值
  5. 如果用户拒绝了或者获取失败了,返回 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:从全局存储获取的导航栈,用于页面跳转
  • uiContextcontext: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() 方法,传入三个参数:

  1. adRequestParams:广告请求参数(adId、adType、isPreload、oaid)
  2. this.adOptions:广告选项(totalDuration)
  3. 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);
  }

页面显示之前做两件事:

  1. 初始化倒计时文本模板:从资源文件中读取"VIP 会员免广告"之类的提示文字,拼接到模板里。最终效果可能是 “5 | VIP会员免广告” 这样的倒计时文案。
  2. 加载广告:从导航参数中获取广告请求配置,然后调用 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%')

isPlayVideotrue 的时候(广告播完了或者被跳过了),底层的 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 是横条,插屏是弹出层)。


总结一下

贴片广告的接入主要分四步:

  1. 配置权限:在 module.json5 里声明 OAID 权限和网络权限
  2. 获取 OAID:在页面初始化时请求权限并获取设备标识
  3. 加载广告:通过 advertising.AdLoader 创建加载器,传入广告请求参数、加载选项和回调监听器
  4. 展示广告:用 AdComponent 组件展示广告,处理各种交互回调(播放状态、倒计时、横竖屏切换、播放完毕等)

整个过程中,比较容易出问题的地方是权限配置(OAID 权限没声明或者用户拒绝了)和广告位 ID 填错了。测试阶段一定要用 testy3cglm3pj0 这个测试 ID,不要填真实的广告位 ID,否则在开发环境里可能拉不到广告。另外,@kit.AdsKit 是系统内置的,不需要在 oh-package.json5 里添加依赖,直接 import 就行——这个点很容易跟其他第三方库搞混。

Logo

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

更多推荐