本文旨在深入探讨华为鸿蒙HarmonyOS Next系统的技术细节,基于实际开发实践进行总结。主要作为技术分享与交流载体,难免错漏,欢迎各位同仁提出宝贵意见和问题,以便共同进步。本文为原创内容,任何形式的转载必须注明出处及原作者。

在HarmonyOS Next的开发世界里,自适应布局是构建灵活、美观界面的得力助手。今天,咱们就深入剖析一下它的原理、能力以及在实际项目中的应用。

自适应布局的基本原理

自适应布局的核心在于建立组件间的相对关系,让元素能够根据容器的变化动态适配。就好比在一个可伸缩的房间里摆放家具,家具之间的距离和大小会随着房间尺寸的改变而自动调整。在HarmonyOS Next中,这种相对关系通过各种布局属性来实现,使得组件能够智能地响应容器尺寸的变化。

元素动态适配的方式多种多样。例如,当容器尺寸改变时,子组件可以按照设定的规则拉伸、收缩、隐藏或重新排列,以确保在不同大小的容器中都能呈现出合理的布局效果。这一过程涉及到对各种布局能力的综合运用,下面我们就来详细了解一下。

自适应布局的7种能力详解

  1. 拉伸能力:拉伸能力让容器组件尺寸变化时,增加或减小的空间全部分配给指定区域。通过Flex布局的flexGrowflexShrink属性来实现,这两个属性常与flexBasis属性搭配。例如,在一个包含图片和留白区域的布局中:

    @Entry
    @Component
    struct StretchSample {
     @State containerWidth: number = 402
     @Builder slider() {
         Slider({ value: this.containerWidth, min: 402, max: 1000, style: SliderStyle.OutSet })
           .blockColor(Color.White)
           .width('60%')
           .onChange((value: number) => {
                 this.containerWidth = value;
             })
           .position({ x: '20%', y: '80%' })
     }
     build() {
         Column() {
             Row() {
                 Row().width(150).height(400).backgroundColor('#FFFFFF').flexGrow(0).flexShrink(1)
                 Image($r("app.media.illustrator")).width(400).height(400)
                   .objectFit(ImageFit.Contain)
                   .backgroundColor("#66F1CCB8")
                   .flexGrow(1).flexShrink(0)
                 Row().width(150).height(400).backgroundColor('#FFFFFF').flexGrow(0).flexShrink(1)
             }
               .width(this.containerWidth)
               .justifyContent(FlexAlign.Center)
               .alignItems(VerticalAlign.Center)
             this.slider()
         }
           .width('100%')
           .height('100%')
           .backgroundColor('#F1F3F5')
           .justifyContent(FlexAlign.Center)
           .alignItems(HorizontalAlign.Center)
     }
    }

    当父容器尺寸大于基准尺寸时,中间图片区域会拉伸;小于基准尺寸时,两侧留白区域收缩。

  2. 均分能力:均分能力用于将容器组件尺寸变化时的空间均匀分配给所有空白区域。通过将RowColumnFlex组件的justifyContent属性设置为FlexAlign.SpaceEvenly实现。比如在底部菜单栏中:

    @Entry
    @Component
    struct EqualDistributionSample {
     readonly list: number[] = [0, 1, 2, 3]
     @State rate: number = 0.6
     @Builder slider() {
         Slider({ value: this.rate * 100, min: 30, max: 60, style: SliderStyle.OutSet })
           .blockColor(Color.White)
           .width('60%')
           .onChange((value: number) => {
                 this.rate = value / 100
             })
           .position({ x: '20%', y: '80%' })
     }
     build() {
         Column() {
             Row() {
                 ForEach(this.list, (item: number) => {
                     Column() {
                         Image($r("app.media.startIcon")).width(48).height(48).margin({ top: 8 })
                         Text('菜单选项')
                           .width(64)
                           .height(30)
                           .lineHeight(15)
                           .fontSize(12)
                           .textAlign(TextAlign.Center)
                           .margin({ top: 8 })
                           .padding({ bottom: 15 })
                     }
                       .width(80)
                       .height(102)
                       .flexShrink(1)
                 })
             }
               .width('100%')
               .justifyContent(FlexAlign.SpaceEvenly)
             this.slider()
         }
           .width('100%')
           .height('100%')
           .backgroundColor('#F1F3F5')
           .justifyContent(FlexAlign.Center)
           .alignItems(HorizontalAlign.Center)
     }
    }

    各菜单选项会均匀分布在父容器中。

  3. 占比能力:子组件的宽或高按预设比例随容器组件变化。可以通过将子组件宽高设为父组件宽高的百分比,或使用layoutWeight属性实现。但要注意,layoutWeight属性仅在父容器为RowColumnFlex时生效,且设置后组件本身尺寸会失效。例如:

    @Entry
    @Component
    struct ProportionSample {
     @State rate: number = 0.5
     @Builder slider() {
         Slider({ value: 100, min: 25, max: 50, style: SliderStyle.OutSet })
           .blockColor(Color.White)
           .width('60%')
           .height(50)
           .onChange((value: number) => {
                 this.rate = value / 100
             })
           .position({ x: '20%', y: '80%' })
     }
     build() {
         Column() {
             Row() {
                 Column() {
                     Image($r("app.media.down")).width(48).height(48)
                 }
                   .height(96)
                   .layoutWeight(1)
                 Column() {
                     Image($r("app.media.pause")).width(48).height(48)
                 }
                   .height(96)
                   .layoutWeight(1)
                 Column() {
                     Image($r("app.media.next")).width(48).height(48)
                 }
                   .height(96)
                   .layoutWeight(1)
             }
               .width(this.rate * 100 + '%')
               .height(96)
               .borderRadius(16)
               .backgroundColor('#FFFFFF')
             this.slider()
         }
           .width('100%')
           .height('100%')
           .backgroundColor('#F1F3F5')
           .justifyContent(FlexAlign.Center)
           .alignItems(HorizontalAlign.Center)
     }
    }

    “上一首”“播放/暂停”“下一首”按钮按1:1:1的比例均分父容器空间。

  4. 缩放能力:缩放能力使子组件宽高按预设比例随容器组件变化,且宽高比不变。通过百分比布局配合aspectRatio属性实现。例如:

    @Entry
    @Component
    struct ScaleSample {
     @State sliderWidth: number = 400
     @State sliderHeight: number = 400
     @Builder slider() {
         Slider({ value: this.sliderHeight, min: 100, max: 400, style: SliderStyle.OutSet })
           .blockColor(Color.White)
           .width('60%')
           .height(50)
           .onChange((value: number) => {
                 this.sliderHeight = value
             })
           .position({ x: '20%', y: '80%' })
         Slider({ value: this.sliderWidth, min: 100, max: 400, style: SliderStyle.OutSet })
           .blockColor(Color.White)
           .width('60%')
           .height(50)
           .onChange((value: number) => {
                 this.sliderWidth = value;
             })
           .position({ x: '20%', y: '87%' })
     }
     build() {
         Column() {
             Column() {
                 Column() {
                     Image($r("app.media.illustrator")).width('100%').height('100%')
                 }
                   .aspectRatio(1)
                   .border({ width: 2, color: "#66F1CCB8"})
             }
               .backgroundColor("#FFFFFF")
               .height(this.sliderHeight)
               .width(this.sliderWidth)
               .justifyContent(FlexAlign.Center)
               .alignItems(HorizontalAlign.Center)
             this.slider()
         }
           .width('100%')
           .height('100%')
           .backgroundColor("#F1F3F5")
           .justifyContent(FlexAlign.Center)
           .alignItems(HorizontalAlign.Center)
     }
    }

    图片所在的Column组件会随父容器缩放,且始终保持宽高比为1。

  5. 延伸能力:容器组件内子组件按顺序随容器尺寸变化显示或隐藏。可以通过List组件或Scroll组件配合Row/Column组件实现。例如:

    @Entry
    @Component
    struct ExtensionSample {
     @State rate: number = 0.60
     readonly appList: number[] = [0, 1, 2, 3, 4, 5, 6, 7]
     @Builder slider() {
         Slider({ value: this.rate * 100, min: 8, max: 60, style: SliderStyle.OutSet })
           .blockColor(Color.White)
           .width('60%')
           .height(50)
           .onChange((value: number) => {
                 this.rate = value / 100
             })
           .position({ x: '20%', y: '80%' })
     }
     build() {
         Column() {
             Row({ space: 10 }) {
                 List({ space: 10 }) {
                     ForEach(this.appList, (item: number) => {
                         ListItem() {
                             Column() {
                                 Image($r("app.media.startIcon")).width(48).height(48).margin({ top: 8 })
                                 Text('应用图标')
                                   .width(64)
                                   .height(30)
                                   .lineHeight(15)
                                   .fontSize(12)
                                   .textAlign(TextAlign.Center)
                                   .margin({ top: 8 })
                                   .padding({ bottom: 15 })
                             }
                               .width(80).height(102)
                         }
                     })
                 }
                   .padding({ top: 16, left: 10 })
                   .listDirection(Axis.Horizontal)
                   .width('100%')
                   .height(118)
                   .borderRadius(16)
                   .backgroundColor(Color.White)
             }
               .width(this.rate * 100 + '%')
             this.slider()
         }
           .width('100%')
           .height('100%')
           .backgroundColor('#F1F3F5')
           .justifyContent(FlexAlign.Center)
           .alignItems(HorizontalAlign.Center)
     }
    }

    随着父容器尺寸变化,展示的图标数量会相应改变。

  6. 隐藏能力:子组件按预设显示优先级随容器尺寸变化显示或隐藏。通过设置displayPriority属性实现,常用于不同分辨率下显示内容有差异的场景。例如:

    @Entry
    @Component
    struct HiddenSample {
     @State rate: number = 0.45
     @Builder slider() {
         Slider({ value: this.rate * 100, min: 10, max: 45, style: SliderStyle.OutSet })
           .blockColor(Color.White)
           .width('60%')
           .height(50)
           .onChange((value: number) => {
                 this.rate = value / 100
             })
           .position({ x: '20%', y: '80%' })
     }
     build() {
         Column() {
             Row({ space:24 }) {
                 Image($r("app.media.favorite")).width(48).height(48).objectFit(ImageFit.Contain).displayPriority(1)
                 Image($r("app.media.down")).width(48).height(48).objectFit(ImageFit.Contain).displayPriority(2)
                 Image($r("app.media.pause")).width(48).height(48).objectFit(ImageFit.Contain).displayPriority(3)
                 Image($r("app.media.next")).width(48).height(48).objectFit(ImageFit.Contain).displayPriority(2)
                 Image($r("app.media.list")).width(48).height(48).objectFit(ImageFit.Contain).displayPriority(1)
             }
               .width(this.rate * 100 + '%')
               .height(96)
               .borderRadius(16)
               .backgroundColor('#FFFFFF')
               .justifyContent(FlexAlign.Center)
               .alignItems(VerticalAlign.Center)
             this.slider()
         }
           .width('100%')
           .height('100%')
           .backgroundColor('#F1F3F5')
           .justifyContent(FlexAlign.Center)
           .alignItems(HorizontalAlign.Center)
     }
    }

    当容器空间不足时,按优先级隐藏元素。

  7. 折行能力:容器组件尺寸变化,布局方向尺寸不足以显示完整内容时自动换行。通过将Flex组件的wrap属性设置为FlexWrap.Wrap实现。例如:

    @Entry
    @Component
    struct WrapSample {
     @State rate: number = 0.7
     readonly imageList: Resource[] = [
         $r('app.media.flexWrap1'),
         $r('app.media.flexWrap2'),
         $r('app.media.flexWrap3'),
         $r('app.media.flexWrap4'),
         $r('app.media.flexWrap5'),
         $r('app.media.flexWrap6')
     ]
     @Builder slider() {
         Slider({ value: this.rate * 100, min: 50, max: 70, style: SliderStyle.OutSet })
           .blockColor(Color.White)
           .width('60%')
           .onChange((value: number) => {
                 this.rate = value / 100
             })
           .position({ x: '20%', y: '87%' })
     }
     build() {
         Flex({ justifyContent: FlexAlign.Center, direction: FlexDirection.Column }) {
             Column() {
                 Flex({
                     direction: FlexDirection.Row,
                     alignItems: ItemAlign.Center,
                     justifyContent: FlexAlign.Center,
                     wrap: FlexWrap.Wrap
                 }) {
                     ForEach(this.imageList, (item:Resource) => {
                         Image(item).width(183).height(138).padding(10)
                     })
                 }
                   .backgroundColor('#FFFFFF')
                   .padding(20)
                   .width(this.rate * 100 + '%')
                   .borderRadius(16)
             }
               .width('100%')
             this.slider()
         }
           .width('100%')
           .height('100%')
           .backgroundColor('#F1F3F5')
     }
    }

    图片在父容器尺寸不足时会自动换行。

自适应布局在实际项目中的应用

假设我们正在开发一个音乐播放应用。在播放界面中,底部的播放控制栏可以使用拉伸和均分能力。“上一首”“播放/暂停”“下一首”按钮通过layoutWeight属性或百分比设置实现均分,而播放进度条则可以利用拉伸能力,随着容器宽度变化自动调整长度。

歌曲列表部分,可以使用延伸能力,根据屏幕空间展示不同数量的歌曲。当屏幕较小时,仅展示热门歌曲;屏幕变大时,逐渐展示更多歌曲。

在适配不同设备时,对于小屏幕设备,采用简单的线性布局,利用拉伸和折行能力,确保元素不拥挤;对于大屏幕设备,可适当增加元素的占比能力,充分利用屏幕空间。


SameX
1 声望2 粉丝