鸿蒙OS开发:【一次开发,多端部署】(典型布局场景)

典型布局场景

虽然不同应用的页面千变万化,但对其进行拆分和分析,页面中的很多布局场景是相似的。本小节将介绍如何借助自适应布局、响应式布局以及常见的容器类组件,实现应用中的典型布局场景。

布局场景实现方案 开发前请熟悉鸿蒙开发指导文档:gitee.com/li-shizhen-skin/harmony-os/blob/master/README.md点击或者复制转到。
[页签栏]Tab组件 + 响应式布局
[运营横幅(Banner)]Swiper组件 + 响应式布局
[网格]Grid组件 / List组件 + 响应式布局
[侧边栏]SideBar组件 + 响应式布局
[单/双栏]Navigation组件 + 响应式布局
[三分栏]SideBar组件 + Navigation组件 + 响应式布局
[自定义弹窗]CustomDialogController组件 + 响应式布局
[大图浏览]Image组件
[操作入口]Scroll组件+Row组件横向均分
[顶部]栅格组件
[缩进布局]栅格组件
[挪移布局]栅格组件
[重复布局]栅格组件

说明:  在本文[媒体查询]小节中已经介绍了如何通过媒体查询监听断点变化,后续的示例中不再重复介绍此部分代码。

页签栏

image.png
布局效果

实现方案

不同断点下,页签在页面中的位置及尺寸都有差异,可以结合响应式布局能力,设置不同断点下[Tab组件]的barPosition、vertical、barWidth和barHeight属性实现目标效果。

另外,页签栏中的文字和图片的相对位置不同,同样可以通过设置不同断点下[tabBar]对应的CustomBuilder中的布局方向,实现目标效果。

参考代码

import { BreakpointSystem, BreakPointType } from '../common/breakpointsystem'

interface TabBar  {
  name: string
  icon: Resource
  selectIcon: Resource
}

@Entry
@Component
struct Home {
  @State currentIndex: number = 0
  @State tabs: Array<TabBar> = [{
                                  name: '首页',
                                  icon: $r('app.media.ic_music_home'),
                                  selectIcon: $r('app.media.ic_music_home_selected')
                                }, {
                                  name: '排行榜',
                                  icon: $r('app.media.ic_music_ranking'),
                                  selectIcon: $r('app.media.ic_music_ranking_selected')
                                }, {
                                  name: '我的',
                                  icon: $r('app.media.ic_music_me_nor'),
                                  selectIcon: $r('app.media.ic_music_me_selected')
                                }]

  @Builder TabBarBuilder(index: number, tabBar: TabBar) {
    Flex({
      direction: new BreakPointType({
        sm: FlexDirection.Column,
        md: FlexDirection.Row,
        lg: FlexDirection.Column
      }).getValue(this.currentBreakpoint),
      justifyContent: FlexAlign.Center,
      alignItems: ItemAlign.Center
    }) {
      Image(this.currentIndex === index ? tabBar.selectIcon : tabBar.icon)
        .size({ width: 36, height: 36 })
      Text(tabBar.name)
        .fontColor(this.currentIndex === index ? '#FF1948' : '#999')
        .margin(new BreakPointType<(Length|Padding)>({
          sm: { top: 4 },
          md: { left: 8 },
          lg: { top: 4 } }).getValue(this.currentBreakpoint)!)
        .fontSize(16)
    }
    .width('100%')
    .height('100%')
  }

  @StorageLink('currentBreakpoint') currentBreakpoint: string = 'md'
  private breakpointSystem: BreakpointSystem = new BreakpointSystem()

  aboutToAppear() {
    this.breakpointSystem.register()
  }

  aboutToDisappear() {
    this.breakpointSystem.unregister()
  }

  build() {
    Tabs({
      barPosition: new BreakPointType({
        sm: BarPosition.End,
        md: BarPosition.End,
        lg: BarPosition.Start
      }).getValue(this.currentBreakpoint)
    }) {
      ForEach(this.tabs, (item:TabBar, index) => {
        TabContent() {
          Stack() {
            Text(item.name).fontSize(30)
          }.width('100%').height('100%')
        }.tabBar(this.TabBarBuilder(index!, item))
      })
    }
    .vertical(new BreakPointType({ sm: false, md: false, lg: true }).getValue(this.currentBreakpoint)!)
    .barWidth(new BreakPointType({ sm: '100%', md: '100%', lg: '96vp' }).getValue(this.currentBreakpoint)!)
    .barHeight(new BreakPointType({ sm: '72vp', md: '56vp', lg: '60%' }).getValue(this.currentBreakpoint)!)
    .animationDuration(0)
    .onChange((index: number) => {
      this.currentIndex = index
    })
  }
}

运营横幅(Banner)

image.png
布局效果

实现方案

运营横幅通常使用[Swiper组件]实现。不同断点下,运营横幅中展示的图片数量不同。只需要结合响应式布局,配置不同断点下Swiper组件的displayCount属性,即可实现目标效果。

参考代码

import { BreakpointSystem, BreakPointType } from '../common/breakpointsystem'

@Entry
@Component
export default struct Banner {
  private data: Array<Resource> = [
    $r('app.media.banner1'),
    $r('app.media.banner2'),
    $r('app.media.banner3'),
    $r('app.media.banner4'),
    $r('app.media.banner5'),
    $r('app.media.banner6'),
  ]
  private breakpointSystem: BreakpointSystem = new BreakpointSystem()
  @StorageProp('currentBreakpoint') currentBreakpoint: string = 'md'

  aboutToAppear() {
    this.breakpointSystem.register()
  }

  aboutToDisappear() {
    this.breakpointSystem.unregister()
  }

  build() {
    Swiper() {
      ForEach(this.data, (item:Resource) => {
        Image(item)
          .size({ width: '100%', height: 200 })
          .borderRadius(12)
          .padding(8)
      })
    }
    .indicator(new BreakPointType({ sm: true, md: false, lg: false }).getValue(this.currentBreakpoint)!)
    .displayCount(new BreakPointType({ sm: 1, md: 2, lg: 3 }).getValue(this.currentBreakpoint)!)
  }
}

网格

image.png
布局效果

实现方案

不同断点下,页面中图片的排布不同,此场景可以通过响应式布局能力结合[Grid组件]实现,通过调整不同断点下的Grid组件的columnsTemplate属性即可实现目标效果。

另外,由于本例中各列的宽度相同,也可以通过响应式布局能力结合[List组件]实现,通过调整不同断点下的List组件的lanes属性也可实现目标效果。

参考代码

通过Grid组件实现

import { BreakpointSystem, BreakPointType } from '../common/breakpointsystem'

interface GridItemInfo {
  name: string
  image: Resource
}

@Entry
@Component
struct MultiLaneList {
  private data: GridItemInfo[] = [
    { name: '歌单集合1', image: $r('app.media.1') },
    { name: '歌单集合2', image: $r('app.media.2') },
    { name: '歌单集合3', image: $r('app.media.3') },
    { name: '歌单集合4', image: $r('app.media.4') },
    { name: '歌单集合5', image: $r('app.media.5') },
    { name: '歌单集合6', image: $r('app.media.6') },
    { name: '歌单集合7', image: $r('app.media.7') },
    { name: '歌单集合8', image: $r('app.media.8') },
    { name: '歌单集合9', image: $r('app.media.9') },
    { name: '歌单集合10', image: $r('app.media.10') },
    { name: '歌单集合11', image: $r('app.media.11') },
    { name: '歌单集合12', image: $r('app.media.12') }
  ]
  private breakpointSystem: BreakpointSystem = new BreakpointSystem()
  @StorageProp('currentBreakpoint') currentBreakpoint: string = 'md'

  aboutToAppear() {
    this.breakpointSystem.register()
  }

  aboutToDisappear() {
    this.breakpointSystem.unregister()
  }

  build() {
    Grid() {
      ForEach(this.data, (item: GridItemInfo) => {
        GridItem() {
          Column() {
            Image(item.image)
              .aspectRatio(1.8)
            Text(item.name)
              .margin({ top: 8 })
              .fontSize(20)
          }.padding(4)
        }
      })
    }
    .columnsTemplate(new BreakPointType({
      sm: '1fr 1fr',
      md: '1fr 1fr 1fr 1fr',
      lg: '1fr 1fr 1fr 1fr 1fr 1fr'
    }).getValue(this.currentBreakpoint)!)
  }
}

通过List组件实现

import { BreakpointSystem, BreakPointType } from '../common/breakpointsystem'

interface ListItemInfo {
  name: string
  image: Resource
}

@Entry
@Component
struct MultiLaneList {
  private data: ListItemInfo[] = [
    { name: '歌单集合1', image: $r('app.media.1') },
    { name: '歌单集合2', image: $r('app.media.2') },
    { name: '歌单集合3', image: $r('app.media.3') },
    { name: '歌单集合4', image: $r('app.media.4') },
    { name: '歌单集合5', image: $r('app.media.5') },
    { name: '歌单集合6', image: $r('app.media.6') },
    { name: '歌单集合7', image: $r('app.media.7') },
    { name: '歌单集合8', image: $r('app.media.8') },
    { name: '歌单集合9', image: $r('app.media.9') },
    { name: '歌单集合10', image: $r('app.media.10') },
    { name: '歌单集合11', image: $r('app.media.11') },
    { name: '歌单集合12', image: $r('app.media.12') }
  ]
  private breakpointSystem: BreakpointSystem = new BreakpointSystem()
  @StorageProp('currentBreakpoint') currentBreakpoint: string = 'md'

  aboutToAppear() {
    this.breakpointSystem.register()
  }

  aboutToDisappear() {
    this.breakpointSystem.unregister()
  }

  build() {
    List() {
      ForEach(this.data, (item: ListItemInfo) => {
        ListItem() {
          Column() {
            Image(item.image)
            Text(item.name)
              .margin({ top: 8 })
              .fontSize(20)
          }.padding(4)
        }
      })
    }
    .lanes(new BreakPointType({ sm: 2, md: 4, lg: 6 }).getValue(this.currentBreakpoint)!)
    .width('100%')
  }
}

侧边栏

布局效果

image.png

实现方案

侧边栏通常通过[SideBarContainer组件]实现,结合响应式布局能力,在不同断点下为SideBarConContainer组件的sideBarWidth、showControlButton等属性配置不同的值,即可实现目标效果。

参考代码

import { BreakpointSystem, BreakPointType } from '../common/breakpointsystem'

interface imagesInfo{
  label:string,
  imageSrc:Resource
}
const images:imagesInfo[]=[
  {
    label:'moon',
    imageSrc:$r('app.media.my_image_moon')
  },
  {
    label:'sun',
    imageSrc:$r('app.media.my_image')
  }
]

@Entry
@Component
struct SideBarSample {
  @StorageLink('currentBreakpoint') private currentBreakpoint: string = "md";
  private breakpointSystem: BreakpointSystem = new BreakpointSystem()
  @State selectIndex: number = 0;
  @State showSideBar:boolean=false;

  aboutToAppear() {
    this.breakpointSystem.register() 
  }

  aboutToDisappear() {
    this.breakpointSystem.unregister()
  }

  @Builder itemBuilder(index: number) {
    Text(images[index].label)
      .fontSize(24)
      .fontWeight(FontWeight.Bold)
      .borderRadius(5)
      .margin(20)
      .backgroundColor('#ffffff')
      .textAlign(TextAlign.Center)
      .width(180)
      .height(36)
      .onClick(() => {
        this.selectIndex = index;
        if(this.currentBreakpoint === 'sm'){
          this.showSideBar=false
        }
      })
  }

  build() {
    SideBarContainer(this.currentBreakpoint === 'sm' ? SideBarContainerType.Overlay : SideBarContainerType.Embed) {
      Column() {
        this.itemBuilder(0)
        this.itemBuilder(1)
      }.backgroundColor('#F1F3F5')
      .justifyContent(FlexAlign.Center)

      Column() {
        Image(images[this.selectIndex].imageSrc)
          .objectFit(ImageFit.Contain)
          .height(300)
          .width(300)
      }
      .justifyContent(FlexAlign.Center)
      .width('100%')
      .height('100%')
    }
    .height('100%')
    .sideBarWidth(this.currentBreakpoint === 'sm' ? '100%' : '33.33%')
    .minSideBarWidth(this.currentBreakpoint === 'sm' ? '100%' : '33.33%')
    .maxSideBarWidth(this.currentBreakpoint === 'sm' ? '100%' : '33.33%')
    .showControlButton(this.currentBreakpoint === 'sm')
    .autoHide(false)
    .showSideBar(this.currentBreakpoint !== 'sm'||this.showSideBar)
    .onChange((isBarShow: boolean) => {
      if(this.currentBreakpoint === 'sm'){
          this.showSideBar=isBarShow
        }         
    })
  }
}

单/双栏

布局效果

image.png

实现方案

单/双栏场景可以使用[Navigation组件]实现,Navigation组件可以根据窗口宽度自动切换单/双栏显示,减少开发工作量。

参考代码

@Component
struct Details {
  private imageSrc: Resource=$r('app.media.my_image_moon')
  build() {
    Column() {
      Image(this.imageSrc)
        .objectFit(ImageFit.Contain)
        .height(300)
        .width(300)
    }
    .justifyContent(FlexAlign.Center)
    .width('100%')
    .height('100%')
  }
}

@Component
struct Item {
  private imageSrc?: Resource
  private label?: string

  build() {
    NavRouter() {
      Text(this.label)
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .borderRadius(5)
        .backgroundColor('#FFFFFF')
        .textAlign(TextAlign.Center)
        .width(180)
        .height(36)
      NavDestination() {
        Details({imageSrc: this.imageSrc})
      }.title(this.label)
      .backgroundColor('#FFFFFF')
    }
  }
}

@Entry
@Component
struct NavigationSample {
  build() {
    Navigation() {
      Column({space: 30}) {
        Item({label: 'moon', imageSrc: $r('app.media.my_image_moon')})
        Item({label: 'sun', imageSrc: $r('app.media.my_image')})
      }
      .justifyContent(FlexAlign.Center)
      .height('100%')
      .width('100%')
    }
    .mode(NavigationMode.Auto)
    .backgroundColor('#F1F3F5')
    .height('100%')
    .width('100%')
    .navBarWidth(360)
    .hideToolBar(true)
    .title('Sample')
  }
}

三分栏

布局效果

image.png

场景说明

为充分利用设备的屏幕尺寸优势,应用在大屏设备上常常有二分栏或三分栏的设计,即“A+C”,“B+C”或“A+B+C”的组合,其中A是侧边导航区,B是列表导航区,C是内容区。在用户动态改变窗口宽度时,当窗口宽度大于或等于840vp时页面呈现A+B+C三列,放大缩小优先变化C列;当窗口宽度小于840vp大于等于600vp时呈现A+C列,放大缩小时优先变化C列;当窗口宽度小于600vp大于等于360vp时,仅呈现C列。

实现方案

三分栏场景可以组合使用[SideBarContainer]组件与[Navigation组件]实现,SideBarContainer组件可以通过侧边栏控制按钮控制显示/隐藏,Navigation组件可以根据窗口宽度自动切换该组件内单/双栏显示,结合响应式布局能力,在不同断点下为SideBarConContainer组件的minContentWidth属性配置不同的值,即可实现目标效果。设置minContentWidth属性的值可以通过[断点]监听窗口尺寸变化的同时设置不同的值并储存成一个全局对象。

参考代码

// MainAbility.ts
import window from '@ohos.window'
import display from '@ohos.display'
import Ability from '@ohos.app.ability.Ability'

export default class MainAbility extends Ability {
  private windowObj?: window.Window
  private curBp?: string
  private myWidth?: number
  // ...
  // 根据当前窗口尺寸更新断点
  private updateBreakpoint(windowWidth:number) :void{
    // 将长度的单位由px换算为vp
    let windowWidthVp = windowWidth / (display.getDefaultDisplaySync().densityDPI / 160)
    let newBp: string = ''
    let newWd: number
    if (windowWidthVp < 320) {
      newBp = 'xs'
      newWd = 360
    } else if (windowWidthVp < 600) {
      newBp = 'sm'
      newWd = 360
    } else if (windowWidthVp < 840) {
      newBp = 'md'
      newWd = 600
    } else {
      newBp = 'lg'
      newWd = 600
    }
    if (this.curBp !== newBp) {
      this.curBp = newBp
      this.myWidth = newWd
      // 使用状态变量记录当前断点值
      AppStorage.setOrCreate('currentBreakpoint', this.curBp)
      // 使用状态变量记录当前minContentWidth值
      AppStorage.setOrCreate('myWidth', this.myWidth)
    }
  }

  onWindowStageCreate(windowStage: window.WindowStage) :void{
    windowStage.getMainWindow().then((windowObj) => {
      this.windowObj = windowObj
      // 获取应用启动时的窗口尺寸
      this.updateBreakpoint(windowObj.getWindowProperties().windowRect.width)
      // 注册回调函数,监听窗口尺寸变化
      windowObj.on('windowSizeChange', (windowSize)=>{
        this.updateBreakpoint(windowSize.width)
      })
    });
   // ...
  }
    
  // 窗口销毁时,取消窗口尺寸变化监听
  onWindowStageDestroy() :void {
    if (this.windowObj) {
      this.windowObj.off('windowSizeChange')
    }
  }
  //...
}


// tripleColumn.ets
@Component
struct Details {
  private imageSrc: Resource=$r('app.media.startIcon')
  build() {
    Column() {
      Image(this.imageSrc)
        .objectFit(ImageFit.Contain)
        .height(300)
        .width(300)
    }
    .justifyContent(FlexAlign.Center)
    .width('100%')
    .height('100%')
  }
}

@Component
struct Item {
  private imageSrc?: Resource
  private label?: string

  build() {
    NavRouter() {
      Text(this.label)
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .backgroundColor('#66000000')
        .textAlign(TextAlign.Center)
        .width('100%')
        .height('30%')
      NavDestination() {
        Details({imageSrc: this.imageSrc})
      }.title(this.label)
      .hideTitleBar(false)
      .backgroundColor('#FFFFFF')
    }
    .margin(10)
  }
}

@Entry
@Component
struct TripleColumnSample {
  @State arr: number[] = [1, 2, 3]
  @StorageProp('myWidth') myWidth: number = 360

  @Builder NavigationTitle() {
    Column() {
      Text('Sample')
        .fontColor('#000000')
        .fontSize(24)
        .width('100%')
        .height('100%')
        .align(Alignment.BottomStart)
        .margin({left:'5%'})
    }.alignItems(HorizontalAlign.Start)
  }

  build() {
    SideBarContainer() {
      Column() {
        List() {
          ForEach(this.arr, (item:number, index) => {
            ListItem() {
              Text('A'+item)
                .width('100%').height("20%").fontSize(24)
                .fontWeight(FontWeight.Bold)
                .textAlign(TextAlign.Center).backgroundColor('#66000000')
            }
          })
        }.divider({ strokeWidth: 5, color: '#F1F3F5' })
      }.width('100%')
      .height('100%')
      .justifyContent(FlexAlign.SpaceEvenly)
      .backgroundColor('#F1F3F5')

      Column() {
        Navigation() {
          List(){
            ListItem() {
              Column() {
                Item({ label: 'B1', imageSrc: $r('app.media.startIcon') })
                Item({ label: 'B2', imageSrc: $r('app.media.startIcon') })
              }
            }.width('100%')
          }
        }
        .mode(NavigationMode.Auto)
        .minContentWidth(360)
        .navBarWidth(240)
        .backgroundColor('#FFFFFF')
        .height('100%')
        .width('100%')
        .hideToolBar(true)
        .title(this.NavigationTitle)
      }.width('100%').height('100%')
    }.sideBarWidth(240)
    .minContentWidth(this.myWidth)
  }
}

自定义弹窗

image.png

布局效果

实现方案

自定义弹窗通常通过[CustomDialogController]实现,有两种方式实现本场景的目标效果:

  • 通过gridCount属性配置自定义弹窗的宽度。

    系统默认对不同断点下的窗口进行了栅格化:sm断点下为4栅格,md断点下为8栅格,lg断点下为12栅格。通过gridCount属性可以配置弹窗占据栅格中的多少列,将该值配置为4即可实现目标效果。

  • 将customStyle设置为true,即弹窗的样式完全由开发者自定义。

    开发者自定义弹窗样式时,开发者可以根据需要配置弹窗的宽高和背景色(非弹窗区域保持默认的半透明色)。自定义弹窗样式配合[栅格组件]同样可以实现目标效果。

参考代码

@Entry
@Component
struct CustomDialogSample {
  // 通过gridCount配置弹窗的宽度
  dialogControllerA: CustomDialogController = new CustomDialogController({
    builder: CustomDialogA ({
      cancel: this.onCancel,
      confirm: this.onConfirm
    }),
    cancel: this.onCancel,
    autoCancel: true,
    gridCount: 4,
    customStyle: false
  })
  // 自定义弹窗样式
  dialogControllerB: CustomDialogController = new CustomDialogController({
    builder: CustomDialogB ({
      cancel: this.onCancel,
      confirm: this.onConfirm
    }),
    cancel: this.onCancel,
    autoCancel: true,
    customStyle: true
  })

  onCancel() {
    console.info('callback when dialog is canceled')
  }

  onConfirm() {
    console.info('callback when dialog is confirmed')
  }

  build() {
    Column() {
      Button('CustomDialogA').margin(12)
        .onClick(() => {
          this.dialogControllerA.open()
        })
      Button('CustomDialogB').margin(12)
        .onClick(() => {
          this.dialogControllerB.open()
        })
    }.width('100%').height('100%').justifyContent(FlexAlign.Center)
  }
}

@CustomDialog
struct CustomDialogA {
  controller?: CustomDialogController
  cancel?: () => void
  confirm?: () => void

  build() {
    Column() {
      Text('是否删除此联系人?')
        .fontSize(16)
        .fontColor('#E6000000')
        .margin({bottom: 8, top: 24, left: 24, right: 24})
      Row() {
        Text('取消')
          .fontColor('#007DFF')
          .fontSize(16)
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
          .onClick(()=>{
            if(this.controller){
                 this.controller.close()
             }
            this.cancel!()
          })
        Line().width(1).height(24).backgroundColor('#33000000').margin({left: 4, right: 4})
        Text('删除')
          .fontColor('#FA2A2D')
          .fontSize(16)
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
          .onClick(()=>{
             if(this.controller){
                 this.controller.close()
             }
            this.confirm!()
          })
      }.height(40)
      .margin({left: 24, right: 24, bottom: 16})
    }.borderRadius(24)
  }
}

@CustomDialog
struct CustomDialogB {
  controller?: CustomDialogController
  cancel?: () => void
  confirm?: () => void

  build() {
    GridRow({columns: {sm: 4, md: 8, lg: 12}}) {
      GridCol({span: 4, offset: {sm: 0, md: 2, lg: 4}}) {
        Column() {
          Text('是否删除此联系人?')
            .fontSize(16)
            .fontColor('#E6000000')
            .margin({bottom: 8, top: 24, left: 24, right: 24})
          Row() {
            Text('取消')
              .fontColor('#007DFF')
              .fontSize(16)
              .layoutWeight(1)
              .textAlign(TextAlign.Center)
              .onClick(()=>{
                if(this.controller){
                 this.controller.close()
                }
                this.cancel!()
              })
            Line().width(1).height(24).backgroundColor('#33000000').margin({left: 4, right: 4})
            Text('删除')
              .fontColor('#FA2A2D')
              .fontSize(16)
              .layoutWeight(1)
              .textAlign(TextAlign.Center)
              .onClick(()=>{
                 if(this.controller){
                 this.controller.close()
                }
                this.confirm!()
              })
          }.height(40)
          .margin({left: 24, right: 24, bottom: 16})
        }.borderRadius(24).backgroundColor('#FFFFFF')
      }
    }.margin({left: 24, right: 24})
  }
}

大图浏览

布局效果

image.png

实现方案

图片通常使用[Image组件]展示,Image组件的objectFit属性默认为ImageFit.Cover,即保持宽高比进行缩小或者放大以使得图片两边都大于或等于显示边界。在大图浏览场景下,因屏幕与图片的宽高比可能有差异,常常会发生图片被截断的问题。此时只需将Image组件的objectFit属性设置为ImageFit.Contain,即保持宽高比进行缩小或者放大并使得图片完全显示在显示边界内,即可解决该问题。

参考代码

@Entry
@Component
struct BigImage {
  build() {
    Row() {
      Image($r("app.media.image"))
        .objectFit(ImageFit.Contain)
    }
  }
}

操作入口

布局效果

image.png

实现方案

Scroll(内容超出宽度时可滚动) + Row(横向均分:justifyContent(FlexAlign.SpaceAround)、 最小宽度约束:constraintSize({ minWidth: ‘100%’ })

参考代码

interface OperationItem {
  name: string
  icon: Resource
}

@Entry
@Component
export default struct OperationEntries {
  @State listData: Array<OperationItem> = [
    { name: '私人FM', icon: $r('app.media.self_fm') },
    { name: '歌手', icon: $r('app.media.singer') },
    { name: '歌单', icon: $r('app.media.song_list') },
    { name: '排行榜', icon: $r('app.media.rank') },
    { name: '热门', icon: $r('app.media.hot') },
    { name: '运动音乐', icon: $r('app.media.sport') },
    { name: '音乐FM', icon: $r('app.media.audio_fm') },
    { name: '福利', icon: $r('app.media.bonus') }]

  build() {
    Scroll() {
      Row() {
        ForEach(this.listData, (item:OperationItem) => {
          Column() {
            Image(item.icon)
              .width(48)
              .aspectRatio(1)
            Text(item.name)
              .margin({ top: 8 })
              .fontSize(16)
          }
          .justifyContent(FlexAlign.Center)
          .height(104)
          .padding({ left: 12, right: 12 })
        })
      }
      .constraintSize({ minWidth: '100%' }).justifyContent(FlexAlign.SpaceAround)
    }
    .width('100%')
    .scrollable(ScrollDirection.Horizontal)
  }
}

顶部

布局效果

image.png

实现方案

最外层使用栅格行组件GridRow布局

文本标题使用栅格列组件GridCol

搜索框使用栅格列组件GridCol

参考代码

@Entry
@Component
export default struct Header {
  @State needWrap: boolean = true

  build() {
    GridRow() {
      GridCol({ span: { sm: 12, md: 6, lg: 7 } }) {
        Row() {
          Text('推荐').fontSize(24)
          Blank()
          Image($r('app.media.ic_public_more'))
            .width(32)
            .height(32)
            .objectFit(ImageFit.Contain)
            .visibility(this.needWrap ? Visibility.Visible : Visibility.None)
        }
        .width('100%').height(40)
        .alignItems(VerticalAlign.Center)
      }

      GridCol({ span: { sm: 12, md: 6, lg: 5 } }) {
        Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center }) {
          Search({ placeholder: '猜您喜欢: 万水千山' })
            .placeholderFont({ size: 16 })
            .margin({ top: 4, bottom: 4 })
          Image($r('app.media.audio_fm'))
            .width(32)
            .height(32)
            .objectFit(ImageFit.Contain)
            .flexShrink(0)
            .margin({ left: 12 })
          Image($r('app.media.ic_public_more'))
            .width(32)
            .height(32)
            .objectFit(ImageFit.Contain)
            .flexShrink(0)
            .margin({ left: 12 })
            .visibility(this.needWrap ? Visibility.None : Visibility.Visible)
        }
      }
    }.onBreakpointChange((breakpoint: string) => {
      if (breakpoint === 'sm') {
        this.needWrap = true
      } else {
        this.needWrap = false
      }
    })
    .padding({ left: 12, right: 12 })
  }
}

缩进布局

布局效果

image.png

实现方案

借助[栅格组件],控制待显示内容在不同的断点下占据不同的列数,即可实现不同设备上的缩进效果。另外还可以调整不同断点下栅格组件与两侧的间距,获得更好的显示效果。

参考代码

@Entry
@Component
struct IndentationSample {
  @State private gridMargin: number = 24
  build() {
    Row() {
      GridRow({columns: {sm: 4, md: 8, lg: 12}, gutter: 24}) {
        GridCol({span: {sm: 4, md: 6, lg: 8}, offset: {md: 1, lg: 2}}) {
          Column() {
            ForEach([0, 1, 2, 4], () => {
              Column() {
                ItemContent()
              }
            })
          }.width('100%')
        }
      }
      .margin({left: this.gridMargin, right: this.gridMargin})
      .onBreakpointChange((breakpoint: string) => {
        if (breakpoint === 'lg') {
          this.gridMargin = 48
        } else if (breakpoint === 'md') {
          this.gridMargin = 32
        } else {
          this.gridMargin = 24
        }
      })
    }
    .height('100%')
    .alignItems((VerticalAlign.Center))
    .backgroundColor('#F1F3f5')
  }
}

@Component
struct ItemContent {
  build() {
    Column() {
      Row() {
        Row() {
        }
        .width(28)
        .height(28)
        .borderRadius(14)
        .margin({ right: 15 })
        .backgroundColor('#E4E6E8')

        Row() {
        }
        .width('30%').height(20).borderRadius(4)
        .backgroundColor('#E4E6E8')
      }.width('100%').height(28)

      Row() {
      }
      .width('100%')
      .height(68)
      .borderRadius(16)
      .margin({ top: 12 })
      .backgroundColor('#E4E6E8')
    }
    .height(128)
    .borderRadius(24)
    .backgroundColor('#FFFFFF')
    .padding({ top: 12, bottom: 12, left: 18, right: 18 })
    .margin({ bottom: 12 })
  }
}

挪移布局

布局效果

image.png

实现方案

不同断点下,栅格子元素占据的列数会随着开发者的配置发生改变。当一行中的列数超过栅格组件在该断点的总列数时,可以自动换行,即实现”上下布局”与”左右布局”之间切换的效果。

参考代码

@Entry
@Component
struct DiversionSample {
  @State private currentBreakpoint: string = 'md'
  @State private imageHeight: number = 0
  build() {
    Row() {
      GridRow() {
        GridCol({span: {sm: 12, md: 6, lg: 6}}) {
          Image($r('app.media.illustrator'))
          .aspectRatio(1)
          .onAreaChange((oldValue: Area, newValue: Area) => {
            this.imageHeight = Number(newValue.height)
          })
          .margin({left: 12, right: 12})
        }

        GridCol({span: {sm: 12, md: 6, lg: 6}}) {
          Column(){
            Text($r('app.string.user_improvement'))
              .textAlign(TextAlign.Center)
              .fontSize(20)
              .fontWeight(FontWeight.Medium)
            Text($r('app.string.user_improvement_tips'))
              .textAlign(TextAlign.Center)
              .fontSize(14)
              .fontWeight(FontWeight.Medium)
          }
          .margin({left: 12, right: 12})
          .justifyContent(FlexAlign.Center)
          .height(this.currentBreakpoint === 'sm' ? 100 : this.imageHeight)
        }
      }.onBreakpointChange((breakpoint: string) => {
        this.currentBreakpoint = breakpoint;
      })
    }
    .height('100%')
    .alignItems((VerticalAlign.Center))
    .backgroundColor('#F1F3F5')
  }
}

重复布局

布局效果

image.png
实现方案

不同断点下,配置栅格子组件占据不同的列数,即可实现“小屏单列显示、大屏双列显示”的效果。另外,还可以通过栅格组件的onBreakpointChange事件,调整页面中显示的元素数量。

参考代码

搜狗高速浏览器截图20240326151344.png

`HarmonyOS与OpenHarmony鸿蒙文档籽料:mau123789是v直接拿`

@Entry
@Component
struct RepeatSample {
  @State private currentBreakpoint: string = 'md'
  @State private listItems: number[] = [1, 2, 3, 4, 5, 6, 7, 8]
  @State private gridMargin: number = 24

  build() {
    Row() {
      // 当目标区域不足以显示所有元素时,可以通过上下滑动查看不同的元素
      Scroll() {
        GridRow({gutter: 24}) {
          ForEach(this.listItems, () => {
           // 通过配置元素在不同断点下占的列数,实现不同的布局效果
            GridCol({span: {sm: 12, md: 6, lg: 6}}) {
              Column() {
                RepeatItemContent()
              }
            }
          })
        }
        .margin({left: this.gridMargin, right: this.gridMargin})
        .onBreakpointChange((breakpoint: string) => {
          this.currentBreakpoint = breakpoint;
          if (breakpoint === 'lg') {
            this.gridMargin = 48
          } else if (breakpoint === 'md') {
            this.gridMargin = 32
          } else {
            this.gridMargin = 24
          }
        })
      }.height(348)
    }
    .height('100%')
    .backgroundColor('#F1F3F5')
  }
}

@Component
struct RepeatItemContent {
  build() {
    Flex() {
      Row() {
      }
      .width(43)
      .height(43)
      .borderRadius(12)
      .backgroundColor('#E4E6E8')
      .flexGrow(0)

      Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Start, justifyContent: FlexAlign.SpaceAround }) {
        Row() {
        }
        .height(10)
        .width('80%')
        .backgroundColor('#E4E6E8')

        Row() {
        }
        .height(10)
        .width('50%')
        .backgroundColor('#E4E6E8')
      }
      .flexGrow(1)
      .margin({ left: 13 })
    }
    .padding({ top: 13, bottom: 13, left: 13, right: 37 })
    .height(69)
    .backgroundColor('#FFFFFF')
    .borderRadius(24)
  }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/634579.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

[机器学习聚类算法实战-1] | Scikit-Learn工具包进阶指南:机器学习聚类算法之层次聚类、特征集聚、均值移位聚类、k-均值聚类实战分析

&#x1f3a9; 欢迎来到技术探索的奇幻世界&#x1f468;‍&#x1f4bb; &#x1f4dc; 个人主页&#xff1a;一伦明悦-CSDN博客 ✍&#x1f3fb; 作者简介&#xff1a; C软件开发、Python机器学习爱好者 &#x1f5e3;️ 互动与支持&#xff1a;&#x1f4ac;评论 &…

面试八股之线程篇2.7——线程中的并发锁篇——死锁与并发程序的问题

文章目录 2.6 synchronized和Lock有什么区别 ?2.7 死锁产生的条件是什么&#xff1f;2.8 如何进行死锁诊断&#xff1f;2.10 ConcurrentHashMap&#xff08;1&#xff09; JDK1.7中concurrentHashMap&#xff08;2&#xff09; JDK1.8中concurrentHashMap 2.11 导致并发程序出…

如何将Docker容器打包并在其他服务器上运行

如何将Docker容器打包并在其他服务器上运行 我会幻想很多次我们的相遇&#xff0c;你穿着合身的T恤&#xff0c;一个素色的外套&#xff0c;搭配一条蓝色的牛仔裤&#xff0c;干净的像那天空中的云朵&#xff0c;而我&#xff0c;还是一个的傻傻的少年&#xff0c;我们相识而笑…

洗地机品牌哪个牌子好?实力派洗地机品牌TOP10榜单

洗地机依靠其洗、拖、吸、烘为一体的功能&#xff0c;能高效的完成地面清洁的工作&#xff0c;深受大家的喜爱。但是洗地机的型号越来越多&#xff0c;功能也越来越多&#xff0c;对于不想花大价钱&#xff0c;又想要高性价比的精致人群来说实在不友好&#xff0c;所以笔者今天…

QMT如何编写策略获取沪深指数数据?(附开通QMT全攻略)

获取指数代码列表 提示 为了获取指数合约列表,首先需要使用函数get_sector_list来获取需要查询的指数索引。具体的索引信息可以通过键入您感兴趣的索引名&#xff08;例如&#xff1a;"沪深指数"或"上证指数"&#xff09;等获得。接下来&#xff0c;通过…

《Effective Objective-C 2.0》读书笔记——对象、消息、运行期

目录 第二章&#xff1a;对象、消息、运行期第6条&#xff1a;理解“属性”这一概念第7条&#xff1a;在对象内部尽量直接访问实例变量第8条&#xff1a;理解“对象等同性”这一概念第9条&#xff1a;以“类族模式”隐藏实现细节第10条&#xff1a;在既有类中使用关联对象存放自…

域内 dcsync 权限维持

一、原理 DCSync 是域渗透中经常会用到的技术&#xff0c;其被整合在了 Mimikatz 中。在 DCSync 功能出现之前&#xff0c;要想获得域用户的哈希&#xff0c;需要登录域控制器&#xff0c;在域控制器上执行代码才能获得域用户的哈希。 Mimikatz的DCSync 功能&#xff1a; 该…

ubuntu20.04安装cmake3.22.1

背景 由于第一套上位机windows clion22 嵌入式ubuntu20.04的开发环境中&#xff0c;ubuntu20.04上安装的是cmake3.22.1, 为了保持一致&#xff0c;需要指定安装的cmake版本。 下载指定版本的cmake 进入cmake官网的download页面&#xff0c;https://cmake.org/download/&…

嵌入式学习——3——TCP-UDP 数据交互,握手,挥手

1、更新源 cd /etc/apt/ sudo cp sources.list sources.list.save 将原镜像备份 sudo vim sources.list 将原镜像修改成阿里源/清华源&#xff0c;如所述 阿里源 deb http://mirrors.aliyun.com/ubuntu/ bionic main …

建站平台布局结构

建站平台布局结构对于网站的成功至关重要。一个良好的布局结构能够有效地吸引用户&#xff0c;提升用户体验&#xff0c;并且有助于网站的搜索引擎优化&#xff08;SEO&#xff09;。在设计网站布局结构时&#xff0c;需要考虑到用户导航、信息层次结构、页面加载速度等方面&am…

跨境小白shopee被封号的原因?如何有效预防?

提到跨境电商平台&#xff0c;大家都知道亚马逊、Temu、TikTok shop这些是比较大的电商平台。但最近几年&#xff0c;在东南亚市场上&#xff0c;Shopee虾皮却是颇负盛名的一个跨境电商平台&#xff0c;这也让众多中国跨境小白蜂拥而至。目前shopee的商家正在不断增多&#xff…

基于Vue的验证码实现

一、验证码核心实现 创建slide-verify.vue&#xff0c;代码如下&#xff1a; <template><divclass"slide-verify":style"{ width: w px }"id"slideVerify"onselectstart"return false;"><!-- 图片加载遮蔽罩 -->&…

全面了解CC攻击和防范策略

前言 “ CC攻击的原理就是攻击者控制某些主机不停地发大量数据包给对方服务器造成服务器资源耗尽&#xff0c;一直到宕机崩溃。” 什么是CC攻击&#xff1f; CC攻击前身是一个名为Fatboy的攻击程序&#xff0c;而之所以后来人们会称之为CC&#xff0c;也叫HTTP-FLOOD&#xff…

modbus开源库libmodbus的C语言使用记录(实现简单的modbus主机/丛机程序,解决libmodbus库安装出现的问题)

libmodbus简介 libmodbus 是一个开源的、跨平台的C库,用于实现Modbus通讯协议。它支持Modbus RTU(RS-232/485)和Modbus TCP协议,可以使开发者方便地在项目中集成Modbus通讯功能。libmodbus的设计目标是简单、灵活和高效,适用于各种大小的嵌入式和桌面应用。 编译运行测试…

Orca: FSS-based Secure Training and Inference with GPUs

目录 1.Abstract2.Accelerating FSS on a GPU2.1 Accelerating FSS-based compute on GPU2.1.1 Faster AES computation (AES)2.1.2 Optimized data layout for cache locality (LAYOUT)2.1.3 Optimizing memory footprint (MEM) 2.2 Reducing time to read FSS keys2.2.1 Bypa…

计算机图形学入门games103——碰撞检测

一.怎么发现碰撞&#xff0c;有没有碰撞 碰撞最难的是布料的碰撞 碰撞检测分为两个部分&#xff1a; 第一阶段&#xff1a;去除不可能发生碰撞的元素&#xff0c;输出的就是可能碰撞的备选目标&#xff0c;这阶段叫做碰撞剔除 这常用的套路有两种&#xff1a;第一种是Spati…

算法题解记录26+++翻转二叉树(百日筑基)

题目描述&#xff1a; 题目难度&#xff1a;简单 给你一棵二叉树的根节点 root &#xff0c;翻转这棵二叉树&#xff0c;并返回其根节点。 示例 1&#xff1a; 输入&#xff1a;root [4,2,7,1,3,6,9] 输出&#xff1a;[4,7,2,9,6,3,1]示例 2&#xff1a; 输入&#xff1a;roo…

C++ TCP发送Socket数据

DEVC需要加入ws2_32库 #include <iostream> #include <winsock2.h>#pragma comment(lib, "ws2_32.lib")void sendData(const char* ip, int port, const char* data) {WSADATA wsaData;SOCKET sockfd;struct sockaddr_in server_addr;// 初始化Winsock…

HDR视频相关标准-HDR vivid(二)

上文介绍了HDRvivid的一些技术。今天从全局角度来看看HDR视频的处理流程&#xff0c;HDR视频系统&#xff0c;即建立一个比SDR视频更大的色彩/亮度坐标体系&#xff0c;并改变系统的传输函数&#xff0c;以再现更大的色域(WCG)和更高的亮度动态范围。 菁彩 HDR技术的专业术语 …

(全面)Nginx格式化插件,Nginx生产工具,Nginx常用命令

目录 &#x1f3ab; 前言 &#x1f389; 开篇福利 &#x1f381; 开篇福利 x2 Double happiness # 介绍 # 地址 # 下载 &#x1f4bb; 命令及解析 # 整个文件系统中搜索名为nginx.conf的文件 # 编辑nginx.conf文件 # 重新加载配置文件 # 快速查找nginx.conf文件并使…