HarmonyOS鸿蒙NEXT中如何<<严格>>按照美工设计图还原页面?

HarmonyOS鸿蒙NEXT中如何<<严格>>按照美工设计图还原页面? 假设美工给了375或者750宽的设计图,图中是px单位标记,有没有什么库,或者什么方法可以快速且简单地完整忠实地还原页面?
我现在的方法是获取状态栏和导航栏高度后,把px计算回vp,但感觉还是有点麻烦,有没有比较好用的库?
回答时可以不需要科普px,vp等基本概念,直接给可行方案即可

最好可以有个简单的设计图+代码demo

或者有好用的库推荐,最好也是有个简单的设计图+代码demo

可以vibe coding大模型回答,但至少请自己运行一遍,简单的搜索和大模型学习我已经做过了,感谢

我的方案+简单demo:

  • 假设设计图750px宽
  • 实现一个高度撑满整个页面,宽度700px的红色区域(相对于750px宽的设计图,横竖屏比例相应变化)

cke_20553.jpeg

cke_38073.jpeg

cke_51891.jpeg

cke_66443.jpeg

cke_80498.jpeg

import { display } from '@kit.ArkUI';
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct ScreenInfoDemo {
  // 显示信息
  @State fullWidth: number = 0;
  @State fullHeight: number = 0;
  @State densityPixels: number = 1.0;

  // AvoidArea(安全区域)
  @State statusBarHeight: number = 0;
  @State bottomNavHeight: number = 0;
  @State indicatorHeight: number = 0;

  // 可用区域
  @State availableWidth: number = 0;
  @State availableHeight: number = 0;

  // 设计稿适配
  @State designWidth: number = 375;
  @State designScale: number = 1.0;
  @State adaptedWidth: number = 0;

  // 窗口尺寸变化监听句柄
  private sizeChangeListener: ((size: window.Size) => void) | null = null;

  aboutToAppear() {
    this.getDisplayInfo();
    this.getAvoidAreaInfo();
    this.registerWindowSizeListener();
  }

  aboutToDisappear() {
    this.unregisterWindowSizeListener();
  }

  private registerWindowSizeListener() {
    const context = this.getUIContext();
    if (!context) return;

    window.getLastWindow(context.getHostContext()).then((win) => {
      // 如果已经注册过,先取消
      this.unregisterWindowSizeListener();

      this.sizeChangeListener = (size: window.Size) => {
        // 尺寸变化时立即刷新所有信息
        this.getDisplayInfo();
        // 延迟一帧确保窗口AvoidArea已更新
        setTimeout(() => {
          this.getAvoidAreaInfo();
        }, 50);
      };

      win.on('windowSizeChange', this.sizeChangeListener);
    }).catch((err: BusinessError) => {
      console.error('注册窗口尺寸监听失败', JSON.stringify(err));
    });
  }

  private unregisterWindowSizeListener() {
    if (this.sizeChangeListener) {
      const context = this.getUIContext();
      if (context) {
        window.getLastWindow(context.getHostContext()).then((win) => {
          win.off('windowSizeChange', this.sizeChangeListener!);
        }).catch(() => {});
      }
      this.sizeChangeListener = null;
    }
  }

  getDisplayInfo() {
    try {
      const displayClass = display.getDefaultDisplaySync();
      this.fullWidth = displayClass.width;
      this.fullHeight = displayClass.height;
      this.densityPixels = displayClass.densityPixels;
    } catch (e) {
      console.error('获取显示信息失败', e);
    }
  }

  getAvoidAreaInfo() {
    const context = this.getUIContext();
    if (!context) return;

    window.getLastWindow(context.getHostContext()).then((win) => {
      const systemAvoid = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
      this.statusBarHeight = systemAvoid.topRect.height;
      this.bottomNavHeight = systemAvoid.bottomRect.height;

      const navAvoid = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);
      this.indicatorHeight = navAvoid.bottomRect.height;

      this.availableWidth = this.fullWidth;
      this.availableHeight = this.fullHeight
        - this.statusBarHeight
        - this.bottomNavHeight
        - this.indicatorHeight;

      this.designScale = this.availableWidth / this.designWidth;
      this.adaptedWidth = this.availableWidth;
    }).catch((err: BusinessError) => {
      console.error('获取AvoidArea失败', JSON.stringify(err));
    });
  }

  /**
   * 设计稿 px 转实际 vp
   * @param designPx 设计稿上的 px 值
   * @param designWidth 设计稿宽度,默认 750
   * @returns 当前屏幕对应的 vp 值
   */
  px2vp(designPx: number, designWidth: number = 750): number {
    if (this.availableWidth <= 0 || designWidth <= 0) {
      return designPx;
    }
    const pxScale = this.availableWidth / designWidth;
    const adaptedPx = designPx * pxScale;
    return adaptedPx / this.densityPixels;
  }

  // px 转 vp(用于可用高度等非设计稿尺寸)
  px2vpRaw(px: number): number {
    if (this.densityPixels <= 0) return px;
    return px / this.densityPixels;
  }

  updateDesignWidth(width: number) {
    this.designWidth = width;
    this.designScale = this.availableWidth / this.designWidth;
  }

  build() {
    Scroll() {
      Column({ space: 24 }) {
        Column(){
          Text('HarmonyOS 屏幕信息')
            .fontSize(28)
            .fontWeight(FontWeight.Bold)
            .fontColor(Color.White)
        }
        .height(28)
        .backgroundColor(Color.Green)

        Text(`屏幕分辨率: ${this.fullWidth} × ${this.fullHeight}`)
          .fontColor(Color.White)
        Text(`屏幕密度: ${this.densityPixels.toFixed(2)}x`)
          .fontColor(Color.White)

        Divider().color(Color.Gray)

        Text('安全区域').fontSize(18).fontWeight(FontWeight.Medium).fontColor(Color.White)
        Text(`状态栏高度: ${this.statusBarHeight} px`).fontColor(Color.White)
        Text(`底部导航栏高度: ${this.bottomNavHeight} px`).fontColor(Color.White)
        Text(`导航指示器高度: ${this.indicatorHeight} px`).fontColor(Color.White)

        Divider().color(Color.Gray)

        Text('可用区域(推荐使用)').fontSize(18).fontWeight(FontWeight.Medium).fontColor(Color.White)
        Text(`可用宽度: ${this.availableWidth} px`).fontColor(Color.White)
        Text(`可用高度: ${this.availableHeight} px`)
          .fontColor(Color.Green)

        Divider().color(Color.Gray)

        Row({ space: 16 }) {
          Text('设计稿宽度:').fontColor(Color.White)
          Radio({ value: '375', group: 'design' })
            .checked(this.designWidth === 375)
            .onChange((checked) => { if (checked) this.updateDesignWidth(375); })
          Text('375').fontColor(Color.White)
          Radio({ value: '750', group: 'design' })
            .checked(this.designWidth === 750)
            .onChange((checked) => { if (checked) this.updateDesignWidth(750); })
          Text('750').fontColor(Color.White)
        }

        Text(`缩放比例: ${this.designScale.toFixed(3)}x`).fontColor(Color.White)
        Text(`设计稿 ${this.designWidth} 对应实际宽度: ${this.adaptedWidth.toFixed(0)} px`)
          .fontColor(Color.Blue)

        Divider().color(Color.Gray)

        Text('示例条设置信息:')
          .fontSize(16)
          .fontColor(Color.Gray)

        Text(`• 750px 条 → 宽度: ${this.px2vp(750, 750).toFixed(0)} vp`)
          .fontColor(Color.White)
        Text(`• 300px 条 → 宽度: ${this.px2vp(300, 750).toFixed(0)} vp`)
          .fontColor(Color.White)

        Divider().color(Color.Gray)

        Text('设计稿 750px 宽度的条(px2vp转换后)')
          .fontSize(16)
          .fontColor(Color.Gray)

        Column() {
          Text('750px 设计稿宽度示例条')
            .fontColor(Color.White)
            .fontSize(18)
        }
        .width(this.px2vp(750, 750))
        .height(80)
        .backgroundColor('#007DFF')
        .borderRadius(16)
        .alignItems(HorizontalAlign.Center)
        .justifyContent(FlexAlign.Center)

        Column() {
          Text('设计稿 300px 宽度的元素')
            .fontColor(Color.White)
            .fontSize(16)
        }
        .width(this.px2vp(300, 750))
        .height(60)
        .backgroundColor('#FF9800')
        .borderRadius(12)
        .alignItems(HorizontalAlign.Center)
        .justifyContent(FlexAlign.Center)

        Divider().color(Color.Gray)

        // ==================== 修改后的红色区域 ====================
        Text(`红色区域 this.px2vp(700, 750) × ${this.availableHeight}px (已转vp)`)
          .fontSize(16)
          .fontColor(Color.Gray)

        Column() {
          Text(`设计稿 ${this.px2vp(700, 750).toFixed(0)}vp\n可用高度 ${this.px2vpRaw(this.availableHeight).toFixed(0)}vp`)
            .fontColor(Color.White)
            .fontSize(18)
            .textAlign(TextAlign.Center)
        }
        .width(this.px2vp(700, 750))
        .height(this.px2vpRaw(this.availableHeight))
        .backgroundColor(Color.Red)
        .borderRadius(16)
        .alignItems(HorizontalAlign.Center)
        .justifyContent(FlexAlign.Center)
      }
      .width('100%')
      .padding({ left: 20, right: 20 })
      .backgroundColor('#333333')
    }
    .width('100%')
    .scrollBar(BarState.Off)
    .edgeEffect(EdgeEffect.Fade)
  }
}

更多关于HarmonyOS鸿蒙NEXT中如何<<严格>>按照美工设计图还原页面?的实战教程也可以访问 https://www.itying.com/category-93-b0.html

10 回复

这类场景建议优先用 designWidth + lpx,比自己拿屏幕宽度、状态栏高度再手算 vp 稳定。比如设计稿按 750px 出图,可以在模块配置里设置 designWidth: 750,页面元素直接写设计稿尺寸:红色区域宽 700px 就写 .width(‘700lpx’),系统会按当前窗口宽度等比换算。状态栏、导航栏、安全区不要混进设计稿比例里手动扣,页面主体用 expandSafeArea 或安全区布局单独处理。简单示例:Stack(){ Column().width(‘700lpx’).height(‘100%’).backgroundColor(Color.Red) }.width(‘100%’).height(‘100%’)。如果要横竖屏都跟设计稿比例走,核心就是统一设计基准宽度,业务组件全部使用 lpx,少量真实设备相关尺寸再用 vp/安全区 API 单独处理。

更多关于HarmonyOS鸿蒙NEXT中如何<<严格>>按照美工设计图还原页面?的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


// main_pages.json
{
  "src": [
    "pages/Index"
  ],
  "window": {
    "designWidth": 750,
    "autoDesignWidth": false
  }
}

学到了,感谢

学习一下大佬

必须支持一下

学习了。

你的方法很好,学习了

又见大神。,

在HarmonyOS NEXT中,严格还原设计图需使用ArkTS声明式UI。将设计稿宽度设为基准(如750vp),在Entry组件或页面级设置.designWidth(750),所有尺寸均使用vp单位。布局采用RowColumnFlexGrid实现精确对齐,图片使用Image组件并指定objectFit(ImageFit.Cover)或精确宽高。文本通过Text.fontSize.fontWeight.letterSpacing按设计值设置。使用@State绑定属性,配合Previewer实时对比,禁止自动缩放。

目前没有现成的官方库,最简单的方法是自己封装一个全局适配工具类。核心思路:在应用启动时获取屏幕密度和可用宽度,基于设计稿宽度(750)算出缩放比,后续所有布局尺寸调用一个统一的转换函数即可。

示例工具类 (DesignHelper.ets)

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

export class DesignHelper {
  private static DESIGN_WIDTH: number = 750;
  private static widthScale: number = 1;
  private static density: number = 1;

  static init(context: Context, designWidth: number = 750) {
    this.DESIGN_WIDTH = designWidth;
    const disp = display.getDefaultDisplaySync();
    this.density = disp.densityPixels;

    window.getLastWindow(context).then(win => {
      const avoid = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
      // 可用宽度(全屏宽,一般不影响横向适配)
      const availWidth = disp.width;
      this.widthScale = availWidth / this.DESIGN_WIDTH;
    });
  }

  /** 设计稿 px → vp */
  static px2vp(designPx: number): number {
    return (designPx * this.widthScale) / this.density;
  }
}

在 EntryAbility 中初始化

export default class EntryAbility extends UIAbility {
  onCreate(want, launchParam) {
    DesignHelper.init(this.context, 750);
  }
}

页面中使用

@Entry
@Component
struct MainPage {
  build() {
    Column()
      .width(DesignHelper.px2vp(700))   // 设计稿 700px → vp
      .height('100%')
      .backgroundColor(Color.Red)
  }
}

这样所有组件只需调用 DesignHelper.px2vp(设计像素值),无需每次手动获取状态栏/密度,代码最简洁。

回到顶部