在最近学习鸿蒙next的媒体查询的过程中,使用了官方视频提供的代码,该代码在预览器可以实现随着屏幕的变化实时更新尺寸的变化,但是自从华为官方更新了最新版的API之后,HSP的模块便不能再预览器上实现了,相同的代码在实现中,不能确保折叠屏状态下手机更换折叠状态,屏幕尺寸信息实时更新的效果。
文章最后我放置了一个运行的代码,可以直接拷贝:
后期做了一定的调整,在新的博客中:完善:(一次开发,多段部署)的相关问题-优快云博客
我在运行代码的时候,主要遇到了一下的问题:
1.屏幕的尺寸状态在build中可以正常显示,但是我在aboutToAppear()声明周期中,判断尺寸大小始终是取不到当前的尺寸,一直都是默认的尺寸
2. 我在折叠屏的状态下,屏幕折叠放开并不会触发媒体查询的监听,展开后在折叠,屏幕显示的尺寸还是展开的尺寸,并没有变化
解决方案
1. 在aboutToAppear()生命周期中,注册完监听器,在判断屏幕状态的时候,要等一秒以后再判断
setTimeout(() => { switch (this.compStr.value) { case "sm": this.flag = 1; break; case "md": this.flag = 1.2; break; case "lg": this.flag = 1.3 break; case "xl": this.flag = 1.5 break; }
2. 我们写的媒体查询只不过是在刚打开设备的时候,判断这个设备的尺寸,并且,如果是折叠屏的展开状态,折叠起来,也不会更新,这时候需要结合使用折叠设备的状态判断方法来实现
import { Callback } from '@kit.BasicServicesKit'; /** * 注册监听的callback参数要采用对象传递. * 若使用匿名函数注册,每次调用会创建一个新的底层对象,引起内存泄漏问题。 */ let callback: Callback<display.FoldStatus> = (data: display.FoldStatus) => { console.info('Listening enabled. Data: ' + JSON.stringify(data)); }; display.on('foldStatusChange', callback);
原理分析:
根据查阅资料得知:
生命周期的执行顺序:
鸿蒙(HarmonyOS)ArkTS 自定义组件的生命周期中,
aboutToAppear()
先执行,之后才会执行build()
。鸿蒙 ArkTS 组件的生命周期顺序
aboutToAppear()
- 在组件即将渲染到界面时执行。
- 适合进行一些初始化操作,例如数据获取、订阅监听器等。
build()
- 负责渲染 UI,组件的视图会在此阶段生成。
- 在
build()
中可以使用@State
和@Prop
来控制 UI 的动态更新。
aboutToDisappear()
- 在组件即将被销毁前执行,适合做清理工作,比如取消订阅、释放资源等。
为什么添加延时器
如果这里说的是严格的顺序执行,那么我们就不用加入延时器了,我们添加延时器主要是
因为 媒体查询(
BreakpointSystem
)的监听是异步的,并不会 立即 更新屏幕尺寸的变量
如果你直接在
aboutToAppear()
里 马上打印屏幕尺寸的变量
,可能会发现:输出的还是你定义的默认值这是因为
BreakpointSystem.start()
会去异步匹配mediaquery
,但 还没完成匹配时,你就打印了屏幕尺寸的变量
这时候
屏幕尺寸的变量
可能还没有更新,还是默认值
为什么 build()
会在 aboutToAppear()
还未执行完时运行?
问题阐述
1.
aboutToAppear()
是同步的,但build()
可能是异步触发的
aboutToAppear()
的执行并不会阻塞build()
build()
可能会在aboutToAppear()
开始运行的过程中被触发,尤其是当@State
变量还没有被赋值时。2. ArkUI 可能会“预构建”组件
在 ArkUI 渲染系统 中:
build()
可能会被提前触发,以提高性能和 UI 响应速度。aboutToAppear()
不会阻塞build()
,所以在aboutToAppear()
还没执行完之前,build()
可能已经执行了。3.
build()
可能被多次执行
- 首次
build()
可能基于默认@State
数据运行aboutToAppear()
运行后,如果@State
变量改变了,build()
会重新执行- 这意味着:
- 可能先执行一次
build()
(此时屏幕的尺寸
还是undefined
)aboutToAppear()
运行并更新屏幕的尺寸
- 由于
@State
发生变化,build()
再次触发,最终渲染正确的值
解决方法(确保 aboutToAppear()
赋值后再 build())
方法 1:使用
@State
,让build()
在状态更新后重新触发你的
屏幕尺寸
不是@State
,所以 ArkUI 可能不会自动重新渲染。可以将他改成@state修饰的为什么这样做有效?
breakpointValue
是@State
,它的更新会 自动触发build()
重新执行setTimeout()
让BreakpointSystem
有时间完成监听器的回调方法 2:在
aboutToAppear()
完成后手动触发build()
你可以用
@State
来控制 是否执行build()
:@State isReady: boolean = false; aboutToAppear(): void { BreakpointSystem.getInstance().attach(this.compStr); BreakpointSystem.getInstance().attach(this.compImg); BreakpointSystem.getInstance().start(); setTimeout(() => { this.isReady = true; // 只有 isReady = true 时,build() 才真正执行 }, 100); } build() { if (!this.isReady) { return Column() { Text("加载中...") }; } return Column() { Text(this.compStr.value).fontSize(24) }; }
这样做的好处
- 避免
build()
过早执行,直到aboutToAppear()
完成后才真正渲染 UI- UI 先显示
"加载中..."
,避免undefined
出现在界面上
问题:你的变量一直都是@state
如果你发现你的屏幕尺寸一直都是@state的类型,依然有这个的问题,是因为@state修饰的变量必须本身变化,才会引起build的更新
解决方法
方法 1:使用
@State
存储屏幕尺寸的变量
,确保build()
重新执行你可以 新建一个
@State
变量 来存储屏幕尺寸的变量
,这样它的变化就能触发build()
:因为:
breakpointValue
是@State
,它的变化会 自动触发build()
重新执行setTimeout()
让BreakpointSystem
先完成监听,确保屏幕尺寸的变量
已更新- 确保
build()
只在breakpointValue
有值时执行
方法 2:直接改变
屏幕尺寸的变量
变量(推荐)如果你不想增加新的
@State
变量,可以直接 让屏幕尺寸的变量
本身变化,这样 ArkUI 就会重新执行build()
:因为:
this.compStr = ...
直接改变@State
变量,让 ArkUI 监听到变化build()
重新执行,UI 更新
可运行代码
下面我这里直接用了华为文档中的代码,加上上面出现的问题的直接方法;形成了一个可以实现媒体查询的功能的代码!无论你在媒体查询中遇到了什么问题,都可以用这个代码:
首先是我的项目列表,用到的文件已经标红:
BreakPointSystem.ets
// common/breakpointsystem.ets
import { mediaquery } from '@kit.ArkUI'
export type BreakpointType = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl'
export interface Breakpoint {
name: BreakpointType
size: number
mediaQueryListener?: mediaquery.MediaQueryListener
}
export class BreakpointSystem {
private static instance: BreakpointSystem
private readonly breakpoints: Breakpoint[] = [
{ name: 'xs', size: 0 },
{ name: 'sm', size: 320 },
{ name: 'md', size: 600 },
{ name: 'lg', size: 840 }
]
private states: Set<BreakpointState<Object>>
private constructor() {
this.states = new Set()
}
public static getInstance(): BreakpointSystem {
if (!BreakpointSystem.instance) {
BreakpointSystem.instance = new BreakpointSystem();
}
return BreakpointSystem.instance
}
public attach(state: BreakpointState<Object>): void {
this.states.add(state)
}
public detach(state: BreakpointState<Object>): void {
this.states.delete(state)
}
public start() {
this.breakpoints.forEach((breakpoint: Breakpoint, index) => {
let condition: string
if (index === this.breakpoints.length - 1) {
condition = `(${breakpoint.size}vp<=width)`
} else {
condition = `(${breakpoint.size}vp<=width<${this.breakpoints[index + 1].size}vp)`
}
breakpoint.mediaQueryListener = mediaquery.matchMediaSync(condition)
if (breakpoint.mediaQueryListener.matches) {
this.updateAllState(breakpoint.name)
}
breakpoint.mediaQueryListener.on('change', (mediaQueryResult) => {
if (mediaQueryResult.matches) {
this.updateAllState(breakpoint.name)
}
})
})
}
private updateAllState(type: BreakpointType): void {
this.states.forEach(state => state.update(type))
}
public stop() {
this.breakpoints.forEach((breakpoint: Breakpoint, index) => {
if (breakpoint.mediaQueryListener) {
breakpoint.mediaQueryListener.off('change')
}
})
this.states.clear()
}
}
export interface BreakpointOptions<T> {
xs?: T
sm?: T
md?: T
lg?: T
xl?: T
xxl?: T
}
export class BreakpointState<T extends Object> {
public value: T | undefined = undefined;
private options: BreakpointOptions<T>
constructor(options: BreakpointOptions<T>) {
this.options = options
}
static of<T extends Object>(options: BreakpointOptions<T>): BreakpointState<T> {
return new BreakpointState(options)
}
public update(type: BreakpointType): void {
if (type === 'xs') {
this.value = this.options.xs
} else if (type === 'sm') {
this.value = this.options.sm
} else if (type === 'md') {
this.value = this.options.md
} else if (type === 'lg') {
this.value = this.options.lg
} else if (type === 'xl') {
this.value = this.options.xl
} else if (type === 'xxl') {
this.value = this.options.xxl
} else {
this.value = undefined
}
}
}
index.ets
export { add } from './src/main/ets/utils/Calc'
export {EmptyView} from './src/main/ets/components/EmptyView'
export {BreakpointSystem,BreakpointState} from './src/main/ets/utils/BreakPointSystem'
export {BreakPointConstants} from './src/main/ets/constants/BreakPointConstants'
Page02_GuidePage.ets
//引导页
import { BasicConstants } from '../Constants/BasicConstants'
//响应式适配
import { BreakpointSystem, BreakpointState } from 'common/src/main/ets/utils/BreakPointSystem'
import { AnimatedDrawableDescriptor, router } from '@kit.ArkUI';
import { loginComponentManager, LoginPanel } from '@hms.core.account.LoginComponent';
//折叠屏的检测
import { display } from '@kit.ArkUI';
import { smartMobilityCommon } from '@kit.CarKit';
@Entry
@Component
struct Page02_GuidePage {
@State message: string = 'Hello World1';
@State time: number = 3;
private timer: number = 0;
@State flag: number = 1
@State mdflage:number|null = null
/**
* 与文档相同
*/
@State compStr: BreakpointState<string> = BreakpointState.of({ sm: "sm", md: "md", lg: "lg" })
@State compImg: BreakpointState<Resource> = BreakpointState.of({
sm: $r('app.media.startIcon'),
md: $r('app.media.layered_image'),
lg: $r('app.media.background')
});
/**
* end
*/
//三秒跳到主页
aboutToAppear(): void {
// this.breakpointsystem.register()
// switch (this.currentBreakpoint){
// case "md":
// this.flag*=3;
// console.log("md")
// break;
// }
BreakpointSystem.getInstance().attach(this.compStr)
BreakpointSystem.getInstance().attach(this.compImg)
BreakpointSystem.getInstance().start()
//折叠屏的检测
let callback: Callback<display.FoldStatus> = (data: display.FoldStatus) => {
console.log('Listening enabled. Data: ' + JSON.stringify(data));
if(data == 1){
this.compStr.value = "md"
this.flag = 1.2;
console.log(this.compStr.value)
}else {
this.compStr.value = "sm"
this.flag = 1;
console.log(this.compStr.value)
}
};
display.on('foldStatusChange', callback);
setTimeout(() => {
switch (this.compStr.value) {
case "sm":
this.flag = 1;
break;
case "md":
this.flag = 1.2;
break;
case "lg":
this.flag = 1.3
break;
case "xl":
this.flag = 1.5
break;
}
// 这里的值应该已经更新为 'md'
}, 100) // 给些时间让媒体查询更新值
this.timer = setInterval(() => {
this.time--
if (this.time == 0) { //三秒之后跳到主页中去
// router.replaceUrl({
// url:"pages/Index"
// })
}
}, 1000) //这个和上面的哪个3000-含义不懂,这里的这个1000表示,每隔一秒钟,执行一次this.time--,上面的3000指的是倒计时3秒
}
aboutToDisappear(): void {
// this.breakpointsystem.unregister()
/**
* 与文档相同 start
*/
BreakpointSystem.getInstance().detach(this.compStr)
BreakpointSystem.getInstance().detach(this.compImg)
BreakpointSystem.getInstance().stop()
/**
* end
*/
}
build() {
Stack({ alignContent: Alignment.TopEnd }) {
Text(`倒计时:${this.time}`)
.fontColor(Color.Black)
.fontSize(14)
.margin({
top: 20,
right: 30
})
.backgroundColor("#ffdddddd")
.padding({
top: 5,
bottom: 5,
right: 10,
left: 10
})
.borderRadius(20)
Flex({
direction: FlexDirection.Column, //direction:FlexDirection.RowReverse排序的主轴方向是水平反方向
//wrap: FlexWrap.Wrap,//wrap:FlexWrap.Wrap允许换行显示,默认是不允许换行显示的
//justifyContent:FlexAlign.SpaceBetween,//控制主轴方向的排序方式
alignItems: ItemAlign.Center//交叉轴的排序方式。主轴是横轴,交叉轴就是竖轴
}) {
//存放logo
Column() {
Image($r("app.media.woniulogo"))
.width(100 * this.flag)
.objectFit(ImageFit.Contain)
.borderRadius(20)
.backgroundColor("#ffe2e2d8")
}
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.flexGrow(1)
Image($r("app.media.woniulogo2"))
.height(27 * this.flag)
.width(114 * this.flag)
Text("WoNiuMall超市")
.fontColor('#66000000')
.fontSize(20 * this.flag)
.letterSpacing(10)//控制字母之间的间距
.margin({
top: 12,
bottom: 136
})
Text(this.compStr.value)
.fontSize(24)
.margin(10)
// Text(this.currentBreakpoint)
}
.height(BasicConstants.FULL_HEIGHT)
.width(BasicConstants.FULL_WIDTH)
//.backgroundColor(Color.Gray)
}
}
}
oh-package.json5
{
"modelVersion": "5.0.1",
"description": "Please describe the basic information.",
"dependencies": {
},
"devDependencies": {
"@ohos/hypium": "1.0.19",
"@ohos/hamock": "1.0.0",
"common": "file:./common",
"homelibrary": "file:./features/homelibrary"
}
}
主要是这一句,第一次添加上依赖以后,把鼠标放在这个绿色的字上面,有个run,需要运行一下,以后每一次更改common内的内容,也要在软件中run一下这个模块: