解决:HarmonyOS NEXT媒体查询不能随折叠屏状态实时更新的问题(一次开发,多端部署)

在最近学习鸿蒙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 组件的生命周期顺序

  1. aboutToAppear()

    • 在组件即将渲染到界面时执行。
    • 适合进行一些初始化操作,例如数据获取、订阅监听器等。
  2. build()

    • 负责渲染 UI,组件的视图会在此阶段生成。
    • build() 中可以使用 @State@Prop 来控制 UI 的动态更新。
  3. 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() 会重新执行
  • 这意味着:
    1. 可能先执行一次 build()(此时 屏幕的尺寸 还是 undefined
    2. aboutToAppear() 运行并更新 屏幕的尺寸
    3. 由于 @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一下这个模块:

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Cx330_i

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值