鸿蒙广告自定义的四种模式-根据场景选择

170 阅读6分钟

一、应用场景

app冷启动/热启动-有广告需求,就打开广告页,没有的话就去登录或者主页

华为有广告业务,但是我们不用

自定义一个ad模块,但是根据不同的业务场景和需求,可以使用不同的打开广告方式

  1. 应用启动时,有广告则直接进入广告页,定时结束或点击跳过,跳转到首页
  2. 打开子窗口,然后通过销毁子窗口方式关闭广告
  3. 封装window窗口广告模式,关闭时直接调用封装的关闭方法
  4. 采用与页面解耦的模式实现广告,关闭时候直接调用封装的关闭方法

总结处有附完整代码

二、新建一个关于广告类的数据模型

1.创建广告类

这个广告模型其实在哪里创建不影响,只要导出,在需要用到的地方可以正常导入就行,以为本项目有har包,就直接创建到har包里-basic- viewmodels/advert.ets,导出省略

export class AdvertClass {

showAd: boolean = false // 是否展示广告

isFull: boolean = true // 是否全屏

adTime: number = 5 // 倒计时数据

adUrl?: string = "" // 要跳转的连接

adImg?: ResourceStr = "" // 图片连接

}

// 全屏广告 半屏广告

2.本地存储广告用来读取和设置广告

这里用的方式不同于首选项方式,而是使用AppStorage和持久化本地存储的方式,这种方式更加简单。

当然无论是首选项的方式还是什么都只是将广告信息存储,都不影响后面任何模式的使用。

import { USER_SETTING, USER_SETTING_AD } from '../constants'

import { AdvertClass } from '../viewmodels'

export const defaultAd: AdvertClass = {

showAd: true,

isFull: true,

adTime: 3,

adImg: $r("app.media.start")

}

// 首选项方式,每次使用前记得给context赋值

// 负责首选项的读取

// export class UserSetting {

// context?: Context

// // 获取仓库

// getStore () {

// return preferences.getPreferencesSync(this.context || getContext(), {

// name: USER_SETTING

// })

// }

// // 设置用户广告

// async setUserAd(ad: AdvertClass) {

// const store = this.getStore()

// store.putSync(USER_SETTING_AD, ad)

// await store.flush() // 让外界能够控制自己的流程

// }

// // 获取用户广告

// getUserAd() {

// const store = this.getStore()

// return store.getSync(USER_SETTING_AD, defaultAd) as AdvertClass

// }

// }

export class UserSetting{

// 初始化仓库

initUserSetting(){

PersistentStorage.persistProp(USER_SETTING_AD,defaultAd)

}

// 设置用户广告

setUserAd(ad: AdvertClass) {

AppStorage.setOrCreate(USER_SETTING_AD, ad)

}

// 获取用户广告

getUserAd(){

return AppStorage.get(USER_SETTING_AD)

}

}

export const userSetting = new UserSetting() // 导出一个单例

但是由于在应用开启时就有打开广告的需求,所以要在EntryAbility中的onWindowStageCreate中就对仓库进行初始化,而不是Index.ets页面。

onWindowStageCreate(){

// ...省略

// 初始化广告仓库

windowStage.loadContent('pages/Index', (err) => {

if (err.code) {

hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));

return;

}

// 初始化广告仓库

userSetting.initUserSetting()

hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');

});

// ...省略

}

这里还要设置常量USER_SETTING_AD,但由于用的不是首选项,所以只要广告名,不需要仓库名

export const USER_SETTING_AD = 'fast_driver_setting_ad' // 用来存储用户设置广告首选项的key

如果你使用的是har包存放静态资源,最后统一在basic/Index.ets统一导出,但是这样导出有个bug,在使用的地方导入,会有代码提示,但是会报错说找不到导出的资源

export * from 'ets/utils'

export * from 'ets/constants'

export * from 'ets/viewmodels'

如果报错请补全路径

export * from './src/main/ets/utils'

export * from './src/main/ets/constants'

export * from './src/main/ets/viewmodels'

最后在ability中引入该har包依赖

如果时在har包中定义的数据,记得在entry中导入你har包的数据.

{

"name": "entry",

"version": "1.0.0",

"description": "Please describe the basic information.",

"main": "",

"author": "",

"license": "",

"dependencies": {

"basic":"file:../basic"

}

}

3.新建一个广告页面Start

Start页面中,读取本地存储的广告,展示广告内容,实现倒计时逻辑,点击跳过会调用toMain()方法。

toMain()方法其实就是你关闭广告的方式,选择不同的广告模式,就要修改不同的toMain()方法

前三种模式都会用到Start页面,所以选择不同方式打开广告,就要用不同方式去关闭广告。

import { AdvertClass, userSetting } from 'basic'

import { router } from '@kit.ArkUI'

@Entry

@Component

struct Start {

// 需要广告对象

@State

ad: AdvertClass = new AdvertClass()

timer: number = -1 // 用来记录定时器的标记

// @State

// ad: Partial = {}

aboutToAppear(): void {

// 获取首选项的广告数据给到ad

this.getAdInfo()

}

getAdInfo() {

// 首选项的读取

this.ad = userSetting.getUserAd()

// 开启倒计时了

if (this.ad.showAd) {

// 如果真的展示广告要开始倒计时

this.timer = setInterval(() => {

if (this.ad.adTime === 0) {

clearInterval(this.timer)

this.toMain()

return // return一定要写

}

this.ad.adTime--

}, 1000)

}

}

// 去主页的方法

toMain() {

// 关闭广告的方式

打开方式不同,关闭方式也就不同

}

aboutToDisappear(): void {

clearInterval(this.timer)

}

build() {

RelativeContainer() {

if (this.ad.showAd) {

Image(this.ad.adImg)

.width("100%")

.height("100%")

.objectFit(ImageFit.Cover)

Text(${this.ad.adTime}秒跳过)

.padding({

left: 10,

right: 10

})

.alignRules({

right: {

anchor: 'container',

align: HorizontalAlign.End

},

top: {

anchor: 'container',

align: VerticalAlign.Top

}

})

.borderRadius(15)

.height(30)

.fontSize(14)

.backgroundColor($r("app.color.background_page"))

.margin({

right: 20,

top: 20

})

.onClick(() => {

// 此时跳过广告

// 跳转到主页

this.toMain()

})

}

}

.height('100%')

.width('100%')

}

}

4.初始化广告信息

因为广告随时有切换的可能,但是我们又不能每次换个广告就去改源码,所以其实是通过发请求的方式,去获取广告的信息,但你有可能在启动时就需要打开广告页,所以依然是在EntryAbility中的onWindowStageCreate方法中就请求回广告信息,并存入本地存储,供广告页面使用。

不过这里我们模拟发送请求就好了,到时候有真实接口再真的请求数据

async onWindowStageCreate(windowStage: window.WindowStage): Promise {

// Main window is created, set main page for this ability

hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');

// 假装向云端获取广告数据的请求

const ad = await new Promise((resolve, reject) => {

setTimeout(() => {

resolve({

showAd: true,

isFull: true,

adTime: 3,

adImg: $r("app.media.start")

})

}, 500)

})

userSetting.setUserAd(ad); // 云端的广告设置到本地

}

一切准备就绪,接下来就时选择不同打开广告方式了。

方式一:应用启动时,有广告则直接进入广告页

这种方式是直接判断有无广告,然后通过windowStage.loadContent()直接打开广告页或者首页,

不过在应用启动时打开广告比较适用,不适合在使用中途打开广告。

// //1. 判断是否需要展示广告

const path = ad.showAd ? 'pages/Start/Start' : 'pages/Index'

// 根据抽取的路径加载对应path, 如果要用这种模式,将默认路径 'pages/Index' 替换为path

// 使用这种只适用于刚打开应用时,因为他的关闭方式其实是直接跳转到首页(固定的)

windowStage.loadContent(path, (err) => {

if (err.code) {

hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));

return;

}

// 初始化广告仓库

userSetting.initUserSetting()

hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');

});

当然使用方式一就要在广告Start页面修改适用他的toMain()方法,点击跳过或者倒计时结束,直接跳转到首页

toMain() {

// 1. 方式一打开广告,关闭是直接跳转到主页

router.replaceUrl({

url: 'pages/Index'

})

}

方式二:打开子窗口,然后通过销毁子窗口方式关闭广告

这种方式相当于在不影响加载主页的同时,另外打开了一个页面,覆盖在主页上,到时候关闭直接露出主页

// 2. 略微有点意思的模式:创建二级窗口,并展示广告

// 用这种方式就在 'pages/Start/Start' 中将关闭广告的方法改为 2

if (ad.showAd) {

const win = await windowStage.createSubWindow("ad_win") // 二级窗口的实际对象

await win.showWindow() // 展示二级窗口

win.setUIContent("pages/Start/Start") // 二级窗口放的还是Start广告页

}

// 这里还是需要正常打开首页的

windowStage.loadContent('pages/Index', (err) => {

if (err.code) {

hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));

return;

}

// 初始化广告仓库

userSetting.initUserSetting()

hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');

});

当然,在广告页Start中的toMain(),也要将关闭方式修改

toMain() {

// 2.使用子窗口模式关闭广告

clearInterval(this.timer) // 先清理一下定时器

this.closeWin()

}

// 这个关闭方法传入刚才打开的子窗口名称即可

closeWin () {

window.findWindow("ad_win").destroyWindow()

}

方式三:封装window窗口广告模式

这种方式是打开一个宽高自定义的窗口,然后把广告页面放在窗口中,通过封装类导出实例对象,调用showAd和closeAd方法来打开关闭广告。

如果不好理解可以先不用管里面具体的API都是什么,就看showAd方法和closeAd方法,和导出的实例,用实例调用打开和关闭的方法,看方法需要什么参数给他传什么参数就行了。

import { display, window } from '@kit.ArkUI'

import { util } from '@kit.ArkTS'

export class AdManager {

context?: Context // 是给ability使用的

private winNames: string [] = []

// 展示广告 采用windows窗口的创建和销毁的方式

async showAd(url: string, width?: number, height?: number) {

if (url) {

let name = win_${util.generateRandomUUID()}

const win = await window.createWindow({

name,

windowType: window.WindowType.TYPE_DIALOG,

ctx: this.context || getContext()

})

if (width && width >= 320 && height && height >= 240) {

const screen = display.getDefaultDisplaySync()

let mainWidth = vp2px(width)

let mainHeight = vp2px(height)

win.resizeAsync(mainWidth, mainHeight)

win.moveWindowToAsync((screen.width - mainWidth) / 2, (screen.height - mainHeight) / 2)

}

await win.showWindow() // 展示窗口

win.setUIContent(url) // 设置地址

this.winNames.push(name)

return name // showAd方法返回了一个name可以接受存下来,用于关闭广告

}

return ""

}

// 关闭广告

async closeAd(name?: string) { // 传入刚才存的name关闭具体小窗,不穿则关闭所有小窗

if (name) {

window.findWindow(name).destroyWindow()

this.winNames = this.winNames.filter(item => item !== name) //清空数组内容

} else {

// 不传就认为 想关闭所有

let index = 0

while (index < this.winNames.length) {

await window.findWindow(this.winNames[index]).destroyWindow()

index++

}

this.winNames = [] // 清空数组

}

}

}

export const adManger = new AdManager()

在EntryAbility.ets中,判断是否需要广告,然后打开小窗口。在别的地方使用也可。

// 3.使用封装window窗口广告模式

// 用这种方式就在 'pages/Start/Start' 中将关闭广告的方法改为 3:adManger.closeAd()

if(ad.showAd){

adManger.context = this.context

adManger.showAd('pages/Start/Start', 320, 240) // 设置小窗宽高

// 如果你在这里接收了一个name,那么也要存起来,在关闭时使用。

// 不过因为暂时只打开了一个小窗,不存关闭所有也是相同的效果

}

在广告页Start中修改关闭方法toMain()

toMain() {

// 3.使用封装window窗口广告模式

clearInterval(this.timer) // 先清理一下定时器

adManger.closeAd() //这里如果不传入name就会关闭所有的小窗

}

方式四:采用与页面解耦的模式实现广告

这种方式相当于弹出一个类似消息提示的弹框(dialog),也是通过封装类导出实例,实现打开和关闭广告的方法。

这个种没有实现倒计时,需要用户手动去点击关闭按钮或者使用返回键关闭广告。

但是这种方式需要给广告类再增加一个dialogName属性,当然叫什么无所谓,就是后面用于关闭弹窗。

这种方式用的不是Start页面,而是自定义的弹框builder,所以不用去Start中修改关闭方式

export class AdvertClass {

showAd: boolean = false // 是否展示广告

isFull: boolean = true // 是否全屏

adTime: number = 5 // 倒计时数据

adUrl?: string = "" // 要跳转的连接

adImg?: ResourceStr = "" // 图片连接

dialogName?: string = "" // 新增的name属性

}

封装ad_manager_final工具

这里因为使用的不是Start页面,所以不用去Start做相应的关闭逻辑

import { ComponentContent, promptAction, window } from '@kit.ArkUI'

import { util } from '@kit.ArkTS'

import { AdvertClass } from 'basic'

// 展示广告的结构最重要写的代码

@Builder

function AdBuilder(ad: AdvertClass) {

Column() {

Image(ad.adImg)

.width("100%")

.height("100%")

.objectFit(ImageFit.Cover)

.borderRadius(10)

Row() {

Image($r("app.media.ic_btn_close"))

.width(14)

.aspectRatio(1)

}

.position({ // 不加定位会看不到关闭按钮

right: 20,

top: 20

})

.width(30)

.aspectRatio(1)

.justifyContent(FlexAlign.Center)

.borderRadius(15)

.border({

color: '#ff343232',

width: 2

})

.margin({

top: 40

})

.onClick(() => {

if (ad.dialogName) {

// 点击关闭的逻辑在这里

adManagerFinal.closeAd(ad.dialogName) // ? name从哪里进来

}

})

}

.width(ad.isFull ? "100%" : "80%")

.height(ad.isFull ? "100%" : "50%")

}

export class AdManagerFinal {

context?: Context

// 所有的弹窗都放到这个map中 通过name来标识

private map: Map<string, ComponentContent> = new Map()

// 实际上需要广告

async showAd(ad: AdvertClass) {

// 按照文档实现

// UIContext上下文必须得等到页面初始化之后才可以进行获取

// 生成一个name

let name = dialog_${util.generateRandomUUID()}

// 通过当前的主窗口来获取

const mainWin = await window.getLastWindow(this.context || getContext())

let uiContext = mainWin.getUIContext() // 拿到UIContext

let promptAction = uiContext.getPromptAction();

ad.dialogName = name // 目的是将dialog的弹窗名称传递到builder中

let contentNode = new ComponentContent(uiContext, wrapBuilder(AdBuilder), ad);

let options: promptAction.BaseDialogOptions = {

alignment: DialogAlignment.Center,

autoCancel: false

};

this.map.set(name, contentNode) // 将key/value写入到map中

promptAction.openCustomDialog(contentNode, options);

// 一般半屏广告 是得用户手动点击才能关闭的 一般不会自动关闭

// setTimeout(() => {

// promptAction.closeCustomDialog(contentNode)

// }, 2000)

}

async closeAd(name: string) {

if (name) {

const mainWin = await window.getLastWindow(this.context || getContext())

let uiContext = mainWin.getUIContext() // 拿到UIContext

let promptAction = uiContext.getPromptAction();

promptAction.closeCustomDialog(this.map.get(name))

// 清理map

this.map.delete(name) // 删除已经关闭的弹窗

}

}

}

export const adManagerFinal = new AdManagerFinal()

在EntryAbility.ets中,判断是否需要广告,打开广告。在别的地方使用也可。

// 4.采用与页面解耦的模式实现广告

// 这种模式是单独打开一个全屏模态框,而不是打开 'pages/Start/Start' 页面,关闭按钮也在 'pages/Start/Start' 页面中

if (ad.showAd) {

adManagerFinal.context = this.context

adManagerFinal.showAd(ad) // 展示广告

}

这种方式是单独拉起一个类似dialog弹框,所以样式是重新定义的,不需要去Start页面修改toMain()方法

总结:

  1. 四种方式各有不同,需要在不同业务场景下合理选择。

  2. 第一种和第二种方式更适合在应用刚打开时的广告展示

  3. 第三种和第四种由于是封装导出的实例,更适合在全局使用。

  4. 前三种都是使用了Start页面,第四种是类似dialog弹框,他们的关闭方式各不相同。

附:完整的EntryAbility.ets和Start.ets

EntryAbility种的1、2、3分别对应Start中toMain()的1、2、3方法

import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';

import { hilog } from '@kit.PerformanceAnalysisKit';

import { window } from '@kit.ArkUI';

import { AdvertClass, userSetting } from 'basic';

import { BusinessError } from '@kit.BasicServicesKit';

import { adManagerFinal, adManger } from '../utils';

const DOMAIN = 0x0000;

export default class EntryAbility extends UIAbility {

onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {

this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);

hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');

}

onDestroy(): void {

hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy');

}

async onWindowStageCreate(windowStage: window.WindowStage): Promise {

// Main window is created, set main page for this ability

hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');

// 初始化广告仓库

userSetting.initUserSetting()

// 假装向云端获取广告数据的请求

const ad = await new Promise((resolve, reject) => {

setTimeout(() => {

resolve({

showAd: true,

isFull: true,

adTime: 3,

adImg: $r("app.media.start")

})

}, 500)

})

userSetting.setUserAd(ad); // 云端的广告设置到本地

// 2. 略微有点意思的模式:创建二级窗口,并展示广告

// 用这种方式就在 'pages/Start/Start' 中将关闭广告的方法改为 2

// if (ad.showAd) {

// const win = await windowStage.createSubWindow("ad_win") // 二级窗口的实际对象

// await win.showWindow() // 展示二级窗口

// win.setUIContent("pages/Start/Start")

// }

// //1. 判断是否需要展示广告

// const path = ad.showAd ? 'pages/Start/Start' : 'pages/Index'

// 根据抽取的路径加载对应path, 如果要用这种模式,将默认路径 'pages/Index' 替换为path

// 使用这种只适用于刚打开应用时,因为他的关闭方式其实是直接跳转到首页(固定的)

windowStage.loadContent('pages/Index', (err) => {

if (err.code) {

hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));

return;

}

hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');

});

// 3.使用封装window窗口广告模式

// 用这种方式就在 'pages/Start/Start' 中将关闭广告的方法改为 3:adManger.closeAd()

// if(ad.showAd){

// adManger.context = this.context

// adManger.showAd('pages/Start/Start', 320, 240)

// }

// 4.采用与页面解耦的模式实现广告

// 这种模式是单独打开一个全屏模态框,而不是打开 'pages/Start/Start' 页面,关闭按钮也在 'pages/Start/Start' 页面中

if (ad.showAd) {

adManagerFinal.context = this.context

adManagerFinal.showAd(ad) // 展示广告

}

}

onWindowStageDestroy(): void {

// Main window is destroyed, release UI related resources

hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');

}

onForeground(): void {

// Ability has brought to foreground

hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground');

}

onBackground(): void {

// Ability has back to background

hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground');

}

}

import { AdvertClass, userSetting } from 'basic'

import { router, window } from '@kit.ArkUI'

import { adManagerFinal, adManger } from '../../utils'

@Entry

@Component

struct Start {

// 需要广告对象

@State

ad: AdvertClass = new AdvertClass()

timer: number = -1 // 用来记录定时器的标记

// @State

// ad: Partial = {}

aboutToAppear(): void {

console.log('123123123')

// 获取首选项的广告数据给到ad

this.getAdInfo()

}

getAdInfo() {

// 首选项的读取

this.ad = userSetting.getUserAd()!

// 开启倒计时了

if (this.ad.showAd) {

// 如果真的展示广告要开始倒计时

this.timer = setInterval(() => {

if (this.ad.adTime === 0) {

clearInterval(this.timer)

this.toMain()

return // return一定要写

}

this.ad.adTime--

}, 1000)

}

}

closeWin () {

window.findWindow("ad_win").destroyWindow()

}

// 去主页的方法

toMain() {

// 1. 普通模式打开广告,关闭是直接跳转到主页

// router.replaceUrl({

// url: 'pages/Index'

// })

// 2.使用子窗口模式关闭广告

// this.closeWin()

// 3.使用封装window窗口广告模式

// clearInterval(this.timer) // 先清理一下定时器

// adManger.closeAd()

}

aboutToDisappear(): void {

clearInterval(this.timer)

}

build() {

RelativeContainer() {

if (this.ad.showAd) {

Image(this.ad.adImg)

.width("100%")

.height("100%")

.objectFit(ImageFit.Cover)

Text(${this.ad.adTime}秒跳过)

.padding({

left: 10,

right: 10

})

.alignRules({

right: {

anchor: 'container',

align: HorizontalAlign.End

},

top: {

anchor: 'container',

align: VerticalAlign.Top

}

})

.borderRadius(15)

.height(30)

.fontSize(14)

.backgroundColor($r("app.color.background_page"))

.margin({

right: 20,

top: 20

})

.onClick(() => {

// 此时跳过广告

// 跳转到主页

this.toMain()

})

}

}

.height('100%')

.width('100%')

}

}