应用的一次开发,多端部署实例解析 原创

在敲键盘的小鱼干很饥饿
发布于 2025-2-11 00:04
浏览
1收藏

1. 效果演示

自适应布局
应用的一次开发,多端部署实例解析-鸿蒙开发者社区
响应式布局
应用的一次开发,多端部署实例解析-鸿蒙开发者社区
应用的一次开发,多端部署实例解析-鸿蒙开发者社区

2. 前言

本篇文章结合本人项目代码,讲解一次开发,多端部署应用。目前通过自适应布局响应式布局,已经完成效果很好的多端部署。通过一套代码,最终编译出来的同一个应用就可以按不同的显示效果分别运行在手机、平板、折叠屏等设备上,这对开发者来说真是既高效又便捷。

3. 自适应布局

自适应布局是指应用能够根据设备的屏幕尺寸和分辨率自动调整界面元素的大小和位置,确保界面在不同设备上都能保持良好的显示效果。自适应布局通常通过使用相对单位(如百分比、vp等)和灵活的布局结构来实现。以下是一些常见的自适应布局技术。

3.1 自适应拉伸

自适应拉伸是通过设置组件与父级容器的相对比例来实现的。比如,在设计稿上,竖屏手机的宽度是“360vp”,折叠屏的宽度是“600vp”。那么,在布局组件的宽度设置上,不要使用固定值“360vp”或“600vp”,而是用“100%”这种相对值。

Column() {
  // 头部...
  Row() {
	//其他
  }
  .width('90%')  // 自适应拉伸
  .justifyContent(FlexAlign.SpaceBetween)
}
.backgroundColor($r("app.color.background_color"))
.width('100%')
.height('100%')
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

在这个代码片段中,Row组件的宽度设置为90%,这意味着它会根据屏幕宽度自动拉伸,因为根容器的宽度也是100%,所以可以填充可用空间。这种自适应拉伸确保了界面在不同设备上都能保持良好的显示效果。

在自适应拉伸布局下设置子组件大小和位置还有两个小技巧
第一个技巧:子组件不刻意设置宽度或设置绝对宽度值,子组件间使用“Blank”组件填充

Row() {
      ForEach(this.title, (item: string, index: number) => {
        Column(){
          //其他
        }
        .justifyContent(FlexAlign.End)
        .height("100%")
      })
      
      Blank()
      
      Image(this.isFullScreen ? $r('app.media.bond_suoxiao'):$r('app.media.bond_fangda'))
        .fillColor($r('app.color.menu_font_color'))
        .width(16)
        .height(16)
        .margin({right:10,top:5})
        .onClick(() => {
          // 使用getLastWindow获取当前窗口
          window.getLastWindow(this.context).then((lastWindow)=>{
            // 使用setPreferredOrientation实现横竖屏切换
            lastWindow.setPreferredOrientation(this.isFullScreen ? window.Orientation.PORTRAIT : window.Orientation.LANDSCAPE)
            this.isShowModal = false
            if(this.isFullScreen){
              if(this.selectTitle === 0) {
                setTimeout(()=>{
                  this.isShowModal = true;
                }, 1000)
              }
            }
            this.isFullScreen = !this.isFullScreen
          })
        })
    }
    .padding({left: "2%"})
    .backgroundColor($r('app.color.background_color_lv2'))
    .borderRadius(5)
    .width("100%")
    .height("56%"l)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.

在上面的示例里面是我的应用的一个头部导航栏示例,左侧放了‘规划’‘地图’等按钮,然后使用blank()组件填充剩余部分,在最后放全屏的图片。以达到多端适配的效果。
在Flex布局中的“justifyContent”也可以实现上述效果。这个后面再讲
第二个技巧:子组件不设置宽度,但通过“layoutWeight”设置该组件在父级容器中的宽度权重比例。在前面代码的上面加上

.layoutWeight(1)
  • 1.

也可以实现相同的效果,同时这个也可以设置按比例分配。注意:"layoutWeight"仅适用于“Flex/Row/Column”布局组件下的子组件。

3.2 自适应缩放

自适应缩放是指根据设备的屏幕分辨率和实际大小,对页面内容进行整体缩放。可以通过设定一个基础的设计尺寸,然后根据设备的分辨率比例来调整所有元素的大小。

  Image(this.isFullScreen ? $r('app.media.bond_suoxiao'):$r('app.media.bond_fangda'))
  .fillColor($r('app.color.menu_font_color'))
  .width(16)
  .height(16)
  .aspectRatio(1.5)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

对于图片的展示,可以锁定宽高比例,同时将宽设置为百分比的值,实现自适应缩放

3.3 自适应延伸

自适应延伸的要点在于不设置父级容器宽度,由子组件将父容器撑开。当不同设备的屏幕宽度发生变化时,组件随之发生自适应延伸显示更多数量。实际项目中,我们会配合“Scroll” 可滚动容器组件,实现被遮挡子组件的显示。
自适应延伸通常可以通过以下两种实现方式:

  1. 通过 List 组件实现:使用 List 组件,可以根据容器的可用高度或宽度,自动计算并展示相应数量的子组件。例如,在一个列表视图中,用户可以根据设备的大小看到更多或更少的列表项。
    List({ scroller: this.scroller }) {
      ForEach(this.locations, (item: routeLocation, index: number) => {
        ListItem() {
          Column() {
            Row() {
              Text(item.name)
                .width('100%')
                .margin({ top: 4, left: 2 })
                .fontSize(16)
                .fontColor($r('app.color.mainMenu_background'))
            }

            Row() {
              Text(item.address)
                .width('100%')
                .margin({ top: 2, left: 2 })
                .fontSize(12)
                .fontWeight(FontWeight.Normal)
                .fontColor($r('app.color.font_color_lv3'))
            }
          }
        }
        .margin({ top: 5 })
        .backgroundColor(this.selectedLocationIndex === index ? $r('app.color.background_color_lv8') : $r('app.color.background_color'))
        .onClick(() => {
          console.log("===" + JSON.stringify(item));
          this.selectedLocationIndex = index;
          this.isTrue = false;
          if(this.confirm){
            this.confirm(item);    // 传入的确认回调函数
          }
        })
      })
    }
    .height('100%')
    .width('85%')
    .layoutWeight(1) // 自适应占满剩余空间
    .listDirection(Axis.Vertical) // 排列方向
    .scrollBar(BarState.Off)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.

在这个例子中,List 组件会自动处理子组件的渲染,根据显示区域的大小来显示适当数量的列表项。

  1. 通过 Scroll 组件配合 Row 组件或 Column 组件实现
Scroll(this.delPlaceScroller[this.selectDayBtn][index]) {
  Row() {
    // 使用 Row 组件包装 PlaceItemComponent,动态生成子组件
    this.dayPlayPlaces[this.selectDayBtn].forEach((playPlace, idx) => {
      PlaceItemComponent({
        playPlace: playPlace,
        predictTime: this.timeCnt[idx],
        isShowTime: this.isShowTime,          // 设置驻留时间
        isShowEdieNote: this.isSHowEdieNote,  // 添加笔记
        isMainModel: this.isShowMine,
        idx: idx,
      })
      .height('100%')
      .borderRadius(5)
      // 这里添加更多的逻辑,例如拖拽事件
      .parallelGesture(LongPressGesture().onAction(() => {
        // TODO: 处理长按事件
      }))
      .onDragStart((event: DragEvent) => {
        this.dragIndex = idx;  // 记录开始拖拽的索引
      })
      .onDragEnter((event: DragEvent) => {
        this.dragEnterIndex = idx;
      })
      .onDrop(async (event: DragEvent) => {
        // 处理拖放事件
        if (this.dragIndex !== -1 && this.dragIndex !== idx) {
          // 交换逻辑
          await swapPlaces(this.dragIndex, idx);
        }
        this.dragIndex = -1; // 重置拖拽索引
        this.dragEnterIndex = -1; // 重置目标索引
      });
    });
  }
  .height(100) // 设置 Row 的高度
  .alignItems(VerticalAlign.Center)
  .justifyContent(FlexAlign.Start) // 以适应容器宽度动态添加项目
  .width('100%');
}
.scrollBar(BarState.Off) // 禁用滚动条
.scrollable(ScrollDirection.Horizontal); // 设置为可横向滚动
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.

Scroll 组件包裹所有的内容,允许在子组件超过视图边界时进行滚动。这样,用户可以通过滑动查看所有的 PlaceItemComponent。

3.4 自适应拆行

自适应拆行能力是指当容器组件的尺寸发生变化时,若当前布局的方向尺寸不足以显示完整内容,组件会自动换行。这种能力在面对不同的屏幕方向(如横屏和竖屏)以及设备切换(如从手机切换到平板)时尤为重要

Scroll(this.scroller) {
  Flex({ wrap: FlexWrap.Wrap, direction: FlexDirection.Row }) {
    ForEach(this.recommendedLocations, (item: routeLocation, index: number) => {
     	//其他
      }
    })
  }
  .width('85%')
}
.scrollable(ScrollDirection.Vertical)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

我们通过设置 Flex 组件的 wrap 属性为 FlexWrap.Wrap,允许组件根据可用的空间自动换行。当横向空间不足以完全展示所有内容时,元素将转向下一行进行展示。将整个 Flex 内容包裹在 Scroll 组件内,使得在内容溢出时,用户可以通过滚动查看所有条目。

3.5 更多

在Flex布局中还要很多关于自适应布局的属性,可以看我的这篇文章详细讲了Scoll和Flex布局Flex弹性布局及如何在Scroll组件中使用Flex

4. 响应式布局

自适应布局可以保证窗口尺寸在一定范围内变化时,页面的显示是正常duandian的。但是将窗口尺寸变化较大时(如窗口宽度从400vp变化为1000vp),仅仅依靠自适应布局可能出现图片异常放大或页面内容稀疏、留白过多等问题,此时就需要借助响应式布局能力调整页面结构。

响应式布局是指页面内的元素可以根据特定的特征(如窗口宽度、屏幕方向等)自动变化以适应外部容器变化的布局能力。响应式布局中最常使用的特征是窗口宽度,可以将窗口宽度划分为不同的范围(下文中称为断点)。当窗口宽度从一个断点变化到另一个断点时,改变页面布局(如将页面内容从单列排布调整为双列排布甚至三列排布等)以获得更好的显示效果。

4.1 断点

断点以应用窗口宽度为切入点,将应用窗口在宽度维度上分成了几个不同的区间即不同的断点,在不同的区间下,开发者可根据需要实现不同的页面布局效果。具体的断点如下所示。
应用的一次开发,多端部署实例解析-鸿蒙开发者社区
可以根据实际需要在lg断点后面新增xl、xxl等断点,但注意新增断点会同时增加UX设计师及应用开发者的工作量,除非必要否则不建议盲目新增断点。
监听断点变化的方法有很多,下面主要讲解通过媒体查询监听应用窗口尺寸变化

4.2 媒体查询

在实际应用开发过程中,开发者常常需要针对不同类型设备或同一类型设备的不同状态来修改应用的样式。媒体查询提供了丰富的媒体特征监听能力,可以监听应用显示区域变化、横竖屏、深浅色、设备类型等等,因此在应用开发过程中使用的非常广泛。
这里仅介绍媒体查询跟断点的结合,即如何借助媒体查询能力,监听断点的变化

// 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
    }
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.

在项目目录新建common目录,新建breakpointsystem.ets,下面让我们来解析上面的代码
1. 断点类型和接口定义

export type BreakpointType = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl';

export interface Breakpoint {
  name: BreakpointType
  size: number
  mediaQueryListener?: mediaquery.MediaQueryListener
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • BreakpointType 定义了支持的断点类型,分别针对超小屏到特大屏。
  • Breakpoint 接口包含断点的名称、对应的屏幕尺寸以及一个可选的媒体查询监听器。
    2. BreakpointSystem 类
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>>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 单例模式:BreakpointSystem 使用单例模式,确保整个应用中只有一个断点管理实例。
  • breakpoints 数组定义了各个断点的名称和屏幕尺寸,以便后续使用。
    3. 断点管理方法
  • attach(state: BreakpointState<Object>): 将一个状态对象添加到监控列表中。
  • detach(state: BreakpointState<Object>): 从监控列表中移除一个状态对象。
  • start(): 启动断点监控。对每个断点创建媒体查询并添加改变事件监听器:
    条件字符串构建,例如:(320vp<=width<600vp)
    根据匹配的结果调用 updateAllState(),更新需要响应的状态。
  • private updateAllState(type: BreakpointType): 遍历所有注册状态并调用它们的 update 方法,传入当前匹配的断点类型。
  • stop(): 停止断点监控和清理状态。
    4. 断点状态管理
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>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • BreakpointOptions 接口定义了不同断点下可能的值。
  • BreakpointState 类持有这些选项并在 update() 方法中根据断点类型更新其 value 属性。

4.3 使用实例解析

1. 状态定义

  @State titleHeight: BreakpointState<string> = BreakpointState.of({ sm: "8.5%", lg: "12%"});
  @State mapProportion: BreakpointState<string> = BreakpointState.of({ sm: "91.5%", lg: "100%" });
  @State widthProportion: BreakpointState<string> = BreakpointState.of({ sm: "100%", lg: "50%"});
  • 1.
  • 2.
  • 3.

这里,titleHeight 表示标题在不同尺寸下的高度,mapProportion 表示地图的宽度占比。它们分别在小屏幕 (sm)和大屏幕 (lg) 上的显示比例不同。还有宽度在不同设备下的比例。
2. 连接断点系统
在组件生命周期的步骤中,我们需要连接到 BreakpointSystem,确保我们的状态可以通过系统进行监听:

aboutToAppear() {
    BreakpointSystem.getInstance().attach(this.titleHeight);
    BreakpointSystem.getInstance().attach(this.mapProportion);
    BreakpointSystem.getInstance().attach(this.widthProportion);
    BreakpointSystem.getInstance().start();
}
//attach:将组件中的需要响应的状态附加到单例实例中注册。
//start:启动断点监控,使得系统开始监听屏幕尺寸变化。这一过程将监测视窗的宽度,并在达到指定的断点后更新对应状态。
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

3. 解绑和停止监控
在组件即将消失时,我们需要解绑状态并停止监控:

aboutToDisappear() {
    BreakpointSystem.getInstance().detach(this.titleHeight);
    BreakpointSystem.getInstance().detach(this.mapProportion);
    BreakpointSystem.getInstance().detach(this.widthProportion);
    BreakpointSystem.getInstance().stop();
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

这一步是为了确保状态管理的整洁,避免内存泄漏和不必要的资源占用。
4. 在 UI 中应用状态

Row() {
    // 标题
    Text("我的标题")
        .height(this.titleHeight.value)  // 使用响应式高度
        .fontSize(24);
}

Row() {
    // 地图
    Web({ src: $rawfile('Map.html'), controller: this.controller })
        .width(this.mapProportion.value)  // 使用响应式宽度
        .height("100%");
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

动态高度/宽度设置:this.titleHeight.value 和 this.mapProportion.value 会根据当前断点的不同自动获取适当的值,从而确保在不同屏幕上有良好的布局效果。
我们也可以切换横竖屏来切换响应式布局效果

.onClick(() => {
  // 使用getLastWindow获取当前窗口
  window.getLastWindow(this.context).then((lastWindow)=>{
    // 使用setPreferredOrientation实现横竖屏切换
    lastWindow.setPreferredOrientation(this.isFullScreen ? window.Orientation.PORTRAIT : window.Orientation.LANDSCAPE)
    this.isShowModal = false
    if(this.isFullScreen){
      if(this.selectTitle === 0) {
        setTimeout(()=>{
          this.isShowModal = true;
        }, 1000)
      }
    }
    this.isFullScreen = !this.isFullScreen
  })
})
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

调用 lastWindow.setPreferredOrientation() 方法来设置,根据当前状态 this.isFullScreen 的值,我们将其进行反转(取反),以便在每次切换时更新该状态。

5. 总结

通过上述的介绍,我们深入了解了一次开发、多端部署的概念,并详细探讨了自适应布局和响应式布局的实现方式及其重要性。通过灵活运用这些布局技术,我们能够确保应用在不同的设备上表现出色,并为用户提供一致且令人满意的体验。这不仅提升了用户的整体感受,也为开发者在多平台开发中创造了更高的价值。
:本文所有代码演示效果都在放在文章开头

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
已于2025-2-11 00:04:15修改
3
收藏 1
回复
举报
3
1


回复
    相关推荐